<commit_before>/*jslint indent: 2, nomen: true, maxlen: 120 */
/*global require */

(function () {
  'use strict';
  var Foxx = require('org/arangodb/foxx'),
    _ = require('underscore'),
    extend = require('org/arangodb/extend').extend,
    // ArangoError = require('internal').ArangoError,
    BaseTransition = require('./base_transition').BaseTransition,
    BaseState = require('./state').State,
    VertexNotFound = require('./graph').VertexNotFound,
    RepositoryWithGraph = require('./repository_with_graph').RepositoryWithGraph,
    ConditionNotFulfilled = require('./base_transition').ConditionNotFulfilled,
    RelationRepository = require('./relation_repository').RelationRepository,
    Transition,
    State,
    Model,
    Repository,
    Strategy,
    AddEntityToRepository,
    LinkFromRepoToEntity,
    Link,
    LinkToService,
    UnlinkTwoEntities,
    LinkToAsyncService,
    LinkTwoEntities,
    FollowToEntity,
    strategies,
    Context;

  Strategy = function () {
    this.type = 'strategy';
  };

  _.extend(Strategy.prototype, {
    executable: function (semantics, from, to) {
      return semantics === this.semantics && from === this.from && to === this.to;
    }
  });

  Strategy.extend = extend;

  LinkTwoEntities = Strategy.extend({
    semantics: 'link',
    from: 'entity',
    to: 'entity',
    relation: 'one-to-one',

    prepare: function () {},

    executeOneToOne: function (controller, graph, relation, from, to) {
      var url = from.urlFor(':entityId') + '/links/' + relation.name,
        relationRepository = new RelationRepository(from, to, relation, graph);

      controller.post(url, function (req, res) {
        relationRepository.replaceRelation(req.params('entityId'), req.body()[relation.name]);
        res.status(204);
      }).pathParam('entityId', {
        description: 'ID of the document',
        type: 'string'
      }).errorResponse(VertexNotFound, 404, 'The vertex could not be found')
        .summary('Set the relation')
        .notes('TODO');
    }
  });

  UnlinkTwoEntities = Strategy.extend({
    semantics: 'unlink',
    from: 'entity',
    to: 'entity',
    relation: 'one-to-one',

    prepare: function () {},

    executeOneToOne: function (controller, graph, relation, from, to) {
      var url = from.urlFor(':entityId') + '/links/' + relation.name,
        relationRepository = new RelationRepository(from, to, relation, graph);

      controller.delete(url, function (req, res) {
        relationRepository.deleteRelation(req.params('entityId'));
        res.status(204);
      }).pathParam('entityId', {
        description: 'ID of the document',
        type: 'string'
      }).errorResponse(VertexNotFound, 404, 'The vertex could not be found')
        .summary('Remove the relation')
        .notes('TODO');
    }
  });

  FollowToEntity = Strategy.extend({
    semantics: 'follow',
    from: 'entity',
    to: 'entity',
    relation: 'one-to-one',

    prepare: function () {},

    executeOneToOne: function (controller, graph, relation, from, to) {}
  });

  AddEntityToRepository = Strategy.extend({
    semantics: 'link',
    from: 'repository',
    to: 'entity',

    prepare: function (from, to) {
      to.collectionName = from.collectionName;
    },

    executeOneToOne: function (controller, graph, relation, repositoryState, entityState) {
      var nameOfRootElement = entityState.name,
        repository = repositoryState.repository,
        BodyParam = Foxx.Model.extend({ schema: relation.parameters }),
        href = repositoryState.urlTemplate,
        name = relation.name,
        precondition = relation.precondition,
        method = 'POST',
        fields,
        title = relation.description;

      fields = _.map(relation.parameters, function (joi, name) {
        var fieldDescription = { name: name, type: joi._type };

        if (!_.isNull(joi._description)) {
          fieldDescription.description = joi._description;
        }

        if (!_.isUndefined(joi._flags.default)) {
          fieldDescription.value = joi._flags.default;
        }

        return fieldDescription;
      });

      repositoryState.addAction(name, method, href, title, fields, precondition);

      controller.post(href, function (req, res) {
        var data = {},
          model = req.params(nameOfRootElement);

        data[nameOfRootElement] = repository.save(model).forClient();
        res.status(201);
        res.json(data);
      }).bodyParam(nameOfRootElement, 'TODO', BodyParam)
        .errorResponse(ConditionNotFulfilled, 403, 'The condition could not be fulfilled')
        .onlyIf(relation.condition)
        .summary(relation.description)
        .notes('TODO');
    }
  });

  LinkFromRepoToEntity = Strategy.extend({
    semantics: 'follow',
    from: 'repository',
    to: 'entity',

    prepare: function (from, to) {
      var repository = from.repository;
      repository.relations = to.relations;
    },

    executeOneToOne: function (controller, graph, relation, repositoryState, entityState) {
      var rel = relation.name,
        href = entityState.urlTemplate,
        title = relation.description,
        repository = repositoryState.repository;

      controller.get(href, function (req, res) {
        var id = req.params('id'),
          entry = repository.byIdWithNeighbors(id);

        res.json(entry.forClient());
      }).pathParam('id', {
        description: 'ID of the document',
        type: 'string'
      }).errorResponse(ConditionNotFulfilled, 403, 'The condition could not be fulfilled')
        .onlyIf(relation.condition)
        .summary(relation.description)
        .notes('TODO');

      repositoryState.addLinkToEntities(rel, href, title, entityState);
    }
  });

  Link = Strategy.extend({
    semantics: 'follow',
    from: 'start',
    to: 'repository',

    prepare: function (from, to) {
    },

    executeOneToOne: function (controller, graph, relation, from, to) {
      var rel = relation.name,
        href = to.urlTemplate,
        precondition = relation.precondition,
        title = relation.description;

      from.addLink([rel], href, title, precondition);

      controller.get(href, function (req, res) {
        res.json({
          properties: to.properties(),
          entities: to.entities(),
          links: to.filteredLinks(req),
          actions: to.filteredActions(req)
        });
      }).summary(relation.description)
        .notes('TODO')
        .errorResponse(ConditionNotFulfilled, 403, 'The condition could not be fulfilled')
        .onlyIf(relation.condition);
    }
  });

  LinkToService = Strategy.extend({
    semantics: 'link',
    from: 'start',
    to: 'service',

    prepare: function (from, to) {
    },

    executeOneToOne: function (controller, graph, relation, from, to) {
      var rel = relation.name,
        href = to.urlTemplate,
        title = relation.description,
        verb = to.verb,
        action = to.action,
        nameOfRootElement = to.name,
        precondition = relation.precondition,
        BodyParam = Foxx.Model.extend({ schema: relation.parameters });

      from.addLink([rel], href, title, precondition);

      controller[verb](href, action)
        .summary(relation.description)
        .bodyParam(nameOfRootElement, 'TODO', BodyParam)
        .notes('TODO')
        .errorResponse(ConditionNotFulfilled, 403, 'The condition could not be fulfilled')
        .onlyIf(relation.condition);
    }
  });

  LinkToAsyncService = Strategy.extend({
    semantics: 'link',
    from: 'start',
    to: 'asyncService',

    prepare: function (from, to) {
    },

    executeOneToOne: function (controller, graph, relation, from, to) {
      var rel = relation.name,
        href = to.urlTemplate,
        title = relation.description,
        verb = to.verb,
        executeAsync = to.executeAsync,
        nameOfRootElement = to.name,
        precondition = relation.precondition,
        BodyParam = Foxx.Model.extend({ schema: relation.parameters });

      from.addLink([rel], href, title, precondition);

      controller[verb](href, function (req, res) {
        var params = req.params(nameOfRootElement);

        executeAsync(params.forClient());

        res.status(201);
        res.json({
          job: 'created'
        });
      }).summary(relation.description)
        .bodyParam(nameOfRootElement, 'TODO', BodyParam)
        .notes('TODO')
        .errorResponse(ConditionNotFulfilled, 403, 'The condition could not be fulfilled')
        .onlyIf(relation.condition);
    }
  });

  strategies = [
    new LinkTwoEntities(),
    new UnlinkTwoEntities(),
    new FollowToEntity(),
    new AddEntityToRepository(),
    new LinkFromRepoToEntity(),
    new LinkToService(),
    new LinkToAsyncService(),
    new Link()
  ];

  Context = function (semantics, from, to) {
    this.strategy = _.find(strategies, function (maybeStrategy) {
      return maybeStrategy.executable(semantics, from, to);
    });

    if (_.isUndefined(this.strategy)) {
      require('console').log('Couldn\'t find a strategy for semantic %s from %s to %s', semantics, from, to);
      throw 'Could not find strategy';
    }
  };

  _.extend(Context.prototype, {
    prepare: function (from, to) {
      this.strategy.prepare(from, to);
    },

    executeOneToOne: function (controller, graph, relation, from, to) {
      this.strategy.executeOneToOne(controller, graph, relation, from, to);
    },

    executeOneToMany: function (controller, graph, relation, from, to) {
      this.strategy.executeOneToMany(controller, graph, relation, from, to);
    }
  });

  Repository = RepositoryWithGraph.extend({
  });

  Model = Foxx.Model.extend({
    forClient: function () {
      var properties = Foxx.Model.prototype.forClient.call(this);

      return {
        properties: properties,
        links: []
      };
    }
  });

  State = BaseState.extend({
    addRepository: function (Repository, states) {
      this.type = 'repository';
      var elementRelation = this.findTransitionBySemantics('link'),
        ModelForRepo = states[elementRelation.to].model;

      this.collection = this.graph.addVertexCollection(this.name);
      this.collectionName = this.collection.name();

      this.repository = new Repository(this.collection, {
        model: ModelForRepo,
        graph: this.graph
      });
    },

    setAsStart: function (controller) {
      var that = this;

      controller.get('/', function (req, res) {
        res.json({
          properties: {},
          links: that.filteredLinks(req),
          actions: that.filteredActions(req)
        });
      }).summary('Billboard URL')
        .notes('TODO');
      this.type = 'start';
    },

    properties: function () {
      return {};
    },

    entities: function () {
      var entities = [];

      if (this.type === 'repository') {
        entities = _.map(this.repository.all(), function (entity) {
          var result = entity.forClient();

          _.each(this.childLinks, function (link) {
            result.links.push({
              rel: link.rel,
              href: link.target.urlFor(entity.get('_key')),
              title: link.title
            });
          });
          return result;
        }, this);
      }

      return entities;
    },

    filteredLinks: function (req) {
      return _.filter(this.links, function (link) {
        return link.precondition(req);
      });
    },

    filteredActions: function (req) {
      return _.filter(this.actions, function (action) {
        return action.precondition(req);
      });
    },

    addLink: function (rel, href, title, precondition) {
      this.links.push({
        precondition: precondition,
        rel: rel,
        href: href,
        title: title
      });
    },

    addLinkToEntities: function (rel, href, title, target) {
      this.childLinks.push({
        rel: rel,
        href: href,
        title: title,
        target: target
      });
    },

    addAction: function (name, method, href, title, fields, precondition) {
      this.actions.push({
        precondition: precondition,
        name: name,
        // class: ?,
        method: method,
        href: href,
        title: title,
        type: 'application/json',
        fields: fields
      });
    }
  });

  Transition = BaseTransition.extend({
    prepare: function (from, to) {
      var context = new Context(this.semantics, from.type, to.type);
      context.prepare(from, to);
    },

    addRoutesForOneRelation: function (controller, graph, relation, from, to) {
      var context = new Context(this.semantics, from.type, to.type);
      context.executeOneToOne(controller, graph, relation, from, to);
    },

    addRoutesForManyRelation: function (controller, graph, relation, from, to) {
      var context = new Context(this.semantics, from.type, to.type);
      context.executeOneToMany(controller, graph, relation, from, to);
    }
  });

  exports.mediaType = {
    Model: Model,
    Repository: Repository,
    Transition: Transition,
    State: State,
    transitions: []
  };
}());
<commit_msg>Add support for Linking two Entities with OneToMany<commit_after>/*jslint indent: 2, nomen: true, maxlen: 120 */
/*global require */

