diff --git a/packages/frontend/tests/acceptance/course-visualizations-instructor-test.js b/packages/frontend/tests/acceptance/course/visualizations-instructor-test.js similarity index 79% rename from packages/frontend/tests/acceptance/course-visualizations-instructor-test.js rename to packages/frontend/tests/acceptance/course/visualizations-instructor-test.js index b3105c2f55..11be8c413e 100644 --- a/packages/frontend/tests/acceptance/course-visualizations-instructor-test.js +++ b/packages/frontend/tests/acceptance/course/visualizations-instructor-test.js @@ -13,7 +13,7 @@ module('Acceptance | course visualizations - instructor', function (hooks) { }); test('it renders', async function (assert) { - assert.expect(21); + assert.expect(26); const instructor = this.server.create('user'); const vocabulary1 = this.server.create('vocabulary'); const vocabulary2 = this.server.create('vocabulary'); @@ -78,24 +78,27 @@ module('Acceptance | course visualizations - instructor', function (hooks) { // wait for charts to load await waitFor('.loaded', { count: 2 }); await waitFor('svg .bars'); - await waitFor('svg .chart'); + await waitFor('svg .slice'); await percySnapshot(assert); assert.strictEqual(page.root.termsChart.chart.bars.length, 3); assert.strictEqual(page.root.termsChart.chart.labels.length, 3); + assert.strictEqual(page.root.termsChart.chart.labels[0].text, 'Vocabulary 1 - term 0'); + assert.strictEqual(page.root.termsChart.chart.labels[1].text, 'Vocabulary 1 - term 1'); + assert.strictEqual(page.root.termsChart.chart.labels[2].text, 'Vocabulary 2 - term 2'); + assert.strictEqual(page.root.sessionTypesChart.chart.slices.length, 2); + assert.strictEqual(page.root.sessionTypesChart.chart.labels.length, 2); + assert.strictEqual(page.root.sessionTypesChart.chart.descriptions.length, 2); + assert.strictEqual(page.root.sessionTypesChart.chart.labels[0].text, 'session type 1'); + assert.strictEqual(page.root.sessionTypesChart.chart.labels[1].text, 'session type 0'); + assert.strictEqual( - page.root.termsChart.chart.labels[0].text, - 'Vocabulary 1 > term 0: 60 Minutes', - ); - assert.strictEqual( - page.root.termsChart.chart.labels[1].text, - 'Vocabulary 1 > term 1: 30 Minutes', + page.root.sessionTypesChart.chart.descriptions[0].text, + 'session type 1 - 30 Minutes', ); assert.strictEqual( - page.root.termsChart.chart.labels[2].text, - 'Vocabulary 2 > term 2: 30 Minutes', + page.root.sessionTypesChart.chart.descriptions[1].text, + 'session type 0 - 60 Minutes', ); - assert.strictEqual(page.root.sessionTypesChart.chart.slices.length, 2); - assert.strictEqual(page.root.sessionTypesChart.chart.slices[0].text, 'session type 0 66.7%'); - assert.strictEqual(page.root.sessionTypesChart.chart.slices[1].text, 'session type 1 33.3%'); + assert.strictEqual(page.root.sessionTypesChart.dataTable.rows.length, 2); }); }); diff --git a/packages/frontend/tests/acceptance/course-visualizations-instructors-test.js b/packages/frontend/tests/acceptance/course/visualizations-instructors-test.js similarity index 87% rename from packages/frontend/tests/acceptance/course-visualizations-instructors-test.js rename to packages/frontend/tests/acceptance/course/visualizations-instructors-test.js index 954b8c9d41..3fe12387bb 100644 --- a/packages/frontend/tests/acceptance/course-visualizations-instructors-test.js +++ b/packages/frontend/tests/acceptance/course/visualizations-instructors-test.js @@ -65,7 +65,7 @@ module('Acceptance | course visualizations - instructors', function (hooks) { }); test('it renders', async function (assert) { - assert.expect(12); + assert.expect(15); await page.visit({ courseId: this.course.id }); assert.strictEqual(currentURL(), '/data/courses/1/instructors'); assert.strictEqual(page.root.title, 'course 0 2022'); @@ -80,15 +80,18 @@ module('Acceptance | course visualizations - instructors', function (hooks) { await waitFor('svg .bars'); await percySnapshot(assert); assert.strictEqual(page.root.instructorsChart.chart.bars.length, 2); - assert.strictEqual(page.root.instructorsChart.chart.labels.length, 2); assert.strictEqual( - page.root.instructorsChart.chart.labels[0].text, - '1 guy M. Mc1son: 75 Minutes', + page.root.instructorsChart.chart.bars[0].description, + '1 guy M. Mc1son - 75 Minutes', ); assert.strictEqual( - page.root.instructorsChart.chart.labels[1].text, - '2 guy M. Mc2son: 90 Minutes', + page.root.instructorsChart.chart.bars[1].description, + '2 guy M. Mc2son - 90 Minutes', ); + assert.strictEqual(page.root.instructorsChart.chart.labels.length, 2); + assert.strictEqual(page.root.instructorsChart.chart.labels[0].text, '1 guy M. Mc1son'); + assert.strictEqual(page.root.instructorsChart.chart.labels[1].text, '2 guy M. Mc2son'); + assert.strictEqual(page.root.instructorsChart.dataTable.rows.length, 2); }); test('clicking chart transitions user to instructor visualization', async function (assert) { @@ -98,10 +101,7 @@ module('Acceptance | course visualizations - instructors', function (hooks) { // wait for charts to load await waitFor('.loaded'); await waitFor('svg .bars'); - assert.strictEqual( - page.root.instructorsChart.chart.labels[0].text, - '1 guy M. Mc1son: 75 Minutes', - ); + assert.strictEqual(page.root.instructorsChart.chart.labels[0].text, '1 guy M. Mc1son'); await page.root.instructorsChart.chart.bars[0].click(); assert.strictEqual(currentURL(), '/data/courses/1/instructors/2'); }); diff --git a/packages/frontend/tests/acceptance/course-visualizations-objectives-test.js b/packages/frontend/tests/acceptance/course/visualizations-objectives-test.js similarity index 88% rename from packages/frontend/tests/acceptance/course-visualizations-objectives-test.js rename to packages/frontend/tests/acceptance/course/visualizations-objectives-test.js index 0782ce2f5a..c7bd2fa699 100644 --- a/packages/frontend/tests/acceptance/course-visualizations-objectives-test.js +++ b/packages/frontend/tests/acceptance/course/visualizations-objectives-test.js @@ -12,7 +12,7 @@ module('Acceptance | course visualizations - objectives', function (hooks) { }); test('it renders', async function (assert) { - assert.expect(14); + assert.expect(17); const school = this.server.create('school'); const course = this.server.create('course', { year: 2021, school }); const courseObjectives = this.server.createList('course-objective', 3, { @@ -72,13 +72,22 @@ module('Acceptance | course visualizations - objectives', function (hooks) { await waitFor('svg .chart'); await percySnapshot(assert); assert.strictEqual(page.root.objectivesChart.chart.slices.length, 2); - assert.strictEqual(page.root.objectivesChart.chart.slices[0].text, '77.8%'); - assert.strictEqual(page.root.objectivesChart.chart.slices[1].text, '22.2%'); + assert.strictEqual(page.root.objectivesChart.chart.slices[0].label, '77.8%'); + assert.strictEqual( + page.root.objectivesChart.chart.slices[0].description, + 'course objective 0 - 630 Minutes', + ); + assert.strictEqual(page.root.objectivesChart.chart.slices[1].label, '22.2%'); + assert.strictEqual( + page.root.objectivesChart.chart.slices[1].description, + 'course objective 1 - 180 Minutes', + ); assert.notOk(page.root.objectivesChart.unlinkedObjectives.isPresent); assert.strictEqual(page.root.objectivesChart.untaughtObjectives.items.length, 1); assert.strictEqual( page.root.objectivesChart.untaughtObjectives.items[0].text, 'course objective 2', ); + assert.strictEqual(page.root.objectivesChart.dataTable.rows.length, 3); }); }); diff --git a/packages/frontend/tests/acceptance/course-visualizations-session-type-test.js b/packages/frontend/tests/acceptance/course/visualizations-session-type-test.js similarity index 83% rename from packages/frontend/tests/acceptance/course-visualizations-session-type-test.js rename to packages/frontend/tests/acceptance/course/visualizations-session-type-test.js index ed6b4e6a27..681492b6ba 100644 --- a/packages/frontend/tests/acceptance/course-visualizations-session-type-test.js +++ b/packages/frontend/tests/acceptance/course/visualizations-session-type-test.js @@ -13,7 +13,7 @@ module('Acceptance | course visualizations - session-type', function (hooks) { }); test('it renders', async function (assert) { - assert.expect(16); + assert.expect(19); const sessionType = this.server.create('session-type'); const vocabulary1 = this.server.create('vocabulary'); const vocabulary2 = this.server.create('vocabulary'); @@ -70,18 +70,21 @@ module('Acceptance | course visualizations - session-type', function (hooks) { await percySnapshot(assert); assert.strictEqual(page.root.title, 'course 0 2022'); assert.strictEqual(page.root.sessionTypeChart.chart.bars.length, 3); - assert.strictEqual(page.root.sessionTypeChart.chart.labels.length, 3); assert.strictEqual( - page.root.sessionTypeChart.chart.labels[0].text, - 'Vocabulary 1 - term 1: 30 Minutes', + page.root.sessionTypeChart.chart.bars[0].description, + 'Vocabulary 1 - term 1 - 30 Minutes', ); assert.strictEqual( - page.root.sessionTypeChart.chart.labels[1].text, - 'Vocabulary 1 - term 0: 60 Minutes', + page.root.sessionTypeChart.chart.bars[1].description, + 'Vocabulary 1 - term 0 - 60 Minutes', ); assert.strictEqual( - page.root.sessionTypeChart.chart.labels[2].text, - 'Vocabulary 2 - term 2: 30 Minutes', + page.root.sessionTypeChart.chart.bars[2].description, + 'Vocabulary 2 - term 2 - 30 Minutes', ); + assert.strictEqual(page.root.sessionTypeChart.chart.labels.length, 3); + assert.strictEqual(page.root.sessionTypeChart.chart.labels[0].text, 'Vocabulary 1 - term 1'); + assert.strictEqual(page.root.sessionTypeChart.chart.labels[1].text, 'Vocabulary 1 - term 0'); + assert.strictEqual(page.root.sessionTypeChart.chart.labels[2].text, 'Vocabulary 2 - term 2'); }); }); diff --git a/packages/frontend/tests/acceptance/course-visualizations-session-types-test.js b/packages/frontend/tests/acceptance/course/visualizations-session-types-test.js similarity index 82% rename from packages/frontend/tests/acceptance/course-visualizations-session-types-test.js rename to packages/frontend/tests/acceptance/course/visualizations-session-types-test.js index 43e0b26e47..12943355be 100644 --- a/packages/frontend/tests/acceptance/course-visualizations-session-types-test.js +++ b/packages/frontend/tests/acceptance/course/visualizations-session-types-test.js @@ -13,7 +13,7 @@ module('Acceptance | course visualizations - session-types', function (hooks) { }); test('it renders', async function (assert) { - assert.expect(14); + assert.expect(18); const sessionType1 = this.server.create('session-type'); const sessionType2 = this.server.create('session-type'); const sessionType3 = this.server.create('session-type'); @@ -72,18 +72,22 @@ module('Acceptance | course visualizations - session-types', function (hooks) { await percySnapshot(assert); assert.strictEqual(page.root.title, 'course 0 2022'); assert.strictEqual(page.root.sessionTypesChart.chart.bars.length, 3); - assert.strictEqual(page.root.sessionTypesChart.chart.labels.length, 3); assert.strictEqual( - page.root.sessionTypesChart.chart.labels[0].text, - 'session type 1: 30 Minutes', + page.root.sessionTypesChart.chart.bars[0].description, + 'session type 1 - 30 Minutes', ); assert.strictEqual( - page.root.sessionTypesChart.chart.labels[1].text, - 'session type 0: 60 Minutes', + page.root.sessionTypesChart.chart.bars[1].description, + 'session type 0 - 60 Minutes', ); assert.strictEqual( - page.root.sessionTypesChart.chart.labels[2].text, - 'session type 2: 120 Minutes', + page.root.sessionTypesChart.chart.bars[2].description, + 'session type 2 - 120 Minutes', ); + assert.strictEqual(page.root.sessionTypesChart.chart.labels.length, 3); + assert.strictEqual(page.root.sessionTypesChart.chart.labels[0].text, 'session type 1'); + assert.strictEqual(page.root.sessionTypesChart.chart.labels[1].text, 'session type 0'); + assert.strictEqual(page.root.sessionTypesChart.chart.labels[2].text, 'session type 2'); + assert.strictEqual(page.root.sessionTypesChart.dataTable.rows.length, 3); }); }); diff --git a/packages/frontend/tests/acceptance/course-visualizations-test.js b/packages/frontend/tests/acceptance/course/visualizations-test.js similarity index 100% rename from packages/frontend/tests/acceptance/course-visualizations-test.js rename to packages/frontend/tests/acceptance/course/visualizations-test.js diff --git a/packages/frontend/tests/acceptance/course-visualizations-vocabularies-test.js b/packages/frontend/tests/acceptance/course/visualizations-vocabularies-test.js similarity index 79% rename from packages/frontend/tests/acceptance/course-visualizations-vocabularies-test.js rename to packages/frontend/tests/acceptance/course/visualizations-vocabularies-test.js index 48c3460842..120b7de207 100644 --- a/packages/frontend/tests/acceptance/course-visualizations-vocabularies-test.js +++ b/packages/frontend/tests/acceptance/course/visualizations-vocabularies-test.js @@ -13,7 +13,7 @@ module('Acceptance | course visualizations - vocabularies', function (hooks) { }); test('it renders', async function (assert) { - assert.expect(12); + assert.expect(16); const sessionType = this.server.create('session-type'); const vocabulary1 = this.server.create('vocabulary'); const vocabulary2 = this.server.create('vocabulary'); @@ -67,10 +67,20 @@ module('Acceptance | course visualizations - vocabularies', function (hooks) { assert.strictEqual(page.root.breadcrumb.crumbs[2].text, 'Vocabularies'); // wait for charts to load await waitFor('.loaded'); - await waitFor('svg .chart'); + await waitFor('svg .bars'); await percySnapshot(assert); - assert.strictEqual(page.root.vocabulariesChart.chart.slices.length, 2); - assert.strictEqual(page.root.vocabulariesChart.chart.slices[0].text, 'Vocabulary 1'); - assert.strictEqual(page.root.vocabulariesChart.chart.slices[1].text, 'Vocabulary 2'); + assert.strictEqual(page.root.vocabulariesChart.chart.bars.length, 2); + assert.strictEqual( + page.root.vocabulariesChart.chart.bars[0].description, + 'Vocabulary 2 - 30 Minutes', + ); + assert.strictEqual( + page.root.vocabulariesChart.chart.bars[1].description, + 'Vocabulary 1 - 90 Minutes', + ); + assert.strictEqual(page.root.vocabulariesChart.chart.labels.length, 2); + assert.strictEqual(page.root.vocabulariesChart.chart.labels[0].text, 'Vocabulary 2'); + assert.strictEqual(page.root.vocabulariesChart.chart.labels[1].text, 'Vocabulary 1'); + assert.strictEqual(page.root.vocabulariesChart.dataTable.rows.length, 2); }); }); diff --git a/packages/frontend/tests/acceptance/course-visualizations-vocabulary-test.js b/packages/frontend/tests/acceptance/course/visualizations-vocabulary-test.js similarity index 89% rename from packages/frontend/tests/acceptance/course-visualizations-vocabulary-test.js rename to packages/frontend/tests/acceptance/course/visualizations-vocabulary-test.js index 21051c0b3d..0314fb3adf 100644 --- a/packages/frontend/tests/acceptance/course-visualizations-vocabulary-test.js +++ b/packages/frontend/tests/acceptance/course/visualizations-vocabulary-test.js @@ -54,8 +54,7 @@ module('Acceptance | course visualizations - vocabulary', function (hooks) { }); test('it renders', async function (assert) { - assert.expect(17); - + assert.expect(21); await page.visit({ courseId: this.course.id, vocabularyId: this.vocabulary.id }); assert.strictEqual(currentURL(), '/data/courses/1/vocabularies/1'); assert.strictEqual(page.root.vocabularyTitle, 'Vocabulary 1'); @@ -74,10 +73,14 @@ module('Acceptance | course visualizations - vocabulary', function (hooks) { await waitFor('svg .bars'); await percySnapshot(assert); assert.strictEqual(page.root.termsChart.chart.bars.length, 3); + assert.strictEqual(page.root.termsChart.chart.bars[0].description, 'term 1 - 30 Minutes'); + assert.strictEqual(page.root.termsChart.chart.bars[1].description, 'term 0 - 60 Minutes'); + assert.strictEqual(page.root.termsChart.chart.bars[2].description, 'term 2 - 150 Minutes'); assert.strictEqual(page.root.termsChart.chart.labels.length, 3); - assert.strictEqual(page.root.termsChart.chart.labels[0].text, 'term 1: 30 Minutes'); - assert.strictEqual(page.root.termsChart.chart.labels[1].text, 'term 0: 60 Minutes'); - assert.strictEqual(page.root.termsChart.chart.labels[2].text, 'term 2: 150 Minutes'); + assert.strictEqual(page.root.termsChart.chart.labels[0].text, 'term 1'); + assert.strictEqual(page.root.termsChart.chart.labels[1].text, 'term 0'); + assert.strictEqual(page.root.termsChart.chart.labels[2].text, 'term 2'); + assert.strictEqual(page.root.termsChart.dataTable.rows.length, 3); }); test('clicking chart transitions user to term visualization', async function (assert) { @@ -86,7 +89,7 @@ module('Acceptance | course visualizations - vocabulary', function (hooks) { // wait for charts to load await waitFor('.loaded'); await waitFor('svg .bars'); - assert.strictEqual(page.root.termsChart.chart.labels[0].text, 'term 1: 30 Minutes'); + assert.strictEqual(page.root.termsChart.chart.labels[0].text, 'term 1'); await page.root.termsChart.chart.bars[0].click(); assert.strictEqual(currentURL(), '/data/courses/1/terms/2'); }); diff --git a/packages/ilios-common/addon-test-support/ilios-common/page-objects/components/course/visualize-instructor-session-type-graph.js b/packages/ilios-common/addon-test-support/ilios-common/page-objects/components/course/visualize-instructor-session-type-graph.js index 45cc0f87c5..9ecd2fc6b9 100644 --- a/packages/ilios-common/addon-test-support/ilios-common/page-objects/components/course/visualize-instructor-session-type-graph.js +++ b/packages/ilios-common/addon-test-support/ilios-common/page-objects/components/course/visualize-instructor-session-type-graph.js @@ -1,4 +1,4 @@ -import { collection, create, notHasClass } from 'ember-cli-page-object'; +import { attribute, clickable, collection, create, notHasClass, text } from 'ember-cli-page-object'; const definition = { scope: '[data-test-course-visualize-instructor-session-type-graph]', @@ -6,6 +6,39 @@ const definition = { chart: { scope: '.simple-chart', slices: collection('svg .slice'), + labels: collection('.slice text'), + descriptions: collection('.slice desc'), + }, + noData: { + scope: '[data-test-no-data]', + }, + dataTable: { + scope: '[data-test-data-table]', + header: { + scope: 'thead', + sessionType: { + scope: '[data-test-session-type]', + toggle: clickable('button'), + }, + sessions: { + scope: '[data-test-sessions]', + toggle: clickable('button'), + }, + minutes: { + scope: '[data-test-minutes]', + toggle: clickable('button'), + }, + }, + rows: collection('tbody tr', { + sessionType: text('[data-test-session-type]'), + sessions: { + scope: '[data-test-sessions]', + links: collection('a', { + url: attribute('href'), + }), + }, + minutes: text('[data-test-minutes]'), + }), }, }; diff --git a/packages/ilios-common/addon-test-support/ilios-common/page-objects/components/course/visualize-instructor-term-graph.js b/packages/ilios-common/addon-test-support/ilios-common/page-objects/components/course/visualize-instructor-term-graph.js index f32aca173e..a54428542a 100644 --- a/packages/ilios-common/addon-test-support/ilios-common/page-objects/components/course/visualize-instructor-term-graph.js +++ b/packages/ilios-common/addon-test-support/ilios-common/page-objects/components/course/visualize-instructor-term-graph.js @@ -1,13 +1,46 @@ -import { collection, create, notHasClass } from 'ember-cli-page-object'; +import { attribute, clickable, collection, create, notHasClass, text } from 'ember-cli-page-object'; const definition = { scope: '[data-test-course-visualize-instructor-term-graph]', isIcon: notHasClass('no-icon'), chart: { scope: '.simple-chart', - bars: collection('.bars rect'), + bars: collection('.bars rect', { + description: text('desc'), + }), labels: collection('.bars text'), }, + noData: { + scope: '[data-test-no-data]', + }, + dataTable: { + scope: '[data-test-data-table]', + header: { + scope: 'thead', + vocabularyTerm: { + scope: '[data-test-vocabulary-term]', + toggle: clickable('button'), + }, + sessions: { + scope: '[data-test-sessions]', + toggle: clickable('button'), + }, + minutes: { + scope: '[data-test-minutes]', + toggle: clickable('button'), + }, + }, + rows: collection('tbody tr', { + vocabularyTerm: text('[data-test-vocabulary-term]'), + sessions: { + scope: '[data-test-sessions]', + links: collection('a', { + url: attribute('href'), + }), + }, + minutes: text('[data-test-minutes]'), + }), + }, }; export default definition; diff --git a/packages/ilios-common/addon-test-support/ilios-common/page-objects/components/course/visualize-instructors-graph.js b/packages/ilios-common/addon-test-support/ilios-common/page-objects/components/course/visualize-instructors-graph.js index 9756c3a2f6..70ce0dcb8a 100644 --- a/packages/ilios-common/addon-test-support/ilios-common/page-objects/components/course/visualize-instructors-graph.js +++ b/packages/ilios-common/addon-test-support/ilios-common/page-objects/components/course/visualize-instructors-graph.js @@ -1,13 +1,48 @@ -import { collection, create, notHasClass } from 'ember-cli-page-object'; +import { attribute, clickable, collection, create, notHasClass, text } from 'ember-cli-page-object'; const definition = { scope: '[data-test-course-visualize-instructors-graph]', isIcon: notHasClass('no-icon'), chart: { scope: '.simple-chart', - bars: collection('.bars rect'), + bars: collection('.bars rect', { + description: text('desc'), + }), labels: collection('.bars text'), - slices: collection('svg .slice'), + }, + noData: { + scope: '[data-test-no-data]', + }, + dataTable: { + scope: '[data-test-data-table]', + header: { + scope: 'thead', + instructor: { + scope: '[data-test-instructor]', + toggle: clickable('button'), + }, + sessions: { + scope: '[data-test-sessions]', + toggle: clickable('button'), + }, + minutes: { + scope: '[data-test-minutes]', + toggle: clickable('button'), + }, + }, + rows: collection('tbody tr', { + instructor: { + scope: '[data-test-instructor]', + url: attribute('href', 'a'), + }, + sessions: { + scope: '[data-test-sessions]', + links: collection('a', { + url: attribute('href'), + }), + }, + minutes: text('[data-test-minutes]'), + }), }, }; diff --git a/packages/ilios-common/addon-test-support/ilios-common/page-objects/components/course/visualize-objectives-graph.js b/packages/ilios-common/addon-test-support/ilios-common/page-objects/components/course/visualize-objectives-graph.js index b9faf66f8a..aed42e518e 100644 --- a/packages/ilios-common/addon-test-support/ilios-common/page-objects/components/course/visualize-objectives-graph.js +++ b/packages/ilios-common/addon-test-support/ilios-common/page-objects/components/course/visualize-objectives-graph.js @@ -5,7 +5,10 @@ const definition = { isIcon: notHasClass('no-icon'), chart: { scope: '.simple-chart', - slices: collection('svg .slice'), + slices: collection('svg .slice', { + label: text('text'), + description: text('desc'), + }), }, unlinkedObjectives: { scope: '[data-test-with-hours]', diff --git a/packages/ilios-common/addon-test-support/ilios-common/page-objects/components/course/visualize-session-type-graph.js b/packages/ilios-common/addon-test-support/ilios-common/page-objects/components/course/visualize-session-type-graph.js index 34a30b5bb5..31f6b8a8b5 100644 --- a/packages/ilios-common/addon-test-support/ilios-common/page-objects/components/course/visualize-session-type-graph.js +++ b/packages/ilios-common/addon-test-support/ilios-common/page-objects/components/course/visualize-session-type-graph.js @@ -1,13 +1,46 @@ -import { collection, create, notHasClass } from 'ember-cli-page-object'; +import { attribute, clickable, collection, create, notHasClass, text } from 'ember-cli-page-object'; const definition = { scope: '[data-test-course-visualize-session-type-graph]', isIcon: notHasClass('no-icon'), chart: { scope: '.simple-chart', - bars: collection('.bars rect'), + bars: collection('.bars rect', { + description: text('desc'), + }), labels: collection('.bars text'), }, + noData: { + scope: '[data-test-no-data]', + }, + dataTable: { + scope: '[data-test-data-table]', + header: { + scope: 'thead', + vocabularyTerm: { + scope: '[data-test-vocabulary-term]', + toggle: clickable('button'), + }, + sessions: { + scope: '[data-test-sessions]', + toggle: clickable('button'), + }, + minutes: { + scope: '[data-test-minutes]', + toggle: clickable('button'), + }, + }, + rows: collection('tbody tr', { + vocabularyTerm: text('[data-test-vocabulary-term]'), + sessions: { + scope: '[data-test-sessions]', + links: collection('a', { + url: attribute('href'), + }), + }, + minutes: text('[data-test-minutes]'), + }), + }, }; export default definition; diff --git a/packages/ilios-common/addon-test-support/ilios-common/page-objects/components/course/visualize-session-types-graph.js b/packages/ilios-common/addon-test-support/ilios-common/page-objects/components/course/visualize-session-types-graph.js index 976103feab..fbf83f345d 100644 --- a/packages/ilios-common/addon-test-support/ilios-common/page-objects/components/course/visualize-session-types-graph.js +++ b/packages/ilios-common/addon-test-support/ilios-common/page-objects/components/course/visualize-session-types-graph.js @@ -1,13 +1,48 @@ -import { collection, create, notHasClass } from 'ember-cli-page-object'; +import { attribute, clickable, collection, create, notHasClass, text } from 'ember-cli-page-object'; const definition = { scope: '[data-test-course-visualize-session-types-graph]', isIcon: notHasClass('no-icon'), chart: { scope: '.simple-chart', - bars: collection('.bars rect'), + bars: collection('.bars rect', { + description: text('desc'), + }), labels: collection('.bars text'), - slices: collection('svg .slice'), + }, + noData: { + scope: '[data-test-no-data]', + }, + dataTable: { + scope: '[data-test-data-table]', + header: { + scope: 'thead', + sessionType: { + scope: '[data-test-session-type]', + toggle: clickable('button'), + }, + sessions: { + scope: '[data-test-sessions]', + toggle: clickable('button'), + }, + minutes: { + scope: '[data-test-minutes]', + toggle: clickable('button'), + }, + }, + rows: collection('tbody tr', { + sessionType: { + scope: '[data-test-session-type]', + url: attribute('href', 'a'), + }, + sessions: { + scope: '[data-test-sessions]', + links: collection('a', { + url: attribute('href'), + }), + }, + minutes: text('[data-test-minutes]'), + }), }, }; diff --git a/packages/ilios-common/addon-test-support/ilios-common/page-objects/components/course/visualize-term-graph.js b/packages/ilios-common/addon-test-support/ilios-common/page-objects/components/course/visualize-term-graph.js new file mode 100644 index 0000000000..21702acd81 --- /dev/null +++ b/packages/ilios-common/addon-test-support/ilios-common/page-objects/components/course/visualize-term-graph.js @@ -0,0 +1,47 @@ +import { attribute, clickable, collection, create, notHasClass, text } from 'ember-cli-page-object'; + +const definition = { + scope: '[data-test-course-visualize-term-graph]', + isIcon: notHasClass('no-icon'), + chart: { + scope: '.simple-chart', + bars: collection('.bars rect', { + description: text('desc'), + }), + labels: collection('.bars text'), + }, + noData: { + scope: '[data-test-no-data]', + }, + dataTable: { + scope: '[data-test-data-table]', + header: { + scope: 'thead', + sessionType: { + scope: '[data-test-session-type]', + toggle: clickable('button'), + }, + sessions: { + scope: '[data-test-sessions]', + toggle: clickable('button'), + }, + minutes: { + scope: '[data-test-minutes]', + toggle: clickable('button'), + }, + }, + rows: collection('tbody tr', { + sessionType: text('[data-test-session-type]'), + sessions: { + scope: '[data-test-sessions]', + links: collection('a', { + url: attribute('href'), + }), + }, + minutes: text('[data-test-minutes]'), + }), + }, +}; + +export default definition; +export const component = create(definition); diff --git a/packages/ilios-common/addon-test-support/ilios-common/page-objects/components/course/visualize-vocabularies-graph.js b/packages/ilios-common/addon-test-support/ilios-common/page-objects/components/course/visualize-vocabularies-graph.js index d6498f2f17..7c6509d2ab 100644 --- a/packages/ilios-common/addon-test-support/ilios-common/page-objects/components/course/visualize-vocabularies-graph.js +++ b/packages/ilios-common/addon-test-support/ilios-common/page-objects/components/course/visualize-vocabularies-graph.js @@ -1,11 +1,48 @@ -import { collection, create, notHasClass } from 'ember-cli-page-object'; +import { attribute, clickable, collection, create, notHasClass, text } from 'ember-cli-page-object'; const definition = { scope: '[data-test-course-visualize-vocabularies-graph]', isIcon: notHasClass('no-icon'), chart: { scope: '.simple-chart', - slices: collection('svg .slice'), + bars: collection('.bars rect', { + description: text('desc'), + }), + labels: collection('.bars text'), + }, + noData: { + scope: '[data-test-no-data]', + }, + dataTable: { + scope: '[data-test-data-table]', + header: { + scope: 'thead', + vocabulary: { + scope: '[data-test-vocabulary]', + toggle: clickable('button'), + }, + sessions: { + scope: '[data-test-sessions]', + toggle: clickable('button'), + }, + minutes: { + scope: '[data-test-minutes]', + toggle: clickable('button'), + }, + }, + rows: collection('tbody tr', { + vocabulary: { + scope: '[data-test-vocabulary]', + url: attribute('href', 'a'), + }, + sessions: { + scope: '[data-test-sessions]', + links: collection('a', { + url: attribute('href'), + }), + }, + minutes: text('[data-test-minutes]'), + }), }, }; diff --git a/packages/ilios-common/addon-test-support/ilios-common/page-objects/components/course/visualize-vocabulary-graph.js b/packages/ilios-common/addon-test-support/ilios-common/page-objects/components/course/visualize-vocabulary-graph.js index 4a58a55a36..e484e2bc26 100644 --- a/packages/ilios-common/addon-test-support/ilios-common/page-objects/components/course/visualize-vocabulary-graph.js +++ b/packages/ilios-common/addon-test-support/ilios-common/page-objects/components/course/visualize-vocabulary-graph.js @@ -1,13 +1,49 @@ -import { collection, create, notHasClass } from 'ember-cli-page-object'; +import { attribute, clickable, collection, create, notHasClass, text } from 'ember-cli-page-object'; const definition = { scope: '[data-test-course-visualize-vocabulary-graph]', isIcon: notHasClass('no-icon'), chart: { scope: '.simple-chart', - bars: collection('.bars rect'), + bars: collection('.bars rect', { + description: text('desc'), + }), labels: collection('.bars text'), }, + noData: { + scope: '[data-test-no-data]', + }, + dataTable: { + scope: '[data-test-data-table]', + header: { + scope: 'thead', + term: { + scope: '[data-test-term]', + toggle: clickable('button'), + }, + sessions: { + scope: '[data-test-sessions]', + toggle: clickable('button'), + }, + minutes: { + scope: '[data-test-minutes]', + toggle: clickable('button'), + }, + }, + rows: collection('tbody tr', { + term: { + scope: '[data-test-term]', + url: attribute('href', 'a'), + }, + sessions: { + scope: '[data-test-sessions]', + links: collection('a', { + url: attribute('href'), + }), + }, + minutes: text('[data-test-minutes]'), + }), + }, }; export default definition; diff --git a/packages/ilios-common/addon/components/course/visualizations.hbs b/packages/ilios-common/addon/components/course/visualizations.hbs index 3c09c142d2..868378e5c9 100644 --- a/packages/ilios-common/addon/components/course/visualizations.hbs +++ b/packages/ilios-common/addon/components/course/visualizations.hbs @@ -44,7 +44,6 @@ @@ -53,7 +52,7 @@

{{t "general.vocabularies"}}

- +
@@ -64,7 +63,6 @@
diff --git a/packages/ilios-common/addon/components/course/visualize-instructor-session-type-graph.hbs b/packages/ilios-common/addon/components/course/visualize-instructor-session-type-graph.hbs index 2020d604a4..ab8c132d82 100644 --- a/packages/ilios-common/addon/components/course/visualize-instructor-session-type-graph.hbs +++ b/packages/ilios-common/addon/components/course/visualize-instructor-session-type-graph.hbs @@ -3,19 +3,79 @@ data-test-course-visualize-instructor-session-type-graph ...attributes > - {{#if (or @isIcon this.data.length)}} - - {{#if this.tooltipContent}} - - {{this.tooltipContent}} - - {{/if}} - + {{#if this.isLoaded}} + {{#if (or @isIcon this.hasChartData)}} + + {{#if this.tooltipContent}} + + {{this.tooltipContent}} + + {{/if}} + + {{/if}} + {{#if (and (not @isIcon) (not this.hasData))}} +
+ {{t "general.courseVisualizationsInstructorNoData" instructor=@user.fullName}} +
+ {{/if}} + {{#if (and (not @isIcon) this.hasData @showDataTable)}} +
+ + + + + {{t "general.sessionType"}} + + + {{t "general.sessions"}} + + + {{t "general.minutes"}} + + + + + {{#each (sort-by this.sortBy this.tableData) as |row|}} + + + + + + {{/each}} + +
{{row.sessionType}} + {{#each row.sessions as |session index|}} + + {{session.title~}} + {{if (not-eq index (sub row.sessions.length 1)) ","}} + {{/each}} + {{row.minutes}}
+
+ {{/if}} + {{else}} + {{/if}} diff --git a/packages/ilios-common/addon/components/course/visualize-instructor-session-type-graph.js b/packages/ilios-common/addon/components/course/visualize-instructor-session-type-graph.js index a696afd29e..3ac7086b04 100644 --- a/packages/ilios-common/addon/components/course/visualize-instructor-session-type-graph.js +++ b/packages/ilios-common/addon/components/course/visualize-instructor-session-type-graph.js @@ -1,47 +1,77 @@ import Component from '@glimmer/component'; import { filter, map } from 'rsvp'; -import { isEmpty } from '@ember/utils'; import { htmlSafe } from '@ember/template'; import { restartableTask, timeout } from 'ember-concurrency'; import { service } from '@ember/service'; import { cached, tracked } from '@glimmer/tracking'; -import { use } from 'ember-could-get-used-to-this'; import { TrackedAsyncData } from 'ember-async-data'; -import AsyncProcess from 'ilios-common/classes/async-process'; -import { findBy, mapBy, uniqueValues } from 'ilios-common/utils/array-helpers'; +import { findById, mapBy, uniqueValues } from 'ilios-common/utils/array-helpers'; +import { action } from '@ember/object'; export default class CourseVisualizeInstructorSessionTypeGraph extends Component { @service router; @service intl; @tracked tooltipContent = null; @tracked tooltipTitle = null; + @tracked sortBy = 'minutes'; @cached - get sessionsData() { - return new TrackedAsyncData(this.args.course.sessions); + get outputData() { + return new TrackedAsyncData(this.getData(this.args.course, this.args.user)); } - get sessions() { - return this.sessionsData.isResolved ? this.sessionsData.value : null; + get data() { + return this.outputData.isResolved ? this.outputData.value : []; } - @use loadedData = new AsyncProcess(() => [this.getData.bind(this), this.sessions]); + get hasData() { + return this.data.length; + } - get data() { - if (!this.loadedData) { - return []; + get chartData() { + return this.data.filter((obj) => obj.data); + } + + get hasChartData() { + return this.chartData.length; + } + + get tableData() { + return this.data.map((obj) => { + const rhett = {}; + rhett.minutes = obj.data; + rhett.sessions = obj.meta.sessions; + rhett.sessionType = obj.meta.sessionType.title; + rhett.sessionTitles = mapBy(rhett.sessions, 'title').join(', '); + return rhett; + }); + } + + get isLoaded() { + return this.outputData.isResolved; + } + + get sortedAscending() { + return this.sortBy.search(/desc/) === -1; + } + + @action + setSortBy(prop) { + if (this.sortBy === prop) { + prop += ':desc'; } - return this.loadedData; + this.sortBy = prop; } - async getData(sessions) { - if (!sessions) { + async getData(course, user) { + const sessions = await course.sessions; + if (!sessions.length) { return []; } - const sessionsWithUser = await filter(sessions.slice(), async (session) => { + const sessionsWithUser = await filter(sessions, async (session) => { const allInstructors = await session.getAllOfferingInstructors(); - return mapBy(allInstructors, 'id').includes(this.args.user.id); + return mapBy(allInstructors, 'id').includes(user.id); }); const sessionsWithSessionType = await map(sessionsWithUser.slice(), async (session) => { @@ -55,55 +85,54 @@ export default class CourseVisualizeInstructorSessionTypeGraph extends Component const dataMap = await map(sessionsWithSessionType, async ({ session, sessionType }) => { const minutes = await session.getTotalSumDurationByInstructor(this.args.user); return { - sessionTitle: session.title, - sessionTypeTitle: sessionType.title, + session, + sessionType, minutes, }; }); - const sessionTypeData = dataMap.reduce((set, obj) => { - const name = obj.sessionTypeTitle; - let existing = findBy(set, 'label', name); - if (!existing) { - existing = { - data: 0, - label: name, - meta: { - sessions: [], - }, - }; - set.push(existing); - } - existing.data += obj.minutes; - existing.meta.sessions.push(obj.sessionTitle); - - return set; - }, []); - - const totalMinutes = mapBy(sessionTypeData, 'data').reduce( - (total, minutes) => total + minutes, - 0, - ); + return dataMap + .reduce((set, obj) => { + const id = obj.sessionType.id; + let existing = findById(set, id); + if (!existing) { + existing = { + id, + data: 0, + label: obj.sessionType.title, + meta: { + sessions: [], + sessionType: obj.sessionType, + }, + }; + set.push(existing); + } + existing.data += obj.minutes; + existing.meta.sessions.push(obj.session); - return sessionTypeData.map((obj) => { - const percent = ((obj.data / totalMinutes) * 100).toFixed(1); - obj.label = `${obj.label} ${percent}%`; - obj.meta.totalMinutes = totalMinutes; - obj.meta.percent = percent; - return obj; - }); + return set; + }, []) + .map((obj) => { + obj.description = `${obj.meta.sessionType.title} - ${obj.data} ${this.intl.t('general.minutes')}`; + delete obj.id; + return obj; + }) + .sort((first, second) => { + return first.data - second.data; + }); } donutHover = restartableTask(async (obj) => { await timeout(100); - if (this.args.isIcon || isEmpty(obj) || obj.empty) { + if (this.args.isIcon || !obj || obj.empty) { this.tooltipTitle = null; this.tooltipContent = null; return; } - const { label, data, meta } = obj; - - this.tooltipTitle = htmlSafe(`${label} ${data} ${this.intl.t('general.minutes')}`); - this.tooltipContent = uniqueValues(meta.sessions).sort().join(', '); + const { data, meta } = obj; + this.tooltipTitle = htmlSafe( + `${meta.sessionType.title} • ${data} ${this.intl.t('general.minutes')}`, + ); + this.tooltipContent = htmlSafe(uniqueValues(mapBy(meta.sessions, 'title')).sort().join(', ')); }); } diff --git a/packages/ilios-common/addon/components/course/visualize-instructor-term-graph.hbs b/packages/ilios-common/addon/components/course/visualize-instructor-term-graph.hbs index e793e14b2b..b9bad31545 100644 --- a/packages/ilios-common/addon/components/course/visualize-instructor-term-graph.hbs +++ b/packages/ilios-common/addon/components/course/visualize-instructor-term-graph.hbs @@ -1,21 +1,81 @@
- {{#if (or @isIcon this.data.length)}} - - {{#if this.tooltipContent}} - - {{this.tooltipContent}} - - {{/if}} - + {{#if this.isLoaded}} + {{#if (or @isIcon this.hasChartData)}} + + {{#if this.tooltipContent}} + + {{this.tooltipContent}} + + {{/if}} + + {{/if}} + {{#if (and (not @isIcon) (not this.hasData))}} +
+ {{t "general.courseVisualizationsInstructorNoData" instructor=@user.fullName}} +
+ {{/if}} + {{#if (and (not @isIcon) this.hasData @showDataTable)}} +
+ + + + + {{t "general.term"}} + + + {{t "general.sessions"}} + + + {{t "general.minutes"}} + + + + + {{#each (sort-by this.sortBy this.tableData) as |row|}} + + + + + + {{/each}} + +
{{row.vocabularyTerm}} + {{#each row.sessions as |session index|}} + + {{session.title~}} + {{if (not-eq index (sub row.sessions.length 1)) ","}} + {{/each}} + {{row.minutes}}
+
+ {{/if}} + {{else}} + {{/if}}
diff --git a/packages/ilios-common/addon/components/course/visualize-instructor-term-graph.js b/packages/ilios-common/addon/components/course/visualize-instructor-term-graph.js index 39bd41c478..7a8e779d11 100644 --- a/packages/ilios-common/addon/components/course/visualize-instructor-term-graph.js +++ b/packages/ilios-common/addon/components/course/visualize-instructor-term-graph.js @@ -5,51 +5,82 @@ import { htmlSafe } from '@ember/template'; import { restartableTask, timeout } from 'ember-concurrency'; import { service } from '@ember/service'; import { cached, tracked } from '@glimmer/tracking'; -import { use } from 'ember-could-get-used-to-this'; import { TrackedAsyncData } from 'ember-async-data'; -import AsyncProcess from 'ilios-common/classes/async-process'; -import { findBy, mapBy, uniqueValues } from 'ilios-common/utils/array-helpers'; +import { findById, mapBy } from 'ilios-common/utils/array-helpers'; +import { action } from '@ember/object'; export default class CourseVisualizeInstructorTermGraph extends Component { @service router; @service intl; @tracked tooltipContent = null; @tracked tooltipTitle = null; + @tracked sortBy = 'minutes'; @cached - get sessionsData() { - return new TrackedAsyncData(this.args.course.sessions); + get outputData() { + return new TrackedAsyncData(this.getData(this.args.course, this.args.user)); } - get sessions() { - return this.sessionsData.isResolved ? this.sessionsData.value : null; + get data() { + return this.outputData.isResolved ? this.outputData.value : []; } - @use loadedData = new AsyncProcess(() => [this.getData.bind(this), this.sessions]); + get hasData() { + return this.data.length; + } - get data() { - if (!this.loadedData) { - return []; + get chartData() { + return this.data.filter((obj) => obj.data); + } + + get hasChartData() { + return this.chartData.length; + } + + get tableData() { + return this.data.map((obj) => { + const rhett = {}; + rhett.minutes = obj.data; + rhett.sessions = obj.meta.sessions; + rhett.vocabularyTerm = `${obj.meta.vocabulary.title} - ${obj.meta.term.title}`; + rhett.sessionTitles = mapBy(rhett.sessions, 'title').join(', '); + return rhett; + }); + } + + get isLoaded() { + return this.outputData.isResolved; + } + + get sortedAscending() { + return this.sortBy.search(/desc/) === -1; + } + + @action + setSortBy(prop) { + if (this.sortBy === prop) { + prop += ':desc'; } - return this.loadedData; + this.sortBy = prop; } - async getData(sessions) { - if (!sessions) { + async getData(course, user) { + const sessions = await course.sessions; + if (!sessions.length) { return []; } - - const sessionsWithUser = await filter(sessions.slice(), async (session) => { + const sessionsWithUser = await filter(sessions, async (session) => { const allInstructors = await session.getAllOfferingInstructors(); - return mapBy(allInstructors, 'id').includes(this.args.user.id); + return mapBy(allInstructors, 'id').includes(user.id); }); const sessionsWithTerms = await map(sessionsWithUser, async (session) => { - const terms = await map((await session.terms).slice(), async (term) => { + const sessionTerms = await session.terms; + const terms = await map(sessionTerms, async (term) => { const vocabulary = await term.vocabulary; return { - termTitle: term.title, - vocabularyTitle: vocabulary.title, + term, + vocabulary, }; }); @@ -59,59 +90,52 @@ export default class CourseVisualizeInstructorTermGraph extends Component { }; }); - const totalMinutes = ( - await map(sessionsWithTerms, async ({ session }) => { - return await session.getTotalSumOfferingsDurationByInstructor(this.args.user); - }) - ).reduce((total, mins) => total + mins, 0); - const dataMap = await map(sessionsWithTerms, async ({ session, terms }) => { const minutes = await session.getTotalSumDurationByInstructor(this.args.user); - return terms.map(({ termTitle, vocabularyTitle }) => { + return terms.map(({ term, vocabulary }) => { return { - sessionTitle: session.title, - termTitle, - vocabularyTitle, + session, + term, + vocabulary, minutes, }; }); }); - const flat = dataMap.reduce((flattened, arr) => { - return [...flattened, ...arr]; - }, []); - - const sessionTermData = flat.reduce((set, obj) => { - const name = `${obj.vocabularyTitle} > ${obj.termTitle}`; - let existing = findBy(set, 'label', name); - if (!existing) { - existing = { - data: 0, - label: name, - meta: { - sessions: [], - vocabularyTitle: obj.vocabularyTitle, - }, - }; - set.push(existing); - } - existing.data += obj.minutes; - existing.meta.sessions.push(obj.sessionTitle); - - return set; - }, []); - - return sessionTermData + return dataMap + .reduce((flattened, arr) => { + return [...flattened, ...arr]; + }, []) + .reduce((set, { term, session, vocabulary, minutes }) => { + const label = vocabulary.title + ' - ' + term.title; + const id = term.id; + let existing = findById(set, id); + if (!existing) { + existing = { + id, + data: 0, + label, + meta: { + term, + vocabulary, + sessions: [], + }, + }; + set.push(existing); + } + existing.data += minutes; + existing.meta.sessions.push(session); + + return set; + }, []) .map((obj) => { - const percent = ((obj.data / totalMinutes) * 100).toFixed(1); - obj.meta.totalMinutes = totalMinutes; - obj.meta.percent = percent; - obj.label = `${obj.label}: ${obj.data} ${this.intl.t('general.minutes')}`; + (obj.description = `${obj.meta.vocabulary.title} - ${obj.meta.term.title} - ${obj.data} ${this.intl.t('general.minutes')}`), + delete obj.id; return obj; }) .sort((first, second) => { return ( - first.meta.vocabularyTitle.localeCompare(second.meta.vocabularyTitle) || + first.meta.vocabulary.title.localeCompare(second.meta.vocabulary.title) || second.data - first.data ); }); @@ -124,9 +148,12 @@ export default class CourseVisualizeInstructorTermGraph extends Component { this.tooltipContent = null; return; } - const { label, meta } = obj; - this.tooltipTitle = htmlSafe(label); - this.tooltipContent = uniqueValues(meta.sessions).sort().join(', '); + const { data, meta } = obj; + + this.tooltipTitle = htmlSafe( + `${meta.vocabulary.title} - ${meta.term.title} • ${data} ${this.intl.t('general.minutes')}`, + ); + this.tooltipContent = htmlSafe(mapBy(meta.sessions, 'title').sort().join(', ')); }); } diff --git a/packages/ilios-common/addon/components/course/visualize-instructor.hbs b/packages/ilios-common/addon/components/course/visualize-instructor.hbs index 21477725d3..d43d20ef17 100644 --- a/packages/ilios-common/addon/components/course/visualize-instructor.hbs +++ b/packages/ilios-common/addon/components/course/visualize-instructor.hbs @@ -42,23 +42,25 @@
-
-

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

- -
-
-

- {{t "general.sessionTypes"}} -

- -
+
+

+ {{t "general.terms"}} +

+ +
+
+

+ {{t "general.sessionTypes"}} +

+ +
diff --git a/packages/ilios-common/addon/components/course/visualize-instructor.js b/packages/ilios-common/addon/components/course/visualize-instructor.js index 0b63470c8b..7294f060cc 100644 --- a/packages/ilios-common/addon/components/course/visualize-instructor.js +++ b/packages/ilios-common/addon/components/course/visualize-instructor.js @@ -1,10 +1,8 @@ import Component from '@glimmer/component'; import { service } from '@ember/service'; import { map, filter } from 'rsvp'; -import { use } from 'ember-could-get-used-to-this'; import { TrackedAsyncData } from 'ember-async-data'; import { cached } from '@glimmer/tracking'; -import AsyncProcess from 'ilios-common/classes/async-process'; import { mapBy } from 'ilios-common/utils/array-helpers'; export default class CourseVisualizeInstructorComponent extends Component { @@ -25,31 +23,28 @@ export default class CourseVisualizeInstructorComponent extends Component { } get sessions() { - return this.sessionsData.isResolved ? this.sessionsData.value : null; + return this.sessionsData.isResolved ? this.sessionsData.value.slice() : []; } - @use minutes = new AsyncProcess(() => [this.getMinutes.bind(this), this.sessions]); + @cached + get minutesData() { + return new TrackedAsyncData(this.getMinutes(this.sessions)); + } + + get minutes() { + return this.minutesData.isResolved ? this.minutesData.value : []; + } get totalInstructionalTime() { - if (!this.minutes) { - return 0; - } return mapBy(this.minutes, 'offeringMinutes').reduce((total, mins) => total + mins, 0); } get totalIlmTime() { - if (!this.minutes) { - return 0; - } return mapBy(this.minutes, 'ilmMinutes').reduce((total, mins) => total + mins, 0); } async getMinutes(sessions) { - if (!sessions) { - return []; - } - - const sessionsWithUser = await filter(sessions.slice(), async (session) => { + const sessionsWithUser = await filter(sessions, async (session) => { const instructors = await session.getAllInstructors(); return mapBy(instructors, 'id').includes(this.args.user.id); }); diff --git a/packages/ilios-common/addon/components/course/visualize-instructors-graph.hbs b/packages/ilios-common/addon/components/course/visualize-instructors-graph.hbs index d8e89ab29c..c7ea34878a 100644 --- a/packages/ilios-common/addon/components/course/visualize-instructors-graph.hbs +++ b/packages/ilios-common/addon/components/course/visualize-instructors-graph.hbs @@ -3,20 +3,84 @@ data-test-course-visualize-instructors-graph ...attributes > - {{#if (or @isIcon this.data.length)}} - - {{#if this.tooltipContent}} - - {{this.tooltipContent}} - - {{/if}} - + {{#if this.isLoaded}} + {{#if (or @isIcon this.hasChartData)}} + + {{#if this.tooltipContent}} + + {{this.tooltipContent}} + + {{/if}} + + {{/if}} + {{#if (and (not @isIcon) (not this.hasData))}} +
+ {{t "general.courseVisualizationsInstructorsGraphNoData"}} +
+ {{/if}} + {{#if (and (not @isIcon) this.hasData @showDataTable)}} +
+ + + + + {{t "general.instructor"}} + + + {{t "general.sessions"}} + + + {{t "general.minutes"}} + + + + + {{#each (sort-by this.sortBy this.tableData) as |row|}} + + + + + + {{/each}} + +
+ + {{row.instructorName}} + + + {{#each row.sessions as |session index|}} + + {{session.title~}} + {{if (not-eq index (sub row.sessions.length 1)) ","}} + {{/each}} + {{row.minutes}}
+
+ {{/if}} + {{else}} + {{/if}} diff --git a/packages/ilios-common/addon/components/course/visualize-instructors-graph.js b/packages/ilios-common/addon/components/course/visualize-instructors-graph.js index 8d9d47666c..c87c0c054c 100644 --- a/packages/ilios-common/addon/components/course/visualize-instructors-graph.js +++ b/packages/ilios-common/addon/components/course/visualize-instructors-graph.js @@ -7,64 +7,86 @@ import { service } from '@ember/service'; import { cached, tracked } from '@glimmer/tracking'; import { action } from '@ember/object'; import { cleanQuery } from 'ilios-common/utils/query-utils'; -import { use } from 'ember-could-get-used-to-this'; import { TrackedAsyncData } from 'ember-async-data'; -import AsyncProcess from 'ilios-common/classes/async-process'; -import { findBy, mapBy, uniqueValues } from 'ilios-common/utils/array-helpers'; +import { findById, mapBy, uniqueValues } from 'ilios-common/utils/array-helpers'; export default class CourseVisualizeInstructorsGraph extends Component { @service router; @service intl; @tracked tooltipContent = null; @tracked tooltipTitle = null; + @tracked sortBy = 'minutes'; @cached - get sessionsData() { - return new TrackedAsyncData(this.args.course.sessions); + get outputData() { + return new TrackedAsyncData(this.getData(this.args.course)); } - get sessions() { - return this.sessionsData.isResolved ? this.sessionsData.value : null; + get isLoaded() { + return this.outputData.isResolved; } - @use loadedData = new AsyncProcess(() => [this.getData.bind(this), this.sessions]); + get data() { + return this.outputData.isResolved ? this.outputData.value : []; + } - get chartType() { - return this.args.chartType || 'horz-bar'; + get hasData() { + return this.data.length; } - get filteredData() { - if (!this.data) { - return []; - } + get chartData() { + return this.data.filter((obj) => obj.data); + } - let data = this.data; - const q = cleanQuery(this.args.filter); - if (q) { - const exp = new RegExp(q, 'gi'); - data = this.data.filter(({ label }) => label.match(exp)); - } + get filteredChartData() { + return this.filterData(this.chartData); + } + + get hasChartData() { + return this.filteredChartData.length; + } + + get filteredData() { + return this.filterData(this.data); + } - return data.sort((first, second) => { - return first.data - second.data; + get tableData() { + return this.filteredData.map((obj) => { + const rhett = {}; + rhett.minutes = obj.data; + rhett.sessions = obj.meta.sessions; + rhett.instructor = obj.meta.user; + rhett.instructorName = obj.meta.user.fullName; + rhett.sessionTitles = mapBy(rhett.sessions, 'title').join(', '); + return rhett; }); } - get data() { - if (!this.loadedData) { - return []; + get sortedAscending() { + return this.sortBy.search(/desc/) === -1; + } + + @action + setSortBy(prop) { + if (this.sortBy === prop) { + prop += ':desc'; } - return this.loadedData; + this.sortBy = prop; } - async getData() { - if (!this.sessions) { - return []; + filterData(data) { + const q = cleanQuery(this.args.filter); + if (q) { + const exp = new RegExp(q, 'gi'); + return data.filter(({ label }) => label.match(exp)); } + return data; + } - const sessionsWithInstructors = await map(this.sessions.slice(), async (session) => { + async getData(course) { + const sessions = await course.sessions; + const sessionsWithInstructors = await map(sessions, async (session) => { const instructors = await session.getAllInstructors(); - const totalInstructionalTime = await session.getTotalSumOfferingsDuration(); const instructorsWithInstructionalTime = await map(instructors, async (instructor) => { const minutes = await session.getTotalSumOfferingsDurationByInstructor(instructor); return { @@ -73,46 +95,41 @@ export default class CourseVisualizeInstructorsGraph extends Component { }; }); return { - sessionTitle: session.title, - totalInstructionalTime: Math.round(totalInstructionalTime * 60), + session, instructorsWithInstructionalTime, }; }); - const instructorData = sessionsWithInstructors.reduce((set, obj) => { - obj.instructorsWithInstructionalTime.forEach((instructorWithInstructionalTime) => { - const name = instructorWithInstructionalTime.instructor.get('fullName'); - const id = instructorWithInstructionalTime.instructor.get('id'); - let existing = findBy(set, 'label', name); - if (!existing) { - existing = { - data: 0, - label: name, - meta: { - userId: id, - sessions: [], - }, - }; - set.push(existing); - } - existing.data += instructorWithInstructionalTime.minutes; - existing.meta.sessions.push(obj.sessionTitle); + return sessionsWithInstructors + .reduce((set, { session, instructorsWithInstructionalTime }) => { + instructorsWithInstructionalTime.forEach(({ instructor, minutes }) => { + const id = instructor.id; + let existing = findById(set, id); + if (!existing) { + existing = { + id, + data: 0, + label: instructor.fullName, + meta: { + user: instructor, + sessions: [], + }, + }; + set.push(existing); + } + existing.data += minutes; + existing.meta.sessions.push(session); + }); + return set; + }, []) + .map((obj) => { + obj.description = `${obj.meta.user.fullName} - ${obj.data} ${this.intl.t('general.minutes')}`; + delete obj.id; + return obj; + }) + .sort((first, second) => { + return first.data - second.data; }); - - return set; - }, []); - - const totalMinutes = mapBy(sessionsWithInstructors, 'totalInstructionalTime').reduce( - (total, minutes) => total + minutes, - 0, - ); - return instructorData.map((obj) => { - const percent = ((obj.data / totalMinutes) * 100).toFixed(1); - obj.label = `${obj.label}: ${obj.data} ${this.intl.t('general.minutes')}`; - obj.meta.totalMinutes = totalMinutes; - obj.meta.percent = percent; - return obj; - }); } barHover = restartableTask(async (obj) => { @@ -122,10 +139,12 @@ export default class CourseVisualizeInstructorsGraph extends Component { this.tooltipContent = null; return; } - const { label, meta } = obj; - const sessions = uniqueValues(meta.sessions).sort().join(', '); - this.tooltipTitle = htmlSafe(label); - this.tooltipContent = htmlSafe(sessions + '

' + this.intl.t('general.clickForMore')); + this.tooltipTitle = htmlSafe( + `${obj.meta.user.fullName} • ${obj.data} ${this.intl.t('general.minutes')}`, + ); + this.tooltipContent = htmlSafe( + uniqueValues(mapBy(obj.meta.sessions, 'title')).sort().join(', '), + ); }); @action @@ -134,10 +153,6 @@ export default class CourseVisualizeInstructorsGraph extends Component { return; } - this.router.transitionTo( - 'course-visualize-instructor', - this.args.course.get('id'), - obj.meta.userId, - ); + this.router.transitionTo('course-visualize-instructor', this.args.course.id, obj.meta.user.id); } } diff --git a/packages/ilios-common/addon/components/course/visualize-instructors.hbs b/packages/ilios-common/addon/components/course/visualize-instructors.hbs index 8846870c04..a7c9fbd782 100644 --- a/packages/ilios-common/addon/components/course/visualize-instructors.hbs +++ b/packages/ilios-common/addon/components/course/visualize-instructors.hbs @@ -43,7 +43,11 @@ >
- +
{{/unless}} diff --git a/packages/ilios-common/addon/components/course/visualize-objectives-graph.hbs b/packages/ilios-common/addon/components/course/visualize-objectives-graph.hbs index 5f99dc16f7..07960b3d0d 100644 --- a/packages/ilios-common/addon/components/course/visualize-objectives-graph.hbs +++ b/packages/ilios-common/addon/components/course/visualize-objectives-graph.hbs @@ -51,77 +51,79 @@ {{/if}} - {{/if}} - {{#if (and (not @isIcon) @showDataTable)}} -
- - - - - {{t "general.percentage"}} - - - {{t "general.courseObjective"}} - - - {{t "general.competencies"}} - - - {{t "general.sessions"}} - - - {{t "general.minutes"}} - - - - - {{#each (sort-by this.sortBy this.tableData) as |row|}} + {{#if (and (not @isIcon) @showDataTable)}} +
+
+ - - - - - + + {{t "general.percentage"}} + + + {{t "general.courseObjective"}} + + + {{t "general.competencies"}} + + + {{t "general.sessions"}} + + + {{t "general.minutes"}} + - {{/each}} - -
{{row.percentageLabel}}{{{row.objective}}}{{row.competencies}} - {{#each row.sessions as |session index|}} - - {{session.title~}} - {{if (not-eq index (sub row.sessions.length 1)) ","}} - {{/each}} - {{row.minutes}}
-
+ + + {{#each (sort-by this.sortBy this.tableData) as |row|}} + + {{row.percentageLabel}} + {{{row.objective}}} + {{row.competencies}} + + {{#each row.sessions as |session index|}} + + {{session.title~}} + {{if (not-eq index (sub row.sessions.length 1)) ","}} + {{/each}} + + {{row.minutes}} + + {{/each}} + + + + {{/if}} + {{else}} + {{/if}} diff --git a/packages/ilios-common/addon/components/course/visualize-objectives-graph.js b/packages/ilios-common/addon/components/course/visualize-objectives-graph.js index 176e12a260..8bee822157 100644 --- a/packages/ilios-common/addon/components/course/visualize-objectives-graph.js +++ b/packages/ilios-common/addon/components/course/visualize-objectives-graph.js @@ -5,9 +5,7 @@ import { service } from '@ember/service'; import { htmlSafe } from '@ember/template'; import { filter, map } from 'rsvp'; import { restartableTask, timeout } from 'ember-concurrency'; -import { use } from 'ember-could-get-used-to-this'; import { TrackedAsyncData } from 'ember-async-data'; -import AsyncProcess from 'ilios-common/classes/async-process'; import { mapBy, sortBy, uniqueValues } from 'ilios-common/utils/array-helpers'; export default class CourseVisualizeObjectivesGraph extends Component { @@ -20,32 +18,29 @@ export default class CourseVisualizeObjectivesGraph extends Component { @tracked sortBy = 'percentage:desc'; @cached - get courseSessionsData() { + get sessionsData() { return new TrackedAsyncData(this.args.course.sessions); } - get courseSessions() { - return this.courseSessionsData.isResolved ? this.courseSessionsData.value : null; + get sessions() { + return this.sessionsData.isResolved ? this.sessionsData.value : []; } - @use dataObjects = new AsyncProcess(() => [this.getDataObjects.bind(this), this.sessions]); + @cached + get outputData() { + return new TrackedAsyncData(this.getDataObjects(this.sessions)); + } - get sortedAscending() { - return this.sortBy.search(/desc/) === -1; + get data() { + return this.outputData.isResolved ? this.outputData.value : []; } - get sessions() { - if (!this.courseSessions) { - return []; - } - return this.courseSessions.slice(); + get sortedAscending() { + return this.sortBy.search(/desc/) === -1; } get tableData() { - if (!this.dataObjects) { - return []; - } - return this.dataObjects.map((obj) => { + return this.data.map((obj) => { const rhett = {}; rhett.minutes = obj.data; // KLUDGE! @@ -64,15 +59,15 @@ export default class CourseVisualizeObjectivesGraph extends Component { } get objectiveWithMinutes() { - return this.dataObjects?.filter((obj) => obj.data !== 0); + return this.data.filter((obj) => obj.data !== 0); } get objectiveWithoutMinutes() { - return this.dataObjects?.filter((obj) => obj.data === 0); + return this.data.filter((obj) => obj.data === 0); } get isLoaded() { - return !!this.dataObjects; + return this.outputData.isResolved; } @action @@ -84,10 +79,6 @@ export default class CourseVisualizeObjectivesGraph extends Component { } async getDataObjects(sessions) { - if (!sessions) { - return []; - } - const sessionsWithMinutes = sessions.map(async (session) => { const hours = await session.getTotalSumDuration(); return { @@ -139,14 +130,14 @@ export default class CourseVisualizeObjectivesGraph extends Component { .filter((title) => !!title) .sort(); const minutes = sessionCourseObjectiveMap.map((obj) => { - if (obj.objectives.includes(courseObjective.get('id'))) { + if (obj.objectives.includes(courseObjective.id)) { return obj.minutes; } else { return 0; } }); const sessionObjectives = sessionCourseObjectiveMap.filter((obj) => - obj.objectives.includes(courseObjective.get('id')), + obj.objectives.includes(courseObjective.id), ); const meta = { competencies: uniqueValues(competencyTitles).join(', '), @@ -168,7 +159,12 @@ export default class CourseVisualizeObjectivesGraph extends Component { return mappedObjectives.map((obj) => { const percent = totalMinutes ? ((obj.data / totalMinutes) * 100).toFixed(1) : 0; + let objectiveTitle = obj.meta.courseObjective.title; + if (obj.meta.competencies) { + objectiveTitle += ` (${obj.meta.competencies})`; + } obj.label = `${percent}%`; + obj.description = `${objectiveTitle} - ${obj.data} ${this.intl.t('general.minutes')}`; obj.percentage = percent; return obj; }); @@ -187,11 +183,9 @@ export default class CourseVisualizeObjectivesGraph extends Component { objectiveTitle += `(${meta.competencies})`; } - const title = htmlSafe(`${objectiveTitle} • ${data} ${this.intl.t('general.minutes')}`); - const sessionTitles = mapBy(meta.sessionObjectives, 'sessionTitle'); - const content = sessionTitles.sort().join(', '); - - this.tooltipTitle = title; - this.tooltipContent = content; + this.tooltipTitle = htmlSafe( + `${objectiveTitle} • ${data} ${this.intl.t('general.minutes')}`, + ); + this.tooltipContent = htmlSafe(mapBy(meta.sessionObjectives, 'sessionTitle').sort().join(', ')); }); } diff --git a/packages/ilios-common/addon/components/course/visualize-session-type-graph.hbs b/packages/ilios-common/addon/components/course/visualize-session-type-graph.hbs index 04eba89903..5b1610c25a 100644 --- a/packages/ilios-common/addon/components/course/visualize-session-type-graph.hbs +++ b/packages/ilios-common/addon/components/course/visualize-session-type-graph.hbs @@ -4,11 +4,11 @@ ...attributes > {{#if this.isLoaded}} - {{#if (or @isIcon this.data.length)}} + {{#if (or @isIcon this.hasChartData)}} @@ -19,5 +19,63 @@ {{/if}} {{/if}} + {{#if (and (not @isIcon) (not this.hasData))}} +
+ {{t "general.courseVisualizationsSessionTypeGraphNoData" sessionType=@sessionType.title}} +
+ {{/if}} + {{#if (and (not @isIcon) this.hasData @showDataTable)}} +
+ + + + + {{t "general.vocabulary"}} - {{t "general.term"}} + + + {{t "general.sessions"}} + + + {{t "general.minutes"}} + + + + + {{#each (sort-by this.sortBy this.tableData) as |row|}} + + + + + + {{/each}} + +
{{row.vocabularyTerm}} + {{#each row.sessions as |session index|}} + + {{session.title~}} + {{if (not-eq index (sub row.sessions.length 1)) ","}} + {{/each}} + {{row.minutes}}
+
+ {{/if}} + {{else}} + {{/if}} diff --git a/packages/ilios-common/addon/components/course/visualize-session-type-graph.js b/packages/ilios-common/addon/components/course/visualize-session-type-graph.js index 246b32e155..ccdef0be63 100644 --- a/packages/ilios-common/addon/components/course/visualize-session-type-graph.js +++ b/packages/ilios-common/addon/components/course/visualize-session-type-graph.js @@ -5,55 +5,69 @@ import { htmlSafe } from '@ember/template'; import { restartableTask, timeout } from 'ember-concurrency'; import { service } from '@ember/service'; import { cached, tracked } from '@glimmer/tracking'; -import { use } from 'ember-could-get-used-to-this'; import { TrackedAsyncData } from 'ember-async-data'; -import AsyncProcess from 'ilios-common/classes/async-process'; -import { findBy, mapBy, uniqueValues } from 'ilios-common/utils/array-helpers'; +import { findById, mapBy } from 'ilios-common/utils/array-helpers'; +import { action } from '@ember/object'; export default class CourseVisualizeSessionTypeGraph extends Component { @service router; @service intl; @tracked tooltipContent = null; @tracked tooltipTitle = null; + @tracked sortBy = 'vocabularyTerm'; @cached - get sessionsData() { - return new TrackedAsyncData(this.args.course.sessions); + get outputData() { + return new TrackedAsyncData(this.getDataObjects(this.args.course, this.args.sessionType)); } - @cached - get sessionTypeSessionsData() { - return new TrackedAsyncData(this.args.sessionType.sessions); + get data() { + return this.outputData.isResolved ? this.outputData.value : []; + } + + get hasData() { + return this.data.length; } - get sessions() { - return this.sessionsData.isResolved ? this.sessionsData.value : []; + get chartData() { + return this.data.filter((obj) => obj.data); } - get sessionTypeSessions() { - return this.sessionTypeSessionsData.isResolved ? this.sessionTypeSessionsData.value : []; + get hasChartData() { + return this.chartData.length; + } + + get isLoaded() { + return this.outputData.isResolved; } - @use dataObjects = new AsyncProcess(() => [ - this.getDataObjects.bind(this), - this.sessionsAndSessionTypeSessions, - ]); - - get sessionsAndSessionTypeSessions() { - const rhett = { - sessions: [], - sessionTypeSessions: [], - }; - if (this.sessions && this.sessionTypeSessions) { - rhett.sessions = this.sessions.slice(); - rhett.sessionTypeSessions = this.sessionTypeSessions.slice(); + get tableData() { + return this.data.map((obj) => { + const rhett = {}; + rhett.minutes = obj.data; + rhett.sessions = obj.meta.sessions; + rhett.vocabularyTerm = `${obj.meta.vocabulary.title} - ${obj.meta.term.title}`; + rhett.sessionTitles = mapBy(rhett.sessions, 'title').join(', '); + return rhett; + }); + } + + get sortedAscending() { + return this.sortBy.search(/desc/) === -1; + } + + @action + setSortBy(prop) { + if (this.sortBy === prop) { + prop += ':desc'; } - return rhett; + this.sortBy = prop; } - async getDataObjects(sessionsAndSessionTypeSessions) { - const sessions = sessionsAndSessionTypeSessions.sessions; - const sessionTypeSessions = sessionsAndSessionTypeSessions.sessionTypeSessions; + async getDataObjects(course, sessionType) { + const sessions = await course.sessions; + const sessionTypeSessions = await sessionType.sessions; + const courseSessionsWithSessionType = sessions.filter((session) => sessionTypeSessions.includes(session), ); @@ -67,65 +81,56 @@ export default class CourseVisualizeSessionTypeGraph extends Component { }); const termData = await map(sessionsWithMinutes, async ({ session, minutes }) => { - const terms = (await session.terms).slice(); + const terms = await session.terms; return map(terms, async (term) => { const vocabulary = await term.vocabulary; return { - sessionTitle: session.title, - termTitle: term.title, - vocabularyTitle: vocabulary.title, + session, + term, + vocabulary, minutes, }; }); }); - return termData.reduce((flattened, arr) => { - return [...flattened, ...arr]; - }, []); - } - - get data() { - const data = this.dataObjects.reduce((set, obj) => { - const label = obj.vocabularyTitle + ' - ' + obj.termTitle; - let existing = findBy(set, 'label', label); - if (!existing) { - existing = { - data: 0, - label, - meta: { - vocabularyTitle: obj.vocabularyTitle, - sessions: [], - }, - }; - set.push(existing); - } - existing.data += obj.minutes; - existing.meta.sessions.push(obj.sessionTitle); - - return set; - }, []); - - const totalMinutes = mapBy(data, 'data').reduce((total, minutes) => total + minutes, 0); - return data + return termData + .reduce((flattened, arr) => { + return [...flattened, ...arr]; + }, []) + .reduce((set, { vocabulary, term, session, minutes }) => { + const label = vocabulary.title + ' - ' + term.title; + const id = term.id; + let existing = findById(set, id); + if (!existing) { + existing = { + id, + data: 0, + label, + meta: { + vocabulary, + term, + sessions: [], + }, + }; + set.push(existing); + } + existing.data += minutes; + existing.meta.sessions.push(session); + return set; + }, []) .map((obj) => { - const percent = ((obj.data / totalMinutes) * 100).toFixed(1); - obj.label = `${obj.label}: ${obj.data} ${this.intl.t('general.minutes')}`; - obj.meta.totalMinutes = totalMinutes; - obj.meta.percent = percent; + obj.description = `${obj.meta.vocabulary.title} - ${obj.meta.term.title} - ${obj.data} ${this.intl.t('general.minutes')}`; + delete obj.id; return obj; }) .sort((first, second) => { return ( - first.meta.vocabularyTitle.localeCompare(second.meta.vocabularyTitle) || + first.meta.vocabulary.title.localeCompare(second.meta.vocabulary.title) || first.data - second.data ); }); } - get isLoaded() { - return !!this.dataObjects; - } - barHover = restartableTask(async (obj) => { await timeout(100); if (this.args.isIcon || isEmpty(obj) || obj.empty) { @@ -133,9 +138,15 @@ export default class CourseVisualizeSessionTypeGraph extends Component { this.tooltipContent = null; return; } - const { label, meta } = obj; - this.tooltipTitle = htmlSafe(label); - this.tooltipContent = uniqueValues(meta.sessions).sort().join(', '); + const { data, meta } = obj; + + const title = htmlSafe( + `${meta.vocabulary.title} - ${meta.term.title} • ${data} ${this.intl.t('general.minutes')}`, + ); + const content = mapBy(meta.sessions, 'title').sort().join(', '); + + this.tooltipTitle = title; + this.tooltipContent = content; }); } diff --git a/packages/ilios-common/addon/components/course/visualize-session-type.hbs b/packages/ilios-common/addon/components/course/visualize-session-type.hbs index cdb1883077..3a5950255d 100644 --- a/packages/ilios-common/addon/components/course/visualize-session-type.hbs +++ b/packages/ilios-common/addon/components/course/visualize-session-type.hbs @@ -41,6 +41,7 @@ {{/unless}} diff --git a/packages/ilios-common/addon/components/course/visualize-session-types-graph.hbs b/packages/ilios-common/addon/components/course/visualize-session-types-graph.hbs index 4195bb746a..5b23278aaf 100644 --- a/packages/ilios-common/addon/components/course/visualize-session-types-graph.hbs +++ b/packages/ilios-common/addon/components/course/visualize-session-types-graph.hbs @@ -3,20 +3,84 @@ data-test-course-visualize-session-types-graph ...attributes > - {{#if (or @isIcon this.data.length)}} - - {{#if this.tooltipContent}} - - {{this.tooltipContent}} - - {{/if}} - + {{#if this.isLoaded}} + {{#if (or @isIcon this.hasChartData)}} + + {{#if this.tooltipContent}} + + {{this.tooltipContent}} + + {{/if}} + + {{/if}} + {{#if (and (not @isIcon) (not this.hasData))}} +
+ {{t "general.courseVisualizationsNoSessions"}} +
+ {{/if}} + {{#if (and (not @isIcon) this.hasData @showDataTable)}} +
+ + + + + {{t "general.sessionType"}} + + + {{t "general.sessions"}} + + + {{t "general.minutes"}} + + + + + {{#each (sort-by this.sortBy this.tableData) as |row|}} + + + + + + {{/each}} + +
+ + {{row.sessionTypeTitle}} + + + {{#each row.sessions as |session index|}} + + {{session.title~}} + {{if (not-eq index (sub row.sessions.length 1)) ","}} + {{/each}} + {{row.minutes}}
+
+ {{/if}} + {{else}} + {{/if}} diff --git a/packages/ilios-common/addon/components/course/visualize-session-types-graph.js b/packages/ilios-common/addon/components/course/visualize-session-types-graph.js index f334a7b6e7..24f1c8ad7e 100644 --- a/packages/ilios-common/addon/components/course/visualize-session-types-graph.js +++ b/packages/ilios-common/addon/components/course/visualize-session-types-graph.js @@ -6,101 +6,124 @@ import { cached, tracked } from '@glimmer/tracking'; import { action } from '@ember/object'; import { cleanQuery } from 'ilios-common/utils/query-utils'; import { map } from 'rsvp'; -import { use } from 'ember-could-get-used-to-this'; import { TrackedAsyncData } from 'ember-async-data'; -import AsyncProcess from 'ilios-common/classes/async-process'; -import { findBy, mapBy, uniqueValues } from 'ilios-common/utils/array-helpers'; +import { findById, mapBy, uniqueValues } from 'ilios-common/utils/array-helpers'; export default class CourseVisualizeSessionTypesGraph extends Component { @service router; @service intl; @tracked tooltipContent = null; @tracked tooltipTitle = null; + @tracked sortBy = 'minutes'; @cached - get sessionsData() { - return new TrackedAsyncData(this.args.course.sessions); + get outputData() { + return new TrackedAsyncData(this.getData(this.args.course)); } - get sessions() { - return this.sessionsData.isResolved ? this.sessionsData.value : null; + get isLoaded() { + return this.outputData.isResolved; } - @use loadedData = new AsyncProcess(() => [this.getData.bind(this), this.sessions]); + get data() { + return this.outputData.isResolved ? this.outputData.value : []; + } + + get hasData() { + return this.data.length; + } + + get chartData() { + return this.data.filter((obj) => obj.data); + } + + get filteredChartData() { + return this.filterData(this.chartData); + } - get chartType() { - return this.args.chartType || 'horz-bar'; + get hasChartData() { + return this.filteredChartData.length; } get filteredData() { - if (!this.data) { - return []; + return this.filterData(this.data); + } + + get tableData() { + return this.filteredData.map((obj) => { + const rhett = {}; + rhett.minutes = obj.data; + rhett.sessions = obj.meta.sessions; + rhett.sessionType = obj.meta.sessionType; + rhett.sessionTypeTitle = obj.meta.sessionType.title; + rhett.sessionTitles = mapBy(rhett.sessions, 'title').join(', '); + return rhett; + }); + } + + get sortedAscending() { + return this.sortBy.search(/desc/) === -1; + } + + @action + setSortBy(prop) { + if (this.sortBy === prop) { + prop += ':desc'; } - let data = this.data; + this.sortBy = prop; + } + + filterData(data) { const q = cleanQuery(this.args.filter); if (q) { const exp = new RegExp(q, 'gi'); - data = this.data.filter(({ label }) => label.match(exp)); + return data.filter(({ label }) => label.match(exp)); } - return data.sort((first, second) => { - return first.data - second.data; - }); + return data; } - get data() { - if (!this.loadedData) { - return []; - } - return this.loadedData; - } + async getData(course) { + const sessions = await course.sessions; - async getData(sessions) { - if (!sessions) { + if (!sessions.length) { return []; } - const dataMap = await map(sessions.slice(), async (session) => { + const dataMap = await map(sessions, async (session) => { const hours = await session.getTotalSumDuration(); const minutes = Math.round(hours * 60); const sessionType = await session.sessionType; return { - sessionTitle: session.title, - sessionTypeTitle: sessionType.title, - sessionTypeId: sessionType.get('id'), + session, + sessionType, minutes, }; }); - const mappedSessionTypes = dataMap.reduce((set, obj) => { - let existing = findBy(set, 'label', obj.sessionTypeTitle); - if (!existing) { - existing = { - data: 0, - label: obj.sessionTypeTitle, - meta: { - sessionType: obj.sessionTypeTitle, - sessionTypeId: obj.sessionTypeId, - sessions: [], - }, - }; - set.push(existing); - } - existing.data += obj.minutes; - existing.meta.sessions.push(obj.sessionTitle); - - return set; - }, []); - - const totalMinutes = mapBy(mappedSessionTypes, 'data').reduce( - (total, minutes) => total + minutes, - 0, - ); - return mappedSessionTypes + return dataMap + .reduce((set, { sessionType, session, minutes }) => { + const id = sessionType.id; + let existing = findById(set, id); + if (!existing) { + existing = { + id, + data: 0, + label: sessionType.title, + meta: { + sessionType, + sessions: [], + }, + }; + set.push(existing); + } + existing.data += minutes; + existing.meta.sessions.push(session); + + return set; + }, []) .map((obj) => { - const percent = ((obj.data / totalMinutes) * 100).toFixed(1); - obj.label = `${obj.meta.sessionType}: ${obj.data} ${this.intl.t('general.minutes')}`; - obj.meta.totalMinutes = totalMinutes; - obj.meta.percent = percent; + obj.description = `${obj.meta.sessionType.title} - ${obj.data} ${this.intl.t('general.minutes')}`; + delete obj.id; return obj; }) .sort((first, second) => { @@ -115,13 +138,11 @@ export default class CourseVisualizeSessionTypesGraph extends Component { this.tooltipContent = null; return; } - const { label, meta } = obj; - - const title = htmlSafe(label); - const sessions = uniqueValues(meta.sessions).sort().join(', '); - - this.tooltipTitle = title; - this.tooltipContent = sessions; + const { data, meta } = obj; + this.tooltipTitle = htmlSafe( + `${meta.sessionType.title} • ${data} ${this.intl.t('general.minutes')}`, + ); + this.tooltipContent = htmlSafe(uniqueValues(mapBy(meta.sessions, 'title')).sort().join(', ')); }); @action @@ -131,8 +152,8 @@ export default class CourseVisualizeSessionTypesGraph extends Component { } this.router.transitionTo( 'course-visualize-session-type', - this.args.course.get('id'), - obj.meta.sessionTypeId, + this.args.course.id, + obj.meta.sessionType.id, ); } } diff --git a/packages/ilios-common/addon/components/course/visualize-session-types.hbs b/packages/ilios-common/addon/components/course/visualize-session-types.hbs index 0a96c7db92..efd6a1df30 100644 --- a/packages/ilios-common/addon/components/course/visualize-session-types.hbs +++ b/packages/ilios-common/addon/components/course/visualize-session-types.hbs @@ -41,6 +41,10 @@ >
- +
diff --git a/packages/ilios-common/addon/components/course/visualize-term-graph.hbs b/packages/ilios-common/addon/components/course/visualize-term-graph.hbs index 71908bed9d..51fd37cd4c 100644 --- a/packages/ilios-common/addon/components/course/visualize-term-graph.hbs +++ b/packages/ilios-common/addon/components/course/visualize-term-graph.hbs @@ -4,11 +4,11 @@ ...attributes > {{#if this.isLoaded}} - {{#if (or @isIcon this.data.length)}} + {{#if (or @isIcon this.hasChartData)}} @@ -19,5 +19,63 @@ {{/if}} {{/if}} + {{#if (and (not @isIcon) (not this.hasData))}} +
+ {{t "general.courseVisualizationsTermGraphNoData" term=@term.title}} +
+ {{/if}} + {{#if (and (not @isIcon) this.hasData @showDataTable)}} +
+ + + + + {{t "general.sessionType"}} + + + {{t "general.sessions"}} + + + {{t "general.minutes"}} + + + + + {{#each (sort-by this.sortBy this.tableData) as |row|}} + + + + + + {{/each}} + +
{{row.sessionType}} + {{#each row.sessions as |session index|}} + + {{session.title~}} + {{if (not-eq index (sub row.sessions.length 1)) ","}} + {{/each}} + {{row.minutes}}
+
+ {{/if}} + {{else}} + {{/if}} diff --git a/packages/ilios-common/addon/components/course/visualize-term-graph.js b/packages/ilios-common/addon/components/course/visualize-term-graph.js index 3c9cdbd0ed..52eb7b33e9 100644 --- a/packages/ilios-common/addon/components/course/visualize-term-graph.js +++ b/packages/ilios-common/addon/components/course/visualize-term-graph.js @@ -1,95 +1,111 @@ import Component from '@glimmer/component'; +import { map } from 'rsvp'; import { htmlSafe } from '@ember/template'; import { restartableTask, timeout } from 'ember-concurrency'; import { service } from '@ember/service'; import { cached, tracked } from '@glimmer/tracking'; import { TrackedAsyncData } from 'ember-async-data'; -import { - findBy, - findById, - mapBy, - uniqueById, - uniqueValues, -} from 'ilios-common/utils/array-helpers'; +import { findById, mapBy } from 'ilios-common/utils/array-helpers'; +import { action } from '@ember/object'; export default class CourseVisualizeTermGraph extends Component { @service router; @service intl; @tracked tooltipContent = null; @tracked tooltipTitle = null; + @tracked sortBy = 'minutes'; @cached - get sessionsData() { - return new TrackedAsyncData(this.args.course.sessions); + get outputData() { + return new TrackedAsyncData(this.getDataObjects(this.args.course, this.args.term)); } - get sessions() { - return this.sessionsData.isResolved ? this.sessionsData.value : null; + get data() { + return this.outputData.isResolved ? this.outputData.value : []; } - @cached - get sessionTypesData() { - if (!this.sessionsData.isResolved) { - return null; - } - return new TrackedAsyncData(Promise.all(this.sessionsData.value.map((s) => s.sessionType))); + get hasData() { + return this.data.length; + } + + get chartData() { + return this.data.filter((obj) => obj.data); } - get sessionTypes() { - return this.sessionTypesData?.isResolved ? uniqueById(this.sessionTypesData.value) : null; + get hasChartData() { + return this.chartData.length; } get isLoaded() { - return !!this.sessionTypes; + return this.outputData.isResolved; } - get termSessionIds() { - return this.args.term.hasMany('sessions').ids(); + get tableData() { + return this.data.map((obj) => { + const rhett = {}; + rhett.minutes = obj.data; + rhett.sessions = obj.meta.sessions; + rhett.sessionType = obj.meta.sessionType.title; + rhett.sessionTitles = mapBy(rhett.sessions, 'title').join(', '); + return rhett; + }); } - get termSessionsInCourse() { - return this.sessions.filter((session) => this.termSessionIds.includes(session.id)); + get sortedAscending() { + return this.sortBy.search(/desc/) === -1; } - get data() { - const sessionTypeData = this.termSessionsInCourse.map((session) => { - const minutes = Math.round(session.totalSumDuration * 60); - const sessionType = findById(this.sessionTypes, session.belongsTo('sessionType').id()); + @action + setSortBy(prop) { + if (this.sortBy === prop) { + prop += ':desc'; + } + this.sortBy = prop; + } + + async getDataObjects(course, term) { + const sessions = await course.sessions; + const sessionIds = term.hasMany('sessions').ids(); + const filteredSessions = sessions.filter((session) => sessionIds.includes(session.id)); + const sessionTypes = await Promise.all(filteredSessions.map((s) => s.sessionType)); + const sessionTypeData = await map(filteredSessions, async (session) => { + const hours = await session.getTotalSumDuration(); + const sessionType = findById(sessionTypes, session.belongsTo('sessionType').id()); return { - sessionTitle: session.title, - sessionTypeTitle: sessionType.title, - minutes, + session, + sessionType, + minutes: Math.round(hours * 60), }; }); - const data = sessionTypeData.reduce((set, obj) => { - let existing = findBy(set, 'label', obj.sessionTypeTitle); - if (!existing) { - existing = { - data: 0, - label: obj.sessionTypeTitle, - meta: { - sessionTypeTitle: obj.sessionTypeTitle, - sessions: [], - }, - }; - set.push(existing); - } - existing.data += obj.minutes; - existing.meta.sessions.push(obj.sessionTitle); - - return set; - }, []); - - const totalMinutes = mapBy(data, 'data').reduce((total, minutes) => total + minutes, 0); - - return data.map((obj) => { - const percent = ((obj.data / totalMinutes) * 100).toFixed(1); - obj.label = `${obj.meta.sessionTypeTitle} ${percent}%`; - obj.meta.totalMinutes = totalMinutes; - obj.meta.percent = percent; - return obj; - }); + return sessionTypeData + .reduce((set, { sessionType, session, minutes }) => { + const id = sessionType.id; + let existing = findById(set, id); + if (!existing) { + existing = { + id, + data: 0, + label: sessionType.title, + meta: { + sessionType: sessionType, + sessions: [], + }, + }; + set.push(existing); + } + existing.data += minutes; + existing.meta.sessions.push(session); + return set; + }, []) + .map((obj) => { + obj.description = `${obj.meta.sessionType.title} - ${obj.data} ${this.intl.t('general.minutes')}`; + delete obj.id; + return obj; + }) + .sort((first, second) => { + return first.data - second.data; + }); } barHover = restartableTask(async (obj) => { @@ -99,9 +115,10 @@ export default class CourseVisualizeTermGraph extends Component { this.tooltipContent = null; return; } - const { label, data, meta } = obj; - - this.tooltipTitle = htmlSafe(`${label} ${data} ${this.intl.t('general.minutes')}`); - this.tooltipContent = uniqueValues(meta.sessions).sort().join(', '); + const { data, meta } = obj; + this.tooltipTitle = htmlSafe( + `${meta.sessionType.title} • ${data} ${this.intl.t('general.minutes')}`, + ); + this.tooltipContent = htmlSafe(mapBy(meta.sessions, 'title').sort().join(', ')); }); } diff --git a/packages/ilios-common/addon/components/course/visualize-term.hbs b/packages/ilios-common/addon/components/course/visualize-term.hbs index ca55d32074..136d0d4d89 100644 --- a/packages/ilios-common/addon/components/course/visualize-term.hbs +++ b/packages/ilios-common/addon/components/course/visualize-term.hbs @@ -46,7 +46,11 @@
- +
{{/unless}} diff --git a/packages/ilios-common/addon/components/course/visualize-vocabularies-graph.hbs b/packages/ilios-common/addon/components/course/visualize-vocabularies-graph.hbs index 3a761a5c1d..166414ed68 100644 --- a/packages/ilios-common/addon/components/course/visualize-vocabularies-graph.hbs +++ b/packages/ilios-common/addon/components/course/visualize-vocabularies-graph.hbs @@ -4,14 +4,14 @@ ...attributes > {{#if this.isLoaded}} - {{#if (or @isIcon this.data.length)}} + {{#if (or @isIcon this.hasChartData)}} {{#if this.tooltipContent}} @@ -20,5 +20,67 @@ {{/if}} {{/if}} + {{#if (and (not @isIcon) (not this.hasData))}} +
+ {{t "general.courseVisualizationsNoSessions"}} +
+ {{/if}} + {{#if (and (not @isIcon) this.hasData @showDataTable)}} +
+ + + + + {{t "general.vocabulary"}} + + + {{t "general.sessions"}} + + + {{t "general.minutes"}} + + + + + {{#each (sort-by this.sortBy this.tableData) as |row|}} + + + + + + {{/each}} + +
+ + {{row.vocabularyTitle}} + + + {{#each row.sessions as |session index|}} + + {{session.title~}} + {{if (not-eq index (sub row.sessions.length 1)) ","}} + {{/each}} + {{row.minutes}}
+
+ {{/if}} + {{else}} + {{/if}} diff --git a/packages/ilios-common/addon/components/course/visualize-vocabularies-graph.js b/packages/ilios-common/addon/components/course/visualize-vocabularies-graph.js index 5b891eb2fd..6f89b2155c 100644 --- a/packages/ilios-common/addon/components/course/visualize-vocabularies-graph.js +++ b/packages/ilios-common/addon/components/course/visualize-vocabularies-graph.js @@ -5,93 +5,142 @@ import { restartableTask, timeout } from 'ember-concurrency'; import { service } from '@ember/service'; import { cached, tracked } from '@glimmer/tracking'; import { action } from '@ember/object'; -import { use } from 'ember-could-get-used-to-this'; import { TrackedAsyncData } from 'ember-async-data'; -import AsyncProcess from 'ilios-common/classes/async-process'; -import { findBy, mapBy } from 'ilios-common/utils/array-helpers'; +import { findById, mapBy, uniqueById } from 'ilios-common/utils/array-helpers'; export default class CourseVisualizeVocabulariesGraph extends Component { @service router; @service intl; @tracked tooltipContent = null; @tracked tooltipTitle = null; + @tracked sortBy = 'minutes'; @cached - get sessionsData() { - return new TrackedAsyncData(this.args.course.sessions); + get outputData() { + return new TrackedAsyncData(this.getDataObjects(this.args.course)); } - get sessions() { - return this.sessionsData.isResolved ? this.sessionsData.value : []; + get data() { + return this.outputData.isResolved ? this.outputData.value : []; + } + + get hasData() { + return this.data.length; + } + + get chartData() { + return this.data.filter((obj) => obj.data); + } + + get hasChartData() { + return this.chartData.length; } - @use dataObjects = new AsyncProcess(() => [this.getDataObjects.bind(this), this.sessions]); + get tableData() { + return this.data.map((obj) => { + const rhett = {}; + rhett.minutes = obj.data; + rhett.sessions = obj.meta.sessions; + rhett.vocabulary = obj.meta.vocabulary; + rhett.vocabularyTitle = obj.meta.vocabulary.title; + rhett.sessionTitles = mapBy(rhett.sessions, 'title').join(', '); + rhett.sessionTitles = mapBy(rhett.sessions, 'title').join(', '); + return rhett; + }); + } get isLoaded() { - return !!this.dataObjects; + return this.outputData.isResolved; + } + + get sortedAscending() { + return this.sortBy.search(/desc/) === -1; } - async getDataObjects(sessions) { - if (!sessions) { + @action + setSortBy(prop) { + if (this.sortBy === prop) { + prop += ':desc'; + } + this.sortBy = prop; + } + + async getDataObjects(course) { + const sessions = await course.sessions; + if (!sessions.length) { return []; } - const sessionsWithMinutes = await map(sessions.slice(), async (session) => { + + const sessionsWithMinutes = await map(sessions, async (session) => { const hours = await session.getTotalSumDuration(); return { session, minutes: Math.round(hours * 60), }; }); - return map(sessionsWithMinutes, async ({ session, minutes }) => { - const terms = (await session.terms).slice(); - const vocabularies = await all(mapBy(terms, 'vocabulary')); - return { - sessionTitle: session.title, - vocabularies, - minutes, - }; - }); - } - get data() { - return this.dataObjects.reduce((set, obj) => { - obj.vocabularies.forEach((vocabulary) => { - const vocabularyTitle = vocabulary.get('title'); - let existing = findBy(set, 'label', vocabularyTitle); - if (!existing) { - existing = { - data: 0, - label: vocabularyTitle, - meta: { - vocabulary, - sessions: [], - }, - }; - set.push(existing); - } - existing.data += obj.minutes; - existing.meta.sessions.push(obj.sessionTitle); - }); + const sessionWithMinutesAndVocabs = await map( + sessionsWithMinutes, + async ({ session, minutes }) => { + const terms = await session.terms; + const vocabularies = await all(mapBy(terms, 'vocabulary')); + return { + session, + vocabularies: uniqueById(vocabularies), + minutes, + }; + }, + ); + + return sessionWithMinutesAndVocabs + .reduce((set, { session, vocabularies, minutes }) => { + vocabularies.forEach((vocabulary) => { + const id = vocabulary.id; + let existing = findById(set, id); + if (!existing) { + existing = { + id, + data: 0, + label: vocabulary.title, + meta: { + vocabulary, + sessions: [], + }, + }; + set.push(existing); + } + existing.data += minutes; + existing.meta.sessions.push(session); + }); - return set; - }, []); + return set; + }, []) + .map((obj) => { + obj.description = `${obj.meta.vocabulary.title} - ${obj.data} ${this.intl.t('general.minutes')}`; + delete obj.id; + return obj; + }) + .sort((first, second) => { + return first.data - second.data; + }); } - donutHover = restartableTask(async (obj) => { + barHover = restartableTask(async (obj) => { await timeout(100); if (this.args.isIcon || !obj || obj.empty) { this.tooltipTitle = null; this.tooltipContent = null; return; } - const { meta } = obj; - - this.tooltipTitle = htmlSafe(meta.vocabulary.get('title')); - this.tooltipContent = this.intl.t('general.clickForMore'); + const { data, meta } = obj; + this.tooltipTitle = htmlSafe( + `${meta.vocabulary.title} • ${data} ${this.intl.t('general.minutes')}`, + ); + this.tooltipContent = htmlSafe(mapBy(meta.sessions, 'title').sort().join(', ')); }); @action - donutClick(obj) { + barClick(obj) { if (this.args.isIcon || !obj || obj.empty || !obj.meta) { return; } diff --git a/packages/ilios-common/addon/components/course/visualize-vocabularies.hbs b/packages/ilios-common/addon/components/course/visualize-vocabularies.hbs index 4845865d9c..537e4fdee7 100644 --- a/packages/ilios-common/addon/components/course/visualize-vocabularies.hbs +++ b/packages/ilios-common/addon/components/course/visualize-vocabularies.hbs @@ -32,6 +32,6 @@
- +
diff --git a/packages/ilios-common/addon/components/course/visualize-vocabulary-graph.hbs b/packages/ilios-common/addon/components/course/visualize-vocabulary-graph.hbs index d68e28cd81..828daf18cd 100644 --- a/packages/ilios-common/addon/components/course/visualize-vocabulary-graph.hbs +++ b/packages/ilios-common/addon/components/course/visualize-vocabulary-graph.hbs @@ -4,7 +4,7 @@ ...attributes > {{#if this.isLoaded}} - {{#if (or @isIcon this.data.length)}} + {{#if (or @isIcon this.hasChartData)}} {{/if}} + {{#if (and (not @isIcon) (not this.hasData))}} +
+ {{t "general.courseVisualizationsVocabularyGraphNoData" vocabulary=@vocabulary.title}} +
+ {{/if}} + {{#if (and (not @isIcon) this.hasData @showDataTable)}} +
+ + + + + {{t "general.term"}} + + + {{t "general.sessions"}} + + + {{t "general.minutes"}} + + + + + {{#each (sort-by this.sortBy this.tableData) as |row|}} + + + + + + {{/each}} + +
+ + {{row.termTitle}} + + + {{#each row.sessions as |session index|}} + + {{session.title~}} + {{if (not-eq index (sub row.sessions.length 1)) ","}} + {{/each}} + {{row.minutes}}
+
+ {{/if}} + {{else}} + {{/if}} diff --git a/packages/ilios-common/addon/components/course/visualize-vocabulary-graph.js b/packages/ilios-common/addon/components/course/visualize-vocabulary-graph.js index a1f2b0e9e4..687f2d4452 100644 --- a/packages/ilios-common/addon/components/course/visualize-vocabulary-graph.js +++ b/packages/ilios-common/addon/components/course/visualize-vocabulary-graph.js @@ -5,99 +5,126 @@ import { restartableTask, timeout } from 'ember-concurrency'; import { service } from '@ember/service'; import { cached, tracked } from '@glimmer/tracking'; import { action } from '@ember/object'; -import { use } from 'ember-could-get-used-to-this'; import { TrackedAsyncData } from 'ember-async-data'; -import AsyncProcess from 'ilios-common/classes/async-process'; -import { findBy, mapBy, uniqueValues } from 'ilios-common/utils/array-helpers'; +import { findById, mapBy } from 'ilios-common/utils/array-helpers'; export default class CourseVisualizeVocabularyGraph extends Component { @service router; @service intl; @tracked tooltipContent = null; @tracked tooltipTitle = null; + @tracked sortBy = 'minutes'; @cached - get sessionsData() { - return new TrackedAsyncData(this.args.course.sessions); + get outputData() { + return new TrackedAsyncData(this.getDataObjects(this.args.course)); } - get sessions() { - return this.sessionsData.isResolved ? this.sessionsData.value : []; + get data() { + return this.outputData.isResolved ? this.outputData.value : []; + } + + get hasData() { + return this.data.length; + } + + get chartData() { + return this.data.filter((obj) => obj.data); + } + + get hasChartData() { + return this.chartData.length; } - @use dataObjects = new AsyncProcess(() => [this.getDataObjects.bind(this), this.sessions]); + get tableData() { + return this.data.map((obj) => { + const rhett = {}; + rhett.minutes = obj.data; + rhett.sessions = obj.meta.sessions; + rhett.term = obj.meta.term; + rhett.termTitle = obj.meta.term.title; + rhett.sessionTitles = mapBy(rhett.sessions, 'title').join(', '); + return rhett; + }); + } get isLoaded() { - return !!this.dataObjects; + return this.outputData.isResolved; + } + + get sortedAscending() { + return this.sortBy.search(/desc/) === -1; + } + + @action + setSortBy(prop) { + if (this.sortBy === prop) { + prop += ':desc'; + } + this.sortBy = prop; } - async getDataObjects(sessions) { - if (!sessions) { + async getDataObjects(course) { + const sessions = await course.sessions; + if (!sessions.length) { return []; } - const sessionsWithMinutes = await map(sessions.slice(), async (session) => { + const sessionsWithMinutes = await map(sessions, async (session) => { const hours = await session.getTotalSumDuration(); return { session, minutes: Math.round(hours * 60), }; }); - const terms = await map(sessionsWithMinutes, async ({ session, minutes }) => { - const sessionTerms = await session.get('terms'); - const sessionTermsInThisVocabulary = await filter(sessionTerms.slice(), async (term) => { - const termVocab = await term.get('vocabulary'); - return termVocab.get('id') === this.args.vocabulary.get('id'); - }); - return sessionTermsInThisVocabulary.map((term) => { - return { - term, - session: { - title: session.get('title'), + const termsWithSessionAndMinutes = await map( + sessionsWithMinutes, + async ({ session, minutes }) => { + const sessionTerms = await session.terms; + const sessionTermsInThisVocabulary = await filter(sessionTerms.slice(), async (term) => { + const termVocab = await term.vocabulary; + return termVocab.id === this.args.vocabulary.id; + }); + return sessionTermsInThisVocabulary.map((term) => { + return { + term, + session, minutes, - }, - }; - }); - }); - - return terms.reduce((flattened, arr) => { - return [...flattened, ...arr]; - }, []); - } - - get data() { - const termData = this.dataObjects.reduce((set, { term, session }) => { - const termTitle = term.get('title'); - let existing = findBy(set, 'label', termTitle); - if (!existing) { - existing = { - data: 0, - label: termTitle, - meta: { - termTitle, - termId: term.get('id'), - sessions: [], - }, - }; - set.push(existing); - } - existing.data += session.minutes; - existing.meta.sessions.push(session.title); - - return set; - }, []); - - const totalMinutes = mapBy(termData, 'data').reduce((total, minutes) => total + minutes, 0); - const mappedTermsWithLabel = termData.map((obj) => { - const percent = ((obj.data / totalMinutes) * 100).toFixed(1); - obj.label = `${obj.meta.termTitle}: ${obj.data} ${this.intl.t('general.minutes')}`; - obj.meta.totalMinutes = totalMinutes; - obj.meta.percent = percent; - return obj; - }); + }; + }); + }, + ); - return mappedTermsWithLabel.sort((first, second) => { - return first.data - second.data; - }); + return termsWithSessionAndMinutes + .reduce((flattened, arr) => { + return [...flattened, ...arr]; + }, []) + .reduce((set, { term, session, minutes }) => { + const id = term.id; + let existing = findById(set, id); + if (!existing) { + existing = { + id, + data: 0, + label: term.title, + meta: { + term, + sessions: [], + }, + }; + set.push(existing); + } + existing.data += minutes; + existing.meta.sessions.push(session); + return set; + }, []) + .map((obj) => { + obj.description = `${obj.meta.term.title} - ${obj.data} ${this.intl.t('general.minutes')}`; + delete obj.id; + return obj; + }) + .sort((first, second) => { + return first.data - second.data; + }); } barHover = restartableTask(async (obj) => { @@ -107,10 +134,11 @@ export default class CourseVisualizeVocabularyGraph extends Component { this.tooltipContent = null; return; } - const { label, meta } = obj; - - this.tooltipTitle = htmlSafe(label); - this.tooltipContent = uniqueValues(meta.sessions).sort().join(', '); + const { data, meta } = obj; + this.tooltipTitle = htmlSafe( + `${meta.term.title} • ${data} ${this.intl.t('general.minutes')}`, + ); + this.tooltipContent = htmlSafe(mapBy(meta.sessions, 'title').sort().join(', ')); }); @action @@ -118,6 +146,6 @@ export default class CourseVisualizeVocabularyGraph extends Component { if (this.args.isIcon || !obj || obj.empty || !obj.meta) { return; } - this.router.transitionTo('course-visualize-term', this.args.course.id, obj.meta.termId); + this.router.transitionTo('course-visualize-term', this.args.course.id, obj.meta.term.id); } } diff --git a/packages/ilios-common/addon/components/course/visualize-vocabulary.hbs b/packages/ilios-common/addon/components/course/visualize-vocabulary.hbs index 8745ba8416..33925dec1e 100644 --- a/packages/ilios-common/addon/components/course/visualize-vocabulary.hbs +++ b/packages/ilios-common/addon/components/course/visualize-vocabulary.hbs @@ -40,6 +40,7 @@ diff --git a/packages/ilios-common/app/styles/ilios-common/components.scss b/packages/ilios-common/app/styles/ilios-common/components.scss index 602b004faa..b8b30cdd2f 100644 --- a/packages/ilios-common/app/styles/ilios-common/components.scss +++ b/packages/ilios-common/app/styles/ilios-common/components.scss @@ -131,6 +131,7 @@ @import "components/course/visualizations"; @import "components/course/visualize-instructor"; @import "components/course/visualize-instructor-session-type-graph"; +@import "components/course/visualize-instructor-term-graph"; @import "components/course/visualize-instructors"; @import "components/course/visualize-instructors-graph"; @import "components/course/visualize-objectives"; diff --git a/packages/ilios-common/app/styles/ilios-common/components/course/visualizations.scss b/packages/ilios-common/app/styles/ilios-common/components/course/visualizations.scss index 1458eba80f..ab03a7671a 100644 --- a/packages/ilios-common/app/styles/ilios-common/components/course/visualizations.scss +++ b/packages/ilios-common/app/styles/ilios-common/components/course/visualizations.scss @@ -4,13 +4,25 @@ @include m.data-visualization; .visualizations { + @include m.for-tablet-and-up { + grid-template-columns: repeat(2, 1fr); + } + + margin-bottom: 1rem; + .course-visualize-instructors-graph, .course-visualize-objectives-graph, .course-visualize-session-types-graph, .course-visualize-vocabularies-graph { - height: 40vh; - margin-bottom: 2rem; - width: 40vw; + display: inline-block; + height: 100%; + text-align: center; + width: 100%; + + .simple-chart { + height: 250px; + width: 250px; + } } } } diff --git a/packages/ilios-common/app/styles/ilios-common/components/course/visualize-instructor-session-type-graph.scss b/packages/ilios-common/app/styles/ilios-common/components/course/visualize-instructor-session-type-graph.scss index 4d8794c5cf..3b458ff58b 100644 --- a/packages/ilios-common/app/styles/ilios-common/components/course/visualize-instructor-session-type-graph.scss +++ b/packages/ilios-common/app/styles/ilios-common/components/course/visualize-instructor-session-type-graph.scss @@ -1,19 +1,5 @@ -.course-visualize-instructor-session-type-graph { - display: inline-block; - height: 1rem; - width: 1rem; - - &.not-icon { - height: 75vh; - width: 75vw; +@use "../../mixins" as m; - .simple-chart-tooltip { - .title { - p { - margin: 0; - padding: 0; - } - } - } - } +.course-visualize-instructor-session-type-graph { + @include m.graph-with-data-table; } diff --git a/packages/ilios-common/app/styles/ilios-common/components/course/visualize-instructor-term-graph.scss b/packages/ilios-common/app/styles/ilios-common/components/course/visualize-instructor-term-graph.scss new file mode 100644 index 0000000000..6f131e323c --- /dev/null +++ b/packages/ilios-common/app/styles/ilios-common/components/course/visualize-instructor-term-graph.scss @@ -0,0 +1,5 @@ +@use "../../mixins" as m; + +.course-visualize-instructor-term-graph { + @include m.graph-with-data-table; +} diff --git a/packages/ilios-common/app/styles/ilios-common/components/course/visualize-instructor.scss b/packages/ilios-common/app/styles/ilios-common/components/course/visualize-instructor.scss index 9c4e14bb4f..7803f1a70b 100644 --- a/packages/ilios-common/app/styles/ilios-common/components/course/visualize-instructor.scss +++ b/packages/ilios-common/app/styles/ilios-common/components/course/visualize-instructor.scss @@ -3,16 +3,11 @@ .course-visualize-instructor { @include m.data-visualization; - .visualizations { - .course-visualize-instructor-session-type-graph, - .course-visualize-instructor-term-graph { - height: 80vh; - width: 80vw; - - @include m.for-laptop-and-up { - height: 40vh; - width: 40vw; - } + @include m.for-laptop-and-up { + .visualizations { + display: grid; + grid-gap: 10px; + grid-template-columns: 1fr 1fr; } } } diff --git a/packages/ilios-common/app/styles/ilios-common/components/course/visualize-instructors-graph.scss b/packages/ilios-common/app/styles/ilios-common/components/course/visualize-instructors-graph.scss index a8a1964a2e..1347eed10f 100644 --- a/packages/ilios-common/app/styles/ilios-common/components/course/visualize-instructors-graph.scss +++ b/packages/ilios-common/app/styles/ilios-common/components/course/visualize-instructors-graph.scss @@ -1,19 +1,5 @@ -.course-visualize-instructors-graph { - display: inline-block; - height: 1rem; - width: 1rem; - - &.not-icon { - height: 75vh; - width: 75vw; +@use "../../mixins" as m; - .simple-chart-tooltip { - .title { - p { - margin: 0; - padding: 0; - } - } - } - } +.course-visualize-instructors-graph { + @include m.graph-with-data-table; } diff --git a/packages/ilios-common/app/styles/ilios-common/components/course/visualize-objectives-graph.scss b/packages/ilios-common/app/styles/ilios-common/components/course/visualize-objectives-graph.scss index f94b5d938a..a1515b6bc7 100644 --- a/packages/ilios-common/app/styles/ilios-common/components/course/visualize-objectives-graph.scss +++ b/packages/ilios-common/app/styles/ilios-common/components/course/visualize-objectives-graph.scss @@ -1,10 +1,7 @@ -@use "../../colors" as c; @use "../../mixins" as m; .course-visualize-objectives-graph { - display: inline-block; - height: 1rem; - width: 1rem; + @include m.graph-with-data-table; .with-hours { p { @@ -16,15 +13,21 @@ } } - .zero-hours { + .objective-row { p { - margin-top: 0.5rem; + margin: 0; } + } + .zero-hours { h4 { @include m.ilios-heading-h4; } + p { + margin-top: 0.5rem; + } + li { list-style-type: disc; margin-left: 1rem; @@ -36,49 +39,7 @@ } } - .data-table { - grid-column: -1/1; - padding-top: 2rem; - - table { - @include m.ilios-table-structure; - @include m.ilios-table-colors; - @include m.ilios-removable-table; - @include m.ilios-zebra-table; - - thead { - background-color: c.$culturedGrey; - } - - td { - vertical-align: top; - } - - .objective { - p { - margin: 0; - } - } - } - } - &.not-icon { - display: grid; grid-template-columns: 2fr 1fr; - height: auto; - width: auto; - - .simple-chart { - height: 80vh; - } - - .simple-chart-tooltip { - .title { - p { - margin: 0; - padding: 0; - } - } - } } } diff --git a/packages/ilios-common/app/styles/ilios-common/components/course/visualize-session-type-graph.scss b/packages/ilios-common/app/styles/ilios-common/components/course/visualize-session-type-graph.scss index 33c08b03d2..16cec328ee 100644 --- a/packages/ilios-common/app/styles/ilios-common/components/course/visualize-session-type-graph.scss +++ b/packages/ilios-common/app/styles/ilios-common/components/course/visualize-session-type-graph.scss @@ -1,19 +1,5 @@ -.course-visualize-session-type-graph { - display: inline-block; - height: 1rem; - width: 1rem; - - &.not-icon { - height: 75vh; - width: 75vw; +@use "../../mixins" as m; - .simple-chart-tooltip { - .title { - p { - margin: 0; - padding: 0; - } - } - } - } +.course-visualize-session-type-graph { + @include m.graph-with-data-table; } diff --git a/packages/ilios-common/app/styles/ilios-common/components/course/visualize-session-types-graph.scss b/packages/ilios-common/app/styles/ilios-common/components/course/visualize-session-types-graph.scss index e9609c27e8..1a076e206c 100644 --- a/packages/ilios-common/app/styles/ilios-common/components/course/visualize-session-types-graph.scss +++ b/packages/ilios-common/app/styles/ilios-common/components/course/visualize-session-types-graph.scss @@ -1,19 +1,5 @@ -.course-visualize-session-types-graph { - display: inline-block; - height: 1rem; - width: 1rem; - - &.not-icon { - height: 75vh; - width: 75vw; +@use "../../mixins" as m; - .simple-chart-tooltip { - .title { - p { - margin: 0; - padding: 0; - } - } - } - } +.course-visualize-session-types-graph { + @include m.graph-with-data-table; } diff --git a/packages/ilios-common/app/styles/ilios-common/components/course/visualize-term-graph.scss b/packages/ilios-common/app/styles/ilios-common/components/course/visualize-term-graph.scss index 65a2808359..7d15d1547a 100644 --- a/packages/ilios-common/app/styles/ilios-common/components/course/visualize-term-graph.scss +++ b/packages/ilios-common/app/styles/ilios-common/components/course/visualize-term-graph.scss @@ -1,19 +1,5 @@ -.course-visualize-term-graph { - display: inline-block; - height: 1rem; - width: 1rem; - - &.not-icon { - height: 75vh; - width: 75vw; +@use "../../mixins" as m; - .simple-chart-tooltip { - .title { - p { - margin: 0; - padding: 0; - } - } - } - } +.course-visualize-term-graph { + @include m.graph-with-data-table; } diff --git a/packages/ilios-common/app/styles/ilios-common/components/course/visualize-vocabularies-graph.scss b/packages/ilios-common/app/styles/ilios-common/components/course/visualize-vocabularies-graph.scss index f8c3f14484..13b8d8f0ea 100644 --- a/packages/ilios-common/app/styles/ilios-common/components/course/visualize-vocabularies-graph.scss +++ b/packages/ilios-common/app/styles/ilios-common/components/course/visualize-vocabularies-graph.scss @@ -1,19 +1,5 @@ -.course-visualize-vocabularies-graph { - display: inline-block; - height: 1rem; - width: 1rem; - - &.not-icon { - height: 75vh; - width: 75vw; +@use "../../mixins" as m; - .simple-chart-tooltip { - .title { - p { - margin: 0; - padding: 0; - } - } - } - } +.course-visualize-vocabularies-graph { + @include m.graph-with-data-table; } diff --git a/packages/ilios-common/app/styles/ilios-common/components/course/visualize-vocabulary-graph.scss b/packages/ilios-common/app/styles/ilios-common/components/course/visualize-vocabulary-graph.scss index 08009c5f0e..97ea18087a 100644 --- a/packages/ilios-common/app/styles/ilios-common/components/course/visualize-vocabulary-graph.scss +++ b/packages/ilios-common/app/styles/ilios-common/components/course/visualize-vocabulary-graph.scss @@ -1,19 +1,5 @@ -.course-visualize-vocabulary-graph { - display: inline-block; - height: 1rem; - width: 1rem; - - &.not-icon { - height: 75vh; - width: 75vw; +@use "../../mixins" as m; - .simple-chart-tooltip { - .title { - p { - margin: 0; - padding: 0; - } - } - } - } +.course-visualize-vocabulary-graph { + @include m.graph-with-data-table; } diff --git a/packages/ilios-common/app/styles/ilios-common/mixins.scss b/packages/ilios-common/app/styles/ilios-common/mixins.scss index 398c8b174e..f067526e86 100644 --- a/packages/ilios-common/app/styles/ilios-common/mixins.scss +++ b/packages/ilios-common/app/styles/ilios-common/mixins.scss @@ -5,6 +5,7 @@ @forward "mixins/data-visualization"; @forward "mixins/detail-container"; @forward "mixins/font-size"; +@forward "mixins/graph-with-data-table"; @forward "mixins/icon"; @forward "mixins/ilios-button"; @forward "mixins/ilios-form"; diff --git a/packages/ilios-common/app/styles/ilios-common/mixins/data-visualization.scss b/packages/ilios-common/app/styles/ilios-common/mixins/data-visualization.scss index bfbaa989b7..f3150802d2 100644 --- a/packages/ilios-common/app/styles/ilios-common/mixins/data-visualization.scss +++ b/packages/ilios-common/app/styles/ilios-common/mixins/data-visualization.scss @@ -30,10 +30,9 @@ } .visualizations { - display: flex; - flex-wrap: wrap; - justify-items: center; - margin-top: 2rem; - padding-left: 0.8rem; + display: grid; + grid-template-columns: 1fr; + grid-gap: 10px; + margin-left: 0.8rem; } } diff --git a/packages/ilios-common/app/styles/ilios-common/mixins/graph-with-data-table.scss b/packages/ilios-common/app/styles/ilios-common/mixins/graph-with-data-table.scss new file mode 100644 index 0000000000..a42d1f9f17 --- /dev/null +++ b/packages/ilios-common/app/styles/ilios-common/mixins/graph-with-data-table.scss @@ -0,0 +1,51 @@ +@use "../colors" as c; +@use "ilios-table" as t; + +@mixin graph-with-data-table() { + display: inline-block; + height: 1rem; + width: 1rem; + + .data-table { + grid-column: -1/1; + padding-top: 2rem; + + table { + @include t.ilios-table-structure; + @include t.ilios-table-colors; + @include t.ilios-removable-table; + @include t.ilios-zebra-table; + + thead { + background-color: c.$culturedGrey; + } + + td { + vertical-align: top; + } + } + } + + &.not-icon { + display: grid; + height: auto; + width: auto; + + .simple-chart { + height: 80vh; + } + + .simple-chart-tooltip { + .title { + p { + margin: 0; + padding: 0; + } + } + } + } + + .no-data { + text-align: center; + } +} diff --git a/packages/ilios-common/translations/en-us.yaml b/packages/ilios-common/translations/en-us.yaml index 0a9649519a..24adc83dde 100644 --- a/packages/ilios-common/translations/en-us.yaml +++ b/packages/ilios-common/translations/en-us.yaml @@ -87,6 +87,12 @@ general: courseTitle: Course Title courseTitlePlaceholder: Enter a title for this course courseVisualizations: Course Visualizations + courseVisualizationsNoSessions: "This course has no sessions." + courseVisualizationsInstructorNoData: "{instructor} is not instructing any sessions in this this course." + courseVisualizationsInstructorsGraphNoData: No instructors have been linked to any sessions in this course. + courseVisualizationsSessionTypeGraphNoData: "No vocabulary terms have been linked to any {sessionType} sessions in this course." + courseVisualizationsTermGraphNoData: "The vocabulary term {term} has not been linked to any sessions in this course." + courseVisualizationsVocabularyGraphNoData: "No {vocabulary} vocabulary terms have been linked to any sessions in this course." currentlySearchingPrompt: searching... dashboardNavigation: Dashboard navigation date: Date diff --git a/packages/ilios-common/translations/es.yaml b/packages/ilios-common/translations/es.yaml index cc93e4ac3a..51b8729b51 100644 --- a/packages/ilios-common/translations/es.yaml +++ b/packages/ilios-common/translations/es.yaml @@ -87,6 +87,12 @@ general: courseTitle: Titulo de Curso courseTitlePlaceholder: Entre en un título para este curso courseVisualizations: Visualizaciones de Cursos + courseVisualizationsNoSessions: Este curso no tiene sesiones. + courseVisualizationsInstructorNoData: "{instructor} no está impartiendo ninguna sesión en este curso." + courseVisualizationsInstructorsGraphNoData: No se han vinculado instructores a ninguna sesión de este curso. + courseVisualizationsSessionTypeGraphNoData: "No se han vinculado términos de vocabulario a ninguna sesión {sessionType} en este curso." + courseVisualizationsTermGraphNoData: "El término de vocabulario {term} no se ha vinculado a ninguna sesión de este curso." + courseVisualizationsVocabularyGraphNoData: "No se han vinculado términos del vocabulario {vocabulary} a ninguna sesión de este curso." currentlySearchingPrompt: buscando... dashboardNavigation: Panel de navegación date: Fecha diff --git a/packages/ilios-common/translations/fr.yaml b/packages/ilios-common/translations/fr.yaml index 19c14c0624..7d840436bf 100644 --- a/packages/ilios-common/translations/fr.yaml +++ b/packages/ilios-common/translations/fr.yaml @@ -87,6 +87,12 @@ general: courseTitle: Titre de Cours courseTitlePlaceholder: Ajoutez titre par ce cours courseVisualizations: Cours Visualisations + courseVisualizationsNoSessions: Ce cours n'a pas de séances. + courseVisualizationsInstructorNoData: "{instructor} n'enseigne aucune séance dans ce cours." + courseVisualizationsInstructorsGraphNoData: Aucun instructeur n'a été lié à aucune session de ce cours. + courseVisualizationsSessionTypeGraphNoData: "Aucun terme de vocabulaire n'a été lié à une session {sessionType} dans ce cours." + courseVisualizationsTermGraphNoData: "Le terme de vocabulaire {term} n'a été lié à aucune session de ce cours." + courseVisualizationsVocabularyGraphNoData: "Aucun terme de vocabulaire {vocabulary} n'a été lié à aucune session de ce cours." currentlySearchingPrompt: Recherchent... dashboardNavigation: Naviguer dans le tableau de bord date: Date diff --git a/packages/test-app/tests/integration/components/course/visualize-instructor-session-type-graph-test.js b/packages/test-app/tests/integration/components/course/visualize-instructor-session-type-graph-test.js index 71672d6ffa..b56686be35 100644 --- a/packages/test-app/tests/integration/components/course/visualize-instructor-session-type-graph-test.js +++ b/packages/test-app/tests/integration/components/course/visualize-instructor-session-type-graph-test.js @@ -11,7 +11,7 @@ module( setupRenderingTest(hooks); setupMirage(hooks); - test('it renders', async function (assert) { + hooks.beforeEach(async function () { const instructor = this.server.create('user'); const sessionType1 = this.server.create('session-type', { title: 'Standalone', @@ -19,17 +19,26 @@ module( const sessionType2 = this.server.create('session-type', { title: 'Campaign', }); - const course = this.server.create('course'); + const sessionType3 = this.server.create('session-type', { + title: 'Prelude', + }); + const linkedCourseWithTime = this.server.create('course'); + const linkedCourseWithoutTime = this.server.create('course'); const session1 = this.server.create('session', { title: 'Berkeley Investigations', - course, + course: linkedCourseWithTime, sessionType: sessionType1, }); const session2 = this.server.create('session', { title: 'The San Leandro Horror', - course, + course: linkedCourseWithTime, sessionType: sessionType2, }); + const session3 = this.server.create('session', { + title: 'Two Slices of Pizza', + course: linkedCourseWithoutTime, + sessionType: sessionType3, + }); this.server.create('offering', { session: session1, startDate: new Date('2019-12-08T12:00:00'), @@ -48,27 +57,156 @@ module( endDate: new Date('2019-12-05T21:00:00'), instructors: [instructor], }); - - const courseModel = await this.owner.lookup('service:store').findRecord('course', course.id); - const instructorModel = await this.owner + this.server.create('offering', { + session: session3, + startDate: new Date('2019-12-05T18:00:00'), + endDate: new Date('2019-12-05T18:00:00'), + instructors: [instructor], + }); + this.emptyCourse = await this.owner + .lookup('service:store') + .findRecord('course', this.server.create('course').id); + this.linkedCourseWithTime = await this.owner .lookup('service:store') - .findRecord('user', instructor.id); + .findRecord('course', linkedCourseWithTime.id); + this.linkedCourseWithoutTime = await this.owner + .lookup('service:store') + .findRecord('course', linkedCourseWithoutTime.id); + this.instructor = await this.owner.lookup('service:store').findRecord('user', instructor.id); + }); + + test('it renders', async function (assert) { + this.set('course', this.linkedCourseWithTime); + this.set('instructor', this.instructor); + await render( + hbs` +`, + ); + assert.notOk(component.noData.isVisible); + await waitFor('.loaded'); + await waitFor('svg .slice'); + assert.strictEqual(component.chart.slices.length, 2); + assert.strictEqual(component.chart.descriptions.length, 2); + assert.strictEqual(component.chart.descriptions[0].text, 'Campaign - 180 Minutes'); + assert.strictEqual(component.chart.descriptions[1].text, 'Standalone - 630 Minutes'); + assert.strictEqual(component.chart.labels.length, 2); + assert.strictEqual(component.chart.labels[0].text, 'Campaign'); + assert.strictEqual(component.chart.labels[1].text, 'Standalone'); + assert.strictEqual(component.dataTable.rows.length, 2); + assert.strictEqual(component.dataTable.rows[0].sessionType, 'Campaign'); + assert.strictEqual(component.dataTable.rows[0].sessions.links.length, 1); + assert.strictEqual( + component.dataTable.rows[0].sessions.links[0].text, + 'The San Leandro Horror', + ); + assert.strictEqual( + component.dataTable.rows[0].sessions.links[0].url, + '/courses/1/sessions/2', + ); + assert.strictEqual(component.dataTable.rows[0].minutes, '180'); + assert.strictEqual(component.dataTable.rows[1].sessionType, 'Standalone'); + assert.strictEqual(component.dataTable.rows[1].sessions.links.length, 1); + assert.strictEqual( + component.dataTable.rows[1].sessions.links[0].text, + 'Berkeley Investigations', + ); + assert.strictEqual( + component.dataTable.rows[1].sessions.links[0].url, + '/courses/1/sessions/1', + ); + assert.strictEqual(component.dataTable.rows[1].minutes, '630'); + }); - this.set('course', courseModel); - this.set('instructor', instructorModel); + test('sort data-table by session type', async function (assert) { + this.set('course', this.linkedCourseWithTime); + this.set('instructor', this.instructor); + await render( + hbs` +`, + ); + assert.strictEqual(component.dataTable.rows[0].sessionType, 'Campaign'); + assert.strictEqual(component.dataTable.rows[1].sessionType, 'Standalone'); + await component.dataTable.header.sessionType.toggle(); + assert.strictEqual(component.dataTable.rows[0].sessionType, 'Campaign'); + assert.strictEqual(component.dataTable.rows[1].sessionType, 'Standalone'); + await component.dataTable.header.sessionType.toggle(); + assert.strictEqual(component.dataTable.rows[0].sessionType, 'Standalone'); + assert.strictEqual(component.dataTable.rows[1].sessionType, 'Campaign'); + await component.dataTable.header.sessionType.toggle(); + assert.strictEqual(component.dataTable.rows[0].sessionType, 'Campaign'); + assert.strictEqual(component.dataTable.rows[1].sessionType, 'Standalone'); + }); + test('sort data-table by sessions', async function (assert) { + this.set('course', this.linkedCourseWithTime); + this.set('instructor', this.instructor); await render( - hbs` + hbs` `, ); + assert.strictEqual(component.dataTable.rows[0].sessions.text, 'The San Leandro Horror'); + assert.strictEqual(component.dataTable.rows[1].sessions.text, 'Berkeley Investigations'); + await component.dataTable.header.sessions.toggle(); + assert.strictEqual(component.dataTable.rows[0].sessions.text, 'Berkeley Investigations'); + assert.strictEqual(component.dataTable.rows[1].sessions.text, 'The San Leandro Horror'); + await component.dataTable.header.sessions.toggle(); + assert.strictEqual(component.dataTable.rows[0].sessions.text, 'The San Leandro Horror'); + assert.strictEqual(component.dataTable.rows[1].sessions.text, 'Berkeley Investigations'); + await component.dataTable.header.sessions.toggle(); + assert.strictEqual(component.dataTable.rows[0].sessions.text, 'Berkeley Investigations'); + assert.strictEqual(component.dataTable.rows[1].sessions.text, 'The San Leandro Horror'); + }); - //let the chart animations finish - await waitFor('.loaded'); - await waitFor('svg .chart .slice'); + test('sort data-table by minutes', async function (assert) { + this.set('course', this.linkedCourseWithTime); + this.set('instructor', this.instructor); + await render( + hbs` +`, + ); + assert.strictEqual(component.dataTable.rows[0].minutes, '180'); + assert.strictEqual(component.dataTable.rows[1].minutes, '630'); + await component.dataTable.header.minutes.toggle(); + assert.strictEqual(component.dataTable.rows[0].minutes, '630'); + assert.strictEqual(component.dataTable.rows[1].minutes, '180'); + await component.dataTable.header.minutes.toggle(); + assert.strictEqual(component.dataTable.rows[0].minutes, '180'); + assert.strictEqual(component.dataTable.rows[1].minutes, '630'); + }); - assert.strictEqual(component.chart.slices.length, 2); - assert.strictEqual(component.chart.slices[0].text, 'Standalone 77.8%'); - assert.strictEqual(component.chart.slices[1].text, 'Campaign 22.2%'); + test('no data', async function (assert) { + this.set('course', this.emptyCourse); + this.set('instructor', this.instructor); + await render( + hbs` +`, + ); + assert.notOk(component.chart.isVisible); + assert.notOk(component.dataTable.isVisible); + assert.strictEqual( + component.noData.text, + '0 guy M. Mc0son is not instructing any sessions in this this course.', + ); + }); + + test('only zero time data', async function (assert) { + this.set('course', this.linkedCourseWithoutTime); + this.set('instructor', this.instructor); + await render( + hbs` +`, + ); + assert.notOk(component.chart.isVisible); + assert.notOk(component.noData.isVisible); + assert.strictEqual(component.dataTable.rows.length, 1); + assert.strictEqual(component.dataTable.rows[0].sessionType, 'Prelude'); + assert.strictEqual(component.dataTable.rows[0].sessions.links.length, 1); + assert.strictEqual(component.dataTable.rows[0].sessions.links[0].text, 'Two Slices of Pizza'); + assert.strictEqual( + component.dataTable.rows[0].sessions.links[0].url, + '/courses/2/sessions/3', + ); + assert.strictEqual(component.dataTable.rows[0].minutes, '0'); }); }, ); diff --git a/packages/test-app/tests/integration/components/course/visualize-instructor-term-graph-test.js b/packages/test-app/tests/integration/components/course/visualize-instructor-term-graph-test.js index 6710586853..948c54447e 100644 --- a/packages/test-app/tests/integration/components/course/visualize-instructor-term-graph-test.js +++ b/packages/test-app/tests/integration/components/course/visualize-instructor-term-graph-test.js @@ -9,7 +9,7 @@ module('Integration | Component | course/visualize-instructor-term-graph', funct setupRenderingTest(hooks); setupMirage(hooks); - test('it renders', async function (assert) { + hooks.beforeEach(async function () { const instructor = this.server.create('user'); const vocabulary1 = this.server.create('vocabulary'); const vocabulary2 = this.server.create('vocabulary'); @@ -21,20 +21,31 @@ module('Integration | Component | course/visualize-instructor-term-graph', funct vocabulary: vocabulary2, title: 'Campaign', }); + const term3 = this.server.create('term', { + vocabulary: vocabulary2, + title: 'Prelude', + }); const sessionType = this.server.create('session-type'); - const course = this.server.create('course'); + const linkedCourseWithTime = this.server.create('course'); + const linkedCourseWithoutTime = this.server.create('course'); const session1 = this.server.create('session', { title: 'Berkeley Investigations', - course, + course: linkedCourseWithTime, terms: [term1], sessionType: sessionType, }); const session2 = this.server.create('session', { title: 'The San Leandro Horror', - course, + course: linkedCourseWithTime, terms: [term2], sessionType: sessionType, }); + const session3 = this.server.create('session', { + title: 'Two Slices of Pizza', + course: linkedCourseWithoutTime, + terms: [term3], + sessionType: sessionType, + }); this.server.create('offering', { session: session1, startDate: new Date('2019-12-08T12:00:00'), @@ -53,24 +64,148 @@ module('Integration | Component | course/visualize-instructor-term-graph', funct endDate: new Date('2019-12-05T21:00:00'), instructors: [instructor], }); + this.server.create('offering', { + session: session3, + startDate: new Date('2019-12-05T18:00:00'), + endDate: new Date('2019-12-05T18:00:00'), + instructors: [instructor], + }); + this.emptyCourse = await this.owner + .lookup('service:store') + .findRecord('course', this.server.create('course').id); + this.linkedCourseWithTime = await this.owner + .lookup('service:store') + .findRecord('course', linkedCourseWithTime.id); + this.linkedCourseWithoutTime = await this.owner + .lookup('service:store') + .findRecord('course', linkedCourseWithoutTime.id); + this.user = await this.owner.lookup('service:store').findRecord('user', instructor.id); + }); - const courseModel = await this.owner.lookup('service:store').findRecord('course', course.id); - const userModel = await this.owner.lookup('service:store').findRecord('user', instructor.id); - - this.set('course', courseModel); - this.set('instructor', userModel); - + test('it renders', async function (assert) { + this.set('course', this.linkedCourseWithTime); + this.set('instructor', this.user); await render( - hbs` + hbs` `, ); + assert.notOk(component.noData.isVisible); //let the chart animations finish await waitFor('.loaded'); await waitFor('svg .bars'); - assert.strictEqual(component.chart.bars.length, 2); + assert.strictEqual( + component.chart.bars[0].description, + 'Vocabulary 1 - Standalone - 630 Minutes', + ); + assert.strictEqual( + component.chart.bars[1].description, + 'Vocabulary 2 - Campaign - 180 Minutes', + ); assert.strictEqual(component.chart.labels.length, 2); - assert.strictEqual(component.chart.labels[0].text, 'Vocabulary 1 > Standalone: 630 Minutes'); - assert.strictEqual(component.chart.labels[1].text, 'Vocabulary 2 > Campaign: 180 Minutes'); + assert.strictEqual(component.chart.labels[0].text, 'Vocabulary 1 - Standalone'); + assert.strictEqual(component.chart.labels[1].text, 'Vocabulary 2 - Campaign'); + assert.strictEqual(component.dataTable.rows[0].vocabularyTerm, 'Vocabulary 2 - Campaign'); + assert.strictEqual(component.dataTable.rows[0].sessions.links.length, 1); + assert.strictEqual( + component.dataTable.rows[0].sessions.links[0].text, + 'The San Leandro Horror', + ); + assert.strictEqual(component.dataTable.rows[0].sessions.links[0].url, '/courses/1/sessions/2'); + assert.strictEqual(component.dataTable.rows[0].minutes, '180'); + assert.strictEqual(component.dataTable.rows[1].vocabularyTerm, 'Vocabulary 1 - Standalone'); + assert.strictEqual(component.dataTable.rows[1].sessions.links.length, 1); + assert.strictEqual( + component.dataTable.rows[1].sessions.links[0].text, + 'Berkeley Investigations', + ); + assert.strictEqual(component.dataTable.rows[1].sessions.links[0].url, '/courses/1/sessions/1'); + assert.strictEqual(component.dataTable.rows[1].minutes, '630'); + }); + + test('sort data-table by vocabulary term', async function (assert) { + this.set('course', this.linkedCourseWithTime); + this.set('instructor', this.user); + await render( + hbs` +`, + ); + assert.strictEqual(component.dataTable.rows[0].vocabularyTerm, 'Vocabulary 2 - Campaign'); + assert.strictEqual(component.dataTable.rows[1].vocabularyTerm, 'Vocabulary 1 - Standalone'); + await component.dataTable.header.vocabularyTerm.toggle(); + assert.strictEqual(component.dataTable.rows[0].vocabularyTerm, 'Vocabulary 1 - Standalone'); + assert.strictEqual(component.dataTable.rows[1].vocabularyTerm, 'Vocabulary 2 - Campaign'); + await component.dataTable.header.vocabularyTerm.toggle(); + assert.strictEqual(component.dataTable.rows[0].vocabularyTerm, 'Vocabulary 2 - Campaign'); + assert.strictEqual(component.dataTable.rows[1].vocabularyTerm, 'Vocabulary 1 - Standalone'); + }); + + test('sort data-table by sessions', async function (assert) { + this.set('course', this.linkedCourseWithTime); + this.set('instructor', this.user); + await render( + hbs` +`, + ); + assert.strictEqual(component.dataTable.rows[0].sessions.text, 'The San Leandro Horror'); + assert.strictEqual(component.dataTable.rows[1].sessions.text, 'Berkeley Investigations'); + await component.dataTable.header.sessions.toggle(); + assert.strictEqual(component.dataTable.rows[0].sessions.text, 'Berkeley Investigations'); + assert.strictEqual(component.dataTable.rows[1].sessions.text, 'The San Leandro Horror'); + await component.dataTable.header.sessions.toggle(); + assert.strictEqual(component.dataTable.rows[0].sessions.text, 'The San Leandro Horror'); + assert.strictEqual(component.dataTable.rows[1].sessions.text, 'Berkeley Investigations'); + await component.dataTable.header.sessions.toggle(); + assert.strictEqual(component.dataTable.rows[0].sessions.text, 'Berkeley Investigations'); + assert.strictEqual(component.dataTable.rows[1].sessions.text, 'The San Leandro Horror'); + }); + + test('sort data-table by minutes', async function (assert) { + this.set('course', this.linkedCourseWithTime); + this.set('instructor', this.user); + await render( + hbs` +`, + ); + assert.strictEqual(component.dataTable.rows[0].minutes, '180'); + assert.strictEqual(component.dataTable.rows[1].minutes, '630'); + await component.dataTable.header.minutes.toggle(); + assert.strictEqual(component.dataTable.rows[0].minutes, '630'); + assert.strictEqual(component.dataTable.rows[1].minutes, '180'); + await component.dataTable.header.minutes.toggle(); + assert.strictEqual(component.dataTable.rows[0].minutes, '180'); + assert.strictEqual(component.dataTable.rows[1].minutes, '630'); + }); + + test('no data', async function (assert) { + this.set('course', this.emptyCourse); + this.set('instructor', this.user); + await render( + hbs` +`, + ); + assert.notOk(component.chart.isVisible); + assert.notOk(component.dataTable.isVisible); + assert.strictEqual( + component.noData.text, + '0 guy M. Mc0son is not instructing any sessions in this this course.', + ); + }); + + test('only zero time data', async function (assert) { + this.set('course', this.linkedCourseWithoutTime); + this.set('instructor', this.user); + await render( + hbs` +`, + ); + assert.notOk(component.chart.isVisible); + assert.notOk(component.noData.isVisible); + assert.strictEqual(component.dataTable.rows.length, 1); + assert.strictEqual(component.dataTable.rows[0].vocabularyTerm, 'Vocabulary 2 - Prelude'); + assert.strictEqual(component.dataTable.rows[0].sessions.links.length, 1); + assert.strictEqual(component.dataTable.rows[0].sessions.links[0].text, 'Two Slices of Pizza'); + assert.strictEqual(component.dataTable.rows[0].sessions.links[0].url, '/courses/2/sessions/3'); + assert.strictEqual(component.dataTable.rows[0].minutes, '0'); }); }); diff --git a/packages/test-app/tests/integration/components/course/visualize-instructor-test.js b/packages/test-app/tests/integration/components/course/visualize-instructor-test.js index 75f46f0399..5d7d975ee7 100644 --- a/packages/test-app/tests/integration/components/course/visualize-instructor-test.js +++ b/packages/test-app/tests/integration/components/course/visualize-instructor-test.js @@ -139,23 +139,15 @@ module('Integration | Component | course/visualize-instructor', function (hooks) // wait for charts to load await waitFor('.loaded'); await waitFor('svg .bars'); - await waitFor('svg .chart'); + await waitFor('svg .slice'); assert.strictEqual(component.termsChart.chart.bars.length, 3); assert.strictEqual(component.termsChart.chart.labels.length, 3); - assert.strictEqual( - component.termsChart.chart.labels[0].text, - 'Vocabulary 1 > term 0: 60 Minutes', - ); - assert.strictEqual( - component.termsChart.chart.labels[1].text, - 'Vocabulary 1 > term 1: 30 Minutes', - ); - assert.strictEqual( - component.termsChart.chart.labels[2].text, - 'Vocabulary 2 > term 2: 30 Minutes', - ); + assert.strictEqual(component.termsChart.chart.labels[0].text, 'Vocabulary 1 - term 0'); + assert.strictEqual(component.termsChart.chart.labels[1].text, 'Vocabulary 1 - term 1'); + assert.strictEqual(component.termsChart.chart.labels[2].text, 'Vocabulary 2 - term 2'); assert.strictEqual(component.sessionTypesChart.chart.slices.length, 2); - assert.strictEqual(component.sessionTypesChart.chart.slices[0].text, 'session type 0 66.7%'); - assert.strictEqual(component.sessionTypesChart.chart.slices[1].text, 'session type 1 33.3%'); + assert.strictEqual(component.sessionTypesChart.chart.labels.length, 2); + assert.strictEqual(component.sessionTypesChart.chart.labels[0].text, 'session type 1'); + assert.strictEqual(component.sessionTypesChart.chart.labels[1].text, 'session type 0'); }); }); diff --git a/packages/test-app/tests/integration/components/course/visualize-instructors-graph-test.js b/packages/test-app/tests/integration/components/course/visualize-instructors-graph-test.js index 3cdf92565c..921503f83d 100644 --- a/packages/test-app/tests/integration/components/course/visualize-instructors-graph-test.js +++ b/packages/test-app/tests/integration/components/course/visualize-instructors-graph-test.js @@ -13,18 +13,22 @@ module('Integration | Component | course/visualize-instructors-graph', function const instructor1 = this.server.create('user', { displayName: 'Marie' }); const instructor2 = this.server.create('user', { displayName: 'Daisy' }); const instructor3 = this.server.create('user', { displayName: 'Duke' }); - const instructor4 = this.server.create('user', { - displayName: 'William', - }); + const instructor4 = this.server.create('user', { displayName: 'William' }); + const instructor5 = this.server.create('user', { displayName: 'Roland' }); + const linkedCourseWithTime = this.server.create('course'); + const linkedCourseWithoutTime = this.server.create('course'); - const course = this.server.create('course'); const session1 = this.server.create('session', { title: 'Berkeley Investigations', - course, + course: linkedCourseWithTime, }); const session2 = this.server.create('session', { title: 'The San Leandro Horror', - course, + course: linkedCourseWithTime, + }); + const session3 = this.server.create('session', { + title: 'Two Slices of Pizza', + course: linkedCourseWithoutTime, }); this.server.create('offering', { session: session1, @@ -44,59 +48,209 @@ module('Integration | Component | course/visualize-instructors-graph', function endDate: new Date('2019-12-05T21:00:00'), instructors: [instructor1, instructor2, instructor3, instructor4], }); - - this.courseModel = await this.owner.lookup('service:store').findRecord('course', course.id); + this.server.create('offering', { + session: session3, + startDate: new Date('2019-12-08T12:00:00'), + endDate: new Date('2019-12-08T12:00:00'), + instructors: [instructor5], + }); + this.emptyCourse = await this.owner + .lookup('service:store') + .findRecord('course', this.server.create('course').id); + this.linkedCourseWithTime = await this.owner + .lookup('service:store') + .findRecord('course', linkedCourseWithTime.id); + this.linkedCourseWithoutTime = await this.owner + .lookup('service:store') + .findRecord('course', linkedCourseWithoutTime.id); }); test('it renders', async function (assert) { - this.set('course', this.courseModel); - - await render(hbs` + this.set('course', this.linkedCourseWithTime); + await render(hbs` `); + assert.notOk(component.noData.isVisible); //let the chart animations finish await waitFor('.loaded'); await waitFor('svg .bars'); - assert.strictEqual(component.chart.bars.length, 4); + assert.strictEqual(component.chart.bars[0].description, 'Daisy - 180 Minutes'); + assert.strictEqual(component.chart.bars[1].description, 'Duke - 180 Minutes'); + assert.strictEqual(component.chart.bars[2].description, 'William - 510 Minutes'); + assert.strictEqual(component.chart.bars[3].description, 'Marie - 810 Minutes'); assert.strictEqual(component.chart.labels.length, 4); - assert.strictEqual(component.chart.labels[0].text, 'Daisy: 180 Minutes'); - assert.strictEqual(component.chart.labels[1].text, 'Duke: 180 Minutes'); - assert.strictEqual(component.chart.labels[2].text, 'William: 510 Minutes'); - assert.strictEqual(component.chart.labels[3].text, 'Marie: 810 Minutes'); + assert.strictEqual(component.chart.labels[0].text, 'Daisy'); + assert.strictEqual(component.chart.labels[1].text, 'Duke'); + assert.strictEqual(component.chart.labels[2].text, 'William'); + assert.strictEqual(component.chart.labels[3].text, 'Marie'); + assert.strictEqual(component.dataTable.rows.length, 4); + assert.strictEqual(component.dataTable.rows[0].instructor.text, 'Daisy'); + assert.strictEqual(component.dataTable.rows[0].instructor.url, '/data/courses/1/instructors/2'); + assert.strictEqual(component.dataTable.rows[0].sessions.links.length, 1); + assert.strictEqual( + component.dataTable.rows[0].sessions.links[0].text, + 'The San Leandro Horror', + ); + assert.strictEqual(component.dataTable.rows[0].sessions.links[0].url, '/courses/1/sessions/2'); + assert.strictEqual(component.dataTable.rows[0].minutes, '180'); + assert.strictEqual(component.dataTable.rows[1].instructor.text, 'Duke'); + assert.strictEqual(component.dataTable.rows[1].instructor.url, '/data/courses/1/instructors/3'); + assert.strictEqual(component.dataTable.rows[1].sessions.links.length, 1); + assert.strictEqual( + component.dataTable.rows[1].sessions.links[0].text, + 'The San Leandro Horror', + ); + assert.strictEqual(component.dataTable.rows[1].sessions.links[0].url, '/courses/1/sessions/2'); + assert.strictEqual(component.dataTable.rows[1].minutes, '180'); + assert.strictEqual(component.dataTable.rows[2].instructor.text, 'William'); + assert.strictEqual(component.dataTable.rows[2].instructor.url, '/data/courses/1/instructors/4'); + assert.strictEqual(component.dataTable.rows[2].sessions.links.length, 2); + assert.strictEqual( + component.dataTable.rows[2].sessions.links[0].text, + 'Berkeley Investigations', + ); + assert.strictEqual(component.dataTable.rows[2].sessions.links[0].url, '/courses/1/sessions/1'); + assert.strictEqual( + component.dataTable.rows[2].sessions.links[1].text, + 'The San Leandro Horror', + ); + assert.strictEqual(component.dataTable.rows[2].sessions.links[1].url, '/courses/1/sessions/2'); + assert.strictEqual(component.dataTable.rows[2].minutes, '510'); + assert.strictEqual(component.dataTable.rows[3].instructor.text, 'Marie'); + assert.strictEqual(component.dataTable.rows[3].instructor.url, '/data/courses/1/instructors/1'); + assert.strictEqual(component.dataTable.rows[3].sessions.links.length, 2); + assert.strictEqual( + component.dataTable.rows[3].sessions.links[0].text, + 'Berkeley Investigations', + ); + assert.strictEqual(component.dataTable.rows[3].sessions.links[0].url, '/courses/1/sessions/1'); + assert.strictEqual( + component.dataTable.rows[3].sessions.links[1].text, + 'The San Leandro Horror', + ); + assert.strictEqual(component.dataTable.rows[3].sessions.links[1].url, '/courses/1/sessions/2'); + assert.strictEqual(component.dataTable.rows[3].minutes, '810'); }); test('filter applies', async function (assert) { this.set('name', 'Marie'); - this.set('course', this.courseModel); - + this.set('course', this.linkedCourseWithTime); await render( - hbs` + hbs` `, ); //let the chart animations finish await waitFor('.loaded'); await waitFor('svg .bars'); - assert.strictEqual(component.chart.bars.length, 1); assert.strictEqual(component.chart.labels.length, 1); - assert.strictEqual(component.chart.labels[0].text, 'Marie: 810 Minutes'); + assert.strictEqual(component.chart.labels[0].text, 'Marie'); + assert.strictEqual(component.dataTable.rows.length, 1); + assert.strictEqual(component.dataTable.rows[0].instructor.text, 'Marie'); }); - test('it renders as donut chart', async function (assert) { - this.set('course', this.courseModel); + test('sort data-table by instructor', async function (assert) { + this.set('course', this.linkedCourseWithTime); + await render(hbs` +`); + assert.strictEqual(component.dataTable.rows[0].instructor.text, 'Daisy'); + assert.strictEqual(component.dataTable.rows[1].instructor.text, 'Duke'); + assert.strictEqual(component.dataTable.rows[2].instructor.text, 'William'); + assert.strictEqual(component.dataTable.rows[3].instructor.text, 'Marie'); + await component.dataTable.header.instructor.toggle(); + assert.strictEqual(component.dataTable.rows[0].instructor.text, 'Daisy'); + assert.strictEqual(component.dataTable.rows[1].instructor.text, 'Duke'); + assert.strictEqual(component.dataTable.rows[2].instructor.text, 'Marie'); + assert.strictEqual(component.dataTable.rows[3].instructor.text, 'William'); + await component.dataTable.header.instructor.toggle(); + assert.strictEqual(component.dataTable.rows[0].instructor.text, 'William'); + assert.strictEqual(component.dataTable.rows[1].instructor.text, 'Marie'); + assert.strictEqual(component.dataTable.rows[2].instructor.text, 'Duke'); + assert.strictEqual(component.dataTable.rows[3].instructor.text, 'Daisy'); + }); - await render( - hbs` -`, + test('sort data-table by sessions', async function (assert) { + this.set('course', this.linkedCourseWithTime); + await render(hbs` +`); + assert.strictEqual(component.dataTable.rows[0].sessions.text, 'The San Leandro Horror'); + assert.strictEqual(component.dataTable.rows[1].sessions.text, 'The San Leandro Horror'); + assert.strictEqual( + component.dataTable.rows[2].sessions.text, + 'Berkeley Investigations, The San Leandro Horror', ); - //let the chart animations finish - await waitFor('.loaded'); - await waitFor('svg .slice'); + assert.strictEqual( + component.dataTable.rows[3].sessions.text, + 'Berkeley Investigations, The San Leandro Horror', + ); + await component.dataTable.header.sessions.toggle(); + assert.strictEqual( + component.dataTable.rows[0].sessions.text, + 'Berkeley Investigations, The San Leandro Horror', + ); + assert.strictEqual( + component.dataTable.rows[1].sessions.text, + 'Berkeley Investigations, The San Leandro Horror', + ); + assert.strictEqual(component.dataTable.rows[2].sessions.text, 'The San Leandro Horror'); + assert.strictEqual(component.dataTable.rows[3].sessions.text, 'The San Leandro Horror'); + await component.dataTable.header.sessions.toggle(); + assert.strictEqual(component.dataTable.rows[0].sessions.text, 'The San Leandro Horror'); + assert.strictEqual(component.dataTable.rows[1].sessions.text, 'The San Leandro Horror'); + assert.strictEqual( + component.dataTable.rows[2].sessions.text, + 'Berkeley Investigations, The San Leandro Horror', + ); + assert.strictEqual( + component.dataTable.rows[3].sessions.text, + 'Berkeley Investigations, The San Leandro Horror', + ); + }); - assert.strictEqual(component.chart.slices.length, 4); - assert.strictEqual(component.chart.slices[0].text, 'Daisy: 180 Minutes'); - assert.strictEqual(component.chart.slices[1].text, 'Duke: 180 Minutes'); - assert.strictEqual(component.chart.slices[2].text, 'William: 510 Minutes'); - assert.strictEqual(component.chart.slices[3].text, 'Marie: 810 Minutes'); + test('sort data-table by minutes', async function (assert) { + this.set('course', this.linkedCourseWithTime); + await render(hbs` +`); + assert.strictEqual(component.dataTable.rows[0].minutes, '180'); + assert.strictEqual(component.dataTable.rows[1].minutes, '180'); + assert.strictEqual(component.dataTable.rows[2].minutes, '510'); + assert.strictEqual(component.dataTable.rows[3].minutes, '810'); + await component.dataTable.header.minutes.toggle(); + assert.strictEqual(component.dataTable.rows[0].minutes, '810'); + assert.strictEqual(component.dataTable.rows[1].minutes, '510'); + assert.strictEqual(component.dataTable.rows[2].minutes, '180'); + assert.strictEqual(component.dataTable.rows[3].minutes, '180'); + await component.dataTable.header.minutes.toggle(); + assert.strictEqual(component.dataTable.rows[0].minutes, '180'); + assert.strictEqual(component.dataTable.rows[1].minutes, '180'); + assert.strictEqual(component.dataTable.rows[2].minutes, '510'); + assert.strictEqual(component.dataTable.rows[3].minutes, '810'); + }); + + test('no data', async function (assert) { + this.set('course', this.emptyCourse); + await render(hbs` +`); + assert.notOk(component.chart.isVisible); + assert.notOk(component.dataTable.isVisible); + assert.strictEqual( + component.noData.text, + 'No instructors have been linked to any sessions in this course.', + ); + }); + + test('only zero time data', async function (assert) { + this.set('course', this.linkedCourseWithoutTime); + await render(hbs` +`); + assert.notOk(component.chart.isVisible); + assert.notOk(component.noData.isVisible); + assert.strictEqual(component.dataTable.rows.length, 1); + assert.strictEqual(component.dataTable.rows[0].instructor.text, 'Roland'); + assert.strictEqual(component.dataTable.rows[0].instructor.url, '/data/courses/2/instructors/5'); + assert.strictEqual(component.dataTable.rows[0].sessions.links.length, 1); + assert.strictEqual(component.dataTable.rows[0].sessions.links[0].text, 'Two Slices of Pizza'); + assert.strictEqual(component.dataTable.rows[0].sessions.links[0].url, '/courses/2/sessions/3'); + assert.strictEqual(component.dataTable.rows[0].minutes, '0'); }); }); diff --git a/packages/test-app/tests/integration/components/course/visualize-instructors-test.js b/packages/test-app/tests/integration/components/course/visualize-instructors-test.js index e6467c6fe3..fd05c73851 100644 --- a/packages/test-app/tests/integration/components/course/visualize-instructors-test.js +++ b/packages/test-app/tests/integration/components/course/visualize-instructors-test.js @@ -55,10 +55,10 @@ module('Integration | Component | course/visualize-instructors', function (hooks await waitFor('svg .bars'); assert.strictEqual(component.instructorsChart.chart.bars.length, 4); assert.strictEqual(component.instructorsChart.chart.labels.length, 4); - assert.strictEqual(component.instructorsChart.chart.labels[0].text, 'Daisy: 180 Minutes'); - assert.strictEqual(component.instructorsChart.chart.labels[1].text, 'Duke: 180 Minutes'); - assert.strictEqual(component.instructorsChart.chart.labels[2].text, 'William: 510 Minutes'); - assert.strictEqual(component.instructorsChart.chart.labels[3].text, 'Marie: 810 Minutes'); + assert.strictEqual(component.instructorsChart.chart.labels[0].text, 'Daisy'); + assert.strictEqual(component.instructorsChart.chart.labels[1].text, 'Duke'); + assert.strictEqual(component.instructorsChart.chart.labels[2].text, 'William'); + assert.strictEqual(component.instructorsChart.chart.labels[3].text, 'Marie'); }); test('filter works', async function (assert) { @@ -85,21 +85,12 @@ module('Integration | Component | course/visualize-instructors', function (hooks assert.strictEqual(component.title, 'course 0 2021'); assert.strictEqual(component.instructorsChart.chart.bars.length, 2); assert.strictEqual(component.instructorsChart.chart.labels.length, 2); - assert.strictEqual( - component.instructorsChart.chart.labels[0].text, - 'foo M. Mc0son: 1440 Minutes', - ); - assert.strictEqual( - component.instructorsChart.chart.labels[1].text, - 'bar M. Mc1son: 1440 Minutes', - ); + assert.strictEqual(component.instructorsChart.chart.labels[0].text, 'foo M. Mc0son'); + assert.strictEqual(component.instructorsChart.chart.labels[1].text, 'bar M. Mc1son'); await component.filter.set('foo'); assert.strictEqual(component.instructorsChart.chart.bars.length, 1); assert.strictEqual(component.instructorsChart.chart.labels.length, 1); - assert.strictEqual( - component.instructorsChart.chart.labels[0].text, - 'foo M. Mc0son: 1440 Minutes', - ); + assert.strictEqual(component.instructorsChart.chart.labels[0].text, 'foo M. Mc0son'); }); test('course year is shown as range if applicable by configuration', async function (assert) { diff --git a/packages/test-app/tests/integration/components/course/visualize-objectives-graph-test.js b/packages/test-app/tests/integration/components/course/visualize-objectives-graph-test.js index d9b3215479..19613e80f5 100644 --- a/packages/test-app/tests/integration/components/course/visualize-objectives-graph-test.js +++ b/packages/test-app/tests/integration/components/course/visualize-objectives-graph-test.js @@ -91,8 +91,16 @@ module('Integration | Component | course/visualize-objectives-graph', function ( await waitFor('svg .slice'); assert.strictEqual(component.chart.slices.length, 2); - assert.strictEqual(component.chart.slices[0].text, '77.8%'); - assert.strictEqual(component.chart.slices[1].text, '22.2%'); + assert.strictEqual(component.chart.slices[0].label, '77.8%'); + assert.strictEqual( + component.chart.slices[0].description, + 'course objective 0 (competency 0, competency 1) - 630 Minutes', + ); + assert.strictEqual(component.chart.slices[1].label, '22.2%'); + assert.strictEqual( + component.chart.slices[1].description, + 'course objective 1 (competency 1) - 180 Minutes', + ); assert.notOk(component.unlinkedObjectives.isPresent); assert.strictEqual(component.untaughtObjectives.items.length, 1); assert.strictEqual(component.untaughtObjectives.items[0].text, 'course objective 2'); diff --git a/packages/test-app/tests/integration/components/course/visualize-objectives-test.js b/packages/test-app/tests/integration/components/course/visualize-objectives-test.js index 601fb35717..9f2cde4fe6 100644 --- a/packages/test-app/tests/integration/components/course/visualize-objectives-test.js +++ b/packages/test-app/tests/integration/components/course/visualize-objectives-test.js @@ -112,8 +112,16 @@ module('Integration | Component | course/visualize-objectives', function (hooks) await waitFor('svg .slice'); assert.strictEqual(component.objectivesChart.chart.slices.length, 2); - assert.strictEqual(component.objectivesChart.chart.slices[0].text, '77.8%'); - assert.strictEqual(component.objectivesChart.chart.slices[1].text, '22.2%'); + assert.strictEqual(component.objectivesChart.chart.slices[0].label, '77.8%'); + assert.strictEqual( + component.objectivesChart.chart.slices[0].description, + 'course objective 0 - 630 Minutes', + ); + assert.strictEqual(component.objectivesChart.chart.slices[1].label, '22.2%'); + assert.strictEqual( + component.objectivesChart.chart.slices[1].description, + 'course objective 1 - 180 Minutes', + ); assert.notOk(component.objectivesChart.unlinkedObjectives.isPresent); assert.strictEqual(component.objectivesChart.untaughtObjectives.items.length, 1); assert.strictEqual( diff --git a/packages/test-app/tests/integration/components/course/visualize-session-type-graph-test.js b/packages/test-app/tests/integration/components/course/visualize-session-type-graph-test.js index c75b6ecc47..b9ba86776b 100644 --- a/packages/test-app/tests/integration/components/course/visualize-session-type-graph-test.js +++ b/packages/test-app/tests/integration/components/course/visualize-session-type-graph-test.js @@ -9,7 +9,7 @@ module('Integration | Component | course/visualize-session-type-graph', function setupRenderingTest(hooks); setupMirage(hooks); - test('it renders', async function (assert) { + hooks.beforeEach(async function () { const vocabulary1 = this.server.create('vocabulary'); const vocabulary2 = this.server.create('vocabulary'); const term1 = this.server.create('term', { @@ -20,19 +20,36 @@ module('Integration | Component | course/visualize-session-type-graph', function vocabulary: vocabulary2, title: 'Campaign', }); + const term3 = this.server.create('term', { + vocabulary: vocabulary2, + title: 'Prelude', + }); const sessionType = this.server.create('session-type'); - const course = this.server.create('course'); + const linkedCourseWithTime = this.server.create('course'); + const linkedCourseWithoutTime = this.server.create('course'); const session1 = this.server.create('session', { title: 'Berkeley Investigations', - course, + course: linkedCourseWithTime, terms: [term1], - sessionType: sessionType, + sessionType, }); const session2 = this.server.create('session', { title: 'The San Leandro Horror', - course, + course: linkedCourseWithTime, + terms: [term2], + sessionType, + }); + this.server.create('session', { + title: 'Two Slices of Pizza', + course: linkedCourseWithTime, + terms: [term3], + sessionType, + }); + this.server.create('session', { + title: 'Peanut Butter Stout', + course: linkedCourseWithoutTime, terms: [term2], - sessionType: sessionType, + sessionType, }); this.server.create('offering', { session: session1, @@ -49,26 +66,163 @@ module('Integration | Component | course/visualize-session-type-graph', function startDate: new Date('2019-12-05T18:00:00'), endDate: new Date('2019-12-05T21:00:00'), }); - - const courseModel = await this.owner.lookup('service:store').findRecord('course', course.id); - const sessionTypeModel = await this.owner + this.emptyCourse = await this.owner + .lookup('service:store') + .findRecord('course', this.server.create('course').id); + this.linkedCourseWithoutTime = await this.owner + .lookup('service:store') + .findRecord('course', linkedCourseWithoutTime.id); + this.linkedCourseWithTime = await this.owner + .lookup('service:store') + .findRecord('course', linkedCourseWithTime.id); + this.sessionType = await this.owner .lookup('service:store') .findRecord('session-type', sessionType.id); + }); - this.set('course', courseModel); - this.set('type', sessionTypeModel); - + test('it renders', async function (assert) { + this.set('course', this.linkedCourseWithTime); + this.set('type', this.sessionType); await render( - hbs` + hbs` `, ); //let the chart animations finish + assert.notOk(component.noData.isVisible); await waitFor('.loaded'); await waitFor('svg .bars'); - assert.strictEqual(component.chart.bars.length, 2); + assert.strictEqual( + component.chart.bars[0].description, + 'Vocabulary 1 - Standalone - 630 Minutes', + ); + assert.strictEqual( + component.chart.bars[1].description, + 'Vocabulary 2 - Campaign - 180 Minutes', + ); assert.strictEqual(component.chart.labels.length, 2); - assert.strictEqual(component.chart.labels[0].text, 'Vocabulary 1 - Standalone: 630 Minutes'); - assert.strictEqual(component.chart.labels[1].text, 'Vocabulary 2 - Campaign: 180 Minutes'); + assert.strictEqual(component.chart.labels[0].text, 'Vocabulary 1 - Standalone'); + assert.strictEqual(component.chart.labels[1].text, 'Vocabulary 2 - Campaign'); + assert.strictEqual(component.dataTable.rows.length, 3); + assert.strictEqual(component.dataTable.rows[0].vocabularyTerm, 'Vocabulary 1 - Standalone'); + assert.strictEqual(component.dataTable.rows[0].sessions.links.length, 1); + assert.strictEqual( + component.dataTable.rows[0].sessions.links[0].text, + 'Berkeley Investigations', + ); + assert.strictEqual(component.dataTable.rows[0].sessions.links[0].url, '/courses/1/sessions/1'); + assert.strictEqual(component.dataTable.rows[0].minutes, '630'); + assert.strictEqual(component.dataTable.rows[1].vocabularyTerm, 'Vocabulary 2 - Campaign'); + assert.strictEqual(component.dataTable.rows[1].sessions.links.length, 1); + assert.strictEqual( + component.dataTable.rows[1].sessions.links[0].text, + 'The San Leandro Horror', + ); + assert.strictEqual(component.dataTable.rows[1].sessions.links[0].url, '/courses/1/sessions/2'); + assert.strictEqual(component.dataTable.rows[1].minutes, '180'); + assert.strictEqual(component.dataTable.rows[2].sessions.links.length, 1); + assert.strictEqual(component.dataTable.rows[2].sessions.links[0].text, 'Two Slices of Pizza'); + assert.strictEqual(component.dataTable.rows[2].sessions.links[0].url, '/courses/1/sessions/3'); + assert.strictEqual(component.dataTable.rows[2].minutes, '0'); + }); + + test('sort data-table by vocabulary term', async function (assert) { + this.set('course', this.linkedCourseWithTime); + this.set('type', this.sessionType); + await render( + hbs` +`, + ); + assert.strictEqual(component.dataTable.rows[0].vocabularyTerm, 'Vocabulary 1 - Standalone'); + assert.strictEqual(component.dataTable.rows[1].vocabularyTerm, 'Vocabulary 2 - Campaign'); + assert.strictEqual(component.dataTable.rows[2].vocabularyTerm, 'Vocabulary 2 - Prelude'); + await component.dataTable.header.vocabularyTerm.toggle(); + assert.strictEqual(component.dataTable.rows[0].vocabularyTerm, 'Vocabulary 2 - Prelude'); + assert.strictEqual(component.dataTable.rows[1].vocabularyTerm, 'Vocabulary 2 - Campaign'); + assert.strictEqual(component.dataTable.rows[2].vocabularyTerm, 'Vocabulary 1 - Standalone'); + await component.dataTable.header.vocabularyTerm.toggle(); + assert.strictEqual(component.dataTable.rows[0].vocabularyTerm, 'Vocabulary 1 - Standalone'); + assert.strictEqual(component.dataTable.rows[1].vocabularyTerm, 'Vocabulary 2 - Campaign'); + assert.strictEqual(component.dataTable.rows[2].vocabularyTerm, 'Vocabulary 2 - Prelude'); + }); + + test('sort data-table by sessions', async function (assert) { + this.set('course', this.linkedCourseWithTime); + this.set('type', this.sessionType); + await render( + hbs` +`, + ); + assert.strictEqual(component.dataTable.rows[0].sessions.text, 'Berkeley Investigations'); + assert.strictEqual(component.dataTable.rows[1].sessions.text, 'The San Leandro Horror'); + assert.strictEqual(component.dataTable.rows[2].sessions.text, 'Two Slices of Pizza'); + await component.dataTable.header.sessions.toggle(); + assert.strictEqual(component.dataTable.rows[0].sessions.text, 'Berkeley Investigations'); + assert.strictEqual(component.dataTable.rows[1].sessions.text, 'The San Leandro Horror'); + assert.strictEqual(component.dataTable.rows[2].sessions.text, 'Two Slices of Pizza'); + await component.dataTable.header.sessions.toggle(); + assert.strictEqual(component.dataTable.rows[0].sessions.text, 'Two Slices of Pizza'); + assert.strictEqual(component.dataTable.rows[1].sessions.text, 'The San Leandro Horror'); + assert.strictEqual(component.dataTable.rows[2].sessions.text, 'Berkeley Investigations'); + await component.dataTable.header.sessions.toggle(); + assert.strictEqual(component.dataTable.rows[0].sessions.text, 'Berkeley Investigations'); + assert.strictEqual(component.dataTable.rows[1].sessions.text, 'The San Leandro Horror'); + assert.strictEqual(component.dataTable.rows[2].sessions.text, 'Two Slices of Pizza'); + await component.dataTable.header.sessions.toggle(); + assert.strictEqual(component.dataTable.rows[0].sessions.text, 'Two Slices of Pizza'); + assert.strictEqual(component.dataTable.rows[1].sessions.text, 'The San Leandro Horror'); + assert.strictEqual(component.dataTable.rows[2].sessions.text, 'Berkeley Investigations'); + }); + + test('sort data-table by minutes', async function (assert) { + this.set('course', this.linkedCourseWithTime); + this.set('type', this.sessionType); + await render( + hbs` +`, + ); + assert.strictEqual(component.dataTable.rows[0].minutes, '630'); + assert.strictEqual(component.dataTable.rows[1].minutes, '180'); + assert.strictEqual(component.dataTable.rows[2].minutes, '0'); + await component.dataTable.header.minutes.toggle(); + assert.strictEqual(component.dataTable.rows[0].minutes, '0'); + assert.strictEqual(component.dataTable.rows[1].minutes, '180'); + assert.strictEqual(component.dataTable.rows[2].minutes, '630'); + await component.dataTable.header.minutes.toggle(); + assert.strictEqual(component.dataTable.rows[0].minutes, '630'); + assert.strictEqual(component.dataTable.rows[1].minutes, '180'); + assert.strictEqual(component.dataTable.rows[2].minutes, '0'); + }); + + test('no data', async function (assert) { + this.set('course', this.emptyCourse); + this.set('type', this.sessionType); + await render( + hbs` +`, + ); + assert.notOk(component.chart.isVisible); + assert.notOk(component.dataTable.isVisible); + assert.strictEqual( + component.noData.text, + 'No vocabulary terms have been linked to any session type 0 sessions in this course.', + ); + }); + + test('only zero time data', async function (assert) { + this.set('course', this.linkedCourseWithoutTime); + this.set('type', this.sessionType); + await render( + hbs` +`, + ); + assert.notOk(component.chart.isVisible); + assert.notOk(component.noData.isVisible); + assert.strictEqual(component.dataTable.rows.length, 1); + assert.strictEqual(component.dataTable.rows[0].vocabularyTerm, 'Vocabulary 2 - Campaign'); + assert.strictEqual(component.dataTable.rows[0].sessions.links.length, 1); + assert.strictEqual(component.dataTable.rows[0].sessions.links[0].text, 'Peanut Butter Stout'); + assert.strictEqual(component.dataTable.rows[0].sessions.links[0].url, '/courses/2/sessions/4'); + assert.strictEqual(component.dataTable.rows[0].minutes, '0'); }); }); diff --git a/packages/test-app/tests/integration/components/course/visualize-session-types-graph-test.js b/packages/test-app/tests/integration/components/course/visualize-session-types-graph-test.js index 2766862fb5..38f2f94a2e 100644 --- a/packages/test-app/tests/integration/components/course/visualize-session-types-graph-test.js +++ b/packages/test-app/tests/integration/components/course/visualize-session-types-graph-test.js @@ -16,17 +16,31 @@ module('Integration | Component | course/visualize-session-types-graph', functio const sessionType2 = this.server.create('session-type', { title: 'Campaign', }); - const course = this.server.create('course'); + const sessionType3 = this.server.create('session-type', { + title: 'Prelude', + }); + const linkedCourseWithTime = this.server.create('course'); + const linkedCourseWithoutTime = this.server.create('course'); const session1 = this.server.create('session', { title: 'Berkeley Investigations', - course, + course: linkedCourseWithTime, sessionType: sessionType1, }); const session2 = this.server.create('session', { title: 'The San Leandro Horror', - course, + course: linkedCourseWithTime, sessionType: sessionType2, }); + this.server.create('session', { + title: 'Two Slices of Pizza', + course: linkedCourseWithTime, + sessionType: sessionType3, + }); + this.server.create('session', { + title: 'Peanut Butter Stout', + course: linkedCourseWithoutTime, + sessionType: sessionType3, + }); this.server.create('offering', { session: session1, startDate: new Date('2019-12-08T12:00:00'), @@ -42,55 +56,173 @@ module('Integration | Component | course/visualize-session-types-graph', functio startDate: new Date('2019-12-05T18:00:00'), endDate: new Date('2019-12-05T21:00:00'), }); - - this.courseModel = await this.owner.lookup('service:store').findRecord('course', course.id); + this.emptyCourse = await this.owner + .lookup('service:store') + .findRecord('course', this.server.create('course').id); + this.linkedCourseWithoutTime = await this.owner + .lookup('service:store') + .findRecord('course', linkedCourseWithoutTime.id); + this.linkedCourseWithTime = await this.owner + .lookup('service:store') + .findRecord('course', linkedCourseWithTime.id); }); - test('it renders as bar chart by default', async function (assert) { - this.set('course', this.courseModel); - - await render(hbs` + test('it renders', async function (assert) { + this.set('course', this.linkedCourseWithTime); + await render(hbs` `); + assert.notOk(component.noData.isVisible); //let the chart animations finish await waitFor('.loaded'); await waitFor('svg .bars'); - assert.strictEqual(component.chart.bars.length, 2); + assert.strictEqual(component.chart.bars[0].description, 'Campaign - 180 Minutes'); + assert.strictEqual(component.chart.bars[1].description, 'Standalone - 630 Minutes'); assert.strictEqual(component.chart.labels.length, 2); - assert.strictEqual(component.chart.labels[0].text, 'Campaign: 180 Minutes'); - assert.strictEqual(component.chart.labels[1].text, 'Standalone: 630 Minutes'); + assert.strictEqual(component.chart.labels[0].text, 'Campaign'); + assert.strictEqual(component.chart.labels[1].text, 'Standalone'); + assert.strictEqual(component.dataTable.rows.length, 3); + assert.strictEqual(component.dataTable.rows[0].sessionType.text, 'Prelude'); + assert.strictEqual( + component.dataTable.rows[0].sessionType.url, + '/data/courses/1/session-types/3', + ); + assert.strictEqual(component.dataTable.rows[0].sessions.links.length, 1); + assert.strictEqual(component.dataTable.rows[0].sessions.links[0].text, 'Two Slices of Pizza'); + assert.strictEqual(component.dataTable.rows[0].sessions.links[0].url, '/courses/1/sessions/3'); + assert.strictEqual(component.dataTable.rows[0].minutes, '0'); + assert.strictEqual(component.dataTable.rows[1].sessionType.text, 'Campaign'); + assert.strictEqual( + component.dataTable.rows[1].sessionType.url, + '/data/courses/1/session-types/2', + ); + assert.strictEqual(component.dataTable.rows[1].sessions.links.length, 1); + assert.strictEqual( + component.dataTable.rows[1].sessions.links[0].text, + 'The San Leandro Horror', + ); + assert.strictEqual(component.dataTable.rows[1].sessions.links[0].url, '/courses/1/sessions/2'); + assert.strictEqual(component.dataTable.rows[1].minutes, '180'); + assert.strictEqual(component.dataTable.rows[2].sessionType.text, 'Standalone'); + assert.strictEqual( + component.dataTable.rows[2].sessionType.url, + '/data/courses/1/session-types/1', + ); + assert.strictEqual(component.dataTable.rows[2].sessions.links.length, 1); + assert.strictEqual( + component.dataTable.rows[2].sessions.links[0].text, + 'Berkeley Investigations', + ); + assert.strictEqual(component.dataTable.rows[2].sessions.links[0].url, '/courses/1/sessions/1'); + assert.strictEqual(component.dataTable.rows[2].minutes, '630'); }); - test('it renders as donut chart', async function (assert) { - this.set('course', this.courseModel); - + test('filter applies', async function (assert) { + this.set('title', 'Campaign'); + this.set('course', this.linkedCourseWithTime); await render( - hbs` + hbs` `, ); + assert.notOk(component.noData.isVisible); //let the chart animations finish await waitFor('.loaded'); - await waitFor('svg .slice'); + await waitFor('svg .bars'); + assert.strictEqual(component.chart.bars.length, 1); + assert.strictEqual(component.chart.labels.length, 1); + assert.strictEqual(component.chart.labels[0].text, 'Campaign'); + assert.strictEqual(component.dataTable.rows.length, 1); + assert.strictEqual(component.dataTable.rows[0].sessionType.text, 'Campaign'); + }); + + test('filter out all data', async function (assert) { + this.set('title', 'Geflarknik'); + this.set('course', this.linkedCourseWithTime); + await render( + hbs` +`, + ); + assert.notOk(component.chart.isVisible); + assert.notOk(component.noData.isVisible); + assert.ok(component.dataTable.isVisible); + assert.strictEqual(component.dataTable.rows.length, 0); + }); - assert.strictEqual(component.chart.slices.length, 2); - assert.strictEqual(component.chart.slices[0].text, 'Campaign: 180 Minutes'); - assert.strictEqual(component.chart.slices[1].text, 'Standalone: 630 Minutes'); + test('sort data-table by session type', async function (assert) { + this.set('course', this.linkedCourseWithTime); + await render(hbs` +`); + assert.strictEqual(component.dataTable.rows[0].sessionType.text, 'Prelude'); + assert.strictEqual(component.dataTable.rows[1].sessionType.text, 'Campaign'); + assert.strictEqual(component.dataTable.rows[2].sessionType.text, 'Standalone'); + await component.dataTable.header.sessionType.toggle(); + assert.strictEqual(component.dataTable.rows[0].sessionType.text, 'Campaign'); + assert.strictEqual(component.dataTable.rows[1].sessionType.text, 'Prelude'); + assert.strictEqual(component.dataTable.rows[2].sessionType.text, 'Standalone'); + await component.dataTable.header.sessionType.toggle(); + assert.strictEqual(component.dataTable.rows[0].sessionType.text, 'Standalone'); + assert.strictEqual(component.dataTable.rows[1].sessionType.text, 'Prelude'); + assert.strictEqual(component.dataTable.rows[2].sessionType.text, 'Campaign'); }); - test('filter applies', async function (assert) { - this.set('title', 'Campaign'); - this.set('course', this.courseModel); + test('sort data-table by sessions', async function (assert) { + this.set('course', this.linkedCourseWithTime); + await render(hbs` +`); + assert.strictEqual(component.dataTable.rows[0].sessions.text, 'Two Slices of Pizza'); + assert.strictEqual(component.dataTable.rows[1].sessions.text, 'The San Leandro Horror'); + assert.strictEqual(component.dataTable.rows[2].sessions.text, 'Berkeley Investigations'); + await component.dataTable.header.sessions.toggle(); + assert.strictEqual(component.dataTable.rows[0].sessions.text, 'Berkeley Investigations'); + assert.strictEqual(component.dataTable.rows[1].sessions.text, 'The San Leandro Horror'); + assert.strictEqual(component.dataTable.rows[2].sessions.text, 'Two Slices of Pizza'); + await component.dataTable.header.sessions.toggle(); + assert.strictEqual(component.dataTable.rows[0].sessions.text, 'Two Slices of Pizza'); + assert.strictEqual(component.dataTable.rows[1].sessions.text, 'The San Leandro Horror'); + assert.strictEqual(component.dataTable.rows[2].sessions.text, 'Berkeley Investigations'); + }); + + test('sort data-table by minutes', async function (assert) { + this.set('course', this.linkedCourseWithTime); + await render(hbs` +`); + assert.strictEqual(component.dataTable.rows[0].minutes, '0'); + assert.strictEqual(component.dataTable.rows[1].minutes, '180'); + assert.strictEqual(component.dataTable.rows[2].minutes, '630'); + await component.dataTable.header.minutes.toggle(); + assert.strictEqual(component.dataTable.rows[0].minutes, '630'); + assert.strictEqual(component.dataTable.rows[1].minutes, '180'); + assert.strictEqual(component.dataTable.rows[2].minutes, '0'); + await component.dataTable.header.minutes.toggle(); + assert.strictEqual(component.dataTable.rows[0].minutes, '0'); + assert.strictEqual(component.dataTable.rows[1].minutes, '180'); + assert.strictEqual(component.dataTable.rows[2].minutes, '630'); + }); + test('no data', async function (assert) { + this.set('course', this.emptyCourse); await render( - hbs` + hbs` `, ); - //let the chart animations finish - await waitFor('.loaded'); - await waitFor('svg .bars'); + assert.notOk(component.chart.isVisible); + assert.notOk(component.dataTable.isVisible); + assert.strictEqual(component.noData.text, 'This course has no sessions.'); + }); - assert.strictEqual(component.chart.bars.length, 1); - assert.strictEqual(component.chart.labels.length, 1); - assert.strictEqual(component.chart.labels[0].text, 'Campaign: 180 Minutes'); + test('only zero time data', async function (assert) { + this.set('course', this.linkedCourseWithoutTime); + await render( + hbs` +`, + ); + assert.notOk(component.chart.isVisible); + assert.notOk(component.noData.isVisible); + assert.strictEqual(component.dataTable.rows.length, 1); + assert.strictEqual(component.dataTable.rows[0].sessionType.text, 'Prelude'); + assert.strictEqual(component.dataTable.rows[0].sessions.links.length, 1); + assert.strictEqual(component.dataTable.rows[0].sessions.links[0].text, 'Peanut Butter Stout'); + assert.strictEqual(component.dataTable.rows[0].sessions.links[0].url, '/courses/2/sessions/4'); + assert.strictEqual(component.dataTable.rows[0].minutes, '0'); }); }); diff --git a/packages/test-app/tests/integration/components/course/visualize-session-types-test.js b/packages/test-app/tests/integration/components/course/visualize-session-types-test.js index 7f7bed2469..aad3534af2 100644 --- a/packages/test-app/tests/integration/components/course/visualize-session-types-test.js +++ b/packages/test-app/tests/integration/components/course/visualize-session-types-test.js @@ -45,11 +45,11 @@ module('Integration | Component | course/visualize-session-types', function (hoo startDate: new Date('2019-12-05T18:00:00'), endDate: new Date('2019-12-05T21:00:00'), }); - this.courseModel = await this.owner.lookup('service:store').findRecord('course', course.id); + this.course = await this.owner.lookup('service:store').findRecord('course', course.id); }); test('it renders', async function (assert) { - this.set('course', this.courseModel); + this.set('course', this.course); await render(hbs` `); assert.strictEqual(component.title, 'course 0 2021'); @@ -58,8 +58,8 @@ module('Integration | Component | course/visualize-session-types', function (hoo await waitFor('svg .bars'); assert.strictEqual(component.sessionTypesChart.chart.bars.length, 2); assert.strictEqual(component.sessionTypesChart.chart.labels.length, 2); - assert.strictEqual(component.sessionTypesChart.chart.labels[0].text, 'Campaign: 180 Minutes'); - assert.strictEqual(component.sessionTypesChart.chart.labels[1].text, 'Standalone: 630 Minutes'); + assert.strictEqual(component.sessionTypesChart.chart.labels[0].text, 'Campaign'); + assert.strictEqual(component.sessionTypesChart.chart.labels[1].text, 'Standalone'); }); test('course year is shown as range if applicable by configuration', async function (assert) { @@ -70,14 +70,14 @@ module('Integration | Component | course/visualize-session-types', function (hoo }, }; }); - this.set('course', this.courseModel); + this.set('course', this.course); await render(hbs` `); assert.strictEqual(component.title, 'course 0 2021 - 2022'); }); test('filter works', async function (assert) { - this.set('course', this.courseModel); + this.set('course', this.course); await render(hbs` `); //let the chart animations finish @@ -86,16 +86,16 @@ module('Integration | Component | course/visualize-session-types', function (hoo assert.strictEqual(component.title, 'course 0 2021'); assert.strictEqual(component.sessionTypesChart.chart.bars.length, 2); assert.strictEqual(component.sessionTypesChart.chart.labels.length, 2); - assert.strictEqual(component.sessionTypesChart.chart.labels[0].text, 'Campaign: 180 Minutes'); - assert.strictEqual(component.sessionTypesChart.chart.labels[1].text, 'Standalone: 630 Minutes'); + assert.strictEqual(component.sessionTypesChart.chart.labels[0].text, 'Campaign'); + assert.strictEqual(component.sessionTypesChart.chart.labels[1].text, 'Standalone'); await component.filter.set('Campaign'); assert.strictEqual(component.sessionTypesChart.chart.bars.length, 1); assert.strictEqual(component.sessionTypesChart.chart.labels.length, 1); - assert.strictEqual(component.sessionTypesChart.chart.labels[0].text, 'Campaign: 180 Minutes'); + assert.strictEqual(component.sessionTypesChart.chart.labels[0].text, 'Campaign'); }); test('breadcrumb', async function (assert) { - this.set('course', this.courseModel); + this.set('course', this.course); await render(hbs` `); diff --git a/packages/test-app/tests/integration/components/course/visualize-term-graph-test.js b/packages/test-app/tests/integration/components/course/visualize-term-graph-test.js index 0ee602e55a..9cdf6f9aa7 100644 --- a/packages/test-app/tests/integration/components/course/visualize-term-graph-test.js +++ b/packages/test-app/tests/integration/components/course/visualize-term-graph-test.js @@ -1,35 +1,46 @@ import { module, test } from 'qunit'; import { setupRenderingTest } from 'test-app/tests/helpers'; -import { render, findAll, waitFor } from '@ember/test-helpers'; +import { render, waitFor } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { setupMirage } from 'test-app/tests/test-support/mirage'; +import { component } from 'ilios-common/page-objects/components/course/visualize-term-graph'; module('Integration | Component | course/visualize-term-graph', function (hooks) { setupRenderingTest(hooks); setupMirage(hooks); - test('it renders', async function (assert) { + hooks.beforeEach(async function () { const vocabulary = this.server.create('vocabulary'); const term = this.server.create('term', { vocabulary }); - const course = this.server.create('course'); + const linkedCourseWithTime = this.server.create('course'); + const linkedCourseWithoutTime = this.server.create('course'); const sessionType1 = this.server.create('session-type', { title: 'Standalone', }); const sessionType2 = this.server.create('session-type', { title: 'Campaign', }); + const sessionType3 = this.server.create('session-type', { + title: 'Prelude', + }); const session1 = this.server.create('session', { title: 'Berkeley Investigations', - course, + course: linkedCourseWithTime, terms: [term], sessionType: sessionType1, }); const session2 = this.server.create('session', { title: 'The San Leandro Horror', - course, + course: linkedCourseWithTime, terms: [term], sessionType: sessionType2, }); + this.server.create('session', { + title: 'Two Slices of Pizza', + course: linkedCourseWithoutTime, + terms: [term], + sessionType: sessionType3, + }); this.server.create('offering', { session: session1, startDate: new Date('2019-12-08T12:00:00'), @@ -45,24 +56,137 @@ module('Integration | Component | course/visualize-term-graph', function (hooks) startDate: new Date('2019-12-05T18:00:00'), endDate: new Date('2019-12-05T21:00:00'), }); + this.emptyCourse = await this.owner + .lookup('service:store') + .findRecord('course', this.server.create('course').id); + this.linkedCourseWithTime = await this.owner + .lookup('service:store') + .findRecord('course', linkedCourseWithTime.id); + this.linkedCourseWithoutTime = await this.owner + .lookup('service:store') + .findRecord('course', linkedCourseWithoutTime.id); + this.term = await this.owner.lookup('service:store').findRecord('term', term.id); + }); - const courseModel = await this.owner.lookup('service:store').findRecord('course', course.id); - const termModel = await this.owner.lookup('service:store').findRecord('term', term.id); - - this.set('course', courseModel); - this.set('term', termModel); - + test('it renders', async function (assert) { + this.set('course', this.linkedCourseWithTime); + this.set('term', this.term); await render( - hbs` + hbs` `, ); + assert.notOk(component.noData.isVisible); //let the chart animations finish await waitFor('.loaded'); await waitFor('svg .bars'); + assert.strictEqual(component.chart.bars.length, 2); + assert.strictEqual(component.chart.bars[0].description, 'Campaign - 180 Minutes'); + assert.strictEqual(component.chart.bars[1].description, 'Standalone - 630 Minutes'); + assert.strictEqual(component.chart.labels.length, 2); + assert.strictEqual(component.chart.labels[0].text, 'Campaign'); + assert.strictEqual(component.chart.labels[1].text, 'Standalone'); + assert.strictEqual(component.dataTable.rows.length, 2); + assert.strictEqual(component.dataTable.rows[0].sessionType, 'Campaign'); + assert.strictEqual(component.dataTable.rows[0].sessions.links.length, 1); + assert.strictEqual( + component.dataTable.rows[0].sessions.links[0].text, + 'The San Leandro Horror', + ); + assert.strictEqual(component.dataTable.rows[0].sessions.links[0].url, '/courses/1/sessions/2'); + assert.strictEqual(component.dataTable.rows[0].minutes, '180'); + assert.strictEqual(component.dataTable.rows[1].sessionType, 'Standalone'); + assert.strictEqual(component.dataTable.rows[1].sessions.links.length, 1); + assert.strictEqual( + component.dataTable.rows[1].sessions.links[0].text, + 'Berkeley Investigations', + ); + assert.strictEqual(component.dataTable.rows[1].sessions.links[0].url, '/courses/1/sessions/1'); + assert.strictEqual(component.dataTable.rows[1].minutes, '630'); + }); - const chartLabels = 'svg .bars text'; - assert.dom(chartLabels).exists({ count: 2 }); - assert.dom(findAll(chartLabels)[0]).hasText('Standalone 77.8%'); - assert.dom(findAll(chartLabels)[1]).hasText('Campaign 22.2%'); + test('sort data-table by session type', async function (assert) { + this.set('course', this.linkedCourseWithTime); + await render( + hbs` +`, + ); + assert.strictEqual(component.dataTable.rows[0].sessionType, 'Campaign'); + assert.strictEqual(component.dataTable.rows[1].sessionType, 'Standalone'); + await component.dataTable.header.sessionType.toggle(); + assert.strictEqual(component.dataTable.rows[0].sessionType, 'Campaign'); + assert.strictEqual(component.dataTable.rows[1].sessionType, 'Standalone'); + await component.dataTable.header.sessionType.toggle(); + assert.strictEqual(component.dataTable.rows[0].sessionType, 'Standalone'); + assert.strictEqual(component.dataTable.rows[1].sessionType, 'Campaign'); + await component.dataTable.header.sessionType.toggle(); + assert.strictEqual(component.dataTable.rows[0].sessionType, 'Campaign'); + assert.strictEqual(component.dataTable.rows[1].sessionType, 'Standalone'); + }); + + test('sort data-table by sessions', async function (assert) { + this.set('course', this.linkedCourseWithTime); + await render( + hbs` +`, + ); + assert.strictEqual(component.dataTable.rows[0].sessions.text, 'The San Leandro Horror'); + assert.strictEqual(component.dataTable.rows[1].sessions.text, 'Berkeley Investigations'); + await component.dataTable.header.sessions.toggle(); + assert.strictEqual(component.dataTable.rows[0].sessions.text, 'Berkeley Investigations'); + assert.strictEqual(component.dataTable.rows[1].sessions.text, 'The San Leandro Horror'); + await component.dataTable.header.sessions.toggle(); + assert.strictEqual(component.dataTable.rows[0].sessions.text, 'The San Leandro Horror'); + assert.strictEqual(component.dataTable.rows[1].sessions.text, 'Berkeley Investigations'); + await component.dataTable.header.sessions.toggle(); + assert.strictEqual(component.dataTable.rows[0].sessions.text, 'Berkeley Investigations'); + assert.strictEqual(component.dataTable.rows[1].sessions.text, 'The San Leandro Horror'); + }); + + test('sort data-table by minutes', async function (assert) { + this.set('course', this.linkedCourseWithTime); + await render( + hbs` +`, + ); + assert.strictEqual(component.dataTable.rows[0].minutes, '180'); + assert.strictEqual(component.dataTable.rows[1].minutes, '630'); + await component.dataTable.header.minutes.toggle(); + assert.strictEqual(component.dataTable.rows[0].minutes, '630'); + assert.strictEqual(component.dataTable.rows[1].minutes, '180'); + await component.dataTable.header.minutes.toggle(); + assert.strictEqual(component.dataTable.rows[0].minutes, '180'); + assert.strictEqual(component.dataTable.rows[1].minutes, '630'); + }); + + test('no data', async function (assert) { + this.set('course', this.emptyCourse); + this.set('term', this.term); + await render( + hbs` +`, + ); + assert.notOk(component.chart.isVisible); + assert.notOk(component.dataTable.isVisible); + assert.strictEqual( + component.noData.text, + 'The vocabulary term term 0 has not been linked to any sessions in this course.', + ); + }); + + test('only zero time data', async function (assert) { + this.set('course', this.linkedCourseWithoutTime); + this.set('term', this.term); + await render( + hbs` +`, + ); + assert.notOk(component.chart.isVisible); + assert.notOk(component.noData.isVisible); + assert.strictEqual(component.dataTable.rows.length, 1); + assert.strictEqual(component.dataTable.rows[0].sessionType, 'Prelude'); + assert.strictEqual(component.dataTable.rows[0].sessions.links.length, 1); + assert.strictEqual(component.dataTable.rows[0].sessions.links[0].text, 'Two Slices of Pizza'); + assert.strictEqual(component.dataTable.rows[0].sessions.links[0].url, '/courses/2/sessions/3'); + assert.strictEqual(component.dataTable.rows[0].minutes, '0'); }); }); diff --git a/packages/test-app/tests/integration/components/course/visualize-vocabularies-graph-test.js b/packages/test-app/tests/integration/components/course/visualize-vocabularies-graph-test.js index 3e5f8c0fab..0c690d3be8 100644 --- a/packages/test-app/tests/integration/components/course/visualize-vocabularies-graph-test.js +++ b/packages/test-app/tests/integration/components/course/visualize-vocabularies-graph-test.js @@ -9,26 +9,36 @@ module('Integration | Component | course/visualize-vocabularies-graph', function setupRenderingTest(hooks); setupMirage(hooks); - test('it renders', async function (assert) { + hooks.beforeEach(async function () { const vocabulary1 = this.server.create('vocabulary', { title: 'Standalone', }); const vocabulary2 = this.server.create('vocabulary', { title: 'Campaign', }); + const vocabulary3 = this.server.create('vocabulary', { + title: 'Prelude', + }); const term1 = this.server.create('term', { vocabulary: vocabulary1 }); const term2 = this.server.create('term', { vocabulary: vocabulary2 }); - const course = this.server.create('course'); + const term3 = this.server.create('term', { vocabulary: vocabulary3 }); + const linkedCourseWithTime = this.server.create('course'); + const linkedCourseWithoutTime = this.server.create('course'); const session1 = this.server.create('session', { title: 'Berkeley Investigations', - course, + course: linkedCourseWithTime, terms: [term1], }); const session2 = this.server.create('session', { title: 'The San Leandro Horror', - course, + course: linkedCourseWithTime, terms: [term2], }); + this.server.create('session', { + title: 'Two Slices of Pizza', + course: linkedCourseWithoutTime, + terms: [term3], + }); this.server.create('offering', { session: session1, startDate: new Date('2019-12-08T12:00:00'), @@ -44,18 +54,139 @@ module('Integration | Component | course/visualize-vocabularies-graph', function startDate: new Date('2019-12-05T18:00:00'), endDate: new Date('2019-12-05T21:00:00'), }); + this.emptyCourse = await this.owner + .lookup('service:store') + .findRecord('course', this.server.create('course').id); + this.linkedCourseWithTime = await this.owner + .lookup('service:store') + .findRecord('course', linkedCourseWithTime.id); + this.linkedCourseWithoutTime = await this.owner + .lookup('service:store') + .findRecord('course', linkedCourseWithoutTime.id); + }); - const courseModel = await this.owner.lookup('service:store').findRecord('course', course.id); - - this.set('course', courseModel); - - await render(hbs` -`); + test('it renders', async function (assert) { + this.set('course', this.linkedCourseWithTime); + await render( + hbs` +`, + ); + assert.notOk(component.noData.isVisible); //let the chart animations finish await waitFor('.loaded'); - await waitFor('svg .slice'); - assert.strictEqual(component.chart.slices.length, 2); - assert.strictEqual(component.chart.slices[0].text, 'Standalone'); - assert.strictEqual(component.chart.slices[1].text, 'Campaign'); + await waitFor('svg .bars'); + assert.strictEqual(component.chart.bars.length, 2); + assert.strictEqual(component.chart.bars[0].description, 'Campaign - 180 Minutes'); + assert.strictEqual(component.chart.bars[1].description, 'Standalone - 630 Minutes'); + assert.strictEqual(component.chart.labels.length, 2); + assert.strictEqual(component.chart.labels[0].text, 'Campaign'); + assert.strictEqual(component.chart.labels[1].text, 'Standalone'); + + assert.strictEqual(component.dataTable.rows.length, 2); + assert.strictEqual(component.dataTable.rows[0].vocabulary.text, 'Campaign'); + assert.strictEqual( + component.dataTable.rows[0].vocabulary.url, + '/data/courses/1/vocabularies/2', + ); + assert.strictEqual(component.dataTable.rows[0].sessions.links.length, 1); + assert.strictEqual( + component.dataTable.rows[0].sessions.links[0].text, + 'The San Leandro Horror', + ); + assert.strictEqual(component.dataTable.rows[0].sessions.links[0].url, '/courses/1/sessions/2'); + assert.strictEqual(component.dataTable.rows[0].minutes, '180'); + assert.strictEqual(component.dataTable.rows[1].vocabulary.text, 'Standalone'); + assert.strictEqual( + component.dataTable.rows[1].vocabulary.url, + '/data/courses/1/vocabularies/1', + ); + assert.strictEqual(component.dataTable.rows[1].sessions.links.length, 1); + assert.strictEqual( + component.dataTable.rows[1].sessions.links[0].text, + 'Berkeley Investigations', + ); + assert.strictEqual(component.dataTable.rows[1].sessions.links[0].url, '/courses/1/sessions/1'); + assert.strictEqual(component.dataTable.rows[1].minutes, '630'); + }); + + test('sort data-table by vocabulary', async function (assert) { + this.set('course', this.linkedCourseWithTime); + await render( + hbs` +`, + ); + assert.strictEqual(component.dataTable.rows[0].vocabulary.text, 'Campaign'); + assert.strictEqual(component.dataTable.rows[1].vocabulary.text, 'Standalone'); + await component.dataTable.header.vocabulary.toggle(); + assert.strictEqual(component.dataTable.rows[0].vocabulary.text, 'Campaign'); + assert.strictEqual(component.dataTable.rows[1].vocabulary.text, 'Standalone'); + await component.dataTable.header.vocabulary.toggle(); + assert.strictEqual(component.dataTable.rows[0].vocabulary.text, 'Standalone'); + assert.strictEqual(component.dataTable.rows[1].vocabulary.text, 'Campaign'); + await component.dataTable.header.vocabulary.toggle(); + assert.strictEqual(component.dataTable.rows[0].vocabulary.text, 'Campaign'); + assert.strictEqual(component.dataTable.rows[1].vocabulary.text, 'Standalone'); + }); + + test('sort data-table by sessions', async function (assert) { + this.set('course', this.linkedCourseWithTime); + await render( + hbs` +`, + ); + assert.strictEqual(component.dataTable.rows[0].sessions.text, 'The San Leandro Horror'); + assert.strictEqual(component.dataTable.rows[1].sessions.text, 'Berkeley Investigations'); + await component.dataTable.header.sessions.toggle(); + assert.strictEqual(component.dataTable.rows[0].sessions.text, 'Berkeley Investigations'); + assert.strictEqual(component.dataTable.rows[1].sessions.text, 'The San Leandro Horror'); + await component.dataTable.header.sessions.toggle(); + assert.strictEqual(component.dataTable.rows[0].sessions.text, 'The San Leandro Horror'); + assert.strictEqual(component.dataTable.rows[1].sessions.text, 'Berkeley Investigations'); + await component.dataTable.header.sessions.toggle(); + assert.strictEqual(component.dataTable.rows[0].sessions.text, 'Berkeley Investigations'); + assert.strictEqual(component.dataTable.rows[1].sessions.text, 'The San Leandro Horror'); + }); + + test('sort data-table by minutes', async function (assert) { + this.set('course', this.linkedCourseWithTime); + await render( + hbs` +`, + ); + assert.strictEqual(component.dataTable.rows[0].minutes, '180'); + assert.strictEqual(component.dataTable.rows[1].minutes, '630'); + await component.dataTable.header.minutes.toggle(); + assert.strictEqual(component.dataTable.rows[0].minutes, '630'); + assert.strictEqual(component.dataTable.rows[1].minutes, '180'); + await component.dataTable.header.minutes.toggle(); + assert.strictEqual(component.dataTable.rows[0].minutes, '180'); + assert.strictEqual(component.dataTable.rows[1].minutes, '630'); + }); + + test('no data', async function (assert) { + this.set('course', this.emptyCourse); + await render( + hbs` +`, + ); + assert.notOk(component.chart.isVisible); + assert.notOk(component.dataTable.isVisible); + assert.strictEqual(component.noData.text, 'This course has no sessions.'); + }); + + test('only zero time data', async function (assert) { + this.set('course', this.linkedCourseWithoutTime); + await render( + hbs` +`, + ); + assert.notOk(component.chart.isVisible); + assert.notOk(component.noData.isVisible); + assert.strictEqual(component.dataTable.rows.length, 1); + assert.strictEqual(component.dataTable.rows[0].vocabulary.text, 'Prelude'); + assert.strictEqual(component.dataTable.rows[0].sessions.links.length, 1); + assert.strictEqual(component.dataTable.rows[0].sessions.links[0].text, 'Two Slices of Pizza'); + assert.strictEqual(component.dataTable.rows[0].sessions.links[0].url, '/courses/2/sessions/3'); + assert.strictEqual(component.dataTable.rows[0].minutes, '0'); }); }); diff --git a/packages/test-app/tests/integration/components/course/visualize-vocabulary-graph-test.js b/packages/test-app/tests/integration/components/course/visualize-vocabulary-graph-test.js index 7ad084c60d..6b8d51610c 100644 --- a/packages/test-app/tests/integration/components/course/visualize-vocabulary-graph-test.js +++ b/packages/test-app/tests/integration/components/course/visualize-vocabulary-graph-test.js @@ -19,17 +19,27 @@ module('Integration | Component | course/visualize-vocabulary-graph', function ( vocabulary, title: 'Campaign', }); - const course = this.server.create('course'); + const term3 = this.server.create('term', { + vocabulary, + title: 'Prelude', + }); + const linkedCourseWithTime = this.server.create('course'); + const linkedCourseWithoutTime = this.server.create('course'); const session1 = this.server.create('session', { title: 'Berkeley Investigations', - course, + course: linkedCourseWithTime, terms: [term1], }); const session2 = this.server.create('session', { title: 'The San Leandro Horror', - course, + course: linkedCourseWithTime, terms: [term2], }); + this.server.create('session', { + title: 'Two Slices of Pizza', + course: linkedCourseWithoutTime, + terms: [term3], + }); this.server.create('offering', { session: session1, startDate: new Date('2019-12-08T12:00:00'), @@ -45,28 +55,140 @@ module('Integration | Component | course/visualize-vocabulary-graph', function ( startDate: new Date('2019-12-05T18:00:00'), endDate: new Date('2019-12-05T21:00:00'), }); - - this.courseModel = await this.owner.lookup('service:store').findRecord('course', course.id); - this.vocabularyModel = await this.owner + this.emptyCourse = await this.owner + .lookup('service:store') + .findRecord('course', this.server.create('course').id); + this.linkedCourseWithTime = await this.owner + .lookup('service:store') + .findRecord('course', linkedCourseWithTime.id); + this.linkedCourseWithoutTime = await this.owner + .lookup('service:store') + .findRecord('course', linkedCourseWithoutTime.id); + this.vocabulary = await this.owner .lookup('service:store') .findRecord('vocabulary', vocabulary.id); }); test('it renders', async function (assert) { - this.set('course', this.courseModel); - this.set('vocabulary', this.vocabularyModel); - + this.set('course', this.linkedCourseWithTime); + this.set('vocabulary', this.vocabulary); await render( - hbs` + hbs` `, ); + assert.notOk(component.noData.isVisible); //let the chart animations finish await waitFor('.loaded'); await waitFor('svg .bars'); - assert.strictEqual(component.chart.bars.length, 2); + assert.strictEqual(component.chart.bars[0].description, 'Campaign - 180 Minutes'); + assert.strictEqual(component.chart.bars[1].description, 'Standalone - 630 Minutes'); assert.strictEqual(component.chart.labels.length, 2); - assert.strictEqual(component.chart.labels[0].text, 'Campaign: 180 Minutes'); - assert.strictEqual(component.chart.labels[1].text, 'Standalone: 630 Minutes'); + assert.strictEqual(component.chart.labels[0].text, 'Campaign'); + assert.strictEqual(component.chart.labels[1].text, 'Standalone'); + assert.strictEqual(component.dataTable.rows.length, 2); + assert.strictEqual(component.dataTable.rows[0].term.text, 'Campaign'); + assert.strictEqual(component.dataTable.rows[0].term.url, '/data/courses/1/terms/2'); + + assert.strictEqual(component.dataTable.rows[0].sessions.links.length, 1); + assert.strictEqual( + component.dataTable.rows[0].sessions.links[0].text, + 'The San Leandro Horror', + ); + assert.strictEqual(component.dataTable.rows[0].sessions.links[0].url, '/courses/1/sessions/2'); + assert.strictEqual(component.dataTable.rows[0].minutes, '180'); + assert.strictEqual(component.dataTable.rows[1].term.text, 'Standalone'); + assert.strictEqual(component.dataTable.rows[1].term.url, '/data/courses/1/terms/1'); + assert.strictEqual(component.dataTable.rows[1].sessions.links.length, 1); + assert.strictEqual( + component.dataTable.rows[1].sessions.links[0].text, + 'Berkeley Investigations', + ); + assert.strictEqual(component.dataTable.rows[1].sessions.links[0].url, '/courses/1/sessions/1'); + assert.strictEqual(component.dataTable.rows[1].minutes, '630'); + }); + + test('sort data-table by term', async function (assert) { + this.set('course', this.linkedCourseWithTime); + this.set('vocabulary', this.vocabulary); + await render( + hbs``, + ); + assert.strictEqual(component.dataTable.rows[0].term.text, 'Campaign'); + assert.strictEqual(component.dataTable.rows[1].term.text, 'Standalone'); + await component.dataTable.header.term.toggle(); + assert.strictEqual(component.dataTable.rows[0].term.text, 'Campaign'); + assert.strictEqual(component.dataTable.rows[1].term.text, 'Standalone'); + await component.dataTable.header.term.toggle(); + assert.strictEqual(component.dataTable.rows[0].term.text, 'Standalone'); + assert.strictEqual(component.dataTable.rows[1].term.text, 'Campaign'); + await component.dataTable.header.term.toggle(); + assert.strictEqual(component.dataTable.rows[0].term.text, 'Campaign'); + assert.strictEqual(component.dataTable.rows[1].term.text, 'Standalone'); + }); + + test('sort data-table by sessions', async function (assert) { + this.set('course', this.linkedCourseWithTime); + this.set('vocabulary', this.vocabulary); + await render( + hbs``, + ); + assert.strictEqual(component.dataTable.rows[0].sessions.text, 'The San Leandro Horror'); + assert.strictEqual(component.dataTable.rows[1].sessions.text, 'Berkeley Investigations'); + await component.dataTable.header.sessions.toggle(); + assert.strictEqual(component.dataTable.rows[0].sessions.text, 'Berkeley Investigations'); + assert.strictEqual(component.dataTable.rows[1].sessions.text, 'The San Leandro Horror'); + await component.dataTable.header.sessions.toggle(); + assert.strictEqual(component.dataTable.rows[0].sessions.text, 'The San Leandro Horror'); + assert.strictEqual(component.dataTable.rows[1].sessions.text, 'Berkeley Investigations'); + await component.dataTable.header.sessions.toggle(); + assert.strictEqual(component.dataTable.rows[0].sessions.text, 'Berkeley Investigations'); + assert.strictEqual(component.dataTable.rows[1].sessions.text, 'The San Leandro Horror'); + }); + + test('sort data-table by minutes', async function (assert) { + this.set('course', this.linkedCourseWithTime); + this.set('vocabulary', this.vocabulary); + await render( + hbs``, + ); + assert.strictEqual(component.dataTable.rows[0].minutes, '180'); + assert.strictEqual(component.dataTable.rows[1].minutes, '630'); + await component.dataTable.header.minutes.toggle(); + assert.strictEqual(component.dataTable.rows[0].minutes, '630'); + assert.strictEqual(component.dataTable.rows[1].minutes, '180'); + await component.dataTable.header.minutes.toggle(); + assert.strictEqual(component.dataTable.rows[0].minutes, '180'); + assert.strictEqual(component.dataTable.rows[1].minutes, '630'); + }); + + test('no data', async function (assert) { + this.set('course', this.emptyCourse); + this.set('vocabulary', this.vocabulary); + await render( + hbs``, + ); + assert.notOk(component.chart.isVisible); + assert.notOk(component.dataTable.isVisible); + assert.strictEqual( + component.noData.text, + 'No Vocabulary 1 vocabulary terms have been linked to any sessions in this course.', + ); + }); + + test('only zero time data', async function (assert) { + this.set('course', this.linkedCourseWithoutTime); + this.set('vocabulary', this.vocabulary); + await render( + hbs``, + ); + assert.notOk(component.chart.isVisible); + assert.notOk(component.noData.isVisible); + assert.strictEqual(component.dataTable.rows.length, 1); + assert.strictEqual(component.dataTable.rows[0].term.text, 'Prelude'); + assert.strictEqual(component.dataTable.rows[0].sessions.links.length, 1); + assert.strictEqual(component.dataTable.rows[0].sessions.links[0].text, 'Two Slices of Pizza'); + assert.strictEqual(component.dataTable.rows[0].sessions.links[0].url, '/courses/2/sessions/3'); + assert.strictEqual(component.dataTable.rows[0].minutes, '0'); }); }); diff --git a/packages/test-app/tests/integration/components/course/visualize-vocabulary-test.js b/packages/test-app/tests/integration/components/course/visualize-vocabulary-test.js index 392063790f..7d084d7bb1 100644 --- a/packages/test-app/tests/integration/components/course/visualize-vocabulary-test.js +++ b/packages/test-app/tests/integration/components/course/visualize-vocabulary-test.js @@ -91,7 +91,7 @@ module('Integration | Component | course/visualize-vocabulary', function (hooks) await waitFor('svg .bars'); assert.strictEqual(component.termsChart.chart.bars.length, 2); assert.strictEqual(component.termsChart.chart.labels.length, 2); - assert.strictEqual(component.termsChart.chart.labels[0].text, 'term 1: 60 Minutes'); - assert.strictEqual(component.termsChart.chart.labels[1].text, 'term 0: 150 Minutes'); + assert.strictEqual(component.termsChart.chart.labels[0].text, 'term 1'); + assert.strictEqual(component.termsChart.chart.labels[1].text, 'term 0'); }); });