<commit_before>const noflo = require('noflo');
const postMessageRuntime = require('noflo-runtime-postmessage');
const qs = require('querystring');

const defaultPermissions = [
  'protocol:graph',
  'protocol:component',
  'protocol:network',
  'protocol:runtime',
  'component:getsource',
  'component:setsource',
];

function getParameterByName(name) {
  const params = (new URL(document.location)).searchParams;
  return params.get(name);
}

function getIdeUrl(options, ide = 'https://app.flowhub.io') {
  const params = {
    protocol: 'opener',
    address: window.location.href,
  };
  if (options.id) {
    params.id = options.id;
  }
  const query = qs.stringify(params);
  return `${ide}#runtime/endpoint?${query}`;
}

function loadGraph(json) {
  return new Promise((resolve, reject) => {
    noflo.graph.loadJSON(json, (err, graphInstance) => {
      if (err) {
        reject(err);
        return;
      }
      resolve(graphInstance);
    });
  });
}

function startRuntime(graph, options = {}) {
  return new Promise((resolve, reject) => {
    const protocol = options.protocol || getParameterByName('fbp_protocol') || 'opener';

    const runtimeOptions = {
      ...options.runtimeOptions,
    };
    if (!runtimeOptions.defaultPermissions) {
      runtimeOptions.defaultPermissions = defaultPermissions;
    }

    switch (protocol) {
      case 'opener': {
        if (!options.debugButton) {
          reject(new Error('No debug button defined'));
          return;
        }
        const button = options.debugButton;
        button.classList.replace('nodebug', 'debug');
        button.href = getIdeUrl(runtimeOptions, options.ide);
        resolve(postMessageRuntime.opener(runtimeOptions, button));
        return;
      }
      case 'iframe': {
        resolve(postMessageRuntime.iframe(runtimeOptions));
        return;
      }
      default: {
        reject(new Error(`Unknown FBP protocol ${protocol}`));
      }
    }
  });
}

function startNetwork(runtime, graph, options) {
  const noLoad = options.noLoad || (getParameterByName('fbp_noload') === 'true');
  if (noLoad) {
    return Promise.resolve(runtime);
  }
  return new Promise((resolve, reject) => {
    const graphName = `${runtime.options.namespace || 'default'}/${graph.name || 'main'}`;
    runtime.graph.registerGraph(graphName, graph);
    // eslint-disable-next-line no-underscore-dangle
    runtime.network._startNetwork(graph, graphName, 'none', (err) => {
      if (err) {
        reject(err);
        return;
      }
      runtime.runtime.setMainGraph(graphName, graph);
      resolve(runtime);
    });
  });
}

module.exports = (graph, options) => loadGraph(graph)
  .then(graphInstance => startRuntime(graphInstance, options)
    .then(runtime => startNetwork(runtime, graphInstance, options)));
<commit_msg>Make linter happy:<commit_after>const noflo = require('noflo');
const postMessageRuntime = require('noflo-runtime-postmessage');
const qs = require('querystring');

const defaultPermissions = [
  'protocol:graph',
  'protocol:component',
  'protocol:network',
  'protocol:runtime',
  'component:getsource',
  'component:setsource',
];

function getParameterByName(name) {
  const params = (new URL(document.location)).searchParams;
  return params.get(name);
}

function getIdeUrl(options, ide = 'https://app.flowhub.io') {
  const params = {
    protocol: 'opener',
    address: window.location.href,
  };
  if (options.id) {
    params.id = options.id;
  }
  const query = qs.stringify(params);
  return `${ide}#runtime/endpoint?${query}`;
}

function loadGraph(json) {
  return new Promise((resolve, reject) => {
    noflo.graph.loadJSON(json, (err, graphInstance) => {
      if (err) {
        reject(err);
        return;
      }
      resolve(graphInstance);
    });
  });
}

function startRuntime(graph, options = {}) {
  return new Promise((resolve, reject) => {
    const protocol = options.protocol || getParameterByName('fbp_protocol') || 'opener';

    const runtimeOptions = {
      ...options.runtimeOptions,
    };
    if (!runtimeOptions.defaultPermissions) {
      runtimeOptions.defaultPermissions = defaultPermissions;
    }

    switch (protocol) {
      case 'opener': {
        if (!options.debugButton) {
          reject(new Error('No debug button defined'));
          return;
        }
        const button = options.debugButton;
        button.classList.replace('nodebug', 'debug');
        button.href = getIdeUrl(runtimeOptions, options.ide);
        resolve(postMessageRuntime.opener(runtimeOptions, button));
        return;
      }
      case 'iframe': {
        resolve(postMessageRuntime.iframe(runtimeOptions));
        return;
      }
      default: {
        reject(new Error(`Unknown FBP protocol ${protocol}`));
      }
    }
  });
}

function startNetwork(runtime, graph, options) {
  const noLoad = options.noLoad || (getParameterByName('fbp_noload') === 'true');
  if (noLoad) {
    return Promise.resolve(runtime);
  }
  return new Promise((resolve, reject) => {
    const graphName = `${runtime.options.namespace || 'default'}/${graph.name || 'main'}`;
    runtime.graph.registerGraph(graphName, graph);
    // eslint-disable-next-line no-underscore-dangle
    runtime.network._startNetwork(graph, graphName, 'none', (err) => {
      if (err) {
        reject(err);
        return;
      }
      runtime.runtime.setMainGraph(graphName, graph);
      resolve(runtime);
    });
  });
}

module.exports = (graph, options) => loadGraph(graph)
  .then((graphInstance) => startRuntime(graphInstance, options)
    .then((runtime) => startNetwork(runtime, graphInstance, options)));
<|endoftext|>


<commit_before>/**
 * @author Russell Toris - rctoris@wpi.edu
 * @author Lars Kunze - l.kunze@cs.bham.ac.uk
 */

/**
 * A navigator can be used to add click-to-navigate options to an object. If
 * withOrientation is set to true, the user can also specify the orientation of
 * the robot by clicking at the goal position and pointing into the desired
 * direction (while holding the button pressed).
 *
 * @constructor
 * @param options - object with following keys:
 *   * ros - the ROSLIB.Ros connection handle
 *   * tfClient (optional) - the TF client
 *   * robot_pose (optional) - the robot topic or TF to listen position
 *   * serverName (optional) - the action server name to use for navigation, like '/move_base'
 *   * actionName (optional) - the navigation action name, like 'move_base_msgs/MoveBaseAction'
 *   * rootObject (optional) - the root object to add the click listeners to and render robot markers to
 *   * withOrientation (optional) - if the Navigator should consider the robot orientation (default: false)
 */
NAV2D.Navigator = function(options) {
  var that = this;
  options = options || {};
  var ros = options.ros;
  var tfClient = options.tfClient || null;
  var robot_pose = options.robot_pose || '/robot_pose';
  var serverName = options.serverName || '/move_base';
  var actionName = options.actionName || 'move_base_msgs/MoveBaseAction';
  var withOrientation = options.withOrientation || false;
  this.rootObject = options.rootObject || new createjs.Container();

  // setup the actionlib client
  var actionClient = new ROSLIB.ActionClient({
    ros : ros,
    actionName : actionName,
    serverName : serverName
  });

  /**
   * Send a goal to the navigation stack with the given pose.
   *
   * @param pose - the goal pose
   */
  function sendGoal(pose) {
    // create a goal
    var goal = new ROSLIB.Goal({
      actionClient : actionClient,
      goalMessage : {
        target_pose : {
          header : {
            frame_id : '/map'
          },
          pose : pose
        }
      }
    });
    goal.send();

    // create a marker for the goal
    var goalMarker = new ROS2D.NavigationArrow({
      size : 15,
      strokeSize : 1,
      fillColor : createjs.Graphics.getRGB(255, 64, 128, 0.66),
      pulse : true
    });
    goalMarker.x = pose.position.x;
    goalMarker.y = -pose.position.y;
    goalMarker.rotation = stage.rosQuaternionToGlobalTheta(pose.orientation);
    goalMarker.scaleX = 1.0 / stage.scaleX;
    goalMarker.scaleY = 1.0 / stage.scaleY;
    that.rootObject.addChild(goalMarker);

    goal.on('result', function() {
      that.rootObject.removeChild(goalMarker);
    });
  }

  // get a handle to the stage
  var stage;
  if (that.rootObject instanceof createjs.Stage) {
    stage = that.rootObject;
  } else {
    stage = that.rootObject.getStage();
  }

  // marker for the robot
  var robotMarker = new ROS2D.NavigationArrow({
    size : 25,
    strokeSize : 1,
    fillColor : createjs.Graphics.getRGB(255, 128, 0, 0.66),
    pulse : true
  });
  // wait for a pose to come in first
  robotMarker.visible = false;
  this.rootObject.addChild(robotMarker);
  var initScaleSet = false;

  var updateRobotPosition = function(pose, orientation) {
    // update the robots position on the map
    robotMarker.x = pose.x;
    robotMarker.y = -pose.y;
    if (!initScaleSet) {
      robotMarker.scaleX = 1.0 / stage.scaleX;
      robotMarker.scaleY = 1.0 / stage.scaleY;
      initScaleSet = true;
    }
    // change the angle
    robotMarker.rotation = stage.rosQuaternionToGlobalTheta(orientation);
    // Set visible
    robotMarker.visible = true;
  }

  if(tfClient !== null) {
    tfClient.subscribe(robot_pose, function(tf) {
      updateRobotPosition(tf.translation,tf.rotation);
    });
  } else {
    // setup a listener for the robot pose
    var poseListener = new ROSLIB.Topic({
      ros: ros,
      name: robot_pose,
      messageType: 'geometry_msgs/Pose',
      throttle_rate: 100
    });
    poseListener.subscribe(function(pose) {
      updateRobotPosition(pose.position,pose.orientation);
    });
  }

  if (withOrientation === false){
    // setup a double click listener (no orientation)
    this.rootObject.addEventListener('dblclick', function(event) {
      // convert to ROS coordinates
      var coords = stage.globalToRos(event.stageX, event.stageY);
      var pose = new ROSLIB.Pose({
        position : new ROSLIB.Vector3(coords)
      });
      // send the goal
      sendGoal(pose);
    });
  } else { // withOrientation === true
    // setup a click-and-point listener (with orientation)
    var position = null;
    var positionVec3 = null;
    var thetaRadians = 0;
    var thetaDegrees = 0;
    var orientationMarker = null;
    var mouseDown = false;
    var xDelta = 0;
    var yDelta = 0;

    var mouseEventHandler = function(event, mouseState) {

      if (mouseState === 'down'){
        // get position when mouse button is pressed down
        position = stage.globalToRos(event.stageX, event.stageY);
        positionVec3 = new ROSLIB.Vector3(position);
        mouseDown = true;
      }
      else if (mouseState === 'move'){
        // remove obsolete orientation marker
        that.rootObject.removeChild(orientationMarker);

        if ( mouseDown === true) {
          // if mouse button is held down:
          // - get current mouse position
          // - calulate direction between stored <position> and current position
          // - place orientation marker
          var currentPos = stage.globalToRos(event.stageX, event.stageY);
          var currentPosVec3 = new ROSLIB.Vector3(currentPos);

          orientationMarker = new ROS2D.NavigationArrow({
            size : 25,
            strokeSize : 1,
            fillColor : createjs.Graphics.getRGB(0, 255, 0, 0.66),
            pulse : false
          });

          xDelta =  currentPosVec3.x - positionVec3.x;
          yDelta =  currentPosVec3.y - positionVec3.y;

          thetaRadians  = Math.atan2(xDelta,yDelta);

          thetaDegrees = thetaRadians * (180.0 / Math.PI);

          if (thetaDegrees >= 0 && thetaDegrees <= 180) {
            thetaDegrees += 270;
          } else {
            thetaDegrees -= 90;
          }

          orientationMarker.x =  positionVec3.x;
          orientationMarker.y = -positionVec3.y;
          orientationMarker.rotation = thetaDegrees;
          orientationMarker.scaleX = 1.0 / stage.scaleX;
          orientationMarker.scaleY = 1.0 / stage.scaleY;

          that.rootObject.addChild(orientationMarker);
        }
      } else if (mouseDown) { // mouseState === 'up'
        // if mouse button is released
        // - get current mouse position (goalPos)
        // - calulate direction between stored <position> and goal position
        // - set pose with orientation
        // - send goal
        mouseDown = false;

        var goalPos = stage.globalToRos(event.stageX, event.stageY);

        var goalPosVec3 = new ROSLIB.Vector3(goalPos);

        xDelta =  goalPosVec3.x - positionVec3.x;
        yDelta =  goalPosVec3.y - positionVec3.y;

        thetaRadians  = Math.atan2(xDelta,yDelta);

        if (thetaRadians >= 0 && thetaRadians <= Math.PI) {
          thetaRadians += (3 * Math.PI / 2);
        } else {
          thetaRadians -= (Math.PI/2);
        }

        var qz =  Math.sin(-thetaRadians/2.0);
        var qw =  Math.cos(-thetaRadians/2.0);

        var orientation = new ROSLIB.Quaternion({x:0, y:0, z:qz, w:qw});

        var pose = new ROSLIB.Pose({
          position :    positionVec3,
          orientation : orientation
        });
        // send the goal
        sendGoal(pose);
      }
    };

    this.rootObject.addEventListener('stagemousedown', function(event) {
      mouseEventHandler(event,'down');
    });

    this.rootObject.addEventListener('stagemousemove', function(event) {
      mouseEventHandler(event,'move');
    });

    this.rootObject.addEventListener('stagemouseup', function(event) {
      mouseEventHandler(event,'up');
    });
  }
};
<commit_msg>Added a missing semicolon<commit_after>/**
 * @author Russell Toris - rctoris@wpi.edu
 * @author Lars Kunze - l.kunze@cs.bham.ac.uk
 */

