<commit_before>import PropTypes from 'prop-types'
import React from 'react'
import { findDOMNode } from 'react-dom'
import cn from 'classnames'

import dates from './utils/dates'
import localizer from './localizer'
import chunk from 'lodash/chunk'

import { navigate, views } from './utils/constants'
import { notify } from './utils/helpers'
import getPosition from 'dom-helpers/query/position'
import raf from 'dom-helpers/util/requestAnimationFrame'

import Popup from './Popup'
import Overlay from 'react-overlays/lib/Overlay'
import DateContentRow from './DateContentRow'
import Header from './Header'
import DateHeader from './DateHeader'

import { accessor, dateFormat } from './utils/propTypes'
import { segStyle, inRange, sortEvents } from './utils/eventLevels'

let eventsForWeek = (evts, start, end, props) =>
  evts.filter(e => inRange(e, start, end, props))

let propTypes = {
  events: PropTypes.array.isRequired,
  date: PropTypes.instanceOf(Date),

  min: PropTypes.instanceOf(Date),
  max: PropTypes.instanceOf(Date),

  step: PropTypes.number,
  now: PropTypes.instanceOf(Date),

  scrollToTime: PropTypes.instanceOf(Date),
  eventPropGetter: PropTypes.func,
  dayPropGetter: PropTypes.func,

  culture: PropTypes.string,
  dayFormat: dateFormat,

  rtl: PropTypes.bool,
  width: PropTypes.number,

  titleAccessor: accessor.isRequired,
  allDayAccessor: accessor.isRequired,
  startAccessor: accessor.isRequired,
  endAccessor: accessor.isRequired,

  selected: PropTypes.object,
  selectable: PropTypes.oneOf([true, false, 'ignoreEvents']),
  longPressThreshold: PropTypes.number,

  onNavigate: PropTypes.func,
  onSelectSlot: PropTypes.func,
  onSelectEvent: PropTypes.func,
  onDoubleClickEvent: PropTypes.func,
  onShowMore: PropTypes.func,
  onDrillDown: PropTypes.func,
  getDrilldownView: PropTypes.func.isRequired,

  dateFormat,

  weekdayFormat: dateFormat,
  popup: PropTypes.bool,

  messages: PropTypes.object,
  components: PropTypes.object.isRequired,
  popupOffset: PropTypes.oneOfType([
    PropTypes.number,
    PropTypes.shape({
      x: PropTypes.number,
      y: PropTypes.number,
    }),
  ]),
}

class MonthView extends React.Component {
  static displayName = 'MonthView'
  static propTypes = propTypes

  static defaultProps = {
    now: new Date()
  }

  constructor(...args) {
    super(...args)

    this._bgRows = []
    this._pendingSelection = []
    this.state = {
      rowLimit: 5,
      needLimitMeasure: true,
    }
  }

  componentWillReceiveProps({ date }) {
    this.setState({
      needLimitMeasure: !dates.eq(date, this.props.date),
    })
  }

  componentDidMount() {
    let running

    if (this.state.needLimitMeasure) this.measureRowLimit(this.props)

    window.addEventListener(
      'resize',
      (this._resizeListener = () => {
        if (!running) {
          raf(() => {
            running = false
            this.setState({ needLimitMeasure: true }) //eslint-disable-line
          })
        }
      }),
      false
    )
  }

  componentDidUpdate() {
    if (this.state.needLimitMeasure) this.measureRowLimit(this.props)
  }

  componentWillUnmount() {
    window.removeEventListener('resize', this._resizeListener, false)
  }

  getContainer = () => {
    return findDOMNode(this)
  }

  render() {
    let { date, culture, weekdayFormat, className } = this.props,
      month = dates.visibleDays(date, culture),
      weeks = chunk(month, 7)

    this._weekCount = weeks.length

    return (

          {this.renderHeaders(weeks[0], weekdayFormat, culture)}

        {weeks.map((week, idx) => this.renderWeek(week, idx))}
        {this.props.popup && this.renderOverlay()}

    )
  }

  renderWeek = (week, weekIdx) => {
    let {
      events,
      components,
      selectable,
      titleAccessor,
      startAccessor,
      endAccessor,
      allDayAccessor,
      eventPropGetter,
      dayPropGetter,
      messages,
      selected,
      now,
      date,
      longPressThreshold,
    } = this.props

    const { needLimitMeasure, rowLimit } = this.state

    events = eventsForWeek(events, week[0], week[week.length - 1], this.props)
    events.sort((a, b) => sortEvents(a, b, this.props))

    return (
      <DateContentRow
        key={weekIdx}
        ref={weekIdx === 0 ? 'slotRow' : undefined}
        container={this.getContainer}
        className="rbc-month-row"
        now={now}
        date={date}
        range={week}
        events={events}
        maxRows={rowLimit}
        selected={selected}
        selectable={selectable}
        messages={messages}
        titleAccessor={titleAccessor}
        startAccessor={startAccessor}
        endAccessor={endAccessor}
        allDayAccessor={allDayAccessor}
        eventPropGetter={eventPropGetter}
        dayPropGetter={dayPropGetter}
        renderHeader={this.readerDateHeading}
        renderForMeasure={needLimitMeasure}
        onShowMore={this.handleShowMore}
        onSelect={this.handleSelectEvent}
        onDoubleClick={this.handleDoubleClickEvent}
        onSelectSlot={this.handleSelectSlot}
        eventComponent={components.event}
        eventWrapperComponent={components.eventWrapper}
        dateCellWrapper={components.dateCellWrapper}
        longPressThreshold={longPressThreshold}
      />
    )
  }

  readerDateHeading = ({ date, className, ...props }) => {
    let {
      date: currentDate,
      getDrilldownView,
      dateFormat,
      culture,
    } = this.props

    let isOffRange = dates.month(date) !== dates.month(currentDate)
    let isCurrent = dates.eq(date, currentDate, 'day')
    let drilldownView = getDrilldownView(date)
    let label = localizer.format(date, dateFormat, culture)
    let DateHeaderComponent = this.props.components.dateHeader || DateHeader

    return (

        <DateHeaderComponent
          label={label}
          date={date}
          drilldownView={drilldownView}
          isOffRange={isOffRange}
          onDrillDown={e => this.handleHeadingClick(date, drilldownView, e)} />

    )
  }

  renderHeaders(row, format, culture) {
    let first = row[0]
    let last = row[row.length - 1]
    let HeaderComponent = this.props.components.header || Header

    return dates.range(first, last, 'day').map((day, idx) => (

        <HeaderComponent
          date={day}
          label={localizer.format(day, format, culture)}
          localizer={localizer}
          format={format}
          culture={culture}
        />

    ))
  }

  renderOverlay () {
    let overlay = (this.state && this.state.overlay) || {}
    let { components } = this.props

    return (
      <Overlay
        rootClose
        placement="bottom"
        container={this}
        show={!!overlay.position}
        onHide={() => this.setState({ overlay: null })}
      >
        <Popup
          {...this.props}
          eventComponent={components.event}
          eventWrapperComponent={components.eventWrapper}
          position={overlay.position}
          events={overlay.events}
          slotStart={overlay.date}
          slotEnd={overlay.end}
          onSelect={this.handleSelectEvent}
        />
      </Overlay>
    )
  }

  measureRowLimit() {
    this.setState({
      needLimitMeasure: false,
      rowLimit: this.refs.slotRow.getRowLimit(),
    })
  }

  handleSelectSlot = (range, slotInfo) => {
    this._pendingSelection = this._pendingSelection.concat(range)

    clearTimeout(this._selectTimer)
    this._selectTimer = setTimeout(() => this.selectDates(slotInfo))
  }

  handleHeadingClick = (date, view, e) => {
    e.preventDefault()
    this.clearSelection()
    notify(this.props.onDrillDown, [date, view])
  }

  handleSelectEvent = (...args) => {
    this.clearSelection()
    notify(this.props.onSelectEvent, args)
  }

  handleDoubleClickEvent = (...args) => {
    this.clearSelection()
    notify(this.props.onDoubleClickEvent, args)
  }

  handleShowMore = (events, date, cell, slot) => {
    const { popup, onDrillDown, onShowMore, getDrilldownView } = this.props
    //cancel any pending selections so only the event click goes through.
    this.clearSelection()

    if (popup) {
      let position = getPosition(cell, findDOMNode(this))

      this.setState({
        overlay: { date, events, position },
      })
    } else {
      notify(onDrillDown, [date, getDrilldownView(date) || views.DAY])
    }

    notify(onShowMore, [events, date, slot])
  }

  selectDates(slotInfo) {
    let slots = this._pendingSelection.slice()

    this._pendingSelection = []

    slots.sort((a, b) => +a - +b)

    notify(this.props.onSelectSlot, {
      slots,
      start: slots[0],
      end: slots[slots.length - 1],
      action: slotInfo.action,
    })
  }

  clearSelection() {
    clearTimeout(this._selectTimer)
    this._pendingSelection = []
  }
}

MonthView.navigate = (date, action) => {
  switch (action) {
    case navigate.PREVIOUS:
      return dates.add(date, -1, 'month')

    case navigate.NEXT:
      return dates.add(date, 1, 'month')

    default:
      return date
  }
}

MonthView.title = (date, { formats, culture }) =>
  localizer.format(date, formats.monthHeaderFormat, culture);