(function () {
  'use strict';
  var Foxx = require('org/arangodb/foxx'),
    _ = require('underscore'),
    extend = require('org/arangodb/extend').extend,
    // ArangoError = require('internal').ArangoError,
    BaseTransition = require('./base_transition').BaseTransition,
    BaseState = require('./state').State,
    VertexNotFound = require('./graph').VertexNotFound,
    RepositoryWithGraph = require('./repository_with_graph').RepositoryWithGraph,
    ConditionNotFulfilled = require('./base_transition').ConditionNotFulfilled,
    RelationRepository = require('./relation_repository').RelationRepository,
    Transition,
    State,
    Model,
    Repository,
    Strategy,
    AddEntityToRepository,
    LinkFromRepoToEntity,
    Link,
    LinkToService,
    UnlinkTwoEntities,
    LinkToAsyncService,
    LinkTwoEntities,
    FollowToEntity,
    strategies,
    Context;

  Strategy = function () {
    this.type = 'strategy';
  };

  _.extend(Strategy.prototype, {
    executable: function (semantics, from, to) {
      return semantics === this.semantics && from === this.from && to === this.to;
    }
  });

  Strategy.extend = extend;

  LinkTwoEntities = Strategy.extend({
    semantics: 'link',
    from: 'entity',
    to: 'entity',
    relation: 'one-to-one',

    prepare: function () {},

    executeOneToOne: function (controller, graph, relation, from, to) {
      var url = from.urlFor(':entityId') + '/links/' + relation.name,
        relationRepository = new RelationRepository(from, to, relation, graph);

      controller.post(url, function (req, res) {
        relationRepository.replaceRelation(req.params('entityId'), req.body()[relation.name]);
        res.status(204);
      }).pathParam('entityId', {
        description: 'ID of the document',
        type: 'string'
      }).errorResponse(VertexNotFound, 404, 'The vertex could not be found')
        .summary('Set the relation')
        .notes('TODO');
    },

    executeOneToMany: function (controller, graph, relation, from, to) {
      var url = from.urlFor(':entityId') + '/links/' + relation.name,
        relationRepository = new RelationRepository(from, to, relation, graph);

      controller.post(url, function (req, res) {
        relationRepository.addRelations(req.params('entityId'), req.body()[relation.name]);
        res.status(204);
      }).pathParam('entityId', {
        description: 'ID of the document',
        type: 'string'
      }).errorResponse(VertexNotFound, 404, 'The vertex could not be found')
        .summary('Set the relation')
        .notes('TODO');
    }
  });

  UnlinkTwoEntities = Strategy.extend({
    semantics: 'unlink',
    from: 'entity',
    to: 'entity',
    relation: 'one-to-one',

    prepare: function () {},

    executeOneToOne: function (controller, graph, relation, from, to) {
      var url = from.urlFor(':entityId') + '/links/' + relation.name,
        relationRepository = new RelationRepository(from, to, relation, graph);

      controller.delete(url, function (req, res) {
        relationRepository.deleteRelation(req.params('entityId'));
        res.status(204);
      }).pathParam('entityId', {
        description: 'ID of the document',
        type: 'string'
      }).errorResponse(VertexNotFound, 404, 'The vertex could not be found')
        .summary('Remove the relation')
        .notes('TODO');
    },

    executeOneToMany: function (controller, graph, relation, from, to) {
    }
  });

  FollowToEntity = Strategy.extend({
    semantics: 'follow',
    from: 'entity',
    to: 'entity',
    relation: 'one-to-one',

    prepare: function () {},

    executeOneToOne: function (controller, graph, relation, from, to) {},
    executeOneToMany: function (controller, graph, relation, from, to) {}
  });

  AddEntityToRepository = Strategy.extend({
    semantics: 'link',
    from: 'repository',
    to: 'entity',

    prepare: function (from, to) {
      to.collectionName = from.collectionName;
    },

    executeOneToOne: function (controller, graph, relation, repositoryState, entityState) {
      var nameOfRootElement = entityState.name,
        repository = repositoryState.repository,
        BodyParam = Foxx.Model.extend({ schema: relation.parameters }),
        href = repositoryState.urlTemplate,
        name = relation.name,
        precondition = relation.precondition,
        method = 'POST',
        fields,
        title = relation.description;

      fields = _.map(relation.parameters, function (joi, name) {
        var fieldDescription = { name: name, type: joi._type };

        if (!_.isNull(joi._description)) {
          fieldDescription.description = joi._description;
        }

        if (!_.isUndefined(joi._flags.default)) {
          fieldDescription.value = joi._flags.default;
        }

        return fieldDescription;
      });

      repositoryState.addAction(name, method, href, title, fields, precondition);

      controller.post(href, function (req, res) {
        var data = {},
          model = req.params(nameOfRootElement);

        data[nameOfRootElement] = repository.save(model).forClient();
        res.status(201);
        res.json(data);
      }).bodyParam(nameOfRootElement, 'TODO', BodyParam)
        .errorResponse(ConditionNotFulfilled, 403, 'The condition could not be fulfilled')
        .onlyIf(relation.condition)
        .summary(relation.description)
        .notes('TODO');
    }
  });

  LinkFromRepoToEntity = Strategy.extend({
    semantics: 'follow',
    from: 'repository',
    to: 'entity',

    prepare: function (from, to) {
      var repository = from.repository;
      repository.relations = to.relations;
    },

    executeOneToOne: function (controller, graph, relation, repositoryState, entityState) {
      var rel = relation.name,
        href = entityState.urlTemplate,
        title = relation.description,
        repository = repositoryState.repository;

      controller.get(href, function (req, res) {
        var id = req.params('id'),
          entry = repository.byIdWithNeighbors(id);

        res.json(entry.forClient());
      }).pathParam('id', {
        description: 'ID of the document',
        type: 'string'
      }).errorResponse(ConditionNotFulfilled, 403, 'The condition could not be fulfilled')
        .onlyIf(relation.condition)
        .summary(relation.description)
        .notes('TODO');

      repositoryState.addLinkToEntities(rel, href, title, entityState);
    }
  });

  Link = Strategy.extend({
    semantics: 'follow',
    from: 'start',
    to: 'repository',

    prepare: function (from, to) {
    },

    executeOneToOne: function (controller, graph, relation, from, to) {
      var rel = relation.name,
        href = to.urlTemplate,
        precondition = relation.precondition,
        title = relation.description;

      from.addLink([rel], href, title, precondition);

      controller.get(href, function (req, res) {
        res.json({
          properties: to.properties(),
          entities: to.entities(),
          links: to.filteredLinks(req),
          actions: to.filteredActions(req)
        });
      }).summary(relation.description)
        .notes('TODO')
        .errorResponse(ConditionNotFulfilled, 403, 'The condition could not be fulfilled')
        .onlyIf(relation.condition);
    }
  });

  LinkToService = Strategy.extend({
    semantics: 'link',
    from: 'start',
    to: 'service',

    prepare: function (from, to) {
    },

    executeOneToOne: function (controller, graph, relation, from, to) {
      var rel = relation.name,
        href = to.urlTemplate,
        title = relation.description,
        verb = to.verb,
        action = to.action,
        nameOfRootElement = to.name,
        precondition = relation.precondition,
        BodyParam = Foxx.Model.extend({ schema: relation.parameters });

      from.addLink([rel], href, title, precondition);

      controller[verb](href, action)
        .summary(relation.description)
        .bodyParam(nameOfRootElement, 'TODO', BodyParam)
        .notes('TODO')
        .errorResponse(ConditionNotFulfilled, 403, 'The condition could not be fulfilled')
        .onlyIf(relation.condition);
    }
  });

  LinkToAsyncService = Strategy.extend({
    semantics: 'link',
    from: 'start',
    to: 'asyncService',

    prepare: function (from, to) {
    },

    executeOneToOne: function (controller, graph, relation, from, to) {
      var rel = relation.name,
        href = to.urlTemplate,
        title = relation.description,
        verb = to.verb,
        executeAsync = to.executeAsync,
        nameOfRootElement = to.name,
        precondition = relation.precondition,
        BodyParam = Foxx.Model.extend({ schema: relation.parameters });

      from.addLink([rel], href, title, precondition);

      controller[verb](href, function (req, res) {
        var params = req.params(nameOfRootElement);

        executeAsync(params.forClient());

        res.status(201);
        res.json({
          job: 'created'
        });
      }).summary(relation.description)
        .bodyParam(nameOfRootElement, 'TODO', BodyParam)
        .notes('TODO')
        .errorResponse(ConditionNotFulfilled, 403, 'The condition could not be fulfilled')
        .onlyIf(relation.condition);
    }
  });

  strategies = [
    new LinkTwoEntities(),
    new UnlinkTwoEntities(),
    new FollowToEntity(),
    new AddEntityToRepository(),
    new LinkFromRepoToEntity(),
    new LinkToService(),
    new LinkToAsyncService(),
    new Link()
  ];

  Context = function (semantics, from, to) {
    this.strategy = _.find(strategies, function (maybeStrategy) {
      return maybeStrategy.executable(semantics, from, to);
    });

    if (_.isUndefined(this.strategy)) {
      require('console').log('Couldn\'t find a strategy for semantic %s from %s to %s', semantics, from, to);
      throw 'Could not find strategy';
    }
  };

  _.extend(Context.prototype, {
    prepare: function (from, to) {
      this.strategy.prepare(from, to);
    },

    executeOneToOne: function (controller, graph, relation, from, to) {
      this.strategy.executeOneToOne(controller, graph, relation, from, to);
    },

    executeOneToMany: function (controller, graph, relation, from, to) {
      this.strategy.executeOneToMany(controller, graph, relation, from, to);
    }
  });

  Repository = RepositoryWithGraph.extend({
  });

  Model = Foxx.Model.extend({
    forClient: function () {
      var properties = Foxx.Model.prototype.forClient.call(this);

      return {
        properties: properties,
        links: []
      };
    }
  });

  State = BaseState.extend({
    addRepository: function (Repository, states) {
      this.type = 'repository';
      var elementRelation = this.findTransitionBySemantics('link'),
        ModelForRepo = states[elementRelation.to].model;

      this.collection = this.graph.addVertexCollection(this.name);
      this.collectionName = this.collection.name();

      this.repository = new Repository(this.collection, {
        model: ModelForRepo,
        graph: this.graph
      });
    },

    setAsStart: function (controller) {
      var that = this;

      controller.get('/', function (req, res) {
        res.json({
          properties: {},
          links: that.filteredLinks(req),
          actions: that.filteredActions(req)
        });
      }).summary('Billboard URL')
        .notes('TODO');
      this.type = 'start';
    },

    properties: function () {
      return {};
    },

    entities: function () {
      var entities = [];

      if (this.type === 'repository') {
        entities = _.map(this.repository.all(), function (entity) {
          var result = entity.forClient();

          _.each(this.childLinks, function (link) {
            result.links.push({
              rel: link.rel,
              href: link.target.urlFor(entity.get('_key')),
              title: link.title
            });
          });
          return result;
        }, this);
      }

      return entities;
    },

    filteredLinks: function (req) {
      return _.filter(this.links, function (link) {
        return link.precondition(req);
      });
    },

    filteredActions: function (req) {
      return _.filter(this.actions, function (action) {
        return action.precondition(req);
      });
    },

    addLink: function (rel, href, title, precondition) {
      this.links.push({
        precondition: precondition,
        rel: rel,
        href: href,
        title: title
      });
    },

    addLinkToEntities: function (rel, href, title, target) {
      this.childLinks.push({
        rel: rel,
        href: href,
        title: title,
        target: target
      });
    },

    addAction: function (name, method, href, title, fields, precondition) {
      this.actions.push({
        precondition: precondition,
        name: name,
        // class: ?,
        method: method,
        href: href,
        title: title,
        type: 'application/json',
        fields: fields
      });
    }
  });

  Transition = BaseTransition.extend({
    prepare: function (from, to) {
      var context = new Context(this.semantics, from.type, to.type);
      context.prepare(from, to);
    },

    addRoutesForOneRelation: function (controller, graph, relation, from, to) {
      var context = new Context(this.semantics, from.type, to.type);
      context.executeOneToOne(controller, graph, relation, from, to);
    },

    addRoutesForManyRelation: function (controller, graph, relation, from, to) {
      var context = new Context(this.semantics, from.type, to.type);
      context.executeOneToMany(controller, graph, relation, from, to);
    }
  });

  exports.mediaType = {
    Model: Model,
    Repository: Repository,
    Transition: Transition,
    State: State,
    transitions: []
  };
}());
<|endoftext|>