/**
 * A navigator can be used to add click-to-navigate options to an object. If
 * withOrientation is set to true, the user can also specify the orientation of
 * the robot by clicking at the goal position and pointing into the desired
 * direction (while holding the button pressed).
 *
 * @constructor
 * @param options - object with following keys:
 *   * ros - the ROSLIB.Ros connection handle
 *   * tfClient (optional) - the TF client
 *   * robot_pose (optional) - the robot topic or TF to listen position
 *   * serverName (optional) - the action server name to use for navigation, like '/move_base'
 *   * actionName (optional) - the navigation action name, like 'move_base_msgs/MoveBaseAction'
 *   * rootObject (optional) - the root object to add the click listeners to and render robot markers to
 *   * withOrientation (optional) - if the Navigator should consider the robot orientation (default: false)
 */
NAV2D.Navigator = function(options) {
  var that = this;
  options = options || {};
  var ros = options.ros;
  var tfClient = options.tfClient || null;
  var robot_pose = options.robot_pose || '/robot_pose';
  var serverName = options.serverName || '/move_base';
  var actionName = options.actionName || 'move_base_msgs/MoveBaseAction';
  var withOrientation = options.withOrientation || false;
  this.rootObject = options.rootObject || new createjs.Container();

  // setup the actionlib client
  var actionClient = new ROSLIB.ActionClient({
    ros : ros,
    actionName : actionName,
    serverName : serverName
  });

  /**
   * Send a goal to the navigation stack with the given pose.
   *
   * @param pose - the goal pose
   */
  function sendGoal(pose) {
    // create a goal
    var goal = new ROSLIB.Goal({
      actionClient : actionClient,
      goalMessage : {
        target_pose : {
          header : {
            frame_id : '/map'
          },
          pose : pose
        }
      }
    });
    goal.send();

    // create a marker for the goal
    var goalMarker = new ROS2D.NavigationArrow({
      size : 15,
      strokeSize : 1,
      fillColor : createjs.Graphics.getRGB(255, 64, 128, 0.66),
      pulse : true
    });
    goalMarker.x = pose.position.x;
    goalMarker.y = -pose.position.y;
    goalMarker.rotation = stage.rosQuaternionToGlobalTheta(pose.orientation);
    goalMarker.scaleX = 1.0 / stage.scaleX;
    goalMarker.scaleY = 1.0 / stage.scaleY;
    that.rootObject.addChild(goalMarker);

    goal.on('result', function() {
      that.rootObject.removeChild(goalMarker);
    });
  }

  // get a handle to the stage
  var stage;
  if (that.rootObject instanceof createjs.Stage) {
    stage = that.rootObject;
  } else {
    stage = that.rootObject.getStage();
  }

  // marker for the robot
  var robotMarker = new ROS2D.NavigationArrow({
    size : 25,
    strokeSize : 1,
    fillColor : createjs.Graphics.getRGB(255, 128, 0, 0.66),
    pulse : true
  });
  // wait for a pose to come in first
  robotMarker.visible = false;
  this.rootObject.addChild(robotMarker);
  var initScaleSet = false;

  var updateRobotPosition = function(pose, orientation) {
    // update the robots position on the map
    robotMarker.x = pose.x;
    robotMarker.y = -pose.y;
    if (!initScaleSet) {
      robotMarker.scaleX = 1.0 / stage.scaleX;
      robotMarker.scaleY = 1.0 / stage.scaleY;
      initScaleSet = true;
    }
    // change the angle
    robotMarker.rotation = stage.rosQuaternionToGlobalTheta(orientation);
    // Set visible
    robotMarker.visible = true;
  };

  if(tfClient !== null) {
    tfClient.subscribe(robot_pose, function(tf) {
      updateRobotPosition(tf.translation,tf.rotation);
    });
  } else {
    // setup a listener for the robot pose
    var poseListener = new ROSLIB.Topic({
      ros: ros,
      name: robot_pose,
      messageType: 'geometry_msgs/Pose',
      throttle_rate: 100
    });
    poseListener.subscribe(function(pose) {
      updateRobotPosition(pose.position,pose.orientation);
    });
  }

  if (withOrientation === false){
    // setup a double click listener (no orientation)
    this.rootObject.addEventListener('dblclick', function(event) {
      // convert to ROS coordinates
      var coords = stage.globalToRos(event.stageX, event.stageY);
      var pose = new ROSLIB.Pose({
        position : new ROSLIB.Vector3(coords)
      });
      // send the goal
      sendGoal(pose);
    });
  } else { // withOrientation === true
    // setup a click-and-point listener (with orientation)
    var position = null;
    var positionVec3 = null;
    var thetaRadians = 0;
    var thetaDegrees = 0;
    var orientationMarker = null;
    var mouseDown = false;
    var xDelta = 0;
    var yDelta = 0;

    var mouseEventHandler = function(event, mouseState) {

      if (mouseState === 'down'){
        // get position when mouse button is pressed down
        position = stage.globalToRos(event.stageX, event.stageY);
        positionVec3 = new ROSLIB.Vector3(position);
        mouseDown = true;
      }
      else if (mouseState === 'move'){
        // remove obsolete orientation marker
        that.rootObject.removeChild(orientationMarker);

        if ( mouseDown === true) {
          // if mouse button is held down:
          // - get current mouse position
          // - calulate direction between stored <position> and current position
          // - place orientation marker
          var currentPos = stage.globalToRos(event.stageX, event.stageY);
          var currentPosVec3 = new ROSLIB.Vector3(currentPos);

          orientationMarker = new ROS2D.NavigationArrow({
            size : 25,
            strokeSize : 1,
            fillColor : createjs.Graphics.getRGB(0, 255, 0, 0.66),
            pulse : false
          });

          xDelta =  currentPosVec3.x - positionVec3.x;
          yDelta =  currentPosVec3.y - positionVec3.y;

          thetaRadians  = Math.atan2(xDelta,yDelta);

          thetaDegrees = thetaRadians * (180.0 / Math.PI);

          if (thetaDegrees >= 0 && thetaDegrees <= 180) {
            thetaDegrees += 270;
          } else {
            thetaDegrees -= 90;
          }

          orientationMarker.x =  positionVec3.x;
          orientationMarker.y = -positionVec3.y;
          orientationMarker.rotation = thetaDegrees;
          orientationMarker.scaleX = 1.0 / stage.scaleX;
          orientationMarker.scaleY = 1.0 / stage.scaleY;

          that.rootObject.addChild(orientationMarker);
        }
      } else if (mouseDown) { // mouseState === 'up'
        // if mouse button is released
        // - get current mouse position (goalPos)
        // - calulate direction between stored <position> and goal position
        // - set pose with orientation
        // - send goal
        mouseDown = false;

        var goalPos = stage.globalToRos(event.stageX, event.stageY);

        var goalPosVec3 = new ROSLIB.Vector3(goalPos);

        xDelta =  goalPosVec3.x - positionVec3.x;
        yDelta =  goalPosVec3.y - positionVec3.y;

        thetaRadians  = Math.atan2(xDelta,yDelta);

        if (thetaRadians >= 0 && thetaRadians <= Math.PI) {
          thetaRadians += (3 * Math.PI / 2);
        } else {
          thetaRadians -= (Math.PI/2);
        }

        var qz =  Math.sin(-thetaRadians/2.0);
        var qw =  Math.cos(-thetaRadians/2.0);

        var orientation = new ROSLIB.Quaternion({x:0, y:0, z:qz, w:qw});

        var pose = new ROSLIB.Pose({
          position :    positionVec3,
          orientation : orientation
        });
        // send the goal
        sendGoal(pose);
      }
    };

    this.rootObject.addEventListener('stagemousedown', function(event) {
      mouseEventHandler(event,'down');
    });

    this.rootObject.addEventListener('stagemousemove', function(event) {
      mouseEventHandler(event,'move');
    });

    this.rootObject.addEventListener('stagemouseup', function(event) {
      mouseEventHandler(event,'up');
    });
  }
};
<|endoftext|>


<commit_before>// ==========================================================================
//
//  Author:   jsandoe
//
//  Copyright (c) 2016 by The Concord Consortium, Inc. All rights reserved.
//
//  Licensed under the Apache License, Version 2.0 (the "License");
//  you may not use this file except in compliance with the License.
//  You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.
// ==========================================================================
/* global Slick:true */
/**
 * @class CaseTableDataManager
 *
 * A specialized, on demand, implementation the SlickGrid DataView interface.
 * See https://github.com/mleibman/SlickGrid/wiki/DataView.
 * We implement getLength() and getItem(index) from the API.
 *
 * @extends SC.Object
 */
