diff --git a/app/components/competency-title-editor.js b/app/components/competency-title-editor.js new file mode 100644 index 0000000000..522b903f90 --- /dev/null +++ b/app/components/competency-title-editor.js @@ -0,0 +1,58 @@ +import Ember from 'ember'; +import { validator, buildValidations } from 'ember-cp-validations'; +import ValidationErrorDisplay from 'ilios/mixins/validation-error-display'; +import { task } from 'ember-concurrency'; + +const { Component, RSVP, isPresent } = Ember; +const { Promise } = RSVP; + +const Validations = buildValidations({ + title: [ + validator('presence', true), + validator('length', { + max: 200 + }), + ], +}); + +export default Component.extend(Validations, ValidationErrorDisplay, { + didReceiveAttrs(){ + this._super(...arguments); + const competency = this.get('competency'); + if (isPresent(competency)) { + this.set('title', competency.get('title')); + } + }, + title: null, + competency: null, + classNames: ['competency-title-editor'], + tagName: 'span', + save: task(function * (){ + this.send('addErrorDisplayFor', 'title'); + let {validations} = yield this.validate(); + + return new Promise((resolve, reject) => { + if (validations.get('isInvalid')) { + reject(); + } else { + const competency = this.get('competency'); + const title = this.get('title'); + if (isPresent(competency)) { + competency.set('title', title); + } + this.send('clearErrorDisplay'); + resolve(); + } + }); + + }), + actions: { + revert() { + this.set('title', null); + const competency = this.get('competency'); + if (isPresent(competency)) { + this.set('title', competency.get('title')); + } + }, + } +}); diff --git a/app/components/new-competency.js b/app/components/new-competency.js new file mode 100644 index 0000000000..bb7e7fda7d --- /dev/null +++ b/app/components/new-competency.js @@ -0,0 +1,32 @@ +import Ember from 'ember'; +import { validator, buildValidations } from 'ember-cp-validations'; +import ValidationErrorDisplay from 'ilios/mixins/validation-error-display'; +import { task, timeout } from 'ember-concurrency'; + +const { Component } = Ember; + +const Validations = buildValidations({ + title: [ + validator('presence', true), + validator('length', { + max: 200 + }), + ], +}); + +export default Component.extend(Validations, ValidationErrorDisplay, { + title: null, + classNames: ['new-competency'], + save: task(function * (){ + this.send('addErrorDisplayFor', 'title'); + let {validations} = yield this.validate(); + if (validations.get('isInvalid')) { + return; + } + yield timeout(10); + const title = this.get('title'); + yield this.get('add')(title); + this.send('clearErrorDisplay'); + this.set('title', null); + }) +}); diff --git a/app/components/school-competencies-expanded.js b/app/components/school-competencies-expanded.js index 49b5a2e8fa..8ec8a0b440 100644 --- a/app/components/school-competencies-expanded.js +++ b/app/components/school-competencies-expanded.js @@ -30,7 +30,7 @@ export default Component.extend({ } }); }, - addCompetencyToBuffer(title, domain){ + addCompetencyToBuffer(domain, title){ let competency = this.get('store').createRecord('competency', {title}); if (isPresent(domain)) { competency.set('parent', domain); @@ -68,8 +68,8 @@ export default Component.extend({ }); bufferedCompetencies.filterBy('isNew').forEach(competency => { competency.set('school', school); - promises.pushObject(competency.save()); }); + promises.pushObjects(bufferedCompetencies.filterBy('hasDirtyAttributes').invoke('save')); schoolCompetencies.clear(); bufferedCompetencies.forEach(competency=>{ schoolCompetencies.pushObject(competency); diff --git a/app/components/school-competencies-manager.js b/app/components/school-competencies-manager.js index c4712c924e..8f6d19d938 100644 --- a/app/components/school-competencies-manager.js +++ b/app/components/school-competencies-manager.js @@ -1,13 +1,9 @@ import Ember from 'ember'; -const { Component, computed, isPresent, isEmpty } = Ember; -const { sort } = computed; +const { Component, computed, isEmpty } = Ember; export default Component.extend({ competencies: [], - sortCompetenciesBy: ['title'], - sortedCompetencies: sort('competencies', 'sortCompetenciesBy'), - newDomainValue: null, domains: computed('competencies.[]', function(){ let competencies = this.get('competencies'); @@ -16,7 +12,7 @@ export default Component.extend({ } let domains = competencies.filterBy('isDomain'); let objs = domains.uniq().map(domain => { - let domainCompetencies = competencies.filter(competency => competency.get('parent.id') === domain.get('id')); + let domainCompetencies = competencies.filter(competency => competency.belongsTo('parent').id() === domain.get('id')); return { domain, competencies: domainCompetencies.sortBy('title') @@ -26,41 +22,8 @@ export default Component.extend({ return objs.sortBy('domain.title'); }), actions: { - createNewDomain(){ - let value = this.get('newDomainValue'); - if (isPresent(value)) { - this.attrs.add(value); - this.set('newDomainValue', null); - } - }, - createNewCompetencyFromButton(e){ - let domainId = parseInt(e.target.value); - let value = this.$(e.target).parent().find('input').val(); - let objs = this.get('domains'); - let obj = objs.find(obj => parseInt(obj.domain.get('id')) === domainId); - let domain = obj.domain; - if (isPresent(value) && isPresent(domain)) { - this.attrs.add(value, domain); - } - - }, - createNewCompetencyFromInput(e){ - if (e.keyCode === 13) { - let value = e.target.value; - let domainId = parseInt(this.$(e.target).parent().find('button').val()); - - let objs = this.get('domains'); - let obj = objs.find(obj => parseInt(obj.domain.get('id')) === domainId); - let domain = obj.domain; - if (isPresent(value) && isPresent(domain)) { - this.attrs.add(value, domain); - } - } - - }, changeCompetencyTitle(value, competency){ competency.set('title', value); - competency.save(); } } }); diff --git a/app/templates/components/competency-title-editor.hbs b/app/templates/components/competency-title-editor.hbs new file mode 100644 index 0000000000..3bf2594837 --- /dev/null +++ b/app/templates/components/competency-title-editor.hbs @@ -0,0 +1,18 @@ +{{#editable-field + value=title + save=(perform save) + close=(action 'revert') + as |isSaving save close| +}} + {{one-way-input + value=title + update=(action (mut title)) + onenter=save + onescape=close + disabled=isSaving + focusOut=(action 'addErrorDisplayFor' 'title') + }} +{{/editable-field}} +{{#if (and (v-get this 'title' 'isInvalid') (is-in showErrorsFor 'title'))}} + {{v-get this 'title' 'message'}} +{{/if}} diff --git a/app/templates/components/new-competency.hbs b/app/templates/components/new-competency.hbs new file mode 100644 index 0000000000..5d19b72b48 --- /dev/null +++ b/app/templates/components/new-competency.hbs @@ -0,0 +1,19 @@ +{{one-way-input + value=title + update=(action (mut title)) + onenter=(perform save) + onescape=(pipe (action 'removeErrorDisplayFor' 'title') (action (mut title) '')) + focusOut=(action 'addErrorDisplayFor' 'title') + keyDown=(action 'addErrorDisplayFor' 'title') + placeholder=(t 'general.title') +}} + +{{#if (and (v-get this 'title' 'isInvalid') (is-in showErrorsFor 'title'))}} + {{v-get this 'title' 'message'}} +{{/if}} diff --git a/app/templates/components/school-competencies-manager.hbs b/app/templates/components/school-competencies-manager.hbs index 66622412f5..86d55f828d 100644 --- a/app/templates/components/school-competencies-manager.hbs +++ b/app/templates/components/school-competencies-manager.hbs @@ -1,13 +1,13 @@ {{#each domains as |obj|}}
- {{inplace-text tagName='h4' value=obj.domain.title save='changeCompetencyTitle' condition=obj.domain}} + {{competency-title-editor competency=obj.domain}} {{#if (eq obj.competencies.length 0)}} {{fa-icon 'trash' class='remove clickable' click=(action this.attrs.remove obj.domain)}} {{/if}}
{{/each}} -
- - {{one-way-input - value=newDomainValue - update=(action (mut newDomainValue)) - onenter=(action 'createNewDomain') - }} - -
+
{{t 'schools.newDomain'}}
+{{new-competency add=(action add null)}} diff --git a/tests/integration/components/competency-title-editor-test.js b/tests/integration/components/competency-title-editor-test.js new file mode 100644 index 0000000000..3614cb7eb6 --- /dev/null +++ b/tests/integration/components/competency-title-editor-test.js @@ -0,0 +1,34 @@ +import Ember from 'ember'; +import { moduleForComponent, test } from 'ember-qunit'; +import hbs from 'htmlbars-inline-precompile'; +import wait from 'ember-test-helpers/wait'; + +const { Object } = Ember; + +moduleForComponent('competency-title-editor', 'Integration | Component | competency title editor', { + integration: true +}); + +test('validation errors do not show up initially', function(assert) { + assert.expect(1); + let competency = Object.create({ + title: 'test' + }); + this.set('competency', competency); + this.render(hbs`{{competency-title-editor competency=competency}}`); + assert.equal(this.$('.validation-error-message').length, 0); +}); + +test('validation errors show up when saving', function(assert) { + assert.expect(1); + let competency = Object.create({ + title: 'test' + }); + this.set('competency', competency); + this.render(hbs`{{competency-title-editor competency=competency}}`); + this.$('.content span:eq(0)').click(); + this.$('input').val('').change(); + this.$('button.done').click(); + assert.equal(this.$('.validation-error-message').length, 1); + return wait(); +}); diff --git a/tests/integration/components/new-competency-test.js b/tests/integration/components/new-competency-test.js new file mode 100644 index 0000000000..e5d07e37d6 --- /dev/null +++ b/tests/integration/components/new-competency-test.js @@ -0,0 +1,42 @@ +import { moduleForComponent, test } from 'ember-qunit'; +import hbs from 'htmlbars-inline-precompile'; +import wait from 'ember-test-helpers/wait'; + +moduleForComponent('new-competency', 'Integration | Component | new competency', { + integration: true +}); + +test('it renders', function(assert) { + this.render(hbs`{{new-competency}}`); + + assert.equal(this.$('input').length, 1); + assert.equal(this.$('button').text().trim(), 'Add'); +}); + +test('save', function(assert) { + assert.expect(1); + this.set('add', (value) => { + assert.equal(value, 'new co'); + }); + this.render(hbs`{{new-competency add=(action add)}}`); + this.$('input').val('new co').change(); + this.$('button').click(); + + return wait(); +}); + +test('validation errors do not show up initially', function(assert) { + assert.expect(1); + + this.render(hbs`{{new-competency}}`); + assert.equal(this.$('.validation-error-message').length, 0); +}); + +test('validation errors show up when saving', function(assert) { + assert.expect(1); + + this.render(hbs`{{new-competency}}`); + this.$('button.save').click(); + assert.equal(this.$('.validation-error-message').length, 1); + return wait(); +}); diff --git a/tests/integration/components/school-competencies-manager-test.js b/tests/integration/components/school-competencies-manager-test.js index 7563d05b71..cbba63a193 100644 --- a/tests/integration/components/school-competencies-manager-test.js +++ b/tests/integration/components/school-competencies-manager-test.js @@ -1,29 +1,126 @@ +import Ember from 'ember'; import { moduleForComponent, test } from 'ember-qunit'; import hbs from 'htmlbars-inline-precompile'; +import wait from 'ember-test-helpers/wait'; + +const { Object } = Ember; + +let Competency = Object.extend({ + id: null, + title: null, + parent: null, + children: [], + isDomain: false, + objectives: [], + belongsTo(){ + let self = this; + return { + id(){ + let parent = self.parent; + if (parent) { + return parent.id; + } + + return null; + } + } + } +}); moduleForComponent('school-competencies-manager', 'Integration | Component | school competencies manager', { integration: true }); test('it renders', function(assert) { - // Set any properties with this.set('myProperty', 'value'); - // Handle any actions with this.on('myAction', function(val) { ... });" + let domain1 = Competency.create({ + id: 1, + title: 'domain1', + isDomain: true, + }); + let competency1 = Competency.create({ + id: 2, + title: 'competency1', + parent: domain1, + objectives: [1, 2, 3] + }); + let competency2 = Competency.create({ + id: 3, + title: 'competency2', + parent: domain1 + }); + domain1.set('children', [competency1, competency2]); + + let competencies = [domain1, competency1, competency2]; + this.set('competencies', competencies); + this.set('nothing', parseInt); + this.render(hbs`{{school-competencies-manager add=(action nothing) remove=(action nothing) competencies=competencies}}`); - this.render(hbs`{{school-competencies-manager}}`); + const title = 'h5'; + const input = 'input'; + const domains = '.hierarchical-list-manager'; + const domain1Title = `${domains} .competency-title-editor:eq(0)`; + const comp1Title = `${domains}:eq(0) li:eq(0)`; + const comp1Delete = `${domains}:eq(0) li:eq(0) i`; + const comp2Title = `${domains}:eq(0) li:eq(1)`; + const comp2Delete = `${domains}:eq(0) li:eq(1) i`; - assert.equal(this.$('label').text().trim(), 'New Domain'); - assert.equal(this.$('button').text().trim(), 'Add'); + assert.equal(this.$(title).text().trim(), 'New Domain'); + assert.equal(this.$(input).length, 2); + assert.equal(this.$(input).attr('placeholder'), 'Title'); + assert.equal(this.$(domain1Title).text().trim(), 'domain1'); + assert.equal(this.$(comp1Title).text().replace(/[\t\n\s]+/g, ""), 'competency1(3)'); + assert.equal(this.$(comp2Title).text().replace(/[\t\n\s]+/g, ""), 'competency2'); + assert.equal(this.$(comp1Delete).length, 0); + assert.equal(this.$(comp2Delete).length, 1); }); -test('add new domain', function(assert) { - assert.expect(1); - this.on('add', (value) => { - assert.equal(value, 'new domain'); +test('delete fires delete', function(assert) { + let domain1 = Competency.create({ + id: 1, + title: 'domain1', + isDomain: true, }); - this.render(hbs`{{school-competencies-manager add=(action 'add')}}`); - this.$('input').val('new domain'); - this.$('input').trigger('change'); - this.$('button').click(); + let competencies = [domain1]; + this.set('competencies', competencies); + this.set('nothing', parseInt); + this.set('remove', (what) => { + assert.equal(what, domain1); + }) + this.render(hbs`{{school-competencies-manager add=(action nothing) remove=(action remove) competencies=competencies}}`); + + const domains = '.hierarchical-list-manager'; + const domain1Icon = `${domains} i`; + + + this.$(domain1Icon).click(); + return wait(); +}); + +test('add fires add', function(assert) { + let domain1 = Competency.create({ + id: 1, + title: 'domain1', + isDomain: true, + }); + + let competencies = [domain1]; + this.set('competencies', competencies); + this.set('nothing', parseInt); + this.set('add', (what, title) => { + assert.equal(what, domain1); + assert.equal(title, 'new c'); + }) + this.render(hbs`{{school-competencies-manager add=(action add) remove=(action nothing) competencies=competencies}}`); + + const domains = '.hierarchical-list-manager'; + const domain1Input = `${domains} input:eq(0)`; + const domain1Add = `${domains} button:eq(0)`; + + + this.$(domain1Input).val('new c').change(); + this.$(domain1Add).click(); + + return wait(); }); diff --git a/tests/integration/components/search-box-test.js b/tests/integration/components/search-box-test.js index ba6f9186a7..54c33e93e0 100644 --- a/tests/integration/components/search-box-test.js +++ b/tests/integration/components/search-box-test.js @@ -16,15 +16,15 @@ test('it renders', function(assert) { assert.equal(this.$('input[type=search]').length, 1); }); -test('clicking search focus input and calls search', function(assert) { +test('clicking search calls search', function(assert) { this.on('search', function(value){ assert.equal(value, ''); }); this.render(hbs`{{search-box search=(action 'search')}}`); - run(() => { - this.$('span').click(); - assert.ok(this.$('input').is(":focus")); - }); + const searchBoxIcon = '.search-icon'; + this.$(searchBoxIcon).click(); + + return wait(); }); test('typing calls search', function(assert) {