<commit_before>/**
 *
 *    Copyright (c) 2020 Silicon Labs
 *
 *    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.
 */

const rendApi = require('../../src-shared/rend-api.js')
const uiUtil = require('./ui-util.js')
const env = require('../util/env.js')

/**
 * @module JS API: renderer API related utilities
 */

/**
 * This method returns the global session UUID from the browser window that is set by the front-end.
 * @param {*} browserWindow
 * @returns session UUID
 */
async function getSessionUuidFromBrowserWindow(browserWindow) {
  let sessionUuid = await browserWindow.webContents.executeJavaScript(
    'window.sessionStorage.getItem("session_uuid")'
  )
  return sessionUuid
}

/**
 * Returns descriptive text about renderer api.
 *
 * @param {*} browserWindow
 * @returns description of renderer api
 */
async function getRendererApiInformation(browserWindow) {
  const info = await browserWindow.webContents.executeJavaScript(
    'window.global_renderer_api_info'
  )
  let msg = `
Prefix: ${info.prefix}
Description: ${info.description}
Functions:`
  info.functions.forEach((fn) => {
    msg = msg.concat(
      `\n  - ${fn.id}: ${fn.description} ${
        'type' in fn ? '[' + fn.type + ']' : ''
      }`
    )
  })
  return msg
}

/**
 * Execute RendererApi commands
 * @param {*} browserWindow
 * @param {*} rendererApiCommand
 * @param  {...any} theArgs
 */

async function execRendererApi(browserWindow, rendererApiCommand, ...theArgs) {
  const info = await browserWindow.webContents.executeJavaScript(
    'window.global_renderer_api_info'
  )

  let apiFound = info.functions.filter((x) => x.id === rendererApiCommand)
  if (!apiFound.length) {
    env.logBrowser(
      `Unhandled renderer API function id invoked: ${rendererApiCommand}`
    )
    return Promise.resolve()
  } else {
    // call javascript
    return await browserWindow.webContents.executeJavaScript(
      `window.global_renderer_api_execute('${rendererApiCommand}', "${theArgs}")`
    )
  }
}

/**
 * This method takes a message and checks if it's a renderer API
 * notification call. If it is, it processe it and returns true.
 * If it's not it returns false.
 *
 * @param {*} message
 * @returns true if message was a notify message and was consumed.
 */
function processRendererNotify(browserWindow, message) {
  if (message.startsWith(rendApi.jsonPrefix)) {
    let obj = JSON.parse(message.slice(rendApi.jsonPrefix.length))
    switch (obj.key) {
      case rendApi.notifyKey.dirtyFlag:
        uiUtil.toggleDirtyFlag(browserWindow, obj.value)
        return true
      case rendApi.notifyKey.fileBrowse:
        uiUtil.openFileDialogAndReportResult(browserWindow, obj.value)
        return true
      default:
        env.logBrowser(`Unhandled renderer API key: ${obj.key}`)
        return false
    }
  }
  return false
}

/**
 * This method calls the reportFiles renderer API call.
 *
 * @param {*} browserWindow
 * @param {*} result
 */
async function reportFiles(browserWindow, result) {
  let resultJson = JSON.stringify(result)
  await browserWindow.webContents.executeJavaScript(
    `window.global_renderer_api_execute('${rendApi.id.reportFiles}', '${resultJson}')`
  )
}

/**
 * Returns cookie for user identification.
 *
 * @param {*} cookieValue
 * @returns cookie value used for user identification
 */
function getUserKeyFromCookieValue(cookieValue) {
  let ret = cookieValue
  if (ret == null) return null
  if (ret.startsWith('connect.sid=')) ret = ret.substring(12)
  if (ret.startsWith('s%3A')) ret = ret.substring(4)
  if (ret.includes('.')) ret = ret.split('.')[0]
  return ret
}

/**
 * Returns the session key
 * @param {*} browserCookie object
 */
function getUserKeyFromBrowserCookie(browserCookie) {
  let sid = browserCookie['connect.sid']
  if (sid) {
    return getUserKeyFromCookieValue(sid)
  } else {
    return null
  }
}

/**
 * Returns a promise that resolves into the session key.
 * @param {*} browserWindow
 */
function getUserKeyFromBrowserWindow(browserWindow) {
  return browserWindow.webContents.session.cookies
    .get({ name: 'connect.sid' })
    .then((cookies) => {
      if (cookies.length == 0)
        throw new Error('Could not find session key in a browser window')
      else return getUserKeyFromCookieValue(cookies[0].value)
    })
}

exports.getSessionUuidFromBrowserWindow = getSessionUuidFromBrowserWindow
exports.getRendererApiInformation = getRendererApiInformation
exports.getUserKeyFromBrowserWindow = getUserKeyFromBrowserWindow
exports.getUserKeyFromBrowserCookie = getUserKeyFromBrowserCookie
exports.getUserKeyFromCookieValue = getUserKeyFromCookieValue
exports.reportFiles = reportFiles
exports.processRendererNotify = processRendererNotify
exports.execRendererApi = execRendererApi
<commit_msg>SQ warnings<commit_after>/**
 *
 *    Copyright (c) 2020 Silicon Labs
 *
 *    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.
 */

const rendApi = require('../../src-shared/rend-api.js')
const uiUtil = require('./ui-util.js')
const env = require('../util/env.js')

/**
 * @module JS API: renderer API related utilities
 */

/**
 * This method returns the global session UUID from the browser window that is set by the front-end.
 * @param {*} browserWindow
 * @returns session UUID
 */
async function getSessionUuidFromBrowserWindow(browserWindow) {
  let sessionUuid = await browserWindow.webContents.executeJavaScript(
    'window.sessionStorage.getItem("session_uuid")'
  )
  return sessionUuid
}

/**
 * Returns descriptive text about renderer api.
 *
 * @param {*} browserWindow
 * @returns description of renderer api
 */
async function getRendererApiInformation(browserWindow) {
  const info = await browserWindow.webContents.executeJavaScript(
    'window.global_renderer_api_info'
  )
  let msg = `
Prefix: ${info.prefix}
Description: ${info.description}
Functions:`
  info.functions.forEach((fn) => {
    msg = msg.concat(
      `\n  - ${fn.id}: ${fn.description} ${
        'type' in fn ? '[' + fn.type + ']' : ''
      }`
    )
  })
  return msg
}

/**
 * Execute RendererApi commands
 * @param {*} browserWindow
 * @param {*} rendererApiCommand
 * @param  {...any} theArgs
 */

async function execRendererApi(browserWindow, rendererApiCommand, ...theArgs) {
  const info = await browserWindow.webContents.executeJavaScript(
    'window.global_renderer_api_info'
  )

  let apiFound = info.functions.filter((x) => x.id === rendererApiCommand)
  if (!apiFound.length) {
    env.logBrowser(
      `Unhandled renderer API function id invoked: ${rendererApiCommand}`
    )
    return Promise.resolve()
  } else {
    // call javascript
    await browserWindow.webContents.executeJavaScript(
      `window.global_renderer_api_execute('${rendererApiCommand}', "${theArgs}")`
    )
  }
}

/**
 * This method takes a message and checks if it's a renderer API
 * notification call. If it is, it processe it and returns true.
 * If it's not it returns false.
 *
 * @param {*} message
 * @returns true if message was a notify message and was consumed.
 */
function processRendererNotify(browserWindow, message) {
  if (message.startsWith(rendApi.jsonPrefix)) {
    let obj = JSON.parse(message.slice(rendApi.jsonPrefix.length))
    switch (obj.key) {
      case rendApi.notifyKey.dirtyFlag:
        uiUtil.toggleDirtyFlag(browserWindow, obj.value)
        return true
      case rendApi.notifyKey.fileBrowse:
        uiUtil.openFileDialogAndReportResult(browserWindow, obj.value)
        return true
      default:
        env.logBrowser(`Unhandled renderer API key: ${obj.key}`)
        return false
    }
  }
  return false
}

/**
 * This method calls the reportFiles renderer API call.
 *
 * @param {*} browserWindow
 * @param {*} result
 */
async function reportFiles(browserWindow, result) {
  let resultJson = JSON.stringify(result)
  await browserWindow.webContents.executeJavaScript(
    `window.global_renderer_api_execute('${rendApi.id.reportFiles}', '${resultJson}')`
  )
}

/**
 * Returns cookie for user identification.
 *
 * @param {*} cookieValue
 * @returns cookie value used for user identification
 */
function getUserKeyFromCookieValue(cookieValue) {
  let ret = cookieValue
  if (ret == null) return null
  if (ret.startsWith('connect.sid=')) ret = ret.substring(12)
  if (ret.startsWith('s%3A')) ret = ret.substring(4)
  if (ret.includes('.')) ret = ret.split('.')[0]
  return ret
}

/**
 * Returns the session key
 * @param {*} browserCookie object
 */
function getUserKeyFromBrowserCookie(browserCookie) {
  let sid = browserCookie['connect.sid']
  if (sid) {
    return getUserKeyFromCookieValue(sid)
  } else {
    return null
  }
}

/**
 * Returns a promise that resolves into the session key.
 * @param {*} browserWindow
 */
function getUserKeyFromBrowserWindow(browserWindow) {
  return browserWindow.webContents.session.cookies
    .get({ name: 'connect.sid' })
    .then((cookies) => {
      if (cookies.length == 0)
        throw new Error('Could not find session key in a browser window')
      else return getUserKeyFromCookieValue(cookies[0].value)
    })
}

exports.getSessionUuidFromBrowserWindow = getSessionUuidFromBrowserWindow
exports.getRendererApiInformation = getRendererApiInformation
exports.getUserKeyFromBrowserWindow = getUserKeyFromBrowserWindow
exports.getUserKeyFromBrowserCookie = getUserKeyFromBrowserCookie
exports.getUserKeyFromCookieValue = getUserKeyFromCookieValue
exports.reportFiles = reportFiles
exports.processRendererNotify = processRendererNotify
exports.execRendererApi = execRendererApi
<|endoftext|>


<commit_before>/**
 * CanvasLayer is a canvas place on top of the basic canvas to manage edition and to draw elements such as cursors (to optimize rendering)
 * @param {LSViewer} viewer
 */