DG.CaseTableDataManager = SC.Object.extend({

  /**
   * @type {DG.DataContext}
   */
  context: null,

  /**
   * The collection that this case table is presenting.
   * @type {DG.CollectionClient}
   */
  collection: null,

  /**
   * @type {DG.CaseTableModel}
   */
  model: null,

  /**
   * @type {boolean}
   */
  suspended: false,

  /**
   * Cache of mappings from row index to case.
   * @type {[DG.Case]}
   */
  _rowCaseMap: null,

  /**
   *
   */
  _collapsedNodeInfoCache: null,

  caseTableLength: function () {
    return this._rowCaseMap.length;
  }.property(),

  init: function () {
    this._rowCaseMap = [];
    this.onRowCountChanged = new Slick.Event();
    this.onRowsChanged = new Slick.Event();
  },

  /**
   * Returns the number of rows in the table.
   *
   * Signature defined by Slick Grid DataView API.
   *
   * Implements a method of the SlickGrid DataView API.
   * The number of rows counts a collapsed group as a
   * single row.
   *
   * @returns {Number}
   */
  getLength: function () {
    return this.get('caseTableLength');
  },

  /**
   * Returns the contents of the given row as a Case.
   *
   * If this particular row is a collapsed group indicator, returns the collapsed
   * case, which is a member of a parent or other ancestor collection. We can
   * tell, then, that the row corresponds to a collapsed group by comparing the
   * case's collection with the case table's collection.
   *
   * Signature defined by Slick Grid DataView API.
   *
   * @param row {number}
   * @returns {DG.Case}
   */
  getItem: function (row) {
    return this._rowCaseMap[row];
  },

  /**
   * Returns metadata for the selected row, if any.
   *
   * We use this API to construct collapsed rows.
   *
   * Signature defined by Slick Grid DataView API.
   *
   * @param row {number}
   * @returns {object} See slickgrid documentation.
   */
  getItemMetadata: function (row) {
    var myCase = this.getItem(row);
    if (myCase.collection.get('id') !== this.collection.get('id')) {
      return {
        columns: {
          0: {
            colspan: "*"
          }
        },
        formatter: function (row, cell, cellValue, colInfo, rowItem) {
          var caseCount = this.subcaseCount(this._rowCaseMap[row]);
          var setName = this.context.getCaseNameForCount(this.collection, 2);
          return '%@ %@'.loc(caseCount, setName);
        }.bind(this)
      };
    }
  },

  /**
   * Resets this object in response to state changes such as:
   *   expand or collapse of a case, addition or deletion of a case.
   *
   * Method signature matches method of Slick Grid default DataView.
   */
  refresh: function () {
    // returns an array of this collection and its ancestor collections.
    function makeCollections(iCollection) {
      var arr = [];
      var col = iCollection;
      var colID;
      while ( col ) {
        arr.push(col);
        colID = col.getParentCollectionID();
        if (!SC.none(colID)) {
          col = context.getCollectionByID(colID);
        } else {
          col = null;
        }
      }
      return arr.reverse();
    }
    // recursively visits a set of nodes assembling a row index to case mapping.
    function visit(nodes, level) {
      nodes.forEach(function (node) {
        if (level === collections.length - 1) {
          rowCaseIndex.push(node);
        } else {
          if (model.isCollapsedNode(node)) {
            rowCaseIndex.push(node);
          } else {
            visit(node.get('children'), level + 1);
          }
        }
      });
    }
    // ---- BEGIN ----
    if (this.get('suspended')) {
      this.set('refreshNeeded', true);
      return;
    }
    var beforeCount = this._rowCaseMap.length;
    var rowCaseIndex = [];
    var myCollection = this.collection;
    var context = this.context;
    var model = this.model;
    var collections = makeCollections(myCollection);
    visit(collections[0].casesController, 0);
    this._rowCaseMap = rowCaseIndex;
    if (rowCaseIndex !== beforeCount) {
      this.onRowCountChanged.notify({previous: beforeCount, current: rowCaseIndex.length}, null, this);
    }
    this.set('refreshNeeded', false);
    //this.onRowsChanged.notify({rows: {}}, null, self);
  },

  /**
   * Event handler fired when Row data changes.
   *
   * Initialized in init to avoid issues resolving slickgrid library.
   *
   * Method signature matches method of Slick Grid default DataView.
   */
  onRowsChanged: null,

  /**
   * Event handler fired when row count changes.
   *
   * Initialized in init to avoid issues resolving slickgrid library.
   *
   * Method signature matches method of Slick Grid default DataView.
   */
  onRowCountChanged: null,

  /**
   * Called before a sequence of operations to avoid premature refreshes.
   *
   * Method signature matches method of Slick Grid default DataView.
   */
  beginUpdate: function () {
    this.set('suspended', true);
  },

  /**
   * Called at the end of a sequence of operations.
   *
   * Invokes refresh if necessary.
   *
   * Method signature matches method of Slick Grid default DataView.
   */
  endUpdate: function () {
    this.set('suspended', false);
    if (this.get('refreshNeeded')) {
      this.refresh();
    }
  },

  /**
   * No-op
   *
   * Method signature matches method of Slick Grid default DataView.
   */
  setItems: function (arr) {},

  /** Unsupported
   *
   * Method signature matches method of Slick Grid default DataView.
   *
   * @returns {null}
   */
  //getItems: function () {
  //  return this.collection.casesController;
  //},

  /**
   * Searches for an item by its id.
   *
   * Method signature matches method of Slick Grid default DataView.
   */
  getItemById: function (id) {
    return this.collection.getCaseByID(id);
  },

  /**
   * Returns the case index of a case by id.
   *
   * Method signature matches method of Slick Grid default DataView.
   * @param id
   * @returns {*|number|undefined}
   */
  getIdxById: function (id) {
    return this.collection.getCaseIndexByID(id);
  },

  /**
   * Gets the index of the case table row, given a case id.
   *
   *
   * Method signature matches method of Slick Grid default DataView.
   *
   * Note that a case table that is part of a collapsed group is not considered
   * to be a part of the table.
   *
   * @param iCaseID {number}
   * @returns {Number}
   */
  getRowById: function (iCaseID) {
    var myCase = DG.store.find('DG.Case', iCaseID);

    return this._rowCaseMap.indexOf(myCase);
  },

  /**
   * No-op
   *
   * Method signature matches method of Slick Grid default DataView.
   */
  insertItem: function (item) {},

  /**
   * No-op
   *
   * Method signature matches method of Slick Grid default DataView.
   */
  addItem: function (item) {},

  /**
   * No-op
   *
   * Method signature matches method of Slick Grid default DataView.
   */
  updateItem: function (id, item) {},

  /**
   * No-op
   *
   * Method signature matches method of Slick Grid default DataView.
   */
  setGrouping: function (obj) {},

  /**
   * Marks the identified case as collapsed.
   *
   * Method signature matches method of Slick Grid default DataView.
   * @param iCaseID {number}
   */
  collapseGroup: function (iCaseID) {

    var myCase = DG.store.find('DG.Case', iCaseID);
    if (myCase) {
      this.model.collapseNode(myCase);
    }
  },

  /**
   * Marks the identified case as expanded.
   *
   * Method signature matches method of Slick Grid default DataView.
   * @param iCaseID {number} A case id.
   */
  expandGroup: function (iCaseID) {
    var myCase = DG.store.find('DG.Case', iCaseID);
    if (myCase) {
      this.model.expandNode(myCase);
    }
  },

  /**
   * Whether the indicated node is collapsed.
   *
   * Method signature matches method of Slick Grid default DataView.
   *
   * @param iCaseID {number} A case id.
   * @returns {boolean}
   */
  isGroupCollapsed: function (iCaseID) {
    var myCase = this.collection.getCaseByID(iCaseID);
    if (myCase) {
      return this.model.isCollapsedNode(myCase);
    }
  },

  /**
   * No-op
   *
   * Method signature matches method of Slick Grid default DataView.
   */
  setRefreshHints: function (obj) {},

  /**
   * A property defining the expand and collapsed counts for this collection.
   *
   * Used for determining the state of the expand all/collapse all buttons.
   */
  expandCollapseCounts: function () {
    var model = this.model;
    var counts = {
      expanded: 0,
      collapsed: 0
    };
    this.collection.casesController.forEach(function (myCase) {
      if (model.isCollapsedNode(myCase)) {
        counts.collapsed += 1;
      } else {
        counts.expanded += 1;
      }
    });
    return counts;
  }.property(),

  /**
   * Counts the cases at this level of the case table hierarchy which descend
   * from the given case.
   *
   * Used for creating a collapsed row message.
   *
   * @param iCase
   * @returns {number}
   */
  subcaseCount: function (iCase) {
    var count = 0;
    if (iCase.collection.id === this.collection.get('id')) {
      count = 1;
    } else {
      iCase.children.forEach(function (myCase) {
        count += this.subcaseCount(myCase);
      }.bind(this));
    }
    return count;
  }
});<commit_msg>Case table data manager was not responding to external update case notification.<commit_after>// ==========================================================================
//
//  Author:   jsandoe
//
//  Copyright (c) 2016 by The Concord Consortium, Inc. All rights reserved.
//
//  Licensed under the Apache License, Version 2.0 (the "License");
//  you may not use this file except in compliance with the License.
//  You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.
// ==========================================================================
/* global Slick:true */
/**
 * @class CaseTableDataManager
 *
 * A specialized, on demand, implementation the SlickGrid DataView interface.
 * See https://github.com/mleibman/SlickGrid/wiki/DataView.
 * We implement getLength() and getItem(index) from the API.
 *
 * @extends SC.Object
 */
