<commit_before>var MessageDB = {
	insertMessage(userId, messageData) {
		/*
		messageData = {
			url -> url that was sent in the message
			platformName -> platform it came from
			domainName -> url's domain name
			title -> from 344 api, title of article
			description -> from 344 api, description of article
			type -> from 344
			imageUrl -> from 344 api, image in article
			sender -> who sent the link
			note -> the text of the message
			timeSent -> string timestamp of when the message was sent
		}

		1. check if link exists
			if does -> go to step 3 with link id
			if doesnt -> do step 2, do step 3
		2. check if domain exists
			if does -> get id
			if doesnt -> insert domain, insert user_domains
			insert link
		3. get platform id
		4. insert message w/ platform id, link id, & recipient id
		5. insert to user_messages
		*/

		// TODO: check if message is already if there before adding again
		return this._connection.queryAsync(
			"SELECT link_id FROM LINKS WHERE url = :url",
			{url: messageData.url}
		).then(linkRows => {
			if (linkRows && linkRows.length > 0) {
				return linkRows[0].link_id
			} else {
				// link doesnt exist

				//check if domain exists
				return this._connection.queryAsync(
					"SELECT domain_id FROM DOMAIN WHERE domain_name = :domain",
					{domain: messageData.domainName}
				).then(domainRows => {
					if (domainRows && domainRows.length > 0) {
						return domainRows[0].domainId
					} else {
						return this._connection.queryAsync(
							"INSERT INTO DOMAIN (domain_name) VALUES (:domain)",
							{domain: messageData.domainName}
						).then(() => {
							return this._connection.lastInsertId()
						}).then(domainId => {

							this._connection.queryAsync(
								"INSERT INTO USER_DOMAINS VALUES (:userId, :domainId)",
								{userId: userId, domainId: domainId}
							)
							return domainId
						})
					}
				}).then(domainId => {
					// insert link, return id
					return this._connection.queryAsync(
						"INSERT INTO LINKS (title, description, type, domain_id, url, img_url) " +
						"VALUES (:title, :description, :type, :domainId, :url, :imgUrl)",
						{
							title: messageData.title,
							description: messageData.description,
							type: messageData.type,
							domainId: domainId,
							url: messageData.url,
							imgUrl: messageData.imageUrl
						}
					).then(() => {
						return this._connection.lastInsertId()
					})
				})
			}
		}).then(linkId => {
			// get platform id
			return this._connection.queryAsync(
				"SELECT platform_id FROM PLATFORM WHERE platform_name = :platform",
				{platform: messageData.platformName}
			).then(platformRows => {
				return {
					platformId: platformRows[0].platform_id,
					linkId: linkId
				}
			})
		}).then(ids => {
			return this._connection.queryAsync(
				"INSERT INTO MESSAGE (link_id, sender, recipient_id, platform_id, note, timeSent, is_read, deleted) " +
				"VALUES (:linkId, :sender, :recipId, :platId, :note, :time, :isRead, :deleted)",
				{
					linkId: ids.linkId,
					sender: messageData.sender,
					recipId: userId,
					platId: ids.platformId,
					note: messageData.note,
					time: messageData.timeSent,
					isRead: false,
					deleted: false
				}
			).then(() => {
				return this._connection.lastInsertId()
			})
		}).then(messageId => {
			return this._connection.queryAsync(
				"INSERT INTO USER_MESSAGES (user_id, message_id) VALUES (:userId, :messageId)",
				{userId: userId, messageId: messageId}
			).then(() => {
				return messageId
			})
		}).then((messageId) => {
			this._connection.end()
			messageData.messageId = messageId
			return messageData
		})

	},

	getMessages(whereClause, options) {
		return this._getObjects(
			(
				'SELECT ' +
					'm.message_id AS messageId' +
					'm.sender, ' +
					'm.note, ' +
					'UNIX_TIMESTAMP(m.timeSent) AS timeSent, ' +
					'm.is_read AS isRead, ' +
					'p.platform_name AS platformName, ' +
					'l.title, ' +
					'l.description, ' +
					'l.type, ' +
					'l.url, ' +
					'l.img_url AS imgUrl, ' +
					'd.domain_name AS domainName' +
					't.tag_text AS tag ' +
				'FROM MESSAGE ' +
				'JOIN PLATFORM p ON m.platform_id = p.platform_id ' +
				'JOIN LINKS l ON m.link_id = l.link_id ' +
				'JOIN DOMAIN d ON l.domain_id = d.domain_id ' +
				'JOIN LINKS_TAGS lt ON l.link_id = lt.link_id ' +
				'JOIN TAGS t ON lt.tag_id = t.tag_id' +
				whereClause
			),
			options
		)
	},

	getUnreadMessages(userId) {
		return this.getMessages(
			(
				'WHERE m.recipient_id = :userId ' +
				'AND m.is_read = :isRead ' +
				'AND m.deleted = :isDeleted'
			),
			{
				userId: userId,
				isRead: false,
				isDeleted: false
			}
		)
	},

	getReadMessages(userId) {
		return this.getMessages(
			(
				'WHERE m.recipient_id = :userId ' +
				'AND m.is_read = :isRead ' +
				'AND m.deleted = :isDeleted'
			),
			{
				userId: userId,
				isRead: true,
				isDeleted: false
			}
		)
	},

	getMessageWithTag(userId, tag) {
		return this.getMessages(
			(
				'WHERE m.recipient_id = :userId ' +
				'AND t.tag_text = :tag ' +
				'AND m.deleted = :isDeleted'
			),
			{
				userId: userId,
				tag: tag,
				isDeleted: false
			}
		)
	},

	getMessagesWithDomain(userId, domain) {
		return this.getMessages(
			(
				'WHERE m.recipient_id = :userId ' +
				'AND d.domain_name = :domain ' +
				'AND m.deleted = :isDeleted'
			),
			{
				userId: userId,
				domain: domain,
				isDeleted: false
			}
		)
	},

	getNewMessagesWithType(userId, type) {
		return this.getMessages(
			(
				'WHERE m.recipient_id = :userId ' +
				'AND m.is_read = :isRead ' +
				'AND l.type = :type ' +
				'AND m.deleted = :isDeleted'
			),
			{
				userId: userId,
				isRead: false,
				type: type,
				isDeleted: false
			}
		)
	},

	getOldMessagesWithType(userId, type) {
		return this.getMessages(
			(
				'WHERE m.recipient_id = :userId ' +
				'AND m.is_read = :isRead ' +
				'AND l.type = :type ' +
				'AND m.deleted = :isDeleted'
			),
			{
				userId: userId,
				isRead: true,
				type: type,
				isDeleted: false
			}
		)
	},

	markRead(messageId) {
		return this._connection.queryAsync(
			'UPDATE MESSAGE SET is_read = 1 WHERE message_id = :messageId',
			{messageId: messageId}
		)
	},

	markDeleted(messageId) {
		return this._connection.queryAsync(
			'UPDATE MESSAGE SET deleted = 1 WHERE message_id = :messageId',
			{messageId: messageId}
		);
	},

	// TODO: this probably has to be modified too
/*
	newMessage(data) {
		//data =>senderId, url, recipientEmail, note, imgUrl, title, domain, description
		var recipientId,
			checkLink = this._connection.queryAsync('SELECT link_id FROM LINKS WHERE url = :url', {url: data.url}),
			getRecipient = this._connection.queryAsync('SELECT user_id FROM USERS WHERE email = :email', {email: data.recipientEmail});

		return Promise.all([checkLink, getRecipient])
			.then(values => {
				recipientId = values[1][0].id;

				var link = values[0];

				if (link && link.length > 0) {
					return this._connection.queryAsync(
						'INSERT INTO MESSAGE (link_id, sender_id, recipient_id, note) ' +
						'VALUES (:linkId, :senderId, :recipientId, :note)',
						{
							linkId: link[0].id,
							senderId: data.senderId,
							recipientId: recipientId,
							note: data.note
						})
				} else {
					return this._connection.queryAsync(
						'SELECT id FROM domain WHERE name = :name',
						{name: data.domain})
						.then(domainRows => {
							if (domainRows && domainRows.length > 0) {
								return domainRows[0].id;
							} else {
								return this._connection.queryAsync(
									'INSERT INTO domain (name) VALUES (:name)',
									{name: data.domain})
									.then(insertData => {
										return insertData.info.insertId;
									})
							}
						})
						.then(domainId => {
							return this._connection.queryAsync(
								'INSERT INTO link (domainId, url, title, description, imgUrl) ' +
								'VALUES (:domainId, :url, :title, :description, :imgUrl)',
								{
									domainId: domainId,
									url: data.url,
									title: data.title,
									description: data.description,
									imgUrl: data.imgUrl
								})
						})
						.then(insertData => {
							var linkId = insertData.info.insertId;
							return this._connection.queryAsync(
								'INSERT INTO message (linkId, senderId, recipientId, note) ' +
								'VALUES (:linkId, :senderId, :recipientId, :note)',
								{
									linkId: linkId,
									senderId: data.senderId,
									recipientId: recipientId,
									note: data.note
								})
						});
				}
			});

	},
*/
	// Given connection, query and params, returns a promise containing query contents
	// If query returns no results, returns a promise containing null
	_getSingleObject(query, params) {
		return this._connection
			.queryAsync(query, {id: params.id || '', email: params.email || ''})
			.then(rows => {
				return rows && rows.length > 0 ? rows[0] : null;
			});
	},

	_getObjects(query, params) {
		return this._connection
			.queryAsync(query, {id: params.id})
			.then(rows => {
				return rows && rows.length > 0 ? rows : null;
			});
	}
}

module.exports = function (connection) {
	var messageDB = Object.create(MessageDB);
	messageDB._connection = connection;
	return messageDB;
}