<commit_before>import { combineEpics } from '../redux-promise-epic';

import { LOCATION_CHANGE, SERVER_RENDER } from 'mwp-router';
import { getMatchedQueries } from 'mwp-router/lib/util';

import * as api from './apiActionCreators';
import {
	apiSuccess, // DEPRECATED
	apiError, // DEPRECATED
} from './syncActionCreators';

const IGNORE_ACTION = Promise.resolve([]);
/**
 * @deprecated
 */
export function getDeprecatedSuccessPayload(successes, errors) {
	const allQueryResponses = [...successes, ...errors];
	return allQueryResponses.reduce(
		(payload, { query, response }) => {
			if (!response) {
				return payload;
			}
			const { ref, error, ...responseBody } = response;
			if (error) {
				// old payload expects error as a property of `value`
				responseBody.value = { error };
			}
			payload.queries.push(query);
			payload.responses.push({ [ref]: responseBody });
			return payload;
		},
		{ queries: [], responses: [] }
	);
}

/**
 * Any operations that keep the browser application in sync with the
 * server should be implemented here.
 *
 * - Navigation-generated API request handling
 * - Arbitrary API request handling (syncActionCreators.apiSuccess)
 *
 * @module syncEpic
 */

/**
 * Navigation actions will provide the `location` as the payload, which this
 * epic will use to collect the current Reactive Queries associated with the
 * active routes.
 *
 * These queries will then be dispatched in the payload of `apiRequest`. Any
 * metadata about the navigation action can also be sent to the `apiRequest`
 * here.
 *
 * note that this function executes _downstream_ of reducers, so the
 * new `routing` data has already been populated in `state`
 *
 * @param {Object} routes The application's React Router routes
 * @returns {Function} an Epic function that emits an API_RESP_REQUEST action
 */
export const getNavEpic = findMatches => (action, store) => {
	if (![LOCATION_CHANGE, SERVER_RENDER].includes(action.type)) {
		return IGNORE_ACTION;
	}
	const { payload: location } = action;
	const state = store.getState();
	const { referrer = {} } = state.routing;
	// inject request metadata from context, including `store.getState()`
	const requestMetadata = {
		referrer: referrer.pathname || state.config.entryPath || '',
		logout: location.pathname.endsWith('logout'), // assume logout route ends with logout - not currently implemented in any app
		clickTracking: true, // clicks should be tracked with this request
		retainRefs: [],
	};
	const cacheAction = requestMetadata.logout && { type: 'CACHE_CLEAR' };

	const previousQueries = referrer.pathname
		? getMatchedQueries(referrer, state)(findMatches(referrer))
		: [];
	const newQueries = getMatchedQueries(location, state)(findMatches(location));
	if (newQueries.filter(q => q).length === 0) {
		// no valid queries - jump straight to 'complete'
		return [api.complete([])];
	}
	// perform a fast comparison of previous route's serialized queries
	// with the new route's serialized queries. All state refs for
	// _shared_ queries should be retained
	const serializedNew = newQueries.map(JSON.stringify);
	const serializedPrev = previousQueries.map(JSON.stringify);
	const sharedRefs = serializedPrev
		.filter(qJSON => serializedNew.includes(qJSON))
		.map(JSON.parse)
		.map(q => q.ref);
	requestMetadata.retainRefs = sharedRefs;

	return Promise.resolve(
		[cacheAction, api.get(newQueries, requestMetadata)].filter(a => a)
	);
};

/**
 * Old apiRequest maps directly onto new api.get
 * @deprecated
 */
export const apiRequestToApiReq = action =>
	action.type === 'API_REQUEST'
		? Promise.resolve([api.get(action.payload, action.meta)])
		: IGNORE_ACTION;

/**
 * Listen for API_REQ and generate response actions from fetch results
 *
 * emits
 * - 1 or more API_RESP_SUCCESS
 * - 1 or more API_RESP_ERROR
 * - API_SUCCESS  // deprecated
 * - API_COMPLETE
 *
 * or
 *
 * - API_RESP_FAIL
 * - API_ERROR  // deprecated
 * - API_COMPLETE
 */
export const getFetchQueriesEpic = (findMatches, fetchQueriesFn) => {
	// keep track of location changes - will first be set by SERVER_RENDER
	let locationIndex = 0;
	const standardizeUrl = location => {
		const matches = findMatches(location);
		return matches && matches.matched
			? matches.matched.pop().match.path.replace(/[^a-zA-Z0-9/]/gi, '')
			: location;
	};

	// set up a closure that will compare the partially-applied location to the current value
	// of `locationIndex` - if `locationIndex` has changed since `currentLocation`
	// was passed in, return an empty array instead of the supplied actions array
	// This is a way of ignoring API return values that happen after a location change
	const ignoreIfLocationChange = currentLocation => actions =>
		currentLocation === locationIndex ? actions : [];

	// return the epic
	return (action, store) => {
		if (action.type === LOCATION_CHANGE) {
			locationIndex = locationIndex + 1;
		}
		if (action.type !== api.API_REQ) {
			return IGNORE_ACTION;
		}
		const { payload: queries, meta } = action;
		// set up the fetch call to the app server
		const {
			config,
			api: { self },
			routing: { location, referrer },
		} = store.getState();

		// clean up path for use as endpoint URL
		const standardizedUrlPath = standardizeUrl(location);
		const standardizedReferrerPath = standardizeUrl(referrer);
		// construct the fetch call using match.path
		const fetchUrl = `${config.apiUrl}${standardizedUrlPath}`;
		const fetchQueries = fetchQueriesFn(fetchUrl, (self || {}).value);

		// supply additional request info, e.g. for tracking
		const requestInfo = {
			standardized_url: standardizedUrlPath,
			standardized_referer: standardizedReferrerPath,
		};

		return fetchQueries(queries, meta, requestInfo)
			.then(({ successes = [], errors = [] }) => {
				// meta contains a Promise that must be resolved
				meta.resolve([...successes, ...errors]);
				const deprecatedSuccessPayload = getDeprecatedSuccessPayload(
					successes,
					errors
				);
				const deprecatedActions = [apiSuccess(deprecatedSuccessPayload)];
				if (meta && meta.onSuccess) {
					deprecatedActions.push(meta.onSuccess(deprecatedSuccessPayload));
				}
				return [
					...successes.map(api.success), // send the successes to success
					...errors.map(api.error), // send errors to error
					...deprecatedActions,
				];
			})
			.catch(err => {
				// meta contains a Promise that must be rejected
				meta.reject(err);
				const deprecatedActions = [apiError(err)];
				if (meta && meta.onError) {
					deprecatedActions.push(meta.onError(err));
				}
				return [api.fail(err), ...deprecatedActions];
			})
			.then(ignoreIfLocationChange(locationIndex))
			.then(actions => [...actions, api.complete(queries)]);
	};
};

export default (findMatches, fetchQueriesFn) =>
	combineEpics(
		getNavEpic(findMatches),
		getFetchQueriesEpic(findMatches, fetchQueriesFn),
		apiRequestToApiReq
	);
<commit_msg>don't find matches for empty URLs, always return string from standardizeUrl<commit_after>import { combineEpics } from '../redux-promise-epic';

