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/ilios-common/addon/components/dashboard/calendar-filters.gjs b/packages/ilios-common/addon/components/dashboard/calendar-filters.gjs index f2a91809ed..da9933c73e 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,12 @@ export default class DashboardCalendarFiltersComponent extends Component { {{/if}} -
-

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

-
- {{#if this.vocabulariesLoaded}} - - {{else}} - - {{/if}} -
-
+ {{else}}
-
  • -

    +
  • +

    {{@vocabulary.title}}

    {{#each (sortBy "title" this.topLevelTerms) as |term|}} 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..262b1ede88 --- /dev/null +++ b/packages/ilios-common/addon/components/dashboard/terms-calendar-filter.gjs @@ -0,0 +1,83 @@ +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { action } from '@ember/object'; +import { service } from '@ember/service'; +import t from 'ember-intl/helpers/t'; +import LoadingSpinner from 'ilios-common/components/loading-spinner'; +import SelectedVocabulary from 'ilios-common/components/dashboard/selected-vocabulary'; + +export default class DashboardTermsCalendarFilterComponent extends Component { + @service dataLoader; + @service iliosConfig; + + @tracked vocabulariesInView = []; + @tracked titlesInView = []; + + @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'; 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 new file mode 100644 index 0000000000..d5edf93f41 --- /dev/null +++ b/packages/test-app/tests/integration/components/dashboard/terms-calendar-filter-test.gjs @@ -0,0 +1,181 @@ +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'; +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.vocab1 = this.server.create('vocabulary', { + school: this.school, + active: true, + }); + const term1 = this.server.create('term', { + vocabulary: this.vocab1, + active: true, + }); + const term2 = this.server.create('term', { + vocabulary: this.vocab1, + active: true, + }); + this.vocab2 = this.server.create('vocabulary', { + school: this.school, + active: true, + }); + const term3 = this.server.create('term', { + vocabulary: this.vocab2, + active: true, + }); + 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, '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.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.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!'); + }); + + test('selected terms are checked', async function (assert) { + await render( + , + ); + 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.vocabularies[0].terms[1].title, 'term 1'); + assert.ok(component.vocabularies[0].terms[1].isChecked); + + assert.strictEqual(component.vocabularies[1].terms[0].title, 'term 2'); + assert.ok(component.vocabularies[1].terms[0].isChecked); + + assert.strictEqual(component.vocabularies[1].terms[1].title, 'term 3'); + assert.notOk(component.vocabularies[1].terms[1].isChecked); + }); + + test('selected terms toggle remove', async function (assert) { + assert.expect(2); + this.set('remove', (id) => { + assert.strictEqual(id, '1'); + }); + await render( + , + ); + assert.ok(component.vocabularies[0].terms[0].isChecked); + await component.vocabularies[0].terms[0].toggle(); + }); + + test('unselected terms toggle add', async function (assert) { + assert.expect(2); + this.set('add', (id) => { + assert.strictEqual(id, '1'); + }); + await render( + , + ); + assert.notOk(component.vocabularies[0].terms[0].isChecked); + await component.vocabularies[0].terms[0].toggle(); + }); +});