/*
	getSearchResults (keyword, platform, direction, type, time, domain, friend, tags)
		return this._getObjects(
			(
				SELECT * FROM MESSAGE m
				JOIN LINKS l ON m.link_id = l.link_id
				JOIN LINKS_TAGS lt ON l.link_id = lt.link_id
				JOIN TAGS t ON lt.tag_id = t.tag_link
				JOIN DOMAIN d ON l.domain_id = d.domain_id
				JOIN PLATFORM p ON l.platform_id = p.platform_id
				WHERE CONTAINS(m.note, :key)
				AND CONTAINS (t.tag_text, @tags)
				AND p.platform_id = :platform_id
				//Need to get platform id
				//direction
				AND l.type == :type
				//time
				AND d.domain_name = :domain
				AND m.sender_id = :friend OR m.recipient_id = :friend
				// Need to retrieve friend id
			),
			{
				keyword: keyword,
				platform: platform,
				direction: direction,
				type: type,
				time: time,
				domain: domain,
				friend: friend,
				tags: tags
			}
		);
	},
	*/<commit_msg>updating enpoints to use database data<commit_after>var MessageDB = {
	insertMessage(userId, messageData) {
		/*
		messageData = {
			url -> url that was sent in the message
			platformName -> platform it came from
			domainName -> url's domain name
			title -> from 344 api, title of article
			description -> from 344 api, description of article
			type -> from 344
			imageUrl -> from 344 api, image in article
			sender -> who sent the link
			note -> the text of the message
			timeSent -> string timestamp of when the message was sent
		}

		1. check if link exists
			if does -> go to step 3 with link id
			if doesnt -> do step 2, do step 3
		2. check if domain exists
			if does -> get id
			if doesnt -> insert domain, insert user_domains
			insert link
		3. get platform id
		4. insert message w/ platform id, link id, & recipient id
		5. insert to user_messages
		*/

		// TODO: check if message is already if there before adding again
		return this._connection.queryAsync(
			"SELECT link_id FROM LINKS WHERE url = :url",
			{url: messageData.url}
		).then(linkRows => {
			if (linkRows && linkRows.length > 0) {
				return linkRows[0].link_id
			} else {
				// link doesnt exist

				//check if domain exists
				return this._connection.queryAsync(
					"SELECT domain_id FROM DOMAIN WHERE domain_name = :domain",
					{domain: messageData.domainName}
				).then(domainRows => {
					if (domainRows && domainRows.length > 0) {
						return domainRows[0].domainId
					} else {
						return this._connection.queryAsync(
							"INSERT INTO DOMAIN (domain_name) VALUES (:domain)",
							{domain: messageData.domainName}
						).then(() => {
							return this._connection.lastInsertId()
						}).then(domainId => {

							this._connection.queryAsync(
								"INSERT INTO USER_DOMAINS VALUES (:userId, :domainId)",
								{userId: userId, domainId: domainId}
							)
							return domainId
						})
					}
				}).then(domainId => {
					// insert link, return id
					return this._connection.queryAsync(
						"INSERT INTO LINKS (title, description, type, domain_id, url, img_url) " +
						"VALUES (:title, :description, :type, :domainId, :url, :imgUrl)",
						{
							title: messageData.title,
							description: messageData.description,
							type: messageData.type,
							domainId: domainId,
							url: messageData.url,
							imgUrl: messageData.imageUrl
						}
					).then(() => {
						return this._connection.lastInsertId()
					})
				})
			}
		}).then(linkId => {
			// get platform id
			return this._connection.queryAsync(
				"SELECT platform_id FROM PLATFORM WHERE platform_name = :platform",
				{platform: messageData.platformName}
			).then(platformRows => {
				return {
					platformId: platformRows[0].platform_id,
					linkId: linkId
				}
			})
		}).then(ids => {
			return this._connection.queryAsync(
				"INSERT INTO MESSAGE (link_id, sender, recipient_id, platform_id, note, timeSent, is_read, deleted) " +
				"VALUES (:linkId, :sender, :recipId, :platId, :note, :time, :isRead, :deleted)",
				{
					linkId: ids.linkId,
					sender: messageData.sender,
					recipId: userId,
					platId: ids.platformId,
					note: messageData.note,
					time: messageData.timeSent,
					isRead: false,
					deleted: false
				}
			).then(() => {
				return this._connection.lastInsertId()
			})
		}).then(messageId => {
			return this._connection.queryAsync(
				"INSERT INTO USER_MESSAGES (user_id, message_id) VALUES (:userId, :messageId)",
				{userId: userId, messageId: messageId}
			).then(() => {
				return messageId
			})
		}).then((messageId) => {
			this._connection.end()
			messageData.messageId = messageId
			return messageData
		})

	},

	getMessages(whereClause, options) {
		return this._getObjects(
			(
				'SELECT ' +
					'm.message_id AS messageId' +
					'm.sender, ' +
					'm.note, ' +
					'm.timeSent, ' +
					'm.is_read AS isRead, ' +
					'p.platform_name AS platformName, ' +
					'l.title, ' +
					'l.description, ' +
					'l.type, ' +
					'l.url, ' +
					'l.img_url AS imgUrl, ' +
					'd.domain_name AS domainName' +
					't.tag_text AS tag ' +
				'FROM MESSAGE ' +
				'JOIN PLATFORM p ON m.platform_id = p.platform_id ' +
				'JOIN LINKS l ON m.link_id = l.link_id ' +
				'JOIN DOMAIN d ON l.domain_id = d.domain_id ' +
				'JOIN LINKS_TAGS lt ON l.link_id = lt.link_id ' +
				'JOIN TAGS t ON lt.tag_id = t.tag_id' +
				whereClause
			),
			options
		)
	},

	getUnreadMessages(userId) {
		return this.getMessages(
			(
				'WHERE m.recipient_id = :userId ' +
				'AND m.is_read = :isRead ' +
				'AND m.deleted = :isDeleted'
			),
			{
				userId: userId,
				isRead: false,
				isDeleted: false
			}
		)
	},

	getReadMessages(userId) {
		return this.getMessages(
			(
				'WHERE m.recipient_id = :userId ' +
				'AND m.is_read = :isRead ' +
				'AND m.deleted = :isDeleted'
			),
			{
				userId: userId,
				isRead: true,
				isDeleted: false
			}
		)
	},

	getMessageWithTag(userId, tag) {
		return this.getMessages(
			(
				'WHERE m.recipient_id = :userId ' +
				'AND t.tag_text = :tag ' +
				'AND m.deleted = :isDeleted'
			),
			{
				userId: userId,
				tag: tag,
				isDeleted: false
			}
		)
	},

	getMessagesWithDomain(userId, domain) {
		return this.getMessages(
			(
				'WHERE m.recipient_id = :userId ' +
				'AND d.domain_name = :domain ' +
				'AND m.deleted = :isDeleted'
			),
			{
				userId: userId,
				domain: domain,
				isDeleted: false
			}
		)
	},

	getNewMessagesWithType(userId, type) {
		return this.getMessages(
			(
				'WHERE m.recipient_id = :userId ' +
				'AND m.is_read = :isRead ' +
				'AND l.type = :type ' +
				'AND m.deleted = :isDeleted'
			),
			{
				userId: userId,
				isRead: false,
				type: type,
				isDeleted: false
			}
		)
	},

	getOldMessagesWithType(userId, type) {
		return this.getMessages(
			(
				'WHERE m.recipient_id = :userId ' +
				'AND m.is_read = :isRead ' +
				'AND l.type = :type ' +
				'AND m.deleted = :isDeleted'
			),
			{
				userId: userId,
				isRead: true,
				type: type,
				isDeleted: false
			}
		)
	},

	markRead(messageId) {
		return this._connection.queryAsync(
			'UPDATE MESSAGE SET is_read = 1 WHERE message_id = :messageId',
			{messageId: messageId}
		)
	},

	markDeleted(messageId) {
		return this._connection.queryAsync(
			'UPDATE MESSAGE SET deleted = 1 WHERE message_id = :messageId',
			{messageId: messageId}
		);
	},

	// TODO: this probably has to be modified too
/*
	newMessage(data) {
		//data =>senderId, url, recipientEmail, note, imgUrl, title, domain, description
		var recipientId,
			checkLink = this._connection.queryAsync('SELECT link_id FROM LINKS WHERE url = :url', {url: data.url}),
			getRecipient = this._connection.queryAsync('SELECT user_id FROM USERS WHERE email = :email', {email: data.recipientEmail});

		return Promise.all([checkLink, getRecipient])
			.then(values => {
				recipientId = values[1][0].id;

				var link = values[0];

				if (link && link.length > 0) {
					return this._connection.queryAsync(
						'INSERT INTO MESSAGE (link_id, sender_id, recipient_id, note) ' +
						'VALUES (:linkId, :senderId, :recipientId, :note)',
						{
							linkId: link[0].id,
							senderId: data.senderId,
							recipientId: recipientId,
							note: data.note
						})
				} else {
					return this._connection.queryAsync(
						'SELECT id FROM domain WHERE name = :name',
						{name: data.domain})
						.then(domainRows => {
							if (domainRows && domainRows.length > 0) {
								return domainRows[0].id;
							} else {
								return this._connection.queryAsync(
									'INSERT INTO domain (name) VALUES (:name)',
									{name: data.domain})
									.then(insertData => {
										return insertData.info.insertId;
									})
							}
						})
						.then(domainId => {
							return this._connection.queryAsync(
								'INSERT INTO link (domainId, url, title, description, imgUrl) ' +
								'VALUES (:domainId, :url, :title, :description, :imgUrl)',
								{
									domainId: domainId,
									url: data.url,
									title: data.title,
									description: data.description,
									imgUrl: data.imgUrl
								})
						})
						.then(insertData => {
							var linkId = insertData.info.insertId;
							return this._connection.queryAsync(
								'INSERT INTO message (linkId, senderId, recipientId, note) ' +
								'VALUES (:linkId, :senderId, :recipientId, :note)',
								{
									linkId: linkId,
									senderId: data.senderId,
									recipientId: recipientId,
									note: data.note
								})
						});
				}
			});

	},
*/
	// Given connection, query and params, returns a promise containing query contents
	// If query returns no results, returns a promise containing null
	_getSingleObject(query, params) {
		return this._connection
			.queryAsync(query, {id: params.id || '', email: params.email || ''})
			.then(rows => {
				return rows && rows.length > 0 ? rows[0] : null;
			});
	},

	_getObjects(query, params) {
		return this._connection
			.queryAsync(query, {id: params.id})
			.then(rows => {
				return rows && rows.length > 0 ? rows : null;
			});
	}
}

module.exports = function (connection) {
	var messageDB = Object.create(MessageDB);
	messageDB._connection = connection;
	return messageDB;
}

/*
	getSearchResults (keyword, platform, direction, type, time, domain, friend, tags)
		return this._getObjects(
			(
				SELECT * FROM MESSAGE m
				JOIN LINKS l ON m.link_id = l.link_id
				JOIN LINKS_TAGS lt ON l.link_id = lt.link_id
				JOIN TAGS t ON lt.tag_id = t.tag_link
				JOIN DOMAIN d ON l.domain_id = d.domain_id
				JOIN PLATFORM p ON l.platform_id = p.platform_id
				WHERE CONTAINS(m.note, :key)
				AND CONTAINS (t.tag_text, @tags)
				AND p.platform_id = :platform_id
				//Need to get platform id
				//direction
				AND l.type == :type
				//time
				AND d.domain_name = :domain
				AND m.sender_id = :friend OR m.recipient_id = :friend
				// Need to retrieve friend id
			),
			{
				keyword: keyword,
				platform: platform,
				direction: direction,
				type: type,
				time: time,
				domain: domain,
				friend: friend,
				tags: tags
			}
		);
	},
	*/<|endoftext|>


<commit_before>Meteor.publishComposite("groups", {
    find: function () {
        return Groups.find();
    },
    children: [{
        find: function (group) {
            return Meteor.users.find({
                groups: group.name
            }, {
                fields: {
                    profile: 1
                }
            });
        }
    }]
});
<commit_msg>fix groups publication<commit_after>Meteor.publishComposite("groups", {
    find: function () {
        return Groups.find();
    }
});
<|endoftext|>


<commit_before>import React from "react";
import { shallow } from "enzyme";
import AppState from "../AppState";
import {
  projects as demoProjects,
  labels as demoLabels
} from "../../demo-data";
import App from "../App";

const jsonFile = [
  {
    id: "1",
    title: "Coffee Swirl",
    person: "Joe Lemon",
    date: "2017-03-15",
    progress: 13,
    health: "ontrack"
  },
  {
    id: "2",
    title: "Coffee Swirl",
    person: "Joe Lemon",
    date: "2017-03-15",
    progress: 13,
    health: "ontrack",
    tags: ["Apple"]
  }
];

const db = jest.fn();
const readFileAsText = jest.fn(() => JSON.stringify(jsonFile));
const appState = shallow(<AppState db={db} readFileAsText={readFileAsText} />);

it("uses the demo data as it's default projects state", () => {
  expect(appState.state("title")).toBe("Demo Dashboard");
  expect(appState.state("projects")).toMatchSnapshot();
  expect(appState.state("labels")).toMatchSnapshot();
});

it("has no error message by default", () => {
  expect(appState.state("errorMessage")).toBeFalsy();
});

it("passes the projects (with embedded label info) to it's child <App />", () => {
  appState.setState({
    projects: demoProjects,
    labels: demoLabels
  });
  expect(appState.find(App).prop("projects")).toEqual([
    {
      id: "1",
      title: "Coffee Swirl",
      person: "Joe Lemon",
      time: "2017-03-15T10:54:04.445Z",
      progress: 13,
      status: "ontrack",
      labels: [
        {
          id: "3",
          initial: "A",
          colour: "#EF5350",
          title: "Apple"
        }
      ]
    },
    {
      id: "2",
      title: "Rake Twister",
      person: "Alex Apple",
      time: "2017-04-12T10:54:04.445Z",
      progress: 50,
      status: "onhold",
      labels: [
        {
          id: "3",
          initial: "A",
          colour: "#EF5350",
          title: "Apple"
        },
        {
          id: "4",
          initial: "O",
          colour: "#7E57C2",
          title: "Orange"
        }
      ]
    }
  ]);
});

it("passes the title state to it's child <App />", () => {
  const title = appState.state("title");
  expect(appState.find(App).prop("title")).toBe(title);
});

it("passess the errorMessage state to it's child <App />", () => {
  appState.setState({ errorMessage: "Error message" });
  expect(appState.find(App).prop("errorMessage")).toBe("Error message");
});

