<commit_before>'use strict';

var srlog = SIREPO.srlog;
var srdbg = SIREPO.srdbg;

SIREPO.app.config(function() {
    SIREPO.appReportTypes  = ['analysis', 'general', 'plan'].map((t) => {
	return `<div data-ng-switch-when="${t}Metadata" data-metadata-table="" data-type="${t}"></div>`;
    }).join('');
});

SIREPO.app.factory('raydataService', function(appState) {
    const self = {};
    appState.setAppService(self);
    return self;
});

SIREPO.app.controller('AnalysisController', function(appState, frameCache, persistentSimulation, $scope) {
    const self = this;
    self.appState = appState;
    self.simScope = $scope;
    self.simComputeModel = 'analysisAnimation';
    self.simHandleStatus = function (data) {
        if (data.frameCount) {
            frameCache.setFrameCount(data.frameCount);
        }
    };
    self.simState = persistentSimulation.initSimulationState(self);
    return self;
});

SIREPO.app.controller('MetadataController', function(appState, frameCache, persistentSimulation, $scope) {
    const self = this;
    return self;
});

SIREPO.app.directive('appFooter', function() {
    return {
        restrict: 'A',
        scope: {
            nav: '=appFooter',
        },
        template: `
            '

'
        `
    };
});

SIREPO.app.directive('appHeader', function(appState, panelState) {
    return {
	restrict: 'A',
	scope: {
            nav: '=appHeader',
	},
        template: `

              <app-header-right-sim-loaded>

 Metadata

 Analysis

              </app-header-right-sim-loaded>

        `
    };
});

SIREPO.app.directive('metadataTable', function() {
    return {
        restrict: 'A',
        scope: {
	    type: '@'
	},
        template: `

                    Field
                    Value

                    {{ k }}
                    {{ v }}
                    <button class="glyphicon glyphicon-plus" data-ng-if="isOverflown(k)" data-ng-click="toggleExpanded(k)">
                    <button class="glyphicon glyphicon-minus" data-ng-if="expanded[k]" data-ng-click="toggleExpanded(k)">

	`,
	controller: function(appState, requestSender, $sce, $scope) {
	    $scope.data = null;
	    $scope.expanded = {};

	    $scope.isOverflown = function(key) {
		// Id's may be malformed (spaces) so use this jquery syntax
		// to cover them
		const e = $(`[id='metadata-table-${key}']`);
		return e.prop('clientWidth') < e.prop('scrollWidth');
	    };

	    $scope.toggleExpanded = function(key) {
		const e = $(`[id='metadata-table-${key}']`);
		if (!(key in $scope.expanded)) {
		    $scope.expanded[key] = false;
		}
		$scope.expanded[key] = ! $scope.expanded[key];
		e.toggleClass('raydata-overflow-text');
	    };

	    requestSender.statelessCompute(
		appState,
		{
		    method: $scope.type + '_metadata'
		},
		(data) => {
		    $scope.data = data.data;
		}
	    );
        },
    };
});
<commit_msg>use number instead of user data for html id attr<commit_after>'use strict';

var srlog = SIREPO.srlog;
var srdbg = SIREPO.srdbg;

SIREPO.app.config(function() {
    SIREPO.appReportTypes  = ['analysis', 'general', 'plan'].map((t) => {
	return `<div data-ng-switch-when="${t}Metadata" data-metadata-table="" data-type="${t}"></div>`;
    }).join('');
});

SIREPO.app.factory('raydataService', function(appState) {
    const self = {};
    appState.setAppService(self);
    return self;
});

SIREPO.app.controller('AnalysisController', function(appState, frameCache, persistentSimulation, $scope) {
    const self = this;
    self.appState = appState;
    self.simScope = $scope;
    self.simComputeModel = 'analysisAnimation';
    self.simHandleStatus = function (data) {
        if (data.frameCount) {
            frameCache.setFrameCount(data.frameCount);
        }
    };
    self.simState = persistentSimulation.initSimulationState(self);
    return self;
});

SIREPO.app.controller('MetadataController', function(appState, frameCache, persistentSimulation, $scope) {
    const self = this;
    return self;
});

SIREPO.app.directive('appFooter', function() {
    return {
        restrict: 'A',
        scope: {
            nav: '=appFooter',
        },
        template: `
            '

'
        `
    };
});

SIREPO.app.directive('appHeader', function(appState, panelState) {
    return {
	restrict: 'A',
	scope: {
            nav: '=appHeader',
	},
        template: `

              <app-header-right-sim-loaded>

 Metadata

 Analysis

              </app-header-right-sim-loaded>

        `
    };
});

SIREPO.app.directive('metadataTable', function() {
    return {
        restrict: 'A',
        scope: {
	    type: '@'
	},
        template: `

                    Field
                    Value

                    {{ v[0] }}
                    {{ v[1] }}
                    <button class="glyphicon glyphicon-plus" data-ng-if="isOverflown(v[0])" data-ng-click="toggleExpanded(v[0])">
                    <button class="glyphicon glyphicon-minus" data-ng-if="expanded[v[0]]" data-ng-click="toggleExpanded(v[0])">

	`,
	controller: function(appState, requestSender, $sce, $scope) {
	    $scope.data = null;
	    $scope.expanded = {};

	    function elementForKey(key) {
		return $('#' + $scope.elementId(key));
	    }

	    $scope.elementId = function(key) {
		return 'metadata-table-' + $scope.type + '-' + $scope.indexOfKey(key);
	    }

	    $scope.indexOfKey = function(key) {
		for (let i in $scope.data) {
		    if ($scope.data[i][0] === key) {
			return i;
		    }
		}
		throw new Error(`No key=${key} in data=${$scope.data}`);
	    }

	    $scope.isOverflown = function(key) {
		const e = elementForKey(key)
		return e.prop('clientWidth') < e.prop('scrollWidth');
	    };

	    $scope.toggleExpanded = function(key) {
		if (!(key in $scope.expanded)) {
		    $scope.expanded[key] = false;
		}
		$scope.expanded[key] = ! $scope.expanded[key];
		elementForKey(key).toggleClass('raydata-overflow-text');
	    };

	    requestSender.statelessCompute(
		appState,
		{
		    method: $scope.type + '_metadata'
		},
		(data) => {
		    $scope.data  = Object.entries(data.data).map(([k, v]) => [k, v]);
		}
	    );
        },
    };
});
<|endoftext|>


<commit_before>import GIFEncoder from 'gif-encoder';
import blobStream from 'blob-stream';
import { saveAs } from 'file-saver';

function randomName() {
  return Math.random()
    .toString(36)
    .replace(/[^a-z]+/g, '')
    .substr(0, 8);
}

function fillCanvasWithFrame(canvas, frameInfo) {
  const { frame, cols, cellSize, frameHeight, frameIdx } = frameInfo;
  const ctx = canvas;
  frame.get('grid').forEach((fillStyle, pixelIdx) => {
    if (!fillStyle) {
      return;
    }
    ctx.fillStyle = fillStyle;

    const col = pixelIdx % cols;
    const row = Math.floor(pixelIdx / cols);
    ctx.fillRect(
      col * cellSize,
      row * cellSize + frameHeight * frameIdx,
      cellSize,
      cellSize
    );
  });
  return ctx;
}

function renderImageToCanvas(type, canvasInfo, currentFrameInfo, frames) {
  const { canvas, canvasHeight, canvasWidth } = canvasInfo;
  const { frame, frameHeight, frameWidth, cellSize } = currentFrameInfo;
  const cols = Math.floor(frameWidth / cellSize);
  let ctx = canvas.getContext('2d');
  ctx.canvas.width = canvasWidth;
  ctx.canvas.height = canvasHeight;
  switch (type) {
    case 'spritesheet':
      frames.forEach((currentFrame, frameIdx) => {
        ctx = fillCanvasWithFrame(ctx, {
          frame: currentFrame,
          cols,
          cellSize,
          frameHeight,
          frameIdx
        });
      });
      break;
    default:
      ctx = fillCanvasWithFrame(ctx, {
        frame,
        cols,
        cellSize,
        frameHeight,
        frameIdx: 0
      });
      break;
  }
  return ctx.getImageData(0, 0, canvasWidth, canvasHeight).data;
}

function renderFrames(settings, sendNotification) {
  const {
    type,
    frames,
    duration,
    activeFrame,
    rows,
    columns,
    cellSize
  } = settings;

  const durationInMillisecond = duration * 1000;
  const frameWidth = columns * cellSize;
  const frameHeight = rows * cellSize;
  const canvasWidth = frameWidth;
  const canvasHeight =
    type === 'spritesheet' ? frameHeight * frames.size : frameHeight;

  const canvas = document.createElement('canvas');
  const gif = new GIFEncoder(canvasWidth, canvasHeight);
  gif.pipe(blobStream()).on('finish', function() {
    const blobURL = this.toBlobURL();
    const a = document.createElement('a');
    a.style.display = 'none';
    a.href = blobURL;
    a.setAttribute('download', `${randomName()}.gif`);
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    setTimeout(() => {
      window.URL.revokeObjectURL(blobURL);
      sendNotification('Downloading...');
    }, 100);
  });

  gif.setRepeat(0); // loop indefinitely
  gif.setDispose(3); // restore to previous
  gif.writeHeader();

  switch (type) {
    case 'single':
      renderImageToCanvas(
        type,
        {
          canvas,
          canvasHeight,
          canvasWidth
        },
        {
          frame: activeFrame,
          frameHeight,
          frameWidth,
          cellSize
        },
        frames
      );
      canvas.toBlob(function(blob) {
        saveAs(blob, `${randomName()}.png`);
        sendNotification('Downloading...');
      });
      break;
    case 'spritesheet':
      gif.addFrame(
        renderImageToCanvas(
          type,
          {
            canvas,
            canvasHeight,
            canvasWidth
          },
          {
            frame: activeFrame,
            frameHeight,
            frameWidth,
            cellSize
          },
          frames
        )
      );
      gif.finish();
      break;
    default: {
      let previousInterval = 0;
      frames.forEach((frame, idx, framesArray) => {
        const isLastFrame = idx === framesArray.length - 1;
        const currentInterval = isLastFrame
          ? 100
          : frames.get(idx).get('interval');
        const diff = currentInterval - previousInterval;
        const delay = diff * 0.01 * durationInMillisecond;

        gif.setDelay(delay);
        previousInterval = currentInterval;

        gif.addFrame(
          renderImageToCanvas(
            type,
            {
              canvas,
              canvasHeight,
              canvasWidth
            },
            {
              frame,
              frameHeight,
              frameWidth,
              cellSize
            }
          )
        );
      });
      gif.finish();
    }
  }
}