define(['jquery', 'pubsub'], function($, pubsub) {
	function CanvasLayer(viewer) {
		if (!viewer.canvas) {
			throw "LSViewer cannot create layer because canvas does not exist";
		}

		this.viewer = viewer;
		var canvasLayer = this._createLayer(viewer);
		this.canvasLayer = canvasLayer[0];
		this.ctx = canvasLayer[0].getContext('2d');
		this.color = "rgba(0,0,255,1)";
		this.mouseCoordsIni = null;
		this.mouseCoordsEnd = null;
		this.coords = {};
		this.mouseDown = false;
		this._listenEvents(canvasLayer);
		this.elems = {};	//elements to be added (can be CLICKABLE or CURSOR)
		this.order = [];    //we keep trace of order in which elements are added, to decide which should be prioritized on click
	}

	CanvasLayer.prototype._createLayer = function(viewer) {
		var canvasEl = $(viewer.canvas),
			idCanvas = canvasEl.attr('id'),
			idLayer = idCanvas + "-layer",
			offset = canvasEl.offset(),
			//top depends on the position (absolute, static..) of the canvas container element
			top = (canvasEl.parent().css("position") == "absolute") ? 0 : offset.top;

		var layersProps = {
			position: "absolute",
			left: offset.left,
			top: top
		};
		var canvasLayer;
		//we remove it, to create a new one (_createLayer it's called only at the beginning or when resizing)
		if ($("canvas#" + idLayer).length !== 0) {
			$("canvas#" + idLayer).remove();
		}
		$("<canvas id='" + idLayer + "' width='" + canvasEl.width() + "' height='" + canvasEl.height() + "'></canvas>").insertAfter(canvasEl);
		canvasLayer = $("#" + idLayer);
		canvasLayer.css(layersProps);
		canvasLayer.css('z-index', 10);
		return canvasLayer;
	};

	CanvasLayer.prototype._getXandY = function(element, event) {
		return {
			x: event.pageX - element.offset().left,
			y: event.pageY - element.offset().top
		};
	};

	/**
	 * Element should have properties:
	 *		CL_NAME
	 *		CL_TYPE
	 *	methods:
	 *		getType
	 *		iEnabled
	 *		enable
	 *		disable
	 *		onSelected
	 *		inPath
	 *
	 * CLICKABLE elements will be enabled always, but 'disable' function is useful for example to simulate event 'onBlur' when unfocusing element
	 *
	 * CL_TYPE can be 'CURSOR' or 'CLICKABLE'
	 * if it's CURSOR, it needs to have also this methods:
	 *		getYs
     *		name
     *		drawCursor
     *		setCursorEditable
  	 */
	CanvasLayer.prototype.addElement = function(elem) {
		if (!elem || !elem.CL_NAME || !elem.getType()) {
			throw 'CanvasLayer element needs CL_NAME and CL_TYPE property';
		}

		if (!(elem.CL_NAME in this.elems)){
			this.elems[elem.CL_NAME] = elem;
			this.order.push(elem.CL_NAME);	//order is useful to control z-index of drawn elements, last drawn elements will be prioritized on click. (see getOneActiveElement())
		}

	};

	CanvasLayer.prototype._listenEvents = function() {
		var self = this,
			xy,
			coords;
		this.mouseDown = false;
		/**
		 * when selecting we need to know which kind of elements (being normally notes, chords or audio), are at the top and at the bottom of the selection.
		 * We will decide by that, which elements will be editable. So far, we take maximum two.
		 * E.g.1 if notes at top and notes at bottom of selection, we only enable edition for notes (even if there are chords in the middle)
		 * E.g.2 if notes at top and chords at bottom of selection, we enable edition on notes and chords at the same time.
		 * @param  {Object} coords e.g.:  {x:12, y:21}
		 *
		 * @return {Array}        array of active elements (being elements like ChordSpaceManager, NoteSpaceManager, WaveDrawer. TextElementManager will never be returned because it is not selectable (it does not have getY() function), it is only thought for being clicked)
		 */
		function getElemsByYs(coords) {
				var minY = 999999,
					maxY = 0,
					minName, maxName, ys;
				var activeElems = [];
				for (var name in self.elems) {
					if (self.elems[name].getType() === 'CURSOR') {

						ys = self.elems[name].getYs(coords);
						if (ys.topY < minY) {
							minY = ys.topY;
							minName = name;
						}
						if (ys.bottomY > maxY) {
							maxY = ys.bottomY;
							maxName = name;
						}
					}
				}
				if (minName) {
					activeElems.push(self.elems[minName]);
				}
				if (maxName && minName != maxName) {
					activeElems.push(self.elems[maxName]);
				}
				return activeElems;
			}
			/**
			 * when clicking on an element we will select one only element, this function chooses which one depending on coords
			 * @param  {Object} coords  e.g.:  {x:12, y:21}
			 * @return {Object}        class of active element (ChordSpaceManager, NoteSpaceManager, WaveDrawer. TextElementManager...etc.)
			 */
		function getOneActiveElement(coords) {
			var name;
			for (var i = self.order.length - 1; i >= 0; i--) {
				name = self.order[i];
				if (self.elems[name].inPath(coords)) {
					return [self.elems[name]];
				}
			}
		}

		function resetElems() {
			for (var name in self.elems) {
				if(self.elems[name].getType() == 'CURSOR'){
					self.elems[name].setCursorEditable(false);
				}
				self.elems[name].disable();

			}
		}

		/**
		 * [selection description]
		 * @param  {Boolean} clicked true when clicked (mouseDown and mouseUp in same position) false when moved mouse onMouseDown
		 */
		function selection(clicked, mouseUp) {
			var cursorPos;
			resetElems();
			var activElems;
			if (clicked) {
				activElems = getOneActiveElement(self.coords);
			} else {
				activElems = getElemsByYs(self.coords);
			}

			for (var i in activElems) {
				activElems[i].onSelected(self.coords, clicked, mouseUp);
				if ( activElems[i].getType() == 'CURSOR'){
					activElems[i].setCursorEditable(true);
				}
				activElems[i].enable();
			}
			self.refresh();
		}

		/**
		 * cursor set to pointer to indicate when an element is clickable. Before it was managed by each element edition class (notes,chords...etc.) but we moved it here
		 * because it has to be centralized otherwise events were interfering to each other (notes pointer when mouse is over notes would not work if at the same time chords editor asks to set pointer to default when mouse is not over chords)
		 * @param {Object} xy  e.g.:  {x:12, y:21}
		 */
		function setPointerIfInPath(xy) {
			if (typeof self.viewer.divContainer.style !== 'undefined') {
				var found = false;
				for (var name in self.elems) {
					if (typeof self.elems[name].inPath !== 'function') {
						continue;
					}
					if (self.elems[name].inPath(xy)) {
						self.viewer.divContainer.style.cursor = 'pointer';
						found = true;
					}
				}
				if (!found) {
					self.viewer.divContainer.style.cursor = 'default';
				}
			}
		}
		$(this.canvasLayer).mousedown(function(evt) {
			coords = self._getXandY($(this), evt);
			self.mouseCoordsIni = [coords.x, coords.y];
			self._setCoords(self.mouseCoordsIni, self.mouseCoordsIni);
			self.mouseDown = true;
		});

		//we prevent default mouse right-click
		document.oncontextmenu = function() {
			return false;
		};
		// Mouseup on canvas is usefull to allow unselect
		$(this.canvasLayer).mouseup(function(evt) {
			self.mouseDown = false;
			var isClick = self.mouseDidntMove();
			if (isClick && evt.button == 2){
				$.publish('right-click');
			}else{
				selection(isClick, true);
			}
		});

		// Mouseup on the whole page allows user to go out of the canvas when he selects (it only work in case a mousemove happened)
		$('html').mouseup(function(evt) {
			if (self.mouseDown === true) {
				self.mouseDown = false;
				selection(self.mouseDidntMove(), true);
			}
		});

		$('html').mousemove(function(evt) {
			//draw cursor selection
			var xy = self._getXandY($(self.canvasLayer), evt);
			if (self.mouseDown) {
				var ctx = self.ctx;
				self.mouseCoordsEnd = [xy.x, xy.y];
				self._setCoords(self.mouseCoordsIni, self.mouseCoordsEnd);
				self._clampCoords($(self.canvasLayer));
				selection();
			}
			setPointerIfInPath(xy);
		});

		$.subscribe('CanvasLayer-refresh', function(el) {
			self.refresh();
		});
	};

	/**
	 * true if position on mouseDown is the same (or almost) as position on mouseUp
	 * @return {Booelan}
	 */
	CanvasLayer.prototype.mouseDidntMove = function() {
		return (Math.abs(this.coords.x - this.coords.xe) < 5 && Math.abs(this.coords.y - this.coords.ye) < 5);
	};

	CanvasLayer.prototype._clampCoords = function(element) {
		xeElement = element.width();
		yeElement = element.height();
		if (this.coords.x < 0) {
			this.coords.x = 1;
		}
		if (this.coords.y < 0) {
			this.coords.y = 1;
		}
		if (this.coords.xe > xeElement) {
			this.coords.xe = Math.floor(xeElement) - 1;
		}
		if (this.coords.ye > yeElement) {
			this.coords.ye = Math.floor(yeElement) - 1;
		}
	};

	CanvasLayer.prototype._setCoords = function(mouseCoordsIni, mouseCoordsEnd) {

		function get(xory, type) {
			var evaluation;
			var num = (xory == "x") ? 0 : 1;
			if (type == "smaller") evaluation = (mouseCoordsIni[num] < mouseCoordsEnd[num]);
			else if (type == "greater") evaluation = (mouseCoordsIni[num] > mouseCoordsEnd[num]);
			return evaluation ? mouseCoordsIni[num] : mouseCoordsEnd[num];
		}
		this.coords.x = get("x", "smaller");
		this.coords.y = get("y", "smaller");
		this.coords.xe = get("x", "greater");
		this.coords.ye = get("y", "greater");

	};

	CanvasLayer.prototype.getCanvas = function() {
		return this.canvasLayer;
	};

	/**
	 * Refresh canvas layer: all elements in canvas layer should be elements cursors or elements that change fast
	 */
	CanvasLayer.prototype.refresh = function() {
		//console.log('refresh');
		this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
		this.viewer.scale(this.ctx);
		// console.log(name1+","+name2);
		// console.log(this.elems);
		var elem;
		for (var name in this.elems) {
			elem = this.elems[name];
			if (elem.isEnabled() && elem.getType() == 'CURSOR') {
				//drawing cursor for notesManager, chordsManager and WaveManager (selection cursor)
				elem.drawCursor(this.ctx);
			}
			//TODO refactor, drawCursor only exists in WaveManager to draw playing cursor
			if (typeof elem.drawPlayingCursor === 'function') {
				elem.drawPlayingCursor(this.ctx);
			}
		}
		this.viewer.resetScale(this.ctx);
		if (this.mouseDown) {
			var style = this.ctx.strokeStyle;
			this.ctx.strokeStyle = this.color;
			this.ctx.strokeRect(
				this.coords.x,
				this.coords.y,
				this.coords.xe - this.coords.x,
				this.coords.ye - this.coords.y
			);
			this.ctx.strokeStyle = style;
		}

	};
	return CanvasLayer;
});<commit_msg>fixed bug audio comments: when new audio comment was created, it was not updating correctly<commit_after>/**
 * CanvasLayer is a canvas place on top of the basic canvas to manage edition and to draw elements such as cursors (to optimize rendering)
 * @param {LSViewer} viewer
 */