DG.CaseTableDataManager = SC.Object.extend({

  /**
   * @type {DG.DataContext}
   */
  context: null,

  /**
   * The collection that this case table is presenting.
   * @type {DG.CollectionClient}
   */
  collection: null,

  /**
   * @type {DG.CaseTableModel}
   */
  model: null,

  /**
   * @type {boolean}
   */
  suspended: false,

  /**
   * Cache of mappings from row index to case.
   * @type {[DG.Case]}
   */
  _rowCaseMap: null,

  /**
   *
   */
  _collapsedNodeInfoCache: null,

  caseTableLength: function () {
    return this._rowCaseMap.length;
  }.property(),

  init: function () {
    this._rowCaseMap = [];
    this.onRowCountChanged = new Slick.Event();
    this.onRowsChanged = new Slick.Event();
  },

  /**
   * Returns the number of rows in the table.
   *
   * Signature defined by Slick Grid DataView API.
   *
   * Implements a method of the SlickGrid DataView API.
   * The number of rows counts a collapsed group as a
   * single row.
   *
   * @returns {Number}
   */
  getLength: function () {
    return this.get('caseTableLength');
  },

  /**
   * Returns the contents of the given row as a Case.
   *
   * If this particular row is a collapsed group indicator, returns the collapsed
   * case, which is a member of a parent or other ancestor collection. We can
   * tell, then, that the row corresponds to a collapsed group by comparing the
   * case's collection with the case table's collection.
   *
   * Signature defined by Slick Grid DataView API.
   *
   * @param row {number}
   * @returns {DG.Case}
   */
  getItem: function (row) {
    return this._rowCaseMap[row];
  },

  /**
   * Returns metadata for the selected row, if any.
   *
   * We use this API to construct collapsed rows.
   *
   * Signature defined by Slick Grid DataView API.
   *
   * @param row {number}
   * @returns {object} See slickgrid documentation.
   */
  getItemMetadata: function (row) {
    var myCase = this.getItem(row);
    if (myCase.collection.get('id') !== this.collection.get('id')) {
      return {
        columns: {
          0: {
            colspan: "*"
          }
        },
        formatter: function (row, cell, cellValue, colInfo, rowItem) {
          var caseCount = this.subcaseCount(this._rowCaseMap[row]);
          var setName = this.context.getCaseNameForCount(this.collection, 2);
          return '%@ %@'.loc(caseCount, setName);
        }.bind(this)
      };
    }
  },

  /**
   * Resets this object in response to state changes such as:
   *   expand or collapse of a case, addition or deletion of a case.
   *
   * Method signature matches method of Slick Grid default DataView.
   */
  refresh: function () {
    // returns an array of this collection and its ancestor collections.
    function makeCollections(iCollection) {
      var arr = [];
      var col = iCollection;
      var colID;
      while ( col ) {
        arr.push(col);
        colID = col.getParentCollectionID();
        if (!SC.none(colID)) {
          col = context.getCollectionByID(colID);
        } else {
          col = null;
        }
      }
      return arr.reverse();
    }
    // recursively visits a set of nodes assembling a row index to case mapping.
    function visit(nodes, level) {
      nodes.forEach(function (node) {
        if (level === collections.length - 1) {
          rowCaseIndex.push(node);
        } else {
          if (model.isCollapsedNode(node)) {
            rowCaseIndex.push(node);
          } else {
            visit(node.get('children'), level + 1);
          }
        }
      });
    }
    // ---- BEGIN ----
    if (this.get('suspended')) {
      this.set('refreshNeeded', true);
      return;
    }
    var beforeCount = this._rowCaseMap.length;
    var rowCaseIndex = [];
    var myCollection = this.collection;
    var context = this.context;
    var model = this.model;
    var collections = makeCollections(myCollection);
    visit(collections[0].casesController, 0);
    this._rowCaseMap = rowCaseIndex;
    if (rowCaseIndex !== beforeCount) {
      this.onRowCountChanged.notify({previous: beforeCount, current: rowCaseIndex.length}, null, this);
    }
    this.set('refreshNeeded', false);
    //this.onRowsChanged.notify({rows: {}}, null, self);
  },

  /**
   * Event handler fired when Row data changes.
   *
   * Initialized in init to avoid issues resolving slickgrid library.
   *
   * Method signature matches method of Slick Grid default DataView.
   */
  onRowsChanged: null,

  /**
   * Event handler fired when row count changes.
   *
   * Initialized in init to avoid issues resolving slickgrid library.
   *
   * Method signature matches method of Slick Grid default DataView.
   */
  onRowCountChanged: null,

  /**
   * Called before a sequence of operations to avoid premature refreshes.
   *
   * Method signature matches method of Slick Grid default DataView.
   */
  beginUpdate: function () {
    this.set('suspended', true);
  },

  /**
   * Called at the end of a sequence of operations.
   *
   * Invokes refresh if necessary.
   *
   * Method signature matches method of Slick Grid default DataView.
   */
  endUpdate: function () {
    this.set('suspended', false);
    if (this.get('refreshNeeded')) {
      this.refresh();
    }
  },

  /**
   * No-op
   *
   * Method signature matches method of Slick Grid default DataView.
   */
  setItems: function (arr) {},

  /** Unsupported
   *
   * Method signature matches method of Slick Grid default DataView.
   *
   * @returns {null}
   */
  //getItems: function () {
  //  return this.collection.casesController;
  //},

  /**
   * Searches for an item by its id.
   *
   * Method signature matches method of Slick Grid default DataView.
   */
  getItemById: function (id) {
    return this.collection.getCaseByID(id);
  },

  /**
   * Returns the case index of a case by id.
   *
   * Method signature matches method of Slick Grid default DataView.
   * @param id
   * @returns {*|number|undefined}
   */
  getIdxById: function (id) {
    return this.collection.getCaseIndexByID(id);
  },

  /**
   * Gets the index of the case table row, given a case id.
   *
   *
   * Method signature matches method of Slick Grid default DataView.
   *
   * Note that a case table that is part of a collapsed group is not considered
   * to be a part of the table.
   *
   * @param iCaseID {number}
   * @returns {Number}
   */
  getRowById: function (iCaseID) {
    var myCase = DG.store.find('DG.Case', iCaseID);

    return this._rowCaseMap.indexOf(myCase);
  },

  /**
   * No-op
   *
   * Method signature matches method of Slick Grid default DataView.
   */
  insertItem: function (item) {},

  /**
   * No-op
   *
   * Method signature matches method of Slick Grid default DataView.
   */
  addItem: function (item) {},

  /**
   * Called for updateCase.
   *
   * We simply refresh.
   * TODO: consider refreshing only if the item is visible.
   *
   * Method signature matches method of Slick Grid default DataView.
   */
  updateItem: function (id, item) {
    this.refresh();
  },

  /**
   * No-op
   *
   * Method signature matches method of Slick Grid default DataView.
   */
  setGrouping: function (obj) {},

  /**
   * Marks the identified case as collapsed.
   *
   * Method signature matches method of Slick Grid default DataView.
   * @param iCaseID {number}
   */
  collapseGroup: function (iCaseID) {

    var myCase = DG.store.find('DG.Case', iCaseID);
    if (myCase) {
      this.model.collapseNode(myCase);
    }
  },

  /**
   * Marks the identified case as expanded.
   *
   * Method signature matches method of Slick Grid default DataView.
   * @param iCaseID {number} A case id.
   */
  expandGroup: function (iCaseID) {
    var myCase = DG.store.find('DG.Case', iCaseID);
    if (myCase) {
      this.model.expandNode(myCase);
    }
  },

  /**
   * Whether the indicated node is collapsed.
   *
   * Method signature matches method of Slick Grid default DataView.
   *
   * @param iCaseID {number} A case id.
   * @returns {boolean}
   */
  isGroupCollapsed: function (iCaseID) {
    var myCase = this.collection.getCaseByID(iCaseID);
    if (myCase) {
      return this.model.isCollapsedNode(myCase);
    }
  },

  /**
   * No-op
   *
   * Method signature matches method of Slick Grid default DataView.
   */
  setRefreshHints: function (obj) {},

  /**
   * A property defining the expand and collapsed counts for this collection.
   *
   * Used for determining the state of the expand all/collapse all buttons.
   */
  expandCollapseCounts: function () {
    var model = this.model;
    var counts = {
      expanded: 0,
      collapsed: 0
    };
    this.collection.casesController.forEach(function (myCase) {
      if (model.isCollapsedNode(myCase)) {
        counts.collapsed += 1;
      } else {
        counts.expanded += 1;
      }
    });
    return counts;
  }.property(),

  /**
   * Counts the cases at this level of the case table hierarchy which descend
   * from the given case.
   *
   * Used for creating a collapsed row message.
   *
   * @param iCase
   * @returns {number}
   */
  subcaseCount: function (iCase) {
    var count = 0;
    if (iCase.collection.id === this.collection.get('id')) {
      count = 1;
    } else {
      iCase.children.forEach(function (myCase) {
        count += this.subcaseCount(myCase);
      }.bind(this));
    }
    return count;
  }
});<|endoftext|>


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

/**
 * External dependencies
 */
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { Chip, ChipCheckmark } from '@material/react-chips';

/**
 * WordPress dependencies
 */
import { __ } from '@wordpress/i18n';
import { ESCAPE, ENTER } from '@wordpress/keycodes';
import {
	useCallback,
	useEffect,
	useRef,
	forwardRef,
	Fragment,
} from '@wordpress/element';

/**
 * Internal dependencies
 */
import Data from 'googlesitekit-data';
import Button from '../Button';
import Link from '../Link';
import ShareIcon from '../../../svg/icons/share.svg';
import CheckIcon from '../../../svg/icons/check.svg';
import useViewContext from '../../hooks/useViewContext';
import { useKeyCodesInside } from '../../hooks/useKeyCodesInside';
import { trackEvent } from '../../util';
import { CORE_MODULES } from '../../googlesitekit/modules/datastore/constants';
import { CORE_UI } from '../../googlesitekit/datastore/ui/constants';
import { EDITING_USER_ROLE_SELECT_SLUG_KEY } from './DashboardSharingSettings/constants';
const { useSelect, useDispatch } = Data;

const ALL_CHIP_ID = 'all';
const ALL_CHIP_DISPLAY_NAME = __( 'All', 'google-site-kit' );

const UserRoleSelect = forwardRef(
	( { moduleSlug, isLocked = false }, ref ) => {
		const viewContext = useViewContext();
		const roleSelectRef = useRef();

		const { setSharedRoles } = useDispatch( CORE_MODULES );
		const { setValue } = useDispatch( CORE_UI );

		const shareableRoles = useSelect( ( select ) =>
			select( CORE_MODULES ).getShareableRoles()
		);
		const sharedRoles = useSelect( ( select ) =>
			select( CORE_MODULES ).getSharedRoles( moduleSlug )
		);
		const editingUserRoleSelect = useSelect( ( select ) =>
			select( CORE_UI ).getValue( EDITING_USER_ROLE_SELECT_SLUG_KEY )
		);
		const editMode = editingUserRoleSelect === moduleSlug;

		useKeyCodesInside( [ ESCAPE ], ref, () => {
			if ( editMode ) {
				setValue( EDITING_USER_ROLE_SELECT_SLUG_KEY, undefined );
			}
		} );

		const haveSharingSettingsRolesChanged = useSelect( ( select ) =>
			select( CORE_MODULES ).haveModuleSharingSettingsChanged(
				moduleSlug,
				'sharedRoles'
			)
		);

		const toggleEditMode = useCallback( () => {
			if ( ! editMode ) {
				// Set the state to disable modules in when editing user roles
				setValue( EDITING_USER_ROLE_SELECT_SLUG_KEY, moduleSlug );
			} else {
				// Reset the state to enable modules in when not editing.
				setValue( EDITING_USER_ROLE_SELECT_SLUG_KEY, undefined );

				if ( haveSharingSettingsRolesChanged ) {
					trackEvent(
						`${ viewContext }_sharing`,
						'change_shared_roles',
						moduleSlug
					);
				}
			}
		}, [
			editMode,
			haveSharingSettingsRolesChanged,
			moduleSlug,
			setValue,
			viewContext,
		] );

		useEffect( () => {
			if ( ! roleSelectRef.current ) {
				return;
			}

			if ( editMode ) {
				// Focus on the "All" roles button.
				roleSelectRef.current.firstChild.focus();
			} else {
				// Focus on the role select button.
				roleSelectRef.current.focus();
			}
		}, [ editMode ] );

		const toggleChip = useCallback(
			( { type, target, keyCode } ) => {
				if ( type === 'keydown' && keyCode !== ENTER ) {
					return;
				}

				const chip = target.closest( '.mdc-chip' );
				const chipID = chip?.dataset?.chipId; // eslint-disable-line sitekit/acronym-case

				if ( ! chipID ) {
					return;
				}

				let updatedSharedRoles;
				if ( chipID === ALL_CHIP_ID ) {
					if ( sharedRoles?.length === shareableRoles?.length ) {
						updatedSharedRoles = [];
					} else {
						updatedSharedRoles = shareableRoles.map(
							( { id } ) => id
						);
					}
				} else if ( sharedRoles === null ) {
					updatedSharedRoles = [ chipID ];
				} else if ( sharedRoles.includes( chipID ) ) {
					updatedSharedRoles = sharedRoles.filter(
						( role ) => role !== chipID
					);
				} else {
					updatedSharedRoles = [ ...sharedRoles, chipID ];
				}

				setSharedRoles( moduleSlug, updatedSharedRoles );
			},
			[ moduleSlug, setSharedRoles, sharedRoles, shareableRoles ]
		);

		const getSharedRolesDisplayNames = () => {
			const roleDisplayNames = shareableRoles?.reduce( ( acc, role ) => {
				if ( sharedRoles.includes( role.id ) ) {
					acc.push( role.displayName );
				}
				return acc;
			}, [] );

			return roleDisplayNames.join( ', ' );
		};

		if ( ! shareableRoles ) {
			return null;
		}

		return (

				{ ! editMode && (
					<Button
						aria-label={ __( 'Edit roles', 'google-site-kit' ) }
						className="googlesitekit-user-role-select__button"
						onClick={ toggleEditMode }
						icon={ <ShareIcon width={ 23 } height={ 23 } /> }
						tabIndex={ isLocked ? -1 : undefined }
						ref={ roleSelectRef }
					/>
				) }

				{ ! editMode && sharedRoles?.length > 0 && (

						{ getSharedRolesDisplayNames() }

				) }

				{ ! editMode &&
					( ! sharedRoles || sharedRoles?.length === 0 ) && (

							<Link
								onClick={ toggleEditMode }
								tabIndex={ isLocked ? -1 : undefined }
							>
								{ __( 'Add roles', 'google-site-kit' ) }
							</Link>

					) }

				{ editMode && (
					<Fragment>

							<Chip
								chipCheckmark={ <ChipCheckmark /> }
								data-chip-id={ ALL_CHIP_ID }
								id={ ALL_CHIP_ID }
								label={ ALL_CHIP_DISPLAY_NAME }
								onClick={ toggleChip }
								onKeyDown={ toggleChip }
								selected={
									sharedRoles?.length ===
									shareableRoles?.length
								}
								className="googlesitekit-user-role-select__chip googlesitekit-user-role-select__chip--all"
							/>

							{ shareableRoles.map(
								( { id, displayName }, index ) => (
									<Chip
										chipCheckmark={ <ChipCheckmark /> }
										data-chip-id={ id }
										id={ id }
										key={ index }
										label={ displayName }
										onClick={ toggleChip }
										onKeyDown={ toggleChip }
										selected={ sharedRoles?.includes( id ) }
										className="googlesitekit-user-role-select__chip"
									/>
								)
							) }

						<Button
							aria-label={ __( 'Done', 'google-site-kit' ) }
							className="googlesitekit-user-role-select__button"
							onClick={ toggleEditMode }
							icon={ <CheckIcon width={ 18 } height={ 18 } /> }
							tabIndex={ isLocked ? -1 : undefined }
						/>
					</Fragment>
				) }

		);
	}
);