it("updates the projects state if the dropped file is json", async function() {
  const mockFile = { MOCK: "FILE" };
  await appState.instance().handleFileDrop(mockFile);
  expect(readFileAsText).toHaveBeenCalledWith(mockFile);
  expect(appState.state("errorMessage")).toBeFalsy();
  expect(appState.state("title")).toBe(null);
  expect(appState.state("projects").length).toEqual(2);
  expect(appState.state("labels").length).toBe(1);
});

it("sets the errorMessage state if the dropped file is invalid", async function() {
  const mockFile = { MOCK: "FILE" };
  readFileAsText.mockImplementationOnce(() =>
    JSON.stringify("this is not a projects file"));
  await appState.instance().handleFileDrop(mockFile);
  expect(readFileAsText).toHaveBeenCalledWith(mockFile);
  expect(appState.state("errorMessage")).toMatchSnapshot();
});
<commit_msg>Test AppState in case where there are no labels<commit_after>import React from "react";
import { shallow } from "enzyme";
import omit from "lodash/omit";
import AppState from "../AppState";
import {
  projects as demoProjects,
  labels as demoLabels
} from "../../demo-data";
import App from "../App";

const jsonFile = [
  {
    id: "1",
    title: "Coffee Swirl",
    person: "Joe Lemon",
    date: "2017-03-15",
    progress: 13,
    health: "ontrack"
  },
  {
    id: "2",
    title: "Coffee Swirl",
    person: "Joe Lemon",
    date: "2017-03-15",
    progress: 13,
    health: "ontrack",
    tags: ["Apple"]
  }
];

const db = jest.fn();
const readFileAsText = jest.fn(() => JSON.stringify(jsonFile));
const appState = shallow(<AppState db={db} readFileAsText={readFileAsText} />);

it("uses the demo data as it's default projects state", () => {
  expect(appState.state("title")).toBe("Demo Dashboard");
  expect(appState.state("projects")).toMatchSnapshot();
  expect(appState.state("labels")).toMatchSnapshot();
});

it("has no error message by default", () => {
  expect(appState.state("errorMessage")).toBeFalsy();
});

it("passes the projects (with embedded label info) to it's child <App />", () => {
  appState.setState({
    projects: demoProjects,
    labels: demoLabels
  });
  expect(appState.find(App).prop("projects")).toEqual([
    {
      id: "1",
      title: "Coffee Swirl",
      person: "Joe Lemon",
      time: "2017-03-15T10:54:04.445Z",
      progress: 13,
      status: "ontrack",
      labels: [
        {
          id: "3",
          initial: "A",
          colour: "#EF5350",
          title: "Apple"
        }
      ]
    },
    {
      id: "2",
      title: "Rake Twister",
      person: "Alex Apple",
      time: "2017-04-12T10:54:04.445Z",
      progress: 50,
      status: "onhold",
      labels: [
        {
          id: "3",
          initial: "A",
          colour: "#EF5350",
          title: "Apple"
        },
        {
          id: "4",
          initial: "O",
          colour: "#7E57C2",
          title: "Orange"
        }
      ]
    }
  ]);
});

it("passes no label info with projects (if there's no labels) to it's child <App />", () => {
  appState.setState({
    projects: demoProjects.map(project => omit(project, "labels"))
  });
  expect(appState.find(App).prop("projects")).toEqual([
    {
      id: "1",
      title: "Coffee Swirl",
      person: "Joe Lemon",
      time: "2017-03-15T10:54:04.445Z",
      progress: 13,
      status: "ontrack"
    },
    {
      id: "2",
      title: "Rake Twister",
      person: "Alex Apple",
      time: "2017-04-12T10:54:04.445Z",
      progress: 50,
      status: "onhold"
    }
  ]);
});

it("passes the title state to it's child <App />", () => {
  const title = appState.state("title");
  expect(appState.find(App).prop("title")).toBe(title);
});

it("passess the errorMessage state to it's child <App />", () => {
  appState.setState({ errorMessage: "Error message" });
  expect(appState.find(App).prop("errorMessage")).toBe("Error message");
});

it("updates the projects state if the dropped file is json", async function() {
  const mockFile = { MOCK: "FILE" };
  await appState.instance().handleFileDrop(mockFile);
  expect(readFileAsText).toHaveBeenCalledWith(mockFile);
  expect(appState.state("errorMessage")).toBeFalsy();
  expect(appState.state("title")).toBe(null);
  expect(appState.state("projects").length).toEqual(2);
  expect(appState.state("labels").length).toBe(1);
});

it("sets the errorMessage state if the dropped file is invalid", async function() {
  const mockFile = { MOCK: "FILE" };
  readFileAsText.mockImplementationOnce(() =>
    JSON.stringify("this is not a projects file"));
  await appState.instance().handleFileDrop(mockFile);
  expect(readFileAsText).toHaveBeenCalledWith(mockFile);
  expect(appState.state("errorMessage")).toMatchSnapshot();
});
<|endoftext|>


<commit_before>import React from 'react';
import XYGraph from '../XYGraph';
import { connect } from 'react-redux';
import * as d3 from 'd3';
import ReactTooltip from 'react-tooltip';

import {
    line,
    area
} from 'd3';

import { properties } from './default.config';

class AreaGraph extends XYGraph {

  constructor(props) {
    super(props, properties);
  }

  componentWillMount() {
    this.initiate(this.props);
  }

  componentDidMount() {
    this.elementGenerator();
    this.updateElements();
  }

  componentWillReceiveProps(nextProps) {
    if(this.props !== nextProps) {
        this.initiate(nextProps);
    }
  }

  componentDidUpdate() {
    this.updateElements();
  }

  initiate(props) {
    const {
        data,
    } = props;

    if (!data || !data.length)
        return;

    this.parseData();

    this.setDimensions(props, this.getData());
    this.updateLegend();
    this.configureAxis(this.getData());
  }

  getYColumns() {
    return this.yColumns;
  }

  getData() {
    return this.data;
  }

  getLegendConfig() {
    return this.legendConfig;
  }

  getDataNest() {
    return this.dataNest;
  }

  getGraph() {
    return this.getSVG().select('.graph-container');
  }

  handleShowEvent() {
    this.getSVG().select('.tooltip-line').style('opacity', 1);
  }

  handleHideEvent() {
    this.hoveredDatum = null;
    this.getSVG().select('.tooltip-line').style('opacity', 0);
  }

  updateTooltipConfiguration() {
    const {
      tooltip,
      linesColumn,
      yColumn
    } = this.getConfiguredProperties();

    const yColumns = this.getYColumns();

    let updatedTooltip = [];

    let insertTooltip = false;
    let format = null;

    if(tooltip) {
      tooltip.forEach(t => {

        //Checking whether need to insert all the dynamic values
        if([linesColumn, yColumn].indexOf(t.column) !== -1) {
          insertTooltip = true;

          //If Format is specified for metric column
          if(t.column == yColumn && t.format) {
            format = t.format;
          }
        } else {
          updatedTooltip.push(Object.assign({}, t));
        }

      })
    }

    if(insertTooltip) {
      yColumns.forEach(column => {
        updatedTooltip.push({
          column: column.key,
          format: format
        })
      })
    }

    this.setTooltipAccessor(updatedTooltip)
  }

  parseData() {
    let {
        data
    } = this.props;

    const {
        yColumn,
        xColumn,
        linesColumn
    } = this.getConfiguredProperties();

    this.data = [];

    if(linesColumn) {
      //Finding all the distinct lines
      this.yColumns = [...new Set(data.map(item => item[linesColumn]))]
        .map(d => ({key: d}));

      let groupedData = d3.nest()
        .key(function(d) {return d[xColumn];})
        .entries(data);

      this.data = [];
      groupedData.forEach(group => {
        //Generating the partial to append to each object will be used in tooltip
        let partial = {};
        group.values.forEach(d => {
          partial[d[linesColumn]] = d[yColumn];
        });

        group.values.forEach(d => {
          this.data.push(Object.assign({}, d, {
            yColumn: d[yColumn],
            columnType: d[linesColumn]
          }, partial));
        })
      })

      this.updateTooltipConfiguration();

    } else {

      this.yColumns = typeof yColumn === 'object' ? yColumn : [{ key: yColumn }];

      data.forEach((d) => {
        this.getYColumns().forEach((ld) => {
            if(d[ld.key] !== null) {
              this.data.push(Object.assign({
                  yColumn: d[ld.key] !== null ? d[ld.key] : 0,
                  columnType: ld.key
              }, d));
            }
        });
      });

    }

    //Nest the entries by symbol
    this.dataNest = d3.nest()
      .key(function(d) {return d.columnType;})
      .entries(this.data);

    //this.dataNest;

    return;
  }

  updateLegend() {

    const {
        chartHeightToPixel,
        chartWidthToPixel,
        circleToPixel,
        legend,
    } = this.getConfiguredProperties();

    if (legend.show)
    {
        const legendFn   = (d) => (d.value) ? d.value : d.key;
        let legendWidth  = legend.show && this.getYColumns().length >= 1 ? this.longestLabelLength(this.getYColumns(), legendFn) * chartWidthToPixel : 0;

        legend.width = legendWidth;

        // Compute the available space considering a legend
        if (this.checkIsVerticalLegend())
        {
            this.leftMargin      +=  legend.width;
            this.availableWidth  -=  legend.width;
        } else {
            const nbElementsPerLine  = parseInt(this.getAvailableWidth() / legend.width, 10);
            const nbLines            = parseInt(this.getYColumns().length / nbElementsPerLine, 10);
            this.availableHeight    -= nbLines * legend.circleSize * circleToPixel + chartHeightToPixel;
        }
    }
    this.legendConfig = legend;
  }

  // generate methods which helps to create charts
  elementGenerator() {

    const svg =  this.getGraph();

    //Add the X Axis
    svg.append('g')
      .attr('class', 'xAxis');

    //Add the Y Axis
    svg.append('g')
      .attr('class', 'yAxis');

    // tooltip line and circles
    svg.select('.tooltip-line').append('line')
      .attr('class', 'hover-line')
      .style('stroke', 'rgb(255,0,0)');

    // for generating transition
    svg.select('.area-chart')
     .append('clipPath')
       .attr('id', `clip-${this.getGraphId()}`)
      .append('rect')
        .attr('width', 0);

    // generate elements for X and Y titles
    this.generateAxisTitleElement();
  }

  // show circle if data length is 1.
  boundaryCircle() {

    const {
        circleRadius
    } = this.getConfiguredProperties();

    let boundaryCircle = this.getGraph()
      .select('.area-chart')
      .selectAll('.boundaryCircle')
      .data(this.getData());

    boundaryCircle.enter().append('circle')
        .attr('class', 'boundaryCircle')
        .style('fill', d => this.getColor({'key': d.columnType}))
        .attr('r', circleRadius)
      .merge(boundaryCircle)
        .attr('cx', d => this.getScale().x(d.xColumn))
        .attr('cy', d => this.getScale().y(d.yColumn));

    boundaryCircle.exit().remove();
  }

  //tooltip circles
  tooltipCircle(data = null) {
    const {
        circleRadius,
        xColumn
    } = this.getConfiguredProperties();

    const yScale = this.getScale().y;
    const cy     = data
      ? (d, i) => {
        return yScale(data[d.key])
      }
      : 1

    const tooltipCircle = this.getGraph()
        .select('.tooltip-line').selectAll('.tooltipCircle')
        .data(this.getDataNest(), d => d.key);

    tooltipCircle.enter().append('circle')
        .attr('class', 'tooltipCircle')
        .style('fill', d => this.getColor(d))
        .attr('r', circleRadius)
      .merge(tooltipCircle)
        .attr('cx', 1)
        .attr('cy', cy);

    tooltipCircle.exit().remove();
  }