define(['jquery', 'pubsub'], function($, pubsub) {
	function CanvasLayer(viewer) {
		if (!viewer.canvas) {
			throw "LSViewer cannot create layer because canvas does not exist";
		}

		this.viewer = viewer;
		var canvasLayer = this._createLayer(viewer);
		this.canvasLayer = canvasLayer[0];
		this.ctx = canvasLayer[0].getContext('2d');
		this.color = "rgba(0,0,255,1)";
		this.mouseCoordsIni = null;
		this.mouseCoordsEnd = null;
		this.coords = {};
		this.mouseDown = false;
		this._listenEvents(canvasLayer);
		this.elems = {};	//elements to be added (can be CLICKABLE or CURSOR)
		this.order = [];    //we keep trace of order in which elements are added, to decide which should be prioritized on click
	}

	CanvasLayer.prototype._createLayer = function(viewer) {
		var canvasEl = $(viewer.canvas),
			idCanvas = canvasEl.attr('id'),
			idLayer = idCanvas + "-layer",
			offset = canvasEl.offset(),
			//top depends on the position (absolute, static..) of the canvas container element
			top = (canvasEl.parent().css("position") == "absolute") ? 0 : offset.top;

		var layersProps = {
			position: "absolute",
			left: offset.left,
			top: top
		};
		var canvasLayer;
		//we remove it, to create a new one (_createLayer it's called only at the beginning or when resizing)
		if ($("canvas#" + idLayer).length !== 0) {
			$("canvas#" + idLayer).remove();
		}
		$("<canvas id='" + idLayer + "' width='" + canvasEl.width() + "' height='" + canvasEl.height() + "'></canvas>").insertAfter(canvasEl);
		canvasLayer = $("#" + idLayer);
		canvasLayer.css(layersProps);
		canvasLayer.css('z-index', 10);
		return canvasLayer;
	};

	CanvasLayer.prototype._getXandY = function(element, event) {
		return {
			x: event.pageX - element.offset().left,
			y: event.pageY - element.offset().top
		};
	};

	/**
	 * Element should have properties:
	 *		CL_NAME
	 *		CL_TYPE
	 *	methods:
	 *		getType
	 *		iEnabled
	 *		enable
	 *		disable
	 *		onSelected
	 *		inPath
	 *
	 * CLICKABLE elements will be enabled always, but 'disable' function is useful for example to simulate event 'onBlur' when unfocusing element
	 *
	 * CL_TYPE can be 'CURSOR' or 'CLICKABLE'
	 * if it's CURSOR, it needs to have also this methods:
	 *		getYs
     *		name
     *		drawCursor
     *		setCursorEditable
  	 */
	CanvasLayer.prototype.addElement = function(elem) {
		if (!elem || !elem.CL_NAME || !elem.getType()) {
			throw 'CanvasLayer element needs CL_NAME and CL_TYPE property';
		}
		// if it's new, we save its order
		if (!(elem.CL_NAME in this.elems)){
			this.order.push(elem.CL_NAME);	//order is useful to control z-index of drawn elements, last drawn elements will be prioritized on click. (see getOneActiveElement())
		}
		//this will update it if not new, and will create the value in the object if it is new. We were having problems when entering it only when it was not new, because
		//audioComments where not updated
		this.elems[elem.CL_NAME] = elem;

	};

	CanvasLayer.prototype._listenEvents = function() {
		var self = this,
			xy,
			coords;
		this.mouseDown = false;
		/**
		 * when selecting we need to know which kind of elements (being normally notes, chords or audio), are at the top and at the bottom of the selection.
		 * We will decide by that, which elements will be editable. So far, we take maximum two.
		 * E.g.1 if notes at top and notes at bottom of selection, we only enable edition for notes (even if there are chords in the middle)
		 * E.g.2 if notes at top and chords at bottom of selection, we enable edition on notes and chords at the same time.
		 * @param  {Object} coords e.g.:  {x:12, y:21}
		 *
		 * @return {Array}        array of active elements (being elements like ChordSpaceManager, NoteSpaceManager, WaveDrawer. TextElementManager will never be returned because it is not selectable (it does not have getY() function), it is only thought for being clicked)
		 */
		function getElemsByYs(coords) {
				var minY = 999999,
					maxY = 0,
					minName, maxName, ys;
				var activeElems = [];
				for (var name in self.elems) {
					if (self.elems[name].getType() === 'CURSOR') {

						ys = self.elems[name].getYs(coords);
						if (ys.topY < minY) {
							minY = ys.topY;
							minName = name;
						}
						if (ys.bottomY > maxY) {
							maxY = ys.bottomY;
							maxName = name;
						}
					}
				}
				if (minName) {
					activeElems.push(self.elems[minName]);
				}
				if (maxName && minName != maxName) {
					activeElems.push(self.elems[maxName]);
				}
				return activeElems;
			}
			/**
			 * when clicking on an element we will select one only element, this function chooses which one depending on coords
			 * @param  {Object} coords  e.g.:  {x:12, y:21}
			 * @return {Object}        class of active element (ChordSpaceManager, NoteSpaceManager, WaveDrawer. TextElementManager...etc.)
			 */
		function getOneActiveElement(coords) {
			var name;
			for (var i = self.order.length - 1; i >= 0; i--) {
				name = self.order[i];
				if (self.elems[name].inPath(coords)) {
					return [self.elems[name]];
				}
			}
		}

		function resetElems() {
			for (var name in self.elems) {
				if(self.elems[name].getType() == 'CURSOR'){
					self.elems[name].setCursorEditable(false);
				}
				self.elems[name].disable();

			}
		}

		/**
		 * [selection description]
		 * @param  {Boolean} clicked true when clicked (mouseDown and mouseUp in same position) false when moved mouse onMouseDown
		 */
		function selection(clicked, mouseUp) {
			var cursorPos;
			resetElems();
			var activElems;
			if (clicked) {
				activElems = getOneActiveElement(self.coords);
			} else {
				activElems = getElemsByYs(self.coords);
			}

			for (var i in activElems) {
				activElems[i].onSelected(self.coords, clicked, mouseUp);
				if ( activElems[i].getType() == 'CURSOR'){
					activElems[i].setCursorEditable(true);
				}
				activElems[i].enable();
			}
			self.refresh();
		}

		/**
		 * cursor set to pointer to indicate when an element is clickable. Before it was managed by each element edition class (notes,chords...etc.) but we moved it here
		 * because it has to be centralized otherwise events were interfering to each other (notes pointer when mouse is over notes would not work if at the same time chords editor asks to set pointer to default when mouse is not over chords)
		 * @param {Object} xy  e.g.:  {x:12, y:21}
		 */
		function setPointerIfInPath(xy) {
			if (typeof self.viewer.divContainer.style !== 'undefined') {
				var found = false;
				for (var name in self.elems) {
					if (typeof self.elems[name].inPath !== 'function') {
						continue;
					}
					if (self.elems[name].inPath(xy)) {
						self.viewer.divContainer.style.cursor = 'pointer';
						found = true;
					}
				}
				if (!found) {
					self.viewer.divContainer.style.cursor = 'default';
				}
			}
		}
		$(this.canvasLayer).mousedown(function(evt) {
			coords = self._getXandY($(this), evt);
			self.mouseCoordsIni = [coords.x, coords.y];
			self._setCoords(self.mouseCoordsIni, self.mouseCoordsIni);
			self.mouseDown = true;
		});

		//we prevent default mouse right-click
		document.oncontextmenu = function() {
			return false;
		};
		// Mouseup on canvas is usefull to allow unselect
		$(this.canvasLayer).mouseup(function(evt) {
			self.mouseDown = false;
			var isClick = self.mouseDidntMove();
			if (isClick && evt.button == 2){
				$.publish('right-click');
			}else{
				selection(isClick, true);
			}
		});

		// Mouseup on the whole page allows user to go out of the canvas when he selects (it only work in case a mousemove happened)
		$('html').mouseup(function(evt) {
			if (self.mouseDown === true) {
				self.mouseDown = false;
				selection(self.mouseDidntMove(), true);
			}
		});

		$('html').mousemove(function(evt) {
			//draw cursor selection
			var xy = self._getXandY($(self.canvasLayer), evt);
			if (self.mouseDown) {
				var ctx = self.ctx;
				self.mouseCoordsEnd = [xy.x, xy.y];
				self._setCoords(self.mouseCoordsIni, self.mouseCoordsEnd);
				self._clampCoords($(self.canvasLayer));
				selection();
			}
			setPointerIfInPath(xy);
		});

		$.subscribe('CanvasLayer-refresh', function(el) {
			self.refresh();
		});
	};

	/**
	 * true if position on mouseDown is the same (or almost) as position on mouseUp
	 * @return {Booelan}
	 */
	CanvasLayer.prototype.mouseDidntMove = function() {
		return (Math.abs(this.coords.x - this.coords.xe) < 5 && Math.abs(this.coords.y - this.coords.ye) < 5);
	};

	CanvasLayer.prototype._clampCoords = function(element) {
		xeElement = element.width();
		yeElement = element.height();
		if (this.coords.x < 0) {
			this.coords.x = 1;
		}
		if (this.coords.y < 0) {
			this.coords.y = 1;
		}
		if (this.coords.xe > xeElement) {
			this.coords.xe = Math.floor(xeElement) - 1;
		}
		if (this.coords.ye > yeElement) {
			this.coords.ye = Math.floor(yeElement) - 1;
		}
	};

	CanvasLayer.prototype._setCoords = function(mouseCoordsIni, mouseCoordsEnd) {

		function get(xory, type) {
			var evaluation;
			var num = (xory == "x") ? 0 : 1;
			if (type == "smaller") evaluation = (mouseCoordsIni[num] < mouseCoordsEnd[num]);
			else if (type == "greater") evaluation = (mouseCoordsIni[num] > mouseCoordsEnd[num]);
			return evaluation ? mouseCoordsIni[num] : mouseCoordsEnd[num];
		}
		this.coords.x = get("x", "smaller");
		this.coords.y = get("y", "smaller");
		this.coords.xe = get("x", "greater");
		this.coords.ye = get("y", "greater");

	};

	CanvasLayer.prototype.getCanvas = function() {
		return this.canvasLayer;
	};

	/**
	 * Refresh canvas layer: all elements in canvas layer should be elements cursors or elements that change fast
	 */
	CanvasLayer.prototype.refresh = function() {
		//console.log('refresh');
		this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
		this.viewer.scale(this.ctx);
		// console.log(name1+","+name2);
		// console.log(this.elems);
		var elem;
		for (var name in this.elems) {
			elem = this.elems[name];
			if (elem.isEnabled() && elem.getType() == 'CURSOR') {
				//drawing cursor for notesManager, chordsManager and WaveManager (selection cursor)
				elem.drawCursor(this.ctx);
			}
			//TODO refactor, drawCursor only exists in WaveManager to draw playing cursor
			if (typeof elem.drawPlayingCursor === 'function') {
				elem.drawPlayingCursor(this.ctx);
			}
		}
		this.viewer.resetScale(this.ctx);
		if (this.mouseDown) {
			var style = this.ctx.strokeStyle;
			this.ctx.strokeStyle = this.color;
			this.ctx.strokeRect(
				this.coords.x,
				this.coords.y,
				this.coords.xe - this.coords.x,
				this.coords.ye - this.coords.y
			);
			this.ctx.strokeStyle = style;
		}

	};
	return CanvasLayer;
});<|endoftext|>