UserRoleSelect.propTypes = {
	moduleSlug: PropTypes.string.isRequired,
	isLocked: PropTypes.bool,
};

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

/**
 * External dependencies
 */
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { Chip, ChipCheckmark } from '@material/react-chips';

/**
 * WordPress dependencies
 */
import { __ } from '@wordpress/i18n';
import { ESCAPE, ENTER } from '@wordpress/keycodes';
import {
	useCallback,
	useEffect,
	useRef,
	forwardRef,
	Fragment,
} from '@wordpress/element';

/**
 * Internal dependencies
 */
import Data from 'googlesitekit-data';
import Button from '../Button';
import Link from '../Link';
import ShareIcon from '../../../svg/icons/share.svg';
import CheckIcon from '../../../svg/icons/check.svg';
import useViewContext from '../../hooks/useViewContext';
import { useKeyCodesInside } from '../../hooks/useKeyCodesInside';
import { trackEvent } from '../../util';
import { CORE_MODULES } from '../../googlesitekit/modules/datastore/constants';
import { CORE_UI } from '../../googlesitekit/datastore/ui/constants';
import { EDITING_USER_ROLE_SELECT_SLUG_KEY } from './DashboardSharingSettings/constants';
const { useSelect, useDispatch } = Data;

const ALL_CHIP_ID = 'all';
const ALL_CHIP_DISPLAY_NAME = __( 'All', 'google-site-kit' );

const UserRoleSelect = forwardRef(
	( { moduleSlug, isLocked = false }, ref ) => {
		const viewContext = useViewContext();
		const roleSelectRef = useRef();

		const { setSharedRoles } = useDispatch( CORE_MODULES );
		const { setValue } = useDispatch( CORE_UI );

		const shareableRoles = useSelect( ( select ) =>
			select( CORE_MODULES ).getShareableRoles()
		);
		const sharedRoles = useSelect( ( select ) =>
			select( CORE_MODULES ).getSharedRoles( moduleSlug )
		);
		const editingUserRoleSelect = useSelect( ( select ) =>
			select( CORE_UI ).getValue( EDITING_USER_ROLE_SELECT_SLUG_KEY )
		);
		const editMode = editingUserRoleSelect === moduleSlug;

		useKeyCodesInside( [ ESCAPE ], ref, () => {
			if ( editMode ) {
				setValue( EDITING_USER_ROLE_SELECT_SLUG_KEY, undefined );
			}
		} );

		const haveSharingSettingsRolesChanged = useSelect( ( select ) =>
			select( CORE_MODULES ).haveModuleSharingSettingsChanged(
				moduleSlug,
				'sharedRoles'
			)
		);

		const toggleEditMode = useCallback( () => {
			if ( ! editMode ) {
				// Set the state to disable modules in when editing user roles
				setValue( EDITING_USER_ROLE_SELECT_SLUG_KEY, moduleSlug );
			} else {
				// Reset the state to enable modules in when not editing.
				setValue( EDITING_USER_ROLE_SELECT_SLUG_KEY, undefined );

				if ( haveSharingSettingsRolesChanged ) {
					trackEvent(
						`${ viewContext }_sharing`,
						'change_shared_roles',
						moduleSlug
					);
				}
			}
		}, [
			editMode,
			haveSharingSettingsRolesChanged,
			moduleSlug,
			setValue,
			viewContext,
		] );

		useEffect( () => {
			if ( ! roleSelectRef.current ) {
				return;
			}

			if ( editMode ) {
				// Focus on the "All" roles button.
				roleSelectRef.current.firstChild.focus();
			} else {
				// Focus on the role select button.
				roleSelectRef.current.focus();
			}
		}, [ editMode ] );

		const toggleChip = useCallback(
			( { type, target, keyCode } ) => {
				if ( type === 'keydown' && keyCode !== ENTER ) {
					return;
				}

				const chip = target.closest( '.mdc-chip' );
				const chipID = chip?.dataset?.chipId; // eslint-disable-line sitekit/acronym-case

				if ( ! chipID ) {
					return;
				}

				let updatedSharedRoles;
				if ( chipID === ALL_CHIP_ID ) {
					if ( sharedRoles?.length === shareableRoles?.length ) {
						updatedSharedRoles = [];
					} else {
						updatedSharedRoles = shareableRoles.map(
							( { id } ) => id
						);
					}
				} else if ( sharedRoles === null ) {
					updatedSharedRoles = [ chipID ];
				} else if ( sharedRoles.includes( chipID ) ) {
					updatedSharedRoles = sharedRoles.filter(
						( role ) => role !== chipID
					);
				} else {
					updatedSharedRoles = [ ...sharedRoles, chipID ];
				}

				setSharedRoles( moduleSlug, updatedSharedRoles );
			},
			[ moduleSlug, setSharedRoles, sharedRoles, shareableRoles ]
		);

		const getSharedRolesDisplayNames = () => {
			const roleDisplayNames = shareableRoles?.reduce( ( acc, role ) => {
				if ( sharedRoles.includes( role.id ) ) {
					acc.push( role.displayName );
				}
				return acc;
			}, [] );

			return roleDisplayNames.join( ', ' );
		};

		if ( ! shareableRoles ) {
			return null;
		}

		return (

				{ ! editMode && (
					<Button
						aria-label={ __( 'Edit roles', 'google-site-kit' ) }
						className="googlesitekit-user-role-select__button"
						onClick={ toggleEditMode }
						icon={ <ShareIcon width={ 23 } height={ 23 } /> }
						tabIndex={ isLocked ? -1 : undefined }
						ref={ roleSelectRef }
					/>
				) }

				{ ! editMode && sharedRoles?.length > 0 && (

						{ getSharedRolesDisplayNames() }

				) }

				{ ! editMode &&
					( ! sharedRoles || sharedRoles?.length === 0 ) && (

							<Link
								onClick={ toggleEditMode }
								tabIndex={ isLocked ? -1 : undefined }
							>
								{ __( 'Add roles', 'google-site-kit' ) }
							</Link>

					) }

				{ editMode && (
					<Fragment>

							<Chip
								chipCheckmark={ <ChipCheckmark /> }
								data-chip-id={ ALL_CHIP_ID }
								id={ ALL_CHIP_ID }
								label={ ALL_CHIP_DISPLAY_NAME }
								onClick={ toggleChip }
								onKeyDown={ toggleChip }
								selected={
									sharedRoles?.length ===
									shareableRoles?.length
								}
								className="googlesitekit-user-role-select__chip googlesitekit-user-role-select__chip--all"
							/>

							{ shareableRoles.map(
								( { id, displayName }, index ) => (
									<Chip
										chipCheckmark={ <ChipCheckmark /> }
										data-chip-id={ id }
										id={ id }
										key={ index }
										label={ displayName }
										onClick={ toggleChip }
										onKeyDown={ toggleChip }
										selected={ sharedRoles?.includes( id ) }
										className="googlesitekit-user-role-select__chip"
									/>
								)
							) }

						<Button
							aria-label={ __(
								'Done editing roles',
								'google-site-kit'
							) }
							title={ __( 'Done', 'google-site-kit' ) }
							className="googlesitekit-user-role-select__button"
							onClick={ toggleEditMode }
							icon={ <CheckIcon width={ 18 } height={ 18 } /> }
							tabIndex={ isLocked ? -1 : undefined }
						/>
					</Fragment>
				) }

		);
	}
);

UserRoleSelect.propTypes = {
	moduleSlug: PropTypes.string.isRequired,
	isLocked: PropTypes.bool,
};

export default UserRoleSelect;
<|endoftext|>


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

/**
 * External dependencies
 */
import PropTypes from 'prop-types';
import classnames from 'classnames';
import map from 'lodash/map';
import { useMount } from 'react-use';

/*
 * WordPress dependencies
 */
import {
	useCallback,
	useState,
	useRef,
	Fragment,
	isValidElement,
} from '@wordpress/element';

/*
 * Internal dependencies
 */
import GoogleLogoIcon from '../../../svg/graphics/logo-g.svg';
import { Cell, Grid, Row } from '../../material-components';
import { getContextScrollTop } from '../../util/scroll';
import { isHashOnly } from '../../util/urls';
import { sanitizeHTML } from '../../util/sanitize';
import DataBlock from '../DataBlock';
import Button from '../Button';
import Warning from '../../../svg/icons/warning.svg';
import ErrorIcon from '../../../svg/icons/error.svg';
import Link from '../Link';
import Badge from '../Badge';
import ModuleIcon from '../ModuleIcon';
import { getItem, setItem, deleteItem } from '../../googlesitekit/api/cache';
import { trackEvent } from '../../util';
import { VIEW_CONTEXT_DASHBOARD } from '../../googlesitekit/constants';
import { useBreakpoint } from '../../hooks/useBreakpoint';

