diff --git a/packages/frontend/.lint-todo b/packages/frontend/.lint-todo index d568bbadd0..7169acfb92 100644 --- a/packages/frontend/.lint-todo +++ b/packages/frontend/.lint-todo @@ -29,7 +29,5 @@ add|ember-template-lint|no-at-ember-render-modifiers|10|6|10|6|d919d2af254f782c0 add|ember-template-lint|no-at-ember-render-modifiers|11|6|11|6|940005188b476a060b0e5d3f05baea24ba178878|1731542400000|1762646400000|1793750400000|app/components/reports/subject/new/term.hbs add|ember-template-lint|no-at-ember-render-modifiers|4|2|4|2|23cd787c79c34a628dadb6e96dd4004d42eebb79|1733184000000|1735776000000|1738368000000|app/components/curriculum-inventory/new-sequence-block.hbs add|ember-template-lint|no-at-ember-render-modifiers|5|2|5|2|77e3831e4ae1b00caee1f808711f2e26ab452a23|1733184000000|1735776000000|1738368000000|app/components/curriculum-inventory/new-sequence-block.hbs -add|ember-template-lint|no-at-ember-render-modifiers|4|2|4|2|e65bc0e9138a1521733c31ef990721569f51eb20|1733184000000|1735776000000|1738368000000|app/components/curriculum-inventory/sequence-block-overview.hbs -add|ember-template-lint|no-at-ember-render-modifiers|5|2|5|2|bd1419d59d5caf1243719b72f63702d3b5bebc07|1733184000000|1735776000000|1738368000000|app/components/curriculum-inventory/sequence-block-overview.hbs add|ember-template-lint|no-at-ember-render-modifiers|5|2|5|2|23cd787c79c34a628dadb6e96dd4004d42eebb79|1733184000000|1735776000000|1738368000000|app/components/curriculum-inventory/sequence-block-session-manager.hbs add|ember-template-lint|no-at-ember-render-modifiers|6|2|6|2|394d8c8b6b1bc28f48ede0d39dcbfbb4dcd8261e|1733184000000|1735776000000|1738368000000|app/components/curriculum-inventory/sequence-block-session-manager.hbs diff --git a/packages/frontend/app/components/curriculum-inventory/sequence-block-overview.hbs b/packages/frontend/app/components/curriculum-inventory/sequence-block-overview.hbs index 4ad5969360..8e63fc5a5a 100644 --- a/packages/frontend/app/components/curriculum-inventory/sequence-block-overview.hbs +++ b/packages/frontend/app/components/curriculum-inventory/sequence-block-overview.hbs @@ -1,498 +1,512 @@
{{#let (unique-id) as |templateId|}}
{{t "general.overview"}}
- {{#if (or this.load.isRunning this.reload.isRunning)}} -
- {{else}} -
- - - {{#if @canUpdate}} - - - - {{else}} - {{if - this.course - this.course.title - (t "general.notApplicableAbbr") - }} - {{/if}} - - {{#if this.course}} +
+ + + {{#if @canUpdate}} + + + + {{else}} + {{if + this.course + this.course.title + (t "general.notApplicableAbbr") + }} + {{/if}} + + {{#if this.selectedCourse}} + {{#if this.selectedCourse.id}} {{t "general.level"}}: - {{this.course.level}}, + {{this.selectedCourse.level}}, {{t "general.startDate"}}: - {{format-date this.course.startDate day="2-digit" month="2-digit" year="numeric"}}, + {{format-date + this.selectedCourse.startDate + day="2-digit" + month="2-digit" + year="numeric" + }}, {{t "general.endDate"}}: - {{format-date this.course.endDate day="2-digit" month="2-digit" year="numeric"}} - {{#if this.course.clerkshipType}} + {{format-date + this.selectedCourse.endDate + day="2-digit" + month="2-digit" + year="numeric" + }} + {{#if this.selectedCourse.clerkshipType}} - {{t "general.clerkship"}} - ({{this.course.clerkshipType.title}}) + ({{this.selectedCourse.clerkshipType.title}}) {{/if}} {{/if}} -
-
- + {{else if this.course}} + + {{t "general.level"}}: + {{this.course.level}}, + {{t "general.startDate"}}: + {{format-date this.course.startDate day="2-digit" month="2-digit" year="numeric"}}, + {{t "general.endDate"}}: + {{format-date this.course.endDate day="2-digit" month="2-digit" year="numeric"}} + {{#if this.course.clerkshipType}} + - + {{t "general.clerkship"}} + ({{this.course.clerkshipType.title}}) + {{/if}} + + {{/if}} +
+
+ + {{#if @canUpdate}} + + + + {{else}} + {{@sequenceBlock.description}} + {{/if}} +
+
+ + {{#if @canUpdate}} + + + + {{else}} + {{this.requiredLabel}} + {{/if}} +
+
+ + {{#if @canUpdate}} + + {{else}} + {{if @sequenceBlock.track (t "general.yes") (t "general.no")}} + {{/if}} +
+ {{#if this.isEditingDatesAndDuration}} +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + +
+
+ {{else}} +
+ {{#if @canUpdate}} - - - + {{#if @sequenceBlock.startDate}} + {{format-date + @sequenceBlock.startDate + day="2-digit" + month="2-digit" + year="numeric" + }} + {{else}} + {{t "general.clickToEdit"}} + {{/if}} + {{else}} - {{@sequenceBlock.description}} + + {{#if @sequenceBlock.startDate}} + {{format-date + @sequenceBlock.startDate + day="2-digit" + month="2-digit" + year="numeric" + }} + {{else}} + {{t "general.notApplicableAbbr"}} + {{/if}} + {{/if}}
-
- +
+ {{#if @canUpdate}} - - - + {{#if @sequenceBlock.endDate}} + {{format-date + @sequenceBlock.endDate + day="2-digit" + month="2-digit" + year="numeric" + }} + {{else}} + {{t "general.clickToEdit"}} + {{/if}} + {{else}} - {{this.requiredLabel}} + + {{#if @sequenceBlock.endDate}} + {{format-date + @sequenceBlock.endDate + day="2-digit" + month="2-digit" + year="numeric" + }} + {{else}} + {{t "general.notApplicableAbbr"}} + {{/if}} + {{/if}}
-
- +
+ {{#if @canUpdate}} - + {{else}} - {{if @sequenceBlock.track (t "general.yes") (t "general.no")}} + + {{#if @sequenceBlock.duration}} + {{@sequenceBlock.duration}} + {{else}} + {{t "general.notApplicableAbbr"}} + {{/if}} + {{/if}}
- {{#if this.isEditingDatesAndDuration}} -
+ + {{#if @canUpdate}} + -
- - - -
-
- - - -
-
- - - -
-
- - -
-
+ + {{else}} -
- - {{#if @canUpdate}} - - {{else}} - - {{#if @sequenceBlock.startDate}} - {{format-date - @sequenceBlock.startDate - day="2-digit" - month="2-digit" - year="numeric" - }} - {{else}} - {{t "general.notApplicableAbbr"}} - {{/if}} - - {{/if}} -
-
- + {{this.childSequenceOrderLabel}} + {{/if}} +
+
+ + + {{#if this.isInOrderedSequence}} {{#if @canUpdate}} - + + {{else}} - - {{#if @sequenceBlock.endDate}} - {{format-date - @sequenceBlock.endDate - day="2-digit" - month="2-digit" - year="numeric" - }} - {{else}} - {{t "general.notApplicableAbbr"}} - {{/if}} - + {{@sequenceBlock.orderInSequence}} {{/if}} + {{else}} + {{t "general.notApplicableAbbr"}} + {{/if}} + +
+
+ + {{#if @canUpdate}} + + + + + {{else}} + {{this.startingAcademicLevel.name}} + {{/if}} +
+
+ + {{#if @canUpdate}} + + + + + {{else}} + {{this.endingAcademicLevel.name}} + {{/if}} +
+
+ +
+ {{#if this.isEditingMinMax}} +
+
+ + +
-
- - {{#if @canUpdate}} - - {{else}} - - {{#if @sequenceBlock.duration}} - {{@sequenceBlock.duration}} - {{else}} - {{t "general.notApplicableAbbr"}} - {{/if}} - - {{/if}} +
+ + +
- {{/if}} -
- - {{#if @canUpdate}} - +
-
- - - {{#if this.isInOrderedSequence}} - {{#if @canUpdate}} - - - + {{#if this.save.isRunning}} + {{else}} - {{@sequenceBlock.orderInSequence}} + {{t "general.done"}} {{/if}} - {{else}} - {{t "general.notApplicableAbbr"}} - {{/if}} - -
-
- - {{#if @canUpdate}} - + +
+
+ {{else}} +
+ + {{#if (and @canUpdate (not this.isElective))}} + {{else}} - {{this.startingAcademicLevel.name}} + {{this.minimum}} {{/if}}
-
- +
+ {{#if @canUpdate}} - - - - + {{this.maximum}} + {{else}} - {{this.endingAcademicLevel.name}} + {{this.maximum}} {{/if}}
-
- -
- {{#if this.isEditingMinMax}} -
-
- - - -
-
- - - -
-
- - -
-
- {{else}} -
- - {{#if (and @canUpdate (not this.isElective))}} - - {{else}} - {{this.minimum}} - {{/if}} -
-
- - {{#if @canUpdate}} - - {{else}} - {{this.maximum}} - {{/if}} -
- {{/if}} {{/if}} {{#unless this.isManagingSessions}}
diff --git a/packages/frontend/app/components/curriculum-inventory/sequence-block-overview.js b/packages/frontend/app/components/curriculum-inventory/sequence-block-overview.js index 0ade0314e7..4781962ff8 100644 --- a/packages/frontend/app/components/curriculum-inventory/sequence-block-overview.js +++ b/packages/frontend/app/components/curriculum-inventory/sequence-block-overview.js @@ -1,11 +1,12 @@ import Component from '@glimmer/component'; -import { tracked } from '@glimmer/tracking'; +import { cached, tracked } from '@glimmer/tracking'; import { action } from '@ember/object'; import { service } from '@ember/service'; import { isPresent } from '@ember/utils'; -import { dropTask, restartableTask } from 'ember-concurrency'; +import { dropTask } from 'ember-concurrency'; import { all } from 'rsvp'; import { ValidateIf } from 'class-validator'; +import { TrackedAsyncData } from 'ember-async-data'; import { validatable, AfterDate, @@ -21,20 +22,12 @@ import { findById } from 'ilios-common/utils/array-helpers'; export default class CurriculumInventorySequenceBlockOverviewComponent extends Component { @service intl; @service store; - @tracked - @Custom('validateStartingEndingLevelCallback', 'validateStartingLevelMessageCallback') - startingAcademicLevel; - @tracked - @Custom('validateStartingEndingLevelCallback', 'validateEndingLevelMessageCallback') - endingAcademicLevel; - @tracked academicLevels = []; + @tracked childSequenceOrder; - @tracked course; @tracked description; @tracked isEditingDatesAndDuration = false; @tracked isEditingMinMax = false; @tracked isManagingSessions = false; - @tracked linkableCourses = []; @tracked @NotBlank() @IsInt() @Gte(0) minimum; @tracked @NotBlank() @@ -42,13 +35,8 @@ export default class CurriculumInventorySequenceBlockOverviewComponent extends C @Gte(0) @Custom('validateMaximumCallback', 'validateMaximumMessageCallback') maximum; - @tracked isInOrderedSequence; @tracked orderInSequence; - @tracked orderInSequenceOptions = []; - @tracked parent; - @tracked report; @tracked required; - @tracked sessions = []; @tracked @NotBlank() @IsInt() @@ -64,90 +52,86 @@ export default class CurriculumInventorySequenceBlockOverviewComponent extends C @NotBlank() @AfterDate('startDate', { granularity: 'day' }) endDate; + @tracked selectedCourse; + @tracked + @Custom('validateStartingEndingLevelCallback', 'validateStartingLevelMessageCallback') + selectedStartingAcademicLevel; + @tracked + @Custom('validateStartingEndingLevelCallback', 'validateEndingLevelMessageCallback') + selectedEndingAcademicLevel; - get linkedCourseIsClerkship() { - if (!this.course) { - return false; - } - return !!this.course.belongsTo('clerkshipType').id(); + constructor() { + super(...arguments); + this.required = this.args.sequenceBlock.required.toString(); + this.duration = this.args.sequenceBlock.duration; + this.startDate = this.args.sequenceBlock.startDate; + this.endDate = this.args.sequenceBlock.endDate; + this.childSequenceOrder = this.args.sequenceBlock.childSequenceOrder.toString(); + this.orderInSequence = this.args.sequenceBlock.orderInSequence; + this.minimum = this.args.sequenceBlock.minimum; + this.maximum = this.args.sequenceBlock.maximum; + this.description = this.args.sequenceBlock.description; } - get hasZeroDuration() { - const num = Number(this.duration); - if (Number.isNaN(num)) { - return false; - } - return 0 === num; + @cached + get reportData() { + return new TrackedAsyncData(this.args.sequenceBlock.report); } - load = restartableTask(async (element, [sequenceBlock]) => { - this.report = await sequenceBlock.report; - this.parent = await sequenceBlock.parent; - this.academicLevels = await this.report.academicLevels; - this.isInOrderedSequence = false; - this.orderInSequenceOptions = []; - if (isPresent(this.parent) && this.parent.isOrdered) { - this.isInOrderedSequence = true; - const siblings = await this.parent.children; - for (let i = 0, n = siblings.length; i < n; i++) { - const num = i + 1; - this.orderInSequenceOptions.push(num); - } - } - this.linkedSessions = await sequenceBlock.sessions; - this.startingAcademicLevel = await sequenceBlock.startingAcademicLevel; - this.endingAcademicLevel = await sequenceBlock.endingAcademicLevel; - this.required = sequenceBlock.required.toString(); - this.duration = sequenceBlock.duration; - this.startDate = sequenceBlock.startDate; - this.endDate = sequenceBlock.endDate; - this.childSequenceOrder = sequenceBlock.childSequenceOrder.toString(); - this.orderInSequence = sequenceBlock.orderInSequence; - this.description = sequenceBlock.description; - this.course = await sequenceBlock.course; - this.minimum = sequenceBlock.minimum; - this.maximum = sequenceBlock.maximum; - this.sessions = await this.getSessions(this.course); - this.linkableCourses = await this.getLinkableCourses(this.report, this.course); - }); + get report() { + return this.reportData.isResolved ? this.reportData.value : null; + } - get requiredLabel() { - switch (this.required) { - case '1': - return this.intl.t('general.required'); - case '2': - return this.intl.t('general.optionalElective'); - case '3': - return this.intl.t('general.requiredInTrack'); - default: - return null; - } + @cached + get academicLevelsData() { + return new TrackedAsyncData(this.report?.academicLevels); } - get isElective() { - return '2' === this.required; + get academicLevels() { + return this.academicLevelsData.isResolved ? this.academicLevelsData.value : []; } - get isSelective() { - if (this.isElective) { + @cached + get startingAcademicLevelData() { + return new TrackedAsyncData(this.args.sequenceBlock.startingAcademicLevel); + } + + get startingAcademicLevel() { + return this.startingAcademicLevelData.isResolved ? this.startingAcademicLevelData.value : null; + } + + @cached + get endingAcademicLevelData() { + return new TrackedAsyncData(this.args.sequenceBlock.endingAcademicLevel); + } + + get endingAcademicLevel() { + return this.endingAcademicLevelData.isResolved ? this.endingAcademicLevelData.value : null; + } + + @cached + get courseData() { + return new TrackedAsyncData(this.args.sequenceBlock.course); + } + + get course() { + return this.courseData.isResolved ? this.courseData.value : null; + } + + get linkedCourseIsClerkship() { + if (!this.course) { return false; } - const minimum = parseInt(this.minimum, 10); - const maximum = parseInt(this.maximum, 10); - return minimum > 0 && minimum !== maximum; + return !!this.course.belongsTo('clerkshipType').id(); } - get childSequenceOrderLabel() { - switch (this.childSequenceOrder) { - case '1': - return this.intl.t('general.ordered'); - case '2': - return this.intl.t('general.unordered'); - case '3': - return this.intl.t('general.parallel'); - default: - return null; - } + @cached + get linkableCourseData() { + return new TrackedAsyncData(this.getLinkableCourses(this.report, this.course)); + } + + get linkableCourses() { + return this.linkableCourseData.isResolved ? this.linkableCourseData.value : []; } /** @@ -179,6 +163,15 @@ export default class CurriculumInventorySequenceBlockOverviewComponent extends C return linkableCourses; } + @cached + get sessionsData() { + return new TrackedAsyncData(this.getSessions(this.course)); + } + + get sessions() { + return this.sessionsData.isResolved ? this.sessionsData.value : []; + } + /** * Returns a list of published sessions that belong to a given course. */ @@ -186,13 +179,97 @@ export default class CurriculumInventorySequenceBlockOverviewComponent extends C if (!course) { return []; } - const sessions = await this.store.query('session', { + return await this.store.query('session', { filters: { - course: course.get('id'), + course: course.id, published: true, }, }); - return sessions; + } + + @cached + get parentData() { + return new TrackedAsyncData(this.args.sequenceBlock.parent); + } + + get parent() { + return this.parentData.isResolved ? this.parentData.value : null; + } + + @cached + get selfAndSiblingsData() { + return new TrackedAsyncData(this.getSelfAndSiblings(this.parent)); + } + + get selfAndSiblings() { + return this.selfAndSiblingsData.isResolved ? this.selfAndSiblingsData.value : []; + } + + async getSelfAndSiblings(parent) { + if (!parent) { + return [this.args.sequenceBlock]; + } + return await parent.children; + } + + get isInOrderedSequence() { + return this.parent && this.parent.isOrdered; + } + + get orderInSequenceOptions() { + const rhett = []; + for (let i = 0, n = this.selfAndSiblings.length; i < n; i++) { + const num = i + 1; + rhett.push(num); + } + return rhett; + } + + get hasZeroDuration() { + const num = Number(this.duration); + if (Number.isNaN(num)) { + return false; + } + return 0 === num; + } + + get requiredLabel() { + switch (this.required) { + case '1': + return this.intl.t('general.required'); + case '2': + return this.intl.t('general.optionalElective'); + case '3': + return this.intl.t('general.requiredInTrack'); + default: + return null; + } + } + + get isElective() { + return '2' === this.required; + } + + get isSelective() { + if (this.isElective) { + return false; + } + const minimum = parseInt(this.minimum, 10); + const maximum = parseInt(this.maximum, 10); + return minimum > 0 && minimum !== maximum; + } + + get childSequenceOrderLabel() { + switch (this.childSequenceOrder) { + case '1': + return this.intl.t('general.ordered'); + case '2': + return this.intl.t('general.unordered'); + case '3': + return this.intl.t('general.parallel'); + default: + return null; + } } changeRequired = dropTask(async () => { @@ -220,21 +297,29 @@ export default class CurriculumInventorySequenceBlockOverviewComponent extends C } @action - async saveCourse() { - const oldCourse = await this.args.sequenceBlock.course; - if (!this.isDestroying) { - if (oldCourse !== this.course) { - this.args.sequenceBlock.set('sessions', []); - this.args.sequenceBlock.set('excludedSessions', []); - } - this.args.sequenceBlock.set('course', this.course); - await this.args.sequenceBlock.save(); + updateCourse(event) { + const value = event.target.value; + if (!value) { + this.selectedCourse = {}; // Use an empty object here to indicate a user selection. + } else { + this.selectedCourse = findById(this.linkableCourses, value); } } @action async revertCourseChanges() { - this.course = await this.args.sequenceBlock.get('course'); + this.selectedCourse = null; + } + + @action + async saveCourse() { + const oldCourse = await this.args.sequenceBlock.course; + if (oldCourse !== this.selectedCourse) { + this.args.sequenceBlock.set('sessions', []); + this.args.sequenceBlock.set('excludedSessions', []); + } + this.args.sequenceBlock.set('course', this.selectedCourse.id ? this.selectedCourse : null); + await this.args.sequenceBlock.save(); } changeTrack = dropTask(async (value) => { @@ -265,48 +350,76 @@ export default class CurriculumInventorySequenceBlockOverviewComponent extends C } changeStartingAcademicLevel = dropTask(async () => { - this.addErrorDisplaysFor(['startingAcademicLevel']); + if (!this.selectedStartingAcademicLevel) { + return; + } + if (this.selectedStartingAcademicLevel.id === this.startingAcademicLevel.id) { + return; + } + this.addErrorDisplaysFor(['selectedStartingAcademicLevel']); const isValid = await this.isValid(); if (!isValid) { return false; } - this.args.sequenceBlock.set('startingAcademicLevel', this.startingAcademicLevel); + this.args.sequenceBlock.set('startingAcademicLevel', this.selectedStartingAcademicLevel); await this.args.sequenceBlock.save(); + this.revertStartingAcademicLevelChanges(); }); @action setStartingAcademicLevel(event) { const id = event.target.value; - this.startingAcademicLevel = findById(this.academicLevels, id); + this.selectedStartingAcademicLevel = findById(this.academicLevels, id); } @action revertStartingAcademicLevelChanges() { - this.startingAcademicLevel = this.args.sequenceBlock.startingAcademicLevel; + this.selectedStartingAcademicLevel = null; } changeEndingAcademicLevel = dropTask(async () => { - this.addErrorDisplaysFor(['endingAcademicLevel']); + if (!this.selectedEndingAcademicLevel) { + return; + } + if (this.selectedEndingAcademicLevel.id === this.endingAcademicLevel.id) { + return; + } + this.addErrorDisplaysFor(['selectedEndingAcademicLevel']); const isValid = await this.isValid(); if (!isValid) { return false; } - this.args.sequenceBlock.set('endingAcademicLevel', this.endingAcademicLevel); + this.args.sequenceBlock.set('endingAcademicLevel', this.selectedEndingAcademicLevel); await this.args.sequenceBlock.save(); + this.revertEndingAcademicLevelChanges(); }); @action setEndingAcademicLevel(event) { const id = event.target.value; - this.endingAcademicLevel = findById(this.academicLevels, id); + this.selectedEndingAcademicLevel = findById(this.academicLevels, id); } @action revertEndingAcademicLevelChanges() { - this.endingAcademicLevel = this.args.sequenceBlock.endingAcademicLevel; + this.selectedStartingAcademicLevel = null; + } + + @action + updateOrderInSequence(event) { + this.orderInSequence = parseInt(event.target.value); + } + + @action + revertOrderInSequenceChanges() { + this.orderInSequence = this.args.sequenceBlock.orderInSequence; } - changeOrderInSequence = dropTask(async () => { + saveOrderInSequenceChanges = dropTask(async () => { + if (this.orderInSequence === this.args.sequenceBlock.orderInSequence) { + return; + } + this.args.sequenceBlock.set('orderInSequence', this.orderInSequence); const savedBlock = await this.args.sequenceBlock.save(); const parent = await savedBlock.parent; @@ -314,11 +427,6 @@ export default class CurriculumInventorySequenceBlockOverviewComponent extends C await all(children.map((child) => child.reload())); }); - @action - revertOrderInSequenceChanges() { - this.orderInSequence = this.args.sequenceBlock.orderInSequence; - } - @action editMinMax() { this.isEditingMinMax = true; @@ -348,26 +456,11 @@ export default class CurriculumInventorySequenceBlockOverviewComponent extends C this.isManagingSessions = false; }); - @action - updateCourse(event) { - const value = event.target.value; - if (!value) { - this.course = null; - } else { - this.course = findById(this.linkableCourses, value); - } - } - @action changeDescription(event) { this.description = event.target.value; } - @action - updateOrderInSequence(event) { - this.orderInSequence = event.target.value; - } - @action async saveOrCancelMinMax(ev) { const keyCode = ev.keyCode; @@ -398,7 +491,9 @@ export default class CurriculumInventorySequenceBlockOverviewComponent extends C @action validateStartingEndingLevelCallback() { - return this.endingAcademicLevel.level >= this.startingAcademicLevel.level; + const startingAcademicLevel = this.selectedStartingAcademicLevel || this.startingAcademicLevel; + const endingAcademicLevel = this.selectedEndingAcademicLevel || this.endingAcademicLevel; + return endingAcademicLevel.level >= startingAcademicLevel.level; } @action