<commit_before>'use strict';

var expect = require('expect.js'),
    isFile = require('../helpers/util/is-file'),
    fs     = require('fs')
;

module.exports = function (automaton) {
    describe('cp', function () {
        var target = __dirname + '/../tmp/cp/';

        beforeEach(function () {
            fs.mkdirSync(target, '0777');
        });

        it('should copy file to file', function (done) {
            var files = {};
            files[__dirname + '/../helpers/assets/file1.json'] = target + 'file1.json';
            files[__dirname + '/../helpers/assets/file2'] = target;

            automaton.run('cp', {
                files: files
            }, function (err) {
                if (err) {
                    throw err;
                }

                expect(isFile(target + 'file1.json')).to.be(true);
                expect(isFile(target + 'file2')).to.be(true);
                done();
            });
        });

        it.skip('should copy file to folder', function () {
            // test file to folder/
            // test file to folder where folder already exists
            // please confirm this behavior in the command line
        });

        it.skip('should copy folder to folder', function () {
            // test folder to folder/
            // test folder to folder where folder does not exist
            //  - it should copy folder to folder
            // test folder to folder where folder alredy exist
            //  - it should copy folder to folder/folder
        });

        it.skip('should copy files with single star pattern', function () {
            // test if ONLY files are copied with source/*
        });

        it.skip('should copy files and folders recursivelly', function () {
            // test if files and folders are recursivelly with with source/**/*
        });

        it.skip('should give error if source file does not exist');
        it.skip('should give error if source folder does not exist');
        it.skip('should give error if sources do not exist', function () {
            // test with source/* and source/**/*
        });

        it('should work with sources as symlinks', function (done) {
            var dir     = 'cp_dst/',
                file    = 'file.js',
                symlink = target + '../file.js',
                files   = {};

            // create file
            fs.writeFileSync(target + file, 'dummy');

            // create dir
            fs.mkdirSync(target + '../' + dir);

            // create symlink to folder in tmp
            fs.symlinkSync(target + file, symlink, 'file');

            // copy file
            files[symlink] = target + '../' + dir;

            automaton.run('cp', {
                files: files
            }, function (err) {
                if (err) {
                    throw err;
                }

                expect(isFile(target + '../' + dir + file)).to.be(true);
                done();
            });
        });

        it.skip('should work sources containing symlinks deeply inside them', function () {
            // test with a symlink inside the folder being copied
            // also test with source/**/* with a symlink deeply inside the source
            // it's important to make those separate tests because they have different piecies of code handling it
            //
            // check what is the unix behavior here.. to copy it as a symlink or follow the symlink
            // still an ISSUE should be created to add an option to the cp in order to change this behavior
        });

        it('should work with destinations as symlinks', function (done) {
            var folder  = target + 'folder/',
                file    = 'file.js',
                symlink = target + '../symlink_to_folder',
                files   = {};

            // create file
            fs.writeFileSync(target + file, 'dummy');

            // create dir
            fs.mkdirSync(folder);

            // create symlink to folder in tmp
            fs.symlinkSync(folder, symlink, 'dir');

            // copy file in /tmp/cp/file.js to /tmp/symlink/file.js
            // where symlink = cp/folder
            files[target + file] = symlink + '/';

            automaton.run('cp', {
                files: files
            }, function (err) {
                if (err) {
                    throw err;
                }

                expect(isFile(symlink + '/' + file)).to.be(true);
                fs.unlinkSync(symlink);

                // copy file in /tmp/cp/file.js to /tmp/symlink
                // where symlink = cp/folder
                files[target + file] = symlink;

                automaton.run('cp', {
                    files: files
                }, function (err) {
                    if (err) {
                        throw err;
                    }

                    expect(isFile(symlink)).to.be(true);
                    done();
                });

            });
        });

        it('should copy with default permissions', function (done) {
            var dir         = target + 'permissions/',
                folder      = 'folder/',
                file        = 'file.js',
                toCopy      = {},
                mode_dir,
                mode_file;

            // create dirs
            fs.mkdirSync(dir);
            fs.mkdirSync(target + folder);

            // get default dir mode
            mode_dir = fs.statSync(target + folder).mode;

            // change it to something else
            fs.chmodSync(target + folder, '0777');

            // create file
            fs.writeFileSync(target + file, 'dummy');

            // get file mode
            mode_file = fs.statSync(target + file).mode;

            // change it to something else
            fs.chmodSync(target + file, '0777');

            // file to copy
            toCopy[target + file]   = dir;
            toCopy[target + folder] = dir + 'folder/';

            automaton.run('cp', {
                files: toCopy
            }, function (err) {
                if (err) {
                    throw err;
                }

                expect(fs.statSync(toCopy[target + file] + file).mode).to.equal(mode_file);
                expect(fs.statSync(toCopy[target + folder]).mode).to.equal(mode_dir);
                done();
            });
        });

        it('should pass over the glob options - should not copy the file', function (done) {
            var files = {};
            files[__dirname + '/../helpers/assets/*file'] = target;

            automaton.run('cp', {
                files: files
            }, function (err) {
                if (err) {
                    throw err;
                }

                expect(isFile(target + '.file')).to.be(false);
                done();
            });
        });

        it('should pass over the glob options - should copy the file', function (done) {
            var files = {};
            files[__dirname + '/../helpers/assets/.file'] = target;

            automaton.run('cp', {
                files: files,
                glob: {
                    dot: true
                }
            }, function (err) {
                if (err) {
                    throw err;
                }
                expect(isFile(target + '.file')).to.be(true);
                done();
            });
        });
    });
};
<commit_msg>Another typo.<commit_after>'use strict';

var expect = require('expect.js'),
    isFile = require('../helpers/util/is-file'),
    fs     = require('fs')
;