export default MonthView
<commit_msg>Avoid unecessary anonymous function (#663)<commit_after>import PropTypes from 'prop-types'
import React from 'react'
import { findDOMNode } from 'react-dom'
import cn from 'classnames'

import dates from './utils/dates'
import localizer from './localizer'
import chunk from 'lodash/chunk'

import { navigate, views } from './utils/constants'
import { notify } from './utils/helpers'
import getPosition from 'dom-helpers/query/position'
import raf from 'dom-helpers/util/requestAnimationFrame'

import Popup from './Popup'
import Overlay from 'react-overlays/lib/Overlay'
import DateContentRow from './DateContentRow'
import Header from './Header'
import DateHeader from './DateHeader'

import { accessor, dateFormat } from './utils/propTypes'
import { segStyle, inRange, sortEvents } from './utils/eventLevels'

let eventsForWeek = (evts, start, end, props) =>
  evts.filter(e => inRange(e, start, end, props))

let propTypes = {
  events: PropTypes.array.isRequired,
  date: PropTypes.instanceOf(Date),

  min: PropTypes.instanceOf(Date),
  max: PropTypes.instanceOf(Date),

  step: PropTypes.number,
  now: PropTypes.instanceOf(Date),

  scrollToTime: PropTypes.instanceOf(Date),
  eventPropGetter: PropTypes.func,
  dayPropGetter: PropTypes.func,

  culture: PropTypes.string,
  dayFormat: dateFormat,

  rtl: PropTypes.bool,
  width: PropTypes.number,

  titleAccessor: accessor.isRequired,
  allDayAccessor: accessor.isRequired,
  startAccessor: accessor.isRequired,
  endAccessor: accessor.isRequired,

  selected: PropTypes.object,
  selectable: PropTypes.oneOf([true, false, 'ignoreEvents']),
  longPressThreshold: PropTypes.number,

  onNavigate: PropTypes.func,
  onSelectSlot: PropTypes.func,
  onSelectEvent: PropTypes.func,
  onDoubleClickEvent: PropTypes.func,
  onShowMore: PropTypes.func,
  onDrillDown: PropTypes.func,
  getDrilldownView: PropTypes.func.isRequired,

  dateFormat,

  weekdayFormat: dateFormat,
  popup: PropTypes.bool,

  messages: PropTypes.object,
  components: PropTypes.object.isRequired,
  popupOffset: PropTypes.oneOfType([
    PropTypes.number,
    PropTypes.shape({
      x: PropTypes.number,
      y: PropTypes.number,
    }),
  ]),
}

class MonthView extends React.Component {
  static displayName = 'MonthView'
  static propTypes = propTypes

  static defaultProps = {
    now: new Date()
  }

  constructor(...args) {
    super(...args)

    this._bgRows = []
    this._pendingSelection = []
    this.state = {
      rowLimit: 5,
      needLimitMeasure: true,
    }
  }

  componentWillReceiveProps({ date }) {
    this.setState({
      needLimitMeasure: !dates.eq(date, this.props.date),
    })
  }

  componentDidMount() {
    let running

    if (this.state.needLimitMeasure) this.measureRowLimit(this.props)

    window.addEventListener(
      'resize',
      (this._resizeListener = () => {
        if (!running) {
          raf(() => {
            running = false
            this.setState({ needLimitMeasure: true }) //eslint-disable-line
          })
        }
      }),
      false
    )
  }

  componentDidUpdate() {
    if (this.state.needLimitMeasure) this.measureRowLimit(this.props)
  }

  componentWillUnmount() {
    window.removeEventListener('resize', this._resizeListener, false)
  }

  getContainer = () => {
    return findDOMNode(this)
  }

  render() {
    let { date, culture, weekdayFormat, className } = this.props,
      month = dates.visibleDays(date, culture),
      weeks = chunk(month, 7)

    this._weekCount = weeks.length

    return (

          {this.renderHeaders(weeks[0], weekdayFormat, culture)}

        {weeks.map(this.renderWeek)}
        {this.props.popup && this.renderOverlay()}

    )
  }

  renderWeek = (week, weekIdx) => {
    let {
      events,
      components,
      selectable,
      titleAccessor,
      startAccessor,
      endAccessor,
      allDayAccessor,
      eventPropGetter,
      dayPropGetter,
      messages,
      selected,
      now,
      date,
      longPressThreshold,
    } = this.props

    const { needLimitMeasure, rowLimit } = this.state

    events = eventsForWeek(events, week[0], week[week.length - 1], this.props)
    events.sort((a, b) => sortEvents(a, b, this.props))

    return (
      <DateContentRow
        key={weekIdx}
        ref={weekIdx === 0 ? 'slotRow' : undefined}
        container={this.getContainer}
        className="rbc-month-row"
        now={now}
        date={date}
        range={week}
        events={events}
        maxRows={rowLimit}
        selected={selected}
        selectable={selectable}
        messages={messages}
        titleAccessor={titleAccessor}
        startAccessor={startAccessor}
        endAccessor={endAccessor}
        allDayAccessor={allDayAccessor}
        eventPropGetter={eventPropGetter}
        dayPropGetter={dayPropGetter}
        renderHeader={this.readerDateHeading}
        renderForMeasure={needLimitMeasure}
        onShowMore={this.handleShowMore}
        onSelect={this.handleSelectEvent}
        onDoubleClick={this.handleDoubleClickEvent}
        onSelectSlot={this.handleSelectSlot}
        eventComponent={components.event}
        eventWrapperComponent={components.eventWrapper}
        dateCellWrapper={components.dateCellWrapper}
        longPressThreshold={longPressThreshold}
      />
    )
  }

  readerDateHeading = ({ date, className, ...props }) => {
    let {
      date: currentDate,
      getDrilldownView,
      dateFormat,
      culture,
    } = this.props

    let isOffRange = dates.month(date) !== dates.month(currentDate)
    let isCurrent = dates.eq(date, currentDate, 'day')
    let drilldownView = getDrilldownView(date)
    let label = localizer.format(date, dateFormat, culture)
    let DateHeaderComponent = this.props.components.dateHeader || DateHeader

    return (

        <DateHeaderComponent
          label={label}
          date={date}
          drilldownView={drilldownView}
          isOffRange={isOffRange}
          onDrillDown={e => this.handleHeadingClick(date, drilldownView, e)} />

    )
  }

  renderHeaders(row, format, culture) {
    let first = row[0]
    let last = row[row.length - 1]
    let HeaderComponent = this.props.components.header || Header

    return dates.range(first, last, 'day').map((day, idx) => (

        <HeaderComponent
          date={day}
          label={localizer.format(day, format, culture)}
          localizer={localizer}
          format={format}
          culture={culture}
        />

    ))
  }

  renderOverlay () {
    let overlay = (this.state && this.state.overlay) || {}
    let { components } = this.props

    return (
      <Overlay
        rootClose
        placement="bottom"
        container={this}
        show={!!overlay.position}
        onHide={() => this.setState({ overlay: null })}
      >
        <Popup
          {...this.props}
          eventComponent={components.event}
          eventWrapperComponent={components.eventWrapper}
          position={overlay.position}
          events={overlay.events}
          slotStart={overlay.date}
          slotEnd={overlay.end}
          onSelect={this.handleSelectEvent}
        />
      </Overlay>
    )
  }

  measureRowLimit() {
    this.setState({
      needLimitMeasure: false,
      rowLimit: this.refs.slotRow.getRowLimit(),
    })
  }

  handleSelectSlot = (range, slotInfo) => {
    this._pendingSelection = this._pendingSelection.concat(range)

    clearTimeout(this._selectTimer)
    this._selectTimer = setTimeout(() => this.selectDates(slotInfo))
  }

  handleHeadingClick = (date, view, e) => {
    e.preventDefault()
    this.clearSelection()
    notify(this.props.onDrillDown, [date, view])
  }

  handleSelectEvent = (...args) => {
    this.clearSelection()
    notify(this.props.onSelectEvent, args)
  }

  handleDoubleClickEvent = (...args) => {
    this.clearSelection()
    notify(this.props.onDoubleClickEvent, args)
  }

  handleShowMore = (events, date, cell, slot) => {
    const { popup, onDrillDown, onShowMore, getDrilldownView } = this.props
    //cancel any pending selections so only the event click goes through.
    this.clearSelection()

    if (popup) {
      let position = getPosition(cell, findDOMNode(this))

      this.setState({
        overlay: { date, events, position },
      })
    } else {
      notify(onDrillDown, [date, getDrilldownView(date) || views.DAY])
    }

    notify(onShowMore, [events, date, slot])
  }

  selectDates(slotInfo) {
    let slots = this._pendingSelection.slice()

    this._pendingSelection = []

    slots.sort((a, b) => +a - +b)

    notify(this.props.onSelectSlot, {
      slots,
      start: slots[0],
      end: slots[slots.length - 1],
      action: slotInfo.action,
    })
  }

  clearSelection() {
    clearTimeout(this._selectTimer)
    this._pendingSelection = []
  }
}

MonthView.navigate = (date, action) => {
  switch (action) {
    case navigate.PREVIOUS:
      return dates.add(date, -1, 'month')

    case navigate.NEXT:
      return dates.add(date, 1, 'month')

    default:
      return date
  }
}

MonthView.title = (date, { formats, culture }) =>
  localizer.format(date, formats.monthHeaderFormat, culture);

export default MonthView
<|endoftext|>


<commit_before>
function optimization_avg(list)
{
    var average = 0;
    var count = 0;
    var display = "-";

    for(var i in list)
    {
        average += parseFloat(list[i].price_optimization);
        count++;
    }

    if(parseFloat(average) !== 0)
    {
        display = parseInt(average / count);
    }

    return display;
}

function items_count(list)
{
    var average = 0;
    var display = "-";
    var count = 0;

    for(var i in list)
    {
        var items = JSON.parse(list[i].items);
        average += items.length;
        count++;
    }

    if(parseFloat(average) !== 0)
    {
        display = parseInt(average / count);
    }

    return display;
}

angular.module('eappApp').controller('AccountOptimizationController', function ($scope, $rootScope, eapp, $mdDialog, appService)
{
    $rootScope.isAccountMenu = true;

    $scope.viewOptimization = function(index, ev)
    {
        var data = [];

        var settings = {};

        switch(parseInt(index))
        {
            case 0:
                // Set week data
                settings.header = "Économies cette semaine";

                for(var x in $scope.userOptimization.checkDay)
                {
                    data.push({ Total : $scope.userOptimization.checkDay[x].total, Day : $scope.userOptimization.checkDay[x].date});
                }

                break;
            case 1:
                // Set month data
                settings.header = "Économies ce mois";

                for(var x in $scope.userOptimization.checkWeek)
                {
                    data.push({ Total : $scope.userOptimization.checkWeek[x].total, Week : $scope.userOptimization.checkWeek[x].date});
                }

                break;
            case 2:
                // Set Year data
                settings.header = "Économies cette année";

                for(var x in $scope.userOptimization.checkMonth)
                {
                    data.push({ Total : $scope.userOptimization.checkMonth[x].total, Month : $scope.userOptimization.checkMonth[x].date});
                }
                break;
            default:
                settings.header = "Économies général";

                for(var x in $scope.userOptimization.overall)
                {
                    data.push({ Total : $scope.userOptimization.overall[x].price_optimization, Day : $scope.userOptimization.overall[x].date_created });
                }
                break;
        }

        $scope.showOptimization(ev, data, settings);

    };

    $scope.showOptimization = function(ev, data, settings)
    {
        // Initialize data here
         switch(settings.header)
        {
            case "Économies cette semaine":
				var Week = [];
				var Total = [];

				for(var i in data) {
					Week.push("Le " + data[i].Day);
					Total.push(data[i].Total);
				}

				var chartdata = {
					labels: Week,
					datasets : [
						{
							label: 'Économies cette semaine en dollars $',
							backgroundColor: 'rgba(54, 162, 235, 0.5)',
							borderColor: 'rgba(54, 162, 235, 0.5)',
							hoverBackgroundColor: 'rgba(54, 162, 235, 0.75)',
							hoverBorderColor: 'rgba(54, 162, 235, 0.75)',

							data: Total
						}
					]
					};
				break;

			case "Économies ce mois":
				var Month = [];
				var Total = [];

				for(var i in data) {
					Month.push("Semaine du " + data[i].Week);
					Total.push(data[i].Total);
				}

				var chartdata = {
					labels: Month,
					datasets : [
						{
							label: 'Économies ce mois en dollars $',
							backgroundColor: 'rgba(255, 206, 86, 0.5)',
							borderColor: 'rgba(255, 206, 86, 0.5)',
							hoverBackgroundColor: 'rgba(255, 206, 86, 0.75)',
							hoverBorderColor: 'rgba(255, 206, 86, 0.75)',
							data: Total
						}
					]
					};
				break;

			case "Économies cette année":
				var Year = [];
				var Total = [];

				for(var i in data) {
					Year.push("Mois: " + data[i].Month);
					Total.push(data[i].Total);
				}

				var chartdata = {
					labels: Year,
					datasets : [
						{
							label: 'Économies cette année en dollars $',
							backgroundColor: 'rgba(200, 200, 200, 0.75)',
							borderColor: 'rgba(200, 200, 200, 0.75)',
							hoverBackgroundColor: 'rgba(200, 200, 200, 1)',
							hoverBorderColor: 'rgba(200, 200, 200, 1)',
							data: Total
						}
					]
					};
				break;
			default:
					//do nothing
				break;

		}

        $scope.header = settings.header;
        $scope.data = data;
        $scope.scrollTop = $(document).scrollTop();
        // Show dialog for user to change the store of the product.
        $mdDialog.show({
            controller: showOptimizationController,
            templateUrl:  appService.baseUrl + 'assets/templates/show-optimization.html',
            parent: angular.element(document.body),
            targetEvent: ev,
            clickOutsideToClose:true,
            disableParentScroll : true,
            preserveScope:true,
            scope : $scope,
            fullscreen: false,
			onComplete : function()
			{
				// Quand le pop up s'affiche
				var ctx = $("#mycanvas");

				var barGraph = new Chart(ctx, {
				type: 'bar',
				data: chartdata,

				options: {
                        title: {
                          display: false
                        },
                        scales: {
                          yAxes: [{
                          	ticks: {
                            	beginAtZero: true
                            }
                          }]
                        }
                      }

			});
			},
            onRemoving : function()
            {
                // Restore scroll
                $(document).scrollTop($scope.scrollTop);
            }
          })
          .then(function(answer)
            {

          }, function() {

          });
    };

    function showOptimizationController($scope, $mdDialog)
    {
        $scope.hide = function()
        {
            $mdDialog.hide();
        };

        $scope.cancel = function()
        {
            $mdDialog.cancel();
        };

    };

    $scope.Init = function()
    {

        var getUserOptimizationPromise = eapp.getUserOptimizations();

        getUserOptimizationPromise.then(function(response)
        {
            $scope.userOptimization = response.data;

            if(appService.isUserLogged)
            {
                var data =
                {
                    label : "Économies cette semaine",
                    value : optimization_avg($scope.userOptimization.currentWeek),
                    count : items_count($scope.userOptimization.currentWeek)
                };

                $scope.optimizations.push(data);

                var data =
                {
                    label : "Économies ce mois",
                    value : optimization_avg($scope.userOptimization.currentMonth),
                    count : items_count($scope.userOptimization.currentMonth)
                };

                $scope.optimizations.push(data);

                var data =
                {
                    label : "Économies cette année",
                    value : optimization_avg($scope.userOptimization.currentYear),
                    count : items_count($scope.userOptimization.currentYear)
                };

                $scope.optimizations.push(data);

                var data =
                {
                    label : "Économies général",
                    value : optimization_avg($scope.userOptimization.overall),
                    count : items_count($scope.userOptimization.overall)
                };

                //$scope.optimizations.push(data);

            }

        });

        $scope.optimizations = [];

    };

    appService.ready.then(function()
    {
        $scope.Init();
    });

});
<commit_msg>disable weekly optimizations<commit_after>
function optimization_avg(list)
{
    var average = 0;
    var count = 0;
    var display = "-";

    for(var i in list)
    {
        average += parseFloat(list[i].price_optimization);
        count++;
    }

    if(parseFloat(average) !== 0)
    {
        display = parseInt(average / count);
    }

    return display;
}

function items_count(list)
{
    var average = 0;
    var display = "-";
    var count = 0;

    for(var i in list)
    {
        var items = JSON.parse(list[i].items);
        average += items.length;
        count++;
    }

    if(parseFloat(average) !== 0)
    {
        display = parseInt(average / count);
    }

    return display;
}

angular.module('eappApp').controller('AccountOptimizationController', function ($scope, $rootScope, eapp, $mdDialog, appService)
{
    $rootScope.isAccountMenu = true;

    $scope.viewOptimization = function(index, ev)
    {
        var data = [];

        var settings = {};

        switch(parseInt(index))
        {
            case 0:
                // Set week data
                settings.header = "Économies cette semaine";

                for(var x in $scope.userOptimization.checkDay)
                {
                    data.push({ Total : $scope.userOptimization.checkDay[x].total, Day : $scope.userOptimization.checkDay[x].date});
                }

                break;
            case 1:
                // Set month data
                settings.header = "Économies ce mois";

                for(var x in $scope.userOptimization.checkWeek)
                {
                    data.push({ Total : $scope.userOptimization.checkWeek[x].total, Week : $scope.userOptimization.checkWeek[x].date});
                }

                break;
            case 2:
                // Set Year data
                settings.header = "Économies cette année";

                for(var x in $scope.userOptimization.checkMonth)
                {
                    data.push({ Total : $scope.userOptimization.checkMonth[x].total, Month : $scope.userOptimization.checkMonth[x].date});
                }
                break;
            default:
                settings.header = "Économies général";

                for(var x in $scope.userOptimization.overall)
                {
                    data.push({ Total : $scope.userOptimization.overall[x].price_optimization, Day : $scope.userOptimization.overall[x].date_created });
                }
                break;
        }

        $scope.showOptimization(ev, data, settings);

    };

    $scope.showOptimization = function(ev, data, settings)
    {
        // Initialize data here
         switch(settings.header)
        {
            case "Économies cette semaine":
				var Week = [];
				var Total = [];

				for(var i in data) {
					Week.push("Le " + data[i].Day);
					Total.push(data[i].Total);
				}

				var chartdata = {
					labels: Week,
					datasets : [
						{
							label: 'Économies cette semaine en dollars $',
							backgroundColor: 'rgba(54, 162, 235, 0.5)',
							borderColor: 'rgba(54, 162, 235, 0.5)',
							hoverBackgroundColor: 'rgba(54, 162, 235, 0.75)',
							hoverBorderColor: 'rgba(54, 162, 235, 0.75)',

							data: Total
						}
					]
					};
				break;

			case "Économies ce mois":
				var Month = [];
				var Total = [];

				for(var i in data) {
					Month.push("Semaine du " + data[i].Week);
					Total.push(data[i].Total);
				}

				var chartdata = {
					labels: Month,
					datasets : [
						{
							label: 'Économies ce mois en dollars $',
							backgroundColor: 'rgba(255, 206, 86, 0.5)',
							borderColor: 'rgba(255, 206, 86, 0.5)',
							hoverBackgroundColor: 'rgba(255, 206, 86, 0.75)',
							hoverBorderColor: 'rgba(255, 206, 86, 0.75)',
							data: Total
						}
					]
					};
				break;

			case "Économies cette année":
				var Year = [];
				var Total = [];

				for(var i in data) {
					Year.push("Mois: " + data[i].Month);
					Total.push(data[i].Total);
				}

				var chartdata = {
					labels: Year,
					datasets : [
						{
							label: 'Économies cette année en dollars $',
							backgroundColor: 'rgba(200, 200, 200, 0.75)',
							borderColor: 'rgba(200, 200, 200, 0.75)',
							hoverBackgroundColor: 'rgba(200, 200, 200, 1)',
							hoverBorderColor: 'rgba(200, 200, 200, 1)',
							data: Total
						}
					]
					};
				break;
			default:
					//do nothing
				break;

		}

        $scope.header = settings.header;
        $scope.data = data;
        $scope.scrollTop = $(document).scrollTop();
        // Show dialog for user to change the store of the product.
        $mdDialog.show({
            controller: showOptimizationController,
            templateUrl:  appService.baseUrl + 'assets/templates/show-optimization.html',
            parent: angular.element(document.body),
            targetEvent: ev,
            clickOutsideToClose:true,
            disableParentScroll : true,
            preserveScope:true,
            scope : $scope,
            fullscreen: false,
			onComplete : function()
			{
				// Quand le pop up s'affiche
				var ctx = $("#mycanvas");

				var barGraph = new Chart(ctx, {
				type: 'bar',
				data: chartdata,

				options: {
                        title: {
                          display: false
                        },
                        scales: {
                          yAxes: [{
                          	ticks: {
                            	beginAtZero: true
                            }
                          }]
                        }
                      }

			});
			},
            onRemoving : function()
            {
                // Restore scroll
                $(document).scrollTop($scope.scrollTop);
            }
          })
          .then(function(answer)
            {

          }, function() {

          });
    };

    function showOptimizationController($scope, $mdDialog)
    {
        $scope.hide = function()
        {
            $mdDialog.hide();
        };

        $scope.cancel = function()
        {
            $mdDialog.cancel();
        };

    };

    $scope.Init = function()
    {

        var getUserOptimizationPromise = eapp.getUserOptimizations();

        getUserOptimizationPromise.then(function(response)
        {
            $scope.userOptimization = response.data;

            if(appService.isUserLogged)
            {
                var data =
                {
                    label : "Économies cette semaine",
                    value : optimization_avg($scope.userOptimization.currentWeek),
                    count : items_count($scope.userOptimization.currentWeek)
                };

                //$scope.optimizations.push(data);

                var data =
                {
                    label : "Économies ce mois",
                    value : optimization_avg($scope.userOptimization.currentMonth),
                    count : items_count($scope.userOptimization.currentMonth)
                };

                $scope.optimizations.push(data);

                var data =
                {
                    label : "Économies cette année",
                    value : optimization_avg($scope.userOptimization.currentYear),
                    count : items_count($scope.userOptimization.currentYear)
                };

                $scope.optimizations.push(data);

                var data =
                {
                    label : "Économies général",
                    value : optimization_avg($scope.userOptimization.overall),
                    count : items_count($scope.userOptimization.overall)
                };

                //$scope.optimizations.push(data);

            }

        });

        $scope.optimizations = [];

    };

    appService.ready.then(function()
    {
        $scope.Init();
    });

});
<|endoftext|>


<commit_before>import createWindow from '../../lib/api/window'

describe(`createWindow()`, () => {
  let sandbox
  beforeEach(() => {
    sandbox = sinon.sandbox.create()
  })
  afterEach(() => {
    sandbox.restore()
  })

  describe(`returned "window" object`, () => {
    let window
    let channelSendSpy
    beforeEach(() => {
      channelSendSpy = sandbox.spy()
      window = createWindow({
        send: channelSendSpy
      })
    })

    it(`has all expected member functions`, () => {
      expect(window.startAutoResizer).to.be.a('function')
      expect(window.stopAutoResizer).to.be.a('function')
      expect(window.updateHeight).to.be.a('function')
    })

    describe(`.startAutoResizer()`, () => {
      let updateHeightSpy
      beforeEach(() => {
        updateHeightSpy = sinon.stub(window, 'updateHeight')
        window.startAutoResizer()
      })

      it(`calls .updateHeight() initially`, () => {
        expect(updateHeightSpy.callCount).to.equal(1)
      })
      it(`calls .updateHeight() without arguments (auto mode)`, () => {
        expect(updateHeightSpy).to.have.been.calledWithExactly()
      })

      describe(`after auto resizer got started`, () => {
        it(`listens to DOM changes and invokes .updateHeigt()`, (done) => {
          updateHeightSpy.restore()
          updateHeightSpy = sandbox.stub(window, 'updateHeight', () => {
            expect(updateHeightSpy).to.have.callCount(1)
            done()
          })
          modifyDOM()
        })
        it(`listens to global "resize" event and invokes .updateHeight()`, () => {
          fireViewportResize()
          expect(updateHeightSpy).to.have.callCount(2)
        })
      })

      describe(`followed by .stopAutoResizer()`, () => {
        beforeEach(() => {
          window.stopAutoResizer()
          updateHeightSpy.reset()
        })

        it(`stops observing DOM and does not invoke updateHeight()`, (done) => {
          setTimeout(() => {
            expect(updateHeightSpy).to.have.callCount(0)
            done()
          }, 0)
          modifyDOM()
        })
        it(`stops listening to "resize" event does not invoke .updateHeight()`, () => {
          fireViewportResize()
          expect(updateHeightSpy).to.have.callCount(0)
        })
      })
    })

    describe(`.stopAutoResizer()`, () => {
      it(`returns nothing, does not fail`, () => {
        expect(window.stopAutoResizer()).to.equal(undefined)
      })
    })

    describe(`.updateHeight()`, () => {
      it(`notifies the parent window`, () => {
        window.updateHeight()
        expect(channelSendSpy).to.have.callCount(1)
      })
    })

    describe(`.updateHeight(number)`, () => {
      beforeEach(() => {
        window.updateHeight(42)
      })

      it(`notifies the parent window`, () => {
        expect(channelSendSpy).to.have.callCount(1)
      })

      describe(`called a second time with the same number`, () => {
        it(`does not notify the parent window a second time`, () => {
          window.updateHeight(42)
          expect(channelSendSpy).to.have.callCount(1)
        })
      })
    })
  })
})

function modifyDOM () {
  const elem = window.document.createElement('p')
  window.document.body.appendChild(elem)
}

function fireViewportResize () {
  window.dispatchEvent(new Event('resize'))
}
<commit_msg>Fix sinon warning<commit_after>import createWindow from '../../lib/api/window'

describe(`createWindow()`, () => {
  let sandbox
  beforeEach(() => {
    sandbox = sinon.sandbox.create()
  })
  afterEach(() => {
    sandbox.restore()
  })

  describe(`returned "window" object`, () => {
    let window
    let channelSendSpy
    beforeEach(() => {
      channelSendSpy = sandbox.spy()
      window = createWindow({
        send: channelSendSpy
      })
    })

    it(`has all expected member functions`, () => {
      expect(window.startAutoResizer).to.be.a('function')
      expect(window.stopAutoResizer).to.be.a('function')
      expect(window.updateHeight).to.be.a('function')
    })

    describe(`.startAutoResizer()`, () => {
      let updateHeightSpy
      beforeEach(() => {
        updateHeightSpy = sinon.stub(window, 'updateHeight')
        window.startAutoResizer()
      })

      it(`calls .updateHeight() initially`, () => {
        expect(updateHeightSpy.callCount).to.equal(1)
      })
      it(`calls .updateHeight() without arguments (auto mode)`, () => {
        expect(updateHeightSpy).to.have.been.calledWithExactly()
      })

      describe(`after auto resizer got started`, () => {
        it(`listens to DOM changes and invokes .updateHeigt()`, (done) => {
          updateHeightSpy.restore()
          updateHeightSpy = sandbox.stub(window, 'updateHeight').callsFake(() => {
            expect(updateHeightSpy).to.have.callCount(1)
            done()
          })
          modifyDOM()
        })
        it(`listens to global "resize" event and invokes .updateHeight()`, () => {
          fireViewportResize()
          expect(updateHeightSpy).to.have.callCount(2)
        })
      })

      describe(`followed by .stopAutoResizer()`, () => {
        beforeEach(() => {
          window.stopAutoResizer()
          updateHeightSpy.reset()
        })

        it(`stops observing DOM and does not invoke updateHeight()`, (done) => {
          setTimeout(() => {
            expect(updateHeightSpy).to.have.callCount(0)
            done()
          }, 0)
          modifyDOM()
        })
        it(`stops listening to "resize" event does not invoke .updateHeight()`, () => {
          fireViewportResize()
          expect(updateHeightSpy).to.have.callCount(0)
        })
      })
    })

    describe(`.stopAutoResizer()`, () => {
      it(`returns nothing, does not fail`, () => {
        expect(window.stopAutoResizer()).to.equal(undefined)
      })
    })

    describe(`.updateHeight()`, () => {
      it(`notifies the parent window`, () => {
        window.updateHeight()
        expect(channelSendSpy).to.have.callCount(1)
      })
    })

    describe(`.updateHeight(number)`, () => {
      beforeEach(() => {
        window.updateHeight(42)
      })

      it(`notifies the parent window`, () => {
        expect(channelSendSpy).to.have.callCount(1)
      })

      describe(`called a second time with the same number`, () => {
        it(`does not notify the parent window a second time`, () => {
          window.updateHeight(42)
          expect(channelSendSpy).to.have.callCount(1)
        })
      })
    })
  })
})

function modifyDOM () {
  const elem = window.document.createElement('p')
  window.document.body.appendChild(elem)
}

function fireViewportResize () {
  window.dispatchEvent(new Event('resize'))
}
<|endoftext|>


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

/**
 * WordPress dependencies
 */
import { __ } from '@wordpress/i18n';

/**
 * Internal dependencies
 */
import {
  CoverrAttribution,
  TenorAttribution,
  UnsplashAttribution,
} from './attribution';

const ContentType = {
  IMAGE: 'image',
  VIDEO: 'video',
  GIF: 'gif',
};

const ProviderType = {
  UNSPLASH: 'unsplash',
  COVERR: 'coverr',
  TENOR: 'tenor',
};

/** @typedef {import('react').React.Component} ReactComponent */

/**
 * @typedef ProviderConfiguration
 * @property {string} displayName The display name of the provider.
 * @property {?string} featureName An optional feature that must be turned on
 * for the provider tab to be displayed.
 * @property {?ContentType} contentTypeFilter Optional. The content type to filter by.
 * @property {boolean} supportsCategories Whether this provider supports
 * filtering media by category.
 * @property {boolean} requiresAuthorAttribution Whether this provider
 * requires showing author
 * attribution on each media element.
 * @property {function(): ReactComponent} attributionComponent A function that
 * constructs an attribution React Component for this provider.
 * @property {string} fetchMediaErrorMessage An error message to show if
 * fetching media from this provider fails.
 * @property {?string} fetchCategoriesErrorMessage An error message to show if
 * fetching categories from this provider fails. Only needs to be specified if
 * the `supportsCategories` is true.
 */

/**
 *
 * @type {Object.<string, ProviderConfiguration>}
 */
export const PROVIDERS = {
  [ProviderType.UNSPLASH]: {
    displayName: __('Images', 'web-stories'),
    supportsCategories: true,
    requiresAuthorAttribution: true,
    attributionComponent: UnsplashAttribution,
    fetchMediaErrorMessage: __(
      'Error loading media from Unsplash',
      'web-stories'
    ),
    fetchCategoriesErrorMessage: __(
      'Error loading categories from Unsplash',
      'web-stories'
    ),
  },
  [ProviderType.COVERR]: {
    displayName: __('Video', 'web-stories'),
    supportsCategories: false,
    requiresAuthorAttribution: false,
    attributionComponent: CoverrAttribution,
    fetchMediaErrorMessage: __(
      'Error loading media from Coverr',
      'web-stories'
    ),
    defaultPreviewWidth: 640,
  },
  [ProviderType.TENOR]: {
    displayName: __('GIFs', 'web-stories'),
    featureName: 'showGifTab',
    contentTypeFilter: ContentType.GIF,
    supportsCategories: true,
    requiresAuthorAttribution: false,
    attributionComponent: TenorAttribution,
    fetchMediaErrorMessage: __('Error loading media from Tenor', 'web-stories'),
  },
};
<commit_msg>undo another change<commit_after>/*
 * Copyright 2020 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * WordPress dependencies
 */
import { __ } from '@wordpress/i18n';

/**
 * Internal dependencies
 */
import {
  CoverrAttribution,
  TenorAttribution,
  UnsplashAttribution,
} from './attribution';

const ContentType = {
  IMAGE: 'image',
  VIDEO: 'video',
  GIF: 'gif',
};

const ProviderType = {
  UNSPLASH: 'unsplash',
  COVERR: 'coverr',
  TENOR: 'tenor',
};

/** @typedef {import('react').React.Component} ReactComponent */

/**
 * @typedef ProviderConfiguration
 * @property {string} displayName The display name of the provider.
 * @property {?string} featureName An optional feature that must be turned on
 * for the provider tab to be displayed.
 * @property {?ContentType} contentTypeFilter Optional. The content type to filter by.
 * @property {boolean} supportsCategories Whether this provider supports
 * filtering media by category.
 * @property {boolean} requiresAuthorAttribution Whether this provider
 * requires showing author
 * attribution on each media element.
 * @property {function(): ReactComponent} attributionComponent A function that
 * constructs an attribution React Component for this provider.
 * @property {string} fetchMediaErrorMessage An error message to show if
 * fetching media from this provider fails.
 * @property {?string} fetchCategoriesErrorMessage An error message to show if
 * fetching categories from this provider fails. Only needs to be specified if
 * the `supportsCategories` is true.
 */

/**
 *
 * @type {Object.<string, ProviderConfiguration>}
 */
export const PROVIDERS = {
  [ProviderType.UNSPLASH]: {
    displayName: __('Images', 'web-stories'),
    supportsCategories: true,
    requiresAuthorAttribution: true,
    attributionComponent: UnsplashAttribution,
    fetchMediaErrorMessage: __(
      'Error loading media from Unsplash',
      'web-stories'
    ),
    fetchCategoriesErrorMessage: __(
      'Error loading categories from Unsplash',
      'web-stories'
    ),
  },
  [ProviderType.COVERR]: {
    displayName: __('Video', 'web-stories'),
    featureName: 'showCoverrTab',
    supportsCategories: false,
    requiresAuthorAttribution: false,
    attributionComponent: CoverrAttribution,
    fetchMediaErrorMessage: __(
      'Error loading media from Coverr',
      'web-stories'
    ),
    defaultPreviewWidth: 640,
  },
  [ProviderType.TENOR]: {
    displayName: __('GIFs', 'web-stories'),
    featureName: 'showGifTab',
    contentTypeFilter: ContentType.GIF,
    supportsCategories: true,
    requiresAuthorAttribution: false,
    attributionComponent: TenorAttribution,
    fetchMediaErrorMessage: __('Error loading media from Tenor', 'web-stories'),
  },
};
<|endoftext|>


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

/**
 * Internal dependencies
 */
import { Fixture } from '../../../karma';
import { useStory } from '../../../app/story';

describe('CUJ: Creator can Transform an Element: Selection integration', () => {
  let fixture;
  let fullbleed;

  beforeEach(async () => {
    fixture = new Fixture();
    await fixture.render();

    fullbleed = fixture.container.querySelector('[data-testid="fullbleed"]');
  });

  afterEach(() => {
    fixture.restore();
  });

  async function getSelection() {
    const storyContext = await fixture.renderHook(() => useStory());
    return storyContext.state.selectedElementIds;
  }

  async function setFontSize(size) {
    const fontSize = fixture.editor.inspector.designPanel.textStyle.fontSize;
    await fixture.events.click(fontSize, { clickCount: 3 });
    await fixture.events.keyboard.type(size);
    await fixture.events.keyboard.press('tab');
    // Give time for the font size to be applied.
    await fixture.events.sleep(100);
  }

  it('should have the last element selected by default', async () => {
    await fixture.events.click(fixture.editor.library.textAdd);
    const frame1 = fixture.editor.canvas.framesLayer.frames[1].node;
    expect(await getSelection()).toEqual([frame1.dataset.elementId]);
  });

  it('should show the selection lines when out of page area', async () => {
    await fixture.events.click(fixture.editor.library.textAdd);
    await setFontSize('30');
    const frame1 = fixture.editor.canvas.framesLayer.frames[1].node;
    const resizeW = fixture
      .querySelector('.moveable-w')
      .getBoundingClientRect();
    await fixture.events.mouse.seq(({ move, moveBy, down, up }) => [
      move(resizeW.left + 1, resizeW.top + 1),
      down(),
      moveBy(-200, 0),
      up(),
    ]);

    expect(await getSelection()).toEqual([frame1.dataset.elementId]);
    await fixture.snapshot();
  });

  it('should show the selection on top of page menu', async () => {
    await fixture.events.click(fixture.editor.library.textAdd);
    await setFontSize('30');
    const frame1 = fixture.editor.canvas.framesLayer.frames[1].node;
    const fbcr = frame1.getBoundingClientRect();
    await fixture.events.mouse.seq(({ moveRel, moveBy, down, up }) => [
      moveRel(frame1, 15, 15),
      down(),
      moveBy(
        0,
        fullbleed.getBoundingClientRect().bottom -
          fbcr.bottom +
          fbcr.height -
          5,
        { steps: 5 }
      ),
      up(),
    ]);
    expect(await getSelection()).toEqual([frame1.dataset.elementId]);
    await fixture.snapshot('selection and page menu');
  });

  it('should show the selection on top of page navigation arrows', async () => {
    await fixture.events.click(fixture.editor.canvas.framesLayer.addPage);

    await fixture.events.click(fixture.editor.library.textAdd);
    await setFontSize('30');

    const frame1 = fixture.editor.canvas.framesLayer.frames[1].node;

    const prevPage = fixture.editor.canvas.framesLayer.prevPage.getBoundingClientRect();
    const resizeW = fixture
      .querySelector('.moveable-w')
      .getBoundingClientRect();
    await fixture.events.mouse.seq(({ move, down, up }) => [
      move(resizeW.left + 1, resizeW.top + 1),
      down(),
      move(prevPage.left - prevPage.width / 2, 0),
      up(),
    ]);
    expect(await getSelection()).toEqual([frame1.dataset.elementId]);
    await fixture.snapshot('selection on top of the page nav');
  });

  it('should return focus to selection when pressing mod+alt+2', async () => {
    await fixture.events.click(fixture.editor.library.textAdd);
    // NB: We can't actually validate that the frame has focus, as that's a bit flaky,
    // But as long as the focus moves in the shortcut press, it's fair to assume that it has
    // Move to the canvas selection.

    // Click elsewhere
    await fixture.events.click(fixture.editor.canvas.header.title);
    expect(fixture.editor.canvas.header.title).toHaveFocus();

    // Return focus with shortcut
    await fixture.events.keyboard.shortcut('mod+alt+2');
    expect(fixture.editor.canvas.header.title).not.toHaveFocus();
    await fixture.snapshot('selected element has focus');
  });
});
<commit_msg>Add Karma test.<commit_after>/*
 * Copyright 2020 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * Internal dependencies
 */
import { Fixture } from '../../../karma';
import { useStory } from '../../../app/story';

describe('CUJ: Creator can Transform an Element: Selection integration', () => {
  let fixture;
  let fullbleed;

  beforeEach(async () => {
    fixture = new Fixture();
    await fixture.render();

    fullbleed = fixture.container.querySelector('[data-testid="fullbleed"]');
  });

  afterEach(() => {
    fixture.restore();
  });

  async function clickOnTarget(target) {
    const { x, y, width, height } = target.getBoundingClientRect();
    await fixture.events.mouse.click(x + width / 2, y + height / 2);
  }

  async function getSelection() {
    const storyContext = await fixture.renderHook(() => useStory());
    return storyContext.state.selectedElementIds;
  }

  async function setFontSize(size) {
    const fontSize = fixture.editor.inspector.designPanel.textStyle.fontSize;
    await fixture.events.click(fontSize, { clickCount: 3 });
    await fixture.events.keyboard.type(size);
    await fixture.events.keyboard.press('tab');
    // Give time for the font size to be applied.
    await fixture.events.sleep(100);
  }

  it('should have the last element selected by default', async () => {
    await fixture.events.click(fixture.editor.library.textAdd);
    const frame1 = fixture.editor.canvas.framesLayer.frames[1].node;
    expect(await getSelection()).toEqual([frame1.dataset.elementId]);
  });

  it('should show the selection lines when an element is being selected', async () => {
    await fixture.events.click(fixture.editor.library.textAdd);
    const frame1 = fixture.editor.canvas.framesLayer.frames[1].node;
    // De-select element by clicking somewhere else.
    const { x, y } = fullbleed.getBoundingClientRect();
    await fixture.events.mouse.click(x - 50, y);
    expect(await getSelection()).toEqual([]);
    // Select the element again.
    await clickOnTarget(frame1);
    expect(
      fixture.querySelector('.moveable-line.moveable-direction.moveable-n')
    ).toBeDefined();
    await fixture.snapshot('text element is selected');
  });

  it('should show the selection lines when out of page area', async () => {
    await fixture.events.click(fixture.editor.library.textAdd);
    await setFontSize('30');
    const frame1 = fixture.editor.canvas.framesLayer.frames[1].node;
    const resizeW = fixture
      .querySelector('.moveable-w')
      .getBoundingClientRect();
    await fixture.events.mouse.seq(({ move, moveBy, down, up }) => [
      move(resizeW.left + 1, resizeW.top + 1),
      down(),
      moveBy(-200, 0),
      up(),
    ]);

    expect(await getSelection()).toEqual([frame1.dataset.elementId]);
    await fixture.snapshot();
  });

  it('should show the selection on top of page menu', async () => {
    await fixture.events.click(fixture.editor.library.textAdd);
    await setFontSize('30');
    const frame1 = fixture.editor.canvas.framesLayer.frames[1].node;
    const fbcr = frame1.getBoundingClientRect();
    await fixture.events.mouse.seq(({ moveRel, moveBy, down, up }) => [
      moveRel(frame1, 15, 15),
      down(),
      moveBy(
        0,
        fullbleed.getBoundingClientRect().bottom -
          fbcr.bottom +
          fbcr.height -
          5,
        { steps: 5 }
      ),
      up(),
    ]);
    expect(await getSelection()).toEqual([frame1.dataset.elementId]);
    await fixture.snapshot('selection and page menu');
  });

  it('should show the selection on top of page navigation arrows', async () => {
    await fixture.events.click(fixture.editor.canvas.framesLayer.addPage);

    await fixture.events.click(fixture.editor.library.textAdd);
    await setFontSize('30');

    const frame1 = fixture.editor.canvas.framesLayer.frames[1].node;

    const prevPage = fixture.editor.canvas.framesLayer.prevPage.getBoundingClientRect();
    const resizeW = fixture
      .querySelector('.moveable-w')
      .getBoundingClientRect();
    await fixture.events.mouse.seq(({ move, down, up }) => [
      move(resizeW.left + 1, resizeW.top + 1),
      down(),
      move(prevPage.left - prevPage.width / 2, 0),
      up(),
    ]);
    expect(await getSelection()).toEqual([frame1.dataset.elementId]);
    await fixture.snapshot('selection on top of the page nav');
  });

  it('should return focus to selection when pressing mod+alt+2', async () => {
    await fixture.events.click(fixture.editor.library.textAdd);
    // NB: We can't actually validate that the frame has focus, as that's a bit flaky,
    // But as long as the focus moves in the shortcut press, it's fair to assume that it has
    // Move to the canvas selection.

    // Click elsewhere
    await fixture.events.click(fixture.editor.canvas.header.title);
    expect(fixture.editor.canvas.header.title).toHaveFocus();

    // Return focus with shortcut
    await fixture.events.keyboard.shortcut('mod+alt+2');
    expect(fixture.editor.canvas.header.title).not.toHaveFocus();
    await fixture.snapshot('selected element has focus');
  });
});
<|endoftext|>


<commit_before>require('ember-views/views/container_view');
require('ember-runtime/system/string');

/**
@module ember
@submodule ember-views
*/

var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt;

/**
  `Ember.CollectionView` is an `Ember.View` descendent responsible for managing
  a collection (an array or array-like object) by maintaing a child view object
  and associated DOM representation for each item in the array and ensuring
  that child views and their associated rendered HTML are updated when items in
  the array are added, removed, or replaced.

  ## Setting content

  The managed collection of objects is referenced as the `Ember.CollectionView`
  instance's `content` property.

  ```javascript
  someItemsView = Ember.CollectionView.create({
    content: ['A', 'B','C']
  })
  ```

  The view for each item in the collection will have its `content` property set
  to the item.

  ## Specifying itemViewClass

  By default the view class for each item in the managed collection will be an
  instance of `Ember.View`. You can supply a different class by setting the
  `CollectionView`'s `itemViewClass` property.

  Given an empty `<body>` and the following code:

  ```javascript
  someItemsView = Ember.CollectionView.create({
    classNames: ['a-collection'],
    content: ['A','B','C'],
    itemViewClass: Ember.View.extend({
      template: Ember.Handlebars.compile("the letter: {{view.content}}")
    })
  });

  someItemsView.appendTo('body');
  ```

  Will result in the following HTML structure

  ```html
  <div class="ember-view a-collection">
    <div class="ember-view">the letter: A</div>
    <div class="ember-view">the letter: B</div>
    <div class="ember-view">the letter: C</div>
  </div>
  ```

  ## Automatic matching of parent/child tagNames

  Setting the `tagName` property of a `CollectionView` to any of
  "ul", "ol", "table", "thead", "tbody", "tfoot", "tr", or "select" will result
  in the item views receiving an appropriately matched `tagName` property.

  Given an empty `<body>` and the following code:

  ```javascript
  anUndorderedListView = Ember.CollectionView.create({
    tagName: 'ul',
    content: ['A','B','C'],
    itemViewClass: Ember.View.extend({
      template: Ember.Handlebars.compile("the letter: {{view.content}}")
    })
  });

  anUndorderedListView.appendTo('body');
  ```

  Will result in the following HTML structure

  ```html
  <ul class="ember-view a-collection">
    <li class="ember-view">the letter: A</li>
    <li class="ember-view">the letter: B</li>
    <li class="ember-view">the letter: C</li>
  </ul>
  ```

  Additional `tagName` pairs can be provided by adding to
  `Ember.CollectionView.CONTAINER_MAP `

  ```javascript
  Ember.CollectionView.CONTAINER_MAP['article'] = 'section'
  ```

  ## Programatic creation of child views

  For cases where additional customization beyond the use of a single
  `itemViewClass` or `tagName` matching is required CollectionView's
  `createChildView` method can be overidden:

  ```javascript
  CustomCollectionView = Ember.CollectionView.extend({
    createChildView: function(viewClass, attrs) {
      if (attrs.content.kind == 'album') {
        viewClass = App.AlbumView;
      } else {
        viewClass = App.SongView;
      }
      return this._super(viewClass, attrs);
    }
  });
  ```

  ## Empty View

  You can provide an `Ember.View` subclass to the `Ember.CollectionView`
  instance as its `emptyView` property. If the `content` property of a
  `CollectionView` is set to `null` or an empty array, an instance of this view
  will be the `CollectionView`s only child.

  ```javascript
  aListWithNothing = Ember.CollectionView.create({
    classNames: ['nothing']
    content: null,
    emptyView: Ember.View.extend({
      template: Ember.Handlebars.compile("The collection is empty")
    })
  });

  aListWithNothing.appendTo('body');
  ```

  Will result in the following HTML structure

  ```html
  <div class="ember-view nothing">
    <div class="ember-view">
      The collection is empty
    </div>
  </div>
  ```

  ## Adding and Removing items

  The `childViews` property of a `CollectionView` should not be directly
  manipulated. Instead, add, remove, replace items from its `content` property.
  This will trigger appropriate changes to its rendered HTML.

  ## Use in templates via the `{{collection}}` `Ember.Handlebars` helper

  `Ember.Handlebars` provides a helper specifically for adding
  `CollectionView`s to templates. See `Ember.Handlebars.collection` for more
  details

  @class CollectionView
  @namespace Ember
  @extends Ember.ContainerView
  @since Ember 0.9
*/
Ember.CollectionView = Ember.ContainerView.extend(/** @scope Ember.CollectionView.prototype */ {

  /**
    A list of items to be displayed by the `Ember.CollectionView`.

    @property content
    @type Ember.Array
    @default null
  */
  content: null,

  /**
    @private

    This provides metadata about what kind of empty view class this
    collection would like if it is being instantiated from another
    system (like Handlebars)

    @property emptyViewClass
  */
  emptyViewClass: Ember.View,

  /**
    An optional view to display if content is set to an empty array.

    @property emptyView
    @type Ember.View
    @default null
  */
  emptyView: null,

  /**
    @property itemViewClass
    @type Ember.View
    @default Ember.View
  */
  itemViewClass: Ember.View,

  init: function() {
    var ret = this._super();
    this._contentDidChange();
    return ret;
  },

  _contentWillChange: Ember.beforeObserver(function() {
    var content = this.get('content');

    if (content) { content.removeArrayObserver(this); }
    var len = content ? get(content, 'length') : 0;
    this.arrayWillChange(content, 0, len);
  }, 'content'),

  /**
    @private

    Check to make sure that the content has changed, and if so,
    update the children directly. This is always scheduled
    asynchronously, to allow the element to be created before
    bindings have synchronized and vice versa.

    @method _contentDidChange
  */
  _contentDidChange: Ember.observer(function() {
    var content = get(this, 'content');

    if (content) {
      Ember.assert(fmt("an Ember.CollectionView's content must implement Ember.Array. You passed %@", [content]), Ember.Array.detect(content));
      content.addArrayObserver(this);
    }

    var len = content ? get(content, 'length') : 0;
    this.arrayDidChange(content, 0, null, len);
  }, 'content'),

  destroy: function() {
    if (!this._super()) { return; }

    var content = get(this, 'content');
    if (content) { content.removeArrayObserver(this); }

    if (this._createdEmptyView) {
      this._createdEmptyView.destroy();
    }

    return this;
  },

  arrayWillChange: function(content, start, removedCount) {
    // If the contents were empty before and this template collection has an
    // empty view remove it now.
    var emptyView = get(this, 'emptyView');
    if (emptyView && emptyView instanceof Ember.View) {
      emptyView.removeFromParent();
    }

    // Loop through child views that correspond with the removed items.
    // Note that we loop from the end of the array to the beginning because
    // we are mutating it as we go.
    var childViews = this._childViews, childView, idx, len;

    len = this._childViews.length;

    var removingAll = removedCount === len;

    if (removingAll) {
      this.currentState.empty(this);
      this.invokeRecursively(function(view) {
        view.removedFromDOM = true;
      }, false);
    }

    for (idx = start + removedCount - 1; idx >= start; idx--) {
      childView = childViews[idx];
      childView.destroy();
    }
  },

  /**
    Called when a mutation to the underlying content array occurs.

    This method will replay that mutation against the views that compose the
    `Ember.CollectionView`, ensuring that the view reflects the model.

    This array observer is added in `contentDidChange`.

    @method arrayDidChange
    @param {Array} content the managed collection of objects
    @param {Number} start the index at which the changes occurred
    @param {Number} removed number of object removed from content
    @param {Number} added number of object added to content
  */
  arrayDidChange: function(content, start, removed, added) {
    var itemViewClass = get(this, 'itemViewClass'),
        addedViews = [], view, item, idx, len;

    if ('string' === typeof itemViewClass) {
      itemViewClass = get(itemViewClass);
    }

    Ember.assert(fmt("itemViewClass must be a subclass of Ember.View, not %@", [itemViewClass]), Ember.View.detect(itemViewClass));

    len = content ? get(content, 'length') : 0;
    if (len) {
      for (idx = start; idx < start+added; idx++) {
        item = content.objectAt(idx);

        view = this.createChildView(itemViewClass, {
          content: item,
          contentIndex: idx
        });

        addedViews.push(view);
      }
    } else {
      var emptyView = get(this, 'emptyView');
      if (!emptyView) { return; }

      var isClass = Ember.CoreView.detect(emptyView);

      emptyView = this.createChildView(emptyView);
      addedViews.push(emptyView);
      set(this, 'emptyView', emptyView);

      if (isClass) { this._createdEmptyView = emptyView; }
    }
    this.replace(start, 0, addedViews);
  },

  createChildView: function(view, attrs) {
    view = this._super(view, attrs);

    var itemTagName = get(view, 'tagName');
    var tagName = (itemTagName === null || itemTagName === undefined) ? Ember.CollectionView.CONTAINER_MAP[get(this, 'tagName')] : itemTagName;

    set(view, 'tagName', tagName);

    return view;
  }
});

/**
  A map of parent tags to their default child tags. You can add
  additional parent tags if you want collection views that use
  a particular parent tag to default to a child tag.

  @property CONTAINER_MAP
  @type Hash
  @static
  @final
*/
Ember.CollectionView.CONTAINER_MAP = {
  ul: 'li',
  ol: 'li',
  table: 'tr',
  thead: 'tr',
  tbody: 'tr',
  tfoot: 'tr',
  tr: 'td',
  select: 'option'
};
<commit_msg>fixed a typo in the api docs<commit_after>require('ember-views/views/container_view');
require('ember-runtime/system/string');

/**
@module ember
@submodule ember-views
*/

var get = Ember.get, set = Ember.set, fmt = Ember.String.fmt;

/**
  `Ember.CollectionView` is an `Ember.View` descendent responsible for managing
  a collection (an array or array-like object) by maintaining a child view object
  and associated DOM representation for each item in the array and ensuring
  that child views and their associated rendered HTML are updated when items in
  the array are added, removed, or replaced.

  ## Setting content

  The managed collection of objects is referenced as the `Ember.CollectionView`
  instance's `content` property.

  ```javascript
  someItemsView = Ember.CollectionView.create({
    content: ['A', 'B','C']
  })
  ```

  The view for each item in the collection will have its `content` property set
  to the item.

  ## Specifying itemViewClass

  By default the view class for each item in the managed collection will be an
  instance of `Ember.View`. You can supply a different class by setting the
  `CollectionView`'s `itemViewClass` property.

  Given an empty `<body>` and the following code:

  ```javascript
  someItemsView = Ember.CollectionView.create({
    classNames: ['a-collection'],
    content: ['A','B','C'],
    itemViewClass: Ember.View.extend({
      template: Ember.Handlebars.compile("the letter: {{view.content}}")
    })
  });

  someItemsView.appendTo('body');
  ```

  Will result in the following HTML structure

  ```html
  <div class="ember-view a-collection">
    <div class="ember-view">the letter: A</div>
    <div class="ember-view">the letter: B</div>
    <div class="ember-view">the letter: C</div>
  </div>
  ```

  ## Automatic matching of parent/child tagNames

  Setting the `tagName` property of a `CollectionView` to any of
  "ul", "ol", "table", "thead", "tbody", "tfoot", "tr", or "select" will result
  in the item views receiving an appropriately matched `tagName` property.

  Given an empty `<body>` and the following code:

  ```javascript
  anUndorderedListView = Ember.CollectionView.create({
    tagName: 'ul',
    content: ['A','B','C'],
    itemViewClass: Ember.View.extend({
      template: Ember.Handlebars.compile("the letter: {{view.content}}")
    })
  });

  anUndorderedListView.appendTo('body');
  ```

  Will result in the following HTML structure

  ```html
  <ul class="ember-view a-collection">
    <li class="ember-view">the letter: A</li>
    <li class="ember-view">the letter: B</li>
    <li class="ember-view">the letter: C</li>
  </ul>
  ```

  Additional `tagName` pairs can be provided by adding to
  `Ember.CollectionView.CONTAINER_MAP `

  ```javascript
  Ember.CollectionView.CONTAINER_MAP['article'] = 'section'
  ```

  ## Programatic creation of child views

  For cases where additional customization beyond the use of a single
  `itemViewClass` or `tagName` matching is required CollectionView's
  `createChildView` method can be overidden:

  ```javascript
  CustomCollectionView = Ember.CollectionView.extend({
    createChildView: function(viewClass, attrs) {
      if (attrs.content.kind == 'album') {
        viewClass = App.AlbumView;
      } else {
        viewClass = App.SongView;
      }
      return this._super(viewClass, attrs);
    }
  });
  ```

  ## Empty View

  You can provide an `Ember.View` subclass to the `Ember.CollectionView`
  instance as its `emptyView` property. If the `content` property of a
  `CollectionView` is set to `null` or an empty array, an instance of this view
  will be the `CollectionView`s only child.

  ```javascript
  aListWithNothing = Ember.CollectionView.create({
    classNames: ['nothing']
    content: null,
    emptyView: Ember.View.extend({
      template: Ember.Handlebars.compile("The collection is empty")
    })
  });

  aListWithNothing.appendTo('body');
  ```

  Will result in the following HTML structure

  ```html
  <div class="ember-view nothing">
    <div class="ember-view">
      The collection is empty
    </div>
  </div>
  ```

  ## Adding and Removing items

  The `childViews` property of a `CollectionView` should not be directly
  manipulated. Instead, add, remove, replace items from its `content` property.
  This will trigger appropriate changes to its rendered HTML.

  ## Use in templates via the `{{collection}}` `Ember.Handlebars` helper

  `Ember.Handlebars` provides a helper specifically for adding
  `CollectionView`s to templates. See `Ember.Handlebars.collection` for more
  details

  @class CollectionView
  @namespace Ember
  @extends Ember.ContainerView
  @since Ember 0.9
*/
Ember.CollectionView = Ember.ContainerView.extend(/** @scope Ember.CollectionView.prototype */ {

  /**
    A list of items to be displayed by the `Ember.CollectionView`.

    @property content
    @type Ember.Array
    @default null
  */
  content: null,

  /**
    @private

    This provides metadata about what kind of empty view class this
    collection would like if it is being instantiated from another
    system (like Handlebars)

    @property emptyViewClass
  */
  emptyViewClass: Ember.View,

  /**
    An optional view to display if content is set to an empty array.

    @property emptyView
    @type Ember.View
    @default null
  */
  emptyView: null,

  /**
    @property itemViewClass
    @type Ember.View
    @default Ember.View
  */
  itemViewClass: Ember.View,

  init: function() {
    var ret = this._super();
    this._contentDidChange();
    return ret;
  },

  _contentWillChange: Ember.beforeObserver(function() {
    var content = this.get('content');

    if (content) { content.removeArrayObserver(this); }
    var len = content ? get(content, 'length') : 0;
    this.arrayWillChange(content, 0, len);
  }, 'content'),

  /**
    @private

    Check to make sure that the content has changed, and if so,
    update the children directly. This is always scheduled
    asynchronously, to allow the element to be created before
    bindings have synchronized and vice versa.

    @method _contentDidChange
  */
  _contentDidChange: Ember.observer(function() {
    var content = get(this, 'content');

    if (content) {
      Ember.assert(fmt("an Ember.CollectionView's content must implement Ember.Array. You passed %@", [content]), Ember.Array.detect(content));
      content.addArrayObserver(this);
    }

    var len = content ? get(content, 'length') : 0;
    this.arrayDidChange(content, 0, null, len);
  }, 'content'),

  destroy: function() {
    if (!this._super()) { return; }

    var content = get(this, 'content');
    if (content) { content.removeArrayObserver(this); }

    if (this._createdEmptyView) {
      this._createdEmptyView.destroy();
    }

    return this;
  },

  arrayWillChange: function(content, start, removedCount) {
    // If the contents were empty before and this template collection has an
    // empty view remove it now.
    var emptyView = get(this, 'emptyView');
    if (emptyView && emptyView instanceof Ember.View) {
      emptyView.removeFromParent();
    }

    // Loop through child views that correspond with the removed items.
    // Note that we loop from the end of the array to the beginning because
    // we are mutating it as we go.
    var childViews = this._childViews, childView, idx, len;

    len = this._childViews.length;

    var removingAll = removedCount === len;

    if (removingAll) {
      this.currentState.empty(this);
      this.invokeRecursively(function(view) {
        view.removedFromDOM = true;
      }, false);
    }

    for (idx = start + removedCount - 1; idx >= start; idx--) {
      childView = childViews[idx];
      childView.destroy();
    }
  },

  /**
    Called when a mutation to the underlying content array occurs.

    This method will replay that mutation against the views that compose the
    `Ember.CollectionView`, ensuring that the view reflects the model.

    This array observer is added in `contentDidChange`.

    @method arrayDidChange
    @param {Array} content the managed collection of objects
    @param {Number} start the index at which the changes occurred
    @param {Number} removed number of object removed from content
    @param {Number} added number of object added to content
  */
  arrayDidChange: function(content, start, removed, added) {
    var itemViewClass = get(this, 'itemViewClass'),
        addedViews = [], view, item, idx, len;

    if ('string' === typeof itemViewClass) {
      itemViewClass = get(itemViewClass);
    }

    Ember.assert(fmt("itemViewClass must be a subclass of Ember.View, not %@", [itemViewClass]), Ember.View.detect(itemViewClass));

    len = content ? get(content, 'length') : 0;
    if (len) {
      for (idx = start; idx < start+added; idx++) {
        item = content.objectAt(idx);

        view = this.createChildView(itemViewClass, {
          content: item,
          contentIndex: idx
        });

        addedViews.push(view);
      }
    } else {
      var emptyView = get(this, 'emptyView');
      if (!emptyView) { return; }

      var isClass = Ember.CoreView.detect(emptyView);

      emptyView = this.createChildView(emptyView);
      addedViews.push(emptyView);
      set(this, 'emptyView', emptyView);

      if (isClass) { this._createdEmptyView = emptyView; }
    }
    this.replace(start, 0, addedViews);
  },

  createChildView: function(view, attrs) {
    view = this._super(view, attrs);

    var itemTagName = get(view, 'tagName');
    var tagName = (itemTagName === null || itemTagName === undefined) ? Ember.CollectionView.CONTAINER_MAP[get(this, 'tagName')] : itemTagName;

    set(view, 'tagName', tagName);

    return view;
  }
});

/**
  A map of parent tags to their default child tags. You can add
  additional parent tags if you want collection views that use
  a particular parent tag to default to a child tag.

  @property CONTAINER_MAP
  @type Hash
  @static
  @final
*/
Ember.CollectionView.CONTAINER_MAP = {
  ul: 'li',
  ol: 'li',
  table: 'tr',
  thead: 'tr',
  tbody: 'tr',
  tfoot: 'tr',
  tr: 'td',
  select: 'option'
};
<|endoftext|>


<commit_before>// @flow
/**
 * Part of GDL gdl-frontend.
 * Copyright (C) 2017 GDL
 *
 * See LICENSE
 */

import * as React from 'react';
import { Trans, I18n } from '@lingui/react';
import {
  Button,
  Card,
  Typography,
  Grid,
  CircularProgress
} from '@material-ui/core';
import {
  ArrowForward as ArrowForwardIcon,
  Sync as SyncIcon,
  Translate as TranslateIcon
} from '@material-ui/icons';
import styled, { css } from 'react-emotion';
import facepaint from 'facepaint';

import doFetch, { fetchMyTranslations } from '../../fetch';
import { Link } from '../../routes';
import type { Translation } from '../../types';
import { securePage } from '../../hocs';
import Layout from '../../components/Layout';
import Container from '../../elements/Container';
import Head from '../../components/Head';
import CoverImage from '../../components/CoverImage';
import TranslateDropdown from '../../components/TranslateDropdown';
import { spacing } from '../../style/theme';
import mq from '../../style/mq';
import { TABLET_BREAKPOINT } from '../../style/theme/misc';
import ReadingLevelTrans from '../../components/ReadingLevelTrans';
import CircleLabel from '../../components/GlobalMenu/CircleLabel';

/**
 * There is a breakpoint interval between 768-865px in width
 * that breaks the UI for the translate dropdown. Which is why
 * a custom media query is used here.
 */
const customMedia = facepaint([
  `@media(min-width: ${TABLET_BREAKPOINT}px)`,
  `@media(min-width: 866px)`
]);

class TranslationCard extends React.Component<
  {
    translation: Translation,
    handleSync: () => void
  },
  { menuIsOpen: boolean, isLoading: boolean, isSynchronized: boolean }
> {
  anchorEl: React$ElementRef<Button> = React.createRef();

  state = {
    menuIsOpen: false,
    isLoading: false,
    isSynchronized: false
  };

  handleSynchronize = async event => {
    event.preventDefault();
    this.setState({ isLoading: true });
    await doFetch(this.props.translation.synchronizeUrl);
    this.setState({ isLoading: false, isSynchronized: true });
    this.props.handleSync();
  };

  closeMenu = (event: SyntheticInputEvent<EventTarget>) => {
    if (this.anchorEl.current.contains(event.target)) {
      return;
    }
    this.setState({ menuIsOpen: false });
  };

  handleToggle = () => {
    this.setState(prev => ({
      menuIsOpen: !prev.menuIsOpen
    }));
  };

  render() {
    const { translation } = this.props;
    const { isLoading, menuIsOpen } = this.state;

    return (
      <Card
        key={translation.id}
        css={mq({
          marginTop: spacing.xlarge,
          marginBottom: [spacing.xxlarge, spacing.medium],
          overflow: 'visible',
          marginLeft: ['-30px', 0, 0],
          marginRight: ['-30px', 0, 0]
        })}
      >
        <CustomGrid>
          <Grid
            item
            css={mq({
              gridArea: 'image',
              marginTop: [-50, 0, 0]
            })}
          >
            <Link
              route="book"
              params={{
                lang: translation.translatedTo.code,
                id: translation.id
              }}
            >

                <CoverImage coverImage={translation.coverImage} size="medium" />

            </Link>
          </Grid>
          <Grid item css={{ gridArea: 'buttons' }}>
            <Button
              onClick={this.handleSynchronize}
              disabled={isLoading}
              color="primary"
              size="large"
              fullWidth
              css={mq({
                justifyContent: 'flex-start',
                borderRadius: 0,
                fontSize: [13, 16, 16]
              })}
            >
              <SyncIcon
                className={isLoading ? spin : null}
                css={mq({ fontSize: [24, 30, 30] })}
              />
              <Trans>Sync</Trans>
            </Button>
            <Button
              buttonRef={this.anchorEl}
              aria-owns={menuIsOpen ? 'menu-list-grow' : undefined}
              aria-haspopup="true"
              onClick={this.handleToggle}
              color="primary"
              size="large"
              variant={menuIsOpen ? 'contained' : 'text'}
              fullWidth
              css={mq({
                justifyContent: 'flex-start',
                borderRadius: 0,
                fontSize: [13, 16, 16]
              })}
            >
              <TranslateIcon css={mq({ fontSize: [24, 30, 30] })} />
              <Trans>Translate</Trans>
            </Button>

            <TranslateDropdown
              ref={this.anchorEl}
              bookId={translation.id}
              crowdinUrl={translation.crowdinUrl}
              onClose={this.closeMenu}
              menuIsOpen={menuIsOpen}
              popperStyle={css`
                margin-right: initial;
                ${customMedia({ paddingRight: [0, 28, 0] })};
              `}
            />
          </Grid>
          <Grid
            container
            direction="column"
            justify="space-between"
            css={mq({
              gridArea: 'content',
              paddingLeft: [0, '1em', '1em'],
              borderRight: [0, '1px solid #e5e5e5', '1px solid #e5e5e5']
            })}
          >
            <Grid item>
              <Typography variant="h5">{translation.title}</Typography>
              <Typography variant="body1" component="span">
                <Trans>From {translation.publisher.name}</Trans>
                {', '}
                <CircleLabel
                  level={translation.readingLevel}
                  style={{
                    marginBottom: '-5px',
                    marginRight: '4px',
                    fontSize: 22
                  }}
                />
                <ReadingLevelTrans readingLevel={translation.readingLevel} />
              </Typography>
            </Grid>

            <Grid item>
              <Grid
                container
                direction="row"
                css={{ marginTop: spacing.medium }}
              >
                <Grid item>
                  <Typography variant="body1">
                    <Trans>From</Trans>:
                  </Typography>
                  <Typography variant="h6">
                    {translation.translatedFrom.name}
                  </Typography>
                </Grid>
                <Grid
                  item
                  css={{
                    display: 'flex',
                    alignItems: 'center',
                    marginLeft: spacing.large,
                    marginRight: spacing.large
                  }}
                >
                  <ArrowForwardIcon style={{ fontSize: 40 }} />
                </Grid>
                <Grid item>
                  <Typography variant="body1">To:</Typography>
                  <Typography variant="h6">
                    {translation.translatedTo.name}
                  </Typography>
                </Grid>
              </Grid>
            </Grid>
          </Grid>
        </CustomGrid>
      </Card>
    );
  }
}

const spin = css`
  font-size: 30px;
  animation-name: spin;
  animation-duration: 2s;
  animation-iteration-count: infinite;
  transform-origin: 50% 50%;
  display: inline-block;

  @keyframes spin {
    from {
      transform: rotate(360deg);
    }
    to {
      transform: rotate(0deg);
    }
  }
`;

const CustomGrid = styled('div')`
  display: grid;
  grid-template-areas:
    'image'
    'content'
    'buttons';

  @media (min-width: 0px) and (orientation: portrait) {
    padding: 30px 0px 30px 30px;
    grid-gap: 1em;
    grid-template-columns: 130px auto;
    grid-template-rows: 150px auto;
    grid-template-areas:
      'image buttons'
      'content content';
  }

  @media (min-width: ${TABLET_BREAKPOINT}px) {
    padding: 40px 0px 40px 40px;
    grid-gap: 1em;
    grid-template-columns: 130px auto 170px;
    grid-template-areas: 'image content buttons';
  }
`;

type LoadingState = 'LOADING' | 'SUCCESS' | 'ERROR';

type State = {
  translations: Array<Translation>,
  loadingState: LoadingState
};

class MyTranslationsPage extends React.Component<{}, State> {
  state = {
    translations: [],
    loadingState: 'LOADING'
  };

  async componentDidMount() {
    this.loadMyTranslations();
  }

  loadMyTranslations = async () => {
    const translationsRes = await fetchMyTranslations();
    if (translationsRes.isOk) {
      this.setState({
        translations: translationsRes.data,
        loadingState: 'SUCCESS'
      });
    } else {
      this.setState({
        loadingState: 'ERROR'
      });
    }
  };

  handleSync = () => {
    // Refreshes my translation cards when a card is synced
    this.loadMyTranslations();
  };

  renderTranslations = () => {
    if (this.state.translations.length === 0) {
      return (
        <Typography
          align="center"
          paragraph
          variant="body1"
          css={{ marginTop: spacing.medium }}
        >
          <Trans>You have not translated any books yet.</Trans>
        </Typography>
      );
    }

    return this.state.translations.map(translation => (
      <TranslationCard
        key={`${translation.id}-${translation.translatedTo.code}`}
        translation={translation}
        handleSync={this.handleSync}
      />
    ));
  };

  render() {
    const { loadingState } = this.state;

    return (
      <Layout>
        <I18n>{({ i18n }) => <Head title={i18n.t`My translations`} />}</I18n>
        <Container style={{ marginBottom: spacing.large }}>
          <Typography
            variant="h4"
            component="h1"
            paragraph
            css={{ marginTop: spacing.large, marginBottom: spacing.medium }}
          >
            <Trans>My translations</Trans>
          </Typography>

          {loadingState === 'LOADING' && (
            <CircularProgress
              css={{
                marginTop: spacing.large,
                marginBottom: spacing.large,
                display: 'block',
                marginLeft: 'auto',
                marginRight: 'auto'
              }}
            />
          )}
          {loadingState === 'SUCCESS' && this.renderTranslations()}
          {loadingState === 'ERROR' && (
            <Typography align="center" color="error" variant="body1">
              <Trans>An error has occurred. Please try again.</Trans>
            </Typography>
          )}
        </Container>
      </Layout>
    );
  }
}

export default securePage(MyTranslationsPage);
<commit_msg>#555: fix design break on long title (#349)<commit_after>// @flow
/**
 * Part of GDL gdl-frontend.
 * Copyright (C) 2017 GDL
 *
 * See LICENSE
 */

import * as React from 'react';
import { Trans, I18n } from '@lingui/react';
import {
  Button,
  Card,
  Typography,
  Grid,
  CircularProgress
} from '@material-ui/core';
import {
  ArrowForward as ArrowForwardIcon,
  Sync as SyncIcon,
  Translate as TranslateIcon
} from '@material-ui/icons';
import styled, { css } from 'react-emotion';
import facepaint from 'facepaint';

import doFetch, { fetchMyTranslations } from '../../fetch';
import { Link } from '../../routes';
import type { Translation } from '../../types';
import { securePage } from '../../hocs';
import Layout from '../../components/Layout';
import Container from '../../elements/Container';
import Head from '../../components/Head';
import CoverImage from '../../components/CoverImage';
import TranslateDropdown from '../../components/TranslateDropdown';
import { spacing } from '../../style/theme';
import mq from '../../style/mq';
import { TABLET_BREAKPOINT } from '../../style/theme/misc';
import ReadingLevelTrans from '../../components/ReadingLevelTrans';
import CircleLabel from '../../components/GlobalMenu/CircleLabel';

/**
 * There is a breakpoint interval between 768-865px in width
 * that breaks the UI for the translate dropdown. Which is why
 * a custom media query is used here.
 */
const customMedia = facepaint([
  `@media(min-width: ${TABLET_BREAKPOINT}px)`,
  `@media(min-width: 866px)`
]);

class TranslationCard extends React.Component<
  {
    translation: Translation,
    handleSync: () => void
  },
  { menuIsOpen: boolean, isLoading: boolean, isSynchronized: boolean }
> {
  anchorEl: React$ElementRef<Button> = React.createRef();

  state = {
    menuIsOpen: false,
    isLoading: false,
    isSynchronized: false
  };

  handleSynchronize = async event => {
    event.preventDefault();
    this.setState({ isLoading: true });
    await doFetch(this.props.translation.synchronizeUrl);
    this.setState({ isLoading: false, isSynchronized: true });
    this.props.handleSync();
  };

  closeMenu = (event: SyntheticInputEvent<EventTarget>) => {
    if (this.anchorEl.current.contains(event.target)) {
      return;
    }
    this.setState({ menuIsOpen: false });
  };

  handleToggle = () => {
    this.setState(prev => ({
      menuIsOpen: !prev.menuIsOpen
    }));
  };

  render() {
    const { translation } = this.props;
    const { isLoading, menuIsOpen } = this.state;

    return (
      <Card
        key={translation.id}
        css={mq({
          marginTop: spacing.xlarge,
          marginBottom: [spacing.xxlarge, spacing.medium],
          overflow: 'visible',
          marginLeft: ['-30px', 0, 0],
          marginRight: ['-30px', 0, 0]
        })}
      >
        <CustomGrid>
          <Grid
            item
            css={mq({
              gridArea: 'image',
              marginTop: [-50, 0, 0]
            })}
          >
            <Link
              route="book"
              params={{
                lang: translation.translatedTo.code,
                id: translation.id
              }}
            >

                <CoverImage coverImage={translation.coverImage} size="medium" />

            </Link>
          </Grid>
          <Grid item css={{ gridArea: 'buttons' }}>
            <Button
              onClick={this.handleSynchronize}
              disabled={isLoading}
              color="primary"
              size="large"
              fullWidth
              css={mq({
                justifyContent: 'flex-start',
                borderRadius: 0,
                fontSize: [13, 16, 16]
              })}
            >
              <SyncIcon
                className={isLoading ? spin : null}
                css={mq({ fontSize: [24, 30, 30] })}
              />
              <Trans>Sync</Trans>
            </Button>
            <Button
              buttonRef={this.anchorEl}
              aria-owns={menuIsOpen ? 'menu-list-grow' : undefined}
              aria-haspopup="true"
              onClick={this.handleToggle}
              color="primary"
              size="large"
              variant={menuIsOpen ? 'contained' : 'text'}
              fullWidth
              css={mq({
                justifyContent: 'flex-start',
                borderRadius: 0,
                fontSize: [13, 16, 16]
              })}
            >
              <TranslateIcon css={mq({ fontSize: [24, 30, 30] })} />
              <Trans>Translate</Trans>
            </Button>

            <TranslateDropdown
              ref={this.anchorEl}
              bookId={translation.id}
              crowdinUrl={translation.crowdinUrl}
              onClose={this.closeMenu}
              menuIsOpen={menuIsOpen}
              popperStyle={css`
                margin-right: initial;
                ${customMedia({ paddingRight: [0, 28, 0] })};
              `}
            />
          </Grid>
          <Grid
            container
            direction="column"
            justify="space-between"
            css={mq({
              gridArea: 'content',
              paddingLeft: [0, '1em', '1em'],
              borderRight: [0, '1px solid #e5e5e5', '1px solid #e5e5e5']
            })}
          >
            <Grid item>
              <Typography variant="h5">{translation.title}</Typography>
              <Typography variant="body1" component="span">
                <Trans>From {translation.publisher.name}</Trans>
                {', '}
                <CircleLabel
                  level={translation.readingLevel}
                  style={{
                    marginBottom: '-5px',
                    marginRight: '4px',
                    fontSize: 22
                  }}
                />
                <ReadingLevelTrans readingLevel={translation.readingLevel} />
              </Typography>
            </Grid>

            <Grid item>
              <Grid
                container
                direction="row"
                css={mq({ marginTop: [spacing.medium, 0, spacing.medium] })}
              >
                <Grid item>
                  <Typography variant="body1">
                    <Trans>From</Trans>:
                  </Typography>
                  <Typography variant="h6">
                    {translation.translatedFrom.name}
                  </Typography>
                </Grid>
                <Grid
                  item
                  css={{
                    display: 'flex',
                    alignItems: 'center',
                    marginLeft: spacing.large,
                    marginRight: spacing.large
                  }}
                >
                  <ArrowForwardIcon style={{ fontSize: 40 }} />
                </Grid>
                <Grid item>
                  <Typography variant="body1">To:</Typography>
                  <Typography variant="h6">
                    {translation.translatedTo.name}
                  </Typography>
                </Grid>
              </Grid>
            </Grid>
          </Grid>
        </CustomGrid>
      </Card>
    );
  }
}

const spin = css`
  font-size: 30px;
  animation-name: spin;
  animation-duration: 2s;
  animation-iteration-count: infinite;
  transform-origin: 50% 50%;
  display: inline-block;

  @keyframes spin {
    from {
      transform: rotate(360deg);
    }
    to {
      transform: rotate(0deg);
    }
  }
`;

const CustomGrid = styled('div')`
  display: grid;
  grid-template-areas:
    'image'
    'content'
    'buttons';

  @media (min-width: 0px) and (orientation: portrait) {
    padding: 30px 0px 30px 30px;
    grid-gap: 1em;
    grid-template-columns: 130px auto;
    grid-template-rows: 150px auto;
    grid-template-areas:
      'image buttons'
      'content content';
  }

  @media (min-width: ${TABLET_BREAKPOINT}px) {
    padding: 40px 0px 40px 40px;
    grid-gap: 1em;
    grid-template-columns: 130px auto 170px;
    grid-template-areas: 'image content buttons';
  }
`;

type LoadingState = 'LOADING' | 'SUCCESS' | 'ERROR';

type State = {
  translations: Array<Translation>,
  loadingState: LoadingState
};

class MyTranslationsPage extends React.Component<{}, State> {
  state = {
    translations: [],
    loadingState: 'LOADING'
  };

  async componentDidMount() {
    this.loadMyTranslations();
  }

  loadMyTranslations = async () => {
    const translationsRes = await fetchMyTranslations();
    if (translationsRes.isOk) {
      this.setState({
        translations: translationsRes.data,
        loadingState: 'SUCCESS'
      });
    } else {
      this.setState({
        loadingState: 'ERROR'
      });
    }
  };

  handleSync = () => {
    // Refreshes my translation cards when a card is synced
    this.loadMyTranslations();
  };

  renderTranslations = () => {
    if (this.state.translations.length === 0) {
      return (
        <Typography
          align="center"
          paragraph
          variant="body1"
          css={{ marginTop: spacing.medium }}
        >
          <Trans>You have not translated any books yet.</Trans>
        </Typography>
      );
    }

    return this.state.translations.map(translation => (
      <TranslationCard
        key={`${translation.id}-${translation.translatedTo.code}`}
        translation={translation}
        handleSync={this.handleSync}
      />
    ));
  };

  render() {
    const { loadingState } = this.state;

    return (
      <Layout>
        <I18n>{({ i18n }) => <Head title={i18n.t`My translations`} />}</I18n>
        <Container style={{ marginBottom: spacing.large }}>
          <Typography
            variant="h4"
            component="h1"
            paragraph
            css={{ marginTop: spacing.large, marginBottom: spacing.medium }}
          >
            <Trans>My translations</Trans>
          </Typography>

          {loadingState === 'LOADING' && (
            <CircularProgress
              css={{
                marginTop: spacing.large,
                marginBottom: spacing.large,
                display: 'block',
                marginLeft: 'auto',
                marginRight: 'auto'
              }}
            />
          )}
          {loadingState === 'SUCCESS' && this.renderTranslations()}
          {loadingState === 'ERROR' && (
            <Typography align="center" color="error" variant="body1">
              <Trans>An error has occurred. Please try again.</Trans>
            </Typography>
          )}
        </Container>
      </Layout>
    );
  }
}

export default securePage(MyTranslationsPage);
<|endoftext|>