  drawArea() {

    const {
      stroke,
      xColumn,
      transition
    } = this.getConfiguredProperties();

    const scale          = this.getScale();
    const availableHeight = this.getAvailableHeight();

    const lineGenerator = line()
      .x( d => scale.x(d[xColumn]))
      .y( d => scale.y(d.yColumn));

    const areaGenerator = area()
      .x( d => scale.x(d[xColumn]))
      .y0( d => availableHeight)
      .y1( d => scale.y(d.yColumn));

    const svg   =  this.getGraph();

    svg.select('.area-chart')
      .select(`#clip-${this.getGraphId()}`)
      .select('rect')
        .attr('height', availableHeight);

    const lines = svg.select('.area-chart')
        .selectAll('.line-block')
        .data(this.getDataNest(), d => d.key );

    const newLines = lines.enter().append('g')
        .attr('class', 'line-block');

    newLines.append('path')
        .attr('class', 'line')
        .style('fill', 'none')
        .style('strokeWidth', stroke.width)
        .attr('clip-path', `url(#clip-${this.getGraphId()})`);

    const allLines = newLines.merge(lines);

    allLines.select('.line')
        .style('stroke', d => this.getColor({'key': d.key}))
        .attr('d', d => lineGenerator(d.values));

    // Add area

    const areas = svg.select('.area-chart')
        .selectAll('.area-block')
        .data(this.getDataNest(), d => d.key );

    const newAreas = areas.enter()
       .append('g')
       .attr('class', 'area-block');

    newAreas.append('path')
        .attr('class', 'area')
        .attr('fill-opacity', stroke.opacity)
        .attr('clip-path', `url(#clip-${this.getGraphId()})`);

    const allAreas = newAreas.merge(areas);

    allAreas.select('.area')
        .style('fill', d => this.getColor({'key': d.key}))
        .attr('d', d => areaGenerator(d.values));

    // add transition effect
    svg.select(`#clip-${this.getGraphId()} rect`)
        .transition().duration(transition)
        .attr('width', this.availableWidth);

    lines.exit().remove();
    //areas.exit().remove();

  }

  // update data on props change or resizing
  updateElements() {
    const {
        data
    } = this.props

    const {
      xTickFontSize,
      yTickFontSize
    } = this.getConfiguredProperties();

    const svg =  this.getGraph();

    //Add the X Axis
    svg.select('.xAxis')
      .style('font-size', xTickFontSize)
      .attr('transform', 'translate(0,'+ this.getAvailableHeight() +')')
      .call(this.getAxis().x);

    //Add the Y Axis
    svg.select('.yAxis')
      .style('font-size', yTickFontSize)
      .call(this.getAxis().y);

    this.setAxisTitles();
    this.renderLegendIfNeeded();

    if(data.length === 1) {
      this.boundaryCircle();
    } else {

      this.drawArea();

      // update tooltip svg
      this.renderTooltip();

      //update hover line
      svg.select('.hover-line')
       .attr('y2', this.getAvailableHeight());

    }
  }

  renderLegendIfNeeded() {
    const {
      stroke,
      colors
    } = this.getConfiguredProperties();

    const label    = (d) => d.value ? d.value : d.key;
    const scale    = this.scaleColor(this.getYColumns(), 'key');
    this.getColor  = (d) => scale ? scale(d.key) : stroke.color || colors[0];

    this.renderNewLegend(this.getYColumns(), this.getLegendConfig(), this.getColor, label);
  }

  // Create tooltip data
  renderTooltip() {

    const {
      xColumn,
    } = this.getConfiguredProperties();

    let scale    = this.getScale();
    let bandwidth = this.getBandScale().x.bandwidth() * 0.8;

    const tooltip = this.getGraph()
        .select('.tooltip-section').selectAll('rect')
        .data(this.getData());

    const newTooltip = tooltip.enter().append('rect')
        .attr('data-for', this.tooltipId)
        .attr('data-effect', 'solid')
        .attr('data-tip', true)
        .attr('y', 0)
        .attr('width', bandwidth)
        .style('cursor', 'pointer')
        .style('opacity', 0)
        .style('fill', 'red');

    const allTooltip = newTooltip.merge(tooltip);

    allTooltip
        .attr('x', d => scale.x(d[xColumn]) - (bandwidth)/2)
        .attr('height', this.getAvailableHeight())
        .on('mouseover',  d  => this.updateVerticalLine(d))
        .on('mouseenter', d => this.hoveredDatum = d)
        .on('mousemove',  d  => this.hoveredDatum = d);

    tooltip.exit().remove();
  }

  //update vertical line on mouseover
  updateVerticalLine(data) {
    const {
        xColumn,
      } = this.getConfiguredProperties();

      ReactTooltip.rebuild();

      const rightMargin = this.getScale().x(data[xColumn]);
      this.tooltipCircle(data);
      this.getGraph()
       .select('.tooltip-line')
         .attr('transform', 'translate('+rightMargin+', 0)');
  }

  render() {

    const {
        data,
        width,
        height
    } = this.props;

    if (!data || !data.length)
        return;

    const {
        margin
    } = this.getConfiguredProperties();

    return (
        <div className='stacked-area-graph'>

            { this.tooltip }

            <svg width={width} height={height}>
              <g ref={node => this.node = node}>
                <g className='graph-container' transform={ `translate(${this.getLeftMargin()},${margin.top})` }>
                    <g className='area-chart'></g>
                    <g className='tooltip-line' transform={ `translate(0,0)` } style={{opacity : 0}}></g>
                    <g className='tooltip-section'></g>
                </g>
                <g className='axis-title'></g>
                <g className='legend'></g>
              </g>
            </svg>
        </div>
    );
  }
}

AreaGraph.propTypes = {
    configuration: React.PropTypes.object,
    response: React.PropTypes.object
};

const actionCreators = (dispatch) => ({
});

export default connect(null, actionCreators)(AreaGraph);<commit_msg>[bug] Tooltip issue<commit_after>import React from 'react';
import XYGraph from '../XYGraph';
import { connect } from 'react-redux';
import * as d3 from 'd3';
import ReactTooltip from 'react-tooltip';

import {
    line,
    area
} from 'd3';

import { properties } from './default.config';

class AreaGraph extends XYGraph {

  constructor(props) {
    super(props, properties);
  }

  componentWillMount() {
    this.initiate(this.props);
  }

  componentDidMount() {
    this.elementGenerator();
    this.updateElements();
  }

  componentWillReceiveProps(nextProps) {
    if(this.props !== nextProps) {
        this.initiate(nextProps);
    }
  }

  componentDidUpdate() {
    this.updateElements();
  }

  initiate(props) {
    const {
        data,
    } = props;

    if (!data || !data.length)
        return;

    this.parseData();

    this.setDimensions(props, this.getData());
    this.updateLegend();
    this.configureAxis(this.getData());
  }

  getYColumns() {
    return this.yColumns;
  }

  getData() {
    return this.data;
  }

  getLegendConfig() {
    return this.legendConfig;
  }

  getDataNest() {
    return this.dataNest;
  }

  getGraph() {
    return this.getSVG().select('.graph-container');
  }

  handleShowEvent() {
    this.getSVG().select('.tooltip-line').style('opacity', 1);
  }

  handleHideEvent() {
    this.hoveredDatum = null;
    this.getSVG().select('.tooltip-line').style('opacity', 0);
  }

  updateTooltipConfiguration() {
    const {
      tooltip,
      linesColumn,
      yColumn
    } = this.getConfiguredProperties();

    const yColumns = this.getYColumns();

    let updatedTooltip = [];

    let insertTooltip = false;
    let format = null;

    if(tooltip) {
      tooltip.forEach(t => {

        //Checking whether need to insert all the dynamic values
        if([linesColumn, yColumn].indexOf(t.column) !== -1) {
          insertTooltip = true;

          //If Format is specified for metric column
          if(t.column == yColumn && t.format) {
            format = t.format;
          }
        } else {
          updatedTooltip.push(Object.assign({}, t));
        }

      })

      if(insertTooltip) {
        yColumns.forEach(column => {
          updatedTooltip.push({
            column: column.key,
            format: format
          })
        })
      }

      this.setTooltipAccessor(updatedTooltip)
    }

  }

  parseData() {
    let {
        data
    } = this.props;

    const {
        yColumn,
        xColumn,
        linesColumn
    } = this.getConfiguredProperties();

    this.data = [];

    if(linesColumn) {
      //Finding all the distinct lines
      this.yColumns = [...new Set(data.map(item => item[linesColumn]))]
        .map(d => ({key: d}));

      let groupedData = d3.nest()
        .key(function(d) {return d[xColumn];})
        .entries(data);

      this.data = [];
      groupedData.forEach(group => {
        //Generating the partial to append to each object will be used in tooltip
        let partial = {};
        group.values.forEach(d => {
          partial[d[linesColumn]] = d[yColumn];
        });

        group.values.forEach(d => {
          this.data.push(Object.assign({}, d, {
            yColumn: d[yColumn],
            columnType: d[linesColumn]
          }, partial));
        })
      })

      this.updateTooltipConfiguration();

    } else {

      this.yColumns = typeof yColumn === 'object' ? yColumn : [{ key: yColumn }];

      data.forEach((d) => {
        this.getYColumns().forEach((ld) => {
            if(d[ld.key] !== null) {
              this.data.push(Object.assign({
                  yColumn: d[ld.key] !== null ? d[ld.key] : 0,
                  columnType: ld.key
              }, d));
            }
        });
      });

    }

    //Nest the entries by symbol
    this.dataNest = d3.nest()
      .key(function(d) {return d.columnType;})
      .entries(this.data);

    //this.dataNest;

    return;
  }

  updateLegend() {

    const {
        chartHeightToPixel,
        chartWidthToPixel,
        circleToPixel,
        legend,
    } = this.getConfiguredProperties();

    if (legend.show)
    {
        const legendFn   = (d) => (d.value) ? d.value : d.key;
        let legendWidth  = legend.show && this.getYColumns().length >= 1 ? this.longestLabelLength(this.getYColumns(), legendFn) * chartWidthToPixel : 0;

        legend.width = legendWidth;

        // Compute the available space considering a legend
        if (this.checkIsVerticalLegend())
        {
            this.leftMargin      +=  legend.width;
            this.availableWidth  -=  legend.width;
        } else {
            const nbElementsPerLine  = parseInt(this.getAvailableWidth() / legend.width, 10);
            const nbLines            = parseInt(this.getYColumns().length / nbElementsPerLine, 10);
            this.availableHeight    -= nbLines * legend.circleSize * circleToPixel + chartHeightToPixel;
        }
    }
    this.legendConfig = legend;
  }

  // generate methods which helps to create charts
  elementGenerator() {

    const svg =  this.getGraph();

    //Add the X Axis
    svg.append('g')
      .attr('class', 'xAxis');

    //Add the Y Axis
    svg.append('g')
      .attr('class', 'yAxis');

    // tooltip line and circles
    svg.select('.tooltip-line').append('line')
      .attr('class', 'hover-line')
      .style('stroke', 'rgb(255,0,0)');

    // for generating transition
    svg.select('.area-chart')
     .append('clipPath')
       .attr('id', `clip-${this.getGraphId()}`)
      .append('rect')
        .attr('width', 0);

    // generate elements for X and Y titles
    this.generateAxisTitleElement();
  }

  // show circle if data length is 1.
  boundaryCircle() {

    const {
        circleRadius
    } = this.getConfiguredProperties();

    let boundaryCircle = this.getGraph()
      .select('.area-chart')
      .selectAll('.boundaryCircle')
      .data(this.getData());

    boundaryCircle.enter().append('circle')
        .attr('class', 'boundaryCircle')
        .style('fill', d => this.getColor({'key': d.columnType}))
        .attr('r', circleRadius)
      .merge(boundaryCircle)
        .attr('cx', d => this.getScale().x(d.xColumn))
        .attr('cy', d => this.getScale().y(d.yColumn));

    boundaryCircle.exit().remove();
  }

  //tooltip circles
  tooltipCircle(data = null) {
    const {
        circleRadius,
        xColumn
    } = this.getConfiguredProperties();

    const yScale = this.getScale().y;
    const cy     = data
      ? (d, i) => {
        return yScale(data[d.key])
      }
      : 1

    const tooltipCircle = this.getGraph()
        .select('.tooltip-line').selectAll('.tooltipCircle')
        .data(this.getDataNest(), d => d.key);

    tooltipCircle.enter().append('circle')
        .attr('class', 'tooltipCircle')
        .style('fill', d => this.getColor(d))
        .attr('r', circleRadius)
      .merge(tooltipCircle)
        .attr('cx', 1)
        .attr('cy', cy);

    tooltipCircle.exit().remove();
  }