module.exports = function (automaton) {
    describe('cp', function () {
        var target = __dirname + '/../tmp/cp/';

        beforeEach(function () {
            fs.mkdirSync(target, '0777');
        });

        it('should copy file to file', function (done) {
            var files = {};
            files[__dirname + '/../helpers/assets/file1.json'] = target + 'file1.json';
            files[__dirname + '/../helpers/assets/file2'] = target;

            automaton.run('cp', {
                files: files
            }, function (err) {
                if (err) {
                    throw err;
                }

                expect(isFile(target + 'file1.json')).to.be(true);
                expect(isFile(target + 'file2')).to.be(true);
                done();
            });
        });

        it.skip('should copy file to folder', function () {
            // test file to folder/
            // test file to folder where folder already exists
            // please confirm this behavior in the command line
        });

        it.skip('should copy folder to folder', function () {
            // test folder to folder/
            // test folder to folder where folder does not exist
            //  - it should copy folder to folder
            // test folder to folder where folder alredy exist
            //  - it should copy folder to folder/folder
        });

        it.skip('should copy files with single star pattern', function () {
            // test if ONLY files are copied with source/*
        });

        it.skip('should copy files and folders recursivelly', function () {
            // test if files and folders are recursivelly copied with source/**/*
        });

        it.skip('should give error if source file does not exist');
        it.skip('should give error if source folder does not exist');
        it.skip('should give error if sources do not exist', function () {
            // test with source/* and source/**/*
        });

        it('should work with sources as symlinks', function (done) {
            var dir     = 'cp_dst/',
                file    = 'file.js',
                symlink = target + '../file.js',
                files   = {};

            // create file
            fs.writeFileSync(target + file, 'dummy');

            // create dir
            fs.mkdirSync(target + '../' + dir);

            // create symlink to folder in tmp
            fs.symlinkSync(target + file, symlink, 'file');

            // copy file
            files[symlink] = target + '../' + dir;

            automaton.run('cp', {
                files: files
            }, function (err) {
                if (err) {
                    throw err;
                }

                expect(isFile(target + '../' + dir + file)).to.be(true);
                done();
            });
        });

        it.skip('should work sources containing symlinks deeply inside them', function () {
            // test with a symlink inside the folder being copied
            // also test with source/**/* with a symlink deeply inside the source
            // it's important to make those separate tests because they have different piecies of code handling it
            //
            // check what is the unix behavior here.. to copy it as a symlink or follow the symlink
            // still an ISSUE should be created to add an option to the cp in order to change this behavior
        });

        it('should work with destinations as symlinks', function (done) {
            var folder  = target + 'folder/',
                file    = 'file.js',
                symlink = target + '../symlink_to_folder',
                files   = {};

            // create file
            fs.writeFileSync(target + file, 'dummy');

            // create dir
            fs.mkdirSync(folder);

            // create symlink to folder in tmp
            fs.symlinkSync(folder, symlink, 'dir');

            // copy file in /tmp/cp/file.js to /tmp/symlink/file.js
            // where symlink = cp/folder
            files[target + file] = symlink + '/';

            automaton.run('cp', {
                files: files
            }, function (err) {
                if (err) {
                    throw err;
                }

                expect(isFile(symlink + '/' + file)).to.be(true);
                fs.unlinkSync(symlink);

                // copy file in /tmp/cp/file.js to /tmp/symlink
                // where symlink = cp/folder
                files[target + file] = symlink;

                automaton.run('cp', {
                    files: files
                }, function (err) {
                    if (err) {
                        throw err;
                    }

                    expect(isFile(symlink)).to.be(true);
                    done();
                });

            });
        });

        it('should copy with default permissions', function (done) {
            var dir         = target + 'permissions/',
                folder      = 'folder/',
                file        = 'file.js',
                toCopy      = {},
                mode_dir,
                mode_file;

            // create dirs
            fs.mkdirSync(dir);
            fs.mkdirSync(target + folder);

            // get default dir mode
            mode_dir = fs.statSync(target + folder).mode;

            // change it to something else
            fs.chmodSync(target + folder, '0777');

            // create file
            fs.writeFileSync(target + file, 'dummy');

            // get file mode
            mode_file = fs.statSync(target + file).mode;

            // change it to something else
            fs.chmodSync(target + file, '0777');

            // file to copy
            toCopy[target + file]   = dir;
            toCopy[target + folder] = dir + 'folder/';

            automaton.run('cp', {
                files: toCopy
            }, function (err) {
                if (err) {
                    throw err;
                }

                expect(fs.statSync(toCopy[target + file] + file).mode).to.equal(mode_file);
                expect(fs.statSync(toCopy[target + folder]).mode).to.equal(mode_dir);
                done();
            });
        });

        it('should pass over the glob options - should not copy the file', function (done) {
            var files = {};
            files[__dirname + '/../helpers/assets/*file'] = target;

            automaton.run('cp', {
                files: files
            }, function (err) {
                if (err) {
                    throw err;
                }

                expect(isFile(target + '.file')).to.be(false);
                done();
            });
        });

        it('should pass over the glob options - should copy the file', function (done) {
            var files = {};
            files[__dirname + '/../helpers/assets/.file'] = target;

            automaton.run('cp', {
                files: files,
                glob: {
                    dot: true
                }
            }, function (err) {
                if (err) {
                    throw err;
                }
                expect(isFile(target + '.file')).to.be(true);
                done();
            });
        });
    });
};
<|endoftext|>


<commit_before>/*
DAL for Q&A Module

 */

(function (mw, $) {
    "use strict";

    var viewedEntries=(function() {
        var _viewedEntries = [];
        if (localStorage["_viewedEntries"]) {
            _viewedEntries = JSON.parse(localStorage["_viewedEntries"]);
        }
        return {
            markAsRead: function(EntryId) {
                // Write to localStorage this item was read
                if (_viewedEntries.indexOf(EntryId) < 0 ) {
                    _viewedEntries.push(EntryId);
                    localStorage["_viewedEntries"] = JSON.stringify(_viewedEntries);

                }
            },
            isRead: function(EntryId) {
                return _viewedEntries.indexOf(EntryId) > -1;
            },
            readThreadsCount: function() {
                return _viewedEntries.length;
            }
        };
    })();

    function QnaThread(ThreadId){
        var _this = this;
        _this.ThreadID = ThreadId;
        _this.entries = ko.observableArray();

        this.getThreadID = function(){
            return _this.ThreadID;
        };

        this.isRead = ko.observable(viewedEntries.isRead(_this.ThreadID));
        this.isCollapsed = ko.observable(true);
        this.appendEntry = function(entry){
            entry.setThread(_this);
            if (!_this.isCollapsed()) {
                _this.entries.push(ko.observable(entry));
            }
            else{
                _this.entries.unshift(ko.observable(entry));
            }
        };

        this.hasUnreadEntries = ko.computed(function() {
            for (var i = 0; i < _this.entries().length; i++) {
                if(!_this.entries()[i]().isRead()){
                    return true;
                }
            }
            return false;
        });

        // Get the timestame of the last Answer on the thread
        // If there are no answers on the thread - return first question (so thread won't jump on new question).
        this.lastTimeForSort = function(){

            if (_this.entries()[0]().getType() === "Announcement"){
                return _this.entries()[0]().getTime();
            }

            var q_time = undefined;
            var a_time = undefined;

            for(var i =0; i < _this.entries().length; ++i) {
                if (_this.entries()[i]().getType() === "Answer"){
                    if (a_time === undefined){
                        a_time = _this.entries()[i]().getTime();
                    }
                    else if (_this.entries()[i]().getTime() > a_time){
                        a_time = _this.entries()[i]().getTime();
                    }
                }
                else if (_this.entries()[i]().getType() === "Question"){
                    if (q_time === undefined){
                        q_time = _this.entries()[i]().getTime();
                    }
                    else if (_this.entries()[i]().getTime() < q_time){
                        q_time = _this.entries()[i]().getTime();
                    }

                }
            }

            if (a_time === undefined){
                return q_time;
            }

            return Math.max(a_time,q_time);

        };

        // This is here so we will be able to save the reply in the context of the thread
        this.replyText = ko.observable(gM("qna-reply-here"));

        this.isTypingAnswer = ko.observable(false);
    };

    function QnaEntry(cuePoint){

        var _this = this;

        this.cuePoint=ko.observable(cuePoint);
        this.timestamp = ko.observable(this.cuePoint().createdAt);

        this.getType = function(){
            return this.cuePoint().metadata.Type;
        };

        this.isRead = ko.observable(viewedEntries.isRead(_this.cuePoint().id) || _this.getType() === "Question");

        this.setThread = function(thread){
            this._thread = thread;
        };

        this.getThread = function(){
            return this._thread;
        };

        this.getContent = function(){
            return this.cuePoint().text;
        };

        this.getTime = function(){
            return this.cuePoint().createdAt;
        };

        this.getOwner = function(){
            return this.cuePoint().userId;
        };

        this.getThreadID = function(){
            return this.cuePoint().metadata.ThreadId;
        };

        this.isAnnouncement = function(){
            return this.type === "Announcement";
        };

        this.getTitle = function(){
            if (this.getType() === "Announcement"){
                return gM('qna-announcement-title');
            }
            else if (this.getType() === "Question"){
                return gM('qna-you-asked');
            }
            else{
                return gM('qna-answered-by') + " " + this.cuePoint().userId;
            }
        };

        this.getText = function(){
            return this.cuePoint().text;
        };

        this.getCurrentTime = function(){
            return this.currentTime;
        };
    }

    mw.KQnaService = function (embedPlayer,qnaPlugin) {
        return this.init(embedPlayer,qnaPlugin);
    };

    mw.KQnaService.prototype = {

        // The bind postfix:
        bindPostfix: '.KQnaService',
        liveAQnaIntervalId: null,
        QnaThreads: ko.observableArray(),
        lastUpdateTime: -1,
        QandA_ResponseProfile: "QandA_ResponseProfile",
        QandA_ResponseProfileSystemName: "QandA",
        QandA_MetadataProfileSystemName: "QandA",
        QandA_cuePointTag: "qna",
        useResponseProfile: false,
        QandA_cuePointTypes: {"Question":1,"Answer":2, "Announcement":3},

        init: function (embedPlayer, qnaPlugin) {
            var _this = this;
            // Remove any old bindings:
            this.destroy();
            // Setup player ref:
            this.embedPlayer = embedPlayer;
            this.qnaPlugin = qnaPlugin;

            this.requestCuePoints();

            if (embedPlayer.isLive()) {
                this.registerItemNotification();
            }

        },

        viewedEntries: viewedEntries,

        destroy: function () {

            if (this.liveAQnaIntervalId) {
                clearInterval(this.liveAQnaIntervalId);
                this.liveAQnaIntervalId = null;
            }
            $(this.embedPlayer).unbind(this.bindPostfix);
        },

        getKClient: function () {
            if (!this.kClient) {
                this.kClient = mw.kApiGetPartnerClient(this.embedPlayer.kwidgetid);
            }
            return this.kClient;
        },

        //returns questions, answers and announcement
        getQnaThreads: function () {
            return this.QnaThreads;
        },

        createMetadataXmlFromObject: function (obj) {
            var xml = "<metadata>";
            for (var propertyName in obj) {
                xml += "<" + propertyName + ">" + obj[propertyName] + "</" + propertyName + ">";
            }
            xml += "</metadata>";
            return xml;
        },

        submitQuestion: function (question, parent) {
            var embedPlayer = this.embedPlayer;
            var _this = this;

            var startTime = new Date();

            var metadata= { };
            if (parent) {
                metadata.ThreadId = parent.cuePoint().metadata.ThreadId;
            }
                metadata.Type="Question";

            var xmlData = _this.createMetadataXmlFromObject(metadata);

            var createCuePointRequest = {
                "service": "cuePoint_cuePoint",
                "action": "add",
                "cuePoint:objectType": "KalturaAnnotation",
                "cuePoint:entryId": embedPlayer.kentryid,
                "cuePoint:startTime": embedPlayer.currentTime,
                "cuePoint:text": question,
                "cuePoint:tags": this.QandA_cuePointTag,
                "cuePoint:partnerData": xmlData
            };
            if (parent) {
                createCuePointRequest["cuePoint:parentId"] = parent.cuePoint().id;
            }

            var listMetadataProfileRequest = {
                service: "metadata_metadataprofile",
                action: "list",
                "filter:systemNameEqual": this.QandA_MetadataProfileSystemName
            };
            var addMetadataRequest = {
                service: "metadata_metadata",
                action: "add",
                metadataProfileId: "{2:result:objects:0:id}",
                objectId: "{1:result:id}",
                xmlData: xmlData,
                objectType: "annotationMetadata.Annotation"
            };

            // mw.log("Submitting a new question: " + question);

            _this.getKClient().doRequest([createCuePointRequest, listMetadataProfileRequest, addMetadataRequest], function (result) {
                    var endTime = new Date();
                    var cuePoint = result[0];
                    var metadata = result[2];
                    if (cuePoint.id) {

                        var item=_this.annotationCuePointToQnaEntry(cuePoint);

                        if (item) {

                            _this.addOrUpdateEntry(item);
                            _this.sortThreads();
                        }
                        mw.log("added Annotation cue point with id: " + cuePoint.id + " took " + (endTime - startTime) + " ms");

                    } else {
                        mw.log("error adding Annotation " + JSON.stringify(cuePoint));

                    }
                },
                false,
                function (err) {
                    mw.log("Error: " + this.bindPostfix + " could not add cue point. Error: " + err);
                });
        },

        // item can be either a QnaThread (for an announcement) or a QnaEntry (for a Q&A thread)
        markAsRead: function (item) {

            item.isRead(true);
            if (item.entries !== undefined){
                item.entries()[0]().isRead(true);
                viewedEntries.markAsRead(item.ThreadID);
                this.updateThread(item);
            }
            else{
                viewedEntries.markAsRead(item.cuePoint().id);
                this.addOrUpdateEntry(item);
                this.qnaPlugin.updateUnreadBadge();
            }
        },

        readThreadsCount: function () {
            return viewedEntries.readThreadsCount();
        },

        updateThread: function(qnaThread){
            var _this=this;
            for (var i = 0; i < _this.QnaThreads().length; i++) {
                if (_this.QnaThreads()[i]().getThreadID() === qnaThread.ThreadID){
                    _this.qnaPlugin.updateUnreadBadge();
                    break;
                }
            }
        },

        sortThreads: function(){
            var _this=this;
            _this.QnaThreads.sort(
                function(a, b){

                    //var a_val = a().isCollapsed() ? a().entries()[0]().getTime() : a().entries()[a().entries().length-1]().getTime();
                    //var b_val = b().isCollapsed() ? b().entries()[0]().getTime() : b().entries()[b().entries().length-1]().getTime();
                    //return b_val - a_val;

                    return b().lastTimeForSort() - a().lastTimeForSort();
                }
            );
        },

        // look for a QnaThread for this QnaEntry
        // if we don't find on we will create it
        addOrUpdateEntry: function (qnaEntry) {

            var _this=this;
            var found = false;

            for (var i = 0; i < _this.QnaThreads().length; i++) {

                if (_this.QnaThreads()[i]().getThreadID() === qnaEntry.getThreadID())
                {
                    // look it this entry exists. If so replace it
                    for (var j = 0; j < _this.QnaThreads()[i]().entries().length; j++){
                        if (_this.QnaThreads()[i]().entries()[j]().cuePoint().id === qnaEntry.cuePoint().id){
                            qnaEntry.setThread(_this.QnaThreads()[i]());
                            _this.QnaThreads()[i]().entries.splice(j, 0, ko.observable(qnaEntry));
                            _this.QnaThreads()[i]().entries.splice(j+1, 1);
                            found = true;
                            break;
                        }
                    }
                    if (!found) {
                        _this.QnaThreads()[i]().appendEntry(qnaEntry);
                    }
                    found = true;
                    break;
                }
            }

            if (!found) {
                var newThread = new QnaThread(qnaEntry.getThreadID());
                newThread.appendEntry(qnaEntry);
                _this.QnaThreads.push(ko.observable(newThread));
            }
        },

        metadataToObject: function(metadata) {
            var xml = $.parseXML(metadata.xml);

            var $xml = $( xml ).find('metadata').children();

            var obj={};
            $.each( $xml, function(inx, node){
                if (node.nodeType===1) {
                    obj[node.nodeName] = node.textContent;
                }
            });
            obj['xml'] = metadata.xml;
            return obj;
        },

        joinMetadataWithCuepoint:function(cuePoint,metadata ){
            if (!metadata)
                return false;

            var obj=this.metadataToObject(metadata);

            delete cuePoint.relatedObjects;

            $.extend(cuePoint,{ metadata: obj, metadataId: metadata.id});

            return true;

        },

        // convert a cuePoint from the server to a QnaEntry object
        annotationCuePointToQnaEntry: function(cuePoint) {

            var metadata=cuePoint.metadata;
            if (cuePoint.relatedObjects &&
                cuePoint.relatedObjects[this.QandA_ResponseProfile] &&
                cuePoint.relatedObjects[this.QandA_ResponseProfile].objects &&
                cuePoint.relatedObjects[this.QandA_ResponseProfile].objects.length>0) {

                metadata=cuePoint.relatedObjects[this.QandA_ResponseProfile].objects[0];

                delete cuePoint.relatedObjects;
            }

            if (!cuePoint.metadata) {

                metadata={ xml: cuePoint.partnerData, id: null };
            }

            if (!this.joinMetadataWithCuepoint(cuePoint,metadata)) {
                mw.log("Cue point "+cuePoint.id+ " was ignored since it's not a valid one" );
                return null;
            }

            if (!cuePoint.metadata.ThreadId) {
                //take the thread id from cue point id
                cuePoint.metadata.ThreadId=cuePoint.id;
            }

            var tempType=this.QandA_cuePointTypes[cuePoint.metadata.Type];

            return new QnaEntry(cuePoint);
        },

        requestCuePoints:function() {
            var _this = this;

            var entryId = _this.embedPlayer.kentryid;
            var request = {
                'service': 'cuepoint_cuepoint',
                'action': 'list',
                'filter:tagsLike':_this.QandA_cuePointTag,
                'filter:entryIdEqual': entryId,
                'filter:objectType': 'KalturaAnnotationFilter',
                'filter:orderBy': '+createdAt'
            };

            if (_this.useResponseProfile) {
                request["responseProfile:objectType"]="KalturaResponseProfileHolder";
                request["responseProfile:systemName"]=_this.QandA_ResponseProfileSystemName;
            }
            var lastUpdatedAt = _this.lastUpdateTime + 1;
            // Only add lastUpdatedAt filter if any cue points already received
            if (lastUpdatedAt > 0) {
                request['filter:updatedAtGreaterThanOrEqual'] = lastUpdatedAt;
            }
            _this.getKClient().doRequest( request,
                function (data) {
                    // if an error pop out:
                    if (!data || data.code) {
                        // todo: add error handling
                        mw.log("Error:: KCuePoints could not retrieve live cuepoints");
                        return;
                    }

                    data.objects.forEach(function(cuePoint) {

                        var item=_this.annotationCuePointToQnaEntry(cuePoint);
                        if (item) {

                            if (_this.lastUpdateTime < cuePoint.updatedAt) {
                                _this.lastUpdateTime = cuePoint.updatedAt;
                            }
                            _this.addOrUpdateEntry(item);
                        }
                    });

                    _this.sortThreads();
                }
            );
        },

        //Currently there is no notification, so we poll the API
        registerItemNotification: function () {
            var _this = this;

            //Start live cuepoint pulling
            this.liveAQnaIntervalId = setInterval(function () {
                _this.requestCuePoints();
            }, mw.getConfig("qnaPollingInterval") || 10000);
        }
    };
})(window.mw, window.jQuery);
<commit_msg>Q&A plugin - deal better with bad data<commit_after>/*
DAL for Q&A Module

 */