export default renderFrames;
<commit_msg>Download spritesheet as png instead of gif for transparency<commit_after>import GIFEncoder from 'gif-encoder';
import blobStream from 'blob-stream';
import { saveAs } from 'file-saver';

function randomName() {
  return Math.random()
    .toString(36)
    .replace(/[^a-z]+/g, '')
    .substr(0, 8);
}

function fillCanvasWithFrame(canvas, frameInfo) {
  const { frame, cols, cellSize, frameHeight, frameIdx } = frameInfo;
  const ctx = canvas;
  frame.get('grid').forEach((fillStyle, pixelIdx) => {
    if (!fillStyle) {
      return;
    }
    ctx.fillStyle = fillStyle;

    const col = pixelIdx % cols;
    const row = Math.floor(pixelIdx / cols);
    ctx.fillRect(
      col * cellSize,
      row * cellSize + frameHeight * frameIdx,
      cellSize,
      cellSize
    );
  });
  return ctx;
}

function renderImageToCanvas(type, canvasInfo, currentFrameInfo, frames) {
  const { canvas, canvasHeight, canvasWidth } = canvasInfo;
  const { frame, frameHeight, frameWidth, cellSize } = currentFrameInfo;
  const cols = Math.floor(frameWidth / cellSize);
  let ctx = canvas.getContext('2d');
  ctx.canvas.width = canvasWidth;
  ctx.canvas.height = canvasHeight;
  switch (type) {
    case 'spritesheet':
      frames.forEach((currentFrame, frameIdx) => {
        ctx = fillCanvasWithFrame(ctx, {
          frame: currentFrame,
          cols,
          cellSize,
          frameHeight,
          frameIdx
        });
      });
      break;
    default:
      ctx = fillCanvasWithFrame(ctx, {
        frame,
        cols,
        cellSize,
        frameHeight,
        frameIdx: 0
      });
      break;
  }
  return ctx.getImageData(0, 0, canvasWidth, canvasHeight).data;
}

function renderFrames(settings, sendNotification) {
  const {
    type,
    frames,
    duration,
    activeFrame,
    rows,
    columns,
    cellSize
  } = settings;

  const durationInMillisecond = duration * 1000;
  const frameWidth = columns * cellSize;
  const frameHeight = rows * cellSize;
  const canvasWidth = frameWidth;
  const canvasHeight =
    type === 'spritesheet' ? frameHeight * frames.size : frameHeight;

  const canvas = document.createElement('canvas');
  const gif = new GIFEncoder(canvasWidth, canvasHeight);
  gif.pipe(blobStream()).on('finish', function() {
    const blobURL = this.toBlobURL();
    const a = document.createElement('a');
    a.style.display = 'none';
    a.href = blobURL;
    a.setAttribute('download', `${randomName()}.gif`);
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    setTimeout(() => {
      window.URL.revokeObjectURL(blobURL);
      sendNotification('Downloading...');
    }, 100);
  });

  gif.setRepeat(0); // loop indefinitely
  gif.setDispose(3); // restore to previous
  gif.writeHeader();

  switch (type) {
    case 'single':
    case 'spritesheet':
      renderImageToCanvas(
        type,
        {
          canvas,
          canvasHeight,
          canvasWidth
        },
        {
          frame: activeFrame,
          frameHeight,
          frameWidth,
          cellSize
        },
        frames
      );
      canvas.toBlob(function(blob) {
        saveAs(blob, `${randomName()}.png`);
        sendNotification('Downloading...');
      });
      break;
    default: {
      let previousInterval = 0;
      frames.forEach((frame, idx, framesArray) => {
        const isLastFrame = idx === framesArray.length - 1;
        const currentInterval = isLastFrame
          ? 100
          : frames.get(idx).get('interval');
        const diff = currentInterval - previousInterval;
        const delay = diff * 0.01 * durationInMillisecond;

        gif.setDelay(delay);
        previousInterval = currentInterval;

        gif.addFrame(
          renderImageToCanvas(
            type,
            {
              canvas,
              canvasHeight,
              canvasWidth
            },
            {
              frame,
              frameHeight,
              frameWidth,
              cellSize
            }
          )
        );
      });
      gif.finish();
    }
  }
}

export default renderFrames;
<|endoftext|>


<commit_before>import { isArray, isFunction, isNumber, isObject, isEmpty, isUndefined } from './is';
import invariant from './invariant';
import { removeItems } from './object';

/**
 * Convert path format to an array
 */
function pathToArray(path) {
	if (isArray(path)) return path;
	if (isEmpty(path)) return [];
	return path
		.replace(/\\\./g, '@')
		.replace(/\./g, '*')
		.replace(/@/g, '.')
		.split('*');
}

/**
 * Handle index of array
 */
function getArrayIndex(head) {
	invariant(isNumber(head), `Array index '${head}' has to be an integer`);
	return head;
}

/**
 * Set a value. Check merge for multiple values
 * @param src The object to evaluate.
 * @param path The path to be set.
 * @param value The value to set.
 */
export function set(src, path, value) {
	invariant(!isEmpty(path), 'path is required for setting data');
	const pathArr = pathToArray(path);

	const setImmutable = (obj, pathList, val) => {
		if (!pathList.length) return isFunction(value) ? val(obj, pathList[0]) : val;
		const isArr = isArray(obj);
		const clone = isArr ? obj.slice() : Object.assign({}, obj);
		const curPath = isArr ? getArrayIndex(pathList[0]) : pathList[0];
		clone[curPath] = setImmutable(!isUndefined(obj[curPath]) ? obj[curPath] : {}, pathList.slice(1), val);
		return clone;
	};

	return setImmutable(src, pathArr, value);
}

/**
 * Get a value by a dot path.
 * @param src The object to evaluate.
 * @param path The path to value that should be returned.
 */
export function get(src, path) {
	const pathList = pathToArray(path);
	if (pathList.length === 0) return src;
	return pathList.reduce((result, pathFragment) => {
		return result ? result[pathFragment] : src[pathFragment];
	}, null);
}

/**
 * Delete an array of items.
 * If target container is an object, the property is deleted.
 * If target container is an array, the index is deleted.
 * If target container is undefined, nothing is deleted.
 * @param src The object to evaluate.
 * @param path The path to target container.
 * @param _ids The array of property or index which will be deleted.
 */
export function remove(src, path, _ids) {
	invariant(arguments.length >= 3, 'src, path and _ids are required');
	invariant(isArray(_ids), `Expected _ids to be an array but got ${typeof _ids}`);
	if (isEmpty(path)) {
		if (isArray(src)) return src.filter((i, index) => !_ids.includes(index));
		if (isObject(src)) return removeItems(src, _ids);
		return src;
	}

	if (isUndefined(get(src, path))) return src;

	return set(src, path, (val) => {
		if (isArray(val)) {
			invariant(!(_ids.some((id) => !isNumber(id))), 'Array index has to be an integer');
			return val.filter((v, i) => !_ids.includes(i));
		} else if (isObject(val)) {
			const idStrList = _ids.map(String);
			return Object.keys(val).reduce((result, k) => {
				return !idStrList.includes(k) ? { ...result, [k]: val[k] } : result;
			}, {});
		}
		return val;
	});
}

/**
 * Toggle a value.  The target value must be boolean.
 * @param src The object to evaluate.
 * @param path The path to the value.
 */
export function toggle(src, path) {
	return set(src, path, (curVal) => {
		invariant(isBool(curVal), `Expected _ids to be boolean but got ${typeof curVal}`);
		return !curVal;
	});
}

<commit_msg>add immutable merge<commit_after>import { isArray, isFunction, isNumber, isObject, isEmpty, isUndefined } from './is';
import invariant from './invariant';
import { removeItems } from './object';

/**
 * Convert path format to array
 */
function pathToArray(path) {
	if (isArray(path)) return path;
	if (isEmpty(path)) return [];
	return path
		.replace(/\\\./g, '@')
		.replace(/\./g, '*')
		.replace(/@/g, '.')
		.split('*');
}

/**
 * Handle index of array
 */
function getArrayIndex(head) {
	invariant(isNumber(head), `Array index '${head}' has to be an integer`);
	return head;
}

/**
 * Set a value. Check merge for multiple values
 * @param src The object to evaluate.
 * @param path The path to be set.
 * @param value The value to set.
 */
export function set(src, path, value) {
	invariant(!isEmpty(path), 'path is required for setting data');
	const pathArr = pathToArray(path);

	const setImmutable = (obj, pathList, val) => {
		if (!pathList.length) return isFunction(value) ? val(obj, pathList[0]) : val;
		const isArr = isArray(obj);
		const clone = isArr ? obj.slice() : Object.assign({}, obj);
		const curPath = isArr ? getArrayIndex(pathList[0]) : pathList[0];
		clone[curPath] = setImmutable(!isUndefined(obj[curPath]) ? obj[curPath] : {}, pathList.slice(1), val);
		return clone;
	};

	return setImmutable(src, pathArr, value);
}

