diff --git a/app/components/skills-typeahead-result.js b/app/components/skills-typeahead-result.js index 28896ee29..32694a830 100644 --- a/app/components/skills-typeahead-result.js +++ b/app/components/skills-typeahead-result.js @@ -18,7 +18,7 @@ export default Component.extend({ hasSkill: computed('skill', function() { let { skill, skillsList } = getProperties(this, 'skill', 'skillsList'); - return skillsList.contains(skill); + return skillsList.includes(skill); }), selected: alias('skill.selected'), diff --git a/app/controllers/project/tasks/new.js b/app/controllers/project/tasks/new.js index f3eee84d0..e81a8c5ab 100644 --- a/app/controllers/project/tasks/new.js +++ b/app/controllers/project/tasks/new.js @@ -4,21 +4,23 @@ import { isNonValidationError } from 'code-corps-ember/utils/error-utils'; const { Controller, get, + RSVP, set } = Ember; export default Controller.extend({ + unsavedTaskSkills: [], actions: { /** - * saveTask - action - * - * Triggered when user clicks 'save' on the new task form. - * Adds task to the correct task list, which is the inbox task list. - * Saves task and, on success, transitions to the task route. - * On failure, calls the save error handler. - * - * @param {DS.Model} task The task to be saved. - */ + saveTask - action + + Triggered when user clicks 'save' on the new task form. + Adds task to the correct task list, which is the inbox task list. + Saves task and, on success, transitions to the task route. + On failure, calls the save error handler. + + @param {DS.Model} task The task to be saved. + */ async saveTask(task) { let project = get(task, 'project'); let inboxTaskList = await this._getInboxTaskList(project); @@ -26,19 +28,40 @@ export default Controller.extend({ set(task, 'taskList', inboxTaskList); return task.save() + .then((task) => this._saveSkills(task)) .then((task) => this.transitionToRoute('project.tasks.task', get(task, 'number'))) .catch((payload) => this._handleTaskSaveError(payload)); + }, + + deselectSkill(skill) { + let unsavedTaskSkills = get(this, 'unsavedTaskSkills'); + unsavedTaskSkills.removeObject(skill); + }, + + toggleSkill(skill) { + let unsavedTaskSkills = get(this, 'unsavedTaskSkills'); + if (unsavedTaskSkills.includes(skill)) { + unsavedTaskSkills.removeObject(skill); + } else { + unsavedTaskSkills.pushObject(skill); + } } }, + _createTaskSkill(skill, task) { + let store = get(this, 'store'); + return store.createRecord('task-skill', { skill, task }).save(); + }, + /** - * _getInboxTaskList - Private method - * - * Returns a promise, which, when resolved, holds the task list for the specified - * project, which is marked as an inbox task list. - * @param {DS.Model} project The currently loaded project. - * @return {DS.Model} The inbox task list for the specified project. - */ + _getInboxTaskList - Private function + + Returns a promise, which, when resolved, holds the task list for the + specified project, which is marked as an inbox task list. + + @param {DS.Model} project The currently loaded project. + @return {DS.Model} The inbox task list for the specified project. + */ async _getInboxTaskList(project) { let taskLists = await get(project, 'taskLists'); let inboxes = taskLists.filterBy('inbox', true); @@ -47,16 +70,31 @@ export default Controller.extend({ }, /** - * _handleTaskSaveError - Private function - * - * Sets the controller `error` property if the error payloed is anything other - * than a validation error - * - * @param {DS.AdapterError} payload The payload to check. - */ + _handleTaskSaveError - Private function + + Sets the controller `error` property if the error payloed is anything + other than a validation error + + @param {DS.AdapterError} payload The payload to check. + */ _handleTaskSaveError(payload) { if (isNonValidationError(payload)) { set(this, 'error', payload); } + }, + + /** + _saveSkills - Private function + + Saves any of the skills added during the creation of the task. + + @param {DS.Model} task The task being created. + @return {RSVP.Promise} promise that is fulfilled when all `promises` + have been fulfilled, or rejected if any of them become rejected. + */ + async _saveSkills(task) { + let unsavedTaskSkills = get(this, 'unsavedTaskSkills'); + let promises = unsavedTaskSkills.map((skill) => this._createTaskSkill(skill, task)); + return RSVP.all(promises).then(() => task); } }); diff --git a/app/routes/project/tasks/new.js b/app/routes/project/tasks/new.js index c0d0ae47d..8f390af5b 100644 --- a/app/routes/project/tasks/new.js +++ b/app/routes/project/tasks/new.js @@ -48,6 +48,7 @@ export default Route.extend(AuthenticatedRouteMixin, { let task = store.createRecord('task', { project, taskType, user }); set(controller, 'task', task); + set(controller, 'unsavedTaskSkills', []); }, actions: { diff --git a/app/services/project-skills-list.js b/app/services/project-skills-list.js index 7a33690b0..ecd08f0c9 100644 --- a/app/services/project-skills-list.js +++ b/app/services/project-skills-list.js @@ -22,9 +22,9 @@ export default Service.extend({ return store.createRecord('project-skill', { project, skill }).save(); }, - contains(skill) { + includes(skill) { let projectSkills = get(this, 'projectSkills'); - return recordsList.contains(projectSkills, skill); + return recordsList.includes(projectSkills, skill); }, find(skill) { @@ -44,7 +44,7 @@ export default Service.extend({ toggle(skill) { let projectSkills = get(this, 'projectSkills'); - if (recordsList.contains(projectSkills, skill)) { + if (recordsList.includes(projectSkills, skill)) { return this.remove(skill); } else { return this.add(skill); diff --git a/app/services/task-skills-list.js b/app/services/task-skills-list.js index c0a4c2527..804633181 100644 --- a/app/services/task-skills-list.js +++ b/app/services/task-skills-list.js @@ -22,9 +22,9 @@ export default Service.extend({ return store.createRecord('task-skill', { task, skill }).save(); }, - contains(skill) { + includes(skill) { let taskSkills = get(this, 'taskSkills'); - return recordsList.contains(taskSkills, skill); + return recordsList.includes(taskSkills, skill); }, find(skill) { @@ -44,7 +44,7 @@ export default Service.extend({ toggle(skill) { let taskSkills = get(this, 'taskSkills'); - if (recordsList.contains(taskSkills, skill)) { + if (recordsList.includes(taskSkills, skill)) { return this.remove(skill); } else { return this.add(skill); diff --git a/app/services/user-skills-list.js b/app/services/user-skills-list.js index 8ccf03233..15c5e438e 100644 --- a/app/services/user-skills-list.js +++ b/app/services/user-skills-list.js @@ -24,9 +24,9 @@ export default Service.extend({ return store.createRecord('user-skill', { user, skill }).save(); }, - contains(skill) { + includes(skill) { let userSkills = get(this, 'userSkills'); - return recordsList.contains(userSkills, skill); + return recordsList.includes(userSkills, skill); }, find(skill) { @@ -41,7 +41,7 @@ export default Service.extend({ toggle(skill) { let userSkills = get(this, 'userSkills'); - if (recordsList.contains(userSkills, skill)) { + if (recordsList.includes(userSkills, skill)) { return this.remove(skill); } else { return this.add(skill); diff --git a/app/templates/project/tasks/new.hbs b/app/templates/project/tasks/new.hbs index 92ec95a09..a8e263d83 100644 --- a/app/templates/project/tasks/new.hbs +++ b/app/templates/project/tasks/new.hbs @@ -6,5 +6,31 @@ {{/if}}
diff --git a/app/utils/records-list.js b/app/utils/records-list.js index bd7cbb1fd..ff3921ce0 100644 --- a/app/utils/records-list.js +++ b/app/utils/records-list.js @@ -12,7 +12,7 @@ export default { @return {Boolean} `true` if found, otherwise `false` @public */ - contains(records, target) { + includes(records, target) { if (records) { return records.any((found) => { let targetId = get(target, 'id'); diff --git a/tests/acceptance/task-creation-test.js b/tests/acceptance/task-creation-test.js index f658f54cf..c2cefa2f6 100644 --- a/tests/acceptance/task-creation-test.js +++ b/tests/acceptance/task-creation-test.js @@ -301,3 +301,59 @@ test('Navigation is aborted if user answers negatively to prompt', function(asse stub.restore(); }); }); + +test('Skills can be assigned to task during creation', function(assert) { + assert.expect(6); + + let user = server.schema.users.create({ username: 'test_user' }); + + let project = createProjectWithSluggedRoute(); + let { organization } = project; + project.createTaskList({ inbox: true }); + + authenticateSession(this.application, { user_id: user.id }); + + projectTasksIndexPage.visit({ organization: organization.slug, project: project.slug }); + + andThen(() => { + projectTasksIndexPage.clickNewTask(); + }); + + let skill = server.create('skill', { title: 'Ruby' }); + + andThen(() => { + assert.equal(currentRouteName(), 'project.tasks.new', 'Button takes us to the proper route'); + assert.equal(find('[name=task-type]').val(), 'issue', 'Has the right default task type'); + + projectTasksNewPage.taskTitle('A task title') + .taskMarkdown('A task body') + .taskType('idea'); + }); + + // NOTE: We need to be doing this async, so the code is ugly + // Possibly switching to await/async might make it nicer + andThen(() => { + // find skill + projectTasksNewPage.skillsTypeahead.fillIn('ru'); + }); + + andThen(() => { + // add skill + projectTasksNewPage.skillsTypeahead.inputItems(0).click(); + }); + + andThen(() => { + projectTasksNewPage.clickSubmit(); + }); + + andThen(() => { + assert.equal(server.schema.tasks.all().models.length, 1, 'A task has been created'); + assert.equal(server.schema.taskSkills.all().models.length, 1, 'A single task skill has been created.'); + + let [task] = server.schema.tasks.all().models; + let [taskSkill] = server.schema.taskSkills.all().models; + + assert.equal(taskSkill.taskId, task.id, 'The correct task was assigned'); + assert.equal(taskSkill.skillId, skill.id, 'The correct skill was assigned'); + }); +}); diff --git a/tests/integration/components/skill-list-item-test.js b/tests/integration/components/skill-list-item-test.js index b406adbc5..38f065bcb 100644 --- a/tests/integration/components/skill-list-item-test.js +++ b/tests/integration/components/skill-list-item-test.js @@ -23,7 +23,7 @@ moduleForComponent('skill-list-item', 'Integration | Component | skill list item }, beforeEach() { stubService(this, 'user-skills-list', { - contains() { + includes() { return true; }, find(skill) { diff --git a/tests/integration/components/skill-list-items-test.js b/tests/integration/components/skill-list-items-test.js index 6e3adf6c3..a0d906af6 100644 --- a/tests/integration/components/skill-list-items-test.js +++ b/tests/integration/components/skill-list-items-test.js @@ -30,7 +30,7 @@ moduleForComponent('skill-list-items', 'Integration | Component | skill list ite integration: true, beforeEach() { stubService(this, 'user-skills-list', { - contains(queriedSkill) { + includes(queriedSkill) { return queriedSkill === skills[1]; }, find(queriedSkill) { diff --git a/tests/integration/components/skills-typeahead-result-test.js b/tests/integration/components/skills-typeahead-result-test.js index 95ad091d7..41ada0386 100644 --- a/tests/integration/components/skills-typeahead-result-test.js +++ b/tests/integration/components/skills-typeahead-result-test.js @@ -14,7 +14,7 @@ let skill = Object.create({ }); let userSkillsList = { - contains() { + includes() { return true; }, find(queriedSkill) { diff --git a/tests/integration/components/skills-typeahead-test.js b/tests/integration/components/skills-typeahead-test.js index 71fd4d02c..cfa9de0c2 100644 --- a/tests/integration/components/skills-typeahead-test.js +++ b/tests/integration/components/skills-typeahead-test.js @@ -26,7 +26,7 @@ let mockStore = { }; let mockListService = { - contains(queriedSkill) { + includes(queriedSkill) { return queriedSkill === skills[1]; }, find(queriedSkill) { diff --git a/tests/pages/project/tasks/new.js b/tests/pages/project/tasks/new.js index 1c9141c09..c801ad983 100644 --- a/tests/pages/project/tasks/new.js +++ b/tests/pages/project/tasks/new.js @@ -3,10 +3,12 @@ import { collection, create, fillable, + text, visitable } from 'ember-cli-page-object'; import projectMenu from 'code-corps-ember/tests/pages/components/project-menu'; +import skillsTypeahead from 'code-corps-ember/tests/pages/components/skills-typeahead'; export default create({ clickPreviewTask: clickable('.preview'), @@ -26,5 +28,16 @@ export default create({ scope: '.body-preview' }, + skillsTypeahead, + + taskSkillsList: collection({ + scope: '.task-skills-list', + itemScope: 'button', + item: { + text: text(), + click: clickable() + } + }), + visit: visitable(':organization/:project/tasks/new') }); diff --git a/tests/unit/utils/records-list-test.js b/tests/unit/utils/records-list-test.js index c579ebe51..9c871213d 100644 --- a/tests/unit/utils/records-list-test.js +++ b/tests/unit/utils/records-list-test.js @@ -4,7 +4,7 @@ import { module, test } from 'qunit'; const { isEmpty, Object } = Ember; -module('Unit | Utility | skills-list'); +module('Unit | Utility | records-list'); let projectSkill = Object.create({ belongsTo(relationshipName) { @@ -53,13 +53,13 @@ test('find returns no match correctly', function(assert) { assert.ok(isEmpty(result)); }); -test('contains returns true when there is a match', function(assert) { +test('includes returns true when there is a match', function(assert) { let projectSkills = [projectSkill]; - let result = recordsList.contains(projectSkills, skill); + let result = recordsList.includes(projectSkills, skill); assert.ok(result); }); -test('contains returns false when there is no match', function(assert) { - let result = recordsList.contains([], skill); +test('includes returns false when there is no match', function(assert) { + let result = recordsList.includes([], skill); assert.notOk(result); });