  drawArea() {

    const {
      stroke,
      xColumn,
      transition
    } = this.getConfiguredProperties();

    const scale          = this.getScale();
    const availableHeight = this.getAvailableHeight();

    const lineGenerator = line()
      .x( d => scale.x(d[xColumn]))
      .y( d => scale.y(d.yColumn));

    const areaGenerator = area()
      .x( d => scale.x(d[xColumn]))
      .y0( d => availableHeight)
      .y1( d => scale.y(d.yColumn));

    const svg   =  this.getGraph();

    svg.select('.area-chart')
      .select(`#clip-${this.getGraphId()}`)
      .select('rect')
        .attr('height', availableHeight);

    const lines = svg.select('.area-chart')
        .selectAll('.line-block')
        .data(this.getDataNest(), d => d.key );

    const newLines = lines.enter().append('g')
        .attr('class', 'line-block');

    newLines.append('path')
        .attr('class', 'line')
        .style('fill', 'none')
        .style('strokeWidth', stroke.width)
        .attr('clip-path', `url(#clip-${this.getGraphId()})`);

    const allLines = newLines.merge(lines);

    allLines.select('.line')
        .style('stroke', d => this.getColor({'key': d.key}))
        .attr('d', d => lineGenerator(d.values));

    // Add area

    const areas = svg.select('.area-chart')
        .selectAll('.area-block')
        .data(this.getDataNest(), d => d.key );

    const newAreas = areas.enter()
       .append('g')
       .attr('class', 'area-block');

    newAreas.append('path')
        .attr('class', 'area')
        .attr('fill-opacity', stroke.opacity)
        .attr('clip-path', `url(#clip-${this.getGraphId()})`);

    const allAreas = newAreas.merge(areas);

    allAreas.select('.area')
        .style('fill', d => this.getColor({'key': d.key}))
        .attr('d', d => areaGenerator(d.values));

    // add transition effect
    svg.select(`#clip-${this.getGraphId()} rect`)
        .transition().duration(transition)
        .attr('width', this.availableWidth);

    lines.exit().remove();
    //areas.exit().remove();

  }

  // update data on props change or resizing
  updateElements() {
    const {
        data
    } = this.props

    const {
      xTickFontSize,
      yTickFontSize
    } = this.getConfiguredProperties();

    const svg =  this.getGraph();

    //Add the X Axis
    svg.select('.xAxis')
      .style('font-size', xTickFontSize)
      .attr('transform', 'translate(0,'+ this.getAvailableHeight() +')')
      .call(this.getAxis().x);

    //Add the Y Axis
    svg.select('.yAxis')
      .style('font-size', yTickFontSize)
      .call(this.getAxis().y);

    this.setAxisTitles();
    this.renderLegendIfNeeded();

    if(data.length === 1) {
      this.boundaryCircle();
    } else {

      this.drawArea();

      // update tooltip svg
      this.renderTooltip();

      //update hover line
      svg.select('.hover-line')
       .attr('y2', this.getAvailableHeight());

    }
  }

  renderLegendIfNeeded() {
    const {
      stroke,
      colors
    } = this.getConfiguredProperties();

    const label    = (d) => d.value ? d.value : d.key;
    const scale    = this.scaleColor(this.getYColumns(), 'key');
    this.getColor  = (d) => scale ? scale(d.key) : stroke.color || colors[0];

    this.renderNewLegend(this.getYColumns(), this.getLegendConfig(), this.getColor, label);
  }

  // Create tooltip data
  renderTooltip() {

    const {
      xColumn,
    } = this.getConfiguredProperties();

    let scale    = this.getScale();
    let bandwidth = this.getBandScale().x.bandwidth() * 0.8;

    const tooltip = this.getGraph()
        .select('.tooltip-section').selectAll('rect')
        .data(this.getData());

    const newTooltip = tooltip.enter().append('rect')
        .attr('data-for', this.tooltipId)
        .attr('data-effect', 'solid')
        .attr('data-tip', true)
        .attr('y', 0)
        .attr('width', bandwidth)
        .style('cursor', 'pointer')
        .style('opacity', 0)
        .style('fill', 'red');

    const allTooltip = newTooltip.merge(tooltip);

    allTooltip
        .attr('x', d => scale.x(d[xColumn]) - (bandwidth)/2)
        .attr('height', this.getAvailableHeight())
        .on('mouseover',  d  => this.updateVerticalLine(d))
        .on('mouseenter', d => this.hoveredDatum = d)
        .on('mousemove',  d  => this.hoveredDatum = d);

    tooltip.exit().remove();
  }

  //update vertical line on mouseover
  updateVerticalLine(data) {
    const {
        xColumn,
      } = this.getConfiguredProperties();

      ReactTooltip.rebuild();

      const rightMargin = this.getScale().x(data[xColumn]);
      this.tooltipCircle(data);
      this.getGraph()
       .select('.tooltip-line')
         .attr('transform', 'translate('+rightMargin+', 0)');
  }

  render() {

    const {
        data,
        width,
        height
    } = this.props;

    if (!data || !data.length)
        return;

    const {
        margin
    } = this.getConfiguredProperties();

    return (
        <div className='stacked-area-graph'>

            { this.tooltip }

            <svg width={width} height={height}>
              <g ref={node => this.node = node}>
                <g className='graph-container' transform={ `translate(${this.getLeftMargin()},${margin.top})` }>
                    <g className='area-chart'></g>
                    <g className='tooltip-line' transform={ `translate(0,0)` } style={{opacity : 0}}></g>
                    <g className='tooltip-section'></g>
                </g>
                <g className='axis-title'></g>
                <g className='legend'></g>
              </g>
            </svg>
        </div>
    );
  }
}

AreaGraph.propTypes = {
    configuration: React.PropTypes.object,
    response: React.PropTypes.object
};

const actionCreators = (dispatch) => ({
});

export default connect(null, actionCreators)(AreaGraph);<|endoftext|>


<commit_before>import React from 'react';
import ReactDOM from 'react-dom';

import { connect } from 'react-redux';

import immutableToJs from 'utils/immutableToJs';

import player from 'state/models/player';
import level from 'state/models/level';

import { toMove } from 'state/actions/move';

import Player from 'components/Player/Player';

const groundToType = Object.freeze({
  doorway: 'dancer',
  road: 'bike',
  roadline: 'bike',
  sidewalk: 'personWalk',
  sky: 'chopper',
  water: 'speedboat',
});

function mapStateToProps(state) {
  const col = player.getCol(state);
  const row = player.getRow(state);
  const direction = player.getDirection(state);
  const groundType = level.groundAt(col, row).get('type');
  const type = groundToType[groundType] || 'person';
  return {
    col,
    row,
    direction,
    type
  };
}

function mapDispatchToProps(dispatch) {
  return {
    onMove(direction) {
      dispatch(toMove(direction));
    }
  };
};

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(Player);
<commit_msg>Pass state when getting ground type<commit_after>import React from 'react';
import ReactDOM from 'react-dom';

import { connect } from 'react-redux';

import immutableToJs from 'utils/immutableToJs';

import player from 'state/models/player';
import level from 'state/models/level';

import { toMove } from 'state/actions/move';

import Player from 'components/Player/Player';

const groundToType = Object.freeze({
  doorway: 'dancer',
  road: 'bike',
  roadline: 'bike',
  sidewalk: 'personWalk',
  sky: 'chopper',
  water: 'speedboat',
});

function mapStateToProps(state) {
  const col = player.getCol(state);
  const row = player.getRow(state);
  const direction = player.getDirection(state);
  const groundType = level.groundAt(col, row, state).get('type');
  const type = groundToType[groundType] || 'person';
  return {
    col,
    row,
    direction,
    type
  };
}

function mapDispatchToProps(dispatch) {
  return {
    onMove(direction) {
      dispatch(toMove(direction));
    }
  };
};

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(Player);
<|endoftext|>


<commit_before>import laserSndData from '../sounds/laser1.wav'; // laserSndData is a data URL (by virtue of Webpack's URL loader which will load wav Data <10k size as data URLs)
import explosion1SndData from '../sounds/explosion1.mp3';
import explosion2SndData from '../sounds/explosion2.mp3';
import explosion3SndData from '../sounds/explosion3.mp3';

export default class SoundCollection {
	constructor() {
		this.buffers = {};
		try {
			window.AudioContext = window.AudioContext || window.webkitAudioContext; // Fix up prefixing
			this.audioContext = new AudioContext();
			this.soundOK = true;
		}
		catch(err) {
			console.log('error: unable to load sound system: ', err);
			this.soundOK = false;
			return;
		}
		if(this.soundOK) {
			this._loadSounds();
		}
	}

	_loadSounds() {
		this._loadSound(laserSndData, 'laserSnd');
		this._loadSound(explosion1SndData, 'explosion1Snd');
		this._loadSound(explosion2SndData, 'explosion2Snd');
		this._loadSound(explosion3SndData, 'explosion3Snd');
	}

	_loadSound(url, sndName) {
		let request = new XMLHttpRequest();
  	request.open('GET', url, true);
  	request.responseType = 'arraybuffer'; 	// audio buffer needs to be in arraybuffer format for use with Sound API

		request.onload = () => {
			this.audioContext.decodeAudioData(request.response, (buffer) => {
				this.buffers[sndName] = buffer;
			}, () => console.log('could not load ', sndName));
		}
  	request.send();
	}

	_playSound(sndName) {

		if(!this.soundOK) return;
		let buffer = this.buffers[sndName];
		if (buffer === undefined) return;

		let source = this.audioContext.createBufferSource();
		source.buffer = buffer;
		source.connect(this.audioContext.destination);
		source.start(0);
	}

	fire(direction) {
		this._playSound('laserSnd');
	}

	playExplosion1() {
			this._playSound('explosion1Snd');
	}

	playExplosion2() {
			this._playSound('explosion2Snd');
	}

	playExplosion3() {
			console.log('explosion3');
			this._playSound('explosion3Snd');
	}

}

<commit_msg>Added more sounds<commit_after>import laserSndData from '../sounds/laser1.wav'; // laserSndData is a data URL (by virtue of Webpack's URL loader which will load wav Data <10k size as data URLs)
import explosion1SndData from '../sounds/explosion1.mp3';
import explosion2SndData from '../sounds/explosion2.mp3';
import explosion3SndData from '../sounds/explosion3.mp3';

export default class SoundCollection {
	constructor() {
		this.buffers = {};
		try {
			window.AudioContext = window.AudioContext || window.webkitAudioContext; // Fix up prefixing
			this.audioContext = new AudioContext();
			this.soundOK = true;
		}
		catch(err) {
			console.log('error: unable to load sound system: ', err);
			this.soundOK = false;
			return;
		}
		if(this.soundOK) {
			this._loadSounds();
		}
	}

	_loadSounds() {
		this._loadSound(laserSndData, 'laserSnd');
		this._loadSound(explosion1SndData, 'explosion1Snd');
		this._loadSound(explosion2SndData, 'explosion2Snd');
		this._loadSound(explosion3SndData, 'explosion3Snd');
	}

	_loadSound(url, sndName) {
		let request = new XMLHttpRequest();
  	request.open('GET', url, true);
  	request.responseType = 'arraybuffer'; 	// audio buffer needs to be in arraybuffer format for use with Sound API

		request.onload = () => {
			this.audioContext.decodeAudioData(request.response, (buffer) => {
				this.buffers[sndName] = buffer;
			}, () => console.log('could not load ', sndName));
		}
  	request.send();
	}

	_playSound(sndName) {

		if(!this.soundOK) return;
		let buffer = this.buffers[sndName];
		if (buffer === undefined) return;

		let source = this.audioContext.createBufferSource();
		source.buffer = buffer;
		source.connect(this.audioContext.destination);
		source.start(0);
	}

	fire(direction) {
		this._playSound('laserSnd');
	}

	playExplosion1() {
			this._playSound('explosion1Snd');
	}

	playExplosion2() {
			this._playSound('explosion2Snd');
	}

	playExplosion3() {
			this._playSound('explosion3Snd');
	}

}