import { LOCATION_CHANGE, SERVER_RENDER } from 'mwp-router';
import { getMatchedQueries } from 'mwp-router/lib/util';

import * as api from './apiActionCreators';
import {
	apiSuccess, // DEPRECATED
	apiError, // DEPRECATED
} from './syncActionCreators';

const IGNORE_ACTION = Promise.resolve([]);
/**
 * @deprecated
 */
export function getDeprecatedSuccessPayload(successes, errors) {
	const allQueryResponses = [...successes, ...errors];
	return allQueryResponses.reduce(
		(payload, { query, response }) => {
			if (!response) {
				return payload;
			}
			const { ref, error, ...responseBody } = response;
			if (error) {
				// old payload expects error as a property of `value`
				responseBody.value = { error };
			}
			payload.queries.push(query);
			payload.responses.push({ [ref]: responseBody });
			return payload;
		},
		{ queries: [], responses: [] }
	);
}

/**
 * Any operations that keep the browser application in sync with the
 * server should be implemented here.
 *
 * - Navigation-generated API request handling
 * - Arbitrary API request handling (syncActionCreators.apiSuccess)
 *
 * @module syncEpic
 */

/**
 * Navigation actions will provide the `location` as the payload, which this
 * epic will use to collect the current Reactive Queries associated with the
 * active routes.
 *
 * These queries will then be dispatched in the payload of `apiRequest`. Any
 * metadata about the navigation action can also be sent to the `apiRequest`
 * here.
 *
 * note that this function executes _downstream_ of reducers, so the
 * new `routing` data has already been populated in `state`
 *
 * @param {Object} routes The application's React Router routes
 * @returns {Function} an Epic function that emits an API_RESP_REQUEST action
 */
export const getNavEpic = findMatches => (action, store) => {
	if (![LOCATION_CHANGE, SERVER_RENDER].includes(action.type)) {
		return IGNORE_ACTION;
	}
	const { payload: location } = action;
	const state = store.getState();
	const { referrer = {} } = state.routing;
	// inject request metadata from context, including `store.getState()`
	const requestMetadata = {
		referrer: referrer.pathname || state.config.entryPath || '',
		logout: location.pathname.endsWith('logout'), // assume logout route ends with logout - not currently implemented in any app
		clickTracking: true, // clicks should be tracked with this request
		retainRefs: [],
	};
	const cacheAction = requestMetadata.logout && { type: 'CACHE_CLEAR' };

	const previousQueries = referrer.pathname
		? getMatchedQueries(referrer, state)(findMatches(referrer))
		: [];
	const newQueries = getMatchedQueries(location, state)(findMatches(location));
	if (newQueries.filter(q => q).length === 0) {
		// no valid queries - jump straight to 'complete'
		return [api.complete([])];
	}
	// perform a fast comparison of previous route's serialized queries
	// with the new route's serialized queries. All state refs for
	// _shared_ queries should be retained
	const serializedNew = newQueries.map(JSON.stringify);
	const serializedPrev = previousQueries.map(JSON.stringify);
	const sharedRefs = serializedPrev
		.filter(qJSON => serializedNew.includes(qJSON))
		.map(JSON.parse)
		.map(q => q.ref);
	requestMetadata.retainRefs = sharedRefs;

	return Promise.resolve(
		[cacheAction, api.get(newQueries, requestMetadata)].filter(a => a)
	);
};

/**
 * Old apiRequest maps directly onto new api.get
 * @deprecated
 */
export const apiRequestToApiReq = action =>
	action.type === 'API_REQUEST'
		? Promise.resolve([api.get(action.payload, action.meta)])
		: IGNORE_ACTION;

/**
 * Listen for API_REQ and generate response actions from fetch results
 *
 * emits
 * - 1 or more API_RESP_SUCCESS
 * - 1 or more API_RESP_ERROR
 * - API_SUCCESS  // deprecated
 * - API_COMPLETE
 *
 * or
 *
 * - API_RESP_FAIL
 * - API_ERROR  // deprecated
 * - API_COMPLETE
 */
export const getFetchQueriesEpic = (findMatches, fetchQueriesFn) => {
	// keep track of location changes - will first be set by SERVER_RENDER
	let locationIndex = 0;
	const standardizeUrl = location => {
		const matches = findMatches(location);
		return matches && matches.matched
			? matches.matched.pop().match.path.replace(/[^a-zA-Z0-9/]/gi, '')
			: '';
	};

	// set up a closure that will compare the partially-applied location to the current value
	// of `locationIndex` - if `locationIndex` has changed since `currentLocation`
	// was passed in, return an empty array instead of the supplied actions array
	// This is a way of ignoring API return values that happen after a location change
	const ignoreIfLocationChange = currentLocation => actions =>
		currentLocation === locationIndex ? actions : [];

	// return the epic
	return (action, store) => {
		if (action.type === LOCATION_CHANGE) {
			locationIndex = locationIndex + 1;
		}
		if (action.type !== api.API_REQ) {
			return IGNORE_ACTION;
		}
		const { payload: queries, meta } = action;
		// set up the fetch call to the app server
		const {
			config,
			api: { self },
			routing: { location, referrer },
		} = store.getState();

		// clean up path for use as endpoint URL
		const standardizedUrlPath = standardizeUrl(location);
		const standardizedReferrerPath =
			referrer && referrer.pathname ? standardizeUrl(referrer) : undefined;
		// construct the fetch call using match.path
		const fetchUrl = `${config.apiUrl}${standardizedUrlPath}`;
		const fetchQueries = fetchQueriesFn(fetchUrl, (self || {}).value);

		// supply additional request info, e.g. for tracking
		const requestInfo = {
			standardized_url: standardizedUrlPath,
			standardized_referer: standardizedReferrerPath,
		};

		return fetchQueries(queries, meta, requestInfo)
			.then(({ successes = [], errors = [] }) => {
				// meta contains a Promise that must be resolved
				meta.resolve([...successes, ...errors]);
				const deprecatedSuccessPayload = getDeprecatedSuccessPayload(
					successes,
					errors
				);
				const deprecatedActions = [apiSuccess(deprecatedSuccessPayload)];
				if (meta && meta.onSuccess) {
					deprecatedActions.push(meta.onSuccess(deprecatedSuccessPayload));
				}
				return [
					...successes.map(api.success), // send the successes to success
					...errors.map(api.error), // send errors to error
					...deprecatedActions,
				];
			})
			.catch(err => {
				// meta contains a Promise that must be rejected
				meta.reject(err);
				const deprecatedActions = [apiError(err)];
				if (meta && meta.onError) {
					deprecatedActions.push(meta.onError(err));
				}
				return [api.fail(err), ...deprecatedActions];
			})
			.then(ignoreIfLocationChange(locationIndex))
			.then(actions => [...actions, api.complete(queries)]);
	};
};

export default (findMatches, fetchQueriesFn) =>
	combineEpics(
		getNavEpic(findMatches),
		getFetchQueriesEpic(findMatches, fetchQueriesFn),
		apiRequestToApiReq
	);
<|endoftext|>


<commit_before>"use strict";

