diff --git a/LICENSE.md b/LICENSE.md old mode 100644 new mode 100755 diff --git a/README.md b/README.md old mode 100644 new mode 100755 index a8af2b0..19f5d43 --- a/README.md +++ b/README.md @@ -4,4 +4,9 @@ Silk icon set 1.3 - Mark James
[http://www.famfamfam.com/lab/icons/silk/](http://www.famfamfam.com/lab/icons/silk/) -The JIRA icon is directly from [Atlassian Software](https://www.atlassian.com/). \ No newline at end of file +The JIRA icon is directly from [Atlassian Software](https://www.atlassian.com/). + +**Contributors** + +David Wolverton +Vodori (https://www.vodori.com/) \ No newline at end of file diff --git a/bigtime.js b/bigtime.js old mode 100644 new mode 100755 diff --git a/bower.json b/bower.json old mode 100644 new mode 100755 index d1aac26..a255e76 --- a/bower.json +++ b/bower.json @@ -1,11 +1,11 @@ { "name": "mytime", - "version": "0.0", + "version": "1.0", "dependencies": { - "dojo": "1.8.4", - "dojox": "1.8.4", - "dijit": "1.8.4", - "lodash": "2.4.1" + "dojo": "1.10", + "dojox": "1.10", + "dijit": "1.10", + "lodash": "3.8.0" }, "devDependencies": { "mocha": "*", diff --git a/docs/notes.txt b/docs/notes.txt old mode 100644 new mode 100755 diff --git a/docs/testing-notes.txt b/docs/testing-notes.txt old mode 100644 new mode 100755 diff --git a/images/jira-icon.png b/images/jira-icon.png old mode 100644 new mode 100755 diff --git a/index.html b/index.html old mode 100644 new mode 100755 index 2e5a729..bb1c9c5 --- a/index.html +++ b/index.html @@ -1,8 +1,10 @@ - + + - Time Logger + My Time + @@ -13,11 +15,9 @@ "> -
-
-
- +
+ \ No newline at end of file diff --git a/script/bootstrap.js b/script/bootstrap.js old mode 100644 new mode 100755 index ae32fad..442736c --- a/script/bootstrap.js +++ b/script/bootstrap.js @@ -15,7 +15,7 @@ require({ packages: [ { name: 'mytime', location: '../../script/mytime' }, - { name: 'lodash', location: '../lodash/dist', main: 'lodash.min' } + { name: 'lodash', location: '../lodash', main: 'lodash.min' } ] }, ['mytime/debug-helper', 'dojo/parser', 'dojo/ready'].concat(main), function(_1, parser, ready) { ready(function() { diff --git a/script/mytime/command/CreateTaskCommand.js b/script/mytime/command/CreateTaskCommand.js old mode 100644 new mode 100755 diff --git a/script/mytime/command/CreateTimeEntryCommand.js b/script/mytime/command/CreateTimeEntryCommand.js old mode 100644 new mode 100755 diff --git a/script/mytime/command/DeleteTaskCommand.js b/script/mytime/command/DeleteTaskCommand.js old mode 100644 new mode 100755 diff --git a/script/mytime/command/DeleteTimeEntryCommand.js b/script/mytime/command/DeleteTimeEntryCommand.js old mode 100644 new mode 100755 diff --git a/script/mytime/command/GetJiraPickListCommand.js b/script/mytime/command/GetJiraPickListCommand.js new file mode 100755 index 0000000..10b6adc --- /dev/null +++ b/script/mytime/command/GetJiraPickListCommand.js @@ -0,0 +1,12 @@ +/** + * @license + * Copyright 2014 David Wolverton + * Available under MIT license + */ +define(["./_Command"], +function (_Command) { + return _Command.makeCommand({ + commandTopic: "integration/jira/list", + query: null + }); +}); \ No newline at end of file diff --git a/script/mytime/command/UpdateJiraIntegrationCommand.js b/script/mytime/command/UpdateJiraIntegrationCommand.js new file mode 100755 index 0000000..b02a7db --- /dev/null +++ b/script/mytime/command/UpdateJiraIntegrationCommand.js @@ -0,0 +1,12 @@ +/** + * @license + * Copyright 2014 David Wolverton + * Available under MIT license + */ +define(["./_Command"], +function (_Command) { + return _Command.makeCommand({ + commandTopic: "time-entry/update", + timeEntry: null + }); +}); \ No newline at end of file diff --git a/script/mytime/command/UpdateTaskCommand.js b/script/mytime/command/UpdateTaskCommand.js old mode 100644 new mode 100755 diff --git a/script/mytime/command/UpdateTimeEntryCommand.js b/script/mytime/command/UpdateTimeEntryCommand.js old mode 100644 new mode 100755 diff --git a/script/mytime/command/_Command.js b/script/mytime/command/_Command.js old mode 100644 new mode 100755 diff --git a/script/mytime/controller/JiraController.js b/script/mytime/controller/JiraController.js new file mode 100755 index 0000000..6ba9e59 --- /dev/null +++ b/script/mytime/controller/JiraController.js @@ -0,0 +1,28 @@ +define(['dojo/_base/lang', 'dojo/_base/declare', "dijit/Destroyable", + "mytime/command/GetJiraPicklistCommand", "mytime/rest/GetJiraPicklistRequest"], +function (lang, declare, Destroyable, + GetJiraPicklistCommand, GetJiraPicklistRequest) { + + return declare([Destroyable], { + + requestQueue: null, + + constructor: function(args) { + lang.mixin(this, args); + this.own( + GetJiraPicklistCommand.subscribe(lang.hitch(this, "handlePicklist")) + ); + }, + + handlePicklist: function(command) { + var request = new GetJiraPicklistRequest(command.query); + request.then(lang.hitch(command, 'resolve')); + this.requestQueue.push(request); + }, + + handleUpdate: function(command) { + + } + + }); +}); \ No newline at end of file diff --git a/script/mytime/controller/TaskController.js b/script/mytime/controller/TaskController.js old mode 100644 new mode 100755 index 911f7f4..a45e1fb --- a/script/mytime/controller/TaskController.js +++ b/script/mytime/controller/TaskController.js @@ -8,12 +8,14 @@ define([ "mytime/model/modelRegistry", "mytime/model/Task", "mytime/command/CreateTaskCommand", "mytime/command/UpdateTaskCommand", "mytime/command/DeleteTaskCommand", + "mytime/rest/PutTaskRequest", "mytime/rest/DeleteTaskRequest", "mytime/controller/_CrudController", "mytime/util/syncFrom", "mytime/util/ColorGenerator" ], function( lang, declare, when, modelRegistry, Task, CreateTaskCommand, UpdateTaskCommand, DeleteTaskCommand, + PutTaskRequest, DeleteTaskRequest, _CrudController, syncFrom, ColorGenerator ) { @@ -35,8 +37,10 @@ define([ colorGenerator: null, timeEntryStore: null, + requestQueue: null, - constructor: function() { + constructor: function(args) { + lang.mixin(this, args); this.colorGenerator = new ColorGenerator(); this.own( syncFrom(modelRegistry, "taskStore", this, "store") ); this.own( syncFrom(modelRegistry, "timeEntryStore", this) ); @@ -61,6 +65,18 @@ define([ this.timeEntryStore.put(timeEntry); }, this); })); + }, + + _afterCreate: function(command, task) { + //this.requestQueue.push(new PutTaskRequest(task)); + }, + + _afterUpdate: function(command, task) { + //this.requestQueue.push(new PutTaskRequest(task)); + }, + + _afterDelete: function(command, task) { + //this.requestQueue.push(new DeleteTaskRequest(task)); } }); diff --git a/script/mytime/controller/TimeEntryController.js b/script/mytime/controller/TimeEntryController.js old mode 100644 new mode 100755 index ac3358f..4e67f40 --- a/script/mytime/controller/TimeEntryController.js +++ b/script/mytime/controller/TimeEntryController.js @@ -4,16 +4,18 @@ * Available under MIT license */ define([ - "dojo/_base/declare", + "dojo/_base/declare", "dojo/_base/lang", "mytime/model/modelRegistry", "mytime/model/TimeEntry", "mytime/command/CreateTimeEntryCommand", "mytime/command/UpdateTimeEntryCommand", "mytime/command/DeleteTimeEntryCommand", + "mytime/rest/PutEntryRequest", "mytime/rest/DeleteEntryRequest", "mytime/controller/_CrudController", "mytime/util/syncFrom" ], function( - declare, + declare, lang, modelRegistry, TimeEntry, CreateTimeEntryCommand, UpdateTimeEntryCommand, DeleteTimeEntryCommand, + PutEntryRequest, DeleteEntryRequest, _CrudController, syncFrom ) { @@ -32,7 +34,8 @@ define([ objectTypeStringForMessages: "time entry", storageKey: "timeEntryStore", - constructor: function() { + constructor: function(args) { + lang.mixin(this, args); this.own( syncFrom(modelRegistry, "timeEntryStore", this, "store") ); }, @@ -56,6 +59,18 @@ define([ entry.endHour = swap; } return true; + }, + + _afterCreate: function(command, entry) { + //this.requestQueue.push(new PutEntryRequest(entry)); + }, + + _afterUpdate: function(command, entry) { + //this.requestQueue.push(new PutEntryRequest(entry)); + }, + + _afterDelete: function(command, entry) { + //this.requestQueue.push(new DeleteEntryRequest(entry)); } }); diff --git a/script/mytime/controller/_CrudController.js b/script/mytime/controller/_CrudController.js old mode 100644 new mode 100755 index d0dc567..a6f13e6 --- a/script/mytime/controller/_CrudController.js +++ b/script/mytime/controller/_CrudController.js @@ -48,18 +48,19 @@ define([ if (!this.store) { command.reject(this._getCommandError("Cannot add {} before system is initialized.")); } else { - var entry = new this.objectTypeConstructor(command[this.commandObjectProperty]); - entry.set("id", IdGenerator.nextIdForType(this.objectTypeName)); + var object = new this.objectTypeConstructor(command[this.commandObjectProperty]); + object.set("id", IdGenerator.nextIdForType(this.objectTypeName)); - this._beforeCreate(command, entry); + this._beforeCreate(command, object); if (command.isFulfilled()) { return; } - console.log("PUT NEW " + JSON.stringify(entry)); - this.store.put(entry); - command.resolve(this._getCommandResult(entry)); + console.log("PUT NEW " + JSON.stringify(object)); + this.store.put(object); + command.resolve(this._getCommandResult(object)); this._persistStore(); + this._afterCreate(command, object); } }, @@ -68,10 +69,19 @@ define([ * resolve or reject the command. * * @param {Object} command - * @param {Object} entry new object about to be created. an instance of objectTypeConstructor + * @param {Object} object new object about to be created. an instance of objectTypeConstructor * @private */ - _beforeCreate: function(command, entry) {}, + _beforeCreate: function(command, object) {}, + + /** + * Override this to extend behavior. Called after creating a new object. + * + * @param {Object} command + * @param {Object} object new object created. an instance of objectTypeConstructor + * @private + */ + _afterCreate: function(command, object) {}, handleUpdate: function(command) { if (!this.store) { @@ -79,22 +89,23 @@ define([ } else { var updateObject = command[this.commandObjectProperty]; var id = updateObject.id; - var existingEntry = this.store.get(id); - if (!existingEntry) { + var existingObject = this.store.get(id); + if (!existingObject) { command.reject(this._getCommandError("Cannot update {}. It does not exist.")); return; } - this._beforeUpdate(command, existingEntry); + this._beforeUpdate(command, existingObject); if (command.isFulfilled()) { return; } - existingEntry.updateFrom(updateObject); - console.log("PUT " + JSON.stringify(existingEntry)); - this.store.put(existingEntry); - command.resolve(this._getCommandResult(existingEntry)); + existingObject.updateFrom(updateObject); + console.log("PUT " + JSON.stringify(existingObject)); + this.store.put(existingObject); + command.resolve(this._getCommandResult(existingObject)); this._persistStore(); + this._afterUpdate(command, existingObject); } }, @@ -103,31 +114,41 @@ define([ * or reject the command. * * @param command - * @param existingEntry the entry from the store before updates are applied + * @param existingObject the object from the store before updates are applied + * @private + */ + _beforeUpdate: function(command, existingObject) {}, + + /** + * Override this to extend behavior. Called after updating an object. + * + * @param {Object} command + * @param {Object} object in the store after updates made. an instance of objectTypeConstructor * @private */ - _beforeUpdate: function(command, existingEntry) {}, + _afterUpdate: function(command, object) {}, handleDelete: function(command) { if (!this.store) { command.reject(this._getCommandError("Cannot delete {} before system is initialized.")); } else { var id = command[this.commandIdProperty]; - var existingEntry = this.store.get(id); - if (!existingEntry) { + var existingObject = this.store.get(id); + if (!existingObject) { command.reject(this._getCommandError("Cannot delete {}. It does not exist.")); return; } - this._beforeDelete(command, existingEntry); + this._beforeDelete(command, existingObject); if (command.isFulfilled()) { return; } - console.log("REMOVE " + JSON.stringify(existingEntry)); + console.log("REMOVE " + JSON.stringify(existingObject)); this.store.remove(id); - command.resolve(this._getCommandResult(existingEntry, id)); + command.resolve(this._getCommandResult(existingObject, id)); this._persistStore(); + this._afterDelete(command, existingObject); } }, @@ -136,10 +157,19 @@ define([ * or reject the command. * * @param command - * @param existingEntry the entry from the store that will be deleted. + * @param existingObject the object from the store that will be deleted. + * @private + */ + _beforeDelete: function(command, existingObject) {}, + + /** + * Override this to extend behavior. Called after deleting an object. + * + * @param {Object} command + * @param {Object} object that was removed from the store. an instance of objectTypeConstructor * @private */ - _beforeDelete: function(command, existingEntry) {}, + _afterDelete: function(command, existingObject) {}, _getCommandResult: function(object, id) { var result = {}; diff --git a/script/mytime/debug-helper.js b/script/mytime/debug-helper.js old mode 100644 new mode 100755 diff --git a/script/mytime/main.js b/script/mytime/main.js old mode 100644 new mode 100755 index 59f7a6c..eb6738e --- a/script/mytime/main.js +++ b/script/mytime/main.js @@ -6,26 +6,32 @@ define([ "mytime/model/modelRegistry", "mytime/model/TimeEntry", "mytime/model/Task", - "mytime/controller/TimeEntryController", "mytime/controller/TaskController", + "mytime/controller/TimeEntryController", "mytime/controller/TaskController", "mytime/controller/JiraController", + "mytime/rest/RequestQueue", "mytime/util/store/EnhancedMemoryStore", "mytime/persistence/LocalStorage", "mytime/mock-data" ], function( modelRegistry, TimeEntry, Task, - TimeEntryController, TaskController, + TimeEntryController, TaskController, JiraController, + RequestQueue, EnhancedMemoryStore, LocalStorage, mockData ) { modelRegistry.set("timeEntryStore", EnhancedMemoryStore.createObservable()); modelRegistry.set("taskStore", EnhancedMemoryStore.createObservable()); - + LocalStorage.loadStore("timeEntryStore", modelRegistry.get("timeEntryStore"), TimeEntry); LocalStorage.loadStore("taskStore", modelRegistry.get("taskStore"), Task); - new TimeEntryController(); - new TaskController(); + var requestQueue = new RequestQueue(); + new TimeEntryController({ requestQueue: requestQueue }); + new TaskController({ requestQueue: requestQueue }); + new JiraController({ requestQueue: requestQueue }); -// mockData(); + //mockData(); + window.taskStore = modelRegistry.get('taskStore'); + window.timeEntryStore = modelRegistry.get('timeEntryStore'); }); \ No newline at end of file diff --git a/script/mytime/mock-data.js b/script/mytime/mock-data.js old mode 100644 new mode 100755 index 07a7c7f..59eb0eb --- a/script/mytime/mock-data.js +++ b/script/mytime/mock-data.js @@ -9,10 +9,10 @@ define([ ], function(CreateTaskCommand, CreateTimeEntryCommand, DateTimeUtil) { return function() { - new CreateTaskCommand({ task: { code: "CAYENNE-1234", name: "Squash a Bug" } }).exec(); - new CreateTaskCommand({ task: { code: "CAYENNE-456", name: "Make it Work" } }).exec(); - new CreateTaskCommand({ task: { code: "CAYENNE-789", name: "Do Something Awesome" } }).exec(); - new CreateTaskCommand({ task: { code: "PSP-100", name: "Uh Oh..." } }).exec(); + new CreateTaskCommand({ task: { description: "CAYENNE-1234 Squash a Bug" } }).exec(); + new CreateTaskCommand({ task: { description: "CAYENNE-456 Make it Work" } }).exec(); + new CreateTaskCommand({ task: { description: "CAYENNE-789 Do Something Awesome" } }).exec(); + new CreateTaskCommand({ task: { description: "PSP-100 Uh Oh..." } }).exec(); new CreateTimeEntryCommand({ timeEntry: { date: DateTimeUtil.getCurrentDate(), startHour: 8.5, endHour: 9.75, diff --git a/script/mytime/model/Task.js b/script/mytime/model/Task.js old mode 100644 new mode 100755 index 33bfcd3..f7ebe94 --- a/script/mytime/model/Task.js +++ b/script/mytime/model/Task.js @@ -10,26 +10,13 @@ define([ module, declare, _ModelBase ) { var Task = declare(module.id, [_ModelBase], { - _propertyNames: ["id", "code", "name", "color"], + _propertyNames: ["id", "description", "color", "integrations"], id: null, - code: null, - name: null, - color: null + description: null, + color: null, + integrations: null }); - /** - * "static" method to build a string for the task, including code and name if available. - * @param task - * @returns {string} - */ - Task.getDisplayText = function(task) { - var text = task.code || ""; - if (task.name) { - text += " " + task.name; - } - return text; - }; - return Task; }); \ No newline at end of file diff --git a/script/mytime/model/TimeEntry.js b/script/mytime/model/TimeEntry.js old mode 100644 new mode 100755 index a660d37..fce7f16 --- a/script/mytime/model/TimeEntry.js +++ b/script/mytime/model/TimeEntry.js @@ -8,7 +8,7 @@ define([ "mytime/model/_ModelBase" ], function (module, declare, _ModelBase) { return declare(module.id, [_ModelBase], { - _propertyNames: ["id", "date", "startHour", "endHour", "text", "taskId"], + _propertyNames: ["id", "date", "startHour", "endHour", "text", "taskId", "integrations"], id: null, date: null, @@ -16,6 +16,7 @@ define([ endHour: null, text: null, - taskId: null + taskId: null, + integrations: null }); }); \ No newline at end of file diff --git a/script/mytime/model/_ModelBase.js b/script/mytime/model/_ModelBase.js old mode 100644 new mode 100755 diff --git a/script/mytime/model/modelRegistry.js b/script/mytime/model/modelRegistry.js old mode 100644 new mode 100755 diff --git a/script/mytime/persistence/Context.js b/script/mytime/persistence/Context.js old mode 100644 new mode 100755 diff --git a/script/mytime/persistence/IdGenerator.js b/script/mytime/persistence/IdGenerator.js old mode 100644 new mode 100755 diff --git a/script/mytime/persistence/ImporterExporter.js b/script/mytime/persistence/ImporterExporter.js old mode 100644 new mode 100755 diff --git a/script/mytime/persistence/LocalStorage.js b/script/mytime/persistence/LocalStorage.js old mode 100644 new mode 100755 index 14c7410..b968e37 --- a/script/mytime/persistence/LocalStorage.js +++ b/script/mytime/persistence/LocalStorage.js @@ -13,13 +13,22 @@ function (exports, _, lang, Context) { */ lang.mixin(exports, { + /** + * The object is automatically stored specifically for the current context. + */ persistObject: function(key, object) { key = this._getKeyForContext(key); + this.persistObjectWithoutContext(key, object); + }, + + /** + * The object is stored in a way that is shared between all contexts. + */ + persistObjectWithoutContext: function(key, object) { localStorage.setItem(key, JSON.stringify(object)); }, /** - * * @param {string} key * @param {function} [constructor] if specified, the retrieved object will be passed into the constructor to * return an object of that type (optional - if not specified the raw persisted data is @@ -28,6 +37,17 @@ function (exports, _, lang, Context) { */ retrieveObject: function(key, constructor) { key = this._getKeyForContext(key); + return this.retrieveObjectWithoutContext(key, constructor); + }, + + /** + * @param {string} key + * @param {function} [constructor] if specified, the retrieved object will be passed into the constructor to + * return an object of that type (optional - if not specified the raw persisted data is + * returned. + * @returns {*} + */ + retrieveObjectWithoutContext: function(key, constructor) { var object = localStorage.getItem(key); if (!object) { return null; diff --git a/script/mytime/rest/DeleteEntryRequest.js b/script/mytime/rest/DeleteEntryRequest.js new file mode 100755 index 0000000..155d487 --- /dev/null +++ b/script/mytime/rest/DeleteEntryRequest.js @@ -0,0 +1,18 @@ +/** + * @license + * Copyright 2014 David Wolverton + * Available under MIT license + */ +define(["lodash", "dojo/_base/declare", "dojo/string"], +function (_, declare, string) { + + return declare([], { + method: 'delete', + url: '/api/entires/${id}', + + constructor: function(entry) { + this.url = string.substitute(this.url, entry); + } + }); +}); + diff --git a/script/mytime/rest/DeleteTaskRequest.js b/script/mytime/rest/DeleteTaskRequest.js new file mode 100755 index 0000000..a5de448 --- /dev/null +++ b/script/mytime/rest/DeleteTaskRequest.js @@ -0,0 +1,18 @@ +/** + * @license + * Copyright 2014 David Wolverton + * Available under MIT license + */ +define(["lodash", "dojo/_base/declare", "dojo/string"], +function (_, declare, string) { + + return declare([], { + method: 'delete', + url: '/api/tasks/${id}', + + constructor: function(task) { + this.url = string.substitute(this.url, task); + } + }); +}); + diff --git a/script/mytime/rest/GetJiraPicklistRequest.js b/script/mytime/rest/GetJiraPicklistRequest.js new file mode 100755 index 0000000..7454dfd --- /dev/null +++ b/script/mytime/rest/GetJiraPicklistRequest.js @@ -0,0 +1,21 @@ +/** + * @license + * Copyright 2014 David Wolverton + * Available under MIT license + */ +define(["lodash", "dojo/_base/declare", "dojo/Deferred"], +function (_, declare, Deferred) { + + return declare([Deferred], { + method: "get", + url: "/api/integrations/jira", + + constructor: function(queryString) { + this.inherited(arguments, []); + if (queryString) { + this.query = { "q": queryString } + } + } + }); +}); + diff --git a/script/mytime/rest/PutEntryRequest.js b/script/mytime/rest/PutEntryRequest.js new file mode 100755 index 0000000..6d2e0e0 --- /dev/null +++ b/script/mytime/rest/PutEntryRequest.js @@ -0,0 +1,23 @@ +/** + * @license + * Copyright 2014 David Wolverton + * Available under MIT license + */ +define(["lodash", "dojo/_base/declare", "dojo/string"], +function (_, declare, string) { + + return declare([], { + method: 'put', + url: '/api/entries/${id}', + + constructor: function(entry) { + this.url = string.substitute(this.url, entry); + this.data = { + taskId: entry.taskId, + start: entry.date + "T" + entry.startHour + ":00:00", + length: (entry.endHour - entry.startHour) * 60 + } + } + }); +}); + diff --git a/script/mytime/rest/PutTaskRequest.js b/script/mytime/rest/PutTaskRequest.js new file mode 100755 index 0000000..f061b0b --- /dev/null +++ b/script/mytime/rest/PutTaskRequest.js @@ -0,0 +1,22 @@ +/** + * @license + * Copyright 2014 David Wolverton + * Available under MIT license + */ +define(["lodash", "dojo/_base/declare", "dojo/string"], +function (_, declare, string) { + + return declare([], { + method: 'put', + url: '/api/tasks/${id}', + + constructor: function(task) { + this.url = string.substitute(this.url, task); + this.data = { + description: task.description, + color: task.color + } + } + }); +}); + diff --git a/script/mytime/rest/RequestQueue.js b/script/mytime/rest/RequestQueue.js new file mode 100755 index 0000000..712673e --- /dev/null +++ b/script/mytime/rest/RequestQueue.js @@ -0,0 +1,107 @@ +/** + * @license + * Copyright 2014 David Wolverton + * Available under MIT license + */ +define(["lodash", "dojo/_base/lang", "dojo/_base/declare", "dojo/request/xhr", "mytime/persistence/LocalStorage"], +function (_, lang, declare, xhr, LocalStorage) { + + return declare([], { + + baseUrl: "", + + baseHeaders: { + "Content-type": "application/json" + }, + + authHeaders: { + jira: "ABC", + bigtime: "DEF" + }, + + _queue: null, + _requestInProgress: false, + + constructor: function() { + this._queue = LocalStorage.retrieveObjectWithoutContext("requestQueue"); + if (!this._queue) { + this._queue = []; + } + }, + + postConstruct: function() { + this._trigger(); + }, + + push: function(request) { + this._enqueue(request); + this._trigger(); + }, + + _trigger: function() { + if (!this._requestInProgress) { + this._doNext(); + } + }, + + _doNext: function() { + if (this._queue.length != 0) { + this._requestInProgress = true; + this._do(this._queue[0]); + } + }, + + _do: function(request) { + var url = this.baseUrl + request.url; + var args = { + handleAs: "json", + method: request.method, + headers: this._getHeaders() + }; + if (request.data) { + args.data = JSON.stringify(request.data); + } + if (request.query) { + args.query = request.query; + } + + xhr(url, args).then(lang.hitch(this, function(response) { + console.log("RESPONSE: ", response); + if (typeof request.resolve === 'function') { + request.resolve(response); + } + this._finishedWithCurrentRequest(); + }), lang.hitch(this, function(err) { + console.log("RESPONSE ERROR: ", err); + // note: in Chrome get response code 0 if cannot connect to server. + if (typeof request.reject === 'function') { + request.reject(err); + } + this._finishedWithCurrentRequest(); + })); + }, + + _finishedWithCurrentRequest: function() { + this._requestInProgress = false; + this._dequeue(); + this._doNext(); + }, + + _getHeaders: function() { + var headers = lang.mixin({}, this.baseHeaders); + headers["Integration-Auth-Tokens"] = JSON.stringify(this.authHeaders); + return headers; + }, + + _enqueue: function(request) { + this._queue.push(request); + LocalStorage.persistObjectWithoutContext("requestQueue", this._queue); + }, + + _dequeue: function() { + this._queue.shift(); + LocalStorage.persistObjectWithoutContext("requestQueue", this._queue); + } + + }); +}); \ No newline at end of file diff --git a/script/mytime/store/DummyJiraPicklistStore.js b/script/mytime/store/DummyJiraPicklistStore.js new file mode 100755 index 0000000..9d28913 --- /dev/null +++ b/script/mytime/store/DummyJiraPicklistStore.js @@ -0,0 +1,50 @@ +/** + * @license + * Copyright 2014 David Wolverton + * Available under MIT license + */ +define(["lodash", "dojo/_base/declare", "dojo/_base/lang", "dojo/Deferred", "mytime/command/GetJiraPickListCommand"], +function (_, declare, lang, Deferred, GetJiraPickListCommand) { + return declare([], { + + get: function(id) { + if (!id) { + return { + id: null, + description: '', + label: 'NONE' + } + } + + return { + id: id, + description: 'Created from ' + id, + label: id + } + }, + + query: function(query) { + var id = this._normalizeQuery(query); + + return [ + this.get(id) + ] + }, + + _normalizeQuery: function(query) { + if (!query) { + return null; + } else if (typeof query === 'string') { + return query; + } else if (query.label) { + return this._normalizeQuery(query.label.toString()); + } else { + return null; + } + }, + + getIdentity: function(object) { + return object ? object.id : null; + } + }); +}); \ No newline at end of file diff --git a/script/mytime/store/JiraPicklistStore.js b/script/mytime/store/JiraPicklistStore.js new file mode 100755 index 0000000..ad18158 --- /dev/null +++ b/script/mytime/store/JiraPicklistStore.js @@ -0,0 +1,44 @@ +/** + * @license + * Copyright 2014 David Wolverton + * Available under MIT license + */ +define(["lodash", "dojo/_base/declare", "dojo/_base/lang", "dojo/Deferred", "mytime/command/GetJiraPickListCommand"], +function (_, declare, lang, Deferred, GetJiraPickListCommand) { + return declare([], { + + get: function(id) { + return this.query(id).then(function(results) { + return results.length == 1 ? results[0] : null; + }); + }, + + query: function(query) { + query = this._normalizeQuery(query); + var deferred = new Deferred(); + new GetJiraPickListCommand({query: query}).exec().then(function(results) { + _.forEach(results, function(item) { + item.label = item.id + ' ' + item.description; + }); + deferred.resolve(results); + }); + return deferred; + }, + + _normalizeQuery: function(query) { + if (!query) { + return null; + } else if (typeof query === 'string') { + return query; + } else if (query.label) { + return this._normalizeQuery(query.label.toString()); + } else { + return null; + } + }, + + getIdentity: function(object) { + return object ? object.id : null; + } + }); +}); \ No newline at end of file diff --git a/script/mytime/util/ColorGenerator.js b/script/mytime/util/ColorGenerator.js old mode 100644 new mode 100755 diff --git a/script/mytime/util/Colors.js b/script/mytime/util/Colors.js old mode 100644 new mode 100755 diff --git a/script/mytime/util/DateTimeUtil.js b/script/mytime/util/DateTimeUtil.js old mode 100644 new mode 100755 index 9fdcbf5..adfa3b7 --- a/script/mytime/util/DateTimeUtil.js +++ b/script/mytime/util/DateTimeUtil.js @@ -4,9 +4,11 @@ * Available under MIT license */ define([ - "exports", "dojo/number" + "exports", "dojo/number", "dojo/date/locale" ], -function (exports, number) { +function (exports, number, locale) { + + var MILLIS_PER_DAY = 24 * 60 * 60 * 1000; /** * Returns the whole number beginning of the hour. For example, 11.5 would return 11. @@ -123,4 +125,27 @@ function (exports, number) { exports.formatWithTwoDecimals = function(duration) { return number.format(duration, { places: 2}); }; + + /** + * Given a JavaScript date, format it in a friendly way (excludes time) + * @param {Date} date + */ + exports.formatFriendlyDate = function(date) { + var now = new Date(); + now.setHours(0); + now.setMinutes(0); + now.setSeconds(0); + now.setMilliseconds(0); + var today = now.valueOf(); + + if (date.valueOf() >= today && date.valueOf() - today < MILLIS_PER_DAY) { + return 'Today'; + } else if (date.valueOf() < today && today - date.valueOf() <= MILLIS_PER_DAY) { + return 'Yesterday'; + } else if (now.getYear() === date.getYear()) { + return locale.format(date, {selector: 'date', datePattern: 'MMM d'}); + } else { + return locale.format(date, {selector: 'date', datePattern: 'MMM d, yyyy'}); + } + } }); \ No newline at end of file diff --git a/script/mytime/util/Integrations.js b/script/mytime/util/Integrations.js new file mode 100755 index 0000000..f5595a8 --- /dev/null +++ b/script/mytime/util/Integrations.js @@ -0,0 +1,66 @@ +/** + * utilities for working with integrations + */ +define([ + "lodash", "exports" +], function ( + _, exports) { + + function getIntegrations(objectOrList) { + if (_.isArray(objectOrList)) { + return objectOrList; + } else if (objectOrList) { + return objectOrList.integrations; + } else { + return null; + } + } + + /** + * @param object either a list of integrations or an object that has an 'integrations' property. + * @param type type of integration to find or create. ex: 'jira' + * @returns Object + */ + exports.getOrAddIntegrationOfType = function(object, type) { + var integrations = getIntegrations(object); + if (!integrations) { + integrations = []; + object.integrations = integrations; + } + var integration = _.find(integrations, {type: type}); + if (!integration) { + integration = { + type: type + }; + integrations.push(integration); + } + return integration; + }; + + /** + * @param object either a list of integrations or an object that has an 'integrations' property. + * @param type type of integration to find. ex: 'jira' + * @returns Object + */ + exports.getIntegrationOfType = function(object, type) { + return _.find(getIntegrations(object), {type: type}); + }; + + /** + * @param object either a list of integrations or an object that has an 'integrations' property. + * @param type type of integration to remove. ex: 'jira' + * @return true if found and removed, false if not found + */ + exports.removeIntegrationOfType = function(object, type) { + var integrations = getIntegrations(object); + if (!integrations) { + return false; + } + var index = _.findIndex(object.integrations, {type: type}); + if (index != -1) { + object.integrations.splice(index, 1); + return true; + } + return false; + }; +}); \ No newline at end of file diff --git a/script/mytime/util/_StatefulSettersMixin.js b/script/mytime/util/_StatefulSettersMixin.js old mode 100644 new mode 100755 diff --git a/script/mytime/util/generateUniqueId.js b/script/mytime/util/generateUniqueId.js old mode 100644 new mode 100755 diff --git a/script/mytime/util/jira.js b/script/mytime/util/jira.js old mode 100644 new mode 100755 diff --git a/script/mytime/util/setIfDifferent.js b/script/mytime/util/setIfDifferent.js old mode 100644 new mode 100755 diff --git a/script/mytime/util/store/EnhancedMemoryStore.QueryEngine.js b/script/mytime/util/store/EnhancedMemoryStore.QueryEngine.js old mode 100644 new mode 100755 index d71aa6b..2f8c52c --- a/script/mytime/util/store/EnhancedMemoryStore.QueryEngine.js +++ b/script/mytime/util/store/EnhancedMemoryStore.QueryEngine.js @@ -3,6 +3,15 @@ * Copyright 2014 David Wolverton * Available under MIT license */ +/** + * This can handle normal queries, and additional operators by suffixing they key with an operator. Supported ops: + * { 'date': '2015-06-12' } date equals 2015-06-12 + * { 'date!': '2015-06-12' } date not equals 2015-06-12 + * { 'date<': '2015-06-12' } date less than 2015-06-12 + * { 'date<=': '2015-06-12' } date less or equal to 2015-06-12 + * { 'date>': '2015-06-12' } date greater than 2015-06-12 + * { 'date>=': '2015-06-12' } date greater or equal to 2015-06-12 + */ define(["lodash", "dojo/store/util/SimpleQueryEngine"], function (_, SimpleQueryEngine) { return function(query, options) { @@ -33,9 +42,14 @@ function (_, SimpleQueryEngine) { }); } else if (lastChar === "=" && key[key.length - 2] === ">") { property = key.substring(0, key.length - 2); - extraFunctions.push(function(object) { + extraFunctions.push(function (object) { return object[property] >= value; }); + } else if (lastChar === "!") { + property = key.substring(0, key.length - 1); + extraFunctions.push(function (object) { + return object[property] != value; + }); } else { modifiedQuery[key] = value; } diff --git a/script/mytime/util/store/EnhancedMemoryStore.js b/script/mytime/util/store/EnhancedMemoryStore.js old mode 100644 new mode 100755 diff --git a/script/mytime/util/store/SingleDayFilteringTimeEntryStore.js b/script/mytime/util/store/SingleDayFilteringTimeEntryStore.js old mode 100644 new mode 100755 diff --git a/script/mytime/util/store/StoreDrivenDom.js b/script/mytime/util/store/StoreDrivenDom.js old mode 100644 new mode 100755 diff --git a/script/mytime/util/store/TransformingStoreView.js b/script/mytime/util/store/TransformingStoreView.js old mode 100644 new mode 100755 diff --git a/script/mytime/util/store/delegateObserve.js b/script/mytime/util/store/delegateObserve.js old mode 100644 new mode 100755 diff --git a/script/mytime/util/store/getAndObserve.js b/script/mytime/util/store/getAndObserve.js new file mode 100755 index 0000000..02a7ad4 --- /dev/null +++ b/script/mytime/util/store/getAndObserve.js @@ -0,0 +1,43 @@ +/** + * @license + * Copyright 2014 David Wolverton + * Available under MIT license + */ +define([ + "dojo/when" +], function( + when +) { + /** + * calls the callback with the item by ID from a store and calls the callback again any time the object is added, + * removed or modified in the store. + * + * @param {store} store an observable store + * @param {id} ID of the item to retrieve from the store + * @Param {function(item)} callback the function to call immediately on fetching the item by ID and again if + * modified. The argument to the callback is the item (null or undefined if not found or removed). + * @return {{remove: function}} handle to stop the + */ + return function(store, id, callback) { + var query = {}; + query[store.idProperty || 'id'] = id; + var results = store.query(query); + + var handle; + if (typeof results.observe === 'function') { + handle = results.observe(function(object, previousIndex, newIndex) { + if (newIndex == -1) { + callback(null); + } else { + callback(object); + } + }, true); + } else { + handle = { remove: _.noop } + } + when(results, function(results) { + callback(results[0]); + }); + return handle; + }; +}); \ No newline at end of file diff --git a/script/mytime/util/syncFrom.js b/script/mytime/util/syncFrom.js old mode 100644 new mode 100755 diff --git a/script/mytime/util/whenAllPropertiesSet.js b/script/mytime/util/whenAllPropertiesSet.js old mode 100644 new mode 100755 diff --git a/script/mytime/view/TimeEntryPane.html b/script/mytime/view/TimeEntryPane.html old mode 100644 new mode 100755 index df884cc..75df8de --- a/script/mytime/view/TimeEntryPane.html +++ b/script/mytime/view/TimeEntryPane.html @@ -1,8 +1,7 @@
-
+
-
+
\ No newline at end of file diff --git a/script/mytime/view/TimeEntryPane.js b/script/mytime/view/TimeEntryPane.js old mode 100644 new mode 100755 index f403240..748d0dc --- a/script/mytime/view/TimeEntryPane.js +++ b/script/mytime/view/TimeEntryPane.js @@ -13,7 +13,7 @@ define([ 'dojox/mvc/sync', 'dojo/text!./TimeEntryPane.html', /* In template: */ - 'mytime/widget/DaysInWeekList', 'mytime/widget/DailyTimeWidget', 'mytime/widget/DailyTimeList' + 'mytime/widget/DaysInWeekList', 'mytime/widget/DailyTimeWidget', "mytime/widget/TimeEntryDetails" ], function ( _, lang, declare, _WidgetBase, @@ -27,24 +27,24 @@ define([ return declare([_WidgetBase, _TemplatedMixin, _WidgetsInTemplateMixin], { templateString: template, + baseClass: 'time-entry-pane', _daysInWeekList: null, _dailyTimeWidget: null, - _dailyTimeList: null, + _timeEntryDetails: null, currentDate: DateTimeUtil.getCurrentDate(), postCreate: function() { this.own(sync(this, 'currentDate', this._daysInWeekList, 'selectedDate')); this.own(sync(this, 'currentDate', this._dailyTimeWidget, 'date')); - this.own(sync(this, 'currentDate', this._dailyTimeList, 'date')); - this.own(sync(this._dailyTimeWidget, 'selectedId', this._dailyTimeList, 'selectedId')); + this.own(syncFrom(this._dailyTimeWidget, 'selectedId', this._timeEntryDetails)); this.own(syncFrom(modelRegistry, 'timeEntryStore', this._daysInWeekList)); this.own(syncFrom(modelRegistry, 'timeEntryStore', this._dailyTimeWidget)); this.own(syncFrom(modelRegistry, 'taskStore', this._dailyTimeWidget)); - this.own(syncFrom(modelRegistry, 'timeEntryStore', this._dailyTimeList)); - this.own(syncFrom(modelRegistry, 'taskStore', this._dailyTimeList)); + this.own(syncFrom(modelRegistry, 'timeEntryStore', this._timeEntryDetails)); + this.own(syncFrom(modelRegistry, 'taskStore', this._timeEntryDetails)); this.own(this._dailyTimeWidget.on('createTimeEntry', lang.hitch(this, '_createTimeEntry'))); this.own(this._dailyTimeWidget.on('updateTimeEntry', lang.hitch(this, '_updateTimeEntry'))); @@ -56,7 +56,7 @@ define([ .then(lang.hitch(this, function(result) { _.defer(lang.hitch(this, function() { // When an entry is added start editing it. - this._dailyTimeList.set("editingId", result.timeEntryId); + this._dailyTimeWidget.set("selectedId", result.timeEntryId); })); })); }, diff --git a/script/mytime/widget/DailyTimeList.js b/script/mytime/widget/DailyTimeList.js deleted file mode 100644 index 13403ff..0000000 --- a/script/mytime/widget/DailyTimeList.js +++ /dev/null @@ -1,267 +0,0 @@ -/** - * @license - * Copyright 2014 David Wolverton - * Available under MIT license - */ -define([ - "lodash", "dojo/_base/lang", "dojo/_base/declare", "dojo/when", - "dojo/dom-construct", "dojo/dom-class", "dojo/dom-attr", "dojo/on", "dojo/query", "dojo/Evented", "dojo/dom", - "dijit/_WidgetBase", "dijit/form/Textarea", "dijit/focus", - "mytime/widget/TaskPickerCombo", "mytime/widget/TaskDialog", - "mytime/model/TimeEntry", - "mytime/command/UpdateTimeEntryCommand", "mytime/command/UpdateTaskCommand", - "mytime/util/Colors", "mytime/util/whenAllPropertiesSet", "mytime/util/store/TransformingStoreView", - "mytime/util/store/StoreDrivenDom", "mytime/util/DateTimeUtil", "mytime/util/jira", - "dojo/text!mytime/widget/DailyTimeList/templates/entry.html", - "dojo/text!mytime/widget/DailyTimeList/templates/entry-edit.html" -], -function ( - _, lang, declare, when, - domConstruct, domClass, domAttr, on, query, Evented, dom, - _WidgetBase, Textarea, focusUtil, - TaskPickerCombo, TaskDialog, - TimeEntry, - UpdateTimeEntryCommand, UpdateTaskCommand, - Colors, whenAllPropertiesSet, TransformingStoreView, - StoreDrivenDom, DateTimeUtil, jira, - template, editTemplate) { - - /** - * - */ - return declare([_WidgetBase, Evented], { - - baseClass: "timelist", - - date: null, - timeEntryStore: null, - taskStore: null, - - selectedId: null, - editingId: null, - _editingStartData: null, - - _internalStore: null, - _list: null, - - _taskCombo: null, - _notesBox: null, - - buildRendering: function() { - this.inherited(arguments); - }, - - postCreate: function() { - var _this = this; - this.own( - whenAllPropertiesSet(this, ["date", "timeEntryStore", "taskStore"], lang.hitch(this, "_initialize")), - on(this.domNode, on.selector(".task, .note, .menu-button", "click"), function(event) { - // NOTE: for 'on' with selector, 'this' is the node identified by the selector. - _this._onEntryClick(event, this); - }), - on(document, 'mouseup', lang.hitch(this, '_onClickElsewhere')) - ); - }, - - _initialize: function() { - this._setupTaskCombo(); - this._setupNotesBox(); - this._internalStore = new TransformingStoreView({ - sourceStore: this.timeEntryStore, - sourceQuery: {date: this.date}, - transform: lang.hitch(this, function(input) { - var entry = new TimeEntry(input); - entry.selected = entry.id === this.selectedId; - entry.editing = entry.id === this.editingId; - return entry; - }) - }).getObservable(); - this._list = new StoreDrivenDom({ - store: this._internalStore, - queryOptions: {sort: [{attribute: "startHour"}]}, - renderNode: lang.hitch(this, "_renderEntry") - }); - - this._list.placeAt(this.domNode); - - this.own( - this.watch("date", lang.hitch(this, "_dateChanged")), - this.watch("editingId", lang.hitch(this, "_editingOrSelectedIdChanged")), - this.watch("selectedId", lang.hitch(this, "_editingOrSelectedIdChanged")) - ); - }, - - _setupTaskCombo: function() { - this._taskCombo = new TaskPickerCombo({ - store: this.taskStore - }); - this.own( - on(this._taskCombo, "change", lang.hitch(this, "_onTaskComboChange")) - ); - }, - - _setupNotesBox: function() { - this._notesBox = new Textarea(); - this.own( - on(this._notesBox, "change", lang.hitch(this, "_onNotesBoxChange")) - ); - }, - - _renderEntry: function(timeEntry) { - var task = null; - if (timeEntry.taskId) { - task = this.taskStore.get(timeEntry.taskId); - } - return this._renderEntryWithTask(timeEntry, task); - }, - - _renderEntryWithTask: function(timeEntry, task) { - var data = { - Colors: Colors, - timeEntryId: timeEntry.id, - taskId: "", - text: timeEntry.text || "", - duration: DateTimeUtil.duration(timeEntry), - code: "[ ]", - name: "No Task", - color: null, - selected: timeEntry.selected, - jiraLoggingUrl: "" - }; - if (task) { - data.taskId = task.id || ""; - data.code = task.code || ""; - data.name = task.name || ""; - data.color = task.color || null; - if (jira.isJiraIssueKey(task.code)) { - data.jiraLoggingUrl = jira.buildTimeLoggingLink(task.code, timeEntry); - } - } - if (timeEntry.editing) { - return this._renderEditingEntry(timeEntry, task, data); - } else { - var html = _.template(template, data); - return domConstruct.toDom(html); - } - }, - - _renderEditingEntry: function(timeEntry, task, data) { - var html = _.template(editTemplate, data); - var dom = domConstruct.toDom(html); - - this._editingStartData = { - taskId: task ? task.id : null, - text: timeEntry.text - }; - - var comboContainer = query(".task", dom)[0]; - this._taskCombo.set("task", task); - this._taskCombo.placeAt(comboContainer); - this._taskCombo.startup(); - - var noteContainer = query(".note", dom)[0]; - this._notesBox.set("value", timeEntry.text); - this._notesBox.placeAt(noteContainer); - this._notesBox.startup(); - - return dom; - }, - - _dateChanged: function() { - this._internalStore.set("sourceQuery", {date: this.date}); - }, - - _onEntryClick: function(event, node) { - var entryNode = this._getTimeEntryNodeContaining(node); - var entryId = domAttr.get(entryNode, "data-timeentry-id"); - var taskId = domAttr.get(entryNode, "data-task-id"); - - if (domClass.contains(node, 'task') || domClass.contains(node, 'note')) { - this.set("selectedId", entryId); - this.set("editingId", entryId); - } else if (domClass.contains(node, 'menu-button')) { - this._onMenuClick(node, entryId, taskId); - } - }, - - _getTimeEntryNodeContaining: function(node) { - do { - if (domClass.contains(node, 'timeentry')) { - return node; - } - node = node.parentNode; - } while (node); - return null; - }, - - _onMenuClick: function(buttonNode, entryId, taskId) { - if (!taskId) { - return; - } - when(this.taskStore.get(taskId), function(task) { - var dialog = new TaskDialog({value: task}); - dialog.showAndWaitForUser().then(function(task) { - dialog.destroy(); - new UpdateTaskCommand({task: task}).exec(); - }, function() { - dialog.destroy(); - }); - }); - }, - - _editingOrSelectedIdChanged: function(prop, prevValue, value) { - if (value !== prevValue) { - if (prevValue) { - this._internalStore.refreshItem(prevValue); - } - if (value) { - this._internalStore.refreshItem(value); - if (prop === "editingId") { - this._taskCombo.focusAndSelectAll(); - } - } - } - }, - - _onTaskComboChange: function() { - var task = this._taskCombo.get('task'); - var taskId = task ? task.id : null; - if (taskId !== this._editingStartData.taskId) { - this._editingStartData.taskId = taskId; - new UpdateTimeEntryCommand({timeEntry: { - id: this.editingId, - taskId: taskId - }}).exec(); - } - }, - - _onNotesBoxChange: function() { - var text = this._notesBox.get('value'); - if (text !== this._editingStartData.text) { - this._editingStartData.text = text; - new UpdateTimeEntryCommand({timeEntry: { - id: this.editingId, - text: text - }}).exec(); - } - }, - - /** - * Hide the editor when clicking outside this widget. - */ - _onClickElsewhere: function(event) { - if (!this.editingId) { - return; - } - if (_.contains(focusUtil.activeStack, this._taskCombo.id) || - _.contains(focusUtil.activeStack, this._notesBox.id)) { - return; - } - if (event.target === this.domNode || dom.isDescendant(event.target, this.domNode)) { - return; - } - this.set('editingId', null); - } - - }); -}); \ No newline at end of file diff --git a/script/mytime/widget/DailyTimeList/templates/entry-edit.html b/script/mytime/widget/DailyTimeList/templates/entry-edit.html deleted file mode 100644 index 270377a..0000000 --- a/script/mytime/widget/DailyTimeList/templates/entry-edit.html +++ /dev/null @@ -1,19 +0,0 @@ -
- - - - - <% if (taskId) { %> - - <% } %> - - - <% if (jiraLoggingUrl) { %> - - - - <% } %> - -
\ No newline at end of file diff --git a/script/mytime/widget/DailyTimeList/templates/entry.html b/script/mytime/widget/DailyTimeList/templates/entry.html deleted file mode 100644 index 2083542..0000000 --- a/script/mytime/widget/DailyTimeList/templates/entry.html +++ /dev/null @@ -1,19 +0,0 @@ -
- - <%- code %> - <%- name %> - - <%- text %> - - <% if (taskId) { %> - - <% } %> - <% if (jiraLoggingUrl) { %> - - - - <% } %> - -
\ No newline at end of file diff --git a/script/mytime/widget/DailyTimeWidget.js b/script/mytime/widget/DailyTimeWidget.js old mode 100644 new mode 100755 index a4e18a7..bea143f --- a/script/mytime/widget/DailyTimeWidget.js +++ b/script/mytime/widget/DailyTimeWidget.js @@ -34,7 +34,7 @@ function ( */ date: null, - startHour: 6, + startHour: 0, endHour: 23, @@ -45,8 +45,8 @@ function ( selectedId: null, - nowHour: 0, - endOfDayHour: 0, + nowHour: -1, + endOfDayHour: -1, // TODO update this according to hours remaining _internalStore: null, @@ -78,6 +78,8 @@ function ( this.own( this._view.on("endDrag", lang.hitch(this, "_endDragListener")), this._view.on("click", lang.hitch(this, "_clickListener")), + this._view.on("prevDay", lang.hitch(this, "addDays", -1)), + this._view.on("nextDay", lang.hitch(this, "addDays", +1)), this.watch("date", lang.hitch(this, "_updateNowHour")) ); @@ -104,6 +106,8 @@ function ( var entry = this._findEntryContainingHour(hour); if (entry && entry.id !== this.selectedId) { this.set("selectedId", entry.id); + } else if (!entry) { + this.set("selectedId", null); } }, @@ -212,6 +216,18 @@ function ( } else { this.set("nowHour", -1); } + }, + + /** + * Adjust the selectedDate by a certain number of days. + * @param daysToAdd number of days to add + or - + */ + addDays: function(daysToAdd) { + var date = DateTimeUtil.convertDateStringToDateObject(this.get('date')); + var millis = date.valueOf(); + millis += 86400000 * daysToAdd; + date.setTime(millis); + this.set('date', DateTimeUtil.convertDateObjectToDateString(date)); } }); diff --git a/script/mytime/widget/DailyTimeWidget/DailyTimeWidgetStore.js b/script/mytime/widget/DailyTimeWidget/DailyTimeWidgetStore.js old mode 100644 new mode 100755 index a30fa9e..1875087 --- a/script/mytime/widget/DailyTimeWidget/DailyTimeWidgetStore.js +++ b/script/mytime/widget/DailyTimeWidget/DailyTimeWidgetStore.js @@ -48,7 +48,8 @@ define([ var task = this.taskStore.get(timeEntry.taskId); if (task) { timeEntry.color = task.color; - timeEntry.code = task.code; + timeEntry.description = task.description; + timeEntry.taskIntegrations = task.integrations; } } return timeEntry; diff --git a/script/mytime/widget/DailyTimeWidget/DailyTimeWidgetView.js b/script/mytime/widget/DailyTimeWidget/DailyTimeWidgetView.js old mode 100644 new mode 100755 index 35cefff..3f5d233 --- a/script/mytime/widget/DailyTimeWidget/DailyTimeWidgetView.js +++ b/script/mytime/widget/DailyTimeWidget/DailyTimeWidgetView.js @@ -11,9 +11,10 @@ define([ "dojo/dom-construct", "dojo/dom-class", "dojo/dom-style", "dojo/dom-geometry", "dojo/date/locale", "dojo/Evented", "dijit/_WidgetBase", "dijit/_TemplatedMixin", - "mytime/util/DateTimeUtil", "mytime/util/Colors", + "mytime/util/DateTimeUtil", "mytime/util/Colors", "mytime/util/Integrations", "mytime/util/jira", "dojo/text!./templates/grid.html", - "dojo/text!./templates/gridrow.html" + "dojo/text!./templates/gridrow.html", + "dojo/text!./templates/bar-content.html" ], function (declare, _, @@ -22,9 +23,10 @@ function (declare, domConstruct, domClass, domStyle, domGeom, dateLocale, Evented, _WidgetBase, _TemplatedMixin, - DateTimeUtil, Colors, - template, - gridRowTemplate) { + DateTimeUtil, Colors, Integrations, jira, + template, gridRowTemplate, barContentTemplate) { + + barContentTemplate = _.template(barContentTemplate); /** * @@ -75,7 +77,9 @@ function (declare, }, _getLabelForHour: function(hour) { - if (hour < 12) { + if (hour == 0) { + return "12:00 am"; + } else if (hour < 12) { return hour + ":00 am"; } else if (hour == 12) { return hour + ":00 pm"; @@ -270,7 +274,7 @@ function (declare, } i++; } - this._setTextOnTimeBars(timeBars, timeEntry.code); + this._setTextOnTimeBars(timeBars, timeEntry); }, _buildTimeBarsForTimeEntry: function(timeEntry) { @@ -280,7 +284,7 @@ function (declare, timeBars.push(timebar); this._placeTimeBar(timebar, slot.hour); }, this); - this._setTextOnTimeBars(timeBars, timeEntry.code); + this._setTextOnTimeBars(timeBars, timeEntry); }, _placeTimeBar: function(timebar, hour) { @@ -320,11 +324,10 @@ function (declare, domClass.toggle(timeBar, "end", slot.isEnd); }, - _setTextOnTimeBars: function(timeBars, text) { + _setTextOnTimeBars: function(timeBars, timeEntry) { if (!timeBars || timeBars.length == 0) { return; } - text = text || ""; var middle; if (timeBars.length % 2 == 0) { // even @@ -338,8 +341,33 @@ function (declare, } _.forEach(timeBars, function(bar, i) { - bar.innerText = (middle === i) ? text : ""; - }); + if (middle == i) { + this._setTextOnTimeBar(bar, timeEntry); + } else { + this._clearTextOnTimeBar(bar); + } + }, this); + }, + + _setTextOnTimeBar: function(timeBar, timeEntry) { + if (!timeEntry || !timeEntry.description) { + this._clearTextOnTimeBar(timeBar); + return; + } + var data = { + timeEntry: timeEntry + }; + var taskJira = Integrations.getIntegrationOfType(timeEntry.taskIntegrations, 'jira'); + if (taskJira && taskJira.id) { + data.jiraKey = taskJira.id; + data.jiraLoggingUrl = jira.buildTimeLoggingLink(taskJira.id, timeEntry); + } + + timeBar.innerHTML = barContentTemplate(data); + }, + + _clearTextOnTimeBar: function(timeBar) { + timeBar.innerHTML = ''; }, _timeEntryRemoved: function(timeEntry) { @@ -372,6 +400,14 @@ function (declare, markNode.parentNode.removeChild(markNode); } } + }, + + _clickPrevDay: function() { + this.emit('prevDay') + }, + + _clickNextDay: function() { + this.emit('nextDay') } }); }); \ No newline at end of file diff --git a/script/mytime/widget/DailyTimeWidget/templates/bar-content.html b/script/mytime/widget/DailyTimeWidget/templates/bar-content.html new file mode 100755 index 0000000..9035b64 --- /dev/null +++ b/script/mytime/widget/DailyTimeWidget/templates/bar-content.html @@ -0,0 +1,8 @@ +<%- timeEntry.description %> +<% if (typeof jiraKey === 'string') { %> + + + <%- jiraKey %> + + +<% } %> \ No newline at end of file diff --git a/script/mytime/widget/DailyTimeWidget/templates/grid.html b/script/mytime/widget/DailyTimeWidget/templates/grid.html old mode 100644 new mode 100755 index 4234626..6649858 --- a/script/mytime/widget/DailyTimeWidget/templates/grid.html +++ b/script/mytime/widget/DailyTimeWidget/templates/grid.html @@ -6,7 +6,11 @@ Weekday, 00 Month - - +
+ + + +
+
\ No newline at end of file diff --git a/script/mytime/widget/DailyTimeWidget/templates/gridrow.html b/script/mytime/widget/DailyTimeWidget/templates/gridrow.html old mode 100644 new mode 100755 diff --git a/script/mytime/widget/DaysInWeekList.js b/script/mytime/widget/DaysInWeekList.js old mode 100644 new mode 100755 diff --git a/script/mytime/widget/DaysInWeekList/template.html b/script/mytime/widget/DaysInWeekList/template.html old mode 100644 new mode 100755 diff --git a/script/mytime/widget/JiraPickerCombo.js b/script/mytime/widget/JiraPickerCombo.js new file mode 100755 index 0000000..eb1b81e --- /dev/null +++ b/script/mytime/widget/JiraPickerCombo.js @@ -0,0 +1,53 @@ +/** + * @license + * Copyright 2014 David Wolverton + * Available under MIT license + */ +define([ + "lodash", "dojo/_base/lang", "dojo/_base/declare", + "dijit/form/ComboBox", + "mytime/command/CreateTaskCommand", "mytime/model/Task", + "mytime/util/Colors", + "mytime/widget/TaskPickerComboStore" +], +function ( + _, lang, declare, + ComboBox, + CreateTaskCommand, Task, + Colors, + TaskPickerComboStore + ) { + return declare([ComboBox], { + + searchAttr: "_searchText", + + labelType: "html", + + queryExpr: "${0}", + + constructor: function() { + this.baseClass += " taskpicker"; + }, + + _getTaskAttr: function() { + return this.get("jiraIssue"); + }, + + _setTaskAttr: function(task) { + if (task) { + task = new Task(task); + task._searchText = task.description; + } + this.set("jiraIssue", task); + }, + + labelFunc: function(item, store) { + return _.escape(item.description); + }, + + focusAndSelectAll: function() { + this.focus(); + this.focusNode.select(); + } + }); +}); \ No newline at end of file diff --git a/script/mytime/widget/TaskDialog.js b/script/mytime/widget/TaskDialog.js deleted file mode 100644 index fc0e3a3..0000000 --- a/script/mytime/widget/TaskDialog.js +++ /dev/null @@ -1,67 +0,0 @@ -define([ - 'dojo/_base/lang', 'dojo/_base/declare', 'dojo/Deferred', - 'dijit/Dialog', - 'mytime/widget/TaskForm' -], function(lang, declare, Deferred, Dialog, TaskForm) { - return declare([Dialog], { - - form: null, - - _formDeferred: null, - - /** - * Show the dialog and return a promise that will be resolved when the user clicks OK or rejected when the user - * clicks Cancel. - * @param value - * @returns {Deferred.promise|*|dojo.Deferred.promise} - */ - showAndWaitForUser: function(value) { - if (value) { - this.set('value', value); - } - this.show(); - this._formDeferred = new Deferred(); - - return this._formDeferred.promise; - }, - - constructor: function() { - this.form = new TaskForm(); - }, - - postCreate: function() { - this.inherited(arguments); - this.set('content', this.form); - this.form.on('submit', lang.hitch(this, '_submit')); - this.form.on('cancel', lang.hitch(this, '_cancel')); - }, - - _setValueAttr: function(value) { - this.set('title', 'Task ' + value.code); - this.form.set('value', value); - }, - - _getValueAttr: function() { - return this.form.get('value'); - }, - - _submit: function(value) { - if (this._formDeferred) { - this._formDeferred.resolve(value); - this._formDeferred = null; - } - // NOTE: hide is not needed because the Dialog code seems to handle it automatically. - }, - - _cancel: function() { - this.hide(); - }, - - onHide: function() { - if (this._formDeferred) { - this._formDeferred.reject('Canceled'); - this._formDeferred = null; - } - } - }); -}); \ No newline at end of file diff --git a/script/mytime/widget/TaskForm.js b/script/mytime/widget/TaskForm.js deleted file mode 100644 index f30763e..0000000 --- a/script/mytime/widget/TaskForm.js +++ /dev/null @@ -1,50 +0,0 @@ -define([ - 'lodash', - 'dojo/_base/lang', 'dojo/_base/declare', - 'dijit/_WidgetBase', 'dijit/_TemplatedMixin', 'dijit/_WidgetsInTemplateMixin', - 'mytime/model/Task', - 'dojo/text!./TaskForm/TaskFormContent.html', - /* widgets in template */ - 'dijit/form/Form', 'dijit/form/TextBox', 'dijit/form/Button' -], function ( - _, - lang, declare, - _WidgetBase, _TemplatedMixin, _WidgetsInTemplateMixin, - Task, - TaskFormContent) { - - return declare([_WidgetBase, _TemplatedMixin, _WidgetsInTemplateMixin], { - - baseClass: 'task-form', - - templateString: TaskFormContent, - - /** - * Attach Point - */ - form: null, - - value: null, - - _setValueAttr: function(value) { - this.value = value; - this.form.set('value', value); - }, - - _getValueAttr: function() { - var props = lang.mixin({}, this.value, this.form.get('value')); - return new Task(props); - }, - - postCreate: function() { - this.form.onSubmit = lang.hitch(this, function() { - this.emit('submit', this.get('value')); - return false; - }); - }, - - cancel: function() { - this.emit('cancel'); - } - }); -}); \ No newline at end of file diff --git a/script/mytime/widget/TaskForm/TaskFormContent.html b/script/mytime/widget/TaskForm/TaskFormContent.html deleted file mode 100644 index 2758466..0000000 --- a/script/mytime/widget/TaskForm/TaskFormContent.html +++ /dev/null @@ -1,20 +0,0 @@ -
-
-
- - -
-
- - -
-
- - -
-
- - Cancel -
-
-
\ No newline at end of file diff --git a/script/mytime/widget/TaskPickerCombo.js b/script/mytime/widget/TaskPickerCombo.js old mode 100644 new mode 100755 index 295a445..bfecdee --- a/script/mytime/widget/TaskPickerCombo.js +++ b/script/mytime/widget/TaskPickerCombo.js @@ -4,14 +4,14 @@ * Available under MIT license */ define([ - "lodash", "dojo/_base/lang", "dojo/_base/declare", + "lodash", "dojo/_base/lang", "dojo/_base/declare", "dojo/on", "dijit/form/ComboBox", "mytime/command/CreateTaskCommand", "mytime/model/Task", "mytime/util/Colors", "mytime/widget/TaskPickerComboStore" ], function ( - _, lang, declare, + _, lang, declare, on, ComboBox, CreateTaskCommand, Task, Colors, @@ -25,19 +25,26 @@ function ( queryExpr: "${0}", + _lastTask: null, + constructor: function() { this.baseClass += " taskpicker"; }, _getTaskAttr: function() { - return this.get("item"); + var task = this.get("item"); + if (!task) { + task = this._parseStringToTask(this.get('value')); + } + return task; }, _setTaskAttr: function(task) { if (task) { task = new Task(task); - task._searchText = Task.getDisplayText(task); + task._searchText = task.description; } + this._lastTask = task; this.set("item", task); }, @@ -50,14 +57,38 @@ function ( _handleOnChange: function(newStringValue) { this.inherited(arguments); - if (!this.item && newStringValue) { - var task = this._parseStringToTask(newStringValue); - if (task) { - new CreateTaskCommand({task: task}).exec().then(lang.hitch(this, function(result) { - this.set("task", result.task); - })); + //if (this.item && this.item.description != newStringValue) { + // this.set('value', this.item.description); + //} + }, + + postCreate: function() { + this.inherited(arguments); + this.own( + on(this, "change", lang.hitch(this, '_checkForChange')) + ); + }, + + _checkForChange: function() { + var task = this.get('task'); + var taskId = task ? task.id : null; + var taskDescription = task ? task.description : null; + if (this._lastTask) { + if (this._lastTask.id !== taskId || this._lastTask.description !== taskDescription) { + this._lastTask = task; + this.onUserchange(task); + } + } else { + if (taskDescription) { + this._lastTask = task; + this.onUserchange(task); } } + + var expectedDescription = task ? task.description : ''; + if (this.get('value') !== expectedDescription) { + this.set('task', task); + } }, _parseStringToTask: function(string) { @@ -67,31 +98,22 @@ function ( } var task = { - code: string + description: string }; - var firstSpace = string.indexOf(' '); - if (firstSpace > -1) { - task.code = string.substring(0, firstSpace); - task.name = string.substring(firstSpace + 1).trim(); - if (task.name.length < 1) { - delete task.name; - } - } - return this._isValidCode(task.code) ? task : null; + return this._isValidDescription(task.description) ? task : null; }, - _isValidCode: function(code) { - return code && code.length > 1; + _isValidDescription: function(description) { + return description && description.length > 1; }, - labelFunc: function(item, store) { - return '' + - _.escape(item.code) + ' ' + _.escape(item.name || "") + ''; - }, + labelAttr: "description", focusAndSelectAll: function() { this.focus(); this.focusNode.select(); - } + }, + + onUserchange: function(task) {} }); }); \ No newline at end of file diff --git a/script/mytime/widget/TaskPickerComboStore.js b/script/mytime/widget/TaskPickerComboStore.js old mode 100644 new mode 100755 index 21707dd..dc8e40e --- a/script/mytime/widget/TaskPickerComboStore.js +++ b/script/mytime/widget/TaskPickerComboStore.js @@ -41,7 +41,7 @@ function ( var task = this.store.get.apply(this.store, arguments); if (task) { task = new Task(task); - task._searchText = Task.getDisplayText(task); + task._searchText = task.description; } return task; }, @@ -53,11 +53,9 @@ function ( if (queryString) { var escapedQueryString = this._escapeRegExp(queryString); var beginningRegExp = new RegExp("(^|\\b)" + escapedQueryString, "i"); - var endingRegExp = new RegExp(escapedQueryString + "$", "i"); rawQuery = function(task) { - return ( task.code && ( beginningRegExp.test(task.code) || endingRegExp.test(task.code) ) ) - || ( task.name && beginningRegExp.test(task.name) ); + return task.description && beginningRegExp.test(task.description); }; this._beginningRegExp = beginningRegExp; @@ -70,7 +68,7 @@ function ( this._queryString = queryString; var options = { -// sort: [{attribute: 'code', descending: true}] +// sort: [{attribute: 'description', descending: true}] }; var rawResults = this.store.query(rawQuery, options); @@ -94,23 +92,18 @@ function ( if (this._beginningRegExp) { task._searchText = this._buildSearchText(task); } else { - task._searchText = Task.getDisplayText(task); + task._searchText = task.description } return task; }, _buildSearchText: function(task) { - var firstInCode = task.code && task.code.search(this._beginningRegExp); - if (firstInCode === 0) { - return Task.getDisplayText(task); + var locationInDescription = task.description && task.description.search(this._beginningRegExp); + if (locationInDescription === 0) { + return task.description; } else { - var firstInName = task.name && task.name.search(this._beginningRegExp); - if (firstInName === 0) { - return task.name + " ~ " + task.code; - } else { - return this._queryString + " ~ " + Task.getDisplayText(task); - } + return this._queryString + " ~ " + task.description; } }, diff --git a/script/mytime/widget/TimeEntryDetails.js b/script/mytime/widget/TimeEntryDetails.js new file mode 100755 index 0000000..2473251 --- /dev/null +++ b/script/mytime/widget/TimeEntryDetails.js @@ -0,0 +1,243 @@ +/** + * @license + * Copyright 2014 David Wolverton + * Available under MIT license + */ +define([ + "lodash", "dojo/_base/lang", "dojo/_base/declare", "dojo/when", "dojo/Stateful", "dojo/Evented", + "dijit/_WidgetBase", + "mytime/widget/TimeEntryDetails/TimeEntryDetailsView", + "mytime/store/DummyJiraPicklistStore", + "mytime/command/UpdateTimeEntryCommand", + "mytime/command/CreateTaskCommand", "mytime/command/UpdateTaskCommand", + "mytime/util/whenAllPropertiesSet", "mytime/util/syncFrom", "mytime/util/store/getAndObserve", + "mytime/util/Integrations" +], +function ( + _, lang, declare, when, Stateful, Evented, + _WidgetBase, + TimeEntryDetailsView, + JiraPicklistStore, + UpdateTimeEntryCommand, CreateTaskCommand, UpdateTaskCommand, + whenAllPropertiesSet, syncFrom, getAndObserve, + Integrations) { + + /** + * details entry/task details pane + */ + return declare([_WidgetBase, Evented], { + + timeEntryStore: null, + taskStore: null, + jiraStore: new JiraPicklistStore(), + + selectedId: null, + + currentTimeEntry: null, + currentTask: null, + + _model: null, + _view: null, + + constructor: function() { + this._model = new Stateful({ + task: null, + jiraKey: null, + linkedEntry: null + }); + + this._view = new TimeEntryDetailsView({ + model: this._model, + jiraStore: this.jiraStore + }); + syncFrom(this, 'taskStore', this._view); + }, + + buildRendering: function() { + this.domNode = this._view.domNode; + }, + + postCreate: function() { + this.own( + whenAllPropertiesSet(this, ["timeEntryStore", "taskStore"], lang.hitch(this, "_initialize")) + ); + }, + + _initialize: function() { + this.own( + this.watch("selectedId", lang.hitch(this, "_selectedIdChanged")), + this._view.on("taskSelected", lang.hitch(this, "_taskSelected")), + this._view.on("jiraSelected", lang.hitch(this, "_jiraSelected")) + ); + this._selectedIdChanged(null, null, this.selectedId); + }, + + _selectedIdChanged: function(prop, prevValue, value) { + if (value !== prevValue) { + if (!value) { + this._view.hide(); + } else { + this._fillInFromId(value); + this._view.show(); + } + } + }, + + _fillInFromId: function(timeEntryId) { + this._timeEntryUpdateHandle && this._timeEntryUpdateHandle.remove(); + this.own(this._timeEntryUpdateHandle = + getAndObserve(this.timeEntryStore, timeEntryId, lang.hitch(this, function(timeEntry) { + if (timeEntry.taskId) { + this._taskUpdateHandle && this._taskUpdateHandle.remove(); + this.own(this._taskUpdateHandle = + getAndObserve(this.taskStore, timeEntry.taskId, lang.hitch(this, function (task) { + this._fillIn(timeEntry, task); + }))); + } else { + this._fillIn(timeEntry, null); + } + }))); + }, + + _fillIn: function(timeEntry, task) { + this.currentTimeEntry = timeEntry; + this.currentTask = task; + + this._model.set('task', task); + + var jiraIntegration = Integrations.getIntegrationOfType(task, 'jira'); + if (jiraIntegration) { + this._model.set('jiraKey', jiraIntegration.id); + } else { + this._model.set('jiraKey', null); + } + + if (task) { + this._updateLinkedEntry(task.id); + } else { + this._model.set('linkedEntry', null); + } + }, + + _updateLinkedEntry: function(taskId) { + when(this.timeEntryStore.query({taskId: taskId, 'id!': this.selectedId}, {sort: [ + {attribute: 'date', descending: true}, + {attribute: 'startHour', descending: true} + ]}), lang.hitch(this, function(entries) { + if (entries.length == 0) { + this._model.set('linkedEntry', null); + } else { + this._model.set('linkedEntry', entries[0]); + } + })); + }, + + _taskSelected: function(task) { + var taskId = task ? task.id : null; + if (!task) { + // unset task from entry + this._updateEntry({ + taskId: null + }); + } else if (task.id) { + // existing task + if (task.id !== this.currentTimeEntry.taskId) { + this._updateEntry({ + taskId: task.id + }); + } + } else if (task.description) { + if (this.currentTask) { + // update existing task + this.currentTask.set('description', task.description); + this._createOrUpdateTask(this.currentTask); + } else { + // new task + this._createTask({ + description: task.description + }).then(lang.hitch(this, function (result) { + this._updateEntry({ + taskId: result.taskId + }); + })); + } + } + }, + + _jiraSelected: function(selectedJiraKey) { + this._onJiraKeyChange(selectedJiraKey); + }, + + _onJiraKeyChange: function(key) { + console.log("onJiraKeyChange", key); + + var task = this.currentTask; + if (task) { + if (key) { + var integration = Integrations.getOrAddIntegrationOfType(task, 'jira'); + if (integration.id === key) { + return; + } + integration.id = key; + } else { + if (!Integrations.removeIntegrationOfType(task, 'jira')) { + return; + } + } + + this._updateTask(task).then(lang.hitch(this, '_updateTimeEntryAfterTaskChange', this.currentTimeEntry)); + } else { + if (key) { + this._setTaskFromJiraKey(key); + } + } + }, + + _setTaskFromJiraKey: function(jiraKey) { + var timeEntry = this.currentTimeEntry; + + when(this.jiraStore.get(jiraKey), lang.hitch(this, function(jiraItem) { + var task = { + description: jiraItem.label + }; + Integrations.getOrAddIntegrationOfType(task, 'jira').id = jiraKey; + this._createTask(task).then(lang.hitch(this, '_updateTimeEntryAfterTaskChange', timeEntry)); + })); + }, + + _updateTimeEntryAfterTaskChange: function(timeEntry, taskUpdateResult) { + var task = taskUpdateResult.task; + var change = false; + if (timeEntry.taskId !== task.id) { + timeEntry.taskId = task.id; + change = true; + } + + if (change) { + this._updateEntry(timeEntry); + } + }, + + _createOrUpdateTask: function(task) { + if (task.id) { + return this._updateTask(task); + } else { + return this._createTask(task); + } + }, + + _updateEntry: function(entry) { + entry = lang.mixin({id: this.currentTimeEntry.id}, entry); + return new UpdateTimeEntryCommand({timeEntry: entry}).exec(); + }, + + _createTask: function(task) { + return new CreateTaskCommand({task: task}).exec(); + }, + + _updateTask: function(task) { + return new UpdateTaskCommand({task: task}).exec(); + } + + }); +}); \ No newline at end of file diff --git a/script/mytime/widget/TimeEntryDetails/TimeEntryDetailsView.js b/script/mytime/widget/TimeEntryDetails/TimeEntryDetailsView.js new file mode 100755 index 0000000..becb5fd --- /dev/null +++ b/script/mytime/widget/TimeEntryDetails/TimeEntryDetailsView.js @@ -0,0 +1,116 @@ +/** + * @license + * Copyright 2014 David Wolverton + * Available under MIT license + */ +define([ + "lodash", "dojo/_base/lang", "dojo/_base/declare", + "dojo/dom-class", "dojo/on", "dojo/query", "dojo/Evented", "dojo/dom", + "dijit/_WidgetBase", "dijit/_TemplatedMixin", "dijit/_WidgetsInTemplateMixin", + "dijit/form/FilteringSelect", "dijit/focus", + "mytime/util/syncFrom", "mytime/util/whenAllPropertiesSet", "mytime/util/DateTimeUtil", + "mytime/widget/TaskPickerCombo", + "dojo/text!mytime/widget/TimeEntryDetails/templates/TimeEntryDetails.html", + /* Widgets in Template */ + "dijit/form/Form" +], +function ( + _, lang, declare, + domClass, on, query, Evented, dom, + _WidgetBase, _TemplatedMixin, _WidgetsInTemplateMixin, + FilteringSelect, focusUtil, + syncFrom, whenAllPropertiesSet, DateTimeUtil, + TaskPickerCombo, + template) { + + + /** + * The slide out details entry/task details pane + * + * @emit taskSelected {id: string, description: string} + * @emit jiraSelected {string} + */ + return declare([_WidgetBase, _TemplatedMixin, _WidgetsInTemplateMixin, Evented], { + + templateString: template, + + // in template + descriptionNode: null, + jiraKeyNode: null, + unlinkArea: null, + linkedEntryDate: null, + + descriptionInput: null, + jiraKeyInput: null, + + model: null, + + _lastSetJiraKey: null, + + postCreate: function() { + this.own( + whenAllPropertiesSet(this, ["taskStore"], lang.hitch(this, "_initialize")) + ); + }, + + _initialize: function() { + this.descriptionInput = new TaskPickerCombo({ + store: this.taskStore + }); + this.descriptionInput.placeAt(this.descriptionNode); + + this.jiraKeyInput = new FilteringSelect({ + store: this.jiraStore, + searchAttr: 'label', + queryExpr: "${0}" + }); + this.jiraKeyInput.placeAt(this.jiraKeyNode); + + this.own( + on(this.descriptionInput, "userchange", lang.hitch(this, '_onDescriptionChange') ), + on(this.jiraKeyInput, "change", lang.hitch(this, "_onJiraKeyChange")), + this.model.watch('task', lang.hitch(this, "_incomingTaskChange")), + this.model.watch('linkedEntry', lang.hitch(this, "_linkedEntryChange")) + ); + + syncFrom(this.model, "jiraKey", this.jiraKeyInput, "value"); + + }, + + show: function() { + domClass.toggle(this.domNode, 'open', true); + this.descriptionInput.focus(); + this.descriptionInput.textbox.select(); + }, + + hide: function() { + domClass.toggle(this.domNode, 'open', false); + }, + + _onDescriptionChange: function(task) { + on.emit(this, "taskSelected", task); + }, + + _onJiraKeyChange: function() { + var jiraKey = this.jiraKeyInput.get('value'); + jiraKey = jiraKey || null; + if (jiraKey !== this.model.get('jiraKey')) { + on.emit(this, "jiraSelected", jiraKey); + } + }, + + _incomingTaskChange: function(prop, prevValue, value) { + this.descriptionInput.set('task', value); + }, + + _linkedEntryChange: function(prop, prevValue, value) { + if (value && value.date) { + var date = DateTimeUtil.convertDateStringToDateObject(value.date); + date = DateTimeUtil.formatFriendlyDate(date); + this.linkedEntryDate.textContent = date; + } + domClass.toggle(this.unlinkArea, 'show', !!value); + } + + }); +}); \ No newline at end of file diff --git a/script/mytime/widget/TimeEntryDetails/templates/TimeEntryDetails.html b/script/mytime/widget/TimeEntryDetails/templates/TimeEntryDetails.html new file mode 100755 index 0000000..5aeffcf --- /dev/null +++ b/script/mytime/widget/TimeEntryDetails/templates/TimeEntryDetails.html @@ -0,0 +1,15 @@ +
+
+
+ +
+
+ +
+ +
+
\ No newline at end of file diff --git a/styles.css b/styles.css old mode 100644 new mode 100755 index 9b4e019..9229421 --- a/styles.css +++ b/styles.css @@ -112,11 +112,21 @@ div.dayslist { div.timegrid { - width: 300px; + width: 600px; font-family: sans-serif; color: #333; } + .time-entry-pane, div.timegrid { + /* for scrolling */ + height:100%; + } + + .timegrid .scroll-container { + height: 100%; + overflow-y: auto; + } + .timegrid table { width: 100%; border-collapse: collapse; @@ -141,7 +151,7 @@ div.timegrid { border-left-color: black; } - .timegrid tbody th { + .timegrid tbody th, .timegrid thead th:first-child { text-align: right; width: 4.2em; } @@ -149,6 +159,10 @@ div.timegrid { .timegrid th, .timegrid td { border: 1px solid black; } + + .timegrid tbody tr { + height: 40px; + } .timegrid td { position: relative; @@ -224,14 +238,14 @@ div.timegrid { .timegrid .start { border-left-style: solid; - border-top-left-radius: .5em; - border-bottom-left-radius: .5em; + border-top-left-radius: 1em; + border-bottom-left-radius: 1em; } .timegrid .end { border-right-style: solid; - border-top-right-radius: .5em; - border-bottom-right-radius: .5em; + border-top-right-radius: 1em; + border-bottom-right-radius: 1em; } .timegrid .time-bar.selected { @@ -256,95 +270,48 @@ div.timegrid { .timegrid .mark.end-of-day { background-color: rgb(30, 198, 80); } - -div.timelist { - width: 300px; - font-family: sans-serif; - color: black; -} - - .timelist .timeentry { - border: 1px solid black; - border-radius: .4em; - padding: .2em .4em; - margin: 0 0 .1em 0; - font-size: 80%; - position: relative; - opacity: .8; - } - .timelist .timeentry.selected { - border-width: 1px 2px 2px 1px; - opacity: 1; + .timegrid .jira-create-worklog { + font-size: 90%; + text-decoration: none; + color: black; + font-weight: normal; } - .dijitMenuItem .task, - .timelist .timeentry .task { - display: block; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + .timegrid .jira-create-worklog .check { + font-weight: bold; + text-shadow: 1px 2px black; } - .dijitMenuItem .task { - font-size: 80%; + .timegrid .jira-create-worklog:visited .check { + color: white; } - .dijitMenuItem .code, - .timelist .timeentry .code { - font-weight: bold; - padding-right: .2em; - } - - .dijitMenuItem .name, - .timelist .timeentry .name { - font-style: italic; - } - - .timelist .top-right { - position: absolute; - top: 3px; - right: 3px; + .timegrid .jira-create-worklog img { + height: 1em; + width: 1em; } - .timelist .bottom-right { - position: absolute; - bottom: 3px; - right: 3px; - } +div.time-entry-details { + border: 1px solid black; + padding: 5px; + display: none; +} + div.time-entry-details.open { + display: block; + } - .timelist .menu-button img { - vertical-align: bottom; - } + div.time-entry-details .unlink-area { + display: none; + } - .timelist .jira-create-worklog { - font-size: 90%; - text-decoration: none; - color: black; - text-shadow: 1px 2px black; + div.time-entry-details .unlink-area.show { + display: block; + } + + div.time-entry-details .linked-entry-date { + font-weight: bold; } - - .timelist .jira-create-worklog:visited { - color: white; - } - - .timelist .jira-create-worklog img { - height: 1em; - width: 1em; - } - - .timelist .timeentry .note .dijitTextArea { - width: 90%; - } - - .timelist .timeentry input.key { - font-weight: bold; - } - - .timelist .timeentry textarea.note { - width: 100%; - } - div.dayslist { float: left; @@ -355,7 +322,7 @@ div.timegrid { margin-left: .5em; } -div.timelist { +div.time-entry-details { float: left; margin-left: .5em; } diff --git a/test.html b/test.html old mode 100644 new mode 100755 index faba92d..49b94ef --- a/test.html +++ b/test.html @@ -7,7 +7,7 @@ diff --git a/test/MockDependencyLoader.js b/test/MockDependencyLoader.js new file mode 100644 index 0000000..ef38e58 --- /dev/null +++ b/test/MockDependencyLoader.js @@ -0,0 +1,148 @@ +/** + * @license + * Copyright 2014 David Wolverton + * Available under MIT license + * + * This module contributed by Vodori (https://www.vodori.com/) + */ +define([ + 'require', + 'lodash', + 'dojo/_base/lang' +], +function( + require, + _, + lang + ) { + + /** + * A set of utilities that allow AMD modules and dependencies to be mocked. + * + * @note Dojo is assumed. + */ + return { + + /** + * Load a new version of the given module. The module will be loaded with the specified + * dependencies replaced by the given implementations. + * @note The original module must be loaded before this is run. + * @param moduleId + * @param {Object.} dependencyOverrides A map of dependencies to override. Key: + * ID of the module to override. Value: the implementation to use for that module. + * @returns {*} The AMD module. + */ + loadModuleWithOverriddenDependencies: function(moduleId, dependencyOverrides) { + if (!dependencyOverrides) { + dependencyOverrides = {}; + } + var modDef = require.modules[moduleId]; + if (!modDef) { + throw 'The original module (' + moduleId + + ') must be loaded before it can be loaded with loadModuleWithOverriddenDependencies().'; + } + + var dependencies = _.clone(modDef.deps); + var factory = modDef.def; + var exports = {}; + // Use a new module ID, so as not to mess with the original. + var mockMid = this._findNextAvailableUniqueModuleId(modDef.mid + '-mock'); + _.forEach(dependencies, lang.hitch(this, function(dependency, idx) { + if (dependency.mid in dependencyOverrides) { + dependencies[idx] = dependencyOverrides[dependency.mid]; + } else { + switch (dependency.mid) { + case 'module': + var tweakedModule = _.clone(modDef.cjs); + tweakedModule.id = mockMid; + dependencies[idx] = tweakedModule; + break; + case 'exports': + dependencies[idx] = exports; + break; + case 'require': + var tweakedRequire = this.createRequireFunctionWithOverriddenDependencies(modDef.require, dependencyOverrides); + tweakedRequire.module = _.clone(tweakedRequire.module); + tweakedRequire.module.id = mockMid; + dependencies[idx] = tweakedRequire; + break; + default: + dependencies[idx] = dependency.result; + break; + } + } + })); + + var result = factory.apply(null, dependencies); + return _.isUndefined(result) ? exports : result; + }, + + /** + * Append a number to the given base id so that it is unique in the AMD cache. + * @param baseMid + * @returns {string} the unique mid + * @private + */ + _findNextAvailableUniqueModuleId: function(baseMid) { + var mid, + i = 0, + alreadyUsed = true; + while (alreadyUsed) { + i++; + mid = baseMid + i; + alreadyUsed = require.modules[mid] || + lang.getObject(mid) || + lang.getObject(mid.replace(/\//g, '.')); + } + + return mid; + }, + + /** + * Create a new AMD require function that replaces specific modules with a different + * implementation. + * @param {function} originalRequire the original 'require' function to replace + * @param {Object.} dependencyOverrides Key: ID of the module to override. Value: + * the different implementation to use for that module. + * @returns {Function} + */ + createRequireFunctionWithOverriddenDependencies: function(originalRequire, dependencyOverrides) { + var moduleIdMapping = this._mapModulesToMockModuleIds(dependencyOverrides); + + var tweakedRequire = function(dependencyIds) { + if (_.isArray(dependencyIds)) { + _.forEach(dependencyIds, function(mid, idx) { + if (mid in dependencyOverrides) { + dependencyIds[idx] = moduleIdMapping[mid]; + } + }); + } + originalRequire.apply(this, arguments); + }; + + lang.mixin(tweakedRequire, originalRequire); + return tweakedRequire; + }, + + /** + * Add modules to the AMD cache with unique made-up names. + * @param {Object.} modules Key: original module ID. Value: the resolved module. + * @return {Object.} Key: original module ID. Value: the made up name + * under which the module is now cached + * @private + */ + _mapModulesToMockModuleIds: function(modules) { + var mapping = {}; + _.forOwn(modules, lang.hitch(this, function(module, mid) { + var mockId = this._findNextAvailableUniqueModuleId('test-utils/mock'); + this._injectAmdModule(mockId, module); + mapping[mid] = mockId; + })); + return mapping; + }, + + _injectAmdModule: function(mid, module) { + define(mid, [], function() { return module; }); + } + }; +}); \ No newline at end of file diff --git a/test/bootstrap.js b/test/bootstrap.js old mode 100644 new mode 100755 index fa83577..677bde5 --- a/test/bootstrap.js +++ b/test/bootstrap.js @@ -6,7 +6,7 @@ require({ packages: [ { name: 'mytime', location: '../../script/mytime' }, - { name: 'lodash', location: '../lodash/dist', main: 'lodash.min' }, + { name: 'lodash', location: '../lodash', main: 'lodash.min' }, { name: 'test', location: '../../test' }, { name: 'mocha', location: '../mocha', main: 'mocha' }, diff --git a/test/dojo/Stateful.spec.js b/test/dojo/Stateful.spec.js old mode 100644 new mode 100755 diff --git a/test/dojo/store/Observable.spec.js b/test/dojo/store/Observable.spec.js old mode 100644 new mode 100755 diff --git a/test/mytime/DailyTimeWidget.spec.js b/test/mytime/DailyTimeWidget.spec.js old mode 100644 new mode 100755 diff --git a/test/mytime/controller/TimeEntryController.spec.js b/test/mytime/controller/TimeEntryController.spec.js old mode 100644 new mode 100755 diff --git a/test/mytime/model/_ModelBase.spec.js b/test/mytime/model/_ModelBase.spec.js old mode 100644 new mode 100755 diff --git a/test/mytime/persistence/LocalStorage.spec.js b/test/mytime/persistence/LocalStorage.spec.js old mode 100644 new mode 100755 diff --git a/test/mytime/util/DateTimeUtil.spec.js b/test/mytime/util/DateTimeUtil.spec.js old mode 100644 new mode 100755 diff --git a/test/mytime/util/jira.spec.js b/test/mytime/util/jira.spec.js old mode 100644 new mode 100755 diff --git a/test/mytime/util/store/EnhancedMemoryStore.spec.js b/test/mytime/util/store/EnhancedMemoryStore.spec.js old mode 100644 new mode 100755 diff --git a/test/mytime/util/store/SingleDayFilteringTimeEntryStore.spec.js b/test/mytime/util/store/SingleDayFilteringTimeEntryStore.spec.js old mode 100644 new mode 100755 diff --git a/test/mytime/util/store/StoreDrivenDom.spec.js b/test/mytime/util/store/StoreDrivenDom.spec.js old mode 100644 new mode 100755 diff --git a/test/mytime/util/store/TransformingStoreView.spec.js b/test/mytime/util/store/TransformingStoreView.spec.js old mode 100644 new mode 100755 diff --git a/test/mytime/util/store/delegateObserve.spec.js b/test/mytime/util/store/delegateObserve.spec.js old mode 100644 new mode 100755 diff --git a/test/mytime/util/store/getAndObserve.spec.js b/test/mytime/util/store/getAndObserve.spec.js new file mode 100755 index 0000000..77a0a53 --- /dev/null +++ b/test/mytime/util/store/getAndObserve.spec.js @@ -0,0 +1,71 @@ +/** + * @license + * Copyright 2014 David Wolverton + * Available under MIT license + */ +define([ + "dojo/store/Memory", "dojo/store/Observable", + "mytime/util/store/getAndObserve" +], function( + Memory, Observable, + getAndObserve +) { + + describe("mytime/util/store/getAndObserve", function() { + + var store, observable; + var callback; + beforeEach(function() { + store = new Memory(); + store.add({id: "a", sort: 10}); + store.add({id: "b", sort: 20}); + store.add({id: "c", sort: 30}); + observable = new Observable(store); + callback = sinon.spy(); + }); + + it('calls callback with initial result', function() { + getAndObserve(observable, "a", callback); + expect(callback).to.have.been.calledWith({id: "a", sort: 10}); + }); + it('calls callback with null if no result', function() { + getAndObserve(observable, "d", callback); + expect(callback).to.have.been.calledWith(undefined); + }); + it('calls callback when item added', function() { + getAndObserve(observable, "d", callback); + callback.reset(); + observable.add({id: "d", sort: 40}); + expect(callback).to.have.been.calledWith({id: "d", sort: 40}); + }); + it('calls callback when item modified', function() { + getAndObserve(observable, "b", callback); + callback.reset(); + observable.put({id: "b", sort: 50}); + expect(callback).to.have.been.calledWith({id: "b", sort: 50}); + }); + it('calls callback when item removed', function() { + getAndObserve(observable, "c", callback); + callback.reset(); + observable.remove("c"); + expect(callback).to.have.been.calledWith(null); + }); + + it('calls callback with initial result, even if store not observable', function() { + getAndObserve(store, "a", callback); + expect(callback).to.have.been.calledWith({id: "a", sort: 10}); + }); + + it('returns handle to stop observation', function() { + var handle1 = getAndObserve(observable, "a", callback); + var handle2 = getAndObserve(store, "a", callback); + callback.reset(); + handle1.remove(); + handle2.remove(); + + observable.remove("a"); + expect(callback).not.to.have.been.called; + }); + + }); +}); \ No newline at end of file diff --git a/test/mytime/util/whenAllPropertiesSet.spec.js b/test/mytime/util/whenAllPropertiesSet.spec.js old mode 100644 new mode 100755 diff --git a/test/mytime/widget/DailyTimeWidget/DailyTimeWidgetStore.spec.js b/test/mytime/widget/DailyTimeWidget/DailyTimeWidgetStore.spec.js old mode 100644 new mode 100755 index bee496d..28c7b8d --- a/test/mytime/widget/DailyTimeWidget/DailyTimeWidgetStore.spec.js +++ b/test/mytime/widget/DailyTimeWidget/DailyTimeWidgetStore.spec.js @@ -281,16 +281,16 @@ define([ ["b", 12.5, 13]); }); - it("gets code and color from task", function() { + it("gets description and color from task", function() { initSourceStore(); addToSource("a", "2010-10-10", 12, 15, "task-2"); - taskStore.add({id: "task-1", code: "CODE1", color: "red"}); - taskStore.add({id: "task-2", code: "CODE2", color: "yellow"}); + taskStore.add({id: "task-1", description: "CODE1", color: "red"}); + taskStore.add({id: "task-2", description: "CODE2", color: "yellow"}); initStore(); var result = store.get("a"); expect(result).to.have.property("taskId", "task-2"); - expect(result).to.have.property("code", "CODE2"); + expect(result).to.have.property("description", "CODE2"); expect(result).to.have.property("color", "yellow"); }); }); diff --git a/test/mytime/widget/DailyTimeWidget/DailyTimeWidgetView.spec.js b/test/mytime/widget/DailyTimeWidget/DailyTimeWidgetView.spec.js old mode 100644 new mode 100755 index 6da0a58..36ef9ec --- a/test/mytime/widget/DailyTimeWidget/DailyTimeWidgetView.spec.js +++ b/test/mytime/widget/DailyTimeWidget/DailyTimeWidgetView.spec.js @@ -82,8 +82,8 @@ define([ it("starts out empty", function() { createBasicView(); - var table = query("table", view.domNode)[0]; - expect(table.rows.length).to.equal(8); + var table = query("table", view.domNode)[1]; + expect(table.rows.length).to.equal(7); expect(table.rows[1].cells.length).to.equal(2); expect(query(".time-bar").length).to.equal(0); }); @@ -92,47 +92,47 @@ define([ createBasicView(); addTimeEntry("a", 9, 10, "blue"); var bar = getBar(); - expectPosition(bar, 2, 0, 0, true, true); + expectPosition(bar, 1, 0, 0, true, true); }); it("renders a time bar that spans first half hour", function() { createBasicView(); addTimeEntry("a", 9, 9.5, "blue"); var bar = getBar(); - expectPosition(bar, 2, 0, 50, true, true); + expectPosition(bar, 1, 0, 50, true, true); }); it("renders a time bar that spans middle half hour", function() { createBasicView(); addTimeEntry("a", 9.25, 9.75, "blue"); var bar = getBar(); - expectPosition(bar, 2, 25, 25, true, true); + expectPosition(bar, 1, 25, 25, true, true); }); it("renders a two hour chunk", function() { createBasicView(); addTimeEntry("a", 8, 10, "blue"); var bars = getBars(2); - expectPosition(bars[0], 1, 0, 0, true, false); - expectPosition(bars[1], 2, 0, 0, false, true); + expectPosition(bars[0], 0, 0, 0, true, false); + expectPosition(bars[1], 1, 0, 0, false, true); }); it("renders a three hour chunk", function() { createBasicView(); addTimeEntry("a", 8, 11, "blue"); var bars = getBars(3); - expectPosition(bars[0], 1, 0, 0, true, false); - expectPosition(bars[1], 2, 0, 0, false, false); - expectPosition(bars[2], 3, 0, 0, false, true); + expectPosition(bars[0], 0, 0, 0, true, false); + expectPosition(bars[1], 1, 0, 0, false, false); + expectPosition(bars[2], 2, 0, 0, false, true); }); it("renders a chunk, straddling three hours", function() { createBasicView(); addTimeEntry("a", 8.75, 10.25, "blue"); var bars = getBars(3); - expectPosition(bars[0], 1, 75, 0, true, false); - expectPosition(bars[1], 2, 0, 0, false, false); - expectPosition(bars[2], 3, 0, 75, false, true); + expectPosition(bars[0], 0, 75, 0, true, false); + expectPosition(bars[1], 1, 0, 0, false, false); + expectPosition(bars[2], 2, 0, 75, false, true); }); it("changes start time within hour", function() { @@ -140,10 +140,10 @@ define([ addTimeEntry("a", 9, 10, "blue"); changeTimeEntry("a", 9.25); - expectPosition(getBar(), 2, 25, 0, true, true); + expectPosition(getBar(), 1, 25, 0, true, true); changeTimeEntry("a", 9); - expectPosition(getBar(), 2, 0, 0, true, true); + expectPosition(getBar(), 1, 0, 0, true, true); }); it("changes end time within hour", function() { @@ -151,10 +151,10 @@ define([ addTimeEntry("a", 9, 10, "blue"); changeTimeEntry("a", null, 9.5); - expectPosition(getBar(), 2, 0, 50, true, true); + expectPosition(getBar(), 1, 0, 50, true, true); changeTimeEntry("a", null, 9.75); - expectPosition(getBar(), 2, 0, 25, true, true); + expectPosition(getBar(), 1, 0, 25, true, true); }); it("adds hours to end", function() { @@ -163,14 +163,14 @@ define([ changeTimeEntry("a", null, 10.5); var bars = getBars(2) - expectPosition(bars[0], 2, 0, 0, true, false); - expectPosition(bars[1], 3, 0, 50, false, true); + expectPosition(bars[0], 1, 0, 0, true, false); + expectPosition(bars[1], 2, 0, 50, false, true); changeTimeEntry("a", null, 12); var bars = getBars(3); - expectPosition(bars[0], 2, 0, 0, true, false); - expectPosition(bars[1], 3, 0, 0, false, false); - expectPosition(bars[2], 4, 0, 0, false, true); + expectPosition(bars[0], 1, 0, 0, true, false); + expectPosition(bars[1], 2, 0, 0, false, false); + expectPosition(bars[2], 3, 0, 0, false, true); }); it("adds hours to beginning", function() { @@ -179,14 +179,14 @@ define([ changeTimeEntry("a", 9); var bars = getBars(2); - expectPosition(bars[0], 2, 0, 0, true, false); - expectPosition(bars[1], 3, 0, 50, false, true); + expectPosition(bars[0], 1, 0, 0, true, false); + expectPosition(bars[1], 2, 0, 50, false, true); changeTimeEntry("a", 8.75); var bars = getBars(3); - expectPosition(bars[0], 1, 75, 0, true, false); - expectPosition(bars[1], 2, 0, 0, false, false); - expectPosition(bars[2], 3, 0, 50, false, true); + expectPosition(bars[0], 0, 75, 0, true, false); + expectPosition(bars[1], 1, 0, 0, false, false); + expectPosition(bars[2], 2, 0, 50, false, true); }); it("remove hours from end", function() { @@ -195,11 +195,11 @@ define([ changeTimeEntry("a", null, 10.5); var bars = getBars(2); - expectPosition(bars[0], 2, 0, 0, true, false); - expectPosition(bars[1], 3, 0, 50, false, true); + expectPosition(bars[0], 1, 0, 0, true, false); + expectPosition(bars[1], 2, 0, 50, false, true); changeTimeEntry("a", null, 10); - expectPosition(getBar(), 2, 0, 0, true, true); + expectPosition(getBar(), 1, 0, 0, true, true); }); it("remove hours from beginning", function() { @@ -208,18 +208,18 @@ define([ changeTimeEntry("a", 9); var bars = getBars(2); - expectPosition(bars[0], 2, 0, 0, true, false); - expectPosition(bars[1], 3, 0, 50, false, true); + expectPosition(bars[0], 1, 0, 0, true, false); + expectPosition(bars[1], 2, 0, 50, false, true); changeTimeEntry("a", 10.25); - expectPosition(getBar(), 3, 25, 50, true, true); + expectPosition(getBar(), 2, 25, 50, true, true); }); it("moves down to a new hour", function() { createBasicView(); addTimeEntry("a", 9.0, 10.0, "blue"); changeTimeEntry("a", 10.0, 11.0); - expectPosition(getBar(), 3, 0, 0, true, true); + expectPosition(getBar(), 2, 0, 0, true, true); }); // TODO remove time entry diff --git a/test/mytime/widget/TaskForm.spec.js b/test/mytime/widget/TaskForm.spec.js deleted file mode 100644 index 55818f3..0000000 --- a/test/mytime/widget/TaskForm.spec.js +++ /dev/null @@ -1,78 +0,0 @@ -define([ - 'lodash', - 'dojo/on', 'dojo/query', - 'mytime/widget/TaskForm', 'mytime/model/Task' -], function ( - _, - on, query, - TaskForm, Task) { - 'use strict'; - - describe('mytime/widget/TaskForm', function() { - - var form; - - var originTask = new Task({ - id: 'A', - code: 'ABC-100', - name: 'Always be Closing', - color: '60,100' - }); - - beforeEach(function() { - - }); - - afterEach(function() { - - }); - - function getInput(name) { - return _.find(form.form.getChildren(), function(input) { - return input.get('name') === name; - }); - } - - function click(cssSelector) { - var node = query(cssSelector, form.domNode)[0]; - on.emit(node, 'click', { - bubbles: true, - cancelable: true - }); - } - - it('populates the form from the value', function() { - form = new TaskForm({value: originTask}); - expect(getInput('code').get('value')).to.equal('ABC-100'); - expect(getInput('name').get('value')).to.equal('Always be Closing'); - expect(getInput('color').get('value')).to.equal('60,100'); - }); - it('gets the value from the form', function() { - form = new TaskForm({value: originTask}); - getInput('name').set('value', 'Name 2'); - getInput('color').set('value', '200,50'); - var value = form.get('value'); - expect(value instanceof Task).to.be.true; - expect(value).to.have.property('id', 'A'); - expect(value).to.have.property('code', 'ABC-100'); - expect(value).to.have.property('name', 'Name 2'); - expect(value).to.have.property('color', '200,50'); - }); - it('emits submit when click submit', function() { - form = new TaskForm({value: originTask}); - var spy = sinon.spy(); - form.on('submit', spy); - click('input[type=submit]'); - expect(spy).to.be.calledOnce; - expect(spy).to.be.calledWith(form.get('value')); - }); - it('emits cancel when click cancel', function() { - form = new TaskForm({value: originTask}); - var spy = sinon.spy(); - form.on('cancel', spy); - click('a'); - expect(spy).to.be.calledOnce; - }); - - }); -}); \ No newline at end of file diff --git a/test/mytime/widget/TaskPickerCombo.spec.js b/test/mytime/widget/TaskPickerCombo.spec.js old mode 100644 new mode 100755 index ff94cdf..4ce3829 --- a/test/mytime/widget/TaskPickerCombo.spec.js +++ b/test/mytime/widget/TaskPickerCombo.spec.js @@ -18,12 +18,12 @@ define([ expect(parse("")).to.equal(null); expect(parse(" ")).to.equal(null); expect(parse("A")).to.deep.equal(null); - expect(parse("AA")).to.deep.equal({ code: "AA" }); - expect(parse("ARP-123")).to.deep.equal({ code: "ARP-123" }); - expect(parse("ARP-123 ")).to.deep.equal({ code: "ARP-123" }); - expect(parse("ARP-123 h")).to.deep.equal({ code: "ARP-123", name: "h" }); - expect(parse("ARP-123 Hello World")).to.deep.equal({ code: "ARP-123", name: "Hello World" }); - expect(parse("ARP-123 Hello World ")).to.deep.equal({ code: "ARP-123", name: "Hello World" }); + expect(parse("AA")).to.deep.equal({ description: "AA" }); + expect(parse("ARP-123")).to.deep.equal({ description: "ARP-123" }); + expect(parse("ARP-123 ")).to.deep.equal({ description: "ARP-123" }); + expect(parse("ARP-123 h")).to.deep.equal({ description: "ARP-123 h" }); + expect(parse("ARP-123 Hello World")).to.deep.equal({ description: "ARP-123 Hello World" }); + expect(parse("ARP-123 Hello World ")).to.deep.equal({ description: "ARP-123 Hello World" }); }); }); }); \ No newline at end of file diff --git a/test/mytime/widget/TimeEntryDetails.spec.js b/test/mytime/widget/TimeEntryDetails.spec.js new file mode 100755 index 0000000..c78e58b --- /dev/null +++ b/test/mytime/widget/TimeEntryDetails.spec.js @@ -0,0 +1,172 @@ +/** + * @license + * Copyright 2014 David Wolverton + * Available under MIT license + */ +define([ + "lodash", + "dojo/_base/declare", "dojo/Evented", + "mytime/widget/TimeEntryDetails", "mytime/model/Task", "mytime/model/TimeEntry", + "mytime/command/CreateTaskCommand", "mytime/command/UpdateTaskCommand", "mytime/command/UpdateTimeEntryCommand", + "mytime/util/store/EnhancedMemoryStore", + "test/MockDependencyLoader" +], function(_, declare, Evented, TimeEntryDetails, Task, TimeEntry, + CreateTaskCommand, UpdateTaskCommand, UpdateTimeEntryCommand, EnhancedMemoryStore, MockDependencyLoader) { + + describe("mytime/TimeEntryDetails", function() { + + var widget, model, view, timeEntryStore, taskStore; + var taskA, taskB, entryWithTask, entryWithoutTask; + var otherEntry1, otherEntry2, otherEntry3; + var createTaskCommand = setupCommandListener(CreateTaskCommand, 'task'); + var updateTaskCommand = setupCommandListener(UpdateTaskCommand, 'task'); + var updateTimeEntryCommand = setupCommandListener(UpdateTimeEntryCommand, 'timeEntry'); + + var MockView = declare([Evented], { + }); + + var TimeEntryDetailsWithMocks = MockDependencyLoader.loadModuleWithOverriddenDependencies("mytime/widget/TimeEntryDetails", { + "mytime/widget/TimeEntryDetails/TimeEntryDetailsView": MockView + }); + + function setupCommandListener(command, propertyOfCommand) { + var handle; + var listener = sinon.stub(); + + beforeEach(function() { + handle = command.subscribe(function(cmd) { + var result = listener(cmd[propertyOfCommand]); + cmd.resolve(result); + }); + }); + + afterEach(function() { + handle.remove(); + listener.reset(); + }); + return listener; + } + + function initStores() { + timeEntryStore = EnhancedMemoryStore.createObservable(); + taskStore = EnhancedMemoryStore.createObservable(); + + taskStore.add(taskA = new Task({id: "A", description: "Alpha"})); + taskStore.add(taskB = new Task({id: "B", description: "Beta"})); + timeEntryStore.add(entryWithTask = new TimeEntry({id: "has-task", taskId: "A"})); + timeEntryStore.add(otherEntry1 = new TimeEntry({id: "other1", taskId: "A", date: "2015-06-10", startHour: 14})); + timeEntryStore.add(otherEntry2 = new TimeEntry({id: "other2", taskId: "A", date: "2015-06-11", startHour: 6})); + timeEntryStore.add(otherEntry3 = new TimeEntry({id: "other3", taskId: "A", date: "2015-06-11", startHour: 12})); + timeEntryStore.add(entryWithoutTask = new TimeEntry({id: "no-task"})); + } + + function initWidget() { + initStores(); + //widget = new TimeEntryDetails({timeEntryStore: timeEntryStore, taskStore: taskStore}); + widget = new TimeEntryDetails(); + widget.set('timeEntryStore', timeEntryStore); + widget.set('taskStore', taskStore); + model = widget._model; + view = widget._view; + } + + it('updates model and view when selectedId set', function() { + initWidget(); + widget.set('selectedId', 'has-task'); + expect(model.task).to.equal(taskA); + }); + it('appears when selectedId set', function() { + initWidget(); + sinon.stub(view, 'show'); + widget.set('selectedId', 'no-task'); + expect(view.show).to.have.been.called; + }); + it('disappears when selectedId unset', function() { + initWidget(); + widget.set('selectedId', 'no-task'); + sinon.stub(view, 'hide'); + widget.set('selectedId', null); + expect(view.hide).to.have.been.called; + }); + + it('when unique description entered AND no task assigned, creates new task and assigns', function() { + initWidget(); + widget.set('selectedId', 'no-task'); + + createTaskCommand.returns({ taskId: 'C', task: { id: 'C', description: 'something new'}}); + + view.emit('taskSelected', { id: null, description: "something new" }); + expect(createTaskCommand).to.have.been.calledWith({ description: "something new"}); + expect(updateTaskCommand).not.to.have.been.called; + expect(updateTimeEntryCommand).to.have.been.calledWith({ id: 'no-task', taskId: 'C'}); + }); + it('when unique description entered AND already has task, changes name of task', function() { + initWidget(); + widget.set('selectedId', 'has-task'); + + //updateTaskCommand.returns({ taskId: 'A', task: { id: 'A', description: 'something new'}}); + + view.emit('taskSelected', { id: null, description: "something new" }); + expect(createTaskCommand).not.to.have.been.called; + expect(updateTaskCommand).to.have.been.calledWith(new Task({ id: 'A', description: "something new"})); + expect(updateTimeEntryCommand).not.to.have.been.called; + }); + it('when existing description entered AND no task assigned, assigns that task', function() { + initWidget(); + widget.set('selectedId', 'no-task'); + + view.emit('taskSelected', { id: 'B', description: "Beta" }); + expect(createTaskCommand).not.to.have.been.called; + expect(updateTaskCommand).not.to.have.been.called; + expect(updateTimeEntryCommand).to.have.been.calledWith({ id: 'no-task', taskId: 'B'}); + }); + it('when existing description entered AND already has task, assigns that task', function() { + initWidget(); + widget.set('selectedId', 'has-task'); + + view.emit('taskSelected', { id: 'B', description: "Beta" }); + expect(createTaskCommand).not.to.have.been.called; + expect(updateTaskCommand).not.to.have.been.called; + expect(updateTimeEntryCommand).to.have.been.calledWith({ id: 'has-task', taskId: 'B'}); + }); + it('when description unset, unassign task', function() { + initWidget(); + widget.set('selectedId', 'has-task'); + + view.emit('taskSelected', null); + expect(createTaskCommand).not.to.have.been.called; + expect(updateTaskCommand).not.to.have.been.called; + expect(updateTimeEntryCommand).to.have.been.calledWith({ id: 'has-task', taskId: null}); + }); + + + it('when the selected task has other entries, displays the option to fork a new task, including info about latest related time entry', function() { + initWidget(); + widget.set('selectedId', 'has-task'); + expect(model.linkedEntry).to.equal(otherEntry3); + + entryWithTask.taskId = "B"; + timeEntryStore.put(entryWithTask); + expect(model.linkedEntry).to.equal(null); + }); + it('when the selected task has no other entries, does not display the option to fork a new task', function() { + initWidget(); + widget.set('selectedId', 'no-task'); + expect(model.linkedEntry).to.equal(null); + }); + it('when the selected task changes to have no other entries, hides the option to fork a new task', function() { + initWidget(); + widget.set('selectedId', 'has-task'); + + entryWithTask.taskId = "B"; + timeEntryStore.put(entryWithTask); + expect(model.linkedEntry).to.equal(null); + }); + + it('when a JIRA is selected AND task already assigned, set the JIRA key on the task'); + it('when a JIRA is unselected AND task already assigned, unset the JIRA key on the task'); + it('when a JIRA is selected AND no task assigned AND another task exist with same JIRA key, assign that task to the entry'); + it('when a JIRA is selected AND no task assigned AND no other task exists with same JIRA key, create a new task with the JIRA description and assign to the entry'); + }); + +}); \ No newline at end of file diff --git a/test/tests.js b/test/tests.js old mode 100644 new mode 100755 index cd87a68..fd27831 --- a/test/tests.js +++ b/test/tests.js @@ -11,13 +11,14 @@ define([ "test/mytime/util/jira.spec", "test/mytime/util/store/EnhancedMemoryStore.spec", "test/mytime/util/store/delegateObserve.spec", + "test/mytime/util/store/getAndObserve.spec", "test/mytime/util/store/StoreDrivenDom.spec", "test/mytime/util/store/TransformingStoreView.spec", "test/mytime/util/whenAllPropertiesSet.spec", "test/mytime/widget/DailyTimeWidget/DailyTimeWidgetStore.spec", "test/mytime/widget/DailyTimeWidget/DailyTimeWidgetView.spec", - "test/mytime/widget/TaskForm.spec", "test/mytime/widget/TaskPickerCombo.spec", + "test/mytime/widget/TimeEntryDetails.spec", "test/dojo/Stateful.spec", "test/dojo/store/Observable.spec"