From c014f2bd808016b34415c66e12748a09a737cb41 Mon Sep 17 00:00:00 2001 From: Michael Chadwick Date: Tue, 12 Aug 2025 17:08:25 -0700 Subject: [PATCH 1/5] created DashboardTermsCalendarFilterComponent --- .../dashboard/terms-calendar-filter.gjs | 105 ++++++++++++++++++ .../dashboard/terms-calendar-filter.js | 1 + 2 files changed, 106 insertions(+) create mode 100644 packages/ilios-common/addon/components/dashboard/terms-calendar-filter.gjs create mode 100644 packages/ilios-common/app/components/dashboard/terms-calendar-filter.js diff --git a/packages/ilios-common/addon/components/dashboard/terms-calendar-filter.gjs b/packages/ilios-common/addon/components/dashboard/terms-calendar-filter.gjs new file mode 100644 index 0000000000..d8f77fdd79 --- /dev/null +++ b/packages/ilios-common/addon/components/dashboard/terms-calendar-filter.gjs @@ -0,0 +1,105 @@ +import Component from '@glimmer/component'; +import { cached, tracked } from '@glimmer/tracking'; +import { action } from '@ember/object'; +import { service } from '@ember/service'; +import { TrackedAsyncData } from 'ember-async-data'; +import { mapBy, sortBy } from 'ilios-common/utils/array-helpers'; +import t from 'ember-intl/helpers/t'; +import FaIcon from 'ilios-common/components/fa-icon'; +import SelectedVocabulary from 'ilios-common/components/dashboard/selected-vocabulary'; + +export default class DashboardTermsCalendarFilterComponent extends Component { + @service dataLoader; + @service iliosConfig; + + @tracked vocabulariesInView = []; + @tracked titlesInView = []; + + @cached + get vocabulariesData() { + return new TrackedAsyncData(this.loadVocabularies(this.args.school)); + } + + get vocabularies() { + return this.vocabulariesData.isResolved ? this.vocabulariesData.value : []; + } + + get vocabulariesLoaded() { + return this.vocabulariesData.isResolved; + } + + async loadVocabularies(school) { + await this.dataLoader.loadSchoolForCalendar(school.id); + const vocabularies = await school.vocabularies; + await Promise.all(mapBy(vocabularies, 'terms')); + return sortBy(vocabularies, 'title'); + } + + @action + addVocabularyInView(vocabulary) { + if (!this.vocabulariesInView.includes(vocabulary)) { + this.vocabulariesInView = [...this.vocabulariesInView, vocabulary]; + } + } + @action + removeVocabularyInView(vocabulary) { + this.vocabulariesInView = this.vocabulariesInView.filter( + (theVocabulary) => theVocabulary !== vocabulary, + ); + } + @action + addTitleInView(title) { + if (!this.titlesInView.includes(title)) { + this.titlesInView = [...this.titlesInView, title]; + } + } + @action + removeTitleInView(title) { + this.titlesInView = this.titlesInView.filter((theTitle) => theTitle !== title); + } + + get vocabularyWithoutTitleView() { + const vocabulariesWithNoTitle = this.vocabulariesInView.filter( + (vocabulary) => !this.titlesInView.includes(vocabulary), + ); + const expandedVocabulariesWithNoTitle = vocabulariesWithNoTitle.filter((vocabulary) => + this.vocabulariesInView.includes(vocabulary), + ); + + if (expandedVocabulariesWithNoTitle.length) { + return expandedVocabulariesWithNoTitle[0]; + } + + return null; + } + +} diff --git a/packages/ilios-common/app/components/dashboard/terms-calendar-filter.js b/packages/ilios-common/app/components/dashboard/terms-calendar-filter.js new file mode 100644 index 0000000000..b962a69f51 --- /dev/null +++ b/packages/ilios-common/app/components/dashboard/terms-calendar-filter.js @@ -0,0 +1 @@ +export { default } from 'ilios-common/components/dashboard/terms-calendar-filter'; From efda330b2f7c7d6972713684c2c12c714a7d66fd Mon Sep 17 00:00:00 2001 From: Michael Chadwick Date: Tue, 12 Aug 2025 17:09:57 -0700 Subject: [PATCH 2/5] swap out vocabulary filter in CalendarFilters component for new TermsCalendarFilter component --- .../components/dashboard/calendar-filters.gjs | 30 +++++-------------- .../dashboard/selected-vocabulary.gjs | 20 +++++++++++-- 2 files changed, 26 insertions(+), 24 deletions(-) diff --git a/packages/ilios-common/addon/components/dashboard/calendar-filters.gjs b/packages/ilios-common/addon/components/dashboard/calendar-filters.gjs index f2a91809ed..c435467b40 100644 --- a/packages/ilios-common/addon/components/dashboard/calendar-filters.gjs +++ b/packages/ilios-common/addon/components/dashboard/calendar-filters.gjs @@ -9,8 +9,8 @@ import FilterCheckbox from 'ilios-common/components/dashboard/filter-checkbox'; import includes from 'ilios-common/helpers/includes'; import { fn } from '@ember/helper'; import FaIcon from 'ilios-common/components/fa-icon'; -import SelectedVocabulary from 'ilios-common/components/dashboard/selected-vocabulary'; import CohortCalendarFilter from 'ilios-common/components/dashboard/cohort-calendar-filter'; +import TermsCalendarFilter from 'ilios-common/components/dashboard/terms-calendar-filter'; export default class DashboardCalendarFiltersComponent extends Component { @service dataLoader; @@ -93,27 +93,13 @@ export default class DashboardCalendarFiltersComponent extends Component { {{/if}} -
-

- {{t "general.terms"}} -

-
- {{#if this.vocabulariesLoaded}} -
    - {{#each this.vocabularies as |vocabulary|}} - - {{/each}} -
- {{else}} - - {{/if}} -
-
+ {{else}}
-
  • -

    +
  • +

    {{@vocabulary.title}}

    {{#each (sortBy "title" this.topLevelTerms) as |term|}} From ac3aa34b11d5c278a8d3f8271cbf1744785c1b8f Mon Sep 17 00:00:00 2001 From: Michael Chadwick Date: Tue, 12 Aug 2025 17:11:01 -0700 Subject: [PATCH 3/5] created test page-object and simple integration test (for now) --- .../dashboard/terms-calendar-filter.js | 16 +++ .../dashboard/terms-calendar-filter-test.gjs | 136 ++++++++++++++++++ 2 files changed, 152 insertions(+) create mode 100644 packages/ilios-common/addon-test-support/ilios-common/page-objects/components/dashboard/terms-calendar-filter.js create mode 100644 packages/test-app/tests/integration/components/dashboard/terms-calendar-filter-test.gjs diff --git a/packages/ilios-common/addon-test-support/ilios-common/page-objects/components/dashboard/terms-calendar-filter.js b/packages/ilios-common/addon-test-support/ilios-common/page-objects/components/dashboard/terms-calendar-filter.js new file mode 100644 index 0000000000..27bb60bc35 --- /dev/null +++ b/packages/ilios-common/addon-test-support/ilios-common/page-objects/components/dashboard/terms-calendar-filter.js @@ -0,0 +1,16 @@ +import { collection, clickable, create, property, text } from 'ember-cli-page-object'; + +const definition = { + scope: '[data-test-vocabulary-filter]', + vocabularies: collection('[data-test-dashboard-selected-vocabulary]', { + title: text('[data-test-title]'), + terms: collection('[data-test-selected-term-tree]', { + title: text(), + toggle: clickable('[data-test-target]'), + isChecked: property('checked', '[data-test-target] input'), + }), + }), +}; + +export default definition; +export const component = create(definition); diff --git a/packages/test-app/tests/integration/components/dashboard/terms-calendar-filter-test.gjs b/packages/test-app/tests/integration/components/dashboard/terms-calendar-filter-test.gjs new file mode 100644 index 0000000000..d964a214c8 --- /dev/null +++ b/packages/test-app/tests/integration/components/dashboard/terms-calendar-filter-test.gjs @@ -0,0 +1,136 @@ +import { module, skip, test } from 'qunit'; +import { setupRenderingTest } from 'test-app/tests/helpers'; +import { render } from '@ember/test-helpers'; +import { setupMirage } from 'test-app/tests/test-support/mirage'; +import { component } from 'ilios-common/page-objects/components/dashboard/terms-calendar-filter'; +import { a11yAudit } from 'ember-a11y-testing/test-support'; +import TermsCalendarFilter from 'ilios-common/components/dashboard/terms-calendar-filter'; +import noop from 'ilios-common/helpers/noop'; +import { array } from '@ember/helper'; + +module('Integration | Component | dashboard/terms-calendar-filter', function (hooks) { + setupRenderingTest(hooks); + setupMirage(hooks); + + hooks.beforeEach(async function () { + this.school = this.server.create('school'); + this.server.create('vocabulary', { + school: this.school, + active: true, + }); + this.server.create('vocabulary', { + school: this.school, + active: true, + }); + this.server.create('term', { + vocabularyId: 1, + active: true, + }); + this.server.create('term', { + vocabularyId: 1, + active: true, + }); + this.server.create('term', { + vocabularyId: 2, + active: true, + }); + this.server.create('term', { + vocabularyId: 2, + active: true, + }); + }); + + test('it renders and is accessible', async function (assert) { + await render( + , + ); + + // assert.strictEqual(component.vocabularies.length, 2); + // assert.strictEqual(parseInt(component.years[0].title, 10), thisYear + 1); + // assert.strictEqual(parseInt(component.years[1].title, 10), thisYear); + // assert.strictEqual(parseInt(component.years[2].title, 10), thisYear - 1); + + // assert.ok(component.years[0].isExpanded); + // assert.notOk(component.years[1].isExpanded); + // assert.notOk(component.years[2].isExpanded); + + // assert.strictEqual(component.years[0].courses.length, 2); + // assert.strictEqual(component.years[1].courses.length, 0); + // assert.strictEqual(component.years[2].courses.length, 0); + + // assert.strictEqual(component.years[0].courses[0].title, 'course 2 (1)'); + // assert.strictEqual(component.years[0].courses[1].title, 'course 3 (1)'); + + await a11yAudit(this.element); + assert.ok(true, 'no a11y errors found!'); + }); + + skip('selected terms are checked', async function (assert) { + await render( + , + ); + assert.strictEqual(component.years[0].courses.length, 4); + assert.strictEqual(component.years[0].courses[0].title, 'course 0'); + assert.notOk(component.years[0].courses[0].isChecked); + + assert.strictEqual(component.years[0].courses[1].title, 'course 1'); + assert.ok(component.years[0].courses[1].isChecked); + + assert.strictEqual(component.years[0].courses[2].title, 'course 2'); + assert.ok(component.years[0].courses[2].isChecked); + + assert.strictEqual(component.years[0].courses[3].title, 'course 3'); + assert.notOk(component.years[0].courses[3].isChecked); + }); + + skip('selected terms toggle remove', async function (assert) { + assert.expect(2); + this.set('remove', (id) => { + assert.strictEqual(id, '1'); + }); + await render( + , + ); + assert.ok(component.years[0].courses[0].isChecked); + await component.years[0].courses[0].toggle(); + }); + + skip('unselected terms toggle add', async function (assert) { + assert.expect(2); + this.set('add', (id) => { + assert.strictEqual(id, '1'); + }); + await render( + , + ); + assert.notOk(component.years[0].courses[0].isChecked); + await component.years[0].courses[0].toggle(); + }); +}); From eae6b41cfd2ebf3fd99df3e54ba08b35224a0523 Mon Sep 17 00:00:00 2001 From: Michael Chadwick Date: Thu, 14 Aug 2025 15:18:20 -0700 Subject: [PATCH 4/5] remove redundant methods and unused @school argument in component that are already happening in parent component --- .../components/dashboard/calendar-filters.gjs | 1 - .../dashboard/terms-calendar-filter.gjs | 30 +++---------------- 2 files changed, 4 insertions(+), 27 deletions(-) diff --git a/packages/ilios-common/addon/components/dashboard/calendar-filters.gjs b/packages/ilios-common/addon/components/dashboard/calendar-filters.gjs index c435467b40..da9933c73e 100644 --- a/packages/ilios-common/addon/components/dashboard/calendar-filters.gjs +++ b/packages/ilios-common/addon/components/dashboard/calendar-filters.gjs @@ -98,7 +98,6 @@ export default class DashboardCalendarFiltersComponent extends Component { @removeTermId={{@removeTermId}} @selectedTermIds={{@selectedTermIds}} @vocabularies={{this.vocabularies}} - @school={{@school}} /> {{else}}
    - {{#if this.vocabulariesLoaded}} + {{#if @vocabularies}}
      {{#each @vocabularies as |vocabulary|}} {{else}} - + {{/if}}
    From 65c2a481df0775d0971335bca2dc7e60a03be920 Mon Sep 17 00:00:00 2001 From: Michael Chadwick Date: Thu, 14 Aug 2025 15:18:47 -0700 Subject: [PATCH 5/5] added component integration test, and fixed child component test --- .../dashboard/selected-vocabulary-test.gjs | 12 ++ .../dashboard/terms-calendar-filter-test.gjs | 137 ++++++++++++------ 2 files changed, 103 insertions(+), 46 deletions(-) diff --git a/packages/test-app/tests/integration/components/dashboard/selected-vocabulary-test.gjs b/packages/test-app/tests/integration/components/dashboard/selected-vocabulary-test.gjs index d3da01c5e4..8446c29550 100644 --- a/packages/test-app/tests/integration/components/dashboard/selected-vocabulary-test.gjs +++ b/packages/test-app/tests/integration/components/dashboard/selected-vocabulary-test.gjs @@ -41,6 +41,10 @@ module('Integration | Component | dashboard/selected-vocabulary', function (hook @selectedTermIds={{this.selectedTermIds}} @add={{(noop)}} @remove={{(noop)}} + @addVocabularyInView={{(noop)}} + @removeVocabularyInView={{(noop)}} + @addTitleInView={{(noop)}} + @removeTitleInView={{(noop)}} /> , ); @@ -68,6 +72,10 @@ module('Integration | Component | dashboard/selected-vocabulary', function (hook @selectedTermIds={{(array)}} @add={{this.add}} @remove={{(noop)}} + @addVocabularyInView={{(noop)}} + @removeVocabularyInView={{(noop)}} + @addTitleInView={{(noop)}} + @removeTitleInView={{(noop)}} /> , ); @@ -88,6 +96,10 @@ module('Integration | Component | dashboard/selected-vocabulary', function (hook @selectedTermIds={{this.selectedTermIds}} @add={{(noop)}} @remove={{this.remove}} + @addVocabularyInView={{(noop)}} + @removeVocabularyInView={{(noop)}} + @addTitleInView={{(noop)}} + @removeTitleInView={{(noop)}} /> , ); diff --git a/packages/test-app/tests/integration/components/dashboard/terms-calendar-filter-test.gjs b/packages/test-app/tests/integration/components/dashboard/terms-calendar-filter-test.gjs index d964a214c8..d5edf93f41 100644 --- a/packages/test-app/tests/integration/components/dashboard/terms-calendar-filter-test.gjs +++ b/packages/test-app/tests/integration/components/dashboard/terms-calendar-filter-test.gjs @@ -1,4 +1,4 @@ -import { module, skip, test } from 'qunit'; +import { module, test } from 'qunit'; import { setupRenderingTest } from 'test-app/tests/helpers'; import { render } from '@ember/test-helpers'; import { setupMirage } from 'test-app/tests/test-support/mirage'; @@ -14,90 +14,135 @@ module('Integration | Component | dashboard/terms-calendar-filter', function (ho hooks.beforeEach(async function () { this.school = this.server.create('school'); - this.server.create('vocabulary', { + this.vocab1 = this.server.create('vocabulary', { school: this.school, active: true, }); - this.server.create('vocabulary', { - school: this.school, + const term1 = this.server.create('term', { + vocabulary: this.vocab1, active: true, }); - this.server.create('term', { - vocabularyId: 1, + const term2 = this.server.create('term', { + vocabulary: this.vocab1, active: true, }); - this.server.create('term', { - vocabularyId: 1, + this.vocab2 = this.server.create('vocabulary', { + school: this.school, active: true, }); - this.server.create('term', { - vocabularyId: 2, + const term3 = this.server.create('term', { + vocabulary: this.vocab2, active: true, }); - this.server.create('term', { - vocabularyId: 2, + const term4 = this.server.create('term', { + vocabulary: this.vocab2, active: true, }); + this.server.create('course', { + year: 2024, + school: this.school, + terms: [term1, term2], + }); + this.server.create('course', { + year: 2025, + school: this.school, + terms: [term3, term4], + }); + + this.vocabModel1 = await this.owner + .lookup('service:store') + .findRecord('vocabulary', this.vocab1.id); + this.vocabModel2 = await this.owner + .lookup('service:store') + .findRecord('vocabulary', this.vocab2.id); }); test('it renders and is accessible', async function (assert) { await render( , ); - // assert.strictEqual(component.vocabularies.length, 2); - // assert.strictEqual(parseInt(component.years[0].title, 10), thisYear + 1); - // assert.strictEqual(parseInt(component.years[1].title, 10), thisYear); - // assert.strictEqual(parseInt(component.years[2].title, 10), thisYear - 1); - - // assert.ok(component.years[0].isExpanded); - // assert.notOk(component.years[1].isExpanded); - // assert.notOk(component.years[2].isExpanded); + assert.strictEqual(component.vocabularies.length, 2, 'vocabulary count is correct'); + assert.strictEqual( + component.vocabularies[0].title, + 'Vocabulary 1', + 'first vocabulary title is correct', + ); + assert.strictEqual( + component.vocabularies[1].title, + 'Vocabulary 2', + 'second vocabulary title is correct', + ); - // assert.strictEqual(component.years[0].courses.length, 2); - // assert.strictEqual(component.years[1].courses.length, 0); - // assert.strictEqual(component.years[2].courses.length, 0); + assert.strictEqual( + component.vocabularies[0].terms.length, + 2, + 'first vocabulary terms count is correct', + ); + assert.strictEqual( + component.vocabularies[0].terms[0].title, + 'term 0', + 'first vocabulary, first term title is correct', + ); + assert.strictEqual( + component.vocabularies[0].terms[1].title, + 'term 1', + 'first vocabulary, second term title is correct', + ); - // assert.strictEqual(component.years[0].courses[0].title, 'course 2 (1)'); - // assert.strictEqual(component.years[0].courses[1].title, 'course 3 (1)'); + assert.strictEqual( + component.vocabularies[1].terms.length, + 2, + 'second vocabulary terms count is correct', + ); + assert.strictEqual( + component.vocabularies[1].terms[0].title, + 'term 2', + 'second vocabulary, first term title is correct', + ); + assert.strictEqual( + component.vocabularies[1].terms[1].title, + 'term 3', + 'second vocabulary, second term title is correct', + ); await a11yAudit(this.element); assert.ok(true, 'no a11y errors found!'); }); - skip('selected terms are checked', async function (assert) { + test('selected terms are checked', async function (assert) { await render( , ); - assert.strictEqual(component.years[0].courses.length, 4); - assert.strictEqual(component.years[0].courses[0].title, 'course 0'); - assert.notOk(component.years[0].courses[0].isChecked); + assert.strictEqual(component.vocabularies[0].terms.length, 2); + + assert.strictEqual(component.vocabularies[0].terms[0].title, 'term 0'); + assert.notOk(component.vocabularies[0].terms[0].isChecked); - assert.strictEqual(component.years[0].courses[1].title, 'course 1'); - assert.ok(component.years[0].courses[1].isChecked); + assert.strictEqual(component.vocabularies[0].terms[1].title, 'term 1'); + assert.ok(component.vocabularies[0].terms[1].isChecked); - assert.strictEqual(component.years[0].courses[2].title, 'course 2'); - assert.ok(component.years[0].courses[2].isChecked); + assert.strictEqual(component.vocabularies[1].terms[0].title, 'term 2'); + assert.ok(component.vocabularies[1].terms[0].isChecked); - assert.strictEqual(component.years[0].courses[3].title, 'course 3'); - assert.notOk(component.years[0].courses[3].isChecked); + assert.strictEqual(component.vocabularies[1].terms[1].title, 'term 3'); + assert.notOk(component.vocabularies[1].terms[1].isChecked); }); - skip('selected terms toggle remove', async function (assert) { + test('selected terms toggle remove', async function (assert) { assert.expect(2); this.set('remove', (id) => { assert.strictEqual(id, '1'); @@ -105,18 +150,18 @@ module('Integration | Component | dashboard/terms-calendar-filter', function (ho await render( , ); - assert.ok(component.years[0].courses[0].isChecked); - await component.years[0].courses[0].toggle(); + assert.ok(component.vocabularies[0].terms[0].isChecked); + await component.vocabularies[0].terms[0].toggle(); }); - skip('unselected terms toggle add', async function (assert) { + test('unselected terms toggle add', async function (assert) { assert.expect(2); this.set('add', (id) => { assert.strictEqual(id, '1'); @@ -124,13 +169,13 @@ module('Integration | Component | dashboard/terms-calendar-filter', function (ho await render( , ); - assert.notOk(component.years[0].courses[0].isChecked); - await component.years[0].courses[0].toggle(); + assert.notOk(component.vocabularies[0].terms[0].isChecked); + await component.vocabularies[0].terms[0].toggle(); }); });