<|endoftext|>


<commit_before>import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';

import Box from '../box';
import { Select } from '../select';
import { NumericInput } from '../input';
import theme from './theme.css';

const currentYear = new Date().getFullYear();
const fromMonth = new Date(currentYear, 0);
const toMonth = new Date(currentYear + 10, 11);

const MonthPicker = ({ date, localeUtils, onChange, small }) => {
  const [selectedMonth, setSelectedMonth] = useState({
    value: date.getMonth(),
    label: localeUtils.formatMonthTitle(date),
  });
  const [selectedYear, setSelectedYear] = useState(date.getFullYear());

  const months = localeUtils.getMonths().map((monthName, index) => {
    return { value: index, label: monthName };
  });

  const years = [];
  for (let i = fromMonth.getFullYear(); i <= toMonth.getFullYear(); i += 1) {
    years.push(i);
  }

  useEffect(() => {
    setSelectedMonth({ value: date.getMonth(), label: localeUtils.formatMonthTitle(date) });
    setSelectedYear(date.getFullYear());
  }, [date]);

  const handleChangeMonth = selectedMonth => {
    onChange(new Date(selectedYear, selectedMonth.value));
  };

  const handleChangeYear = (_, selectedYear) => {
    onChange(new Date(selectedYear, selectedMonth.value));
  };

  return (
    <Box className={theme['caption']}>
      <Box display="flex" justifyContent="center">
        <Select
          value={selectedMonth}
          className={theme['month-picker-field']}
          options={months}
          onChange={handleChangeMonth}
          width={small ? 88 : 112}
          size="small"
        />
        <NumericInput
          value={`${selectedYear}`}
          className={theme['month-picker-field']}
          onChange={handleChangeYear}
          width={small ? 72 : 80}
          size="small"
        />
      </Box>
    </Box>
  );
};

MonthPicker.propTypes = {
  /** Current date */
  date: PropTypes.bool,
  /** Callback function that is fired when the month has changed. */
  onChange: PropTypes.func,
  /** The localeUtils from the DatePicker */
  localeUtils: PropTypes.instanceOf(Date),
};

export default MonthPicker;
<commit_msg>💄 Only show first 3 letters of selected month<commit_after>import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';

import Box from '../box';
import { Select } from '../select';
import { NumericInput } from '../input';
import theme from './theme.css';

const currentYear = new Date().getFullYear();
const fromMonth = new Date(currentYear, 0);
const toMonth = new Date(currentYear + 10, 11);

const formatSelectedMonth = ({ label, value }) => ({ value, label: label.substring(0, 3) });

const MonthPicker = ({ date, localeUtils, onChange, small }) => {
  const [selectedMonth, setSelectedMonth] = useState({
    value: date.getMonth(),
    label: localeUtils.formatMonthTitle(date),
  });
  const [selectedYear, setSelectedYear] = useState(date.getFullYear());

  const months = localeUtils.getMonths().map((monthName, index) => {
    return { value: index, label: monthName };
  });

  const years = [];
  for (let i = fromMonth.getFullYear(); i <= toMonth.getFullYear(); i += 1) {
    years.push(i);
  }

  useEffect(() => {
    setSelectedMonth({ value: date.getMonth(), label: localeUtils.formatMonthTitle(date) });
    setSelectedYear(date.getFullYear());
  }, [date]);

  const handleChangeMonth = selectedMonth => {
    onChange(new Date(selectedYear, selectedMonth.value));
  };

  const handleChangeYear = (_, selectedYear) => {
    onChange(new Date(selectedYear, selectedMonth.value));
  };

  return (
    <Box className={theme['caption']}>
      <Box display="flex" justifyContent="center">
        <Select
          value={formatSelectedMonth(selectedMonth)}
          className={theme['month-picker-field']}
          options={months}
          onChange={handleChangeMonth}
          width={small ? 88 : 112}
          size="small"
        />
        <NumericInput
          value={`${selectedYear}`}
          className={theme['month-picker-field']}
          onChange={handleChangeYear}
          width={small ? 72 : 80}
          size="small"
        />
      </Box>
    </Box>
  );
};

MonthPicker.propTypes = {
  /** Current date */
  date: PropTypes.bool,
  /** Callback function that is fired when the month has changed. */
  onChange: PropTypes.func,
  /** The localeUtils from the DatePicker */
  localeUtils: PropTypes.instanceOf(Date),
};

export default MonthPicker;
<|endoftext|>


<commit_before>/* eslint unicorn/filename-case: "off", func-names: "off", camelcase: "off", no-unused-vars: "off" */