(function (mw, $) {
    "use strict";

    var viewedEntries=(function() {
        var _viewedEntries = [];
        if (localStorage["_viewedEntries"]) {
            _viewedEntries = JSON.parse(localStorage["_viewedEntries"]);
        }
        return {
            markAsRead: function(EntryId) {
                // Write to localStorage this item was read
                if (_viewedEntries.indexOf(EntryId) < 0 ) {
                    _viewedEntries.push(EntryId);
                    localStorage["_viewedEntries"] = JSON.stringify(_viewedEntries);

                }
            },
            isRead: function(EntryId) {
                return _viewedEntries.indexOf(EntryId) > -1;
            },
            readThreadsCount: function() {
                return _viewedEntries.length;
            }
        };
    })();

    function QnaThread(ThreadId){
        var _this = this;
        _this.ThreadID = ThreadId;
        _this.entries = ko.observableArray();

        this.getThreadID = function(){
            return _this.ThreadID;
        };

        this.isRead = ko.observable(viewedEntries.isRead(_this.ThreadID));
        this.isCollapsed = ko.observable(true);
        this.appendEntry = function(entry){
            entry.setThread(_this);
            if (!_this.isCollapsed()) {
                _this.entries.push(ko.observable(entry));
            }
            else{
                _this.entries.unshift(ko.observable(entry));
            }
        };

        this.hasUnreadEntries = ko.computed(function() {
            for (var i = 0; i < _this.entries().length; i++) {
                if(!_this.entries()[i]().isRead()){
                    return true;
                }
            }
            return false;
        });

        // Get the timestame of the last Answer on the thread
        // If there are no answers on the thread - return first question (so thread won't jump on new question).
        this.lastTimeForSort = function(){

            if (_this.entries()[0]().getType() === "Announcement"){
                return _this.entries()[0]().getTime();
            }

            var q_time = undefined;
            var a_time = undefined;

            for(var i =0; i < _this.entries().length; ++i) {
                if (_this.entries()[i]().getType() === "Answer"){
                    if (a_time === undefined){
                        a_time = _this.entries()[i]().getTime();
                    }
                    else if (_this.entries()[i]().getTime() > a_time){
                        a_time = _this.entries()[i]().getTime();
                    }
                }
                else if (_this.entries()[i]().getType() === "Question"){
                    if (q_time === undefined){
                        q_time = _this.entries()[i]().getTime();
                    }
                    else if (_this.entries()[i]().getTime() < q_time){
                        q_time = _this.entries()[i]().getTime();
                    }

                }
            }

            if (a_time === undefined && q_time === undefined) {
                console.log("both a_time and q_time are undefined - data error");
                return 0;
            }

            if (a_time === undefined){
                return q_time;
            }

            if (q_time === undefined){
                return a_time;
            }

            return Math.max(a_time,q_time);

        };

        // This is here so we will be able to save the reply in the context of the thread
        this.replyText = ko.observable(gM("qna-reply-here"));

        this.isTypingAnswer = ko.observable(false);
    };

    function QnaEntry(cuePoint){

        var _this = this;

        this.cuePoint=ko.observable(cuePoint);
        this.timestamp = ko.observable(this.cuePoint().createdAt);

        this.getType = function(){
            return this.cuePoint().metadata.Type;
        };

        this.isRead = ko.observable(viewedEntries.isRead(_this.cuePoint().id) || _this.getType() === "Question");

        this.setThread = function(thread){
            this._thread = thread;
        };

        this.getThread = function(){
            return this._thread;
        };

        this.getContent = function(){
            return this.cuePoint().text;
        };

        this.getTime = function(){
            return this.cuePoint().createdAt;
        };

        this.getOwner = function(){
            return this.cuePoint().userId;
        };

        this.getThreadID = function(){
            return this.cuePoint().metadata.ThreadId;
        };

        this.isAnnouncement = function(){
            return this.type === "Announcement";
        };

        this.getTitle = function(){
            if (this.getType() === "Announcement"){
                return gM('qna-announcement-title');
            }
            else if (this.getType() === "Question"){
                return gM('qna-you-asked');
            }
            else{
                return gM('qna-answered-by') + " " + this.cuePoint().userId;
            }
        };

        this.getText = function(){
            return this.cuePoint().text;
        };

        this.getCurrentTime = function(){
            return this.currentTime;
        };
    }

    mw.KQnaService = function (embedPlayer,qnaPlugin) {
        return this.init(embedPlayer,qnaPlugin);
    };

    mw.KQnaService.prototype = {

        // The bind postfix:
        bindPostfix: '.KQnaService',
        liveAQnaIntervalId: null,
        QnaThreads: ko.observableArray(),
        lastUpdateTime: -1,
        QandA_ResponseProfile: "QandA_ResponseProfile",
        QandA_ResponseProfileSystemName: "QandA",
        QandA_MetadataProfileSystemName: "QandA",
        QandA_cuePointTag: "qna",
        useResponseProfile: false,
        QandA_cuePointTypes: {"Question":1,"Answer":2, "Announcement":3},

        init: function (embedPlayer, qnaPlugin) {
            var _this = this;
            // Remove any old bindings:
            this.destroy();
            // Setup player ref:
            this.embedPlayer = embedPlayer;
            this.qnaPlugin = qnaPlugin;

            this.requestCuePoints();

            if (embedPlayer.isLive()) {
                this.registerItemNotification();
            }

        },

        viewedEntries: viewedEntries,

        destroy: function () {

            if (this.liveAQnaIntervalId) {
                clearInterval(this.liveAQnaIntervalId);
                this.liveAQnaIntervalId = null;
            }
            $(this.embedPlayer).unbind(this.bindPostfix);
        },

        getKClient: function () {
            if (!this.kClient) {
                this.kClient = mw.kApiGetPartnerClient(this.embedPlayer.kwidgetid);
            }
            return this.kClient;
        },

        //returns questions, answers and announcement
        getQnaThreads: function () {
            return this.QnaThreads;
        },

        createMetadataXmlFromObject: function (obj) {
            var xml = "<metadata>";
            for (var propertyName in obj) {
                xml += "<" + propertyName + ">" + obj[propertyName] + "</" + propertyName + ">";
            }
            xml += "</metadata>";
            return xml;
        },

        submitQuestion: function (question, parent) {
            var embedPlayer = this.embedPlayer;
            var _this = this;

            var startTime = new Date();

            var metadata= { };
            if (parent) {
                metadata.ThreadId = parent.cuePoint().metadata.ThreadId;
            }
                metadata.Type="Question";

            var xmlData = _this.createMetadataXmlFromObject(metadata);

            var createCuePointRequest = {
                "service": "cuePoint_cuePoint",
                "action": "add",
                "cuePoint:objectType": "KalturaAnnotation",
                "cuePoint:entryId": embedPlayer.kentryid,
                "cuePoint:startTime": embedPlayer.currentTime,
                "cuePoint:text": question,
                "cuePoint:tags": this.QandA_cuePointTag,
                "cuePoint:partnerData": xmlData
            };
            if (parent) {
                createCuePointRequest["cuePoint:parentId"] = parent.cuePoint().id;
            }

            var listMetadataProfileRequest = {
                service: "metadata_metadataprofile",
                action: "list",
                "filter:systemNameEqual": this.QandA_MetadataProfileSystemName
            };
            var addMetadataRequest = {
                service: "metadata_metadata",
                action: "add",
                metadataProfileId: "{2:result:objects:0:id}",
                objectId: "{1:result:id}",
                xmlData: xmlData,
                objectType: "annotationMetadata.Annotation"
            };

            // mw.log("Submitting a new question: " + question);

            _this.getKClient().doRequest([createCuePointRequest, listMetadataProfileRequest, addMetadataRequest], function (result) {
                    var endTime = new Date();
                    var cuePoint = result[0];
                    var metadata = result[2];
                    if (cuePoint.id) {

                        var item=_this.annotationCuePointToQnaEntry(cuePoint);

                        if (item) {

                            _this.addOrUpdateEntry(item);
                            _this.sortThreads();
                        }
                        mw.log("added Annotation cue point with id: " + cuePoint.id + " took " + (endTime - startTime) + " ms");

                    } else {
                        mw.log("error adding Annotation " + JSON.stringify(cuePoint));

                    }
                },
                false,
                function (err) {
                    mw.log("Error: " + this.bindPostfix + " could not add cue point. Error: " + err);
                });
        },

        // item can be either a QnaThread (for an announcement) or a QnaEntry (for a Q&A thread)
        markAsRead: function (item) {

            item.isRead(true);
            if (item.entries !== undefined){
                item.entries()[0]().isRead(true);
                viewedEntries.markAsRead(item.ThreadID);
                this.updateThread(item);
            }
            else{
                viewedEntries.markAsRead(item.cuePoint().id);
                this.addOrUpdateEntry(item);
                this.qnaPlugin.updateUnreadBadge();
            }
        },

        readThreadsCount: function () {
            return viewedEntries.readThreadsCount();
        },

        updateThread: function(qnaThread){
            var _this=this;
            for (var i = 0; i < _this.QnaThreads().length; i++) {
                if (_this.QnaThreads()[i]().getThreadID() === qnaThread.ThreadID){
                    _this.qnaPlugin.updateUnreadBadge();
                    break;
                }
            }
        },

        sortThreads: function(){
            var _this=this;
            _this.QnaThreads.sort(
                function(a, b){

                    //var a_val = a().isCollapsed() ? a().entries()[0]().getTime() : a().entries()[a().entries().length-1]().getTime();
                    //var b_val = b().isCollapsed() ? b().entries()[0]().getTime() : b().entries()[b().entries().length-1]().getTime();
                    //return b_val - a_val;

                    return b().lastTimeForSort() - a().lastTimeForSort();
                }
            );
        },

        // look for a QnaThread for this QnaEntry
        // if we don't find on we will create it
        addOrUpdateEntry: function (qnaEntry) {

            var _this=this;
            var found = false;

            for (var i = 0; i < _this.QnaThreads().length; i++) {

                if (_this.QnaThreads()[i]().getThreadID() === qnaEntry.getThreadID())
                {
                    // look it this entry exists. If so replace it
                    for (var j = 0; j < _this.QnaThreads()[i]().entries().length; j++){
                        if (_this.QnaThreads()[i]().entries()[j]().cuePoint().id === qnaEntry.cuePoint().id){
                            qnaEntry.setThread(_this.QnaThreads()[i]());
                            _this.QnaThreads()[i]().entries.splice(j, 0, ko.observable(qnaEntry));
                            _this.QnaThreads()[i]().entries.splice(j+1, 1);
                            found = true;
                            break;
                        }
                    }
                    if (!found) {
                        _this.QnaThreads()[i]().appendEntry(qnaEntry);
                    }
                    found = true;
                    break;
                }
            }

            if (!found) {
                var newThread = new QnaThread(qnaEntry.getThreadID());
                newThread.appendEntry(qnaEntry);
                _this.QnaThreads.push(ko.observable(newThread));
            }
        },

        metadataToObject: function(metadata) {
            var xml = $.parseXML(metadata.xml);

            var $xml = $( xml ).find('metadata').children();

            var obj={};
            $.each( $xml, function(inx, node){
                if (node.nodeType===1) {
                    obj[node.nodeName] = node.textContent;
                }
            });
            obj['xml'] = metadata.xml;
            return obj;
        },

        joinMetadataWithCuepoint:function(cuePoint,metadata ){
            if (!metadata)
                return false;

            var obj=this.metadataToObject(metadata);

            delete cuePoint.relatedObjects;

            $.extend(cuePoint,{ metadata: obj, metadataId: metadata.id});

            return true;

        },

        // convert a cuePoint from the server to a QnaEntry object
        annotationCuePointToQnaEntry: function(cuePoint) {

            var metadata=cuePoint.metadata;
            if (cuePoint.relatedObjects &&
                cuePoint.relatedObjects[this.QandA_ResponseProfile] &&
                cuePoint.relatedObjects[this.QandA_ResponseProfile].objects &&
                cuePoint.relatedObjects[this.QandA_ResponseProfile].objects.length>0) {

                metadata=cuePoint.relatedObjects[this.QandA_ResponseProfile].objects[0];

                delete cuePoint.relatedObjects;
            }

            if (!cuePoint.metadata) {

                metadata={ xml: cuePoint.partnerData, id: null };
            }

            if (!this.joinMetadataWithCuepoint(cuePoint,metadata)) {
                mw.log("Cue point "+cuePoint.id+ " was ignored since it's not a valid one" );
                return null;
            }

            if (!cuePoint.metadata.ThreadId) {
                //take the thread id from cue point id
                cuePoint.metadata.ThreadId=cuePoint.id;
            }

            var tempType=this.QandA_cuePointTypes[cuePoint.metadata.Type];

            return new QnaEntry(cuePoint);
        },

        requestCuePoints:function() {
            var _this = this;

            var entryId = _this.embedPlayer.kentryid;
            var request = {
                'service': 'cuepoint_cuepoint',
                'action': 'list',
                'filter:tagsLike':_this.QandA_cuePointTag,
                'filter:entryIdEqual': entryId,
                'filter:objectType': 'KalturaAnnotationFilter',
                'filter:orderBy': '+createdAt'
            };

            if (_this.useResponseProfile) {
                request["responseProfile:objectType"]="KalturaResponseProfileHolder";
                request["responseProfile:systemName"]=_this.QandA_ResponseProfileSystemName;
            }
            var lastUpdatedAt = _this.lastUpdateTime + 1;
            // Only add lastUpdatedAt filter if any cue points already received
            if (lastUpdatedAt > 0) {
                request['filter:updatedAtGreaterThanOrEqual'] = lastUpdatedAt;
            }
            _this.getKClient().doRequest( request,
                function (data) {
                    // if an error pop out:
                    if (!data || data.code) {
                        // todo: add error handling
                        mw.log("Error:: KCuePoints could not retrieve live cuepoints");
                        return;
                    }

                    data.objects.forEach(function(cuePoint) {

                        var item=_this.annotationCuePointToQnaEntry(cuePoint);
                        if (item) {

                            if (_this.lastUpdateTime < cuePoint.updatedAt) {
                                _this.lastUpdateTime = cuePoint.updatedAt;
                            }
                            _this.addOrUpdateEntry(item);
                        }
                    });

                    _this.sortThreads();
                }
            );
        },

        //Currently there is no notification, so we poll the API
        registerItemNotification: function () {
            var _this = this;

            //Start live cuepoint pulling
            this.liveAQnaIntervalId = setInterval(function () {
                _this.requestCuePoints();
            }, mw.getConfig("qnaPollingInterval") || 10000);
        }
    };
})(window.mw, window.jQuery);
<|endoftext|>