module.exports = (function(){
  var request = require("request");
  var SERVER_URL = process.env.SERVER_URL;
  var PAGE_ACCESS_TOKEN = process.env.PAGE_ACCESS_TOKEN;

  // This function is basically the engine of this module.
  // All of the sending functions utilize this
  function callSendAPI(messageData){
    console.log("CALLING SEND API");
    console.log(messageData);
    request({
      uri: "https://graph.facebook.com/v2.8/me/messages",
      qs: { access_token: PAGE_ACCESS_TOKEN },
      method: "POST",
      json: messageData

    }, function (error, response, body) {
      if (!error && response.statusCode === 200) {
        var recipientId = body.recipient_id;
        var messageId = body.message_id;

        if (messageId) {
          console.log("Successfully sent message with id %s to recipient %s", messageId, recipientId);
        } else {
          console.log("Successfully called Send API for recipient %s", recipientId);
        }
      } else {
        console.error("Failed calling Send API", response.statusCode, response.statusMessage, body.error);
      }
    });
  }

  return {
    sendTextMessage: function(recipientId, messageText){
      console.log("SENDING TEXT MESSAGE " + messageText + "\n TO " + recipientId);
      var messageData = {
        recipient: {
          id: recipientId
        },
        message: {
          text: messageText,
          metadata: "DEVELOPER_DEFINED_METADATA"
        }
      };

      callSendAPI(messageData);
    },
    sendImageMessage: function(recipientId){
      var messageData = {
        recipient: {
          id: recipientId
        },
        message: {
          attachment: {
            type: "image",
            payload: {
              url: SERVER_URL + "/assets/rift.png"
            }
          }
        }
      };

      callSendAPI(messageData);
    },
    sendGifMessage: function(recipientId){
      var messageData = {
        recipient: {
          id: recipientId
        },
        message: {
          attachment: {
            type: "image",
            payload: {
              url: SERVER_URL + "/assets/instagram_logo.gif"
            }
          }
        }
      };

      callSendAPI(messageData);
    },
    sendAudioMessage: function(recipientId){
      var messageData = {
        recipient: {
          id: recipientId
        },
        message: {
          attachment: {
            type: "audio",
            payload: {
              url: SERVER_URL + "/assets/sample.mp3"
            }
          }
        }
      };

      callSendAPI(messageData);
    },
    sendVideoMessage: function(recipientId){
      var messageData = {
        recipient: {
          id: recipientId
        },
        message: {
          attachment: {
            type: "video",
            payload: {
              url: SERVER_URL + "/assets/allofus480.mov"
            }
          }
        }
      };

      callSendAPI(messageData);
    },
    sendFileMessage: function(recipientId){
      var messageData = {
        recipient: {
          id: recipientId
        },
        message: {
          attachment: {
            type: "file",
            payload: {
              url: SERVER_URL + "/assets/test.txt"
            }
          }
        }
      };

      callSendAPI(messageData);
    },
    sendButtonMessage: function(recipientId){
      var messageData = {
        recipient: {
          id: recipientId
        },
        message: {
          attachment: {
            type: "template",
            payload: {
              template_type: "button",
              text: "This is test text",
              buttons:[{
                type: "web_url",
                url: "https://www.oculus.com/en-us/rift/",
                title: "Open Web URL"
              }, {
                type: "postback",
                title: "Trigger Postback",
                payload: "DEVELOPED_DEFINED_PAYLOAD"
              }, {
                type: "phone_number",
                title: "Call Phone Number",
                payload: "+16505551234"
              }]
            }
          }
        }
      };

      callSendAPI(messageData);
    },
    sendGenericMessage: function(recipientId){
      var messageData = {
        recipient: {
          id: recipientId
        },
        message: {
          attachment: {
            type: "template",
            payload: {
              template_type: "generic",
              elements: [{
                title: "rift",
                subtitle: "Next-generation virtual reality",
                item_url: "https://www.oculus.com/en-us/rift/",
                image_url: SERVER_URL + "/assets/rift.png",
                buttons: [{
                  type: "web_url",
                  url: "https://www.oculus.com/en-us/rift/",
                  title: "Open Web URL"
                }, {
                  type: "postback",
                  title: "Call Postback",
                  payload: "Payload for first bubble",
                }],
              }, {
                title: "touch",
                subtitle: "Your Hands, Now in VR",
                item_url: "https://www.oculus.com/en-us/touch/",
                image_url: SERVER_URL + "/assets/touch.png",
                buttons: [{
                  type: "web_url",
                  url: "https://www.oculus.com/en-us/touch/",
                  title: "Open Web URL"
                }, {
                  type: "postback",
                  title: "Call Postback",
                  payload: "Payload for second bubble",
                }]
              }]
            }
          }
        }
      };

      callSendAPI(messageData);
    },
    sendReceiptMessage: function(recipientId){
      // Generate a random receipt ID as the API requires a unique ID
      var receiptId = "order" + Math.floor(Math.random()*1000);

      var messageData = {
        recipient: {
          id: recipientId
        },
        message:{
          attachment: {
            type: "template",
            payload: {
              template_type: "receipt",
              recipient_name: "Peter Chang",
              order_number: receiptId,
              currency: "USD",
              payment_method: "Visa 1234",
              timestamp: "1428444852",
              elements: [{
                title: "Oculus Rift",
                subtitle: "Includes: headset, sensor, remote",
                quantity: 1,
                price: 599.00,
                currency: "USD",
                image_url: SERVER_URL + "/assets/riftsq.png"
              }, {
                title: "Samsung Gear VR",
                subtitle: "Frost White",
                quantity: 1,
                price: 99.99,
                currency: "USD",
                image_url: SERVER_URL + "/assets/gearvrsq.png"
              }],
              address: {
                street_1: "1 Hacker Way",
                street_2: "",
                city: "Menlo Park",
                postal_code: "94025",
                state: "CA",
                country: "US"
              },
              summary: {
                subtotal: 698.99,
                shipping_cost: 20.00,
                total_tax: 57.67,
                total_cost: 626.66
              },
              adjustments: [{
                name: "New Customer Discount",
                amount: -50
              }, {
                name: "$100 Off Coupon",
                amount: -100
              }]
            }
          }
        }
      };

      callSendAPI(messageData);
    },
    sendQuickReply: function(recipientId){
      var messageData = {
        recipient: {
          id: recipientId
        },
        message: {
          text: "What's your favorite movie genre?",
          metadata: "DEVELOPER_DEFINED_METADATA",
          quick_replies: [
            {
              "content_type":"text",
              "title":"Action",
              "payload":"DEVELOPER_DEFINED_PAYLOAD_FOR_PICKING_ACTION"
            },
            {
              "content_type":"text",
              "title":"Comedy",
              "payload":"DEVELOPER_DEFINED_PAYLOAD_FOR_PICKING_COMEDY"
            },
            {
              "content_type":"text",
              "title":"Drama",
              "payload":"DEVELOPER_DEFINED_PAYLOAD_FOR_PICKING_DRAMA"
            }
          ]
        }
      };

      callSendAPI(messageData);
    },
    sendReadReceipt: function(recipientId){
      console.log("Sending a read receipt to mark message as seen");

      var messageData = {
        recipient: {
          id: recipientId
        },
        sender_action: "mark_seen"
      };

      callSendAPI(messageData);

    },
    sendTypingOn: function(recipientId){
      console.log("Turning typing indicator on");

      var messageData = {
        recipient: {
          id: recipientId
        },
        sender_action: "typing_on"
      };

      callSendAPI(messageData);
    },
    sendTypingOff: function(recipientId){
      console.log("Turning typing indicator off");

      var messageData = {
        recipient: {
          id: recipientId
        },
        sender_action: "typing_off"
      };

      callSendAPI(messageData);
    },
    sendAccountLinking: function(recipientId){
      var messageData = {
        recipient: {
          id: recipientId
        },
        message: {
          attachment: {
            type: "template",
            payload: {
              template_type: "button",
              text: "Welcome. Link your account.",
              buttons:[{
                type: "account_link",
                url: SERVER_URL + "/authorize"
              }]
            }
          }
        }
      };

      callSendAPI(messageData);
    },
  };
}());
<commit_msg>maybe<commit_after>"use strict";

module.exports = (function(){
  var request = require("request");
  var SERVER_URL = process.env.SERVER_URL;
  var PAGE_ACCESS_TOKEN = process.env.PAGE_ACCESS_TOKEN;

  // This function is basically the engine of this module.
  // All of the sending functions utilize this
  function callSendAPI(messageData){
    console.log("CALLING SEND API");
    console.log(messageData);
    request({
      uri: "https://graph.facebook.com/v2.8/me/messages",
      qs: { access_token: process.env.PAGE_ACCESS_TOKEN },
      method: "POST",
      json: messageData

    }, function (error, response, body) {
      if (!error && response.statusCode === 200) {
        var recipientId = body.recipient_id;
        var messageId = body.message_id;

        if (messageId) {
          console.log("Successfully sent message with id %s to recipient %s", messageId, recipientId);
        } else {
          console.log("Successfully called Send API for recipient %s", recipientId);
        }
      } else {
        console.error("Failed calling Send API", response.statusCode, response.statusMessage, body.error);
      }
    });
  }

  return {
    sendTextMessage: function(recipientId, messageText){
      console.log("SENDING TEXT MESSAGE " + messageText + "\n TO " + recipientId);
      var messageData = {
        recipient: {
          id: recipientId
        },
        message: {
          text: messageText,
          metadata: "DEVELOPER_DEFINED_METADATA"
        }
      };

      callSendAPI(messageData);
    },
    sendImageMessage: function(recipientId){
      var messageData = {
        recipient: {
          id: recipientId
        },
        message: {
          attachment: {
            type: "image",
            payload: {
              url: SERVER_URL + "/assets/rift.png"
            }
          }
        }
      };

      callSendAPI(messageData);
    },
    sendGifMessage: function(recipientId){
      var messageData = {
        recipient: {
          id: recipientId
        },
        message: {
          attachment: {
            type: "image",
            payload: {
              url: SERVER_URL + "/assets/instagram_logo.gif"
            }
          }
        }
      };

      callSendAPI(messageData);
    },
    sendAudioMessage: function(recipientId){
      var messageData = {
        recipient: {
          id: recipientId
        },
        message: {
          attachment: {
            type: "audio",
            payload: {
              url: SERVER_URL + "/assets/sample.mp3"
            }
          }
        }
      };

      callSendAPI(messageData);
    },
    sendVideoMessage: function(recipientId){
      var messageData = {
        recipient: {
          id: recipientId
        },
        message: {
          attachment: {
            type: "video",
            payload: {
              url: SERVER_URL + "/assets/allofus480.mov"
            }
          }
        }
      };

      callSendAPI(messageData);
    },
    sendFileMessage: function(recipientId){
      var messageData = {
        recipient: {
          id: recipientId
        },
        message: {
          attachment: {
            type: "file",
            payload: {
              url: SERVER_URL + "/assets/test.txt"
            }
          }
        }
      };

      callSendAPI(messageData);
    },
    sendButtonMessage: function(recipientId){
      var messageData = {
        recipient: {
          id: recipientId
        },
        message: {
          attachment: {
            type: "template",
            payload: {
              template_type: "button",
              text: "This is test text",
              buttons:[{
                type: "web_url",
                url: "https://www.oculus.com/en-us/rift/",
                title: "Open Web URL"
              }, {
                type: "postback",
                title: "Trigger Postback",
                payload: "DEVELOPED_DEFINED_PAYLOAD"
              }, {
                type: "phone_number",
                title: "Call Phone Number",
                payload: "+16505551234"
              }]
            }
          }
        }
      };

      callSendAPI(messageData);
    },
    sendGenericMessage: function(recipientId){
      var messageData = {
        recipient: {
          id: recipientId
        },
        message: {
          attachment: {
            type: "template",
            payload: {
              template_type: "generic",
              elements: [{
                title: "rift",
                subtitle: "Next-generation virtual reality",
                item_url: "https://www.oculus.com/en-us/rift/",
                image_url: SERVER_URL + "/assets/rift.png",
                buttons: [{
                  type: "web_url",
                  url: "https://www.oculus.com/en-us/rift/",
                  title: "Open Web URL"
                }, {
                  type: "postback",
                  title: "Call Postback",
                  payload: "Payload for first bubble",
                }],
              }, {
                title: "touch",
                subtitle: "Your Hands, Now in VR",
                item_url: "https://www.oculus.com/en-us/touch/",
                image_url: SERVER_URL + "/assets/touch.png",
                buttons: [{
                  type: "web_url",
                  url: "https://www.oculus.com/en-us/touch/",
                  title: "Open Web URL"
                }, {
                  type: "postback",
                  title: "Call Postback",
                  payload: "Payload for second bubble",
                }]
              }]
            }
          }
        }
      };

      callSendAPI(messageData);
    },
    sendReceiptMessage: function(recipientId){
      // Generate a random receipt ID as the API requires a unique ID
      var receiptId = "order" + Math.floor(Math.random()*1000);

      var messageData = {
        recipient: {
          id: recipientId
        },
        message:{
          attachment: {
            type: "template",
            payload: {
              template_type: "receipt",
              recipient_name: "Peter Chang",
              order_number: receiptId,
              currency: "USD",
              payment_method: "Visa 1234",
              timestamp: "1428444852",
              elements: [{
                title: "Oculus Rift",
                subtitle: "Includes: headset, sensor, remote",
                quantity: 1,
                price: 599.00,
                currency: "USD",
                image_url: SERVER_URL + "/assets/riftsq.png"
              }, {
                title: "Samsung Gear VR",
                subtitle: "Frost White",
                quantity: 1,
                price: 99.99,
                currency: "USD",
                image_url: SERVER_URL + "/assets/gearvrsq.png"
              }],
              address: {
                street_1: "1 Hacker Way",
                street_2: "",
                city: "Menlo Park",
                postal_code: "94025",
                state: "CA",
                country: "US"
              },
              summary: {
                subtotal: 698.99,
                shipping_cost: 20.00,
                total_tax: 57.67,
                total_cost: 626.66
              },
              adjustments: [{
                name: "New Customer Discount",
                amount: -50
              }, {
                name: "$100 Off Coupon",
                amount: -100
              }]
            }
          }
        }
      };

      callSendAPI(messageData);
    },
    sendQuickReply: function(recipientId){
      var messageData = {
        recipient: {
          id: recipientId
        },
        message: {
          text: "What's your favorite movie genre?",
          metadata: "DEVELOPER_DEFINED_METADATA",
          quick_replies: [
            {
              "content_type":"text",
              "title":"Action",
              "payload":"DEVELOPER_DEFINED_PAYLOAD_FOR_PICKING_ACTION"
            },
            {
              "content_type":"text",
              "title":"Comedy",
              "payload":"DEVELOPER_DEFINED_PAYLOAD_FOR_PICKING_COMEDY"
            },
            {
              "content_type":"text",
              "title":"Drama",
              "payload":"DEVELOPER_DEFINED_PAYLOAD_FOR_PICKING_DRAMA"
            }
          ]
        }
      };

      callSendAPI(messageData);
    },
    sendReadReceipt: function(recipientId){
      console.log("Sending a read receipt to mark message as seen");

      var messageData = {
        recipient: {
          id: recipientId
        },
        sender_action: "mark_seen"
      };

      callSendAPI(messageData);

    },
    sendTypingOn: function(recipientId){
      console.log("Turning typing indicator on");

      var messageData = {
        recipient: {
          id: recipientId
        },
        sender_action: "typing_on"
      };

      callSendAPI(messageData);
    },
    sendTypingOff: function(recipientId){
      console.log("Turning typing indicator off");

      var messageData = {
        recipient: {
          id: recipientId
        },
        sender_action: "typing_off"
      };

      callSendAPI(messageData);
    },
    sendAccountLinking: function(recipientId){
      var messageData = {
        recipient: {
          id: recipientId
        },
        message: {
          attachment: {
            type: "template",
            payload: {
              template_type: "button",
              text: "Welcome. Link your account.",
              buttons:[{
                type: "account_link",
                url: SERVER_URL + "/authorize"
              }]
            }
          }
        }
      };

      callSendAPI(messageData);
    },
  };
}());
<|endoftext|>