function BannerNotification( {
	anchorLink,
	anchorLinkLabel,
	badgeLabel,
	blockData,
	children,
	className,
	ctaLabel,
	ctaLink,
	ctaTarget,
	description,
	dismiss,
	dismissExpires,
	format,
	id,
	isDismissible,
	learnMoreDescription,
	learnMoreLabel,
	learnMoreURL,
	logo,
	module,
	moduleName,
	noBottomPadding,
	onCTAClick,
	onDismiss,
	pageIndex,
	showOnce,
	SmallImageSVG,
	title,
	type,
	WinImageSVG,
} ) {
	// Closed notifications are invisible, but still occupy space.
	const [ isClosed, setIsClosed ] = useState( false );
	// Start with an undefined dismissed state due to async resolution.
	const [ isDismissed, setIsDismissed ] = useState( false );
	const cardRef = useRef();
	const cacheKeyDismissed = `notification::dismissed::${ id }`;
	// Persists the notification dismissal to browser storage.
	// Dismissed notifications don't expire.
	const persistDismissal = () =>
		setItem( cacheKeyDismissed, new Date(), { ttl: null } );

	const breakpoint = useBreakpoint();

	useMount( async () => {
		await trackEvent(
			`${ VIEW_CONTEXT_DASHBOARD }_site-notification`,
			'view_notification',
			id
		);

		if ( dismissExpires > 0 ) {
			await expireDismiss();
		}

		if ( isDismissible ) {
			const { cacheHit } = await getItem( cacheKeyDismissed );
			setIsDismissed( cacheHit );
		}

		if ( showOnce ) {
			// Set the dismissed flag in cache without immediately hiding it.
			await persistDismissal();
		}
	} );

	async function handleDismiss( e ) {
		e.persist();
		e.preventDefault();

		await trackEvent(
			`${ VIEW_CONTEXT_DASHBOARD }_site-notification`,
			'dismiss_notification',
			id
		);

		if ( onDismiss ) {
			await onDismiss( e );
		}
		dismissNotification();
	}

	function dismissNotification() {
		const card = cardRef.current;

		setIsClosed( true );

		setTimeout( async () => {
			await persistDismissal();

			if ( card?.style ) {
				card.style.display = 'none';
			}

			// Emit an event for the notification counter to listen for.
			const event = new Event( 'notificationDismissed' );
			document.dispatchEvent( event );
		}, 350 );
	}

	async function handleCTAClick( e ) {
		e.persist();

		await trackEvent(
			`${ VIEW_CONTEXT_DASHBOARD }_site-notification`,
			'confirm_notification',
			id
		);

		if ( onCTAClick ) {
			await onCTAClick( e );
		}

		if ( isDismissible ) {
			dismissNotification();
		}
	}

	const handleAnchorLinkClick = useCallback(
		( event ) => {
			if ( isHashOnly( anchorLink ) ) {
				event.preventDefault();

				global.history.replaceState( {}, '', anchorLink );

				global.scrollTo( {
					top: getContextScrollTop( anchorLink, breakpoint ),
					behavior: 'smooth',
				} );
			}
		},
		[ anchorLink, breakpoint ]
	);

	async function handleLearnMore( e ) {
		e.persist();

		await trackEvent(
			`${ VIEW_CONTEXT_DASHBOARD }_site-notification`,
			'click_learn_more_link',
			id
		);
	}

	async function expireDismiss() {
		const { value: dismissed } = await getItem( cacheKeyDismissed );

		if ( dismissed ) {
			const expiration = new Date( dismissed );
			expiration.setSeconds(
				expiration.getSeconds() + parseInt( dismissExpires, 10 )
			);

			if ( expiration < new Date() ) {
				await deleteItem( cacheKeyDismissed );
			}
		}
	}

	// isDismissed will be undefined until resolved from browser storage.
	if ( isDismissible && ( undefined === isDismissed || isDismissed ) ) {
		return null;
	}

	const closedClass = isClosed ? 'is-closed' : 'is-open';
	const inlineLayout = 'large' === format && 'win-stats-increase' === type;

	let layoutSizeCellProps = {
		smSize: 4,
		mdSize: inlineLayout ? 7 : 8,
		lgSize: 12,
	};
	let layoutOrderCellProps = {
		smOrder: 2,
		mdOrder: 1,
	};
	let winImageCellProps = {
		smSize: 4,
		mdSize: 2,
		lgSize: 4,
		smOrder: 1,
		mdOrder: 2,
	};

	if ( 'small' === format ) {
		layoutOrderCellProps = {};
	}

	if ( 'larger' === format ) {
		layoutOrderCellProps = {
			...layoutOrderCellProps,
			mdOrder: 2,
			lgOrder: 1,
		};

		layoutSizeCellProps = {
			...layoutSizeCellProps,
			mdSize: 8,
			lgSize: 5,
		};

		winImageCellProps = {
			...winImageCellProps,
			smSize: 4,
			mdSize: 8,
			lgSize: 7,
			mdOrder: 1,
			lgOrder: 2,
		};
	}

	Object.keys( layoutSizeCellProps ).forEach( ( key ) => {
		let size = layoutSizeCellProps[ key ];
		if ( 'win-error' === type || 'win-warning' === type ) {
			size = size - 1;
		}

		if ( SmallImageSVG ) {
			size = size - 1;
		}

		if ( WinImageSVG ) {
			if (
				winImageCellProps[ key ] &&
				key !== 'smSize' &&
				key !== 'mdSize' &&
				format !== 'larger'
			) {
				size = size - winImageCellProps[ key ];
			}
		}

		layoutSizeCellProps[ key ] = size;
	} );

	let icon;
	if ( 'win-warning' === type ) {
		icon = <Warning width={ 34 } />;
	} else if ( 'win-error' === type ) {
		icon = <ErrorIcon width={ 28 } />;
	} else {
		icon = '';
	}

	const dataBlockMarkup = (
		<Fragment>
			{ blockData && (
				<Row>
					{ map( blockData, ( block, i ) => {
						return (
							<Cell key={ i } lgSize={ inlineLayout ? 5 : 4 }>

									<DataBlock { ...block } />

							</Cell>
						);
					} ) }
				</Row>
			) }
		</Fragment>
	);

	const inlineMarkup = (
		<Fragment>
			{ title && (

					{ title }

					{ badgeLabel && <Badge label={ badgeLabel } /> }

			) }
			{ anchorLink && anchorLinkLabel && (

					<Link href={ anchorLink } onClick={ handleAnchorLinkClick }>
						{ anchorLinkLabel }
					</Link>

			) }
			{ description && (

						{ isValidElement( description ) ? (
							description
						) : (

						) }

						{ learnMoreLabel && (
							<Fragment>
								{ ' ' }
								<Link
									onClick={ handleLearnMore }
									href={ learnMoreURL }
									external
									inherit
								>
									{ learnMoreLabel }
								</Link>
								{ learnMoreDescription }
							</Fragment>
						) }
						{ pageIndex && (

								{ pageIndex }

						) }

			) }
			{ children }
		</Fragment>
	);

	const logoSVG = module ? (
		<ModuleIcon slug={ module } size={ 19 } />
	) : (
		<GoogleLogoIcon height="34" width="32" />
	);

	const logoCellProps = inlineLayout
		? {
				size: 12,
				smOrder: 2,
				mdOrder: 1,
		  }
		: { size: 12 };

	return (
		<section
			id={ id }
			ref={ cardRef }
			className={ classnames( className, 'googlesitekit-publisher-win', {
				[ `googlesitekit-publisher-win--${ format }` ]: format,
				[ `googlesitekit-publisher-win--${ type }` ]: type,
				[ `googlesitekit-publisher-win--${ closedClass }` ]: closedClass,
				'googlesitekit-publisher-win--no-bottom-padding': noBottomPadding,
			} ) }
		>
			<Grid
				className={ classnames( {
					'googlesitekit-padding-bottom-0': noBottomPadding,
				} ) }
			>
				<Row>
					{ logo && (
						<Cell { ...logoCellProps }>

								{ logoSVG }

							{ moduleName && (

									{ moduleName }

							) }
						</Cell>
					) }

					{ SmallImageSVG && (
						<Cell
							size={ 1 }
							className="googlesitekit-publisher-win__small-media"
						>
							<SmallImageSVG />
						</Cell>
					) }

					<Cell
						{ ...layoutSizeCellProps }
						{ ...layoutOrderCellProps }
						className="googlesitekit-publisher-win__content"
					>
						{ inlineLayout ? (
							<Row>
								<Cell mdSize={ 8 } lgSize={ 5 }>
									{ inlineMarkup }
								</Cell>
								<Cell alignBottom mdSize={ 8 } lgSize={ 7 }>
									{ dataBlockMarkup }
								</Cell>
							</Row>
						) : (
							<Fragment>
								{ inlineMarkup }
								{ dataBlockMarkup }
							</Fragment>
						) }

						{ ctaLink && (
							<Button
								className="googlesitekit-notification__cta"
								href={ ctaLink }
								target={ ctaTarget }
								onClick={ handleCTAClick }
							>
								{ ctaLabel }
							</Button>
						) }

						{ isDismissible && dismiss && (
							<Link onClick={ handleDismiss }>{ dismiss }</Link>
						) }
					</Cell>

					{ WinImageSVG && (
						<Cell
							{ ...winImageCellProps }
							alignBottom={
								format === 'larger' && noBottomPadding
							}
						>
							<div
								className={ `googlesitekit-publisher-win__image-${ format }` }
							>
								<WinImageSVG />

						</Cell>
					) }

					{ ( 'win-error' === type || 'win-warning' === type ) && (
						<Cell size={ 1 }>

								{ icon }

						</Cell>
					) }
				</Row>
			</Grid>

	);
}

BannerNotification.propTypes = {
	id: PropTypes.string.isRequired,
	className: PropTypes.string,
	title: PropTypes.string.isRequired,
	description: PropTypes.node,
	learnMoreURL: PropTypes.string,
	learnMoreDescription: PropTypes.string,
	learnMoreLabel: PropTypes.string,
	blockData: PropTypes.array,
	WinImageSVG: PropTypes.elementType,
	SmallImageSVG: PropTypes.elementType,
	format: PropTypes.string,
	ctaLink: PropTypes.string,
	ctaLabel: PropTypes.string,
	type: PropTypes.string,
	dismiss: PropTypes.string,
	isDismissible: PropTypes.bool,
	logo: PropTypes.bool,
	module: PropTypes.string,
	moduleName: PropTypes.string,
	pageIndex: PropTypes.string,
	dismissExpires: PropTypes.number,
	showOnce: PropTypes.bool,
	onCTAClick: PropTypes.func,
	onDismiss: PropTypes.func,
	anchorLink: PropTypes.string,
	anchorLinkLabel: PropTypes.string,
};

BannerNotification.defaultProps = {
	isDismissible: true,
	className: '',
	dismissExpires: 0,
	showOnce: false,
	noBottomPadding: false,
};

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

/**
 * External dependencies
 */
import PropTypes from 'prop-types';
import classnames from 'classnames';
import map from 'lodash/map';
import { useMount } from 'react-use';

/*
 * WordPress dependencies
 */
import {
	useCallback,
	useState,
	useRef,
	Fragment,
	isValidElement,
} from '@wordpress/element';

/*
 * Internal dependencies
 */
import GoogleLogoIcon from '../../../svg/graphics/logo-g.svg';
import { Cell, Grid, Row } from '../../material-components';
import { getContextScrollTop } from '../../util/scroll';
import { isHashOnly } from '../../util/urls';
import { sanitizeHTML } from '../../util/sanitize';
import DataBlock from '../DataBlock';
import Button from '../Button';
import Warning from '../../../svg/icons/warning.svg';
import ErrorIcon from '../../../svg/icons/error.svg';
import Link from '../Link';
import Badge from '../Badge';
import ModuleIcon from '../ModuleIcon';
import { getItem, setItem, deleteItem } from '../../googlesitekit/api/cache';
import { trackEvent } from '../../util';
import { VIEW_CONTEXT_DASHBOARD } from '../../googlesitekit/constants';
import { useBreakpoint } from '../../hooks/useBreakpoint';