<commit_before><commit_msg>Disable non-functional test<commit_after><|endoftext|>


<commit_before>/* eslint-disable react/forbid-prop-types */
/**
 * mSupply Mobile
 * Sustainable Solutions (NZ) Ltd. 2019
 */

import React from 'react';
import { View } from 'react-native';
import PropTypes from 'prop-types';

/**
 * Simple layout component. For use when needing a simple View with flex
 * direction of row.
 * @param {React.node} children       Children of this component
 * @param {Number}     flex           The flex amount i.e. 1
 * @param {Object}     style          An additional styles object.
 */
export const FlexView = ({ children, flex, style }) => {
  const internalStyle = React.useMemo(() => ({ flex, ...style }));
  return <View style={internalStyle}>{children}</View>;
};

FlexView.defaultProps = {
  children: null,
  flex: 1,
  style: {},
};

FlexView.propTypes = {
  children: PropTypes.oneOf([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]),
  flex: PropTypes.number,
  style: PropTypes.object,
};
<commit_msg>Add corrected comment<commit_after>/* eslint-disable react/forbid-prop-types */
/**
 * mSupply Mobile
 * Sustainable Solutions (NZ) Ltd. 2019
 */

import React from 'react';
import { View } from 'react-native';
import PropTypes from 'prop-types';