<commit_before>import _throttle from 'lodash-es/throttle';

import { drag as d3_drag } from 'd3-drag';
import {
    select as d3_select,
    event as d3_event,
    selectAll as d3_selectAll
} from 'd3-selection';

import {
    osmEntity,
    osmNote
} from '../osm';

import {
    uiDataEditor,
    uiFeatureList,
    uiInspector,
    uiNoteEditor
} from './index';

import { textDirection } from '../util/locale';

export function uiSidebar(context) {
    var inspector = uiInspector(context);
    var dataEditor = uiDataEditor(context);
    var noteEditor = uiNoteEditor(context);
    var _current;
    var _wasData = false;
    var _wasNote = false;

    function sidebar(selection) {

        var resizer = selection
            .append('div')
            .attr('id', 'sidebar-resizer');

        // set the initial width constraints
        selection.style('min-width', '280px');
        selection.style('max-width', '400px');
        selection.style('width', '33.3333%');

        var container = d3_select('#id-container');
        resizer.call(d3_drag()
            .container(container.node())
            .on('drag', function() {

                var containerWidthPx = container.node().getBoundingClientRect().width;

                var xMarginProperty = textDirection === 'rtl' ? 'margin-right' : 'margin-left';

                // subtact 1px so the mouse stays in the div and maintains the col-resize cursor
                var newWidthPx = textDirection === 'rtl' ? containerWidthPx - d3_event.x-1 : d3_event.x-1;

                var shouldCollapse = newWidthPx < 280;
                container.classed('sidebar-collapsed', shouldCollapse);
                // allow large widths
                selection.style('max-width', '85%');
                if (shouldCollapse) {
                    selection.style(xMarginProperty,'-400px')
                        .style('width', '400px');
                }
                else {

                    var newWidthPercent = (newWidthPx / containerWidthPx) * 100;
                    selection.style(xMarginProperty, null)
                        .style('width', newWidthPercent+'%');
                }
                context.ui().onResize();
            })
        );

        var featureListWrap = selection
            .append('div')
            .attr('class', 'feature-list-pane')
            .call(uiFeatureList(context));

        var inspectorWrap = selection
            .append('div')
            .attr('class', 'inspector-hidden inspector-wrap fr');

        function hover(datum) {
            if (datum && datum.__featurehash__) {   // hovering on data
                _wasData = true;
                sidebar
                    .show(dataEditor.datum(datum));

                selection.selectAll('.sidebar-component')
                    .classed('inspector-hover', true);

            } else if (datum instanceof osmNote) {
                if (context.mode().id === 'drag-note') return;
                _wasNote = true;

                sidebar
                    .show(noteEditor.note(datum));

                selection.selectAll('.sidebar-component')
                    .classed('inspector-hover', true);

            } else if (!_current && (datum instanceof osmEntity)) {
                featureListWrap
                    .classed('inspector-hidden', true);

                inspectorWrap
                    .classed('inspector-hidden', false)
                    .classed('inspector-hover', true);

                if (inspector.entityID() !== datum.id || inspector.state() !== 'hover') {
                    inspector
                        .state('hover')
                        .entityID(datum.id);

                    inspectorWrap
                        .call(inspector);
                }

            } else if (!_current) {
                featureListWrap
                    .classed('inspector-hidden', false);
                inspectorWrap
                    .classed('inspector-hidden', true);
                inspector
                    .state('hide');

            } else if (_wasData || _wasNote) {
                _wasNote = false;
                _wasData = false;
                d3_selectAll('.note').classed('hover', false);
                sidebar.hide();
            }
        }

        sidebar.hover = _throttle(hover, 200);

        sidebar.select = function(id, newFeature) {
            if (!_current && id) {
                // uncollapse the sidebar to show the editor
                sidebar.toggleCollapse(false);

                featureListWrap
                    .classed('inspector-hidden', true);

                inspectorWrap
                    .classed('inspector-hidden', false)
                    .classed('inspector-hover', false);

                if (inspector.entityID() !== id || inspector.state() !== 'select') {
                    inspector
                        .state('select')
                        .entityID(id)
                        .newFeature(newFeature);

                    inspectorWrap
                        .call(inspector);
                }

            } else if (!_current) {
                featureListWrap
                    .classed('inspector-hidden', false);
                inspectorWrap
                    .classed('inspector-hidden', true);
                inspector
                    .state('hide');
            }
        };

        sidebar.show = function(component, element) {
            featureListWrap
                .classed('inspector-hidden', true);
            inspectorWrap
                .classed('inspector-hidden', true);

            if (_current) _current.remove();
            _current = selection
                .append('div')
                .attr('class', 'sidebar-component')
                .call(component, element);
        };

        sidebar.hide = function() {
            featureListWrap
                .classed('inspector-hidden', false);
            inspectorWrap
                .classed('inspector-hidden', true);

            if (_current) _current.remove();
            _current = null;
        };

        sidebar.toggleCollapse = function(shouldCollapse) {

            if (d3_event) {
                d3_event.preventDefault();
            }

            var container = d3_select('#id-container');
            var collapsing;
            var isCollapsed = container.classed('sidebar-collapsed');
            if (typeof shouldCollapse !== 'undefined') {
                if (shouldCollapse === isCollapsed) {
                    return;
                }
                collapsing = shouldCollapse;
            } else {
                collapsing = !isCollapsed;
            }
            var sidebar = d3_select('#sidebar');
            var xMarginProperty = textDirection === 'rtl' ? 'margin-right' : 'margin-left';
            if (collapsing) {
                var preSidebarWidthInPx = sidebar.node().getBoundingClientRect().width;
                sidebar.style('width', preSidebarWidthInPx+'px');
                sidebar.transition()
                    .style('width', '400px')
                    .style(xMarginProperty,'-400px')
                    .on('end',function(){
                        context.ui().onResize();
                    });
                    container.classed('sidebar-collapsed', true);
            }  else {
                var containerWidthPx = container.node().getBoundingClientRect().width;
                var postSidebarWidthInPx = Math.max(containerWidthPx*0.333333, 280);
                sidebar.transition()
                    .style('width', postSidebarWidthInPx)
                    .style(xMarginProperty, '0px')
                    .on('end',function(){
                        sidebar.style('width', '33.3333%');
                        context.ui().onResize();
                    });
                    container.classed('sidebar-collapsed', false);
            }
        };
        // toggle the sidebar collapse when double-clicking the resizer
        resizer.on('dblclick', sidebar.toggleCollapse);
    }

    sidebar.hover = function() {};
    sidebar.hover.cancel = function() {};
    sidebar.select = function() {};
    sidebar.show = function() {};
    sidebar.hide = function() {};
    sidebar.toggleCollapse = function() {};

    return sidebar;
}
<commit_msg>Fixes an error where preventDefault would be called on an event object without that function<commit_after>import _throttle from 'lodash-es/throttle';

import { drag as d3_drag } from 'd3-drag';
import {
    select as d3_select,
    event as d3_event,
    selectAll as d3_selectAll
} from 'd3-selection';

import {
    osmEntity,
    osmNote
} from '../osm';

import {
    uiDataEditor,
    uiFeatureList,
    uiInspector,
    uiNoteEditor
} from './index';

import { textDirection } from '../util/locale';

