<commit_before>import Vue from "vue";
import VueRouter from "vue-router";
import { getRemoteData } from "../webapp/lib/utils";
import { EventBus } from "../webapp/lib/mixins/event_bus";
import { checkAndReportAccessibility } from "lib/vue/accesibility";

if (Vue.config.devtools) {
  Vue.use(checkAndReportAccessibility)
}
Vue.use(VueRouter);
Vue.config.productionTip = false;

export class CostsController {
  constructor(options) {
    this.charts = {};

    const selector = "gobierto-visualizations-costs-app";

    // Mount Vue applications
    const entryPoint = document.getElementById(selector);

    if (entryPoint) {
      const htmlRouterBlock = `<router-view></router-view>`;

      entryPoint.innerHTML = htmlRouterBlock;

      const Home = () =>
        import("../webapp/containers/costs/Home.vue");
      const TableFirstLevel = () =>
        import("../webapp/containers/costs/table/TableFirstLevel.vue");
      const TableSecondLevel = () =>
        import("../webapp/containers/costs/table/TableSecondLevel.vue");
      const TableItem = () =>
        import("../webapp/containers/costs/table/TableItem.vue");

      Promise.resolve(getRemoteData(options.costsEndpoint)).then(rawData => {
        this.setGlobalVariables(rawData);

        const router = new VueRouter({
          mode: "history",
          routes: [
            {
              path: "/visualizaciones/costes/",
              name: "Home",
              component: Home,
              children: [
                {
                  path: "/visualizaciones/costes/:year?",
                  component: TableFirstLevel,
                  name: "TableFirstLevel"
                },
                {
                  path: "/visualizaciones/costes/:year?/:id?",
                  component: TableSecondLevel,
                  name: "TableSecondLevel"
                },
                {
                  path: "/visualizaciones/costes/:year?/:id?/:item?",
                  component: TableItem,
                  name: "TableItem"
                }
              ]
            }
          ],
          scrollBehavior(to) {
            let element;
            //Get a different position scroll
            if (to.name !== "TableFirstLevel") {
              element = document.getElementById(
                "gobierto-visualizations-title-detail"
              );
              element.scrollIntoView({ behavior: "smooth" });
            }
          }
        });

        const baseTitle = document.title;
        router.afterEach(to => {
          // Wait 2 ticks
          Vue.nextTick(() =>
            Vue.nextTick(() => {
              let title = baseTitle;
              if (to.name === "TableItem") {
                const { description: itemTitle } = to.params;

                if (itemTitle) {
                  title = `${itemTitle} ${baseTitle}`;
                }
              } else if (to.name === "TableSecondLevel") {
                const { description: itemTitle } = to.params;

                if (itemTitle) {
                  title = `${itemTitle} ${baseTitle}`;
                }
              }
              document.title = title;
            })
          );
        });

        this.vueApp = new Vue({
          router,
          data: Object.assign(options, this.data)
        }).$mount(entryPoint);

        EventBus.$on("mounted", () => {
          // Hide the external loader once the vueApp has been mounted in the DOM
          const loadingElement = document.querySelector(".js-loading");
          if (loadingElement) {
            loadingElement.classList.add("hidden");
          }
        });
      });
    }
  }

  setGlobalVariables(rawData) {
    //Convert strings with some format to Numbers without format
    function convertStringToNumbers(amount) {
      if (amount === "") {
        return +amount || 0;
      } else {
        return Number(parseFloat(amount));
      }
    }

    //Array with all the strings that we've to convert to Number
    const amountStrings = [
      "costdirecte",
      "costindirecte",
      "costtotal",
      "costperhabit",
      "costpersonal",
      "costrestadir",
      "ingressos",
      "taxapreupub",
      "subvencions",
      "ingressos_cost",
      "costdirectepers",
      "costdirebens",
      "costdirservext",
      "costdirtransf",
      "costdirequip",
      "costdirfin"
    ];

    const population = ["128291", "129661", "129120"];

    let yearsCosts = [...new Set(rawData.map(item => item.any_))];

    rawData.forEach((item, index) => {
      rawData[index].population = population[yearsCosts.findIndex(year => year === rawData[index]["any_"])]
      for (let amounts of amountStrings) {
        rawData[index][amounts] = convertStringToNumbers(
          rawData[index][amounts]
        );
      }
    })

    let groupDataByYears = [];

    //Create an array of objects with all years
    for (let index = 0; index < yearsCosts.length; index++) {
      const data = rawData.reduce((acc, item) => {
        if (item.any_ === yearsCosts[index]) {
          acc.push({ ...item, population: population[index] });
        }
        return acc;
      }, []);
      const dataGroup = groupDataByYear(data, yearsCosts[index]);
      groupDataByYears.push(dataGroup);
    }

    function groupDataByYear(data, year) {
      //Filter groupData by "sumadatos": https://github.com/PopulateTools/issues/issues/1097
      let dataFilterSum = data.filter(({ sumadatos }) => sumadatos === "");
      let groupData = [
        ...dataFilterSum
          .reduce((r, o) => {
            const key = o.agrupacio;

            const item =
              r.get(key) ||
              Object.assign({}, o, {
                costdirecte: 0,
                costindirecte: 0,
                costtotal: 0,
                ingressos: 0,
                total: 0,
                totalPerHabitant: 0,
                coverage: 0
              });

            item.costdirecte += o.costdirecte;
            item.costindirecte += o.costindirecte;
            item.costtotal += o.costtotal;
            item.ingressos += o.ingressos;
            //New item with the sum of values of each agrupacio
            item.total += (o.total || 0) + 1;
            item.coverage = (item.ingressos * 100) / item.costtotal;
            item.totalPerHabitant = item.costtotal / o.population;

            return r.set(key, item);
          }, new Map())
          .values()
      ];
      return (groupData = groupData.filter(
        ({ agrupacio, any_ }) => agrupacio !== "" && any_ === year
      ));
    }

    groupDataByYears = groupDataByYears
      .flat()
      .sort(({ costtotal: a }, { costtotal: b }) => (a > b ? -1 : 1));

    this.data = {
      costData: rawData,
      groupData: groupDataByYears,
      yearsCosts: yearsCosts
    };
  }
}
<commit_msg>resolved changes @crashillo<commit_after>import Vue from "vue";
import VueRouter from "vue-router";
import { getRemoteData } from "../webapp/lib/utils";
import { EventBus } from "../webapp/lib/mixins/event_bus";
import { checkAndReportAccessibility } from "lib/vue/accesibility";

if (Vue.config.devtools) {
  Vue.use(checkAndReportAccessibility)
}
Vue.use(VueRouter);
Vue.config.productionTip = false;

export class CostsController {
  constructor(options) {
    this.charts = {};

    const selector = "gobierto-visualizations-costs-app";

    // Mount Vue applications
    const entryPoint = document.getElementById(selector);

    if (entryPoint) {
      const htmlRouterBlock = `<router-view></router-view>`;

      entryPoint.innerHTML = htmlRouterBlock;

      const Home = () =>
        import("../webapp/containers/costs/Home.vue");
      const TableFirstLevel = () =>
        import("../webapp/containers/costs/table/TableFirstLevel.vue");
      const TableSecondLevel = () =>
        import("../webapp/containers/costs/table/TableSecondLevel.vue");
      const TableItem = () =>
        import("../webapp/containers/costs/table/TableItem.vue");

      Promise.resolve(getRemoteData(options.costsEndpoint)).then(rawData => {
        this.setGlobalVariables(rawData);

        const router = new VueRouter({
          mode: "history",
          routes: [
            {
              path: "/visualizaciones/costes/",
              name: "Home",
              component: Home,
              children: [
                {
                  path: "/visualizaciones/costes/:year?",
                  component: TableFirstLevel,
                  name: "TableFirstLevel"
                },
                {
                  path: "/visualizaciones/costes/:year?/:id?",
                  component: TableSecondLevel,
                  name: "TableSecondLevel"
                },
                {
                  path: "/visualizaciones/costes/:year?/:id?/:item?",
                  component: TableItem,
                  name: "TableItem"
                }
              ]
            }
          ],
          scrollBehavior(to) {
            let element;
            //Get a different position scroll
            if (to.name !== "TableFirstLevel") {
              element = document.getElementById(
                "gobierto-visualizations-title-detail"
              );
              element.scrollIntoView({ behavior: "smooth" });
            }
          }
        });

        const baseTitle = document.title;
        router.afterEach(to => {
          // Wait 2 ticks
          Vue.nextTick(() =>
            Vue.nextTick(() => {
              let title = baseTitle;
              if (to.name === "TableItem") {
                const { description: itemTitle } = to.params;

                if (itemTitle) {
                  title = `${itemTitle} ${baseTitle}`;
                }
              } else if (to.name === "TableSecondLevel") {
                const { description: itemTitle } = to.params;

                if (itemTitle) {
                  title = `${itemTitle} ${baseTitle}`;
                }
              }
              document.title = title;
            })
          );
        });

        this.vueApp = new Vue({
          router,
          data: Object.assign(options, this.data)
        }).$mount(entryPoint);

        EventBus.$on("mounted", () => {
          // Hide the external loader once the vueApp has been mounted in the DOM
          const loadingElement = document.querySelector(".js-loading");
          if (loadingElement) {
            loadingElement.classList.add("hidden");
          }
        });
      });
    }
  }

  setGlobalVariables(rawData) {
    //Convert strings with some format to Numbers without format
    const toNumber = (value) => value ? +value || 0 : +(parseFloat(value))
    //Array with all the strings that we've to convert to Number
    const amountStrings = [
      "costdirecte",
      "costindirecte",
      "costtotal",
      "costperhabit",
      "costpersonal",
      "costrestadir",
      "ingressos",
      "taxapreupub",
      "subvencions",
      "ingressos_cost",
      "costdirectepers",
      "costdirebens",
      "costdirservext",
      "costdirtransf",
      "costdirequip",
      "costdirfin"
    ];

    const population = ["128291", "129661", "129120"];

    let yearsCosts = [...new Set(rawData.map(item => item.any_))];
    const setPopulation = (value, item) => value[yearsCosts.findIndex(year => year === item["any_"])]

    const data = rawData.map((item) => {
      for (let amounts of amountStrings) {
        item[amounts] = toNumber(item[amounts]);
      }
      return {
        population: setPopulation(population, item),
        ...item
      }
    })

    let groupDataByYears = [];

    //Create an array of objects with all years
    for (let index = 0; index < yearsCosts.length; index++) {
      const data = rawData.reduce((acc, item) => {
        if (item.any_ === yearsCosts[index]) {
          acc.push({ ...item, population: population[index] });
        }
        return acc;
      }, []);
      const dataGroup = groupDataByYear(data, yearsCosts[index]);
      groupDataByYears.push(dataGroup);
    }

    function groupDataByYear(data, year) {
      //Filter groupData by "sumadatos": https://github.com/PopulateTools/issues/issues/1097
      let dataFilterSum = data.filter(({ sumadatos }) => sumadatos === "");
      let groupData = [
        ...dataFilterSum
          .reduce((r, o) => {
            const key = o.agrupacio;

            const item =
              r.get(key) ||
              Object.assign({}, o, {
                costdirecte: 0,
                costindirecte: 0,
                costtotal: 0,
                ingressos: 0,
                total: 0,
                totalPerHabitant: 0,
                coverage: 0
              });

            item.costdirecte += o.costdirecte;
            item.costindirecte += o.costindirecte;
            item.costtotal += o.costtotal;
            item.ingressos += o.ingressos;
            //New item with the sum of values of each agrupacio
            item.total += (o.total || 0) + 1;
            item.coverage = (item.ingressos * 100) / item.costtotal;
            item.totalPerHabitant = item.costtotal / o.population;

            return r.set(key, item);
          }, new Map())
          .values()
      ];
      return (groupData = groupData.filter(
        ({ agrupacio, any_ }) => agrupacio !== "" && any_ === year
      ));
    }

    groupDataByYears = groupDataByYears
      .flat()
      .sort(({ costtotal: a }, { costtotal: b }) => (a > b ? -1 : 1));

    this.data = {
      costData: data,
      groupData: groupDataByYears,
      yearsCosts: yearsCosts
    };
  }
}
<|endoftext|>