/**
 * Get a value by a dot path.
 * @param src The object to evaluate.
 * @param path The path to value that should be returned.
 */
export function get(src, path) {
	const pathList = pathToArray(path);
	if (pathList.length === 0) return src;
	return pathList.reduce((result, pathFragment) => {
		return result ? result[pathFragment] : src[pathFragment];
	}, null);
}

/**
 * Delete an array of items.
 * If target container is an object, the property is deleted.
 * If target container is an array, the index is deleted.
 * If target container is undefined, nothing is deleted.
 * @param src The object to evaluate.
 * @param path The path to target container.
 * @param _ids The array of property or index which will be deleted.
 */
export function remove(src, path, _ids) {
	invariant(arguments.length >= 3, 'src, path and _ids are required');
	invariant(isArray(_ids), `Expected _ids to be an array but got ${typeof _ids}`);
	if (isEmpty(path)) {
		if (isArray(src)) return src.filter((i, index) => !_ids.includes(index));
		if (isObject(src)) return removeItems(src, _ids);
		return src;
	}

	if (isUndefined(get(src, path))) return src;

	return set(src, path, (val) => {
		if (isArray(val)) {
			invariant(!(_ids.some((id) => !isNumber(id))), 'Array index has to be an integer');
			return val.filter((v, i) => !_ids.includes(i));
		} else if (isObject(val)) {
			const idStrList = _ids.map(String);
			return Object.keys(val).reduce((result, k) => {
				return !idStrList.includes(k) ? { ...result, [k]: val[k] } : result;
			}, {});
		}
		return val;
	});
}

/**
 * Merges a value.  The target value must be an object, array, null, or undefined.
 * If target is an object, Object.assign({}, target, param) is used.
 * If target an array, target.concat(param) is used.
 * If target is null or undefined, the value is simply set.
 * @param src The object to evaluate.
 * @param path The path to the value.
 * @param val The value to merge into the target value.
 */
export function merge(src, path, val) {
	return set(src, path, (curVal) => {
		if (curVal === null || isUndefined(curVal)) {
			return val;
		} else if (isArray(curVal)) {
			return curVal.concat(val);
		} else if (isObject(curVal)) {
			return Object.assign({}, curVal, val);
		}
		return src;
	});
}
<|endoftext|>


<commit_before>import fs from "fs";
import elasticsearch from "elasticsearch";
import BPromise from 'bluebird';
import request from 'request';

const esClient = new elasticsearch.Client({
  //log: "trace",
  host: "http://128.193.70.68:9200",
  keepAlive: false,
  requestTimeout: 60 * 60 * 1000 // 1 hour
});

let index = "magic_v2";

  describe("magic.data_doi", () => {

    it("should update data dois", function (done) { setTimeout(() => {
      this.timeout(0);

      console.log(process.env.EZID_USER, process.env.EZID_PASS);

      esClient.search({
        index: index, type: "contribution", size: 1e4,
        _source: ["summary.contribution.id", "summary.contribution._reference.title", "summary.contribution._reference.long_authors"],
        body: {
          "query": { "bool": {
            "must": [
              { "exists": { "field": "summary.contribution._reference.long_authors" }},
              { "exists": { "field": "summary.contribution._reference.title" }},
              { "term": { "summary.contribution._is_latest": "true"}}
            ],
            "must_not": [{ "term": { "summary.contribution._has_data_doi": "true"}}]
            //"filter": { "term": { "summary.contribution.id": 16450}}
          }}
        }
      }).then((resp) => {
        console.log('Contributions without a data DOI:', resp.hits.total);
        if (resp.hits.total > 0) {

          BPromise.each(resp.hits.hits, hit => {
            return new Promise((resolve) => {
              let xml = `<?xml version="1.0"?>
<database xmlns="http://www.crossref.org/schema/4.4.0"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://www.crossref.org/schema/4.4.0
  http://www.crossref.org/schema/deposit/crossref4.4.0.xsd"
>
  <contributors>
    <person_name contributor_role="author" sequence="first">
      <surname>${hit._source.summary.contribution._reference.long_authors}</surname>
    </person_name>
  </contributors>
  <titles>
    <title>${hit._source.summary.contribution._reference.title}</title>
  </titles>
  <publisher_item>
    <item_number item_number_type="MagIC Contribution ID">${hit._source.summary.contribution.id}</item_number>
  </publisher_item>
  <doi_data>
    <doi></doi>
    <resource></resource>
  </doi_data>
</database>`;
              xml = xml.replace(/%/g, "%25").replace(/\s*\n\s*/g, "%0A").replace(/\r/g, "%0D").replace(/:/g, "%3A");
              let payload =
`_crossref: no
_profile: crossref
_status: public
_target: https://earthref.org/MagIC/${hit._source.summary.contribution.id}
crossref: ${xml}`;

              request({
                method: 'PUT',
                uri: 'https://ezid.cdlib.org/id/doi:10.7288/V4/MagIC/' + hit._source.summary.contribution.id,
                auth: {
                  user: '
                  pass: '
                },
                headers: {'content-type' : 'text/plain; charset=UTF-8'},
                body: payload
              }, function (error, response, body) {
                if (error) {
                  console.error(error);
                  resolve();
                } else if (body === 'error: bad request - identifier already exists') {
                  esClient.update({
                    "index": hit._index,
                    "type":hit._type,
                    "id": hit._id,
                    "refresh": true,
                    "body": {
                      "doc": {
                        "summary": {
                          "contribution": {
                            "_has_data_doi": "true"
                          }
                        }
                      }
                    }
                  }).then(() => {
                    console.log(hit._id, 'already had a Data DOI minted');
                    resolve();
                  });
                }  else if (/^error: /.test(body)) {
                  console.error(body);
                  resolve();
                } else {
                  esClient.update({
                    "index": hit._index,
                    "type":hit._type,
                    "id": hit._id,
                    "refresh": true,
                    "body": {
                      "doc": {
                        "summary": {
                          "contribution": {
                            "_has_data_doi": "true"
                          }
                        }
                      }
                    }
                  }).then(() => {
                    console.log(hit._id, 'now has a Data DOI');
                    resolve();
                  });
                }
              });

            });

          }, { concurrency: 5 }).then((results) => {
            done();
          });

        }
      });

    }, 0)});

  }
);<commit_msg>Add publisher and year to DataDOI minting<commit_after>import fs from "fs";
import elasticsearch from "elasticsearch";
import BPromise from 'bluebird';
import request from 'request';

const esClient = new elasticsearch.Client({
  //log: "trace",
  host: "http://128.193.70.68:9200",
  keepAlive: false,
  requestTimeout: 60 * 60 * 1000 // 1 hour
});

let index = "magic_v2";

  describe("magic.data_doi", () => {

    it("should update data dois", function (done) { setTimeout(() => {
      this.timeout(0);

      console.log(process.env.EZID_USER, process.env.EZID_PASS);

      esClient.search({
        index: index, type: "contribution", size: 1e4,
        _source: [
          "summary.contribution.id",
          "summary.contribution._reference.title",
          "summary.contribution._reference.long_authors",
          "summary.contribution._reference.year"
        ],
        body: {
          "query": { "bool": {
            "must": [
              { "exists": { "field": "summary.contribution._reference.long_authors" }},
              { "exists": { "field": "summary.contribution._reference.title" }},
              { "exists": { "field": "summary.contribution._reference.year" }}
              //{ "term": { "summary.contribution._is_latest": "true"}}
            ],
            //"must_not": [{ "term": { "summary.contribution._has_data_doi": "true"}}]
            //"filter": { "term": { "summary.contribution.id": 16450}}
          }}
        }
      }).then((resp) => {
        console.log('Contributions without a data DOI:', resp.hits.total);
        if (resp.hits.total > 0) {

          BPromise.each(resp.hits.hits, hit => {
            return new Promise((resolve) => {
              let xml = `<?xml version="1.0"?>
<database xmlns="http://www.crossref.org/schema/4.4.0"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://www.crossref.org/schema/4.4.0
  http://www.crossref.org/schema/deposit/crossref4.4.0.xsd"
>
  <contributors>
    <person_name contributor_role="author" sequence="first">
      <surname>${hit._source.summary.contribution._reference.long_authors}</surname>
    </person_name>
  </contributors>
  <titles>
    <title>${hit._source.summary.contribution._reference.title}</title>
  </titles>
  <publisher_item>
    <item_number item_number_type="MagIC Contribution ID">${hit._source.summary.contribution.id}</item_number>
  </publisher_item>
  <publisher><publisher_name>Magnetics Information Consortium (MagIC)</publisher_name></publisher>
  <publication_date><year>${hit._source.summary.contribution._reference.year}</year></publication_date>
  <doi_data>
    <doi></doi>
    <resource></resource>
  </doi_data>
</database>`;
              xml = xml.replace(/%/g, "%25").replace(/\s*\n\s*/g, "%0A").replace(/\r/g, "%0D").replace(/:/g, "%3A");
              let payload =
`_profile: crossref
_status: public
_target: https://earthref.org/MagIC/${hit._source.summary.contribution.id}
crossref: ${xml}`;

              request({
                method: 'POST',
                uri: 'https://ezid.cdlib.org/id/doi:10.7288/V4/MagIC/' + hit._source.summary.contribution.id,
                auth: {
                  user: '
                  pass: '
                },
                headers: {'content-type' : 'text/plain; charset=UTF-8'},
                body: payload
              }, function (error, response, body) {
                if (error) {
                  console.error(error);
                  resolve();
                } else if (body === 'error: bad request - identifier already exists') {
                  esClient.update({
                    "index": hit._index,
                    "type":hit._type,
                    "id": hit._id,
                    "refresh": true,
                    "body": {
                      "doc": {
                        "summary": {
                          "contribution": {
                            "_has_data_doi": "true"
                          }
                        }
                      }
                    }
                  }).then(() => {
                    console.log(hit._id, 'already had a Data DOI minted');
                    resolve();
                  });
                }  else if (/^error: /.test(body)) {
                  console.error(body);
                  resolve();
                } else {
                  esClient.update({
                    "index": hit._index,
                    "type":hit._type,
                    "id": hit._id,
                    "refresh": true,
                    "body": {
                      "doc": {
                        "summary": {
                          "contribution": {
                            "_has_data_doi": "true"
                          }
                        }
                      }
                    }
                  }).then(() => {
                    console.log(hit._id, 'now has a Data DOI');
                    resolve();
                  });
                }
              });

            });

          }, { concurrency: 5 }).then((results) => {
            done();
          });

        }
      });

    }, 0)});

  }
);<|endoftext|>