/**
 * Simple layout component. For use when needing a simple View with flex.
 *
 * @param {React.node} children       Children of this component
 * @param {Number}     flex           The flex amount i.e. 1
 * @param {Object}     style          An additional styles object.
 */
export const FlexView = ({ children, flex, style }) => {
  const internalStyle = React.useMemo(() => ({ flex, ...style }));
  return <View style={internalStyle}>{children}</View>;
};

FlexView.defaultProps = {
  children: null,
  flex: 1,
  style: {},
};

FlexView.propTypes = {
  children: PropTypes.oneOf([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]),
  flex: PropTypes.number,
  style: PropTypes.object,
};
<|endoftext|>


<commit_before>let t;

try {
  t = require('tap')
} catch (e) {
  t = require('../');
  t.equals(e.code, 'MODULE_NOT_FOUND')
  t.end();
  return
}

const {join} = require('path')

const testPath = join(__dirname, './test.mjs')

t.test('test esm entry point', t => {
  t.spawn('node', [testPath]);
  t.done();
})
<commit_msg>minor style nits, and remove call to removed t.done() alias<commit_after>let t;

try {
  t = require('tap')
} catch (e) {
  t = require('../')
  t.equals(e.code, 'MODULE_NOT_FOUND')
  t.end()
  return
}

const {join} = require('path')

const testPath = join(__dirname, './test.mjs')

t.test('test esm entry point', t => {
  t.spawn('node', [testPath])
  t.end()
})
<|endoftext|>


<commit_before>var del = require('del')
var fs = require('fs')
var os = require('os')
var path = require('path')
var should = require('should')

console.log('Preparing temp folder...')
var tmpDir = fs.realpathSync(os.tmpdir()) + path.sep
var TEMP = fs.mkdtempSync(tmpDir)
console.log('TEMP:', TEMP)

var cleanup = function() {
	console.log('Clearing temp folder...', TEMP)
	del.sync(TEMP, { force: true })
}

describe('Mango class', function() {
	var Mango = require('../lib/mango')
	var Config = require('../lib/helpers/config')

	describe('should create a new instance', function() {

		it('with no arguments', function() {
			new Mango()
		})

		it('with only the folder argument', function() {
			new Mango(process.cwd())
		})

		it('with a folder argument and an empty object as the config parameter', function() {
			new Mango(process.cwd(), {})
		})

	})

	describe('should in a temp directory', function() {
		var mango

		it('init a template', function(done) {
			this.timeout(15000)
			var pkg = require('../package')
			mango = new Mango(TEMP)
			mango.init(pkg.config.default_fork_repo, done)
		})

		it('read the configuration file', function() {
			var config = new Config(TEMP)
			mango = new Mango(TEMP, config.get())
		})

		it('install dependencies', function(done) {
			this.timeout(60000)
			mango.install(done)
		})

		it('run build scripts', function(done) {
			this.timeout(120000)
			mango.build(['scripts'], [], done)
		})

		it('run build styles', function(done) {
			this.timeout(120000)
			mango.build(['styles'], [], done)
		})

		it('run build static', function(done) {
			this.timeout(120000)
			mango.build(['static'], [], done)
		})

		it('run build sprites', function(done) {
			this.timeout(120000)
			mango.build(['static'], [], done)
		})

		it('run build templates', function(done) {
			this.timeout(120000)
			mango.build(['templates'], [], done)
		})

		it('run build images', function(done) {
			this.timeout(120000)
			mango.build(['images'], [], done)
		})

		it('run the production default build tasks', function(done) {
			this.timeout(120000)
			mango.build([], [], done)
		})

		after(cleanup)
	})

})
<commit_msg>tests: renamed titles<commit_after>var del = require('del')
var fs = require('fs')
var os = require('os')
var path = require('path')
var should = require('should')

console.log('Preparing temp folder...')
var tmpDir = fs.realpathSync(os.tmpdir()) + path.sep
var TEMP = fs.mkdtempSync(tmpDir)
console.log('TEMP:', TEMP)

var cleanup = function() {
	console.log('Clearing temp folder...', TEMP)
	del.sync(TEMP, { force: true })
}

describe('Mango class', function() {
	var Mango = require('../lib/mango')
	var Config = require('../lib/helpers/config')

	describe('should create a new instance', function() {

		it('with no arguments', function() {
			new Mango()
		})

		it('with only the folder argument', function() {
			new Mango(process.cwd())
		})

		it('with a folder argument and an empty object as the config parameter', function() {
			new Mango(process.cwd(), {})
		})

	})

	describe('should in a temp directory', function() {
		var mango

		it('init a template', function(done) {
			this.timeout(15000)
			var pkg = require('../package')
			mango = new Mango(TEMP)
			mango.init(pkg.config.default_fork_repo, done)
		})

		it('read the configuration file', function() {
			var config = new Config(TEMP)
			mango = new Mango(TEMP, config.get())
		})

		it('install dependencies', function(done) {
			this.timeout(60000)
			mango.install(done)
		})

		it('build scripts', function(done) {
			this.timeout(120000)
			mango.build(['scripts'], [], done)
		})

		it('build styles', function(done) {
			this.timeout(120000)
			mango.build(['styles'], [], done)
		})

		it('build static', function(done) {
			this.timeout(120000)
			mango.build(['static'], [], done)
		})

		it('build sprites', function(done) {
			this.timeout(120000)
			mango.build(['static'], [], done)
		})

		it('build templates', function(done) {
			this.timeout(120000)
			mango.build(['templates'], [], done)
		})

		it('build images', function(done) {
			this.timeout(120000)
			mango.build(['images'], [], done)
		})

		it('run the production build tasks', function(done) {
			this.timeout(120000)
			mango.build([], [], done)
		})

		after(cleanup)
	})

})
<|endoftext|>


<commit_before>import expect from 'expect';
import React from 'react';
import createHistory from 'history/lib/createHashHistory';
import resetHash from './resetHash';
import execSteps from './execSteps';
import Router from '../Router';
import Route from '../Route';

describe('pushState', function () {
  beforeEach(resetHash);

  var node;
  beforeEach(function () {
    node = document.createElement('div');
  });

  afterEach(function () {
    React.unmountComponentAtNode(node);
  });

  describe('when the target path contains a colon', function () {
    it('works', function (done) {
      var Index = React.createClass({
        render() {
          return
Index
;
        }
      });

      var Home = React.createClass({
        render() {
          return
Home
;
        }
      });

      var steps = [
        function () {
          expect(this.state.location.pathname).toEqual('/');
          this.history.pushState(null, '/home/hi:there');
        },
        function () {
          expect(this.state.location.pathname).toEqual('/home/hi:there');
        }
      ];

      var execNextStep = execSteps(steps, done);
      var history = createHistory();

      React.render((
        <Router history={history} onUpdate={execNextStep}>
          <Route path="/" component={Index}/>
          <Route path="/home/hi:there" component={Home}/>
        </Router>
      ), node, execNextStep);
    });
  });
});
<commit_msg>Update pushState test<commit_after>import expect from 'expect';
import React from 'react';
import resetHash from './resetHash';
import execSteps from './execSteps';
import Router from '../Router';
import Route from '../Route';

describe('pushState', function () {
  beforeEach(resetHash);

  var node;
  beforeEach(function () {
    node = document.createElement('div');
  });

  afterEach(function () {
    React.unmountComponentAtNode(node);
  });

  describe('when the target path contains a colon', function () {
    it('works', function (done) {
      var Index = React.createClass({
        render() {
          return
Index
;
        }
      });

      var Home = React.createClass({
        render() {
          return
Home
;
        }
      });

      var steps = [
        function () {
          expect(this.state.location.pathname).toEqual('/');
          this.history.pushState(null, '/home/hi:there');
        },
        function () {
          expect(this.state.location.pathname).toEqual('/home/hi:there');
        }
      ];

      var execNextStep = execSteps(steps, done);

      React.render((
        <Router onUpdate={execNextStep}>
          <Route path="/" component={Index}/>
          <Route path="/home/hi:there" component={Home}/>
        </Router>
      ), node, execNextStep);
    });
  });
});
<|endoftext|>