<commit_before>import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ReactSwipeableViews from 'react-swipeable-views';
import classNames from 'classnames';
import Permalink from '../../../components/permalink';
import ComposeForm from '../../compose/components/compose_form';
import Search from '../../compose/components/search';
import NavigationBar from '../../compose/components/navigation_bar';
import ColumnHeader from './column_header';
import { List as ImmutableList } from 'immutable';

const noop = () => { };

const messages = defineMessages({
  home_title: { id: 'column.home', defaultMessage: 'Home' },
  notifications_title: { id: 'column.notifications', defaultMessage: 'Notifications' },
  local_title: { id: 'column.community', defaultMessage: 'Local timeline' },
  federated_title: { id: 'column.public', defaultMessage: 'Federated timeline' },
});

const PageOne = ({ acct, domain }) => (

<FormattedMessage id='onboarding.page_one.welcome' defaultMessage='Welcome to Mastodon!' />

<FormattedMessage id='onboarding.page_one.federation' defaultMessage='Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.' />

<FormattedMessage id='onboarding.page_one.handle' defaultMessage='You are on {domain}, so your full handle is {handle}' values={{ domain, handle: {acct}@{domain} }} />

);

PageOne.propTypes = {
  acct: PropTypes.string.isRequired,
  domain: PropTypes.string.isRequired,
};

const PageTwo = ({ me }) => (

        <NavigationBar account={me} />

      <ComposeForm
        text='Awoo! #introductions'
        suggestions={ImmutableList()}
        mentionedDomains={[]}
        spoiler={false}
        onChange={noop}
        onSubmit={noop}
        onPaste={noop}
        onPickEmoji={noop}
        onChangeSpoilerText={noop}
        onClearSuggestions={noop}
        onFetchSuggestions={noop}
        onSuggestionSelected={noop}
        showSearch
      />

<FormattedMessage id='onboarding.page_two.compose' defaultMessage='Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.' />

);

PageTwo.propTypes = {
  me: ImmutablePropTypes.map.isRequired,
};