<commit_before>/* @flow strict-local */
import type { PmRecipientUser, Message, Outbox, User } from '../types';

// Filter a list of PM recipients in the quirky way that we do.
//
// Specifically: all users, except the self-user, except if it's the
// self-1:1 thread then include the self-user after all.
//
// This is a module-private helper.  See callers for what this set of
// conditions *means* -- two different things, in fact, that have the same
// behavior by coincidence.
const filterRecipients = (recipients: PmRecipientUser[], ownUserId: number): PmRecipientUser[] =>
  recipients.length === 1 ? recipients : recipients.filter(r => r.id !== ownUserId);

// TODO types: this union is confusing
export const normalizeRecipients = (recipients: $ReadOnlyArray<{ email: string, ... }> | string) =>
  !Array.isArray(recipients)
    ? recipients
    : recipients
        .map(s => s.email.trim())
        .filter(x => x.length > 0)
        .sort()
        .join(',');

export const normalizeRecipientsSansMe = (
  recipients: $ReadOnlyArray<{ email: string, ... }>,
  ownEmail: string,
) =>
  recipients.length === 1
    ? recipients[0].email
    : normalizeRecipients(recipients.filter(r => r.email !== ownEmail));

/**
 * The set of users to show in the UI to identify a PM conversation.
 *
 * See also:
 *  * `pmKeyRecipientsFromMessage`, which should be used when a consistent,
 *    unique key is needed for identifying different PM conversations in our
 *    data structures.
 */
export const pmUiRecipientsFromMessage = (
  message: Message | Outbox,
  ownUser: User,
): PmRecipientUser[] => {
  if (message.type !== 'private') {
    throw new Error('pmUiRecipientsFromMessage: expected PM, got stream message');
  }
  return filterRecipients(message.display_recipient, ownUser.user_id);
};

/**
 * The set of users to identify a PM conversation by in our data structures.
 *
 * Typically we go on to take either the emails or user IDs in the result,
 * stringify them, and join with `,` to produce a string key.  IDs are
 * preferred; see #3764.
 *
 * See also:
 *  * `pmUiRecipientsFromMessage`, which gives a set of users to show in the
 *    UI.
 *
 *  * The `Narrow` type and its constructors in `narrow.js`, which with
 *    `JSON.stringify` we use to make keys to identify narrows in general,
 *    including stream and topic narrows.
 *
 *  * `normalizeRecipients`, `normalizeRecipientsSansMe`, and
 *    `pmUnreadsKeyFromMessage`, which do the same job as this function with
 *    slight variations, and which we variously use in different places in
 *    the app.
 *
 *    It would be great to unify on a single version, as the variation is a
 *    possible source of bugs.
 */
export const pmKeyRecipientsFromMessage = (
  message: Message | Outbox,
  ownUser: User,
): PmRecipientUser[] => {
  if (message.type !== 'private') {
    throw new Error('pmKeyRecipientsFromMessage: expected PM, got stream message');
  }
  return filterRecipients(message.display_recipient, ownUser.user_id);
};

/**
 * The key this PM is filed under in the "unread messages" data structure.
 *
 * Note this diverges slightly from pmKeyRecipientsFromMessage in its
 * behavior -- it encodes a different set of users.
 *
 * See also:
 *  * `pmKeyRecipientsFromMessage`, which we use for other data structures.
 *  * `UnreadState`, the type of `state.unread`, which is the data structure
 *    these keys appear in.
 *
 * @param ownUserId - Required if the message could be a 1:1 PM; optional if
 *   it is definitely a group PM.
 */
// Specifically, this includes all user IDs for group PMs and self-PMs,
// and just the other user ID for non-self 1:1s; and in each case the list
// is sorted and encoded in ASCII-decimal, comma-separated.
// See the `unread_msgs` data structure in `src/api/initialDataTypes.js`.
export const pmUnreadsKeyFromMessage = (message: Message, ownUserId?: number): string => {
  if (message.type !== 'private') {
    throw new Error('pmUnreadsKeyFromMessage: expected PM, got stream message');
  }
  const recipients: PmRecipientUser[] = message.display_recipient;
  // This includes all users in the thread; see `Message#display_recipient`.
  const userIds = recipients.map(r => r.id);

  if (userIds.length === 1) {
    // Self-PM.
    return userIds[0].toString();
  } else if (userIds.length === 2) {
    // Non-self 1:1 PM.  Unlike display_recipient, leave out the self user.
    if (ownUserId === undefined) {
      throw new Error('getRecipientsIds: got 1:1 PM, but ownUserId omitted');
    }
    return userIds.filter(userId => userId !== ownUserId)[0].toString();
  } else {
    // Group PM.
    return userIds.sort((a, b) => a - b).join(',');
  }
};

export const isSameRecipient = (
  message1: Message | Outbox,
  message2: Message | Outbox,
): boolean => {
  if (message1 === undefined || message2 === undefined) {
    return false;
  }

  if (message1.type !== message2.type) {
    return false;
  }

  switch (message1.type) {
    case 'private':
      return (
        normalizeRecipients(message1.display_recipient).toLowerCase()
        === normalizeRecipients(message2.display_recipient).toLowerCase()
      );
    case 'stream':
      return (
        message1.display_recipient.toLowerCase() === message2.display_recipient.toLowerCase()
        && message1.subject.toLowerCase() === message2.subject.toLowerCase()
      );
    default:
      // Invariant
      return false;
  }
};
<commit_msg>recipient [nfc]: Add minor documentation clarification.<commit_after>/* @flow strict-local */
import type { PmRecipientUser, Message, Outbox, User } from '../types';

// Filter a list of PM recipients in the quirky way that we do.
//
// Specifically: all users, except the self-user, except if it's the
// self-1:1 thread then include the self-user after all.
//
// This is a module-private helper.  See callers for what this set of
// conditions *means* -- two different things, in fact, that have the same
// behavior by coincidence.
const filterRecipients = (recipients: PmRecipientUser[], ownUserId: number): PmRecipientUser[] =>
  recipients.length === 1 ? recipients : recipients.filter(r => r.id !== ownUserId);

// TODO types: this union is confusing
export const normalizeRecipients = (recipients: $ReadOnlyArray<{ email: string, ... }> | string) =>
  !Array.isArray(recipients)
    ? recipients
    : recipients
        .map(s => s.email.trim())
        .filter(x => x.length > 0)
        .sort()
        .join(',');

export const normalizeRecipientsSansMe = (
  recipients: $ReadOnlyArray<{ email: string, ... }>,
  ownEmail: string,
) =>
  recipients.length === 1
    ? recipients[0].email
    : normalizeRecipients(recipients.filter(r => r.email !== ownEmail));

/**
 * The set of users to show in the UI to identify a PM conversation.
 *
 * See also:
 *  * `pmKeyRecipientsFromMessage`, which should be used when a consistent,
 *    unique key is needed for identifying different PM conversations in our
 *    data structures.
 */
export const pmUiRecipientsFromMessage = (
  message: Message | Outbox,
  ownUser: User,
): PmRecipientUser[] => {
  if (message.type !== 'private') {
    throw new Error('pmUiRecipientsFromMessage: expected PM, got stream message');
  }
  return filterRecipients(message.display_recipient, ownUser.user_id);
};

/**
 * The set of users to identify a PM conversation by in our data structures.
 *
 * Typically we go on to take either the emails or user IDs in the result,
 * stringify them, and join with `,` to produce a string key.  IDs are
 * preferred; see #3764.
 *
 * See also:
 *  * `pmUiRecipientsFromMessage`, which gives a set of users to show in the
 *    UI.
 *
 *  * The `Narrow` type and its constructors in `narrow.js`, which with
 *    `JSON.stringify` we use to make keys to identify narrows in general,
 *    including stream and topic narrows.
 *
 *  * `normalizeRecipients`, `normalizeRecipientsSansMe`, and
 *    `pmUnreadsKeyFromMessage`, which do the same job as this function with
 *    slight variations, and which we variously use in different places in
 *    the app.
 *
 *    It would be great to unify on a single version, as the variation is a
 *    possible source of bugs.
 */
export const pmKeyRecipientsFromMessage = (
  message: Message | Outbox,
  ownUser: User,
): PmRecipientUser[] => {
  if (message.type !== 'private') {
    throw new Error('pmKeyRecipientsFromMessage: expected PM, got stream message');
  }
  return filterRecipients(message.display_recipient, ownUser.user_id);
};

/**
 * The key this PM is filed under in the "unread messages" data structure.
 *
 * Note this diverges slightly from pmKeyRecipientsFromMessage in its
 * behavior -- it encodes a different set of users.
 *
 * See also:
 *  * `pmKeyRecipientsFromMessage`, which we use for other data structures.
 *  * `UnreadState`, the type of `state.unread`, which is the data structure
 *    these keys appear in.
 *
 * @param ownUserId - Required if the message could be a 1:1 PM; optional if
 *   it is definitely a group PM.
 */