module.exports = function (iface) {
    const {mqttPub, mqttSub, mqttStatus, log, Service, Characteristic} = iface;

    return function createService_FilterMaintenance(acc, settings, subtype) {
        acc.addService(Service.FilterMaintenance, settings.name, subtype)
            .getCharacteristic(Characteristic.FilterChangeIndication)
            .on('get', callback => {
                const val = mqttStatus[settings.topic.statusFilterChangeIndication];
                log.debug('< hap get', settings.name, 'FilterChangeIndication');
                log.debug('> hap re_get', settings.name, val);
                callback(null, val);
            });

        mqttSub(settings.topic.statusFilterChangeIndication, val => {
            log.debug('> hap update', settings.name, 'FilterChangeIndication', val);
            acc.getService(subtype)
                .updateCharacteristic(Characteristic.FilterChangeIndication, val);
        });

        if (settings.topic.statusFilterLifeLevel) {
            acc.getService(subtype)
                .getCharacteristic(Characteristic.FilterLifeLevel)
                .on('get', callback => {
                    log.debug('< hap get', settings.name, 'FilterLifeLevel');
                    const level = mqttStatus[settings.topic.statusFilterLifeLevel];

                    log.debug('> hap re_get', settings.name, 'FilterLifeLevel', level);
                    callback(null, level);
                });

            mqttSub(settings.topic.statusFilterLifeLevel, val => {
                log.debug('> hap update', settings.name, 'FilterLifeLevel', val);
                acc.getService(subtype)
                    .updateCharacteristic(Characteristic.FilterLifeLevel, val);
            });
        }

        if (settings.topic.setResetFilterIndication) {
            acc.getService(subtype)
                .getCharacteristic(Characteristic.ResetFilterIndication)
                .on('set', (value, callback) => {
                    log.debug('< hap set', settings.name, 'ResetFilterIndication', value);
                    mqttPub(settings.topic.setResetFilterIndication, value);
                    callback();
                });
        }
    };
};
<commit_msg>refactor FilterMaintenance (#126)<commit_after>/* eslint unicorn/filename-case: "off", func-names: "off", camelcase: "off", no-unused-vars: "off" */

module.exports = function (iface) {
    const {mqttPub, mqttSub, mqttStatus, log, Service, Characteristic} = iface;

    return function createService_FilterMaintenance(acc, settings, subtype) {
        acc.addService(Service.FilterMaintenance, settings.name, subtype);

        const obj = {acc, settings, subtype};

        require('../characteristics')('FilterChangeIndication', obj, iface);
        require('../characteristics')('FilterLifeLevel', obj, iface);
        require('../characteristics')('ResetFilterIndication', obj, iface);
    };
};
<|endoftext|>


<commit_before>const R = require('ramda');
const knex = require('knex')({ client: 'mysql' });

// Useful for creating extra if-conditions for non-admins
const ifNotAdmin = (role, str) =>
  R.ifElse(R.equals('admin'), R.always(''), R.always(str))(role);

/**
  ATTENTION:
  all those SQL-esque helpers like whereAnd, inClause, etc.
  are subject to be obsolete. We are planning to migrate to
  a dedicated SQL-builder lib (knex)
* */

// Creates a WHERE statement with AND inbetween non-empty conditions
const whereAnd = whereConds =>
  R.compose(
    R.reduce((ret, str) => {
      if (ret === '') {
        return `WHERE ${str}`;
      }
      return `${ret} AND ${str}`;
    }, ''),
    R.filter(R.compose(R.not, R.isEmpty)),
  )(whereConds);

// Creates an IN clause like this: $field IN (val1,val2,val3)
// or on empty values: $field IN (NULL)
const inClause = (field, values) =>
  R.compose(
    str => `${field} IN (${str})`,
    R.ifElse(R.isEmpty, R.always('NULL'), R.identity),
    R.join(','),
    R.defaultTo([]),
  )(values);

const inClauseOr = conds =>
  R.compose(
    R.reduce((ret, str) => {
      if (ret === '') {
        return str;
      }
      return `${ret} OR ${str}`;
    }, ''),
    R.map(([field, values]) => inClause(field, values)),
  )(conds);

// Promise wrapper for doing sql queries
const query = (sqlClient, sql) =>
  new Promise((resolve, reject) => {
    sqlClient.query(sql, (err, rows) => {
      if (err) {
        reject(err);
      }
      resolve(rows);
    });
    setTimeout(() => {
      reject('Timeout while talking to the Database');
    }, 2000);
  });

// We use this just for consistency of the api calls
const prepare = (sqlClient, sql) => sqlClient.prepare(sql);

const isPatchEmpty = R.compose(R.isEmpty, R.propOr({}, 'patch'));

// Tells if a user tries to modify an sshKey in the given patch
// payload
const hasSshKeyPatch = R.compose(
  R.anyPass([R.has('key_value'), R.has('key_type')]),
  R.propOr({}, 'patch'),
);

module.exports = {
  ifNotAdmin,
  inClause,
  inClauseOr,
  knex,
  prepare,
  query,
  whereAnd,
  isPatchEmpty,
  hasSshKeyPatch,
};
<commit_msg>Format<commit_after>const R = require('ramda');
const knex = require('knex')({ client: 'mysql' });

// Useful for creating extra if-conditions for non-admins
const ifNotAdmin = (role, str) =>
  R.ifElse(R.equals('admin'), R.always(''), R.always(str))(role);

/**
  ATTENTION:
  all those SQL-esque helpers like whereAnd, inClause, etc.
  are subject to be obsolete. We are planning to migrate to
  a dedicated SQL-builder lib (knex)
* */

// Creates a WHERE statement with AND inbetween non-empty conditions
const whereAnd = whereConds =>
  R.compose(
    R.reduce((ret, str) => {
      if (ret === '') {
        return `WHERE ${str}`;
      }
      return `${ret} AND ${str}`;
    }, ''),
    R.filter(
      R.compose(
        R.not,
        R.isEmpty,
      ),
    ),
  )(whereConds);

// Creates an IN clause like this: $field IN (val1,val2,val3)
// or on empty values: $field IN (NULL)
const inClause = (field, values) =>
  R.compose(
    str => `${field} IN (${str})`,
    R.ifElse(R.isEmpty, R.always('NULL'), R.identity),
    R.join(','),
    R.defaultTo([]),
  )(values);

const inClauseOr = conds =>
  R.compose(
    R.reduce((ret, str) => {
      if (ret === '') {
        return str;
      }
      return `${ret} OR ${str}`;
    }, ''),
    R.map(([field, values]) => inClause(field, values)),
  )(conds);

// Promise wrapper for doing sql queries
const query = (sqlClient, sql) =>
  new Promise((resolve, reject) => {
    sqlClient.query(sql, (err, rows) => {
      if (err) {
        reject(err);
      }
      resolve(rows);
    });
    setTimeout(() => {
      reject('Timeout while talking to the Database');
    }, 2000);
  });

// We use this just for consistency of the api calls
const prepare = (sqlClient, sql) => sqlClient.prepare(sql);

const isPatchEmpty = R.compose(
  R.isEmpty,
  R.propOr({}, 'patch'),
);

// Tells if a user tries to modify an sshKey in the given patch
// payload
const hasSshKeyPatch = R.compose(
  R.anyPass([R.has('key_value'), R.has('key_type')]),
  R.propOr({}, 'patch'),
);

module.exports = {
  ifNotAdmin,
  inClause,
  inClauseOr,
  knex,
  prepare,
  query,
  whereAnd,
  isPatchEmpty,
  hasSshKeyPatch,
};
<|endoftext|>


<commit_before>/**
 * @ngdoc service
 * @name Attr2MapOptions
 * @description
 *   Converts tag attributes to options used by google api v3 objects
 */
/* global google */
(function() {
  'use strict';

  //i.e. "2015-08-12T06:12:40.858Z"
  var isoDateRE =
    /^(\d{4}\-\d\d\-\d\d([tT][\d:\.]*)?)([zZ]|([+\-])(\d\d):?(\d\d))?$/;

  var Attr2MapOptions = function(
      $parse, $timeout, $log, NavigatorGeolocation, GeoCoder,
      camelCaseFilter, jsonizeFilter
    ) {

    /**
     * Returns the attributes of an element as hash
     * @memberof Attr2MapOptions
     * @param {HTMLElement} el html element
     * @returns {Hash} attributes
     */
    var orgAttributes = function(el) {
      (el.length > 0) && (el = el[0]);
      var orgAttributes = {};
      for (var i=0; i<el.attributes.length; i++) {
        var attr = el.attributes[i];
        orgAttributes[attr.name] = attr.value;
      }
      return orgAttributes;
    };

    var getJSON = function(input) {
      var re =/^[\+\-]?[0-9\.]+,[ ]*\ ?[\+\-]?[0-9\.]+$/; //lat,lng
      if (input.match(re)) {
        input = "["+input+"]";
      }
      return JSON.parse(jsonizeFilter(input));
    };

    var getLatLng = function(input) {
      var output = input;
      if (input[0].constructor == Array) { // [[1,2],[3,4]]
        output = input.map(function(el) {
          return new google.maps.LatLng(el[0], el[1]);
        });
      } else if(!isNaN(parseFloat(input[0])) && isFinite(input[0])) {
        output = new google.maps.LatLng(output[0], output[1]);
      }
      return output;
    };

    var toOptionValue = function(input, options) {
      var output;
      try { // 1. Number?
        output = getNumber(input);
      } catch(err) {
        try { // 2. JSON?
          var output = getJSON(input);
          if (output instanceof Array) {
            // [{a:1}] : not lat/lng ones
            if (output[0].constructor == Object) {
              output = output;
            } else { // [[1,2],[3,4]] or [1,2]
              output = getLatLng(output);
            }
          }
          // JSON is an object (not array or null)
          else if (output === Object(output)) {
            // check for nested hashes and convert to Google API options
            var newOptions = options;
            newOptions.doNotConverStringToNumber = true;
            output = getOptions(output, newOptions);
          }
        } catch(err2) {
          // 3. Google Map Object function Expression. i.e. LatLng(80,-49)
          if (input.match(/^[A-Z][a-zA-Z0-9]+\(.*\)$/)) {
            try {
              var exp = "new google.maps."+input;
              output = eval(exp); /* jshint ignore:line */
            } catch(e) {
              output = input;
            }
          // 4. Google Map Object constant Expression. i.e. MayTypeId.HYBRID
          } else if (input.match(/^([A-Z][a-zA-Z0-9]+)\.([A-Z]+)$/)) {
            try {
              var matches = input.match(/^([A-Z][a-zA-Z0-9]+)\.([A-Z]+)$/);
              output = google.maps[matches[1]][matches[2]];
            } catch(e) {
              output = input;
            }
          // 5. Google Map Object constant Expression. i.e. HYBRID
          } else if (input.match(/^[A-Z]+$/)) {
            try {
              var capitalizedKey = options.key.charAt(0).toUpperCase() +
                options.key.slice(1);
              if (options.key.match(/temperatureUnit|windSpeedUnit|labelColor/)) {
                capitalizedKey = capitalizedKey.replace(/s$/,"");
                output = google.maps.weather[capitalizedKey][input];
              } else {
                output = google.maps[capitalizedKey][input];
              }
            } catch(e) {
              output = input;
            }
          // 6. Date Object as ISO String
          } else if (input.match(isoDateRE)) {
            try {
              output = new Date(input);
            } catch(e) {
              output = input;
            }
          // 7. evaluate dynamically bound values
          } else if (input.match(/^{/) && options.scope) {
            try {
              var expr = input.replace(/{{/,'').replace(/}}/g,'');
              output = options.scope.$eval(expr);
            } catch (err) {
              output = input;
            }
          } else {
            output = input;
          }
        } // catch(err2)
      } // catch(err)

      // convert output more for center and position
      if (
        (options.key == 'center' || options.key == 'center') &&
        output instanceof Array
      ) {
        output = new google.maps.LatLng(output[0], output[1]);
      }

      // convert output more for shape bounds
      if (options.key == 'bounds' && output instanceof Array) {
        output = new google.maps.LatLngBounds(output[0], output[1]);
      }

      // convert output more for shape icons
      if (options.key == 'icons' && output instanceof Array) {

        for (var i=0; i<output.length; i++) {
          var el = output[i];
          if (el.icon.path.match(/^[A-Z_]+$/)) {
            el.icon.path =  google.maps.SymbolPath[el.icon.path];
          }
        }
      }

      // convert output more for marker icon
      if (options.key == 'icon' && output instanceof Object) {
        if ((""+output.path).match(/^[A-Z_]+$/)) {
          output.path = google.maps.SymbolPath[output.path];
        }
        for (var key in output) { //jshint ignore:line
          var arr = output[key];
          if (key == "anchor" || key == "origin" || key == "labelOrigin") {
            output[key] = new google.maps.Point(arr[0], arr[1]);
          } else if (key == "size" || key == "scaledSize") {
            output[key] = new google.maps.Size(arr[0], arr[1]);
          }
        }
      }

      return output;
    };

    var getAttrsToObserve = function(attrs) {
      var attrsToObserve = [];

      if (!attrs.noWatcher) {
        for (var attrName in attrs) { //jshint ignore:line
          var attrValue = attrs[attrName];
          if (attrValue && attrValue.match(/\{\{.*\}\}/)) { // if attr value is {{..}}
            attrsToObserve.push(camelCaseFilter(attrName));
          }
        }
      }

      return attrsToObserve;
    };

    /**
     * filters attributes by skipping angularjs methods $.. $$..
     * @memberof Attr2MapOptions
     * @param {Hash} attrs tag attributes
     * @returns {Hash} filterd attributes
     */
    var filter = function(attrs) {
      var options = {};
      for(var key in attrs) {
        if (key.match(/^\$/) || key.match(/^ng[A-Z]/)) {
          void(0);
        } else {
          options[key] = attrs[key];
        }
      }
      return options;
    };

    /**
     * converts attributes hash to Google Maps API v3 options
     * ```
     *  . converts numbers to number
     *  . converts class-like string to google maps instance
     *    i.e. `LatLng(1,1)` to `new google.maps.LatLng(1,1)`
     *  . converts constant-like string to google maps constant
     *    i.e. `MapTypeId.HYBRID` to `google.maps.MapTypeId.HYBRID`
     *    i.e. `HYBRID"` to `google.maps.MapTypeId.HYBRID`
     * ```
     * @memberof Attr2MapOptions
     * @param {Hash} attrs tag attributes
     * @param {Hash} options
     * @returns {Hash} options converted attributess
     */
    var getOptions = function(attrs, params) {
      params = params || {};
      var options = {};
      for(var key in attrs) {
        if (attrs[key] || attrs[key] === 0) {
          if (key.match(/^on[A-Z]/)) { //skip events, i.e. on-click
            continue;
          } else if (key.match(/ControlOptions$/)) { // skip controlOptions
            continue;
          } else {
            // nested conversions need to be typechecked
            // (non-strings are fully converted)
            if (typeof attrs[key] !== 'string') {
              options[key] = attrs[key];
            } else {
              if (params.doNotConverStringToNumber &&
                attrs[key].match(/^[0-9]+$/)
              ) {
                options[key] = attrs[key];
              } else {
                options[key] = toOptionValue(attrs[key], {key: key, scope: params.scope});
              }
            }
          }
        } // if (attrs[key])
      } // for(var key in attrs)
      return options;
    };

    /**
     * converts attributes hash to scope-specific event function
     * @memberof Attr2MapOptions
     * @param {scope} scope angularjs scope
     * @param {Hash} attrs tag attributes
     * @returns {Hash} events converted events
     */
    var getEvents = function(scope, attrs) {
      var events = {};
      var toLowercaseFunc = function($1){
        return "_"+$1.toLowerCase();
      };
      var EventFunc = function(attrValue) {
        // funcName(argsStr)
        var matches = attrValue.match(/([^\(]+)\(([^\)]*)\)/);
        var funcName = matches[1];
        var argsStr = matches[2].replace(/event[ ,]*/,'');  //remove string 'event'
        var argsExpr = $parse("["+argsStr+"]"); //for perf when triggering event
        return function(event) {
          var args = argsExpr(scope); //get args here to pass updated model values
          function index(obj,i) {return obj[i];}
          var f = funcName.split('.').reduce(index, scope);
          f && f.apply(this, [event].concat(args));
          $timeout( function() {
            scope.$apply();
          });
        };
      };

      for(var key in attrs) {
        if (attrs[key]) {
          if (!key.match(/^on[A-Z]/)) { //skip if not events
            continue;
          }

          //get event name as underscored. i.e. zoom_changed
          var eventName = key.replace(/^on/,'');
          eventName = eventName.charAt(0).toLowerCase() + eventName.slice(1);
          eventName = eventName.replace(/([A-Z])/g, toLowercaseFunc);

          var attrValue = attrs[key];
          events[eventName] = new EventFunc(attrValue);
        }
      }
      return events;
    };

    /**
     * control means map controls, i.e streetview, pan, etc, not a general control
     * @memberof Attr2MapOptions
     * @param {Hash} filtered filtered tag attributes
     * @returns {Hash} Google Map options
     */
    var getControlOptions = function(filtered) {
      var controlOptions = {};
      if (typeof filtered != 'object') {
        return false;
      }

      for (var attr in filtered) {
        if (filtered[attr]) {
          if (!attr.match(/(.*)ControlOptions$/)) {
            continue; // if not controlOptions, skip it
          }

          //change invalid json to valid one, i.e. {foo:1} to {"foo": 1}
          var orgValue = filtered[attr];
          var newValue = orgValue.replace(/'/g, '"');
          newValue = newValue.replace(/([^"]+)|("[^"]+")/g, function($0, $1, $2) {
            if ($1) {
              return $1.replace(/([a-zA-Z0-9]+?):/g, '"$1":');
            } else {
              return $2;
            }
          });
          try {
            var options = JSON.parse(newValue);
            for (var key in options) { //assign the right values
              if (options[key]) {
                var value = options[key];
                if (typeof value === 'string') {
                  value = value.toUpperCase();
                } else if (key === "mapTypeIds") {
                  value = value.map( function(str) {
                    if (str.match(/^[A-Z]+$/)) { // if constant
                      return google.maps.MapTypeId[str.toUpperCase()];
                    } else { // else, custom map-type
                      return str;
                    }
                  });
                }

                if (key === "style") {
                  var str = attr.charAt(0).toUpperCase() + attr.slice(1);
                  var objName = str.replace(/Options$/,'')+"Style";
                  options[key] = google.maps[objName][value];
                } else if (key === "position") {
                  options[key] = google.maps.ControlPosition[value];
                } else {
                  options[key] = value;
                }
              }
            }
            controlOptions[attr] = options;
          } catch (e) {
            console.error('invald option for', attr, newValue, e, e.stack);
          }
        }
      } // for

      return controlOptions;
    };

    return {
      filter: filter,
      getOptions: getOptions,
      getEvents: getEvents,
      getControlOptions: getControlOptions,
      toOptionValue: toOptionValue,
      getAttrsToObserve: getAttrsToObserve,
      orgAttributes: orgAttributes
    }; // return

  };
  Attr2MapOptions.$inject= [
    '$parse', '$timeout', '$log', 'NavigatorGeolocation', 'GeoCoder',
    'camelCaseFilter', 'jsonizeFilter'
  ];

  angular.module('ngMap').service('Attr2MapOptions', Attr2MapOptions);
})();
<commit_msg>Fixed bug #511<commit_after>/**
 * @ngdoc service
 * @name Attr2MapOptions
 * @description
 *   Converts tag attributes to options used by google api v3 objects
 */
/* global google */
(function() {
  'use strict';

  //i.e. "2015-08-12T06:12:40.858Z"
  var isoDateRE =
    /^(\d{4}\-\d\d\-\d\d([tT][\d:\.]*)?)([zZ]|([+\-])(\d\d):?(\d\d))?$/;

  var Attr2MapOptions = function(
      $parse, $timeout, $log, NavigatorGeolocation, GeoCoder,
      camelCaseFilter, jsonizeFilter
    ) {

    /**
     * Returns the attributes of an element as hash
     * @memberof Attr2MapOptions
     * @param {HTMLElement} el html element
     * @returns {Hash} attributes
     */
    var orgAttributes = function(el) {
      (el.length > 0) && (el = el[0]);
      var orgAttributes = {};
      for (var i=0; i<el.attributes.length; i++) {
        var attr = el.attributes[i];
        orgAttributes[attr.name] = attr.value;
      }
      return orgAttributes;
    };

    var getJSON = function(input) {
      var re =/^[\+\-]?[0-9\.]+,[ ]*\ ?[\+\-]?[0-9\.]+$/; //lat,lng
      if (input.match(re)) {
        input = "["+input+"]";
      }
      return JSON.parse(jsonizeFilter(input));
    };

    var getLatLng = function(input) {
      var output = input;
      if (input[0].constructor == Array) {
        if ((input[0][0].constructor == Array && input[0][0].length == 2) || input[0][0].constructor == Object) {
            var preoutput;
            var outputArray = [];
            for (var i = 0; i < input.length; i++) {
                preoutput = input[i].map(function(el){
                    return new google.maps.LatLng(el[0], el[1]);
                });
                outputArray.push(preoutput);
            }
            output = outputArray;
        } else {
            output = input.map(function(el) {
                return new google.maps.LatLng(el[0], el[1]);
            });
        }
      } else if (!isNaN(parseFloat(input[0])) && isFinite(input[0])) {
        output = new google.maps.LatLng(output[0], output[1]);
      }
      return output;
    };

    var toOptionValue = function(input, options) {
      var output;
      try { // 1. Number?
        output = getNumber(input);
      } catch(err) {
        try { // 2. JSON?
          var output = getJSON(input);
          if (output instanceof Array) {
            if (output[0].constructor == Object) {
              output = output;
            } else if (output[0] instanceof Array) {
              if (output[0][0].constructor == Object) {
                output = output;
              } else {
                output = getLatLng(output);
              }
            } else {
                output = getLatLng(output);
            }
          }
          // JSON is an object (not array or null)
          else if (output === Object(output)) {
            // check for nested hashes and convert to Google API options
            var newOptions = options;
            newOptions.doNotConverStringToNumber = true;
            output = getOptions(output, newOptions);
          }
        } catch(err2) {
          // 3. Google Map Object function Expression. i.e. LatLng(80,-49)
          if (input.match(/^[A-Z][a-zA-Z0-9]+\(.*\)$/)) {
            try {
              var exp = "new google.maps."+input;
              output = eval(exp); /* jshint ignore:line */
            } catch(e) {
              output = input;
            }
          // 4. Google Map Object constant Expression. i.e. MayTypeId.HYBRID
          } else if (input.match(/^([A-Z][a-zA-Z0-9]+)\.([A-Z]+)$/)) {
            try {
              var matches = input.match(/^([A-Z][a-zA-Z0-9]+)\.([A-Z]+)$/);
              output = google.maps[matches[1]][matches[2]];
            } catch(e) {
              output = input;
            }
          // 5. Google Map Object constant Expression. i.e. HYBRID
          } else if (input.match(/^[A-Z]+$/)) {
            try {
              var capitalizedKey = options.key.charAt(0).toUpperCase() +
                options.key.slice(1);
              if (options.key.match(/temperatureUnit|windSpeedUnit|labelColor/)) {
                capitalizedKey = capitalizedKey.replace(/s$/,"");
                output = google.maps.weather[capitalizedKey][input];
              } else {
                output = google.maps[capitalizedKey][input];
              }
            } catch(e) {
              output = input;
            }
          // 6. Date Object as ISO String
          } else if (input.match(isoDateRE)) {
            try {
              output = new Date(input);
            } catch(e) {
              output = input;
            }
          // 7. evaluate dynamically bound values
          } else if (input.match(/^{/) && options.scope) {
            try {
              var expr = input.replace(/{{/,'').replace(/}}/g,'');
              output = options.scope.$eval(expr);
            } catch (err) {
              output = input;
            }
          } else {
            output = input;
          }
        } // catch(err2)
      } // catch(err)

      // convert output more for center and position
      if (
        (options.key == 'center' || options.key == 'center') &&
        output instanceof Array
      ) {
        output = new google.maps.LatLng(output[0], output[1]);
      }

      // convert output more for shape bounds
      if (options.key == 'bounds' && output instanceof Array) {
        output = new google.maps.LatLngBounds(output[0], output[1]);
      }

      // convert output more for shape icons
      if (options.key == 'icons' && output instanceof Array) {

        for (var i=0; i<output.length; i++) {
          var el = output[i];
          if (el.icon.path.match(/^[A-Z_]+$/)) {
            el.icon.path =  google.maps.SymbolPath[el.icon.path];
          }
        }
      }

      // convert output more for marker icon
      if (options.key == 'icon' && output instanceof Object) {
        if ((""+output.path).match(/^[A-Z_]+$/)) {
          output.path = google.maps.SymbolPath[output.path];
        }
        for (var key in output) { //jshint ignore:line
          var arr = output[key];
          if (key == "anchor" || key == "origin" || key == "labelOrigin") {
            output[key] = new google.maps.Point(arr[0], arr[1]);
          } else if (key == "size" || key == "scaledSize") {
            output[key] = new google.maps.Size(arr[0], arr[1]);
          }
        }
      }

      return output;
    };

    var getAttrsToObserve = function(attrs) {
      var attrsToObserve = [];

      if (!attrs.noWatcher) {
        for (var attrName in attrs) { //jshint ignore:line
          var attrValue = attrs[attrName];
          if (attrValue && attrValue.match(/\{\{.*\}\}/)) { // if attr value is {{..}}
            attrsToObserve.push(camelCaseFilter(attrName));
          }
        }
      }

      return attrsToObserve;
    };

    /**
     * filters attributes by skipping angularjs methods $.. $$..
     * @memberof Attr2MapOptions
     * @param {Hash} attrs tag attributes
     * @returns {Hash} filterd attributes
     */
    var filter = function(attrs) {
      var options = {};
      for(var key in attrs) {
        if (key.match(/^\$/) || key.match(/^ng[A-Z]/)) {
          void(0);
        } else {
          options[key] = attrs[key];
        }
      }
      return options;
    };

    /**
     * converts attributes hash to Google Maps API v3 options
     * ```
     *  . converts numbers to number
     *  . converts class-like string to google maps instance
     *    i.e. `LatLng(1,1)` to `new google.maps.LatLng(1,1)`
     *  . converts constant-like string to google maps constant
     *    i.e. `MapTypeId.HYBRID` to `google.maps.MapTypeId.HYBRID`
     *    i.e. `HYBRID"` to `google.maps.MapTypeId.HYBRID`
     * ```
     * @memberof Attr2MapOptions
     * @param {Hash} attrs tag attributes
     * @param {Hash} options
     * @returns {Hash} options converted attributess
     */
    var getOptions = function(attrs, params) {
      params = params || {};
      var options = {};
      for(var key in attrs) {
        if (attrs[key] || attrs[key] === 0) {
          if (key.match(/^on[A-Z]/)) { //skip events, i.e. on-click
            continue;
          } else if (key.match(/ControlOptions$/)) { // skip controlOptions
            continue;
          } else {
            // nested conversions need to be typechecked
            // (non-strings are fully converted)
            if (typeof attrs[key] !== 'string') {
              options[key] = attrs[key];
            } else {
              if (params.doNotConverStringToNumber &&
                attrs[key].match(/^[0-9]+$/)
              ) {
                options[key] = attrs[key];
              } else {
                options[key] = toOptionValue(attrs[key], {key: key, scope: params.scope});
              }
            }
          }
        } // if (attrs[key])
      } // for(var key in attrs)
      return options;
    };

    /**
     * converts attributes hash to scope-specific event function
     * @memberof Attr2MapOptions
     * @param {scope} scope angularjs scope
     * @param {Hash} attrs tag attributes
     * @returns {Hash} events converted events
     */
    var getEvents = function(scope, attrs) {
      var events = {};
      var toLowercaseFunc = function($1){
        return "_"+$1.toLowerCase();
      };
      var EventFunc = function(attrValue) {
        // funcName(argsStr)
        var matches = attrValue.match(/([^\(]+)\(([^\)]*)\)/);
        var funcName = matches[1];
        var argsStr = matches[2].replace(/event[ ,]*/,'');  //remove string 'event'
        var argsExpr = $parse("["+argsStr+"]"); //for perf when triggering event
        return function(event) {
          var args = argsExpr(scope); //get args here to pass updated model values
          function index(obj,i) {return obj[i];}
          var f = funcName.split('.').reduce(index, scope);
          f && f.apply(this, [event].concat(args));
          $timeout( function() {
            scope.$apply();
          });
        };
      };

      for(var key in attrs) {
        if (attrs[key]) {
          if (!key.match(/^on[A-Z]/)) { //skip if not events
            continue;
          }

          //get event name as underscored. i.e. zoom_changed
          var eventName = key.replace(/^on/,'');
          eventName = eventName.charAt(0).toLowerCase() + eventName.slice(1);
          eventName = eventName.replace(/([A-Z])/g, toLowercaseFunc);

          var attrValue = attrs[key];
          events[eventName] = new EventFunc(attrValue);
        }
      }
      return events;
    };

    /**
     * control means map controls, i.e streetview, pan, etc, not a general control
     * @memberof Attr2MapOptions
     * @param {Hash} filtered filtered tag attributes
     * @returns {Hash} Google Map options
     */
    var getControlOptions = function(filtered) {
      var controlOptions = {};
      if (typeof filtered != 'object') {
        return false;
      }

      for (var attr in filtered) {
        if (filtered[attr]) {
          if (!attr.match(/(.*)ControlOptions$/)) {
            continue; // if not controlOptions, skip it
          }

          //change invalid json to valid one, i.e. {foo:1} to {"foo": 1}
          var orgValue = filtered[attr];
          var newValue = orgValue.replace(/'/g, '"');
          newValue = newValue.replace(/([^"]+)|("[^"]+")/g, function($0, $1, $2) {
            if ($1) {
              return $1.replace(/([a-zA-Z0-9]+?):/g, '"$1":');
            } else {
              return $2;
            }
          });
          try {
            var options = JSON.parse(newValue);
            for (var key in options) { //assign the right values
              if (options[key]) {
                var value = options[key];
                if (typeof value === 'string') {
                  value = value.toUpperCase();
                } else if (key === "mapTypeIds") {
                  value = value.map( function(str) {
                    if (str.match(/^[A-Z]+$/)) { // if constant
                      return google.maps.MapTypeId[str.toUpperCase()];
                    } else { // else, custom map-type
                      return str;
                    }
                  });
                }

                if (key === "style") {
                  var str = attr.charAt(0).toUpperCase() + attr.slice(1);
                  var objName = str.replace(/Options$/,'')+"Style";
                  options[key] = google.maps[objName][value];
                } else if (key === "position") {
                  options[key] = google.maps.ControlPosition[value];
                } else {
                  options[key] = value;
                }
              }
            }
            controlOptions[attr] = options;
          } catch (e) {
            console.error('invald option for', attr, newValue, e, e.stack);
          }
        }
      } // for

      return controlOptions;
    };

    return {
      filter: filter,
      getOptions: getOptions,
      getEvents: getEvents,
      getControlOptions: getControlOptions,
      toOptionValue: toOptionValue,
      getAttrsToObserve: getAttrsToObserve,
      orgAttributes: orgAttributes
    }; // return

  };
  Attr2MapOptions.$inject= [
    '$parse', '$timeout', '$log', 'NavigatorGeolocation', 'GeoCoder',
    'camelCaseFilter', 'jsonizeFilter'
  ];

  angular.module('ngMap').service('Attr2MapOptions', Attr2MapOptions);
})();
<|endoftext|>