export function uiSidebar(context) {
    var inspector = uiInspector(context);
    var dataEditor = uiDataEditor(context);
    var noteEditor = uiNoteEditor(context);
    var _current;
    var _wasData = false;
    var _wasNote = false;

    function sidebar(selection) {

        var resizer = selection
            .append('div')
            .attr('id', 'sidebar-resizer');

        // set the initial width constraints
        selection.style('min-width', '280px');
        selection.style('max-width', '400px');
        selection.style('width', '33.3333%');

        var container = d3_select('#id-container');
        resizer.call(d3_drag()
            .container(container.node())
            .on('drag', function() {

                var containerWidthPx = container.node().getBoundingClientRect().width;

                var xMarginProperty = textDirection === 'rtl' ? 'margin-right' : 'margin-left';

                // subtact 1px so the mouse stays in the div and maintains the col-resize cursor
                var newWidthPx = textDirection === 'rtl' ? containerWidthPx - d3_event.x-1 : d3_event.x-1;

                var shouldCollapse = newWidthPx < 280;
                container.classed('sidebar-collapsed', shouldCollapse);
                // allow large widths
                selection.style('max-width', '85%');
                if (shouldCollapse) {
                    selection.style(xMarginProperty,'-400px')
                        .style('width', '400px');
                }
                else {

                    var newWidthPercent = (newWidthPx / containerWidthPx) * 100;
                    selection.style(xMarginProperty, null)
                        .style('width', newWidthPercent+'%');
                }
                context.ui().onResize();
            })
        );

        var featureListWrap = selection
            .append('div')
            .attr('class', 'feature-list-pane')
            .call(uiFeatureList(context));

        var inspectorWrap = selection
            .append('div')
            .attr('class', 'inspector-hidden inspector-wrap fr');

        function hover(datum) {
            if (datum && datum.__featurehash__) {   // hovering on data
                _wasData = true;
                sidebar
                    .show(dataEditor.datum(datum));

                selection.selectAll('.sidebar-component')
                    .classed('inspector-hover', true);

            } else if (datum instanceof osmNote) {
                if (context.mode().id === 'drag-note') return;
                _wasNote = true;

                sidebar
                    .show(noteEditor.note(datum));

                selection.selectAll('.sidebar-component')
                    .classed('inspector-hover', true);

            } else if (!_current && (datum instanceof osmEntity)) {
                featureListWrap
                    .classed('inspector-hidden', true);

                inspectorWrap
                    .classed('inspector-hidden', false)
                    .classed('inspector-hover', true);

                if (inspector.entityID() !== datum.id || inspector.state() !== 'hover') {
                    inspector
                        .state('hover')
                        .entityID(datum.id);

                    inspectorWrap
                        .call(inspector);
                }

            } else if (!_current) {
                featureListWrap
                    .classed('inspector-hidden', false);
                inspectorWrap
                    .classed('inspector-hidden', true);
                inspector
                    .state('hide');

            } else if (_wasData || _wasNote) {
                _wasNote = false;
                _wasData = false;
                d3_selectAll('.note').classed('hover', false);
                sidebar.hide();
            }
        }

        sidebar.hover = _throttle(hover, 200);

        sidebar.select = function(id, newFeature) {
            if (!_current && id) {
                // uncollapse the sidebar to show the editor
                sidebar.toggleCollapse(false);

                featureListWrap
                    .classed('inspector-hidden', true);

                inspectorWrap
                    .classed('inspector-hidden', false)
                    .classed('inspector-hover', false);

                if (inspector.entityID() !== id || inspector.state() !== 'select') {
                    inspector
                        .state('select')
                        .entityID(id)
                        .newFeature(newFeature);

                    inspectorWrap
                        .call(inspector);
                }

            } else if (!_current) {
                featureListWrap
                    .classed('inspector-hidden', false);
                inspectorWrap
                    .classed('inspector-hidden', true);
                inspector
                    .state('hide');
            }
        };

        sidebar.show = function(component, element) {
            featureListWrap
                .classed('inspector-hidden', true);
            inspectorWrap
                .classed('inspector-hidden', true);

            if (_current) _current.remove();
            _current = selection
                .append('div')
                .attr('class', 'sidebar-component')
                .call(component, element);
        };

        sidebar.hide = function() {
            featureListWrap
                .classed('inspector-hidden', false);
            inspectorWrap
                .classed('inspector-hidden', true);

            if (_current) _current.remove();
            _current = null;
        };

        sidebar.toggleCollapse = function(shouldCollapse) {

            if (d3_event && typeof d3_event.preventDefault == 'function') {
                d3_event.preventDefault();
            }

            var container = d3_select('#id-container');
            var collapsing;
            var isCollapsed = container.classed('sidebar-collapsed');
            if (typeof shouldCollapse !== 'undefined') {
                if (shouldCollapse === isCollapsed) {
                    return;
                }
                collapsing = shouldCollapse;
            } else {
                collapsing = !isCollapsed;
            }
            var sidebar = d3_select('#sidebar');
            var xMarginProperty = textDirection === 'rtl' ? 'margin-right' : 'margin-left';
            if (collapsing) {
                var preSidebarWidthInPx = sidebar.node().getBoundingClientRect().width;
                sidebar.style('width', preSidebarWidthInPx+'px');
                sidebar.transition()
                    .style('width', '400px')
                    .style(xMarginProperty,'-400px')
                    .on('end',function(){
                        context.ui().onResize();
                    });
                    container.classed('sidebar-collapsed', true);
            }  else {
                var containerWidthPx = container.node().getBoundingClientRect().width;
                var postSidebarWidthInPx = Math.max(containerWidthPx*0.333333, 280);
                sidebar.transition()
                    .style('width', postSidebarWidthInPx)
                    .style(xMarginProperty, '0px')
                    .on('end',function(){
                        sidebar.style('width', '33.3333%');
                        context.ui().onResize();
                    });
                    container.classed('sidebar-collapsed', false);
            }
        };
        // toggle the sidebar collapse when double-clicking the resizer
        resizer.on('dblclick', sidebar.toggleCollapse);
    }

    sidebar.hover = function() {};
    sidebar.hover.cancel = function() {};
    sidebar.select = function() {};
    sidebar.show = function() {};
    sidebar.hide = function() {};
    sidebar.toggleCollapse = function() {};

    return sidebar;
}
<|endoftext|>


<commit_before>import _throttle from 'lodash-es/throttle';

import { drag as d3_drag } from 'd3-drag';
import { interpolateNumber as d3_interpolateNumber } from 'd3-interpolate';

import {
    select as d3_select,
    event as d3_event,
    selectAll as d3_selectAll
} from 'd3-selection';

import {
    osmEntity,
    osmNote
} from '../osm';

import {
    uiDataEditor,
    uiFeatureList,
    uiInspector,
    uiNoteEditor
} from './index';

import { textDirection } from '../util/locale';

export function uiSidebar(context) {
    var inspector = uiInspector(context);
    var dataEditor = uiDataEditor(context);
    var noteEditor = uiNoteEditor(context);
    var _current;
    var _wasData = false;
    var _wasNote = false;

    function sidebar(selection) {
        var container = d3_select('#id-container');
        var minWidth = 280;
        var sidebarWidth;
        var containerWidth;
        var dragOffset;

        var resizer = selection
            .append('div')
            .attr('id', 'sidebar-resizer');

        // Set the initial width constraints
        selection
            .style('min-width', minWidth + 'px')
            .style('max-width', '400px')
            .style('width', '33.3333%');

        resizer.call(d3_drag()
            .container(container.node())
            .on('start', function() {
                // offset from edge of sidebar-resizer
                dragOffset = d3_event.sourceEvent.offsetX - 1;

                sidebarWidth = selection.node().getBoundingClientRect().width;
                containerWidth = container.node().getBoundingClientRect().width;
                var widthPct = (sidebarWidth / containerWidth) * 100;
                selection
                    .style('width', widthPct + '%')    // lock in current width
                    .style('max-width', '85%');        // but allow larger widths
            })
            .on('drag', function() {
                var isRTL = (textDirection === 'rtl');
                var xMarginProperty = isRTL ? 'margin-right' : 'margin-left';

                var x = d3_event.x - dragOffset;
                sidebarWidth = isRTL ? containerWidth - x : x;

                var isCollapsed = selection.classed('collapsed');
                var shouldCollapse = sidebarWidth < minWidth;

                selection.classed('collapsed', shouldCollapse);

                if (shouldCollapse) {
                    if (!isCollapsed) {
                        selection
                            .style(xMarginProperty, '-400px')
                            .style('width', '400px');

                        context.ui().onResize([sidebarWidth - d3_event.dx, 0]);
                    }

                } else {
                    var widthPct = (sidebarWidth / containerWidth) * 100;
                    selection
                        .style(xMarginProperty, null)
                        .style('width', widthPct + '%');

                    if (isCollapsed) {
                        context.ui().onResize([-sidebarWidth, 0]);
                    } else {
                        context.ui().onResize([-d3_event.dx, 0]);
                    }
                }
            })
        );

        var featureListWrap = selection
            .append('div')
            .attr('class', 'feature-list-pane')
            .call(uiFeatureList(context));

        var inspectorWrap = selection
            .append('div')
            .attr('class', 'inspector-hidden inspector-wrap fr');

        function hover(datum) {
            if (datum && datum.__featurehash__) {   // hovering on data
                _wasData = true;
                sidebar
                    .show(dataEditor.datum(datum));

                selection.selectAll('.sidebar-component')
                    .classed('inspector-hover', true);

            } else if (datum instanceof osmNote) {
                if (context.mode().id === 'drag-note') return;
                _wasNote = true;

                sidebar
                    .show(noteEditor.note(datum));

                selection.selectAll('.sidebar-component')
                    .classed('inspector-hover', true);

            } else if (!_current && (datum instanceof osmEntity)) {
                featureListWrap
                    .classed('inspector-hidden', true);

                inspectorWrap
                    .classed('inspector-hidden', false)
                    .classed('inspector-hover', true);

                if (inspector.entityID() !== datum.id || inspector.state() !== 'hover') {
                    inspector
                        .state('hover')
                        .entityID(datum.id);

                    inspectorWrap
                        .call(inspector);
                }

            } else if (!_current) {
                featureListWrap
                    .classed('inspector-hidden', false);
                inspectorWrap
                    .classed('inspector-hidden', true);
                inspector
                    .state('hide');

            } else if (_wasData || _wasNote) {
                _wasNote = false;
                _wasData = false;
                d3_selectAll('.note').classed('hover', false);
                sidebar.hide();
            }
        }

        sidebar.hover = _throttle(hover, 200);

        sidebar.select = function(id, newFeature) {
            if (!_current && id) {
                // uncollapse the sidebar to show the editor
                sidebar.expand(true);

                featureListWrap
                    .classed('inspector-hidden', true);

                inspectorWrap
                    .classed('inspector-hidden', false)
                    .classed('inspector-hover', false);

                if (inspector.entityID() !== id || inspector.state() !== 'select') {
                    inspector
                        .state('select')
                        .entityID(id)
                        .newFeature(newFeature);

                    inspectorWrap
                        .call(inspector);
                }

            } else if (!_current) {
                featureListWrap
                    .classed('inspector-hidden', false);
                inspectorWrap
                    .classed('inspector-hidden', true);
                inspector
                    .state('hide');
            }
        };

        sidebar.show = function(component, element) {
            featureListWrap
                .classed('inspector-hidden', true);
            inspectorWrap
                .classed('inspector-hidden', true);

            if (_current) _current.remove();
            _current = selection
                .append('div')
                .attr('class', 'sidebar-component')
                .call(component, element);
        };

        sidebar.hide = function() {
            featureListWrap
                .classed('inspector-hidden', false);
            inspectorWrap
                .classed('inspector-hidden', true);

            if (_current) _current.remove();
            _current = null;
        };

        sidebar.expand = function(moveMap) {
            if (selection.classed('collapsed')) {
                sidebar.toggle(moveMap);
            }
        };

        sidebar.collapse = function(moveMap) {
            if (!selection.classed('collapsed')) {
                sidebar.toggle(moveMap);
            }
        };

        sidebar.toggle = function(moveMap) {
            var e = d3_event;
            if (e && e.sourceEvent) {
                e.sourceEvent.preventDefault();
            } else if (e) {
                e.preventDefault();
            }

            var isCollapsed = selection.classed('collapsed');
            var isCollapsing = !isCollapsed;
            var xMarginProperty = textDirection === 'rtl' ? 'margin-right' : 'margin-left';

            sidebarWidth = selection.node().getBoundingClientRect().width;

            // switch from % to px
            selection.style('width', sidebarWidth + 'px');

            var startMargin, endMargin, lastMargin;
            if (isCollapsing) {
                startMargin = lastMargin = 0;
                endMargin = -sidebarWidth;
            } else {
                startMargin = lastMargin = -sidebarWidth;
                endMargin = 0;
            }

            selection.transition()
                .style(xMarginProperty, endMargin + 'px')
                .tween('panner', function() {
                    var i = d3_interpolateNumber(startMargin, endMargin);
                    return function(t) {
                        var dx = lastMargin - Math.round(i(t));
                        lastMargin = lastMargin - dx;
                        context.ui().onResize(moveMap ? undefined : [dx, 0]);
                    };
                })
                .on('end', function() {
                    selection.classed('collapsed', isCollapsing);

                    // switch back from px to %
                    if (!isCollapsing) {
                        var containerWidth = container.node().getBoundingClientRect().width;
                        var widthPct = (sidebarWidth / containerWidth) * 100;
                        selection
                            .style(xMarginProperty, null)
                            .style('width', widthPct + '%');
                    }
                });
        };

        // toggle the sidebar collapse when double-clicking the resizer
        resizer.on('dblclick', sidebar.toggle);
    }

    sidebar.hover = function() {};
    sidebar.hover.cancel = function() {};
    sidebar.select = function() {};
    sidebar.show = function() {};
    sidebar.hide = function() {};
    sidebar.expand = function() {};
    sidebar.collapse = function() {};
    sidebar.toggle = function() {};

    return sidebar;
}
<commit_msg>Avoid obscuring selected entity when expanding the sidebar<commit_after>import _throttle from 'lodash-es/throttle';