// Specifically, this includes all user IDs for group PMs and self-PMs,
// and just the other user ID for non-self 1:1s; and in each case the list
// is sorted numerically and encoded in ASCII-decimal, comma-separated.
// See the `unread_msgs` data structure in `src/api/initialDataTypes.js`.
export const pmUnreadsKeyFromMessage = (message: Message, ownUserId?: number): string => {
  if (message.type !== 'private') {
    throw new Error('pmUnreadsKeyFromMessage: expected PM, got stream message');
  }
  const recipients: PmRecipientUser[] = message.display_recipient;
  // This includes all users in the thread; see `Message#display_recipient`.
  const userIds = recipients.map(r => r.id);

  if (userIds.length === 1) {
    // Self-PM.
    return userIds[0].toString();
  } else if (userIds.length === 2) {
    // Non-self 1:1 PM.  Unlike display_recipient, leave out the self user.
    if (ownUserId === undefined) {
      throw new Error('getRecipientsIds: got 1:1 PM, but ownUserId omitted');
    }
    return userIds.filter(userId => userId !== ownUserId)[0].toString();
  } else {
    // Group PM.
    return userIds.sort((a, b) => a - b).join(',');
  }
};

export const isSameRecipient = (
  message1: Message | Outbox,
  message2: Message | Outbox,
): boolean => {
  if (message1 === undefined || message2 === undefined) {
    return false;
  }

  if (message1.type !== message2.type) {
    return false;
  }

  switch (message1.type) {
    case 'private':
      return (
        normalizeRecipients(message1.display_recipient).toLowerCase()
        === normalizeRecipients(message2.display_recipient).toLowerCase()
      );
    case 'stream':
      return (
        message1.display_recipient.toLowerCase() === message2.display_recipient.toLowerCase()
        && message1.subject.toLowerCase() === message2.subject.toLowerCase()
      );
    default:
      // Invariant
      return false;
  }
};
<|endoftext|>


<commit_before>'use strict'

module.exports = function () {
  var init = function (scope, elem, attrs, formField) {
    // add watchers for any logical functions which need to be executed based on a form variable
    if (formField.skipLogic.func && formField.skipLogic.func.watchingVars) {
      formField.skipLogic.func.watchingVars.forEach(function (watchingVar, key) {
        var varToWatch = 'form.' + watchingVar + '.$viewValue'
        scope.$watch(varToWatch, function (value, oldValue) {
          var params = formField.skipLogic.func.params
          var paramsObj = {}
          for (var p = 0; p < params.length; p++) {
            if (scope.form[params[p]]) {
              paramsObj[params[p]] = scope.form[params[p]].$modelValue
            } else {
              if (typeof params[p] === 'object') {
                for (var key in params[p]) {
                  if (params[p].hasOwnProperty(key)) {
                    paramsObj[key] = params[p][key]
                  }
                }
              } else {
                paramsObj[params[p]] = params[p]
              }
            }
          }

          if (typeof formField.skipLogic.func.execute === 'function') {
            var funcVal = formField.skipLogic.func.execute(paramsObj)
            scope.form[scope.field.name].$setViewValue(funcVal)
            scope.form[scope.field.name].$setUntouched()
            scope.form[scope.field.name].$setPristine()
            scope.form[scope.field.name].$render()
          }
        })
      })
    }

    // add watchers for any logic checks (show/hide)
    if (formField.skipLogic.checks.length > 0) {
      for (var i = 0; i < formField.skipLogic.checks.length; i++) {
        var check = formField.skipLogic.checks[i]
        if (check && check.group) {
          for (var j = 0; j < check.group.length; j++) {
            scope.$watch(check.group[j].variable.startsWith('form.') ? check.group[j].variable : 'form.' + check.group[j].variable + '.$modelValue', function (value, oldValue) {
              skipLogicGroupCheck(scope, value, check)
            }, true)
          }
        } else {
          scope.$watch(check.variable.startsWith('form.') ? check.variable : 'form.' + check.variable + '.$modelValue', function (value, oldValue) {
            skipLogicOperandCheck(scope, value, check)
          }, true)
        }
      }
    }
  }

  var operators = {
    '=': function (a, b) { return a === b },
    '!=': function (a, b) { return a !== b },
    '<': function (a, b) { return a < b },
    '<=': function (a, b) { return a <= b },
    '>': function (a, b) { return a > b },
    '>=': function (a, b) { return a >= b },

    'in': function (a, b) {
      if (b.indexOf(a) >= 0) {
        return true
      }
      return false
    },
    '!in': function (a, b) {
      if (b.indexOf(a) === -1) {
        return true
      }
      return false
    }
  }

  var skipLogicOperandCheck = function (scope, value, check) {
    skipLogicCheck(check.action, operators[check.operand](value, check.value), scope)
  }

  var skipLogicCheck = function (action, check, scope) {
    switch (action) {
      case 'checkPhoneNumber':
        if (check) {
          scope.field.settings.checkPhoneNumber = true
        } else {
          scope.field.settings.checkPhoneNumber = false
        }
        break
      case 'checkIdNumber':
        if (check) {
          scope.field.settings.checkIdNumber = true
        } else {
          scope.field.settings.checkIdNumber = false
        }
        break

      case 'disabled':
        if (check) {
          scope.field.settings.disabled = true
        } else {
          scope.field.settings.disabled = false
        }
        break
      case 'required':
        if (check) {
          scope.field.settings.required = true
        } else {
          scope.field.settings.required = false
        }
        break
      case 'showhide':
      default:
        if (check) {
          scope.field.show = true
        } else {
          scope.field.show = false
        }
    }
  }

  // TODO do a complex (more than one depth) groups, probably xor xnor would handle this
  var skipLogicGroupCheck = function (scope, value, check) {
    var logicGate = check.logicGate
    var checkEval = null
    for (var i = 0; i < check.group.length; i++) {
      var c = check.group[i]
      if (checkEval !== null) {
        if (logicGate === 'and') {
          checkEval = checkEval && operators[c.operand](value, c.value)
        } else if (logicGate === 'or') {
          checkEval = checkEval || operators[c.operand](value, c.value)
        }
      } else {
        checkEval = operators[c.operand](value, c.value)
      }
    }
    skipLogicCheck(check.action, checkEval, scope)
  }

  return {
    init: init,

    operators: operators,
    skipLogicOperandCheck: skipLogicOperandCheck,
    skipLogicGroupCheck: skipLogicGroupCheck
  }
}
<commit_msg>ANSA-288: Fixed logical gates 'or' and 'and' bug<commit_after>'use strict'

module.exports = function () {
  var init = function (scope, elem, attrs, formField) {
    // add watchers for any logical functions which need to be executed based on a form variable
    if (formField.skipLogic.func && formField.skipLogic.func.watchingVars) {
      formField.skipLogic.func.watchingVars.forEach(function (watchingVar, key) {
        var varToWatch = 'form.' + watchingVar + '.$viewValue'
        scope.$watch(varToWatch, function (value, oldValue) {
          var params = formField.skipLogic.func.params
          var paramsObj = {}
          for (var p = 0; p < params.length; p++) {
            if (scope.form[params[p]]) {
              paramsObj[params[p]] = scope.form[params[p]].$modelValue
            } else {
              if (typeof params[p] === 'object') {
                for (var key in params[p]) {
                  if (params[p].hasOwnProperty(key)) {
                    paramsObj[key] = params[p][key]
                  }
                }
              } else {
                paramsObj[params[p]] = params[p]
              }
            }
          }

          if (typeof formField.skipLogic.func.execute === 'function') {
            var funcVal = formField.skipLogic.func.execute(paramsObj)
            scope.form[scope.field.name].$setViewValue(funcVal)
            scope.form[scope.field.name].$setUntouched()
            scope.form[scope.field.name].$setPristine()
            scope.form[scope.field.name].$render()
          }
        })
      })
    }

    // add watchers for any logic checks (show/hide)
    if (formField.skipLogic.checks.length > 0) {
      for (var i = 0; i < formField.skipLogic.checks.length; i++) {
        var check = formField.skipLogic.checks[i]
        if (check && check.group) {
          for (var j = 0; j < check.group.length; j++) {
            scope.$watch(check.group[j].variable.startsWith('form.') ? check.group[j].variable : 'form.' + check.group[j].variable + '.$modelValue', function (value, oldValue) {
              skipLogicGroupCheck(scope, value, check)
            }, true)
          }
        } else {
          scope.$watch(check.variable.startsWith('form.') ? check.variable : 'form.' + check.variable + '.$modelValue', function (value, oldValue) {
            skipLogicOperandCheck(scope, value, check)
          }, true)
        }
      }
    }
  }

  var operators = {
    '=': function (a, b) { return a === b },
    '!=': function (a, b) { return a !== b },
    '<': function (a, b) { return a < b },
    '<=': function (a, b) { return a <= b },
    '>': function (a, b) { return a > b },
    '>=': function (a, b) { return a >= b },

    'in': function (a, b) {
      if (b.indexOf(a) >= 0) {
        return true
      }
      return false
    },
    '!in': function (a, b) {
      if (b.indexOf(a) === -1) {
        return true
      }
      return false
    }
  }

  var skipLogicOperandCheck = function (scope, value, check) {
    skipLogicCheck(check.action, operators[check.operand](value, check.value), scope)
  }

  var skipLogicCheck = function (action, check, scope) {
    switch (action) {
      case 'checkPhoneNumber':
        if (check) {
          scope.field.settings.checkPhoneNumber = true
        } else {
          scope.field.settings.checkPhoneNumber = false
        }
        break
      case 'checkIdNumber':
        if (check) {
          scope.field.settings.checkIdNumber = true
        } else {
          scope.field.settings.checkIdNumber = false
        }
        break

      case 'disabled':
        if (check) {
          scope.field.settings.disabled = true
        } else {
          scope.field.settings.disabled = false
        }
        break
      case 'required':
        if (check) {
          scope.field.settings.required = true
        } else {
          scope.field.settings.required = false
        }
        break
      case 'showhide':
      default:
        if (check) {
          scope.field.show = true
        } else {
          scope.field.show = false
        }
    }
  }

  // TODO do a complex (more than one depth) groups, probably xor xnor would handle this
  var skipLogicGroupCheck = function (scope, value, check) {
    var logicGate = check.logicGate
    var checkEval = null
    for (var i = 0; i < check.group.length; i++) {
      var c = check.group[i]
      value = scope.form && c.variable ? scope.form[c.variable].$modelValue : value
      if (checkEval !== null) {
        if (logicGate === 'and') {
          checkEval = checkEval && operators[c.operand](value, c.value)
        } else if (logicGate === 'or') {
          checkEval = checkEval || operators[c.operand](value, c.value)
        }
      } else {
        checkEval = operators[c.operand](value, c.value)
      }
    }
    skipLogicCheck(check.action, checkEval, scope)
  }

  return {
    init: init,

    operators: operators,
    skipLogicOperandCheck: skipLogicOperandCheck,
    skipLogicGroupCheck: skipLogicGroupCheck
  }
}
<|endoftext|>


