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) {