function BannerNotification( {
	anchorLink,
	anchorLinkLabel,
	badgeLabel,
	blockData,
	children,
	className,
	ctaLabel,
	ctaLink,
	ctaTarget,
	description,
	dismiss,
	dismissExpires,
	format,
	id,
	isDismissible,
	learnMoreDescription,
	learnMoreLabel,
	learnMoreURL,
	logo,
	module,
	moduleName,
	noBottomPadding,
	onCTAClick,
	onDismiss,
	pageIndex,
	showOnce,
	SmallImageSVG,
	title,
	type,
	WinImageSVG,
} ) {
	// Closed notifications are invisible, but still occupy space.
	const [ isClosed, setIsClosed ] = useState( false );
	// Start with an undefined dismissed state due to async resolution.
	const [ isDismissed, setIsDismissed ] = useState( false );
	const cardRef = useRef();
	const cacheKeyDismissed = `notification::dismissed::${ id }`;
	// Persists the notification dismissal to browser storage.
	// Dismissed notifications don't expire.
	const persistDismissal = () =>
		setItem( cacheKeyDismissed, new Date(), { ttl: null } );

	const breakpoint = useBreakpoint();

	useMount( async () => {
		await trackEvent(
			`${ VIEW_CONTEXT_DASHBOARD }_site-notification`,
			'view_notification',
			id
		);

		if ( dismissExpires > 0 ) {
			await expireDismiss();
		}

		if ( isDismissible ) {
			const { cacheHit } = await getItem( cacheKeyDismissed );
			setIsDismissed( cacheHit );
		}

		if ( showOnce ) {
			// Set the dismissed flag in cache without immediately hiding it.
			await persistDismissal();
		}
	} );

	async function handleDismiss( e ) {
		e.persist();
		e.preventDefault();

		await trackEvent(
			`${ VIEW_CONTEXT_DASHBOARD }_site-notification`,
			'dismiss_notification',
			id
		);

		if ( onDismiss ) {
			await onDismiss( e );
		}
		dismissNotification();
	}

	function dismissNotification() {
		const card = cardRef.current;

		setIsClosed( true );

		setTimeout( async () => {
			await persistDismissal();

			if ( card?.style ) {
				card.style.display = 'none';
			}

			// Emit an event for the notification counter to listen for.
			const event = new Event( 'notificationDismissed' );
			document.dispatchEvent( event );
		}, 350 );
	}

	async function handleCTAClick( e ) {
		e.persist();

		await trackEvent(
			`${ VIEW_CONTEXT_DASHBOARD }_site-notification`,
			'confirm_notification',
			id
		);

		if ( onCTAClick ) {
			await onCTAClick( e );
		}

		if ( isDismissible ) {
			dismissNotification();
		}
	}

	const handleAnchorLinkClick = useCallback(
		( event ) => {
			if ( isHashOnly( anchorLink ) ) {
				event.preventDefault();

				global.history.replaceState( {}, '', anchorLink );

				global.scrollTo( {
					top: getContextScrollTop( anchorLink, breakpoint ),
					behavior: 'smooth',
				} );
			}
		},
		[ anchorLink, breakpoint ]
	);

	async function handleLearnMore( e ) {
		e.persist();

		await trackEvent(
			`${ VIEW_CONTEXT_DASHBOARD }_site-notification`,
			'click_learn_more_link',
			id
		);
	}

	async function expireDismiss() {
		const { value: dismissed } = await getItem( cacheKeyDismissed );

		if ( dismissed ) {
			const expiration = new Date( dismissed );
			expiration.setSeconds(
				expiration.getSeconds() + parseInt( dismissExpires, 10 )
			);

			if ( expiration < new Date() ) {
				await deleteItem( cacheKeyDismissed );
			}
		}
	}

	// isDismissed will be undefined until resolved from browser storage.
	if ( isDismissible && ( undefined === isDismissed || isDismissed ) ) {
		return null;
	}

	const closedClass = isClosed ? 'is-closed' : 'is-open';
	const inlineLayout = 'large' === format && 'win-stats-increase' === type;

	let layoutSizeCellProps = {
		smSize: 4,
		mdSize: inlineLayout ? 7 : 8,
		lgSize: 12,
	};
	let layoutOrderCellProps = {
		smOrder: 2,
		mdOrder: 1,
	};
	let winImageCellProps = {
		smSize: 4,
		mdSize: 2,
		lgSize: 4,
		smOrder: 1,
		mdOrder: 2,
	};

	if ( 'small' === format ) {
		layoutOrderCellProps = {};
	}

	if ( 'larger' === format ) {
		layoutOrderCellProps = {
			...layoutOrderCellProps,
			mdOrder: 2,
			lgOrder: 1,
		};

		layoutSizeCellProps = {
			...layoutSizeCellProps,
			mdSize: 8,
			lgSize: 5,
		};

		winImageCellProps = {
			...winImageCellProps,
			smSize: 4,
			mdSize: 8,
			lgSize: 7,
			mdOrder: 1,
			lgOrder: 2,
		};
	}

	Object.keys( layoutSizeCellProps ).forEach( ( key ) => {
		let size = layoutSizeCellProps[ key ];
		if ( 'win-error' === type || 'win-warning' === type ) {
			size = size - 1;
		}

		if ( SmallImageSVG ) {
			size = size - 1;
		}

		if ( WinImageSVG ) {
			if (
				winImageCellProps[ key ] &&
				key !== 'smSize' &&
				key !== 'mdSize' &&
				format !== 'larger'
			) {
				size = size - winImageCellProps[ key ];
			}
		}

		layoutSizeCellProps[ key ] = size;
	} );

	let icon;
	if ( 'win-warning' === type ) {
		icon = <Warning width={ 34 } />;
	} else if ( 'win-error' === type ) {
		icon = <ErrorIcon width={ 28 } />;
	} else {
		icon = '';
	}

	const dataBlockMarkup = (
		<Fragment>
			{ blockData && (
				<Row>
					{ map( blockData, ( block, i ) => {
						return (
							<Cell key={ i } lgSize={ inlineLayout ? 5 : 4 }>

									<DataBlock { ...block } />

							</Cell>
						);
					} ) }
				</Row>
			) }
		</Fragment>
	);

	const inlineMarkup = (
		<Fragment>
			{ title && (

					{ title }

					{ badgeLabel && <Badge label={ badgeLabel } /> }

			) }
			{ anchorLink && anchorLinkLabel && (

					<Link href={ anchorLink } onClick={ handleAnchorLinkClick }>
						{ anchorLinkLabel }
					</Link>

			) }
			{ description && (

						{ isValidElement( description ) ? (
							description
						) : (

						) }

						{ learnMoreLabel && (
							<Fragment>
								{ ' ' }
								<Link
									onClick={ handleLearnMore }
									href={ learnMoreURL }
									external
									inherit
								>
									{ learnMoreLabel }
								</Link>
								{ learnMoreDescription }
							</Fragment>
						) }
						{ pageIndex && (

								{ pageIndex }

						) }

			) }
			{ children }
		</Fragment>
	);

	const logoSVG = module ? (
		<ModuleIcon slug={ module } size={ 19 } />
	) : (
		<GoogleLogoIcon height="34" width="32" />
	);

	const logoCellProps = inlineLayout
		? {
				size: 12,
				smOrder: 2,
				mdOrder: 1,
		  }
		: { size: 12 };

	return (
		<section
			id={ id }
			ref={ cardRef }
			className={ classnames( className, 'googlesitekit-publisher-win', {
				[ `googlesitekit-publisher-win--${ format }` ]: format,
				[ `googlesitekit-publisher-win--${ type }` ]: type,
				[ `googlesitekit-publisher-win--${ closedClass }` ]: closedClass,
				'googlesitekit-publisher-win--no-bottom-padding': noBottomPadding,
			} ) }
		>
			<Grid
				className={ classnames( {
					'googlesitekit-padding-bottom-0': noBottomPadding,
				} ) }
			>
				<Row>
					{ logo && (
						<Cell { ...logoCellProps }>

								{ logoSVG }

							{ moduleName && (

									{ moduleName }

							) }
						</Cell>
					) }

					{ SmallImageSVG && (
						<Cell
							size={ 1 }
							className="googlesitekit-publisher-win__small-media"
						>
							<SmallImageSVG />
						</Cell>
					) }

					<Cell
						{ ...layoutSizeCellProps }
						{ ...layoutOrderCellProps }
						className="googlesitekit-publisher-win__content"
					>
						{ inlineLayout ? (
							<Row>
								<Cell mdSize={ 8 } lgSize={ 5 }>
									{ inlineMarkup }
								</Cell>
								<Cell alignBottom mdSize={ 8 } lgSize={ 7 }>
									{ dataBlockMarkup }
								</Cell>
							</Row>
						) : (
							<Fragment>
								{ inlineMarkup }
								{ dataBlockMarkup }
							</Fragment>
						) }

						{ ctaLink && (
							<Button
								className="googlesitekit-notification__cta"
								href={ ctaLink }
								target={ ctaTarget }
								onClick={ handleCTAClick }
							>
								{ ctaLabel }
							</Button>
						) }

						{ isDismissible && dismiss && (
							<Link onClick={ handleDismiss }>{ dismiss }</Link>
						) }
					</Cell>

					{ WinImageSVG && (
						<Cell
							{ ...winImageCellProps }
							alignBottom={
								format === 'larger' && noBottomPadding
							}
						>
							<div
								className={ `googlesitekit-publisher-win__image-${ format }` }
							>
								<WinImageSVG />

						</Cell>
					) }

					{ ( 'win-error' === type || 'win-warning' === type ) && (
						<Cell size={ 1 }>

								{ icon }

						</Cell>
					) }
				</Row>
			</Grid>

	);
}

BannerNotification.propTypes = {
	id: PropTypes.string.isRequired,
	className: PropTypes.string,
	title: PropTypes.string.isRequired,
	description: PropTypes.node,
	learnMoreURL: PropTypes.string,
	learnMoreDescription: PropTypes.string,
	learnMoreLabel: PropTypes.string,
	blockData: PropTypes.array,
	WinImageSVG: PropTypes.elementType,
	SmallImageSVG: PropTypes.elementType,
	format: PropTypes.string,
	ctaLink: PropTypes.string,
	ctaLabel: PropTypes.string,
	type: PropTypes.string,
	dismiss: PropTypes.string,
	isDismissible: PropTypes.bool,
	logo: PropTypes.bool,
	module: PropTypes.string,
	moduleName: PropTypes.string,
	pageIndex: PropTypes.string,
	dismissExpires: PropTypes.number,
	showOnce: PropTypes.bool,
	onCTAClick: PropTypes.func,
	onDismiss: PropTypes.func,
	anchorLink: PropTypes.string,
	anchorLinkLabel: PropTypes.string,
	badgeLabel: PropTypes.string,
	noBottomPadding: PropTypes.bool,
};

BannerNotification.defaultProps = {
	isDismissible: true,
	className: '',
	dismissExpires: 0,
	showOnce: false,
	noBottomPadding: false,
};

export default BannerNotification;
<|endoftext|>


<commit_before>/*
 * Project: Twitter Bootstrap Hover Dropdown
 * Author: Cameron Spear
 * Contributors: Mattia Larentis
 *
 * Dependencies?: Twitter Bootstrap's Dropdown plugin
 *
 * A simple plugin to enable twitter bootstrap dropdowns to active on hover and provide a nice user experience.
 *
 * No license, do what you want. I'd love credit or a shoutout, though.
 *
 * http://cameronspear.com/blog/twitter-bootstrap-dropdown-on-hover-plugin/
 */