<commit_before>describe('Abe', function() {

  describe('data', function() {

    before(function(client, done) {
      done();
    });

    after(function(client, done) {
      client.end(function() {
        done();
      });
    });

    afterEach(function(client, done) {
      done();
    });

    beforeEach(function(client, done) {
      done();
    });

    it('autocomplete: Create a autocomplete template post', function(client) {
      client
        .useXpath()
        .url('http://localhost:3003/abe/editor')
        .click('//*[@id="selectTemplate"]/option[2]')
        .waitForElementVisible("//div[@data-precontrib-templates='autocomplete']//input[@id='name']", 1000)
        .setValue("//div[@data-precontrib-templates='autocomplete']//input[@id='name']", 'autocomplete')
        .click("//button[@type='submit']")
        .pause(1000)
        .waitForElementVisible('//*[@id="abeForm"]', 2000)
        .assert.urlEquals("http://localhost:3003/abe/editor/autocomplete.html", "Clicked URL Matches with URL of the New Window");
    });

    it('autocomplete: Check input select fields', function(client) {
      client
        .useXpath()
        .url('http://localhost:3003/abe/editor/autocomplete.html')
        .click('//*[@id="colors.multiple"]/option[2]')
        .getText('//*[@id="colors"]', function(result) {
          elementValue = result.value;
          //console.log(result)
        })
        .perform(function() {
          console.log('elementValue', elementValue);
        })
        // .source(function (result){
        //     // Source will be stored in result.value
        //     console.log(result.value);
        // })
        .waitForElementVisible('//*[@data-parent-id="colors.multiple"]', 1000)
        .assert.containsText('//*[@data-parent-id="colors.multiple"]', 'rouge')
        .click('//*[@data-parent-id="colors.multiple"]/span')
        .assert.elementNotPresent('//*[@data-parent-id="colors.multiple"]')
    });

    it('autocomplete: Check input autocomplete fields', function(client) {
      client
        .useXpath()
        .url('http://localhost:3003/abe/editor/autocomplete.html')
        .setValue('//*[@id="colors.colors_autocomplete"]', 'rouge')
        .waitForElementVisible('//*[@id="colors"]/div/div/div/div[3]/div/div[2]/div', 1000)
        .click('//*[@id="colors"]/div/div/div/div[3]/div/div[2]/div')
        .waitForElementVisible('//*[@data-parent-id="colors.colors_autocomplete"]', 1000)
        .assert.containsText('//*[@data-parent-id="colors.colors_autocomplete"]', 'rouge')
        .click('//*[@data-parent-id="colors.colors_autocomplete"]/span')
        .assert.elementNotPresent('//*[@data-parent-id="colors.colors_autocomplete"]')
    });

    // it('autocomplete: Abe type data reference json', function(client) {
    //   client
    //     .useXpath()
    //     .url('http://localhost:3003/abe/editor/autocomplete.html')
    //     .waitForElementVisible('//body')
    //     .pause(1000)
    //     .click('//*[@id="abeForm"]/ul/li[2]/a')
    //     .waitForElementVisible('//*[@id="reference"]/div')
    //     .click('//*[@id="reference.single"]/option[2]')
    //     .click('//*[@id="reference.multiple"]/option[3]')
    //     .setValue('//*[@id="reference.autocomplete"]', 'rouge')
    //     .waitForElementVisible('//*[@id="reference"]/div/div/div/div[3]/div[2]', 2000)
    //     .click('//*[@id="reference"]/div/div/div/div[3]/div[2]/div[1]')
    //     .waitForElementVisible('//*[@id="reference"]/div/div/div/div[3]/div/div', 2000);
    // });
    //

    it('autocomplete: The autocomplete article is deleted in the manager', function(client) {
      client
        .useXpath()
        .url('http://localhost:3003/abe/editor')
        .waitForElementVisible('//body')
        .pause(1000)
        .click("//table[@id='navigation-list']/tbody/tr[1]/td[7]/div/a")
        .pause(1000)
        .acceptAlert()
        .url('http://localhost:3003/abe/editor')
        .pause(2000)
        .expect.element("//table[@id='navigation-list']/tbody/tr[1]/td[2]/a").text.to.not.contain('/articles/ftest.html');
    });
  });
});<commit_msg>ic: fix e2e<commit_after>describe('Abe', function() {

  describe('data', function() {

    before(function(client, done) {
      done();
    });

    after(function(client, done) {
      client.end(function() {
        done();
      });
    });

    afterEach(function(client, done) {
      done();
    });

    beforeEach(function(client, done) {
      done();
    });

    it('autocomplete: Create a autocomplete template post', function(client) {
      client
        .useXpath()
        .url('http://localhost:3003/abe/editor')
        .click('//*[@id="selectTemplate"]/option[2]')
        .waitForElementVisible("//div[@data-precontrib-templates='autocomplete']//input[@id='name']", 1000)
        .setValue("//div[@data-precontrib-templates='autocomplete']//input[@id='name']", 'autocomplete')
        .click("//button[@type='submit']")
        .pause(1000)
        .waitForElementVisible('//*[@id="abeForm"]', 2000)
        .assert.urlEquals("http://localhost:3003/abe/editor/autocomplete.html", "Clicked URL Matches with URL of the New Window");
    });

    it('autocomplete: Check input select fields', function(client) {
      client
        .useXpath()
        .url('http://localhost:3003/abe/editor/autocomplete.html')
        .execute(function() {
          var sel = document.getElementById('colors.multiple')
          sel.options[1].selected = true;

          if ("createEvent" in document) {
              var evt = document.createEvent("HTMLEvents");
              evt.initEvent("change", false, true);
              sel.dispatchEvent(evt);
          }
          else {
              sel.fireEvent("onchange");
          }
        })
        //.click('//*[@id="colors.multiple"]/option[2]')
        .getText('//*[@id="colors"]', function(result) {
          elementValue = result.value;
          //console.log(result)
        })
        .perform(function() {
          console.log('elementValue', elementValue);
        })
        // .source(function (result){
        //     // Source will be stored in result.value
        //     console.log(result.value);
        // })
        .waitForElementVisible('//*[@data-parent-id="colors.multiple"]', 1000)
        .assert.containsText('//*[@data-parent-id="colors.multiple"]', 'rouge')
        .click('//*[@data-parent-id="colors.multiple"]/span')
        .assert.elementNotPresent('//*[@data-parent-id="colors.multiple"]')
    });

    it('autocomplete: Check input autocomplete fields', function(client) {
      client
        .useXpath()
        .url('http://localhost:3003/abe/editor/autocomplete.html')
        .setValue('//*[@id="colors.colors_autocomplete"]', 'rouge')
        .waitForElementVisible('//*[@id="colors"]/div/div/div/div[3]/div/div[2]/div', 1000)
        .click('//*[@id="colors"]/div/div/div/div[3]/div/div[2]/div')
        .waitForElementVisible('//*[@data-parent-id="colors.colors_autocomplete"]', 1000)
        .assert.containsText('//*[@data-parent-id="colors.colors_autocomplete"]', 'rouge')
        .click('//*[@data-parent-id="colors.colors_autocomplete"]/span')
        .assert.elementNotPresent('//*[@data-parent-id="colors.colors_autocomplete"]')
    });

    // it('autocomplete: Abe type data reference json', function(client) {
    //   client
    //     .useXpath()
    //     .url('http://localhost:3003/abe/editor/autocomplete.html')
    //     .waitForElementVisible('//body')
    //     .pause(1000)
    //     .click('//*[@id="abeForm"]/ul/li[2]/a')
    //     .waitForElementVisible('//*[@id="reference"]/div')
    //     .click('//*[@id="reference.single"]/option[2]')
    //     .click('//*[@id="reference.multiple"]/option[3]')
    //     .setValue('//*[@id="reference.autocomplete"]', 'rouge')
    //     .waitForElementVisible('//*[@id="reference"]/div/div/div/div[3]/div[2]', 2000)
    //     .click('//*[@id="reference"]/div/div/div/div[3]/div[2]/div[1]')
    //     .waitForElementVisible('//*[@id="reference"]/div/div/div/div[3]/div/div', 2000);
    // });
    //

    it('autocomplete: The autocomplete article is deleted in the manager', function(client) {
      client
        .useXpath()
        .url('http://localhost:3003/abe/editor')
        .waitForElementVisible('//body')
        .pause(1000)
        .click("//table[@id='navigation-list']/tbody/tr[1]/td[7]/div/a")
        .pause(1000)
        .acceptAlert()
        .url('http://localhost:3003/abe/editor')
        .pause(2000)
        .expect.element("//table[@id='navigation-list']/tbody/tr[1]/td[2]/a").text.to.not.contain('/articles/ftest.html');
    });
  });
});<|endoftext|>