const PageThree = ({ me }) => (

      <Search
        value=''
        onChange={noop}
        onSubmit={noop}
        onClear={noop}
        onShow={noop}
      />

        <NavigationBar account={me} />

<FormattedMessage id='onboarding.page_three.search' defaultMessage='Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.' values={{ illustration: <Permalink to='/timelines/tag/illustration' href='/tags/illustration'>#illustration</Permalink>, introductions: <Permalink to='/timelines/tag/introductions' href='/tags/introductions'>#introductions</Permalink> }} />

<FormattedMessage id='onboarding.page_three.profile' defaultMessage='Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.' />

);

PageThree.propTypes = {
  me: ImmutablePropTypes.map.isRequired,
};

const PageFour = ({ domain, intl }) => (

<ColumnHeader icon='home' type={intl.formatMessage(messages.home_title)} />

<FormattedMessage id='onboarding.page_four.home' defaultMessage='The home timeline shows posts from people you follow.' />

<ColumnHeader icon='bell' type={intl.formatMessage(messages.notifications_title)} />

<FormattedMessage id='onboarding.page_four.notifications' defaultMessage='The notifications column shows when someone interacts with you.' />

<ColumnHeader icon='users' type={intl.formatMessage(messages.local_title)} />

<ColumnHeader icon='globe' type={intl.formatMessage(messages.federated_title)} />

<FormattedMessage id='onboarding.page_five.public_timelines' defaultMessage='The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.' values={{ domain }} />

);

PageFour.propTypes = {
  domain: PropTypes.string.isRequired,
  intl: PropTypes.object.isRequired,
};

const PageSix = ({ admin, domain }) => {
  let adminSection = '';

  if (admin) {
    adminSection = (

        <FormattedMessage id='onboarding.page_six.admin' defaultMessage="Your instance's admin is {admin}." values={{ admin: <Permalink href={admin.get('url')} to={`/accounts/${admin.get('id')}`}>@{admin.get('acct')}</Permalink> }} />

        <FormattedMessage id='onboarding.page_six.read_guidelines' defaultMessage="Please read {domain}'s {guidelines}!" values={{ domain, guidelines: <FormattedMessage id='onboarding.page_six.guidelines' defaultMessage='community guidelines' /> }} />

    );
  }

  return (

<FormattedMessage id='onboarding.page_six.almost_done' defaultMessage='Almost done...' />

      {adminSection}

<FormattedMessage id='onboarding.page_six.github' defaultMessage='Mastodon is free open-source software. You can report bugs, request features, or contribute to the code on {github}.' values={{ github: GitHub }} />

<FormattedMessage id='onboarding.page_six.apps_available' defaultMessage='There are {apps} available for iOS, Android and other platforms.' values={{ apps: <FormattedMessage id='onboarding.page_six.various_app' defaultMessage='mobile apps' /> }} />

<FormattedMessage id='onboarding.page_six.appetoot' defaultMessage='Bon Appetoot!' />

  );
};

PageSix.propTypes = {
  admin: ImmutablePropTypes.map,
  domain: PropTypes.string.isRequired,
};

const mapStateToProps = state => ({
  me: state.getIn(['accounts', state.getIn(['meta', 'me'])]),
  admin: state.getIn(['accounts', state.getIn(['meta', 'admin'])]),
  domain: state.getIn(['meta', 'domain']),
});

@connect(mapStateToProps)
@injectIntl
export default class OnboardingModal extends React.PureComponent {

  static propTypes = {
    onClose: PropTypes.func.isRequired,
    intl: PropTypes.object.isRequired,
    me: ImmutablePropTypes.map.isRequired,
    domain: PropTypes.string.isRequired,
    admin: ImmutablePropTypes.map,
  };

  state = {
    currentIndex: 0,
  };

  componentWillMount() {
    const { me, admin, domain, intl } = this.props;
    this.pages = [
      <PageOne acct={me.get('acct')} domain={domain} />,
      <PageTwo me={me} />,
      <PageThree me={me} />,
      <PageFour domain={domain} intl={intl} />,
      <PageSix admin={admin} domain={domain} />,
    ];
  };

  componentDidMount() {
    window.addEventListener('keyup', this.handleKeyUp);
  }

  componentWillUnmount() {
    window.addEventListener('keyup', this.handleKeyUp);
  }

  handleSkip = (e) => {
    e.preventDefault();
    this.props.onClose();
  }

  handleDot = (e) => {
    const i = Number(e.currentTarget.getAttribute('data-index'));
    e.preventDefault();
    this.setState({ currentIndex: i });
  }

  handlePrev = () => {
    this.setState(({ currentIndex }) => ({
      currentIndex: Math.max(0, currentIndex - 1),
    }));
  }

  handleNext = () => {
    const { pages } = this;
    this.setState(({ currentIndex }) => ({
      currentIndex: Math.min(currentIndex + 1, pages.length - 1),
    }));
  }

  handleSwipe = (index) => {
    this.setState({ currentIndex: index });
  }

  handleKeyUp = ({ key }) => {
    switch (key) {
    case 'ArrowLeft':
      this.handlePrev();
      break;
    case 'ArrowRight':
      this.handleNext();
      break;
    }
  }

  handleClose = () => {
    this.props.onClose();
  }

  render () {
    const { pages } = this;
    const { currentIndex } = this.state;
    const hasMore = currentIndex < pages.length - 1;

    const nextOrDoneBtn = hasMore ? (
      <button
        onClick={this.handleNext}
        className='onboarding-modal__nav onboarding-modal__next'
      >
        <FormattedMessage id='onboarding.next' defaultMessage='Next' />
      </button>
    ) : (
      <button
        onClick={this.handleClose}
        className='onboarding-modal__nav onboarding-modal__done'
      >
        <FormattedMessage id='onboarding.done' defaultMessage='Done' />
      </button>
    );

    return (

        <ReactSwipeableViews index={currentIndex} onChangeIndex={this.handleSwipe} className='onboarding-modal__pager'>
          {pages.map((page, i) => {
            const className = classNames('onboarding-modal__page__wrapper', {
              'onboarding-modal__page__wrapper--active': i === currentIndex,
            });
            return (

{page}

            );
          })}
        </ReactSwipeableViews>

            <button
              onClick={this.handleSkip}
              className='onboarding-modal__nav onboarding-modal__skip'
            >
              <FormattedMessage id='onboarding.skip' defaultMessage='Skip' />
            </button>

            {pages.map((_, i) => {
              const className = classNames('onboarding-modal__dot', {
                active: i === currentIndex,
              });
              return (
                <div
                  key={`dot-${i}`}
                  role='button'
                  tabIndex='0'
                  data-index={i}
                  onClick={this.handleDot}
                  className={className}
                />
              );
            })}

            {nextOrDoneBtn}

    );
  }

}
<commit_msg>add missing @ to the onboarding modal (#4560)<commit_after>import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ReactSwipeableViews from 'react-swipeable-views';
import classNames from 'classnames';
import Permalink from '../../../components/permalink';
import ComposeForm from '../../compose/components/compose_form';
import Search from '../../compose/components/search';
import NavigationBar from '../../compose/components/navigation_bar';
import ColumnHeader from './column_header';
import { List as ImmutableList } from 'immutable';

const noop = () => { };

const messages = defineMessages({
  home_title: { id: 'column.home', defaultMessage: 'Home' },
  notifications_title: { id: 'column.notifications', defaultMessage: 'Notifications' },
  local_title: { id: 'column.community', defaultMessage: 'Local timeline' },
  federated_title: { id: 'column.public', defaultMessage: 'Federated timeline' },
});

const PageOne = ({ acct, domain }) => (

<FormattedMessage id='onboarding.page_one.welcome' defaultMessage='Welcome to Mastodon!' />

<FormattedMessage id='onboarding.page_one.federation' defaultMessage='Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.' />

<FormattedMessage id='onboarding.page_one.handle' defaultMessage='You are on {domain}, so your full handle is {handle}' values={{ domain, handle: @{acct}@{domain} }} />

);

PageOne.propTypes = {
  acct: PropTypes.string.isRequired,
  domain: PropTypes.string.isRequired,
};

const PageTwo = ({ me }) => (

        <NavigationBar account={me} />

      <ComposeForm
        text='Awoo! #introductions'
        suggestions={ImmutableList()}
        mentionedDomains={[]}
        spoiler={false}
        onChange={noop}
        onSubmit={noop}
        onPaste={noop}
        onPickEmoji={noop}
        onChangeSpoilerText={noop}
        onClearSuggestions={noop}
        onFetchSuggestions={noop}
        onSuggestionSelected={noop}
        showSearch
      />

<FormattedMessage id='onboarding.page_two.compose' defaultMessage='Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.' />

);

PageTwo.propTypes = {
  me: ImmutablePropTypes.map.isRequired,
};

const PageThree = ({ me }) => (

      <Search
        value=''
        onChange={noop}
        onSubmit={noop}
        onClear={noop}
        onShow={noop}
      />

        <NavigationBar account={me} />

<FormattedMessage id='onboarding.page_three.search' defaultMessage='Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.' values={{ illustration: <Permalink to='/timelines/tag/illustration' href='/tags/illustration'>#illustration</Permalink>, introductions: <Permalink to='/timelines/tag/introductions' href='/tags/introductions'>#introductions</Permalink> }} />

<FormattedMessage id='onboarding.page_three.profile' defaultMessage='Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.' />

);

PageThree.propTypes = {
  me: ImmutablePropTypes.map.isRequired,
};

const PageFour = ({ domain, intl }) => (

<ColumnHeader icon='home' type={intl.formatMessage(messages.home_title)} />

<FormattedMessage id='onboarding.page_four.home' defaultMessage='The home timeline shows posts from people you follow.' />

<ColumnHeader icon='bell' type={intl.formatMessage(messages.notifications_title)} />

<FormattedMessage id='onboarding.page_four.notifications' defaultMessage='The notifications column shows when someone interacts with you.' />

<ColumnHeader icon='users' type={intl.formatMessage(messages.local_title)} />

<ColumnHeader icon='globe' type={intl.formatMessage(messages.federated_title)} />

<FormattedMessage id='onboarding.page_five.public_timelines' defaultMessage='The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.' values={{ domain }} />

);

PageFour.propTypes = {
  domain: PropTypes.string.isRequired,
  intl: PropTypes.object.isRequired,
};

const PageSix = ({ admin, domain }) => {
  let adminSection = '';

  if (admin) {
    adminSection = (

        <FormattedMessage id='onboarding.page_six.admin' defaultMessage="Your instance's admin is {admin}." values={{ admin: <Permalink href={admin.get('url')} to={`/accounts/${admin.get('id')}`}>@{admin.get('acct')}</Permalink> }} />

        <FormattedMessage id='onboarding.page_six.read_guidelines' defaultMessage="Please read {domain}'s {guidelines}!" values={{ domain, guidelines: <FormattedMessage id='onboarding.page_six.guidelines' defaultMessage='community guidelines' /> }} />

    );
  }

  return (

<FormattedMessage id='onboarding.page_six.almost_done' defaultMessage='Almost done...' />

      {adminSection}

<FormattedMessage id='onboarding.page_six.github' defaultMessage='Mastodon is free open-source software. You can report bugs, request features, or contribute to the code on {github}.' values={{ github: GitHub }} />

<FormattedMessage id='onboarding.page_six.apps_available' defaultMessage='There are {apps} available for iOS, Android and other platforms.' values={{ apps: <FormattedMessage id='onboarding.page_six.various_app' defaultMessage='mobile apps' /> }} />

<FormattedMessage id='onboarding.page_six.appetoot' defaultMessage='Bon Appetoot!' />

  );
};

PageSix.propTypes = {
  admin: ImmutablePropTypes.map,
  domain: PropTypes.string.isRequired,
};

const mapStateToProps = state => ({
  me: state.getIn(['accounts', state.getIn(['meta', 'me'])]),
  admin: state.getIn(['accounts', state.getIn(['meta', 'admin'])]),
  domain: state.getIn(['meta', 'domain']),
});

@connect(mapStateToProps)
@injectIntl
export default class OnboardingModal extends React.PureComponent {

  static propTypes = {
    onClose: PropTypes.func.isRequired,
    intl: PropTypes.object.isRequired,
    me: ImmutablePropTypes.map.isRequired,
    domain: PropTypes.string.isRequired,
    admin: ImmutablePropTypes.map,
  };

  state = {
    currentIndex: 0,
  };

  componentWillMount() {
    const { me, admin, domain, intl } = this.props;
    this.pages = [
      <PageOne acct={me.get('acct')} domain={domain} />,
      <PageTwo me={me} />,
      <PageThree me={me} />,
      <PageFour domain={domain} intl={intl} />,
      <PageSix admin={admin} domain={domain} />,
    ];
  };

  componentDidMount() {
    window.addEventListener('keyup', this.handleKeyUp);
  }

  componentWillUnmount() {
    window.addEventListener('keyup', this.handleKeyUp);
  }

  handleSkip = (e) => {
    e.preventDefault();
    this.props.onClose();
  }

  handleDot = (e) => {
    const i = Number(e.currentTarget.getAttribute('data-index'));
    e.preventDefault();
    this.setState({ currentIndex: i });
  }

  handlePrev = () => {
    this.setState(({ currentIndex }) => ({
      currentIndex: Math.max(0, currentIndex - 1),
    }));
  }

  handleNext = () => {
    const { pages } = this;
    this.setState(({ currentIndex }) => ({
      currentIndex: Math.min(currentIndex + 1, pages.length - 1),
    }));
  }

  handleSwipe = (index) => {
    this.setState({ currentIndex: index });
  }

  handleKeyUp = ({ key }) => {
    switch (key) {
    case 'ArrowLeft':
      this.handlePrev();
      break;
    case 'ArrowRight':
      this.handleNext();
      break;
    }
  }

  handleClose = () => {
    this.props.onClose();
  }

  render () {
    const { pages } = this;
    const { currentIndex } = this.state;
    const hasMore = currentIndex < pages.length - 1;

    const nextOrDoneBtn = hasMore ? (
      <button
        onClick={this.handleNext}
        className='onboarding-modal__nav onboarding-modal__next'
      >
        <FormattedMessage id='onboarding.next' defaultMessage='Next' />
      </button>
    ) : (
      <button
        onClick={this.handleClose}
        className='onboarding-modal__nav onboarding-modal__done'
      >
        <FormattedMessage id='onboarding.done' defaultMessage='Done' />
      </button>
    );

    return (

        <ReactSwipeableViews index={currentIndex} onChangeIndex={this.handleSwipe} className='onboarding-modal__pager'>
          {pages.map((page, i) => {
            const className = classNames('onboarding-modal__page__wrapper', {
              'onboarding-modal__page__wrapper--active': i === currentIndex,
            });
            return (

{page}

            );
          })}
        </ReactSwipeableViews>

            <button
              onClick={this.handleSkip}
              className='onboarding-modal__nav onboarding-modal__skip'
            >
              <FormattedMessage id='onboarding.skip' defaultMessage='Skip' />
            </button>

            {pages.map((_, i) => {
              const className = classNames('onboarding-modal__dot', {
                active: i === currentIndex,
              });
              return (
                <div
                  key={`dot-${i}`}
                  role='button'
                  tabIndex='0'
                  data-index={i}
                  onClick={this.handleDot}
                  className={className}
                />
              );
            })}

            {nextOrDoneBtn}

    );
  }

}
<|endoftext|>


<commit_before>/* File: profileColoration.js
 *
 * Copyright (c) 2013-2016
 * Centre National d’Enseignement à Distance (Cned), Boulevard Nicephore Niepce, 86360 CHASSENEUIL-DU-POITOU, France
 * (direction-innovation@cned.fr)
 *
 * GNU Affero General Public License (AGPL) version 3.0 or later version
 *
 * This file is part of a program which is free software: you can
 * redistribute it and/or modify it under the terms of the
 * GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public
 * License along with this program.
 * If not, see <http://www.gnu.org/licenses/>.
 *
 */

/*global cnedApp,Hyphenator, $:false */
'use strict';

/**
 * Directive to set coloration with TextAngular
 */
cnedApp.directive('textAngularProfileColoration',

    function (UtilsService, $timeout, $compile, $rootScope, $log) {
        return {
            restrict: 'A',
            link: function (scope, element) {

                var htmlWatcher = null;

                /**
                 * Bind the watcher to detect change in the editor
                 */
                var bindHtmlWatcher = function () {
                    if (!htmlWatcher) {
                        $log.debug('bind html watcher');
                        htmlWatcher = $rootScope.$watch(function () {
                            return element.html().length;
                        }, function (newvalue, oldValue) {

                            if (newvalue != oldValue) {
                                generateColoration();
                            }

                        });
                    }
                };

                /**
                 * Color lines
                 * @param ref
                 * @param maxLines
                 */
                var colorLines = function (ref, maxLines) {
                    var prevTop = -9999;
                    var line = 0;
                    angular.forEach(ref.find('span'), function (word) {
                        var wordRef = angular.element(word);

                        if (!wordRef[0].id) {
                            var top = wordRef[0].offsetTop;

                            if (top > prevTop) {
                                if (line >= maxLines) {
                                    line = 1;
                                } else {
                                    line++;
                                }
                            }

                            wordRef.addClass('line' + line);

                            prevTop = top;
                        }

                    });
                };

                var generateColoration = function () {

                    if (htmlWatcher) {
                        htmlWatcher();
                        htmlWatcher = null;

                        $log.debug('destroy html watcher', htmlWatcher);
                    }

                    $timeout(function () {

                        var profile = $rootScope.currentProfile.data;
                        var text = element.html();

                        if (profile && text) {

                            for (var i = 0; i < profile.profileTags.length; i++) {

                                var coloration = profile.profileTags[i].coloration;

                                angular.forEach(element.find(profile.profileTags[i].tagDetail.balise), function (elem) {

                                    var savedSel = rangy.saveSelection();

                                    var ref = angular.element(elem);
                                    var textTransform = ref.html();

                                    $log.debug('html span', textTransform);

                                    var rangyCursorPattern = /(( )*)/gi;

                                    // Save rangy cursor

                                    var rangyCursorResult = textTransform.match(rangyCursorPattern);
                                    var rangyCursors = [];

                                    if (rangyCursorResult && rangyCursorResult.length > 0) {
                                        for (var i = 0; i < rangyCursorResult.length; i++) {
                                            var marker = '';

                                            if (coloration === 'Colorer les syllabes') {
                                                marker = '%%RG' + i + '%%';
                                            } else {
                                                marker = '%%RG' + i + '%%';
                                            }

                                            rangyCursors.push({
                                                marker: marker,
                                                cursor: rangyCursorResult[i]
                                            });

                                            textTransform = textTransform.replace(rangyCursorResult[i], '%%RG' + i + '%%');
                                        }
                                    }
                                    textTransform = textTransform.replace(/ /gi, ' %%NB%% ');
                                    textTransform = UtilsService.removeSpan(textTransform);

                                    $log.debug('remove span ', textTransform);

                                    // Split Text
                                    switch (coloration) {
                                        case 'Colorer les lignes RBV':
                                        case 'Colorer les lignes RVJ':
                                        case 'Surligner les lignes RBV':
                                        case 'Surligner les lignes RVJ':
                                        case 'Colorer les lignes RBVJ':
                                        case 'Surligner les lignes RBVJ':

                                            textTransform = UtilsService.splitOnWordWithSpace(textTransform);
                                            textTransform = textTransform.replace(/\s%%NB%%\s<\/span>\s/gi, ' ');
                                            break;
                                        case 'Colorer les mots':
                                        case 'Surligner les mots':

                                            textTransform = UtilsService.splitOnWordWithOutSpace(textTransform);
                                            textTransform = textTransform.replace(/\s\s%%NB%%<\/span>\s\s/gi, ' ');
                                            break;
                                        case 'Colorer les syllabes':

                                            textTransform = UtilsService.splitOnSyllable(textTransform);
                                            textTransform = textTransform.replace(/\s%%NB<\/span>%%\s/gi, ' ');
                                            break;
                                    }

                                    $log.debug('split ', textTransform);

                                    // Restore rangy cursor
                                    for (var i = 0; i < rangyCursors.length; i++) {
                                        textTransform = textTransform.replace(new RegExp(rangyCursors[i].marker, 'gi'), rangyCursors[i].cursor);
                                    }

                                    $log.debug('replace nbsp ', textTransform);

                                    ref.html(textTransform);

                                    switch (coloration) {
                                        case 'Colorer les lignes RBV':
                                        case 'Colorer les lignes RVJ':
                                        case 'Surligner les lignes RBV':
                                        case 'Surligner les lignes RVJ':
                                            colorLines(ref, 3);
                                            break;
                                        case 'Colorer les lignes RBVJ':
                                        case 'Surligner les lignes RBVJ':
                                            colorLines(ref, 4);
                                            break;
                                    }

                                    rangy.restoreSelection(savedSel);

                                });

                            }

                        }
                        bindHtmlWatcher();

                    }, 200);
                };

                /**
                 * Watcher for the current profile
                 */
                $rootScope.$watch('currentProfile', function (newvalue) {

                    $log.debug('change current profile');

                    if (newvalue) {
                        generateColoration();
                    }

                }, true);
            }
        };

    });<commit_msg>Add picture accessidys<commit_after>/* File: profileColoration.js
 *
 * Copyright (c) 2013-2016
 * Centre National d’Enseignement à Distance (Cned), Boulevard Nicephore Niepce, 86360 CHASSENEUIL-DU-POITOU, France
 * (direction-innovation@cned.fr)
 *
 * GNU Affero General Public License (AGPL) version 3.0 or later version
 *
 * This file is part of a program which is free software: you can
 * redistribute it and/or modify it under the terms of the
 * GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public
 * License along with this program.
 * If not, see <http://www.gnu.org/licenses/>.
 *
 */

/*global cnedApp,Hyphenator, $:false */
'use strict';

/**
 * Directive to set coloration with TextAngular
 */
cnedApp.directive('textAngularProfileColoration',

    function (UtilsService, $timeout, $compile, $rootScope, $log) {
        return {
            restrict: 'A',
            link: function (scope, element) {

                var htmlWatcher = null;

                /**
                 * Bind the watcher to detect change in the editor
                 */
                var bindHtmlWatcher = function () {
                    if (!htmlWatcher) {
                        $log.debug('bind html watcher');
                        htmlWatcher = $rootScope.$watch(function () {
                            return element.html().length;
                        }, function (newvalue, oldValue) {

                            if (newvalue != oldValue) {
                                generateColoration();
                            }

                        });
                    }
                };

                /**
                 * Color lines
                 * @param ref
                 * @param maxLines
                 */
                var colorLines = function (ref, maxLines) {
                    var prevTop = -9999;
                    var line = 0;
                    angular.forEach(ref.find('span'), function (word) {
                        var wordRef = angular.element(word);

                        if (!wordRef[0].id) {
                            var top = wordRef[0].offsetTop;

                            if (top > prevTop) {
                                if (line >= maxLines) {
                                    line = 1;
                                } else {
                                    line++;
                                }
                            }

                            wordRef.addClass('line' + line);

                            prevTop = top;
                        }

                    });
                };

                var generateColoration = function () {

                    if (htmlWatcher) {
                        htmlWatcher();
                        htmlWatcher = null;

                        $log.debug('destroy html watcher', htmlWatcher);
                    }

                    $timeout(function () {

                        var profile = $rootScope.currentProfile.data;
                        var text = element.html();

                        if (profile && text) {

                            for (var i = 0; i < profile.profileTags.length; i++) {

                                var coloration = profile.profileTags[i].coloration;

                                angular.forEach(element.find(profile.profileTags[i].tagDetail.balise), function (elem) {

                                    var savedSel = rangy.saveSelection();

                                    var ref = angular.element(elem);
                                    var textTransform = ref.html();

                                    $log.debug('html span', textTransform);

                                    var rangyCursorPattern = /(( )*)/gi;

                                    // Save rangy cursor

                                    var rangyCursorResult = textTransform.match(rangyCursorPattern);
                                    var rangyCursors = [];

                                    if (rangyCursorResult && rangyCursorResult.length > 0) {
                                        for (var i = 0; i < rangyCursorResult.length; i++) {
                                            var marker = '';

                                            if (coloration === 'Colorer les syllabes') {
                                                marker = '%%RG' + i + '%%';
                                            } else {
                                                marker = '%%RG' + i + '%%';
                                            }

                                            rangyCursors.push({
                                                marker: marker,
                                                cursor: rangyCursorResult[i]
                                            });

                                            textTransform = textTransform.replace(rangyCursorResult[i], '%%RG' + i + '%%');
                                        }
                                    }
                                    textTransform = textTransform.replace(/ /gi, ' %%NB%% ');
                                    textTransform = UtilsService.removeSpan(textTransform);

                                    $log.debug('remove span ', textTransform);

                                    // Split Text
                                    switch (coloration) {
                                        case 'Colorer les lignes RBV':
                                        case 'Colorer les lignes RVJ':
                                        case 'Surligner les lignes RBV':
                                        case 'Surligner les lignes RVJ':
                                        case 'Colorer les lignes RBVJ':
                                        case 'Surligner les lignes RBVJ':

                                            textTransform = UtilsService.splitOnWordWithSpace(textTransform);
                                            textTransform = textTransform.replace(/\s%%NB%%\s<\/span>\s/gi, ' ');
                                            break;
                                        case 'Colorer les mots':
                                        case 'Surligner les mots':

                                            textTransform = UtilsService.splitOnWordWithOutSpace(textTransform);
                                            textTransform = textTransform.replace(/\s\s%%NB%%<\/span>\s\s/gi, ' ');
                                            break;
                                        case 'Colorer les syllabes':

                                            textTransform = UtilsService.splitOnSyllable(textTransform);
                                            textTransform = textTransform.replace(/\s%%NB<\/span>%%\s/gi, ' ');
                                            break;
                                        default:
                                            textTransform = textTransform.replace(/\s%%NB%%\s/gi, ' ');
                                            break;
                                    }

                                    $log.debug('split ', textTransform);

                                    // Restore rangy cursor
                                    for (var i = 0; i < rangyCursors.length; i++) {
                                        textTransform = textTransform.replace(new RegExp(rangyCursors[i].marker, 'gi'), rangyCursors[i].cursor);
                                    }

                                    $log.debug('replace nbsp ', textTransform);

                                    ref.html(textTransform);

                                    switch (coloration) {
                                        case 'Colorer les lignes RBV':
                                        case 'Colorer les lignes RVJ':
                                        case 'Surligner les lignes RBV':
                                        case 'Surligner les lignes RVJ':
                                            colorLines(ref, 3);
                                            break;
                                        case 'Colorer les lignes RBVJ':
                                        case 'Surligner les lignes RBVJ':
                                            colorLines(ref, 4);
                                            break;
                                    }

                                    rangy.restoreSelection(savedSel);

                                });

                            }

                        }
                        bindHtmlWatcher();

                    }, 200);
                };

                /**
                 * Watcher for the current profile
                 */
                $rootScope.$watch('currentProfile', function (newvalue) {

                    $log.debug('change current profile');

                    if (newvalue) {
                        generateColoration();
                    }

                }, true);
            }
        };

    });<|endoftext|>


<commit_before>import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { c } from 'ttag';
import {
    FormModal,
    useLoading,
    useEventManager,
    useApi,
    useNotifications,
    Loader,
    SimpleTabs,
    useGetCalendarBootstrap,
    useGetAddresses,
    useGetAddressKeys
} from 'react-components';

import { createCalendar, updateCalendarSettings, updateCalendar } from 'proton-shared/lib/api/calendars';
import { getPrimaryKey } from 'proton-shared/lib/keys/keys';

import { DEFAULT_CALENDAR, DEFAULT_EVENT_DURATION, NOTIFICATION_TYPE } from '../../constants';
import CalendarSettingsTab from './CalendarSettingsTab';
import EventSettingsTab from './EventSettingsTab';
import {
    notificationsToModel,
    modelToNotifications,
    DEFAULT_FULL_DAY_NOTIFICATION,
    DEFAULT_FULL_DAY_NOTIFICATIONS,
    DEFAULT_PART_DAY_NOTIFICATION,
    DEFAULT_PART_DAY_NOTIFICATIONS
} from '../../helpers/notifications';
import { setupCalendarKeys } from '../setup/resetHelper';

const validate = ({ name }) => {
    const errors = {};

    if (!name) {
        errors.name = c('Error').t`Name required`;
    }

    return errors;
};

const CalendarModal = ({ calendar, ...rest }) => {
    const api = useApi();
    const { call } = useEventManager();
    const getAddresses = useGetAddresses();
    const getCalendarBootstrap = useGetCalendarBootstrap();
    const getAddressKeys = useGetAddressKeys();
    const [loadingSetup, withLoading] = useLoading(true);
    const [loadingAction, withLoadingAction] = useLoading();
    const { createNotification } = useNotifications();
    const [isSubmitted, setIsSubmitted] = useState(false);

    const isEdit = !!calendar;

    const title = isEdit ? c('Title').t`Update calendar` : c('Title').t`Create calendar`;

    const [model, setModel] = useState(() => ({
        name: '',
        description: '',
        color: DEFAULT_CALENDAR.color,
        display: true,
        addressOptions: [],
        duration: DEFAULT_EVENT_DURATION,
        defaultPartDayNotification: DEFAULT_PART_DAY_NOTIFICATION,
        defaultFullDayNotification: DEFAULT_FULL_DAY_NOTIFICATION,
        partDayNotifications: notificationsToModel(DEFAULT_PART_DAY_NOTIFICATIONS, false),
        fullDayNotifications: notificationsToModel(DEFAULT_FULL_DAY_NOTIFICATIONS, true)
    }));

    useEffect(() => {
        const initializeEmptyCalendar = async () => {
            const addresses = await getAddresses();

            setModel((prev) => ({
                ...prev,
                addressID: addresses[0].ID,
                addressOptions: addresses.map(({ ID, Email = '' }) => ({ value: ID, text: Email }))
            }));
        };

        const initializeCalendar = async () => {
            const [{ Members, CalendarSettings }, addresses] = await Promise.all([
                getCalendarBootstrap(calendar.ID),
                getAddresses()
            ]);

            const [{ Email: memberEmail } = {}] = Members;
            const { ID: addressID } = addresses.find(({ Email }) => memberEmail === Email) || {};

            if (!addressID) {
                rest.onClose();
                return createNotification({ text: c('Error').t`Member address not found`, type: 'error' });
            }

            const {
                DefaultPartDayNotifications = DEFAULT_PART_DAY_NOTIFICATIONS,
                DefaultFullDayNotifications = DEFAULT_FULL_DAY_NOTIFICATIONS,
                DefaultEventDuration = DEFAULT_EVENT_DURATION
            } = CalendarSettings;

            const partDayNotifications = notificationsToModel(DefaultPartDayNotifications, false);
            const fullDayNotifications = notificationsToModel(DefaultFullDayNotifications, true);

            // Filter out any email notifications because they are currently not supported.
            const devicePartDayNotifications = partDayNotifications.filter(
                ({ type }) => type === NOTIFICATION_TYPE.DEVICE
            );
            const deviceFullDayNotifications = fullDayNotifications.filter(
                ({ type }) => type === NOTIFICATION_TYPE.DEVICE
            );

            const emailPartDayNotifications = partDayNotifications.filter(
                ({ type }) => type === NOTIFICATION_TYPE.EMAIL
            );
            const emailFullDayNotifications = fullDayNotifications.filter(
                ({ type }) => type === NOTIFICATION_TYPE.EMAIL
            );

            setModel((prev) => ({
                ...prev,
                calendarID: calendar.ID,
                name: calendar.Name,
                display: calendar.Display,
                description: calendar.Description,
                color: calendar.Color,
                addressID,
                addressOptions: addresses.map(({ ID, Email = '' }) => ({ value: ID, text: Email })),
                duration: DefaultEventDuration,
                partDayNotifications: devicePartDayNotifications,
                fullDayNotifications: deviceFullDayNotifications,
                _emailPartDayNotifications: emailPartDayNotifications,
                _emailFullDayNotifications: emailFullDayNotifications
            }));
        };

        const promise = calendar ? initializeCalendar() : initializeEmptyCalendar();

        withLoading(
            promise.catch(() => {
                // Just close if it failed to load.
                rest.onClose();
            })
        );
    }, []);

    const handleCreateCalendar = async (addressID, calendarPayload) => {
        const [addresses, addressKeys] = await Promise.all([getAddresses(), getAddressKeys(addressID)]);

        const { Email: primaryAddressEmail = '' } = addresses.find(({ ID }) => ID === addressID) || {};
        const { privateKey: primaryAddressKey, publicKey: primaryAddressPublicKey } = getPrimaryKey(addressKeys) || {};
        if (!primaryAddressKey || !primaryAddressKey.isDecrypted()) {
            createNotification({ text: c('Error').t`Primary address key is not decrypted.`, type: 'error' });
            return;
        }

        const { Calendar = {} } = await api(
            createCalendar({
                ...calendarPayload,
                AddressID: addressID
            })
        );

        await setupCalendarKeys({
            api,
            calendars: [Calendar],
            addressID,
            addressEmail: primaryAddressEmail,
            privateKey: primaryAddressKey,
            publicKey: primaryAddressPublicKey
        });

        return Calendar.ID;
    };

    const handleUpdateCalendar = async (calendarPayload) => {
        await api(updateCalendar(calendar.ID, calendarPayload));
        return calendar.ID;
    };

    const formattedModel = {
        ...model,
        name: model.name.trim(),
        description: model.description.trim()
    };

    const errors = validate(formattedModel);

    const handleSubmit = async () => {
        setIsSubmitted(true);
        if (Object.keys(errors).length > 0) {
            return;
        }

        const calendarPayload = {
            Name: formattedModel.name,
            Color: formattedModel.color,
            Display: +formattedModel.display,
            Description: formattedModel.description
        };

        const actualCalendarID = calendar
            ? await handleUpdateCalendar(calendarPayload)
            : await handleCreateCalendar(formattedModel.addressID, calendarPayload);

        const {
            duration,
            fullDayNotifications,
            partDayNotifications,
            _emailPartDayNotifications = [],
            _emailFullDayNotifications = []
        } = formattedModel;

        const calendarSettingsData = {
            DefaultEventDuration: +duration,
            DefaultFullDayNotifications: modelToNotifications(fullDayNotifications.concat(_emailFullDayNotifications)),
            DefaultPartDayNotifications: modelToNotifications(partDayNotifications.concat(_emailPartDayNotifications))
        };

        await api(updateCalendarSettings(actualCalendarID, calendarSettingsData));
        await call();

        rest.onClose();
        createNotification({
            text: calendar ? c('Success').t`Calendar updated` : c('Success').t`Calendar created`
        });
    };

    const tabs = [
        {
            title: c('Header').t`Calendar settings`,
            content: (
                <CalendarSettingsTab
                    isSubmitted={isSubmitted}
                    errors={errors}
                    model={model}
                    setModel={setModel}
                    onClose={rest.onClose}
                />
            )
        },
        {
            title: c('Header').t`Event settings`,
            content: <EventSettingsTab isSubmitted={isSubmitted} errors={errors} model={model} setModel={setModel} />
        }
    ];

    return (
        <FormModal
            className="pm-modal--shorterLabels w100"
            title={title}
            submit={calendar ? c('Action').t`Update` : c('Action').t`Create`}
            onSubmit={() => withLoadingAction(handleSubmit())}
            loading={loadingSetup || loadingAction}
            {...rest}
        >
            {loadingSetup ? <Loader /> : <SimpleTabs tabs={tabs} />}
        </FormModal>
    );
};

CalendarModal.propTypes = {
    calendar: PropTypes.object,
    members: PropTypes.arrayOf(PropTypes.object)
};

export default CalendarModal;
<commit_msg>CALENDAR-535 - Remove disabled addresses from calendar creation selection<commit_after>import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { c } from 'ttag';
import {
    FormModal,
    useLoading,
    useEventManager,
    useApi,
    useNotifications,
    Loader,
    SimpleTabs,
    useGetCalendarBootstrap,
    useGetAddresses,
    useGetAddressKeys
} from 'react-components';

import { createCalendar, updateCalendarSettings, updateCalendar } from 'proton-shared/lib/api/calendars';
import { getPrimaryKey } from 'proton-shared/lib/keys/keys';
import { ADDRESS_STATUS } from 'proton-shared/lib/constants';

import { DEFAULT_CALENDAR, DEFAULT_EVENT_DURATION, NOTIFICATION_TYPE } from '../../constants';
import CalendarSettingsTab from './CalendarSettingsTab';
import EventSettingsTab from './EventSettingsTab';
import {
    notificationsToModel,
    modelToNotifications,
    DEFAULT_FULL_DAY_NOTIFICATION,
    DEFAULT_FULL_DAY_NOTIFICATIONS,
    DEFAULT_PART_DAY_NOTIFICATION,
    DEFAULT_PART_DAY_NOTIFICATIONS
} from '../../helpers/notifications';
import { setupCalendarKeys } from '../setup/resetHelper';

const validate = ({ name }) => {
    const errors = {};

    if (!name) {
        errors.name = c('Error').t`Name required`;
    }

    return errors;
};

const CalendarModal = ({ calendar, ...rest }) => {
    const api = useApi();
    const { call } = useEventManager();
    const getAddresses = useGetAddresses();
    const getCalendarBootstrap = useGetCalendarBootstrap();
    const getAddressKeys = useGetAddressKeys();
    const [loadingSetup, withLoading] = useLoading(true);
    const [loadingAction, withLoadingAction] = useLoading();
    const { createNotification } = useNotifications();
    const [isSubmitted, setIsSubmitted] = useState(false);

    const isEdit = !!calendar;

    const title = isEdit ? c('Title').t`Update calendar` : c('Title').t`Create calendar`;

    const [model, setModel] = useState(() => ({
        name: '',
        description: '',
        color: DEFAULT_CALENDAR.color,
        display: true,
        addressOptions: [],
        duration: DEFAULT_EVENT_DURATION,
        defaultPartDayNotification: DEFAULT_PART_DAY_NOTIFICATION,
        defaultFullDayNotification: DEFAULT_FULL_DAY_NOTIFICATION,
        partDayNotifications: notificationsToModel(DEFAULT_PART_DAY_NOTIFICATIONS, false),
        fullDayNotifications: notificationsToModel(DEFAULT_FULL_DAY_NOTIFICATIONS, true)
    }));

    useEffect(() => {
        const initializeEmptyCalendar = async () => {
            const addresses = await getAddresses();
            const filteredAddresses = addresses.filter(({ Status }) => {
                return Status === ADDRESS_STATUS.STATUS_ENABLED;
            });

            if (filteredAddresses.length === 0) {
                rest.onClose();
                return createNotification({ text: c('Error').t`No valid addresses found`, type: 'error' });
            }

            setModel((prev) => ({
                ...prev,
                addressID: filteredAddresses[0].ID,
                addressOptions: filteredAddresses.map(({ ID, Email = '' }) => ({ value: ID, text: Email }))
            }));
        };

        const initializeCalendar = async () => {
            const [{ Members, CalendarSettings }, addresses] = await Promise.all([
                getCalendarBootstrap(calendar.ID),
                getAddresses()
            ]);

            const [{ Email: memberEmail } = {}] = Members;
            const { ID: addressID } = addresses.find(({ Email }) => memberEmail === Email) || {};

            if (!addressID) {
                rest.onClose();
                return createNotification({ text: c('Error').t`Member address not found`, type: 'error' });
            }

            const {
                DefaultPartDayNotifications = DEFAULT_PART_DAY_NOTIFICATIONS,
                DefaultFullDayNotifications = DEFAULT_FULL_DAY_NOTIFICATIONS,
                DefaultEventDuration = DEFAULT_EVENT_DURATION
            } = CalendarSettings;

            const partDayNotifications = notificationsToModel(DefaultPartDayNotifications, false);
            const fullDayNotifications = notificationsToModel(DefaultFullDayNotifications, true);

            // Filter out any email notifications because they are currently not supported.
            const devicePartDayNotifications = partDayNotifications.filter(
                ({ type }) => type === NOTIFICATION_TYPE.DEVICE
            );
            const deviceFullDayNotifications = fullDayNotifications.filter(
                ({ type }) => type === NOTIFICATION_TYPE.DEVICE
            );

            const emailPartDayNotifications = partDayNotifications.filter(
                ({ type }) => type === NOTIFICATION_TYPE.EMAIL
            );
            const emailFullDayNotifications = fullDayNotifications.filter(
                ({ type }) => type === NOTIFICATION_TYPE.EMAIL
            );

            setModel((prev) => ({
                ...prev,
                calendarID: calendar.ID,
                name: calendar.Name,
                display: calendar.Display,
                description: calendar.Description,
                color: calendar.Color,
                addressID,
                addressOptions: addresses.map(({ ID, Email = '' }) => ({ value: ID, text: Email })),
                duration: DefaultEventDuration,
                partDayNotifications: devicePartDayNotifications,
                fullDayNotifications: deviceFullDayNotifications,
                _emailPartDayNotifications: emailPartDayNotifications,
                _emailFullDayNotifications: emailFullDayNotifications
            }));
        };

        const promise = calendar ? initializeCalendar() : initializeEmptyCalendar();

        withLoading(
            promise.catch(() => {
                // Just close if it failed to load.
                rest.onClose();
            })
        );
    }, []);

    const handleCreateCalendar = async (addressID, calendarPayload) => {
        const [addresses, addressKeys] = await Promise.all([getAddresses(), getAddressKeys(addressID)]);

        const { Email: primaryAddressEmail = '' } = addresses.find(({ ID }) => ID === addressID) || {};
        const { privateKey: primaryAddressKey, publicKey: primaryAddressPublicKey } = getPrimaryKey(addressKeys) || {};
        if (!primaryAddressKey || !primaryAddressKey.isDecrypted()) {
            createNotification({ text: c('Error').t`Primary address key is not decrypted.`, type: 'error' });
            return;
        }

        const { Calendar = {} } = await api(
            createCalendar({
                ...calendarPayload,
                AddressID: addressID
            })
        );

        await setupCalendarKeys({
            api,
            calendars: [Calendar],
            addressID,
            addressEmail: primaryAddressEmail,
            privateKey: primaryAddressKey,
            publicKey: primaryAddressPublicKey
        });

        return Calendar.ID;
    };

    const handleUpdateCalendar = async (calendarPayload) => {
        await api(updateCalendar(calendar.ID, calendarPayload));
        return calendar.ID;
    };

    const formattedModel = {
        ...model,
        name: model.name.trim(),
        description: model.description.trim()
    };

    const errors = validate(formattedModel);

    const handleSubmit = async () => {
        setIsSubmitted(true);
        if (Object.keys(errors).length > 0) {
            return;
        }

        const calendarPayload = {
            Name: formattedModel.name,
            Color: formattedModel.color,
            Display: +formattedModel.display,
            Description: formattedModel.description
        };

        const actualCalendarID = calendar
            ? await handleUpdateCalendar(calendarPayload)
            : await handleCreateCalendar(formattedModel.addressID, calendarPayload);

        const {
            duration,
            fullDayNotifications,
            partDayNotifications,
            _emailPartDayNotifications = [],
            _emailFullDayNotifications = []
        } = formattedModel;

        const calendarSettingsData = {
            DefaultEventDuration: +duration,
            DefaultFullDayNotifications: modelToNotifications(fullDayNotifications.concat(_emailFullDayNotifications)),
            DefaultPartDayNotifications: modelToNotifications(partDayNotifications.concat(_emailPartDayNotifications))
        };

        await api(updateCalendarSettings(actualCalendarID, calendarSettingsData));
        await call();

        rest.onClose();
        createNotification({
            text: calendar ? c('Success').t`Calendar updated` : c('Success').t`Calendar created`
        });
    };

    const tabs = [
        {
            title: c('Header').t`Calendar settings`,
            content: (
                <CalendarSettingsTab
                    isSubmitted={isSubmitted}
                    errors={errors}
                    model={model}
                    setModel={setModel}
                    onClose={rest.onClose}
                />
            )
        },
        {
            title: c('Header').t`Event settings`,
            content: <EventSettingsTab isSubmitted={isSubmitted} errors={errors} model={model} setModel={setModel} />
        }
    ];

    return (
        <FormModal
            className="pm-modal--shorterLabels w100"
            title={title}
            submit={calendar ? c('Action').t`Update` : c('Action').t`Create`}
            onSubmit={() => withLoadingAction(handleSubmit())}
            loading={loadingSetup || loadingAction}
            {...rest}
        >
            {loadingSetup ? <Loader /> : <SimpleTabs tabs={tabs} />}
        </FormModal>
    );
};

CalendarModal.propTypes = {
    calendar: PropTypes.object,
    members: PropTypes.arrayOf(PropTypes.object)
};

export default CalendarModal;
<|endoftext|>


<commit_before>import $ from 'jquery';
import {computed, setProperties, set, get} from '@ember/object';
import {isBlank, isNone} from '@ember/utils';
import {run} from '@ember/runloop';
import {warn, assert} from '@ember/debug';
import ModelsTable from './models-table';
import layout from '../templates/components/models-table';

const {extend} = $;

/**
 * Table-component with pagination, sorting and filtering.
 *
 * It should be used when pagination, filtering and sorting are done on the server-side. Otherwise [models-table](Components.ModelsTable.html) should be used.
 *
 * This component extends the base models-table component. For the end user, it can be used (nearly) the same:
 *
 * ```hbs
 * {{models-table-server-paginated data=data columns=columns}}
 * ```
 *
 * Usage with block context:
 *
 * ```hbs
 * {{#models-table-server-paginated data=data columns=columns as |mt|}}
 *   {{mt.global-filter}}
 *   {{mt.columns-dropdown}}
 *   {{mt.table}}
 *   {{mt.footer}}
 * {{/models-table}}
 * ```
 *
 * ModelsTableServerPaginated yields references to the following contextual components:
 *
 * * [models-table/global-filter](Components.ModelsTableGlobalFilter.html) - global filter used for table data
 * * [models-table/columns-dropdown](Components.ModelsTableColumnsDropdown.html) - dropdown with list of options to toggle columns and column-sets visibility
 * * [models-table/table](Components.ModelsTableTable.html) - table with a data
 * * [models-table/footer](Components.ModelsTableFooter.html) - summary and pagination
 *
 * Check own docs for each component to get detailed info.
 *
 * ModelsTableServerPaginated has a lot of options you may configure, but there are two required properties called `data` and `columns`. First one contains data-query:
 *
 * ```js
 * model: function() {
 *  return this.store.query('my-model', {});
 * }
 * ```
 *
 * It will then take this query and extend it with pagination, sorting and filtering information. All other query parameters added in will remain untouched. Everything else works exactly the same - global filters, column filters etc. still use the same properties to control them. A few things to notice:
 *
 * - When using `filterWithSelect` for a column, you must use `predefinedFilterOptions`, because the automatic loading of possible filter values cannot work here.
 * - There is a new optional field `filteredBy` for columns, which works much like `sortedBy`: if set, this field will be used as query parameter, otherwise it will use the `propertyName`.
 * - Sorting will not use multipleColumnSorting, it will only sort by one column.
 * - If you set `sortedBy: false` on a column, sorting will be disabled for this column.
 *
 * There are a couple of things which can be configured to adapt to your API:
 *
 * ``` js
 * // The property on meta to load the pages count from.
 * metaPagesCountProperty: 'pagesCount',
 *
 * // The property on meta to load the total item count from.
 * metaItemsCountProperty: 'itemsCount',
 *
 * // The time to wait until new data is actually loaded.
 * // This can be tweaked to avoid making too many server requests.
 * debounceDataLoadTime: 500,
 *
 * // The query parameters to use for server side filtering / querying.
 * filterQueryParameters: {
 *   globalFilter: 'search',
 *   sort: 'sort',
 *   sortDirection: 'sortDirection',
 *   page: 'page',
 *   pageSize: 'pageSize'
 * },
 ```
 *
 * This default configuration would try to get the total page count from `model.get('meta.pagesCount')` and the total item count from `model.get('meta.itemsCount')`, and would then go on to build the following query:
 *
 * ``` js
 * columns: [
 *   {
 *     propertyName: 'name',
 *     filteredBy: 'model_name'
 *   }
 * ]
 *
 * // after searching globally for "searchtexthere"
 * // and in the name column for "filterforname",
 * // and going to page 2,
 * // the following query would be built:
 * ?page=2&pageSize=50&search=searchtexthere&sort=name&sortDirection=ASC&model_name=filterforname
 * ```
 *
 * @class ModelsTableServerPaginated
 * @namespace Components
 * @extends Components.ModelsTable
 */
export default ModelsTable.extend({

  layout,

  /**
   * True if data is currently being loaded from the server.
   * Can be used in the template to e.g. display a loading spinner.
   *
   * @type boolean
   * @property isLoading
   * @default false
   * @private
   */
  isLoading: false,

  /**
   * True if last data query promise has been rejected.
   * Can be used in the template to e.g. indicate stale data or to e.g. show error state.
   *
   * @type boolean
   * @property isError
   * @default false
   * @private
   */
  isError: false,

  /**
   * The property on meta to load the pages count from.
   *
   * @type string
   * @property metaPagesCountProperty
   * @default 'pagesCount'
   */
  metaPagesCountProperty: 'pagesCount',
  /**
   * The property on meta to load the total item count from.
   *
   * @type {string}
   * @property metaItemsCountProperty
   * @default 'itemsCount'
   */
  metaItemsCountProperty: 'itemsCount',

  /**
   * The time to wait until new data is actually loaded.
   * This can be tweaked to avoid making too many server requests.
   *
   * @type number
   * @property debounceDataLoadTime
   * @default 500
   */
  debounceDataLoadTime: 500,

  /**
   * The query parameters to use for server side filtering / querying.
   *
   * @type object
   * @property filterQueryParameters
   */
  filterQueryParameters: {
    globalFilter: 'search',
    sort: 'sort',
    sortDirection: 'sortDirection',
    page: 'page',
    pageSize: 'pageSize'
  },

  /**
   * @property observedProperties
   * @type string[]
   * @private
   */
  observedProperties: ['currentPageNumber', 'sortProperties.[]', 'pageSize', 'filterString', 'processedColumns.@each.filterString'],

  /**
   * This is set during didReceiveAttr and whenever the page/filters change.
   *
   * @override
   * @property filteredContent
   * @default []
   * @private
   * @type object[]
   */
  filteredContent: [],

  /**
   * For server side filtering, visibleContent is same as the filtered content
   *
   * @override
   * @property visibleContent
   * @private
   * @type object[]
   */
  visibleContent: computed.alias('arrangedContent'),

  /**
   * For server side filtering, arrangedContent is same as the filtered content
   *
   * @override
   * @property arrangedContent
   * @private
   * @type object[]
   */
  arrangedContent: computed.alias('filteredContent'),

  /**
   * Can't be used within `models-table-server-paginated`. Back-end determines how to filter data
   *
   * @override
   * @property filteringIgnoreCase
   */
  filteringIgnoreCase: computed({
    set() {
      assert(`"filteringIgnoreCase" can't be used with "models-table-server-paginated"`, false);
    },
    get() {
      return undefined;
    }
  }),

  /**
   * Can't be used within `models-table-server-paginated`. Back-end determines how to filter data
   *
   * @override
   * @property doFilteringByHiddenColumns
   */
  doFilteringByHiddenColumns: computed({
    set() {
      assert(`"doFilteringByHiddenColumns" can't be used with "models-table-server-paginated"`, false);
    },
    get() {
      return undefined;
    }
  }),

  /**
   * The total content length is get from the meta information.
   * Set metaItemsCountProperty to change from which meta property this is loaded.
   *
   * @override
   * @type number
   * @property arrangedContentLength
   * @private
   */
  arrangedContentLength: computed('filteredContent.meta', function () {
    let itemsCountProperty = get(this, 'metaItemsCountProperty');
    let meta = get(this, 'filteredContent.meta') || {};
    return get(meta, itemsCountProperty) || 0;
  }),

  /**
   * The pages count is get from the meta information.
   * Set metaPagesCountProperty to change from which meta property this is loaded.
   *
   * @type number
   * @property pagesCount
   * @override
   * @private
   */
  pagesCount: computed('filteredContent.meta', function () {
    let pagesCountProperty = get(this, 'metaPagesCountProperty');
    let meta = get(this, 'filteredContent.meta') || {};
    return get(meta, pagesCountProperty) || 1;
  }),

  /**
   * The index of the last item that is currently being shown.
   *
   * @type number
   * @property lastIndex
   * @override
   * @private
   */
  lastIndex: computed('pageSize', 'currentPageNumber', 'arrangedContentLength', function () {
    let pageMax = parseInt(get(this, 'pageSize'), 10) * get(this, 'currentPageNumber');
    let itemsCount = get(this, 'arrangedContentLength');
    return Math.min(pageMax, itemsCount);
  }),

  /**
   * This function actually loads the data from the server.
   * It takes the store, modelName and query from the passed in data-object and adds page, sorting & filtering to it.
   *
   * @returns {Promise}
   * @method _loadData
   * @private
   */
  _loadData: function () {
    let data = get(this, 'data');
    let currentPageNumber = get(this, 'currentPageNumber');
    let pageSize = get(this, 'pageSize');
    let columns = get(this, 'processedColumns');
    let sortProperties = get(this, 'sortProperties');
    let filterString = get(this, 'filterString');

    if (!get(data, 'query')) {
      warn('You must use http://emberjs.com/api/data/classes/DS.Store.html#method_query for loading data');
      return;
    }
    let query = extend({}, get(data, 'query'));
    let store = get(data, 'store');
    let modelName = get(data, 'type.modelName');

    // Add pagination information
    query[get(this, 'filterQueryParameters.page')] = currentPageNumber;
    query[get(this, 'filterQueryParameters.pageSize')] = pageSize;
    // Add sorting information
    let sort = sortProperties && get(sortProperties, 'length') ? sortProperties[0] : null;
    if (sort) {
      let [sortBy, sortDirection] = sort.split(':');
      query = this.sortingWrapper(query, sortBy, sortDirection.toUpperCase());
    } else {
      delete query[[get(this, 'filterQueryParameters.sort')]];
      delete query[[get(this, 'filterQueryParameters.sortDirection')]];
    }

    // Add global filter
    let globalFilter = get(this, 'filterQueryParameters.globalFilter');
    if (filterString) {
      query[globalFilter] = filterString;
    } else {
      delete query[globalFilter];
    }

    // Add per-column filter
    if (get(this, 'useFilteringByColumns')) {
      columns.forEach(column => {
        let filter = get(column, 'filterString');
        let filterTitle = this.getCustomFilterTitle(column);
        this._setQueryFilter(query, column, filterTitle, filter);
      });
    }

    setProperties(this, {isLoading: true, isError: false});
    return this.doQuery(store, modelName, query)
      .then(() =>  setProperties(this, {isLoading: false, isError: false}))
      .catch(() => setProperties(this, {isLoading: false, isError: true}));
  },

  /**
   * Do query-request to load new data
   *
   * You may override this method to add some extra behavior or even additional requests
   *
   * @method doQuery
   * @param {object} store
   * @param {string} modelName
   * @param {object} query
   * @returns {Promise}
   */
  doQuery(store, modelName, query) {
    return store.query(modelName, query).then(newData => set(this, 'filteredContent', newData));
  },

  /**
   * Actually set the filter on a query.
   * This can be overwritten for special case handling.
   * Note that this will mutate the given query object!
   *
   * @param {object} query the query to mutate
   * @param {object} column the column that is filtering
   * @param {string} filterTitle the query param name for filtering
   * @param {mixed} filter the actual filter value
   * @returns {undefined}
   * @method _setQueryFilter
   * @private
   */
  _setQueryFilter(query, column, filterTitle, filter) {
    if (!isBlank(filter)) {
      query[filterTitle] = filter;
    } else {
      delete query[filterTitle];
    }
  },

  /**
   * Wrapper for sorting query
   *
   * @param {object} query parameters
   * @param {string} sortBy
   * @param {string} sortDirection
   * @returns {object} query parameters
   * @method sortingWrapper
   */
  sortingWrapper(query, sortBy, sortDirection) {
    query[get(this, 'filterQueryParameters.sort')] = sortBy;
    query[get(this, 'filterQueryParameters.sortDirection')] = sortDirection;

    return query;
  },

  /**
   * Customize filter title
   *
   * @method getCustomFilterTitle
   * @param {object} column
   * @returns {string} title
   */
  getCustomFilterTitle(column) {
    return get(column, 'filteredBy') || get(column, 'propertyName');
  },

  actions: {

    /**
     * @method actions.gotoNext
     * @returns {undefined}
     */
    gotoNext () {
      if (!get(this, 'gotoForwardEnabled')) {
        return;
      }
      let pagesCount = get(this, 'pagesCount');
      let currentPageNumber = get(this, 'currentPageNumber');
      if (pagesCount > currentPageNumber) {
        this.incrementProperty('currentPageNumber');
      }
    },

    /**
     * @method actions.gotoLast
     * @returns {undefined}
     */
    gotoLast () {
      if (!get(this, 'gotoForwardEnabled')) {
        return;
      }
      let pagesCount = get(this, 'pagesCount');
      set(this, 'currentPageNumber', pagesCount);
    },

    /**
     * @override
     * @method actions.sort
     * @param {ModelsTableColumn} column
     * @returns {undefined}
     */
    sort (column) {
      const sortMap = {
        none: 'asc',
        asc: 'desc',
        desc: 'none'
      };
      let sortedBy = get(column, 'sortedBy') || get(column, 'propertyName');
      if (isNone(sortedBy)) {
        return;
      }
      let currentSorting = get(column, 'sorting');
      let newSorting = sortMap[currentSorting.toLowerCase()];
      let sortingArgs = [column, sortedBy, newSorting];
      this._singleColumnSorting(...sortingArgs);
      this.userInteractionObserver();
    }

  },

  didReceiveAttrs() {
    set(this, 'filteredContent', get(this, 'data'));
  },

  _addPropertyObserver() {
    run.debounce(this, this._loadData, get(this, 'debounceDataLoadTime'));
  },

  willInsertElement() {
    this._super(...arguments);
    let observedProperties = get(this, 'observedProperties');
    observedProperties.forEach(propertyName => this.addObserver(propertyName, this._addPropertyObserver));
  },

  willDestroyElement() {
    this._super(...arguments);

    let observedProperties = get(this, 'observedProperties');
    observedProperties.forEach(propertyName => this.removeObserver(propertyName));
  }
});
<commit_msg>Remove unneeded action handlers<commit_after>import $ from 'jquery';
import {computed, setProperties, set, get} from '@ember/object';
import {isBlank, isNone} from '@ember/utils';
import {run} from '@ember/runloop';
import {warn, assert} from '@ember/debug';
import ModelsTable from './models-table';
import layout from '../templates/components/models-table';

const {extend} = $;

/**
 * Table-component with pagination, sorting and filtering.
 *
 * It should be used when pagination, filtering and sorting are done on the server-side. Otherwise [models-table](Components.ModelsTable.html) should be used.
 *
 * This component extends the base models-table component. For the end user, it can be used (nearly) the same:
 *
 * ```hbs
 * {{models-table-server-paginated data=data columns=columns}}
 * ```
 *
 * Usage with block context:
 *
 * ```hbs
 * {{#models-table-server-paginated data=data columns=columns as |mt|}}
 *   {{mt.global-filter}}
 *   {{mt.columns-dropdown}}
 *   {{mt.table}}
 *   {{mt.footer}}
 * {{/models-table}}
 * ```
 *
 * ModelsTableServerPaginated yields references to the following contextual components:
 *
 * * [models-table/global-filter](Components.ModelsTableGlobalFilter.html) - global filter used for table data
 * * [models-table/columns-dropdown](Components.ModelsTableColumnsDropdown.html) - dropdown with list of options to toggle columns and column-sets visibility
 * * [models-table/table](Components.ModelsTableTable.html) - table with a data
 * * [models-table/footer](Components.ModelsTableFooter.html) - summary and pagination
 *
 * Check own docs for each component to get detailed info.
 *
 * ModelsTableServerPaginated has a lot of options you may configure, but there are two required properties called `data` and `columns`. First one contains data-query:
 *
 * ```js
 * model: function() {
 *  return this.store.query('my-model', {});
 * }
 * ```
 *
 * It will then take this query and extend it with pagination, sorting and filtering information. All other query parameters added in will remain untouched. Everything else works exactly the same - global filters, column filters etc. still use the same properties to control them. A few things to notice:
 *
 * - When using `filterWithSelect` for a column, you must use `predefinedFilterOptions`, because the automatic loading of possible filter values cannot work here.
 * - There is a new optional field `filteredBy` for columns, which works much like `sortedBy`: if set, this field will be used as query parameter, otherwise it will use the `propertyName`.
 * - Sorting will not use multipleColumnSorting, it will only sort by one column.
 * - If you set `sortedBy: false` on a column, sorting will be disabled for this column.
 *
 * There are a couple of things which can be configured to adapt to your API:
 *
 * ``` js
 * // The property on meta to load the pages count from.
 * metaPagesCountProperty: 'pagesCount',
 *
 * // The property on meta to load the total item count from.
 * metaItemsCountProperty: 'itemsCount',
 *
 * // The time to wait until new data is actually loaded.
 * // This can be tweaked to avoid making too many server requests.
 * debounceDataLoadTime: 500,
 *
 * // The query parameters to use for server side filtering / querying.
 * filterQueryParameters: {
 *   globalFilter: 'search',
 *   sort: 'sort',
 *   sortDirection: 'sortDirection',
 *   page: 'page',
 *   pageSize: 'pageSize'
 * },
 ```
 *
 * This default configuration would try to get the total page count from `model.get('meta.pagesCount')` and the total item count from `model.get('meta.itemsCount')`, and would then go on to build the following query:
 *
 * ``` js
 * columns: [
 *   {
 *     propertyName: 'name',
 *     filteredBy: 'model_name'
 *   }
 * ]
 *
 * // after searching globally for "searchtexthere"
 * // and in the name column for "filterforname",
 * // and going to page 2,
 * // the following query would be built:
 * ?page=2&pageSize=50&search=searchtexthere&sort=name&sortDirection=ASC&model_name=filterforname
 * ```
 *
 * @class ModelsTableServerPaginated
 * @namespace Components
 * @extends Components.ModelsTable
 */
export default ModelsTable.extend({

  layout,

  /**
   * True if data is currently being loaded from the server.
   * Can be used in the template to e.g. display a loading spinner.
   *
   * @type boolean
   * @property isLoading
   * @default false
   * @private
   */
  isLoading: false,

  /**
   * True if last data query promise has been rejected.
   * Can be used in the template to e.g. indicate stale data or to e.g. show error state.
   *
   * @type boolean
   * @property isError
   * @default false
   * @private
   */
  isError: false,

  /**
   * The property on meta to load the pages count from.
   *
   * @type string
   * @property metaPagesCountProperty
   * @default 'pagesCount'
   */
  metaPagesCountProperty: 'pagesCount',
  /**
   * The property on meta to load the total item count from.
   *
   * @type {string}
   * @property metaItemsCountProperty
   * @default 'itemsCount'
   */
  metaItemsCountProperty: 'itemsCount',

  /**
   * The time to wait until new data is actually loaded.
   * This can be tweaked to avoid making too many server requests.
   *
   * @type number
   * @property debounceDataLoadTime
   * @default 500
   */
  debounceDataLoadTime: 500,

  /**
   * The query parameters to use for server side filtering / querying.
   *
   * @type object
   * @property filterQueryParameters
   */
  filterQueryParameters: {
    globalFilter: 'search',
    sort: 'sort',
    sortDirection: 'sortDirection',
    page: 'page',
    pageSize: 'pageSize'
  },

  /**
   * @property observedProperties
   * @type string[]
   * @private
   */
  observedProperties: ['currentPageNumber', 'sortProperties.[]', 'pageSize', 'filterString', 'processedColumns.@each.filterString'],

  /**
   * This is set during didReceiveAttr and whenever the page/filters change.
   *
   * @override
   * @property filteredContent
   * @default []
   * @private
   * @type object[]
   */
  filteredContent: [],

  /**
   * For server side filtering, visibleContent is same as the filtered content
   *
   * @override
   * @property visibleContent
   * @private
   * @type object[]
   */
  visibleContent: computed.alias('arrangedContent'),

  /**
   * For server side filtering, arrangedContent is same as the filtered content
   *
   * @override
   * @property arrangedContent
   * @private
   * @type object[]
   */
  arrangedContent: computed.alias('filteredContent'),

  /**
   * Can't be used within `models-table-server-paginated`. Back-end determines how to filter data
   *
   * @override
   * @property filteringIgnoreCase
   */
  filteringIgnoreCase: computed({
    set() {
      assert(`"filteringIgnoreCase" can't be used with "models-table-server-paginated"`, false);
    },
    get() {
      return undefined;
    }
  }),

  /**
   * Can't be used within `models-table-server-paginated`. Back-end determines how to filter data
   *
   * @override
   * @property doFilteringByHiddenColumns
   */
  doFilteringByHiddenColumns: computed({
    set() {
      assert(`"doFilteringByHiddenColumns" can't be used with "models-table-server-paginated"`, false);
    },
    get() {
      return undefined;
    }
  }),

  /**
   * The total content length is get from the meta information.
   * Set metaItemsCountProperty to change from which meta property this is loaded.
   *
   * @override
   * @type number
   * @property arrangedContentLength
   * @private
   */
  arrangedContentLength: computed('filteredContent.meta', function () {
    let itemsCountProperty = get(this, 'metaItemsCountProperty');
    let meta = get(this, 'filteredContent.meta') || {};
    return get(meta, itemsCountProperty) || 0;
  }),

  /**
   * The pages count is get from the meta information.
   * Set metaPagesCountProperty to change from which meta property this is loaded.
   *
   * @type number
   * @property pagesCount
   * @override
   * @private
   */
  pagesCount: computed('filteredContent.meta', function () {
    let pagesCountProperty = get(this, 'metaPagesCountProperty');
    let meta = get(this, 'filteredContent.meta') || {};
    return get(meta, pagesCountProperty) || 1;
  }),

  /**
   * The index of the last item that is currently being shown.
   *
   * @type number
   * @property lastIndex
   * @override
   * @private
   */
  lastIndex: computed('pageSize', 'currentPageNumber', 'arrangedContentLength', function () {
    let pageMax = parseInt(get(this, 'pageSize'), 10) * get(this, 'currentPageNumber');
    let itemsCount = get(this, 'arrangedContentLength');
    return Math.min(pageMax, itemsCount);
  }),

  /**
   * This function actually loads the data from the server.
   * It takes the store, modelName and query from the passed in data-object and adds page, sorting & filtering to it.
   *
   * @returns {Promise}
   * @method _loadData
   * @private
   */
  _loadData: function () {
    let data = get(this, 'data');
    let currentPageNumber = get(this, 'currentPageNumber');
    let pageSize = get(this, 'pageSize');
    let columns = get(this, 'processedColumns');
    let sortProperties = get(this, 'sortProperties');
    let filterString = get(this, 'filterString');

    if (!get(data, 'query')) {
      warn('You must use http://emberjs.com/api/data/classes/DS.Store.html#method_query for loading data');
      return;
    }
    let query = extend({}, get(data, 'query'));
    let store = get(data, 'store');
    let modelName = get(data, 'type.modelName');

    // Add pagination information
    query[get(this, 'filterQueryParameters.page')] = currentPageNumber;
    query[get(this, 'filterQueryParameters.pageSize')] = pageSize;
    // Add sorting information
    let sort = sortProperties && get(sortProperties, 'length') ? sortProperties[0] : null;
    if (sort) {
      let [sortBy, sortDirection] = sort.split(':');
      query = this.sortingWrapper(query, sortBy, sortDirection.toUpperCase());
    } else {
      delete query[[get(this, 'filterQueryParameters.sort')]];
      delete query[[get(this, 'filterQueryParameters.sortDirection')]];
    }

    // Add global filter
    let globalFilter = get(this, 'filterQueryParameters.globalFilter');
    if (filterString) {
      query[globalFilter] = filterString;
    } else {
      delete query[globalFilter];
    }

    // Add per-column filter
    if (get(this, 'useFilteringByColumns')) {
      columns.forEach(column => {
        let filter = get(column, 'filterString');
        let filterTitle = this.getCustomFilterTitle(column);
        this._setQueryFilter(query, column, filterTitle, filter);
      });
    }

    setProperties(this, {isLoading: true, isError: false});
    return this.doQuery(store, modelName, query)
      .then(() =>  setProperties(this, {isLoading: false, isError: false}))
      .catch(() => setProperties(this, {isLoading: false, isError: true}));
  },

  /**
   * Do query-request to load new data
   *
   * You may override this method to add some extra behavior or even additional requests
   *
   * @method doQuery
   * @param {object} store
   * @param {string} modelName
   * @param {object} query
   * @returns {Promise}
   */
  doQuery(store, modelName, query) {
    return store.query(modelName, query).then(newData => set(this, 'filteredContent', newData));
  },

  /**
   * Actually set the filter on a query.
   * This can be overwritten for special case handling.
   * Note that this will mutate the given query object!
   *
   * @param {object} query the query to mutate
   * @param {object} column the column that is filtering
   * @param {string} filterTitle the query param name for filtering
   * @param {mixed} filter the actual filter value
   * @returns {undefined}
   * @method _setQueryFilter
   * @private
   */
  _setQueryFilter(query, column, filterTitle, filter) {
    if (!isBlank(filter)) {
      query[filterTitle] = filter;
    } else {
      delete query[filterTitle];
    }
  },

  /**
   * Wrapper for sorting query
   *
   * @param {object} query parameters
   * @param {string} sortBy
   * @param {string} sortDirection
   * @returns {object} query parameters
   * @method sortingWrapper
   */
  sortingWrapper(query, sortBy, sortDirection) {
    query[get(this, 'filterQueryParameters.sort')] = sortBy;
    query[get(this, 'filterQueryParameters.sortDirection')] = sortDirection;

    return query;
  },

  /**
   * Customize filter title
   *
   * @method getCustomFilterTitle
   * @param {object} column
   * @returns {string} title
   */
  getCustomFilterTitle(column) {
    return get(column, 'filteredBy') || get(column, 'propertyName');
  },

  actions: {

    /**
     * @override
     * @method actions.sort
     * @param {ModelsTableColumn} column
     * @returns {undefined}
     */
    sort (column) {
      const sortMap = {
        none: 'asc',
        asc: 'desc',
        desc: 'none'
      };
      let sortedBy = get(column, 'sortedBy') || get(column, 'propertyName');
      if (isNone(sortedBy)) {
        return;
      }
      let currentSorting = get(column, 'sorting');
      let newSorting = sortMap[currentSorting.toLowerCase()];
      let sortingArgs = [column, sortedBy, newSorting];
      this._singleColumnSorting(...sortingArgs);
      this.userInteractionObserver();
    }

  },

  didReceiveAttrs() {
    set(this, 'filteredContent', get(this, 'data'));
  },

  _addPropertyObserver() {
    run.debounce(this, this._loadData, get(this, 'debounceDataLoadTime'));
  },

  willInsertElement() {
    this._super(...arguments);
    let observedProperties = get(this, 'observedProperties');
    observedProperties.forEach(propertyName => this.addObserver(propertyName, this._addPropertyObserver));
  },

  willDestroyElement() {
    this._super(...arguments);

    let observedProperties = get(this, 'observedProperties');
    observedProperties.forEach(propertyName => this.removeObserver(propertyName));
  }
});
<|endoftext|>