;(function($, window, undefined) {
    // outside the scope of the jQuery plugin to
    // keep track of all dropdowns
    var $allDropdowns = $();

    // if instantlyCloseOthers is true, then it will instantly
    // shut other nav items when a new one is hovered over
    $.fn.dropdownHover = function(options) {

        // the element we really care about
        // is the dropdown-toggle's parent
        $allDropdowns = $allDropdowns.add(this.parent());

        return this.each(function() {
            var $this = $(this),
                $parent = $this.parent(),
                defaults = {
                    delay: 500,
                    instantlyCloseOthers: true
                },
                data = {
                    delay: $(this).data('delay'),
                    instantlyCloseOthers: $(this).data('close-others')
                },
                settings = $.extend(true, {}, defaults, options, data),
                timeout;

            $parent.hover(function(event) {
                // so a neighbor can't open the dropdown
                if(!$parent.hasClass('open') && !$this.is(event.target)) {
                    return true;
                }

                if(shouldHover) {
                    if(settings.instantlyCloseOthers === true)
                        $allDropdowns.removeClass('open');

                    window.clearTimeout(timeout);
                    $parent.addClass('open');
                }
            }, function() {
                if(shouldHover) {
                    timeout = window.setTimeout(function() {
                        $parent.removeClass('open');
                    }, settings.delay);
                }

            });

            // this helps with button groups!
            $this.hover(function() {
                if(shouldHover) {
                    if(settings.instantlyCloseOthers === true)
                        $allDropdowns.removeClass('open');

                    window.clearTimeout(timeout);
                    $parent.addClass('open');
                }
            });

            // handle submenus
            $parent.find('.dropdown-submenu').each(function(){
                var $this = $(this);
                var subTimeout;
                $this.hover(function() {
                    if(shouldHover) {
                        window.clearTimeout(subTimeout);
                        $this.children('.dropdown-menu').show();
                        // always close submenu siblings instantly
                        $this.siblings().children('.dropdown-menu').hide();
                    }
                }, function() {
                    var $submenu = $this.children('.dropdown-menu');
                    if(shouldHover) {
                        subTimeout = window.setTimeout(function() {
                            $submenu.hide();
                        }, settings.delay);
                    } else {
                        // emulate Twitter Bootstrap's default behavior
                        $submenu.hide();
                    }
                });
            });
        });
    };

    // helper variables to guess if they are using a mouse
    var shouldHover = false,
        mouse_info = {
            hits: 0,
            x: null,
            y: null
        };
    $(document).ready(function() {
        // apply dropdownHover to all elements with the data-hover="dropdown" attribute
        $('[data-hover="dropdown"]').dropdownHover();

        // if the mouse movements are "smooth" or there are more than 20, they probably have a real mouse
        $(document).mousemove(function(e){
            mouse_info.hits++;
            if (mouse_info.hits > 20 || (Math.abs(e.pageX - mouse_info.x) + Math.abs(e.pageY - mouse_info.y)) < 4) {
                $(this).unbind(e);
                shouldHover = true;
            }
            else {
                mouse_info.x = e.pageX;
                mouse_info.y = e.pageY;
            }
        });
    });

    // for the submenu to close on delay, we need to override Bootstrap's CSS in this case
    var css = '.dropdown-submenu:hover>.dropdown-menu{display:none}';
    var style = document.createElement('style');
    style.type = 'text/css';
    if (style.styleSheet) {
        style.styleSheet.cssText = css;
    } else {
        style.appendChild(document.createTextNode(css));
    }
    $('head')[0].appendChild(style);
})(jQuery, this);<commit_msg>Updated Twitter Bootstrap Hover Dropdown plugin.<commit_after>/*
 * Project: Twitter Bootstrap Hover Dropdown
 * Author: Cameron Spear
 * Contributors: Mattia Larentis
 *
 * Dependencies?: Twitter Bootstrap's Dropdown plugin
 *
 * A simple plugin to enable twitter bootstrap dropdowns to active on hover and provide a nice user experience.
 *
 * No license, do what you want. I'd love credit or a shoutout, though.
 *
 * http://cameronspear.com/blog/twitter-bootstrap-dropdown-on-hover-plugin/
 */
;(function($, window, undefined) {
    // outside the scope of the jQuery plugin to
    // keep track of all dropdowns
    var $allDropdowns = $();

    // if instantlyCloseOthers is true, then it will instantly
    // shut other nav items when a new one is hovered over
    $.fn.dropdownHover = function(options) {

        // the element we really care about
        // is the dropdown-toggle's parent
        $allDropdowns = $allDropdowns.add(this.parent());

        return this.each(function() {
            var $this = $(this),
                $parent = $this.parent(),
                defaults = {
                    delay: 500,
                    instantlyCloseOthers: true
                },
                data = {
                    delay: $(this).data('delay'),
                    instantlyCloseOthers: $(this).data('close-others')
                },
                settings = $.extend(true, {}, defaults, options, data),
                timeout;

            $parent.hover(function(event) {
                // so a neighbor can't open the dropdown
                if(!$parent.hasClass('open') && !$this.is(event.target)) {
                    return true;
                }

                if(settings.instantlyCloseOthers === true)
                    $allDropdowns.removeClass('open');

                window.clearTimeout(timeout);
                $parent.addClass('open');
            }, function() {
                timeout = window.setTimeout(function() {
                    $parent.removeClass('open');
                }, settings.delay);
            });

            // this helps with button groups!
            $this.hover(function() {
                if(settings.instantlyCloseOthers === true)
                    $allDropdowns.removeClass('open');

                window.clearTimeout(timeout);
                $parent.addClass('open');
            });

            // handle submenus
            $parent.find('.dropdown-submenu').each(function(){
                var $this = $(this);
                var subTimeout;
                $this.hover(function() {
                    window.clearTimeout(subTimeout);
                    $this.children('.dropdown-menu').show();
                    // always close submenu siblings instantly
                    $this.siblings().children('.dropdown-menu').hide();
                }, function() {
                    var $submenu = $this.children('.dropdown-menu');
                    subTimeout = window.setTimeout(function() {
                        $submenu.hide();
                    }, settings.delay);
                });
            });
        });
    };

    $(document).ready(function() {
        // apply dropdownHover to all elements with the data-hover="dropdown" attribute
        $('[data-hover="dropdown"]').dropdownHover();
    });
})(jQuery, this);
<|endoftext|>


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

/**
 * External dependencies
 */
import styled, { css } from 'styled-components';
import { rgba } from 'polished';
import PropTypes from 'prop-types';
import { useRef } from 'react';

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

/**
 * Internal dependencies
 */
import { Add, EditPencil } from '../../../icons';
import { useKeyDownEffect } from '../../keyboard';
import { PanelTitle } from '../panel';

const buttonCSS = css`
  border: none;
  background: transparent;
  width: 30px;
  height: 28px;
  color: ${({ theme }) => rgba(theme.colors.fg.white, 0.84)};
  cursor: pointer;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
`;

const AddPresetButton = styled.button`
  ${buttonCSS}
  svg {
    width: 26px;
    height: 28px;
  }
`;

const EditMode = styled.button`
  ${buttonCSS}

  ${({ isEditMode }) =>
    isEditMode
      ? css`
          color: ${({ theme }) => theme.colors.fg.white};
          font-size: 12px;
          line-height: 14px;
          padding: 7px;
          height: initial;
        `
      : css`
          svg {
            width: 16px;
            height: 20px;
          }
        `}
`;

function Button({ onClick, Icon, children, ...rest }) {
  // We unfortunately have to manually assign this listener, as it would be default behaviour
  // if it wasn't for our listener further up the stack interpreting enter as "enter edit mode"
  // for text elements. For non-text element selection, this does nothing, that default beviour
  // wouldn't do.
  const ref = useRef();
  useKeyDownEffect(ref, 'enter', onClick, [onClick]);
  return (
    <Icon ref={ref} onClick={onClick} {...rest}>
      {children}
    </Icon>
  );
}

Button.propTypes = {
  children: PropTypes.node.isRequired,
  Icon: PropTypes.elementType.isRequired,
  onClick: PropTypes.func.isRequired,
};

function PresetsHeader({
  title,
  handleAddPreset,
  isEditMode,
  setIsEditMode,
  presets,
  canCollapse,
}) {
  const hasPresets = presets.length > 0;

  const getActions = () => {
    return (
      <>
        {hasPresets && (
          <Button
            Icon={EditMode}
            onClick={(evt) => {
              evt.stopPropagation();
              setIsEditMode(!isEditMode);
            }}
            aria-label={
              isEditMode
                ? __('Exit edit mode', 'web-stories')
                : __('Edit presets', 'web-stories')
            }
            isEditMode={isEditMode}
          >
            {isEditMode ? __('Exit', 'web-stories') : <EditPencil />}
          </Button>
        )}
        {!isEditMode && (
          <Button
            Icon={AddPresetButton}
            onClick={handleAddPreset}
            aria-label={__('Add preset', 'web-stories')}
          >
            <Add />
          </Button>
        )}
      </>
    );
  };

  // Todo: Rename label to 'Presets' post-beta.
  return (
    <PanelTitle secondaryAction={getActions()} canCollapse={canCollapse}>
      {title}
    </PanelTitle>
  );
}

PresetsHeader.propTypes = {
  presets: PropTypes.array.isRequired,
  isEditMode: PropTypes.bool.isRequired,
  handleAddPreset: PropTypes.func.isRequired,
  setIsEditMode: PropTypes.func.isRequired,
  canCollapse: PropTypes.bool.isRequired,
  title: PropTypes.string.isRequired,
};

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

/**
 * External dependencies
 */
import styled, { css } from 'styled-components';
import { rgba } from 'polished';
import PropTypes from 'prop-types';
import { useRef } from 'react';

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

/**
 * Internal dependencies
 */
import { Add, EditPencil } from '../../../icons';
import { useKeyDownEffect } from '../../keyboard';
import { PanelTitle } from '../panel';

const buttonCSS = css`
  border: none;
  background: transparent;
  width: 30px;
  height: 28px;
  color: ${({ theme }) => rgba(theme.colors.fg.white, 0.84)};
  cursor: pointer;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
`;

const AddPresetButton = styled.button`
  ${buttonCSS}
  svg {
    width: 26px;
    height: 28px;
  }
`;

const EditMode = styled.button`
  ${buttonCSS}

  ${({ isEditMode }) =>
    isEditMode
      ? css`
          color: ${({ theme }) => theme.colors.fg.white};
          font-size: 12px;
          line-height: 14px;
          padding: 7px;
          height: initial;
        `
      : css`
          svg {
            width: 16px;
            height: 20px;
          }
        `}
`;

function Button({ onClick, Icon, children, ...rest }) {
  // We unfortunately have to manually assign this listener, as it would be default behaviour
  // if it wasn't for our listener further up the stack interpreting enter as "enter edit mode"
  // for text elements. For non-text element selection, this does nothing, that default beviour
  // wouldn't do.
  const ref = useRef();
  useKeyDownEffect(ref, 'enter', onClick, [onClick]);
  return (
    <Icon ref={ref} onClick={onClick} {...rest}>
      {children}
    </Icon>
  );
}

Button.propTypes = {
  children: PropTypes.node.isRequired,
  Icon: PropTypes.elementType.isRequired,
  onClick: PropTypes.func.isRequired,
};

function PresetsHeader({
  title,
  handleAddPreset,
  isEditMode,
  setIsEditMode,
  presets,
  canCollapse,
}) {
  const hasPresets = presets.length > 0;

  const getActions = () => {
    return (
      <>
        {hasPresets && (
          <Button
            Icon={EditMode}
            onClick={(evt) => {
              evt.stopPropagation();
              setIsEditMode(!isEditMode);
            }}
            aria-label={
              isEditMode
                ? __('Exit edit mode', 'web-stories')
                : __('Edit presets', 'web-stories')
            }
            isEditMode={isEditMode}
          >
            {isEditMode ? __('Exit', 'web-stories') : <EditPencil />}
          </Button>
        )}
        {!isEditMode && (
          <Button
            Icon={AddPresetButton}
            onClick={handleAddPreset}
            aria-label={__('Add preset', 'web-stories')}
          >
            <Add />
          </Button>
        )}
      </>
    );
  };

  return (
    <PanelTitle secondaryAction={getActions()} canCollapse={canCollapse}>
      {title}
    </PanelTitle>
  );
}

PresetsHeader.propTypes = {
  presets: PropTypes.array.isRequired,
  isEditMode: PropTypes.bool.isRequired,
  handleAddPreset: PropTypes.func.isRequired,
  setIsEditMode: PropTypes.func.isRequired,
  canCollapse: PropTypes.bool.isRequired,
  title: PropTypes.string.isRequired,
};

export default PresetsHeader;
<|endoftext|>