import { drag as d3_drag } from 'd3-drag';
import { interpolateNumber as d3_interpolateNumber } from 'd3-interpolate';

import {
    select as d3_select,
    event as d3_event,
    selectAll as d3_selectAll
} from 'd3-selection';

import {
    osmEntity,
    osmNote
} from '../osm';

import {
    uiDataEditor,
    uiFeatureList,
    uiInspector,
    uiNoteEditor
} from './index';

import { textDirection } from '../util/locale';

export function uiSidebar(context) {
    var inspector = uiInspector(context);
    var dataEditor = uiDataEditor(context);
    var noteEditor = uiNoteEditor(context);
    var _current;
    var _wasData = false;
    var _wasNote = false;

    function sidebar(selection) {
        var container = d3_select('#id-container');
        var minWidth = 280;
        var sidebarWidth;
        var containerWidth;
        var dragOffset;

        var resizer = selection
            .append('div')
            .attr('id', 'sidebar-resizer');

        // Set the initial width constraints
        selection
            .style('min-width', minWidth + 'px')
            .style('max-width', '400px')
            .style('width', '33.3333%');

        resizer.call(d3_drag()
            .container(container.node())
            .on('start', function() {
                // offset from edge of sidebar-resizer
                dragOffset = d3_event.sourceEvent.offsetX - 1;

                sidebarWidth = selection.node().getBoundingClientRect().width;
                containerWidth = container.node().getBoundingClientRect().width;
                var widthPct = (sidebarWidth / containerWidth) * 100;
                selection
                    .style('width', widthPct + '%')    // lock in current width
                    .style('max-width', '85%');        // but allow larger widths
            })
            .on('drag', function() {
                var isRTL = (textDirection === 'rtl');
                var xMarginProperty = isRTL ? 'margin-right' : 'margin-left';

                var x = d3_event.x - dragOffset;
                sidebarWidth = isRTL ? containerWidth - x : x;

                var isCollapsed = selection.classed('collapsed');
                var shouldCollapse = sidebarWidth < minWidth;

                selection.classed('collapsed', shouldCollapse);

                if (shouldCollapse) {
                    if (!isCollapsed) {
                        selection
                            .style(xMarginProperty, '-400px')
                            .style('width', '400px');

                        context.ui().onResize([sidebarWidth - d3_event.dx, 0]);
                    }

                } else {
                    var widthPct = (sidebarWidth / containerWidth) * 100;
                    selection
                        .style(xMarginProperty, null)
                        .style('width', widthPct + '%');

                    if (isCollapsed) {
                        context.ui().onResize([-sidebarWidth, 0]);
                    } else {
                        context.ui().onResize([-d3_event.dx, 0]);
                    }
                }
            })
        );

        var featureListWrap = selection
            .append('div')
            .attr('class', 'feature-list-pane')
            .call(uiFeatureList(context));

        var inspectorWrap = selection
            .append('div')
            .attr('class', 'inspector-hidden inspector-wrap fr');

        function hover(datum) {
            if (datum && datum.__featurehash__) {   // hovering on data
                _wasData = true;
                sidebar
                    .show(dataEditor.datum(datum));

                selection.selectAll('.sidebar-component')
                    .classed('inspector-hover', true);

            } else if (datum instanceof osmNote) {
                if (context.mode().id === 'drag-note') return;
                _wasNote = true;

                sidebar
                    .show(noteEditor.note(datum));

                selection.selectAll('.sidebar-component')
                    .classed('inspector-hover', true);

            } else if (!_current && (datum instanceof osmEntity)) {
                featureListWrap
                    .classed('inspector-hidden', true);

                inspectorWrap
                    .classed('inspector-hidden', false)
                    .classed('inspector-hover', true);

                if (inspector.entityID() !== datum.id || inspector.state() !== 'hover') {
                    inspector
                        .state('hover')
                        .entityID(datum.id);

                    inspectorWrap
                        .call(inspector);
                }

            } else if (!_current) {
                featureListWrap
                    .classed('inspector-hidden', false);
                inspectorWrap
                    .classed('inspector-hidden', true);
                inspector
                    .state('hide');

            } else if (_wasData || _wasNote) {
                _wasNote = false;
                _wasData = false;
                d3_selectAll('.note').classed('hover', false);
                sidebar.hide();
            }
        }

        sidebar.hover = _throttle(hover, 200);

        sidebar.intersects = function(extent) {
            var rect = selection.node().getBoundingClientRect();
            return extent.intersects([
                context.projection.invert([0, rect.height]),
                context.projection.invert([rect.width, 0])
            ]);
        };

        sidebar.select = function(id, newFeature) {
            if (!_current && id) {
                // uncollapse the sidebar
                if (selection.classed('collapsed')) {
                    var entity = context.entity(id);
                    var extent = entity.extent(context.graph());
                    sidebar.expand(sidebar.intersects(extent));
                }

                featureListWrap
                    .classed('inspector-hidden', true);

                inspectorWrap
                    .classed('inspector-hidden', false)
                    .classed('inspector-hover', false);

                if (inspector.entityID() !== id || inspector.state() !== 'select') {
                    inspector
                        .state('select')
                        .entityID(id)
                        .newFeature(newFeature);

                    inspectorWrap
                        .call(inspector);
                }

            } else if (!_current) {
                featureListWrap
                    .classed('inspector-hidden', false);
                inspectorWrap
                    .classed('inspector-hidden', true);
                inspector
                    .state('hide');
            }
        };

        sidebar.show = function(component, element) {
            featureListWrap
                .classed('inspector-hidden', true);
            inspectorWrap
                .classed('inspector-hidden', true);

            if (_current) _current.remove();
            _current = selection
                .append('div')
                .attr('class', 'sidebar-component')
                .call(component, element);
        };

        sidebar.hide = function() {
            featureListWrap
                .classed('inspector-hidden', false);
            inspectorWrap
                .classed('inspector-hidden', true);

            if (_current) _current.remove();
            _current = null;
        };

        sidebar.expand = function(moveMap) {
            if (selection.classed('collapsed')) {
                sidebar.toggle(moveMap);
            }
        };

        sidebar.collapse = function(moveMap) {
            if (!selection.classed('collapsed')) {
                sidebar.toggle(moveMap);
            }
        };

        sidebar.toggle = function(moveMap) {
            var e = d3_event;
            if (e && e.sourceEvent) {
                e.sourceEvent.preventDefault();
            } else if (e) {
                e.preventDefault();
            }

            var isCollapsed = selection.classed('collapsed');
            var isCollapsing = !isCollapsed;
            var xMarginProperty = textDirection === 'rtl' ? 'margin-right' : 'margin-left';

            sidebarWidth = selection.node().getBoundingClientRect().width;

            // switch from % to px
            selection.style('width', sidebarWidth + 'px');

            var startMargin, endMargin, lastMargin;
            if (isCollapsing) {
                startMargin = lastMargin = 0;
                endMargin = -sidebarWidth;
            } else {
                startMargin = lastMargin = -sidebarWidth;
                endMargin = 0;
            }

            selection.transition()
                .style(xMarginProperty, endMargin + 'px')
                .tween('panner', function() {
                    var i = d3_interpolateNumber(startMargin, endMargin);
                    return function(t) {
                        var dx = lastMargin - Math.round(i(t));
                        lastMargin = lastMargin - dx;
                        context.ui().onResize(moveMap ? undefined : [dx, 0]);
                    };
                })
                .on('end', function() {
                    selection.classed('collapsed', isCollapsing);

                    // switch back from px to %
                    if (!isCollapsing) {
                        var containerWidth = container.node().getBoundingClientRect().width;
                        var widthPct = (sidebarWidth / containerWidth) * 100;
                        selection
                            .style(xMarginProperty, null)
                            .style('width', widthPct + '%');
                    }
                });
        };

        // toggle the sidebar collapse when double-clicking the resizer
        resizer.on('dblclick', sidebar.toggle);
    }

    sidebar.hover = function() {};
    sidebar.hover.cancel = function() {};
    sidebar.intersects = function() {};
    sidebar.select = function() {};
    sidebar.show = function() {};
    sidebar.hide = function() {};
    sidebar.expand = function() {};
    sidebar.collapse = function() {};
    sidebar.toggle = function() {};

    return sidebar;
}
<|endoftext|>