<commit_before>// https://eslint.org/docs/user-guide/configuring

module.exports = {
  root: true,
  parser: 'babel-eslint',
  parserOptions: {
    sourceType: 'module'
  },
  env: {
    browser: true,
  },
  {{#if_eq lintConfig "standard"}}
  // https://github.com/standard/standard/blob/master/docs/RULES-en.md
  extends: 'standard',
  {{/if_eq}}
  {{#if_eq lintConfig "airbnb"}}
  extends: 'airbnb-base',
  {{/if_eq}}
  // required to lint *.vue files
  plugins: [
    'html'
  ],
  {{#if_eq lintConfig "airbnb"}}
  // check if imports actually resolve
  'settings': {
    'import/resolver': {
      'webpack': {
        'config': 'build/webpack.base.conf.js'
      }
    }
  },
  {{/if_eq}}
  // add your custom rules here
  'rules': {
    {{#if_eq lintConfig "standard"}}
    // allow paren-less arrow functions
    'arrow-parens': 0,
    // allow async-await
    'generator-star-spacing': 0,
    {{/if_eq}}
    {{#if_eq lintConfig "airbnb"}}
    // don't require .vue extension when importing
    'import/extensions': ['error', 'always', {
      'js': 'never',
      'vue': 'never'
    }],
    // allow optionalDependencies
    'import/no-extraneous-dependencies': ['error', {
      'optionalDependencies': ['test/unit/index.js']
    }],
    {{/if_eq}}
    // allow debugger during development
    'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0
  }
}
<commit_msg>Turn "prefer-template" off<commit_after>// https://eslint.org/docs/user-guide/configuring

module.exports = {
  root: true,
  parser: 'babel-eslint',
  parserOptions: {
    sourceType: 'module'
  },
  env: {
    browser: true,
  },
  {{#if_eq lintConfig "standard"}}
  // https://github.com/standard/standard/blob/master/docs/RULES-en.md
  extends: 'standard',
  {{/if_eq}}
  {{#if_eq lintConfig "airbnb"}}
  extends: 'airbnb-base',
  {{/if_eq}}
  // required to lint *.vue files
  plugins: [
    'html'
  ],
  {{#if_eq lintConfig "airbnb"}}
  // check if imports actually resolve
  'settings': {
    'import/resolver': {
      'webpack': {
        'config': 'build/webpack.base.conf.js'
      }
    }
  },
  {{/if_eq}}
  // add your custom rules here
  'rules': {
    {{#if_eq lintConfig "standard"}}
    // allow paren-less arrow functions
    'arrow-parens': 0,
    // allow async-await
    'generator-star-spacing': 0,
    {{/if_eq}}
    {{#if_eq lintConfig "airbnb"}}
    // don't require .vue extension when importing
    'import/extensions': ['error', 'always', {
      'js': 'never',
      'vue': 'never'
    }],
    // allow optionalDependencies
    'import/no-extraneous-dependencies': ['error', {
      'optionalDependencies': ['test/unit/index.js']
    }],
    {{/if_eq}}
    // allow debugger during development
    'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
    'prefer-template': 'off'
  }
}
<|endoftext|>


<commit_before>// Copyright 2015-2020, University of Colorado Boulder

/**
 * Abstract type for handling color profiles in simulations. Handles multiple color profiles (by string name),
 * and controls color Property values based on the active color profile.
 *
 * It also contains hooks that allow synchronizing the colors over iframe communication, so that colors can be
 * controlled by a color picker.
 *
 * ColorProfile will take an object map ({string} color name => {Object} profile map) and will create a
 * {Property.<Color>} for each, that will change based on the color profile's profileNameProperty's current value.
 *
 * For example:
 *
 * var profile = new ColorProfile( [ 'default', 'projector' ], {
 *   fishFill: {
 *     default: new Color( 0, 0, 0 ),
 *     projector: 'white'
 *   },
 *   fishStroke: {
 *     default: '#0f0'
 *     projector: Color.BLUE
 *   }
 * } );
 *
 * creates a ColorProfile object that now contains the three Properties:
 * {
 *   profileNameProperty: {Property.<string>} - initially 'default',
 *   fishFillProperty: {Property.<Color>} - initially the new Color( 0, 0, 0 ),
 *   fishStrokeProperty: {Property.<Color>} - initially #0f0 converted to a Color object
 * }
 *
 * The color Properties will change whenever the profileName Property changes, so:
 *
 * profile.profileNameProperty.value = 'projector';
 *
 * will set the ColorProfile to the 'projector' profile, updating both of the color Properties to their specified
 * 'projector' colors (converted to Scenery Color objects as necessary).
 *
 * NOTE: It is acceptable to omit a non-default profile key for colors, e.g. just { default: ... }. If a profile key
 * is not present for the color, then the default will be used.
 *
 * NOTE: It is ideal to pass color Properties directly to Scenery object fill/strokes, e.g.:
 *
 *   new Path( ..., { fill: profile.fishFillProperty } );
 *
 * NOTE: Generally a require.js module should be responsible for returning a singleton instance of ColorProfile for
 * a simulation, e.g. GravityAndOrbitsColorProfile.
 *
 * @author Jonathan Olson <jonathan.olson@colorado.edu>
 * @author Aaron Davis
 */

import Property from '../../axon/js/Property.js';
import StringProperty from '../../axon/js/StringProperty.js';
import merge from '../../phet-core/js/merge.js';
import Color from '../../scenery/js/util/Color.js';
import Tandem from '../../tandem/js/Tandem.js';
import sceneryPhet from './sceneryPhet.js';

class ColorProfile {

  /**
   * @param {Array.<string>} profileNames - A list of valid profile names that can be taken.
   * @param {Object} colors - See documentation above
   * @param {Object} [options]
   */
  constructor( profileNames, colors, options ) {

    options = merge( {

      // phet-io
      tandem: Tandem.REQUIRED
    }, options );

    // @public (read-only)
    this.profileNames = profileNames;

    // Query parameter may override the default profile name.
    const initialProfileName = phet.chipper.queryParameters.colorProfile || ColorProfile.DEFAULT_COLOR_PROFILE_NAME;
    if ( profileNames.indexOf( initialProfileName ) === -1 ) {
      throw new Error( `invalid colorProfile: ${initialProfileName}` );
    }

    // @public {Property.<string>}
    // The current profile name. Change this Property's value to change which profile is currently active.
    this.profileNameProperty = new StringProperty( initialProfileName, {
      tandem: options.tandem.createTandem( 'profileNameProperty' ),
      validValues: profileNames
    } );

    Object.keys( colors ).sort().forEach( key => {
      if ( colors.hasOwnProperty( key ) ) {

        // Turn strings/hex to Color objects
        const colorMap = _.mapValues( colors[ key ], Color.toColor );

        assert && assert( colorMap.hasOwnProperty( ColorProfile.DEFAULT_COLOR_PROFILE_NAME ),
          `missing default color for key=${key}` );
        assert && assert( key !== 'profileName',
          'Unlikely, but would have hilarious consequences since we would overwrite profileNameProperty' );

        // Use the requested initial profile, fallback to default.
        const initialColor = colorMap[ initialProfileName ] || colorMap[ ColorProfile.DEFAULT_COLOR_PROFILE_NAME ];

        // Create a Property for the color
        const colorProperty = new Property( initialColor );
        this[ key + 'Property' ] = colorProperty;

        // Update the Property on profile name changes
        this.profileNameProperty.lazyLink( profileName => {
          colorProperty.value = colorMap[ profileName ] || colorMap[ ColorProfile.DEFAULT_COLOR_PROFILE_NAME ];
        } );

        // Communicate color changes to the iframe
        colorProperty.link( color => this.reportColor( key ) );
      }
    } );

    // receives iframe communication to set a color
    window.addEventListener( 'message', event => {
      let data;
      try {
        data = JSON.parse( event.data );
      }
      catch( e ) {
        // We don't do anything with the caught value. If this happens, it is not JSON. This can happen with the
        // LoL wrappers, see https://github.com/phetsims/joist/issues/484.
      }

      if ( data && data.type === 'setColor' ) {
        this[ data.name + 'Property' ].value = new Color( data.value );
      }
    } );
  }

  /**
   * Sends color change events as iframe messages, so that a container can be notified (and possibly update color pickers).
   * @param {string} key - The color name that was changed
   * @private
   */
  reportColor( key ) {
    let hexColor = this[ key + 'Property' ].value.toNumber().toString( 16 );
    while ( hexColor.length < 6 ) {
      hexColor = '0' + hexColor;
    }

    ( window.parent !== window ) && window.parent.postMessage( JSON.stringify( {
      type: 'reportColor',
      name: key,
      value: '#' + hexColor
    } ), '*' );
  }

  /**
   * Does this ColorProfile have the specified color profile?
   * @param {string} profileName
   * @returns {boolean}
   * @public
   */
  hasProfile( profileName ) {
    return ( this.profileNames.indexOf( profileName ) !== -1 );
  }
}

sceneryPhet.register( 'ColorProfile', ColorProfile );

// @public (read-only) the default profile required by all ColorProfile instances
ColorProfile.DEFAULT_COLOR_PROFILE_NAME = 'default';

// @public (read-only) a common profile that appears in sims that have a 'Projector Mode' feature
ColorProfile.PROJECTOR_COLOR_PROFILE_NAME = 'projector';

export default ColorProfile;<commit_msg>set default options.tandem in ColorProfile, https://github.com/phetsims/joist/issues/625<commit_after>// Copyright 2015-2020, University of Colorado Boulder

/**
 * Abstract type for handling color profiles in simulations. Handles multiple color profiles (by string name),
 * and controls color Property values based on the active color profile.
 *
 * It also contains hooks that allow synchronizing the colors over iframe communication, so that colors can be
 * controlled by a color picker.
 *
 * ColorProfile will take an object map ({string} color name => {Object} profile map) and will create a
 * {Property.<Color>} for each, that will change based on the color profile's profileNameProperty's current value.
 *
 * For example:
 *
 * var profile = new ColorProfile( [ 'default', 'projector' ], {
 *   fishFill: {
 *     default: new Color( 0, 0, 0 ),
 *     projector: 'white'
 *   },
 *   fishStroke: {
 *     default: '#0f0'
 *     projector: Color.BLUE
 *   }
 * } );
 *
 * creates a ColorProfile object that now contains the three Properties:
 * {
 *   profileNameProperty: {Property.<string>} - initially 'default',
 *   fishFillProperty: {Property.<Color>} - initially the new Color( 0, 0, 0 ),
 *   fishStrokeProperty: {Property.<Color>} - initially #0f0 converted to a Color object
 * }
 *
 * The color Properties will change whenever the profileName Property changes, so:
 *
 * profile.profileNameProperty.value = 'projector';
 *
 * will set the ColorProfile to the 'projector' profile, updating both of the color Properties to their specified
 * 'projector' colors (converted to Scenery Color objects as necessary).
 *
 * NOTE: It is acceptable to omit a non-default profile key for colors, e.g. just { default: ... }. If a profile key
 * is not present for the color, then the default will be used.
 *
 * NOTE: It is ideal to pass color Properties directly to Scenery object fill/strokes, e.g.:
 *
 *   new Path( ..., { fill: profile.fishFillProperty } );
 *
 * NOTE: Generally a require.js module should be responsible for returning a singleton instance of ColorProfile for
 * a simulation, e.g. GravityAndOrbitsColorProfile.
 *
 * @author Jonathan Olson <jonathan.olson@colorado.edu>
 * @author Aaron Davis
 */

import Property from '../../axon/js/Property.js';
import StringProperty from '../../axon/js/StringProperty.js';
import merge from '../../phet-core/js/merge.js';
import Color from '../../scenery/js/util/Color.js';
import Tandem from '../../tandem/js/Tandem.js';
import sceneryPhet from './sceneryPhet.js';

class ColorProfile {

  /**
   * @param {Array.<string>} profileNames - A list of valid profile names that can be taken.
   * @param {Object} colors - See documentation above
   * @param {Object} [options]
   */
  constructor( profileNames, colors, options ) {

    options = merge( {

      // phet-io
      tandem: Tandem.GLOBAL.createTandem( 'colorProfile' )
    }, options );

    // @public (read-only)
    this.profileNames = profileNames;

    // Query parameter may override the default profile name.
    const initialProfileName = phet.chipper.queryParameters.colorProfile || ColorProfile.DEFAULT_COLOR_PROFILE_NAME;
    if ( profileNames.indexOf( initialProfileName ) === -1 ) {
      throw new Error( `invalid colorProfile: ${initialProfileName}` );
    }

    // @public {Property.<string>}
    // The current profile name. Change this Property's value to change which profile is currently active.
    this.profileNameProperty = new StringProperty( initialProfileName, {
      tandem: options.tandem.createTandem( 'profileNameProperty' ),
      validValues: profileNames
    } );

    Object.keys( colors ).sort().forEach( key => {
      if ( colors.hasOwnProperty( key ) ) {

        // Turn strings/hex to Color objects
        const colorMap = _.mapValues( colors[ key ], Color.toColor );

        assert && assert( colorMap.hasOwnProperty( ColorProfile.DEFAULT_COLOR_PROFILE_NAME ),
          `missing default color for key=${key}` );
        assert && assert( key !== 'profileName',
          'Unlikely, but would have hilarious consequences since we would overwrite profileNameProperty' );

        // Use the requested initial profile, fallback to default.
        const initialColor = colorMap[ initialProfileName ] || colorMap[ ColorProfile.DEFAULT_COLOR_PROFILE_NAME ];

        // Create a Property for the color
        const colorProperty = new Property( initialColor );
        this[ key + 'Property' ] = colorProperty;

        // Update the Property on profile name changes
        this.profileNameProperty.lazyLink( profileName => {
          colorProperty.value = colorMap[ profileName ] || colorMap[ ColorProfile.DEFAULT_COLOR_PROFILE_NAME ];
        } );

        // Communicate color changes to the iframe
        colorProperty.link( color => this.reportColor( key ) );
      }
    } );

    // receives iframe communication to set a color
    window.addEventListener( 'message', event => {
      let data;
      try {
        data = JSON.parse( event.data );
      }
      catch( e ) {
        // We don't do anything with the caught value. If this happens, it is not JSON. This can happen with the
        // LoL wrappers, see https://github.com/phetsims/joist/issues/484.
      }

      if ( data && data.type === 'setColor' ) {
        this[ data.name + 'Property' ].value = new Color( data.value );
      }
    } );
  }

  /**
   * Sends color change events as iframe messages, so that a container can be notified (and possibly update color pickers).
   * @param {string} key - The color name that was changed
   * @private
   */
  reportColor( key ) {
    let hexColor = this[ key + 'Property' ].value.toNumber().toString( 16 );
    while ( hexColor.length < 6 ) {
      hexColor = '0' + hexColor;
    }

    ( window.parent !== window ) && window.parent.postMessage( JSON.stringify( {
      type: 'reportColor',
      name: key,
      value: '#' + hexColor
    } ), '*' );
  }

  /**
   * Does this ColorProfile have the specified color profile?
   * @param {string} profileName
   * @returns {boolean}
   * @public
   */
  hasProfile( profileName ) {
    return ( this.profileNames.indexOf( profileName ) !== -1 );
  }
}

sceneryPhet.register( 'ColorProfile', ColorProfile );

// @public (read-only) the default profile required by all ColorProfile instances
ColorProfile.DEFAULT_COLOR_PROFILE_NAME = 'default';

// @public (read-only) a common profile that appears in sims that have a 'Projector Mode' feature
ColorProfile.PROJECTOR_COLOR_PROFILE_NAME = 'projector';

export default ColorProfile;<|endoftext|>


<commit_before>// Copyright 2019-2020, University of Colorado Boulder

/**
 * Data structure for an item in a ComboBox.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */

import merge from '../../phet-core/js/merge.js';
import Node from '../../scenery/js/nodes/Node.js';
import sun from './sun.js';

class ComboBoxItem {

  /**
   * @param {Node} node
   * @param {*} value
   * @param {Object} [options]
   */
  constructor( node, value, options ) {

    assert && assert( node instanceof Node, 'invalid node: ' + node );
    assert && assert( !node.accessibleContent, 'accessibleContent is set by ComboBox, use options.a11yLabel' );

    options = merge( {

      // {Playable} - Sound that will be played when this item is selected.  If set to `null` a default sound will be
      // used that is based on this item's position in the combo box list.  Playable.NO_SOUND can be used to disable.
      soundPlayer: null,

      // phet-io
      tandemName: null, // {string|null} the tandem name for this item's associated Node in the combo box

      // pdom
      a11yLabel: null // {string|null} the label for this item's associated Node in the combo box

    }, options );

    assert && assert( options.tandemName === null || typeof options.tandemName === 'string',
      `invalid tandemName: ${options.tandemName}` );
    assert && assert( !options.tandemName || options.tandemName.endsWith( 'Item' ),
      'ComboBoxItem tandemName must end with \'Item\'' );

    // @public (read-only)
    this.node = node;
    this.value = value;
    this.soundPlayer = options.soundPlayer;
    this.tandemName = options.tandemName;
    this.a11yLabel = options.a11yLabel;
  }
}

sun.register( 'ComboBoxItem', ComboBoxItem );
export default ComboBoxItem;<commit_msg>add tandemName value to ComboBoxItem assertion message, https://github.com/phetsims/sun/issues/615<commit_after>// Copyright 2019-2020, University of Colorado Boulder

/**
 * Data structure for an item in a ComboBox.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */

import merge from '../../phet-core/js/merge.js';
import Node from '../../scenery/js/nodes/Node.js';
import sun from './sun.js';

class ComboBoxItem {

  /**
   * @param {Node} node
   * @param {*} value
   * @param {Object} [options]
   */
  constructor( node, value, options ) {

    assert && assert( node instanceof Node, 'invalid node: ' + node );
    assert && assert( !node.accessibleContent, 'accessibleContent is set by ComboBox, use options.a11yLabel' );

    options = merge( {

      // {Playable} - Sound that will be played when this item is selected.  If set to `null` a default sound will be
      // used that is based on this item's position in the combo box list.  Playable.NO_SOUND can be used to disable.
      soundPlayer: null,

      // phet-io
      tandemName: null, // {string|null} the tandem name for this item's associated Node in the combo box

      // pdom
      a11yLabel: null // {string|null} the label for this item's associated Node in the combo box

    }, options );

    assert && assert( options.tandemName === null || typeof options.tandemName === 'string',
      `invalid tandemName: ${options.tandemName}` );
    assert && assert( !options.tandemName || options.tandemName.endsWith( 'Item' ),
      `ComboBoxItem tandemName must end with 'Item': ${options.tandemName}` );

    // @public (read-only)
    this.node = node;
    this.value = value;
    this.soundPlayer = options.soundPlayer;
    this.tandemName = options.tandemName;
    this.a11yLabel = options.a11yLabel;
  }
}

sun.register( 'ComboBoxItem', ComboBoxItem );
export default ComboBoxItem;<|endoftext|>