diff --git a/README.md b/README.md index cf49e28efd..04c76a2b2e 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,34 @@ A modern, lightweight learning management system. ## Migration Progress +Important: When completing a frontend migration, please update the below list regarding the component you have migrated. + SUMMARY: -73 / 132 components migrated +- `89 / 183` components migrated +- `19` components no longer in the doubtfire-lms/9.x branch + +NO LONGER IN doubtfire-lms/9.x + +- [x] ./src/app/projects/states/all/directives/all-projects-list/all-projects-list.coffee +- [x] ./src/app/projects/states/all/all.coffee +- [x] ./src/app/groups/tutor-group-manager/tutor-group-manager.coffee +- [x] ./src/app/tasks/task-definition-selector/task-definition-selector.coffee +- [x] ./src/app/tasks/task-status-selector/task-status-selector.coffee +- [x] ./src/app/config/debug/debug.coffee +- [x] ./src/app/projects/states/all/directives/directives.coffee +- [x] ./src/app/projects/states/dashboard/directives/task-dashboard/directives/directives.coffee +- [x] ./src/app/projects/states/dashboard/directives/task-dashboard/directives/task-outcomes-card/task-outcomes-card.coffee +- [x] ./src/app/admin/states/states.coffee +- [x] ./src/app/admin/admin.coffee +- [x] ./src/app/units/states/tasks/viewer/directives/directives.coffee +- [x] ./src/app/units/states/tasks/viewer/viewer.coffee +- [x] ./src/app/units/states/all/directives/all-units-list/all-units-list.coffee +- [x] ./src/app/units/states/all/directives/directives.coffee +- [x] ./src/app/units/states/all/all.coffee +- [x] ./src/app/common/alert-list/alert-list.coffee +- [x] ./src/app/common/modals/progress-modal/progress-modal.coffee +- [x] ./src/app/errors/states/not-found/not-found.coffee MIGRATED: @@ -25,6 +50,7 @@ MIGRATED: - [x] ./src/app/tasks/task-comments-viewer/extension-comment/extension-comment.component.ts - [x] ./src/app/tasks/task-comments-viewer/intelligent-discussion-player/intelligent-discussion-player.component.ts - [x] ./src/app/tasks/task-comments-viewer/intelligent-discussion-player/intelligent-discussion-recorder/intelligent-discussion-recorder.component.ts +- [x] ./src/app/tasks/project-tasks-list/project-tasks-list.coffee - [x] ./src/app/tasks/task-comments-viewer/pdf-image-comment/pdf-image-comment.component.ts - [x] ./src/app/tasks/task-comments-viewer/comment-bubble-action/comment-bubble-action.component.ts - [x] ./src/app/tasks/task-comments-viewer/task-comments-viewer.component.ts @@ -46,10 +72,10 @@ MIGRATED: - [x] ./src/app/admin/tii-action-log/tii-action-log.component.ts - [x] ./src/app/admin/states/teaching-periods/teaching-period-list/teaching-period-list.component.ts - [x] ./src/app/admin/states/teaching-periods/teaching-period-unit-import/teaching-period-unit-import.dialog.ts +- [x] ./src/app/admin/modals/create-unit-modal/create-new-unit-modal.component.ts - [x] ./src/app/eula/accept-eula/accept-eula.component.ts - [x] ./src/app/welcome/welcome.component.ts - [x] ./src/app/units/states/tasks/inbox/directives/staff-task-list/staff-task-list.component.ts -- [x] ./src/app/units/states/tasks/inbox/inbox.component.ts - [x] ./src/app/units/states/edit/directives/unit-students-editor/student-tutorial-select/student-tutorial-select.component.ts - [x] ./src/app/units/states/edit/directives/unit-students-editor/unit-students-editor.component.ts - [x] ./src/app/units/states/edit/directives/unit-students-editor/student-campus-select/student-campus-select.component.ts @@ -64,6 +90,7 @@ MIGRATED: - [x] ./src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-editor.component.ts - [x] ./src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-general/task-definition-general.component.ts - [x] ./src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-upload/task-definition-upload.component.ts +- [x] ./src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.ts - [x] ./src/app/units/states/analytics/unit-analytics-route.component.ts - [x] ./src/app/common/footer/footer.component.ts - [x] ./src/app/common/audio-recorder/audio/audio-comment-recorder/audio-comment-recorder.ts @@ -91,7 +118,37 @@ MIGRATED: - [x] ./src/app/common/services/alert.service.ts - [x] ./src/app/sessions/states/sign-in/sign-in.component.ts - [x] ./src/app/account/edit-profile/edit-profile.component.ts +- [x] ./src/app/tasks/modals/grade-task-modal/grade-task-modal.component.ts +- [x] ./src/app/units/modals/unit-student-enrolment-modal/unit-student-enrolment-modal.component.ts +- [x] ./src/app/visualisations/progress-burndown-chart/progressburndownchart.component.ts +- [x] ./src/app/config/privacy-policy/privacy-policy.coffee +- [x] ./src/app/units/states/tasks/viewer/directives/task-sheet-view/task-sheet-view.coffee +- [x] ./src/app/units/states/tasks/viewer/directives/task-details-view/task-details-view.coffee +- [x] ./src/app/units/states/tasks/viewer/directives/unit-task-list/unit-task-list.coffee +- [x] ./src/app/projects/states/dashboard/directives/student-task-list/student-task-list.coffee +- [x] ./src/app/units/states/tasks/inbox/inbox.coffee +- [x] ./src/app/admin/states/units/units.component.ts +- [x] ./src/app/admin/states/users/users.component.ts +- [x] ./src/app/common/grade-icon/grade-icon.component.ts +- [x] ./src/app/common/services/grade.service.ts +- [x] ./src/app/common/services/alert.service.ts +- [x] ./src/app/errors/states/unauthorised/unauthorised.component.ts +- [x] ./src/app/groups/group-set-selector/group-set-selector.component.ts - [x] ./src/app/admin/modals/create-unit-modal/create-unit-modal.coffee +- [x] ./src/app/common/services/date.service.ts +- [x] ./src/app/units/states/edit/directives/unit-details-editor/unit-details-editor.coffee (IN 10.0.x) +- [x] ./src/app/groups/group-member-list/group-member-list.coffee +- [x] ./src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.coffee +- [x] ./src/app/common/modals/confirmation-modal/confirmation-modal.coffee +- [x] ./src/app/common/modals/comments-modal/comments-modal.coffee (IN 10.0.x) +- [x] ./src/app/groups/group-selector/group-selector.coffee +- [x] ./src/app/groups/group-set-manager/group-set-manager.coffee +- [x] ./src/app/common/file-uploader/file-uploader.coffee +- [x] ./src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.coffee +- [x] ./src/app/sessions/auth/http-auth-injector.coffee +- [x] ./src/app/sessions/sessions.coffee +- [x] ./src/app/projects/states/portfolio/directives/portfolio-learning-summary-report-step/portfolio-learning-summary-report-step.coffee +- [x] ./src/app/projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.coffee TODO: @@ -101,94 +158,59 @@ TODO: - [ ] ./src/app/visualisations/achievement-custom-bar-chart.coffee - [ ] ./src/app/visualisations/student-task-status-pie-chart.coffee - [ ] ./src/app/visualisations/alignment-bullet-chart.coffee -- [ ] ./src/app/visualisations/progress-burndown-chart.coffee - [ ] ./src/app/visualisations/task-status-pie-chart.coffee - [ ] ./src/app/visualisations/achievement-box-plot.coffee - [ ] ./src/app/visualisations/task-completion-box-plot.coffee - [ ] ./src/app/visualisations/visualisations.coffee -- [ ] ./src/app/tasks/task-status-selector/task-status-selector.coffee - [ ] ./src/app/tasks/tasks.coffee -- [ ] ./src/app/tasks/modals/modals.coffee - [ ] ./src/app/tasks/modals/upload-submission-modal/upload-submission-modal.coffee -- [ ] ./src/app/tasks/modals/grade-task-modal/grade-task-modal.coffee -- [ ] ./src/app/tasks/task-definition-selector/task-definition-selector.coffee -- [ ] ./src/app/tasks/project-tasks-list/project-tasks-list.coffee -- [ ] ./src/app/tasks/task-ilo-alignment/task-ilo-alignment-rater/task-ilo-alignment-rater.coffee - [ ] ./src/app/tasks/task-ilo-alignment/modals/task-ilo-alignment-modal/task-ilo-alignment-modal.coffee - [ ] ./src/app/tasks/task-ilo-alignment/modals/task-ilo-alignment.coffee - [ ] ./src/app/tasks/task-ilo-alignment/task-ilo-alignment-editor/task-ilo-alignment-editor.coffee - [ ] ./src/app/tasks/task-ilo-alignment/task-ilo-alignment.coffee - [ ] ./src/app/tasks/task-ilo-alignment/task-ilo-alignment-viewer/task-ilo-alignment-viewer.coffee -- [ ] ./src/app/config/privacy-policy/privacy-policy.coffee +- [ ] ./src/app/tasks/task-ilo-alignment/task-ilo-alignment-rater/task-ilo-alignment-rater.coffee +- [ ] ./src/app/tasks/modals/modals.coffee - [ ] ./src/app/config/config.coffee - [ ] ./src/app/config/runtime/runtime.coffee - [ ] ./src/app/config/root-controller/root-controller.coffee - [ ] ./src/app/config/local-storage/local-storage.coffee -- [ ] ./src/app/config/routing/routing.coffee - [ ] ./src/app/config/vendor-dependencies/vendor-dependencies.coffee +- [ ] ./src/app/config/routing/routing.coffee - [ ] ./src/app/config/analytics/analytics.coffee -- [ ] ./src/app/config/debug/debug.coffee - [ ] ./src/app/projects/projects.coffee - [ ] ./src/app/projects/project-progress-dashboard/project-progress-dashboard.coffee - [ ] ./src/app/projects/states/states.coffee -- [ ] ./src/app/projects/states/all/directives/directives.coffee -- [ ] ./src/app/projects/states/all/directives/all-projects-list/all-projects-list.coffee -- [ ] ./src/app/projects/states/all/all.coffee - [ ] ./src/app/projects/states/groups/groups.coffee - [ ] ./src/app/projects/states/feedback/feedback.coffee - [ ] ./src/app/projects/states/dashboard/directives/directives.coffee - [ ] ./src/app/projects/states/dashboard/directives/progress-dashboard/progress-dashboard.coffee -- [ ] ./src/app/projects/states/dashboard/directives/student-task-list/student-task-list.coffee -- [ ] ./src/app/projects/states/dashboard/directives/task-dashboard/directives/directives.coffee -- [ ] ./src/app/projects/states/dashboard/directives/task-dashboard/directives/task-outcomes-card/task-outcomes-card.coffee - [ ] ./src/app/projects/states/dashboard/directives/task-dashboard/task-dashboard.coffee - [ ] ./src/app/projects/states/dashboard/dashboard.coffee - [ ] ./src/app/projects/states/outcomes/outcomes.coffee - [ ] ./src/app/projects/states/portfolio/directives/portfolio-review-step/portfolio-review-step.coffee - [ ] ./src/app/projects/states/portfolio/directives/directives.coffee -- [ ] ./src/app/projects/states/portfolio/directives/portfolio-learning-summary-report-step/portfolio-learning-summary-report-step.coffee -- [ ] ./src/app/projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.coffee -- [ ] ./src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.coffee -- [ ] ./src/app/projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.coffee - [ ] ./src/app/projects/states/portfolio/directives/portfolio-tasks-step/portfolio-tasks-step.coffee +- [ ] ./src/app/projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.coffee - [ ] ./src/app/projects/states/portfolio/portfolio.coffee - [ ] ./src/app/projects/states/index/index.coffee -- [ ] ./src/app/projects/states/tutorials/tutorials.coffee - [ ] ./src/app/projects/project-outcome-alignment/project-outcome-alignment.coffee +- [ ] ./src/app/projects/states/tutorials/tutorials.coffee - [ ] ./src/app/admin/modals/modals.coffee -- [ ] ./src/app/admin/states/states.coffee -- [ ] ./src/app/admin/states/units/units.coffee -- [ ] ./src/app/admin/states/users/users.coffee -- [ ] ./src/app/admin/admin.coffee -- [ ] ./src/app/groups/group-selector/group-selector.coffee -- [ ] ./src/app/groups/group-set-manager/group-set-manager.coffee - [ ] ./src/app/groups/group-member-contribution-assigner/group-member-contribution-assigner.coffee -- [ ] ./src/app/groups/group-member-list/group-member-list.coffee -- [ ] ./src/app/groups/group-set-selector/group-set-selector.coffee - [ ] ./src/app/groups/tutor-group-manager/tutor-group-manager.coffee - [ ] ./src/app/groups/groups.coffee -- [ ] ./src/app/units/modals/unit-student-enrolment-modal/unit-student-enrolment-modal.coffee +- [ ] ./src/app/units/states/groups/groups.coffee +- [ ] ./src/app/units/states/edit/directives/unit-group-set-editor/unit-group-set-editor.coffee - [ ] ./src/app/units/modals/modals.coffee - [ ] ./src/app/units/modals/unit-ilo-edit-modal/unit-ilo-edit-modal.coffee - [ ] ./src/app/units/units.coffee - [ ] ./src/app/units/states/states.coffee -- [ ] ./src/app/units/states/tasks/inbox/inbox.coffee - [ ] ./src/app/units/states/tasks/tasks.coffee -- [ ] ./src/app/units/states/tasks/viewer/directives/directives.coffee -- [ ] ./src/app/units/states/tasks/viewer/directives/task-sheet-view/task-sheet-view.coffee -- [ ] ./src/app/units/states/tasks/viewer/directives/task-details-view/task-details-view.coffee -- [ ] ./src/app/units/states/tasks/viewer/directives/unit-task-list/unit-task-list.coffee -- [ ] ./src/app/units/states/tasks/viewer/viewer.coffee - [ ] ./src/app/units/states/tasks/definition/definition.coffee - [ ] ./src/app/units/states/portfolios/portfolios.coffee -- [ ] ./src/app/units/states/all/directives/all-units-list/all-units-list.coffee -- [ ] ./src/app/units/states/all/directives/directives.coffee -- [ ] ./src/app/units/states/all/all.coffee -- [ ] ./src/app/units/states/groups/groups.coffee +- [ ] ./src/app/units/states/analytics/analytics.coffee - [ ] ./src/app/units/states/edit/directives/directives.coffee -- [ ] ./src/app/units/states/edit/directives/unit-group-set-editor/unit-group-set-editor.coffee -- [ ] ./src/app/units/states/edit/directives/unit-details-editor/unit-details-editor.coffee -- [ ] ./src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.coffee - [ ] ./src/app/units/states/edit/directives/unit-ilo-editor/unit-ilo-editor.coffee - [ ] ./src/app/units/states/edit/edit.coffee - [ ] ./src/app/units/states/rollover/directives/directives.coffee @@ -196,34 +218,21 @@ TODO: - [ ] ./src/app/units/states/rollover/rollover.coffee - [ ] ./src/app/units/states/index/index.coffee - [ ] ./src/app/units/states/students-list/students-list.coffee -- [ ] ./src/app/units/states/analytics/analytics.coffee -- [ ] ./src/app/common/filters/filters.coffee -- [ ] ./src/app/common/content-editable/content-editable.coffee -- [ ] ./src/app/common/alert-list/alert-list.coffee -- [ ] ./src/app/common/modals/confirmation-modal/confirmation-modal.coffee -- [ ] ./src/app/common/modals/comments-modal/comments-modal.coffee - [ ] ./src/app/common/modals/modals.coffee - [ ] ./src/app/common/modals/csv-result-modal/csv-result-modal.coffee -- [ ] ./src/app/common/modals/progress-modal/progress-modal.coffee -- [ ] ./src/app/common/grade-icon/grade-icon.coffee -- [ ] ./src/app/common/file-uploader/file-uploader.coffee - [ ] ./src/app/common/common.coffee -- [ ] ./src/app/common/services/grade-service.coffee -- [ ] ./src/app/common/services/date-service.coffee -- [ ] ./src/app/common/services/alert-service.coffee +- [ ] ./src/app/common/content-editable/content-editable.coffee - [ ] ./src/app/common/services/media-service.coffee - [ ] ./src/app/common/services/recorder-service.coffee - [ ] ./src/app/common/services/outcome-service.coffee - [ ] ./src/app/common/services/listener-service.coffee -- [ ] ./src/app/common/services/analytics-service.coffee - [ ] ./src/app/common/services/services.coffee -- [ ] ./src/app/sessions/auth/http-auth-injector.coffee -- [ ] ./src/app/sessions/sessions.coffee +- [ ] ./src/app/common/services/date-service.coffee +- [ ] ./src/app/common/services/analytics-service.coffee - [ ] ./src/app/errors/errors.coffee - [ ] ./src/app/errors/states/states.coffee -- [ ] ./src/app/errors/states/unauthorised/unauthorised.coffee -- [ ] ./src/app/errors/states/not-found/not-found.coffee - [ ] ./src/app/errors/states/timeout/timeout.coffee +- [ ] ./src/app/common/filters/filters.coffee ## Table of Contents diff --git a/eslint.config.js b/eslint.config.js index a327c5d852..a519ed7891 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -29,6 +29,7 @@ module.exports = tseslint.config( processor: angular.processInlineTemplates, // Override specific rules for TypeScript files (these will take priority over the extended configs above) rules: { + '@typescript-eslint/no-inferrable-types': 'off', '@angular-eslint/directive-selector': [ 'error', { diff --git a/package-lock.json b/package-lock.json index c3968a6562..f95bc84f0b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "@angular/service-worker": "^18.0", "@angular/upgrade": "^18.0", "@ctrl/ngx-emoji-mart": "^9.2.0", + "@iplab/ngx-file-upload": "^18.0.0", "@ngneat/hotkeys": "^4.0.0", "@swimlane/ngx-charts": "^20.5.0", "@uirouter/angular": "^14.0", @@ -4534,6 +4535,21 @@ "node": ">=18" } }, + "node_modules/@iplab/ngx-file-upload": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/@iplab/ngx-file-upload/-/ngx-file-upload-18.0.0.tgz", + "integrity": "sha512-Uz+011ZOGtVeFAPuOcFHBB/hyLZrV3RNOqT21J13YMqWr5jadba0t67towrQ7VTHLMYt1Du/UHDmv5wV/h7/sg==", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/animations": "^18.0.0", + "@angular/common": "^18.0.0", + "@angular/core": "^18.0.0", + "@angular/forms": "^18.0.0", + "rxjs": "^7.0.0" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "dev": true, diff --git a/package.json b/package.json index e3ad87adbc..506258d1be 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "@angular/service-worker": "^18.0", "@angular/upgrade": "^18.0", "@ctrl/ngx-emoji-mart": "^9.2.0", + "@iplab/ngx-file-upload": "^18.0.0", "@ngneat/hotkeys": "^4.0.0", "@uirouter/angular": "^14.0", "@uirouter/angular-hybrid": "^18.0", diff --git a/src/app/api/models/project.ts b/src/app/api/models/project.ts index 380c62d199..3c154842e2 100644 --- a/src/app/api/models/project.ts +++ b/src/app/api/models/project.ts @@ -347,7 +347,7 @@ export class Project extends Entity { } public isEnrolledIn(tutorial: Tutorial): boolean { - return this.tutorials.includes(tutorial); + return this.tutorials.some((t) => t.id === tutorial.id); } public updateUnitEnrolment(): void { diff --git a/src/app/api/services/unit.service.ts b/src/app/api/services/unit.service.ts index e04d3bec82..42c8211ab8 100644 --- a/src/app/api/services/unit.service.ts +++ b/src/app/api/services/unit.service.ts @@ -53,7 +53,8 @@ export class UnitService extends CachedEntityService { }, }, { - keys: 'unitRoles', + // keys: 'unitRoles', + keys: 'staff', toEntityOp: (data, key, entity) => { const unitRoleService = AppInjector.get(UnitRoleService); // Add staff diff --git a/src/app/common/common.coffee b/src/app/common/common.coffee index f330d8f6ac..6710f40ec4 100644 --- a/src/app/common/common.coffee +++ b/src/app/common/common.coffee @@ -2,6 +2,5 @@ angular.module("doubtfire.common", [ 'doubtfire.common.services' 'doubtfire.common.filters' 'doubtfire.common.modals' - 'doubtfire.common.file-uploader' 'doubtfire.common.content-editable' ]) diff --git a/src/app/common/file-uploader/file-uploader.coffee b/src/app/common/file-uploader/file-uploader.coffee deleted file mode 100644 index c492dac115..0000000000 --- a/src/app/common/file-uploader/file-uploader.coffee +++ /dev/null @@ -1,298 +0,0 @@ -angular.module('doubtfire.common.file-uploader', ["ngFileUpload"]) - -.directive 'fileUploader', -> - restrict: 'E' - replace: true - templateUrl: 'common/file-uploader/file-uploader.tpl.html' - scope: - # Files map a key (file name to be uploaded) to a value (containing a - # a display name, and the type of file that is to be accepted, where - # type is one of [document, csv, archive, code, image] - # E.g.: - # { file0: { name: 'Silly Name Code', type: 'code' }, - # fileX: { name: 'Silly name Shot', type: 'image' } ... } - files: '=' - # URL to where image is to be uploaded - url: '=' - # Optional HTTP method used to post data (defaults to POST) - method: '@' - # Other payload data to pass in the upload - # E.g.: - # { unit_id: 10, other: { key: data, with: [array, of, stuff] } ... } - payload: '=?' - # Optional function to notify just prior to upload, enables injection of payload for example - onBeforeUpload: '=?' - # Optional function to perform on success (with one response parameter) - onSuccess: '=?' - # Optional function to perform on failure (with one response parameter) - onFailure: '=?' - # Optional function to perform when the upload is successful and about - # to go back into its default state - onComplete: '=?' - # This value is bound to whether or not the uploader is currently uploading - isUploading: '=?' - # This value is bound to whether or not the uploader is ready to upload - isReady: '=?' - # Shows the names of files to be uploaded (defaults to true) - showName: '=?' - # Shows initially as button - asButton: '=?' - # Exposed files that are in the zone - filesSelected: '=?' - # Whether we have one or many drop zones (default is false) - singleDropZone: '=?' - # Whether or not we show the upload button or do we hide it allowing an - # external trigger to upload (default is true) - showUploadButton: '=?' - # Sets this scope variable to a function that can then be triggered externally - # from outside the scope - initiateUpload: '=?' - # What happens when we click cancel on failure - onClickFailureCancel: '=?' - # Whether we should reset after upload - resetAfterUpload: '=?' - controller: ($scope, $timeout, newUserService) -> - # - # Accepted upload types with associated data - # - ACCEPTED_TYPES = - document: - extensions: ['pdf', 'ps'] - icon: 'fa-file-pdf-o' - name: 'PDF' - csv: - extensions: ['csv','xls','xlsx'] - icon: 'fa-file-excel-o' - name: 'CSV' - code: - extensions: ['pas', 'cpp', 'c', 'cs', 'csv', 'h', 'hpp', 'java', 'py', 'js', 'html', 'coffee', 'rb', 'css', - 'scss', 'yaml', 'yml', 'xml', 'json', 'ts', 'r', 'rmd', 'rnw', 'rhtml', 'rpres', 'tex', - 'vb', 'sql', 'txt', 'md', 'jack', 'hack', 'asm', 'hdl', 'tst', 'out', 'cmp', 'vm', 'sh', 'bat', - 'dat', 'ipynb', 'pml', 'vue'] - icon: 'fa-file-code-o' - name: 'code' - image: - extensions: ['png', 'bmp', 'tiff', 'tif', 'jpeg', 'jpg', 'gif'] - name: 'image' - icon: 'fa-file-image-o' - zip: - extensions: ['zip', 'tar.gz', 'tar'] - name: 'archive' - icon: 'fa-file-zip-o' - - # - # Error handling; check if empty files - # - throw Error "No files provided to uploader" if $scope.files?.length is 0 - - # - # Whether or not clearEnqueuedFiles is enabled - # - $scope.clearEnqueuedUpload = (upload) -> - upload.model = null - refreshShownUploadZones() - - # - # Default showName - # - $scope.showName ?= true - - # - # Default singleDropZone - # - $scope.singleDropZone ?= false - - # - # Default asButton - # - $scope.asButton ?= false - - # - # Only initially show uploader if not presenting as button - # - $scope.showUploader = !$scope.asButton - - # - # Default show upload button - # - $scope.showUploadButton ?= true - - # - # Default resetAfterUpload to true - # - $scope.resetAfterUpload ?= true - - # - # When a file is dropped, if there has been rejected files - # warn the user that that file is not okay - # - checkForError = (upload) -> - if upload.rejects?.length > 0 - upload.display.error = yes - upload.rejects = null - $timeout (-> upload.display.error = no), 4000 - return true - false - - # Called when the model has changed - $scope.modelChanged = (newFiles, upload) -> - return unless newFiles.length > 0 || upload.rejects.length > 0 - gotError = checkForError(upload) - unless gotError - $scope.filesSelected = _.flatten(_.map($scope.uploadZones, 'model')) - if $scope.singleDropZone - $scope.selectedFiles = $scope.uploadZones - refreshShownUploadZones() - - # - # Will refresh which shown drop zones are shown - # Only changes if showing one drop zone - # - refreshShownUploadZones = -> - if $scope.singleDropZone - # Find the first-most empty model in each zone - firstEmptyZone = _.find($scope.uploadZones, (zone) -> !zone.model? || zone.model.length == 0) - if firstEmptyZone? - $scope.shownUploadZones = [firstEmptyZone] - else - $scope.shownUploadZones = [] - - # - # Whether or not drop is supported by this browser - assume - # true initially, but the drop zone will alter this - # - $scope.dropSupported = true - - # - # Data required for each upload zone - # - createUploadZones = (files) -> - zones = _.map(files, (uploadData, uploadName) -> - type = uploadData.type - typeData = ACCEPTED_TYPES[type] - # No typeData found? - unless typeData? - throw Error "Invalid type provided to File Uploader #{type}" - zone = - name: uploadName - model: null - accept: "'." + typeData.extensions.join(',.') + "'" - # Rejected files - rejects: null - display: - name: uploadData.name - # Font awesome supports PDF (from Document), - # CSV, Code and Image icons - icon: typeData.icon - type: typeData.name - # Whether or not a reject error is shown - error: false - zone - ) - # Remove all but the active drop zone - if $scope.singleDropZone - $scope.shownUploadZones = [_.first(zones)] - else - $scope.shownUploadZones = zones - $scope.uploadZones = zones - createUploadZones($scope.files) - - # - # Watch for changes in the files, and recreate the zones when - # they do change - # - $scope.$watch 'files', (files, oldFiles) -> - createUploadZones(files) - - # - # Checks if okay to upload (i.e., file models exist for each drop zone) - # - $scope.readyToUpload = -> - $scope.isReady = _.compact(_.flatten (upload.model for upload in $scope.uploadZones)).length is _.keys($scope.files).length - - # - # Resets the uploader and call it - # - $scope.resetUploader = -> - # No upload info and we're not uploading - $scope.uploadingInfo = null - $scope.isUploading = false - $scope.showUploader = !$scope.asButton - for upload in $scope.uploadZones - $scope.clearEnqueuedUpload(upload) - $scope.resetUploader() - - # - # Override on click failure cancel if not set to just reset uploader - # - $scope.onClickFailureCancel ?= $scope.resetUploader - - - # - # Initiates the upload - # - $scope.initiateUpload = -> - return unless $scope.readyToUpload() - $scope.onBeforeUpload?() - - xhr = new XMLHttpRequest() - form = new FormData() - # Append data - files = ({ name: zone.name; data: zone.model[0] } for zone in $scope.uploadZones) - form.append file.name, file.data for file in files - # Append payload - payload = ({ key: k; value: v } for k, v of $scope.payload) - for payloadItem in payload - payloadItem.value = JSON.stringify(payloadItem.value) if _.isObject payloadItem.value - form.append payloadItem.key, payloadItem.value - # Set the percent - $scope.uploadingInfo = - progress: 5 - success: null - error: null - complete: false - $scope.isUploading = true - # Callbacks - xhr.onreadystatechange = -> - if xhr.readyState is 4 - $timeout (-> - # Upload is now complete - $scope.uploadingInfo.complete = true - response = null - try - response = JSON.parse xhr.responseText - catch e - if xhr.status is 0 - response = { error: 'Could not connect to the Doubtfire server' } - else - response = xhr.responseText - # Success (20x success range) - if xhr.status >= 200 and xhr.status < 300 - $scope.onSuccess?(response) - $scope.uploadingInfo.success = true - $timeout((-> - $scope.onComplete?() - if $scope.resetAfterUpload - $scope.resetUploader() - ), 2500) - # Fail - else - $scope.onFailure?(response) - $scope.uploadingInfo.success = false - $scope.uploadingInfo.error = response.error or "Unknown error" - $scope.$apply() - ), 2000 - xhr.upload.onprogress = (event) -> - $scope.uploadingInfo.progress = parseInt(100.0 * event.position / event.totalSize) - $scope.$apply() - # Default the method to POST if it was not defined - $scope.method = 'POST' unless $scope.method? - - # Send it - xhr.open $scope.method, $scope.url, true - - # Add auth details - xhr.setRequestHeader('Auth-Token', newUserService.currentUser.authenticationToken) - xhr.setRequestHeader('Username', newUserService.currentUser.username) - - xhr.send form diff --git a/src/app/common/file-uploader/file-uploader.component.html b/src/app/common/file-uploader/file-uploader.component.html new file mode 100644 index 0000000000..54dc30d842 --- /dev/null +++ b/src/app/common/file-uploader/file-uploader.component.html @@ -0,0 +1,164 @@ + + + @if (!showUploader) { + + } + +
+ @if (showUploader && uploadingInfo === null && shownUploadZones.length) { +
+ @for (upload of shownUploadZones; track upload) { + @if (!singleDropZone && showName) { +
+ {{ uploadZones.length === 1 ? '' : $index + 1 + ' - ' }} + {{ upload.display.name }} +
+ } + + @if (singleDropZone && showName) { +
Select {{ upload.display.name }}
+ } + +
+ + +
+ @if (!upload.display.error) { + {{ upload.display.icon }} + @if (dropSupported) { +

+ Drop {{ upload.display.type }} file here
or click to select +

+ } @else { +

Click to select {{ upload.display.type }} file

+ } + } @else { +
+ block +

Invalid file provided

+ Accepted: {{ upload.accept.split(',').join(', ') }} +
+ } +
+
+ + Browse for file +
+
+ + @if (!singleDropZone && upload.model?.length > 0) { +
+ {{ upload.display.icon }} + {{ upload.model[0].name }} + +
+ } + } +
+ } + + @if (showUploader && singleDropZone && uploadingInfo === null) { +
+
Upload Summary
+ + @for (upload of uploadZones; track upload) { +
+
+ {{ upload.display.icon }} + {{ upload.display.name }} +
+ + @if (upload.model?.length > 0) { + {{ upload.model[0].name }} + + } @else { + File Pending + } +
+ } +
+ } +
+ + @if (showUploader && !isUploading) { +
+ @if (showUploadButton && readyToUpload() && uploadingInfo === null) { + + } + + @if (asButton) { + + } +
+ } + + @if (showUploader && readyToUpload() && isUploading) { + @if (!uploadingInfo?.complete) { +
+
+ @for (upload of uploadZones; track upload) { + {{ upload.display.icon }} + } + arrow_right_alt + +
+ + + +
+ } + + @if (uploadingInfo?.complete) { +
+
+ + {{ uploadingInfo.success ? 'check_circle' : 'cancel' }} + + + + File Upload {{ uploadingInfo.success ? 'Successful' : 'Failed' }} + +
+ + @if (!uploadingInfo.success) { +
+
+

Error Message: {{ uploadingInfo.error }}

+ +
+ + +
+
+ } +
+ } + } +
+
diff --git a/src/app/common/file-uploader/file-uploader.component.scss b/src/app/common/file-uploader/file-uploader.component.scss new file mode 100644 index 0000000000..6809e06815 --- /dev/null +++ b/src/app/common/file-uploader/file-uploader.component.scss @@ -0,0 +1,16 @@ +file-upload .mat-icon, +.complete .mat-icon { + font-size: 50px; + height: 50px; + width: 50px; +} + +.uploading .mat-icon { + font-size: 75px; + height: 75px; + width: 75px; +} + +::ng-deep file-upload .upload-input { + width: 100%; +} diff --git a/src/app/common/file-uploader/file-uploader.component.ts b/src/app/common/file-uploader/file-uploader.component.ts new file mode 100644 index 0000000000..85eb85e792 --- /dev/null +++ b/src/app/common/file-uploader/file-uploader.component.ts @@ -0,0 +1,326 @@ +import { + Component, + EventEmitter, + Input, + OnChanges, + OnInit, + Output, + SimpleChanges, +} from '@angular/core'; +import {FileUploadControl} from '@iplab/ngx-file-upload'; +import {UserService} from 'src/app/api/services/user.service'; +import {DoubtfireConstants} from 'src/app/config/constants/doubtfire-constants'; + +interface FileData { + name: string; + type: string; +} + +interface UploadDisplay { + name: string; + icon: string; + type: string; + error: boolean; +} +interface UploadZone { + name: string; + model: File[]; + accept: string; + accepts: string[]; + rejects: string[]; + display: UploadDisplay; +} + +interface UploadingInfo { + progress: number; + success: boolean; + error: string; + complete: boolean; +} + +export const ACCEPTED_TYPES = { + document: { + extensions: ['pdf', 'ps'], + // icon: 'fa-file-pdf-o', + icon: 'article_outlined', + name: 'PDF', + }, + csv: { + extensions: ['csv', 'xls', 'xlsx'], + icon: 'insert_chart_outlined', + name: 'CSV', + }, + code: { + // prettier-ignore + extensions: [ + 'pas', 'cpp', 'c', 'cs', 'csv', 'h', 'hpp', 'java', 'py', 'js', 'html', 'coffee', 'rb', 'css', + 'scss', 'yaml', 'yml', 'xml', 'json', 'ts', 'r', 'rmd', 'rnw', 'rhtml', 'rpres', 'tex', + 'vb', 'sql', 'txt', 'md', 'jack', 'hack', 'asm', 'hdl', 'tst', 'out', 'cmp', 'vm', 'sh', 'bat', + 'dat', 'ipynb', 'pml', 'vue' + ], + // icon: 'fa-file-code-o', + // icon: 'code', + icon: 'integration_instructions_outlined', + name: 'code', + }, + image: { + extensions: ['png', 'bmp', 'tiff', 'tif', 'jpeg', 'jpg', 'gif'], + // icon: 'fa-file-image-o', + icon: 'image_outlined', + name: 'image', + }, + zip: { + extensions: ['zip', 'tar.gz', 'tar'], + // icon: 'fa-file-zip-o', + icon: 'zip_outlined', + name: 'archive', + }, +} as const; + +@Component({ + selector: 'f-file-uploader', + templateUrl: './file-uploader.component.html', + styleUrls: ['./file-uploader.component.scss'], +}) +export class FileUploaderComponent implements OnInit, OnChanges { + @Input() files: FileData[]; + @Input() url: string; + @Input() method = 'POST'; + @Input() payload?: unknown; + + @Input() onBeforeUpload?: () => void; + @Input() onSuccess?: (response) => void; + @Input() onFailure?: (response) => void; + @Input() onComplete?: () => void; + @Input() onClickFailureCancel?: () => void; + + @Input() isUploading: boolean; + @Input() isReady: boolean; + @Input() showName: boolean = true; + @Input() asButton: boolean = false; + @Input() singleDropZone: boolean = false; + @Input() showUploadButton: boolean = true; + @Input() resetAfterUpload: boolean = true; + + @Input() initiateUpload?: () => void; + + // HACK: workaround for TypeScript -> Coffeescript communication + // Once all parent components such as upload-submission-modal are migrated.. + // .. these *wont* be necessary anymore + // Parent components should declare the file-uploader using @ViewChild() and directly call initiateUpload() + @Output() isReadyChange = new EventEmitter(); + @Output() uploadReady = new EventEmitter<() => void>(); + + public readonly ACCEPTED_TYPES = ACCEPTED_TYPES; + + public showUploader: boolean = false; + public uploadingInfo: UploadingInfo = null; + + public fileUploadControl = new FileUploadControl({listVisible: false, discardInvalid: true}); + public shownUploadZones: UploadZone[] = []; + public uploadZones: UploadZone[] = []; + public dropSupported: boolean = true; + + constructor( + private userService: UserService, + private constants: DoubtfireConstants, + ) {} + + private externalName: string = 'OnTrack'; + + ngOnInit(): void { + this.showUploader = !this.asButton; + this.createUploadZones(this.files); + + this.fileUploadControl.valueChanges.subscribe(() => { + setTimeout(() => { + this.validateFiles(); + }); + }); + + this.uploadReady.emit(this.initiateUploadInternal.bind(this)); + + if (!this.onClickFailureCancel) { + this.onClickFailureCancel = this.resetUploader; + } + + this.resetUploader(); + + this.constants.ExternalName.subscribe((name) => { + this.externalName = name; + }); + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes['files']) { + this.createUploadZones(changes.files.currentValue); + } + } + + public backToUpload() { + this.isUploading = false; + this.uploadingInfo = null; + } + + validateFiles() { + for (const upload of this.shownUploadZones) { + if (upload.model?.length) { + const name: string = upload.model[0].name.toLowerCase(); + const accepts: string[] = upload.accepts.map((ext: string) => ext.toLowerCase()); + const valid = accepts.some((ext) => name.endsWith(ext)); + if (!valid) { + upload.model = null; + upload.display.error = true; + setTimeout(() => { + upload.display.error = null; + }, 5000); + } + } + } + this.refreshShownUploadZones(); + } + + clearEnqueuedUpload(upload: UploadZone) { + upload.model = null; + this.refreshShownUploadZones(); + } + + readyToUpload(): boolean { + const allSelected = this.uploadZones.every((zone) => zone.model?.length); + this.updateReadyState(allSelected); + return allSelected; + } + + updateReadyState(ready: boolean) { + this.isReady = ready; + this.isReadyChange.emit(ready); + } + + resetUploader() { + this.uploadingInfo = null; + this.isUploading = false; + this.showUploader = !this.asButton; + for (const upload of this.uploadZones) { + this.clearEnqueuedUpload(upload); + } + } + + initiateUploadInternal() { + if (!this.readyToUpload()) { + return; + } + if (this.onBeforeUpload) { + this.onBeforeUpload(); + } + + this.uploadingInfo = { + progress: 5, + success: null, + error: null, + complete: false, + }; + + this.isUploading = true; + + const xhr = new XMLHttpRequest(); + const form = new FormData(); + + // Append files + for (const zone of this.uploadZones) { + if (zone.model?.length) { + form.append(zone.name, zone.model[0]); + } + } + + // Append payload + if (this.payload) { + for (const [key, value] of Object.entries(this.payload)) { + let v = value; + if (typeof v === 'object') v = JSON.stringify(v); + form.append(key, v); + } + } + + xhr.upload.onprogress = (event) => { + if (event.total) { + this.uploadingInfo.progress = Math.floor((event.loaded / event.total) * 100); + } + }; + + xhr.onreadystatechange = () => { + if (xhr.readyState === 4) { + setTimeout(() => { + this.uploadingInfo.complete = true; + let response = null; + try { + response = JSON.parse(xhr.responseText); + } catch (e) { + console.error(e); + if (xhr.status === 0) { + response = {error: `Could not connect to ${this.externalName} the server`}; + } else { + response = xhr.responseText; + } + } + + if (xhr.status >= 200 && xhr.status < 300) { + this.onSuccess?.(response); + this.uploadingInfo.success = true; + setTimeout(() => { + this.onComplete?.(); + if (this.resetAfterUpload) { + this.resetUploader(); + } + }, 2500); + } else { + this.onFailure?.(response); + this.uploadingInfo.success = false; + this.uploadingInfo.error = (response?.error ?? 'Unknown error') as string; + } + }, 2000); + } + }; + const method = this.method ?? 'POST'; + xhr.open(method, this.url, true); + + xhr.setRequestHeader('Auth-Token', this.userService.currentUser.authenticationToken); + xhr.setRequestHeader('Username', this.userService.currentUser.username); + + xhr.send(form); + } + + // onClickFailureCancelInternal() { + // console.log('onClickFailureCancelInternal'); + // } + + refreshShownUploadZones = () => { + if (this.singleDropZone) { + const firstEmpty = this.uploadZones.find((z) => !z.model || z.model.length === 0); + this.shownUploadZones = firstEmpty ? [firstEmpty] : []; + } + }; + + createUploadZones(files: FileData[]) { + const zones = Object.entries(files).map(([uploadName, uploadData]) => { + const typeData = ACCEPTED_TYPES[uploadData.type]; + if (!typeData) throw new Error(`Invalid type provided to File Uploader ${uploadData.type}`); + + return { + name: uploadName, + model: null, + accept: `.${typeData.extensions.join(',.')}`, + accepts: typeData.extensions, + rejects: null, + display: { + name: uploadData.name, + icon: typeData.icon, + type: typeData.name, + error: false, + }, + }; + }); + + this.shownUploadZones = this.singleDropZone ? [zones[0]] : zones; + this.uploadZones = zones; + } +} diff --git a/src/app/common/file-uploader/file-uploader.scss b/src/app/common/file-uploader/file-uploader.scss deleted file mode 100644 index 4ca5cb6988..0000000000 --- a/src/app/common/file-uploader/file-uploader.scss +++ /dev/null @@ -1,139 +0,0 @@ -.file-uploader { - display: block; - // Add some margin like a

- margin: 2.5em 0; - - // Colors to make it easy to understand - $hover-color: $brand-primary; - $accept-color: $brand-success; - $reject-color: $brand-danger; - - // Extra additional icons - $ban-icon: $fa-var-ban; - $download-icon: $fa-var-download; - - .upload-commit-actions { - margin-top: 1em; - .btn-upload { - margin-right: 1.5ex; - } - } - - .well.drop { - border: 2px #bbb dotted; - font-size: larger; - font-weight: bold; - color: #aaa; - text-align: center; - &, i { - @include transition(all 0.25s ease); - } - p small { - display: block; - } - &:hover { - cursor: pointer; - border-color: $hover-color; - color: $hover-color; - p small { - text-decoration: underline; - } - } - } - - // Wells which have file over - .well.drop.file-over { - cursor: copy; - border-color: $accept-color; - color: $accept-color; - // Switch the icon over - p.fa::before { - content: $download-icon; - } - } - // Rejected file over - .well.drop.file-rejected { - border-color: $reject-color; - color: $reject-color; - // Switch the icon over - p.fa::before { - content: $ban-icon; - } - } - - // File header - .selected-files { - &:not(.list-group) { - display: inline-block; - } - .selected-file { - display: block; - font-size: 1.2em; - i.file-type { - margin-right: 1ex; - font-size: 1.2em; - } - &.highlight { - animation: highlight-selected-file-animation; - animation-duration: 0.75s; - @keyframes highlight-selected-file-animation { - 0% { background: rgba(33, 150, 243, 0.4); } - 0% { box-shadow: 0 0 6px rgba(33, 150, 243, 1); } - 100% { box-shadow: 0 0 0px rgba(255, 255, 255, 0); } - } - } - } - } - a.clear-upload { - margin-left: 1ex; - &:hover i { - font-size: 1.15em; - color: $reject-color; - } - display: inline-block; - } - // Upload area/result - .upload-area { - .progress-area { - .progress { - margin-bottom: 0; - } - .icons { - width: 100%; - display: flex; - justify-content: center; - } - i.fa-arrow-right { - @include animation-wobble(); - } - i { - flex-basis: auto !important; - margin-right: 1ex; - font-size: 2em; - margin-bottom: 0.5em; - } - } - .result-area { - .result-text { - margin-bottom: 0; - display: flex; - justify-content: center; - align-items: center; - min-height: 34px; - } - i { - font-size: 2em; - @include animation-grow; - margin-right: 1ex; - } - .retry-options { - font-weight: bolder; - font-size: 1.2em; - a:first-child { - display: inline-block; - margin-right: 2ex; - } - } - } - } -} diff --git a/src/app/common/file-uploader/file-uploader.tpl.html b/src/app/common/file-uploader/file-uploader.tpl.html deleted file mode 100644 index 368f53380c..0000000000 --- a/src/app/common/file-uploader/file-uploader.tpl.html +++ /dev/null @@ -1,104 +0,0 @@ -

-
- -
-
-
-
- {{uploadZones.length == 1 ? '' : $index + 1 + ' -'}} {{upload.display.name}} -
-
- Select {{upload.display.name}} -
-
-

-

- Invalid file provided - Accepted files: {{upload.accept.split(',').join(', ')}} -

-

- Drop {{upload.display.type}} file here - or click to select one -

-

- Click to select {{upload.display.type}} file -

-
-
- - - {{upload.model[0].name}} - - - - -
-
-
-
Upload Summary
-
-
-
- - {{upload.display.name}} -
-
- {{upload.model[0].name}} - File Pending - - - -
-
-
-
-
-
- - -
-
-
-
- - - -
- -
-
-

- - File Upload {{uploadingInfo.success === true ? 'Successful' : 'Failed'}} -

-
-
-

- Error Message: - {{uploadingInfo.error}} -

-

- Retry Upload - Cancel -

-
-
-
-
diff --git a/src/app/common/header/header.component.ts b/src/app/common/header/header.component.ts index b5e1960c8a..842287b92e 100644 --- a/src/app/common/header/header.component.ts +++ b/src/app/common/header/header.component.ts @@ -104,8 +104,7 @@ export class HeaderComponent implements OnInit, OnDestroy { } isUniqueRole = (unit) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const units = this.unitRoles.filter((role: any) => role.unit.id === unit.unit.id); + const units = this.unitRoles.filter((role: UnitRole) => role.unit?.id === unit.unit?.id); return units.length == 1 || unit.role == 'Tutor'; }; diff --git a/src/app/common/modals/confirmation-modal/confirmation-modal.coffee b/src/app/common/modals/confirmation-modal/confirmation-modal.coffee deleted file mode 100644 index c7999098cf..0000000000 --- a/src/app/common/modals/confirmation-modal/confirmation-modal.coffee +++ /dev/null @@ -1,35 +0,0 @@ -angular.module("doubtfire.common.modals.confirmation-modal", []) - -.factory("ConfirmationModal", ($modal) -> - ConfirmationModal = {} - - # - # Show a modal asking the user to confirm their indicated action. - # - ConfirmationModal.show = (title, message, action) -> - modalInstance = $modal.open - templateUrl: 'common/modals/confirmation-modal/confirmation-modal.tpl.html' - controller: 'ConfirmationModalCtrl' - resolve: - title: -> title - message: -> message - action: -> action - - ConfirmationModal -) - -# -# Controller for confirmation modal -# -.controller('ConfirmationModalCtrl', ($scope, $modalInstance, title, message, action, alertService) -> - $scope.title = title - $scope.message = message - - $scope.confirmAction = -> - action() - $modalInstance.dismiss() - - $scope.cancelAction = -> - alertService.message "#{title} action cancelled", 3000 - $modalInstance.dismiss() -) diff --git a/src/app/common/modals/confirmation-modal/confirmation-modal.component.html b/src/app/common/modals/confirmation-modal/confirmation-modal.component.html new file mode 100644 index 0000000000..3ec07fc729 --- /dev/null +++ b/src/app/common/modals/confirmation-modal/confirmation-modal.component.html @@ -0,0 +1,20 @@ +
+

+
+ +
+
{{ title }}
+ Please confirm that you want to perform this action. +
+
+

+ + {{ message }} + + + + + +
diff --git a/src/app/projects/states/dashboard/directives/progress-dashboard/progress-dashboard.scss b/src/app/common/modals/confirmation-modal/confirmation-modal.component.scss similarity index 100% rename from src/app/projects/states/dashboard/directives/progress-dashboard/progress-dashboard.scss rename to src/app/common/modals/confirmation-modal/confirmation-modal.component.scss diff --git a/src/app/common/modals/confirmation-modal/confirmation-modal.component.ts b/src/app/common/modals/confirmation-modal/confirmation-modal.component.ts new file mode 100644 index 0000000000..f9506e5b8b --- /dev/null +++ b/src/app/common/modals/confirmation-modal/confirmation-modal.component.ts @@ -0,0 +1,47 @@ +import {Component, OnInit, Input, Inject} from '@angular/core'; +import {AlertService} from '../../services/alert.service'; +import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog'; + +export interface ConfirmationModalData { + title: string; + message: string; + action?: any; +} + +@Component({ + selector: 'confirmation-modal', + templateUrl: './confirmation-modal.component.html', + styleUrls: ['./confirmation-modal.component.scss'], +}) +export class ConfirmationModalComponent implements OnInit { + @Input() title: string; + @Input() message: string; + @Input() action: () => void; + + constructor( + @Inject(AlertService) private alertService: AlertService, + @Inject(MAT_DIALOG_DATA) public data: ConfirmationModalData, + + public dialogRef: MatDialogRef, + ) {} + + ngOnInit(): void { + this.title = this.data.title; + this.message = this.data.message; + this.action = this.data.action; + } + + public confirmAction() { + if (typeof this.action === 'function') { + this.action(); + } else { + this.alertService.error(`${this.title} action failed.`); + } + this.dialogRef.close(); + } + + public cancelAction() { + this.alertService.success(`${this.title} action cancelled.`); + this.dialogRef.close(); + } +} diff --git a/src/app/common/modals/confirmation-modal/confirmation-modal.scss b/src/app/common/modals/confirmation-modal/confirmation-modal.scss deleted file mode 100644 index f30b0e345c..0000000000 --- a/src/app/common/modals/confirmation-modal/confirmation-modal.scss +++ /dev/null @@ -1,3 +0,0 @@ -.confirmation-modal .modal-body { - font-size: 1.5em; -} diff --git a/src/app/common/modals/confirmation-modal/confirmation-modal.service.ts b/src/app/common/modals/confirmation-modal/confirmation-modal.service.ts new file mode 100644 index 0000000000..6257277eff --- /dev/null +++ b/src/app/common/modals/confirmation-modal/confirmation-modal.service.ts @@ -0,0 +1,26 @@ +import {Injectable} from '@angular/core'; +import {MatDialog} from '@angular/material/dialog'; +import {ConfirmationModalComponent, ConfirmationModalData} from './confirmation-modal.component'; + +@Injectable({ + providedIn: 'root', +}) +export class ConfirmationModalService { + constructor(public dialog: MatDialog) {} + + public show(title: string, message: string, action?: any) { + this.dialog.open( + ConfirmationModalComponent, + { + data: { + title, + message, + action, + }, + position: {top: '2.5%'}, + width: '100%', + maxWidth: '650px', + }, + ); + } +} diff --git a/src/app/common/modals/confirmation-modal/confirmation-modal.tpl.html b/src/app/common/modals/confirmation-modal/confirmation-modal.tpl.html deleted file mode 100644 index 4bd87f6868..0000000000 --- a/src/app/common/modals/confirmation-modal/confirmation-modal.tpl.html +++ /dev/null @@ -1,22 +0,0 @@ -
- - - -
diff --git a/src/app/common/modals/csv-result-modal/csv-upload-modal.tpl.html b/src/app/common/modals/csv-result-modal/csv-upload-modal.tpl.html index 6112f50c7e..5f94b642fe 100644 --- a/src/app/common/modals/csv-result-modal/csv-upload-modal.tpl.html +++ b/src/app/common/modals/csv-result-modal/csv-upload-modal.tpl.html @@ -7,7 +7,7 @@ +
diff --git a/src/app/projects/states/portfolio/directives/directives.coffee b/src/app/projects/states/portfolio/directives/directives.coffee index 8ce9be0be3..239280567d 100644 --- a/src/app/projects/states/portfolio/directives/directives.coffee +++ b/src/app/projects/states/portfolio/directives/directives.coffee @@ -1,8 +1,4 @@ angular.module('doubtfire.projects.states.portfolio.directives', [ - 'doubtfire.projects.states.portfolio.directives.portfolio-add-extra-files-step' - 'doubtfire.projects.states.portfolio.directives.portfolio-grade-select-step' - 'doubtfire.projects.states.portfolio.directives.portfolio-learning-summary-report-step' 'doubtfire.projects.states.portfolio.directives.portfolio-review-step' 'doubtfire.projects.states.portfolio.directives.portfolio-tasks-step' - 'doubtfire.projects.states.portfolio.directives.portfolio-welcome-step' ]) diff --git a/src/app/projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.coffee b/src/app/projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.coffee deleted file mode 100644 index f62984ff63..0000000000 --- a/src/app/projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.coffee +++ /dev/null @@ -1,25 +0,0 @@ -angular.module('doubtfire.projects.states.portfolio.directives.portfolio-add-extra-files-step', []) - -# -# Allow students to add additional files to the end of their portfolio -# They can choose any file they want to upload -# -.directive('portfolioAddExtraFilesStep', -> - restrict: 'E' - replace: true - templateUrl: 'projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.tpl.html' - controller: ($scope) -> - otherFileFileUploadData = (type) -> - type: { - file0: { name: "Other", type: type } - }, - payload: { - name: "Other" - kind: type - } - - $scope.uploadType = 'document' - $scope.$watch 'uploadType', (newType) -> - return unless newType? - $scope.uploadFileData = otherFileFileUploadData newType -) diff --git a/src/app/projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.component.html b/src/app/projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.component.html new file mode 100644 index 0000000000..53707b6891 --- /dev/null +++ b/src/app/projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.component.html @@ -0,0 +1,51 @@ + + + Upload Other Files + + +

+ Now is your chance to upload any extra files to include in your portfolio. They'll appear at + the very top of your portfolio, before your tasks. +

+
+
    + @for (file of extraFiles; track file) { +
  1. +
    + {{ icons[file.kind] }} + {{ file.name }} +
    + +
  2. + } @empty { +

    If you do not have any files to add, you can skip this step.

    + } +
+
+
+ + Select type of file: + + Document File + Code File + Image File + + +
+ + +
+ + + + +
diff --git a/src/app/projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.component.scss b/src/app/projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.component.ts b/src/app/projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.component.ts new file mode 100644 index 0000000000..93b49f47a1 --- /dev/null +++ b/src/app/projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.component.ts @@ -0,0 +1,96 @@ +import {Component, Injector, Input, OnInit} from '@angular/core'; +import {MatSelectChange} from '@angular/material/select'; +import {Project} from 'src/app/api/models/project'; +import {AlertService} from 'src/app/common/services/alert.service'; + +@Component({ + selector: 'f-portfolio-add-extra-files-step', + templateUrl: 'portfolio-add-extra-files-step.component.html', + styleUrls: ['portfolio-add-extra-files-step.component.scss'], +}) +export class PortfolioAddExtraFilesStepComponent implements OnInit { + @Input() project: Project; + + public uploadType: 'document' | 'code' | 'image' = 'document'; + + public isUploading: boolean; + + public uploadFileType = { + file0: { + name: 'Other', + type: 'document', + }, + }; + + public uploadFilePayload = { + name: 'Other', + kind: 'document', + }; + + constructor( + private injector: Injector, + private alertService: AlertService, + ) {} + + public readonly icons = { + document: 'article_outlined', + code: 'integration_instructions_outlined', + image: 'image_outlined', + zip: 'zip_outlined', + }; + + ngOnInit(): void { + this.uploadType = 'document'; + + this.uploadFileType = { + file0: { + name: 'Other', + type: 'document', + }, + }; + + this.uploadFilePayload = { + name: 'Other', + kind: 'document', + }; + } + onTypeChange(event: MatSelectChange) { + console.log('on type change', event); + this.uploadFileType = { + file0: { + name: 'Other', + type: event.value, + }, + }; + + this.uploadFilePayload = { + name: 'Other', + kind: event.value, + }; + } + + public get extraFiles() { + // If file.idx === 0, then it's the Learning Summary Report, so we ignore it here + return this.project?.portfolioFiles.filter((file) => file.idx !== 0); + } + + deleteFileFromPortfolio(file: {idx: number; kind: string; name: string}) { + this.project.deleteFileFromPortfolio(file).subscribe({ + next: () => { + this.alertService.success('Succesfully delete file', 3000); + }, + error: (error) => { + this.alertService.error(`Failed to delete file: ${error}`, 6000); + }, + }); + } + + // TODO: remove this once parent component is migrated + advanceActiveTab(index: 1 | -1) { + this.injector.get('$scope').advanceActiveTab(index); + } + + addNewFilesToPortfolio(newFile: {kind: string; name: string; idx: number}) { + this.project.portfolioFiles.push(newFile); + } +} diff --git a/src/app/projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.scss b/src/app/projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.scss deleted file mode 100644 index 47c481a372..0000000000 --- a/src/app/projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.scss +++ /dev/null @@ -1,10 +0,0 @@ -.portfolio-add-extra-files-step { - a.clear-upload { - margin-left: 1ex; - &:hover i { - font-size: 1.15em; - color: $brand-danger; - } - display: inline-block; - } -} diff --git a/src/app/projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.tpl.html b/src/app/projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.tpl.html deleted file mode 100644 index 4de3b6f696..0000000000 --- a/src/app/projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.tpl.html +++ /dev/null @@ -1,58 +0,0 @@ -
-
-

{{activeTab.title}}

-
-
-

- Now is your chance to upload extra files to include in your portfolio. - These files will be added at to your portfolio before your selected tasks - from the previous step. -

-
-
-

No files to add?

-

If you do not have any files to add you can skip this step.

-
-
-

Extra file{{extraFiles().length > 1 ? 's' : ''}} added

-

- {{extraFiles().length > 1 ? 'The files you add will appear in the portfolio in the order shown below.' : ''}} - If you want to delete a file, click the cross beside the file's name. -

-
    -
  1. - {{file.name}} - - - -
  2. -
-
-
-
- -
- -
-
- - -
-
-
- -
diff --git a/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.coffee b/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.coffee deleted file mode 100644 index 3a7ee7f2a4..0000000000 --- a/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.coffee +++ /dev/null @@ -1,22 +0,0 @@ -angular.module('doubtfire.projects.states.portfolio.directives.portfolio-grade-select-step', []) - -# -# Allows students to select the target grade they are hoping -# to achieve with their portfolio -# -.directive('portfolioGradeSelectStep', -> - restrict: 'E' - replace: true - templateUrl: 'projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.tpl.html' - controller: ($scope, newProjectService, gradeService) -> - if ! $scope.project.submittedGrade - $scope.project.submittedGrade = 0 - $scope.grades = gradeService.gradeValues - $scope.gradeName = (grade) -> gradeService.grades[grade] - $scope.agreedToAssessmentCriteria = $scope.projectHasLearningSummaryReport() - $scope.chooseGrade = (idx) -> - $scope.project.submittedGrade = idx - newProjectService.update($scope.project).subscribe((project) -> - $scope.project.refreshBurndownChartData() - ) -) diff --git a/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.component.html b/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.component.html new file mode 100644 index 0000000000..ad2c4c4f0c --- /dev/null +++ b/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.component.html @@ -0,0 +1,96 @@ +
+ + + + +

Select Grade

+
+
+ + +

+ In preparing your portfolio, you need to undertake a self-assessment. Use the unit's + assessment criteria to determine the grade your portfolio should be awarded. +

+ + + + + + warning + Read the assessment criteria + + + + +

+ Make sure that you have reviewed the Assessment Criteria for the grade you are applying + for. Each grade will have a list of criteria that you can use to determine if you meet + the requirements to achieve that grade. +

+
+ + + + I have read the Assessment Criteria for this unit + + +
+ + + + @if (agreedToAssessmentCriteria) { + + + + Grade Application + + + + +

+ Select the grade you are applying for {{ unit.code }} + {{ unit.name }} below. +

+
+ + + + @for (grade of gradeValues; track grade) { + + + + } + + +

+ Make sure your Learning Summary Report justifies how your portfolio + demonstrates you have + met all unit learning outcomes to a {{ targetGrade }} level +

+
+
+ } +
+ + + + + + +
+
diff --git a/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.component.scss b/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.component.ts b/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.component.ts new file mode 100644 index 0000000000..cce8ee1748 --- /dev/null +++ b/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.component.ts @@ -0,0 +1,57 @@ +import {Component, Injector, Input} from '@angular/core'; +import {Project, Unit} from 'src/app/api/models/doubtfire-model'; +import {ProjectService} from 'src/app/api/services/project.service'; +import {GradeService} from 'src/app/common/services/grade.service'; + +@Component({ + selector: 'f-portfolio-grade-select-step', + templateUrl: 'portfolio-grade-select-step.component.html', + styleUrls: ['portfolio-grade-select-step.component.scss'], +}) +export class PortfolioGradeSelectStepComponent { + @Input() project: Project; + @Input() unit: Unit; + + public agreedToAssessmentCriteria: boolean = false; + + constructor( + private gradeService: GradeService, + private injector: Injector, + private projectService: ProjectService, + ) { + this.$scope = this.injector.get('$scope'); + } + + public get gradeValues() { + return this.gradeService.gradeValues; + } + + updateSubmittedGrade(newGrade: number): void { + const previousSubmittedGrade = this.project.submittedGrade; + this.project.submittedGrade = newGrade; + + this.projectService.update(this.project).subscribe( + (project) => { + project.refreshBurndownChartData?.(); + }, + (error) => { + this.project.submittedGrade = previousSubmittedGrade; + console.error('Error updating target grade:', error); + }, + ); + } + + // TODO: remove this once parent component has been migrated + private $scope: any; + goToNextStep(): void { + if (typeof this.$scope?.advanceActiveTab === 'function') { + this.$scope.advanceActiveTab(1); + } + } + + goToPreviousStep(): void { + if (typeof this.$scope?.advanceActiveTab === 'function') { + this.$scope.advanceActiveTab(-1); + } + } +} diff --git a/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.scss b/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.scss deleted file mode 100644 index bfc229c4b1..0000000000 --- a/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.scss +++ /dev/null @@ -1,10 +0,0 @@ -.project-portfolio-wizard .portfolio-grade-select-step { - .confirm-read-assessment-criteria { - font-size: 1.2em; - } - .select-the-grade { - .btn { - padding: 1em; - } - } -} diff --git a/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.tpl.html b/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.tpl.html deleted file mode 100644 index 097b685e35..0000000000 --- a/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.tpl.html +++ /dev/null @@ -1,60 +0,0 @@ -
-
-

Select Grade

-
-
-

- In preparing your portfolio, you need to undertake a self assessment. Use the unit's assessment criteria to - determine the grade your portfolio should be awarded. -

-
-
-

Read the assessment criteria

- Make sure that you have reviewed the Assessment Criteria for the grade you are applying for. Each grade will - have a list of criteria that you can use to determine if you meet the requirements to achieve that grade. -
-
- - -
-
- -
-
-

Grade Application

- Select the grade you are applying for {{unit.name}} below. -
-
-
- -
-

- Make sure your Learning Summary Report justifies how your portfolio demonstrates you have - met all unit learning outcomes to a {{gradeName(project.submittedGrade)}} level -

-
-
- -
- - -
diff --git a/src/app/projects/states/portfolio/directives/portfolio-learning-summary-report-step/portfolio-learning-summary-report-step.coffee b/src/app/projects/states/portfolio/directives/portfolio-learning-summary-report-step/portfolio-learning-summary-report-step.coffee deleted file mode 100644 index f2f27caaf7..0000000000 --- a/src/app/projects/states/portfolio/directives/portfolio-learning-summary-report-step/portfolio-learning-summary-report-step.coffee +++ /dev/null @@ -1,29 +0,0 @@ -angular.module('doubtfire.projects.states.portfolio.directives.portfolio-learning-summary-report-step', []) - -# -# Step to justify the portfolio with a Learning Summary Report -# -.directive('portfolioLearningSummaryReportStep', -> - restrict: 'E' - replace: true - templateUrl: 'projects/states/portfolio/directives/portfolio-learning-summary-report-step/portfolio-learning-summary-report-step.tpl.html' - controller: ($scope) -> - $scope.forceLSRSubmit = false - $scope.acceptUploadNewLearningSummary = false - - $scope.learningSummaryReportFileUploadData = { - type: { - file0: { name: "Learning Summary Report", type: "document" } - }, - payload: { - name: "LearningSummaryReport" # DO NOT MODIFY - case senstitive on API - kind: "document" - } - } - - $scope.addNewFile = (newFile) -> - $scope.addNewFilesToPortfolio(newFile) - $scope.projectHasDraftLearningSummaryReport = false - $scope.acceptUploadNewLearningSummary = false - $scope.forceLSRSubmit = false -) diff --git a/src/app/projects/states/portfolio/directives/portfolio-learning-summary-report-step/portfolio-learning-summary-report-step.component.html b/src/app/projects/states/portfolio/directives/portfolio-learning-summary-report-step/portfolio-learning-summary-report-step.component.html new file mode 100644 index 0000000000..3ef553e787 --- /dev/null +++ b/src/app/projects/states/portfolio/directives/portfolio-learning-summary-report-step/portfolio-learning-summary-report-step.component.html @@ -0,0 +1,91 @@ + + + Learning Summary Report + + +

+ Upload the Learning Summary Report, the primary porfolio document which justifies your desired + grade. +

+

+ Your Learning Summary Report is a + summary of what you have learnt in this unit. It consists of two sections: +

+ +
    +
  1. a self-assessment, and
  2. +
  3. your reflections on the unit.
  4. +
+ +

+ The self-assessment indicates how your portfolio aligns with the assessment + criteria, and which grade you are applying for. +

+ +

+ Your reflections are a personal comment on what you have learnt in the unit, + and how your knowledge and skills have developed. +

+ + @if ( + projectHasDraftLearningSummaryReport && !forceLSRSubmit && !acceptUploadNewLearningSummary + ) { +
+ + @if (draftTaskDefinitionWasUsed()) { +
+ Your learning summary report was automatically submitted from your + {{ unit.draftTaskDefinition.abbreviation }} {{ unit.draftTaskDefinition.name }} + submission. +
+ } + + + +
+ } +
+
+ warning + + Remember to provide a justification for why you believe you have achieved a + {{ targetGradeLabel }} in {{ unit.code }} {{ unit.name }}. +
+ +
+
+ + +
+ @if (!projectHasDraftLearningSummaryReport) { +
+ You're missing a Learning Summary Report. Upload one to continue. +
+ } + +
+
+
diff --git a/src/app/projects/states/portfolio/directives/portfolio-learning-summary-report-step/portfolio-learning-summary-report-step.component.scss b/src/app/projects/states/portfolio/directives/portfolio-learning-summary-report-step/portfolio-learning-summary-report-step.component.scss new file mode 100644 index 0000000000..fde6acc603 --- /dev/null +++ b/src/app/projects/states/portfolio/directives/portfolio-learning-summary-report-step/portfolio-learning-summary-report-step.component.scss @@ -0,0 +1,5 @@ +.submitted .mat-icon { + font-size: 50px; + width: 50px; + height: 50px; +} diff --git a/src/app/projects/states/portfolio/directives/portfolio-learning-summary-report-step/portfolio-learning-summary-report-step.component.ts b/src/app/projects/states/portfolio/directives/portfolio-learning-summary-report-step/portfolio-learning-summary-report-step.component.ts new file mode 100644 index 0000000000..2f7744af06 --- /dev/null +++ b/src/app/projects/states/portfolio/directives/portfolio-learning-summary-report-step/portfolio-learning-summary-report-step.component.ts @@ -0,0 +1,67 @@ +import {Component, Injector, Input} from '@angular/core'; +import {Project} from 'src/app/api/models/project'; +import {Unit} from 'src/app/api/models/unit'; +import {GradeService} from 'src/app/common/services/grade.service'; + +@Component({ + selector: 'f-portfolio-learning-summary-report-step', + templateUrl: 'portfolio-learning-summary-report-step.component.html', + styleUrls: ['portfolio-learning-summary-report-step.component.scss'], +}) +export class PortfolioLearningSummaryReportStepComponent { + @Input() unit: Unit; + @Input() project: Project; + + public learningSummaryReportFileUploadData = { + type: { + file0: {name: 'Learning Summary Report', type: 'document'}, + }, + payload: { + name: 'LearningSummaryReport', // DO NOT MODIFY - case sensitive on API + kind: 'document', + }, + }; + + public forceLSRSubmit: boolean = false; + public acceptUploadNewLearningSummary: boolean = false; + + constructor( + private injector: Injector, + private gradeService: GradeService, + ) {} + + public get projectHasDraftLearningSummaryReport() { + return ( + this.project?.usesDraftLearningSummary || + this.project?.portfolioFiles.find((f) => f.idx === 0) + ); + } + + public get targetGradeLabel(): string { + return this.gradeService.grades[this.project.targetGrade]; + } + + // TODO: remove this once parent component is migrated + advanceActiveTab(index: 1 | -1) { + this.injector.get('$scope').advanceActiveTab(index); + } + + addNewFile(newFile: {kind: string; name: string; idx: number}) { + this.project.portfolioFiles.push(newFile); + this.acceptUploadNewLearningSummary = false; + this.forceLSRSubmit = false; + } + + draftTaskDefinitionWasUsed(): boolean { + const draftTaskDef = this.unit.draftTaskDefinition; + if (draftTaskDef) { + const task = this.project.findTaskForDefinition(draftTaskDef.id); + if (task && task.inSubmittedState()) { + return true; + } + } + return false; + } + + // downloadLearningSummaryReport(){} +} diff --git a/src/app/projects/states/portfolio/directives/portfolio-learning-summary-report-step/portfolio-learning-summary-report-step.tpl.html b/src/app/projects/states/portfolio/directives/portfolio-learning-summary-report-step/portfolio-learning-summary-report-step.tpl.html deleted file mode 100644 index 960c0014ab..0000000000 --- a/src/app/projects/states/portfolio/directives/portfolio-learning-summary-report-step/portfolio-learning-summary-report-step.tpl.html +++ /dev/null @@ -1,73 +0,0 @@ -
-
-

- {{activeTab.title}} -

-
-
-

- Upload the Learning Summary Report, the primary porfolio document which - justifies your desired grade. -

-

- Your Learning Summary Report is a summary of what you have learnt in this unit. - It consists of two sections: -

    -
  1. a self-assessment, and
  2. -
  3. your reflections on the unit.
  4. -
-

-

- The self-assessment indicates how your portfolio aligns - with the assessment criteria, and which grade you are applying for. -

-

- Your reflections are a personal comment on what you have - learnt in the unit, and how your knowledge and skills have developed. -

-
-
-

- Before you submit your portfolio... -

- Your draft learning summary has already been copied over, - it is advised you upload a revised copy. -
-
- - -
-
- -
-
-

- Learning Summary Report Submitted -

- Click here to re-upload a new Learning Summary Report -
- -
-

- Before you submit the Learning Summary Report... -

- Remember to provide a justification for why you believe - you have achieved a {{targetGrade}} in {{unit.name}}. -
-
- -
-
-
- -
diff --git a/src/app/projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.coffee b/src/app/projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.coffee deleted file mode 100644 index c34f31b354..0000000000 --- a/src/app/projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.coffee +++ /dev/null @@ -1,10 +0,0 @@ -angular.module('doubtfire.projects.states.portfolio.directives.portfolio-welcome-step', []) - -# -# Welcome introductory step -# -.directive('portfolioWelcomeStep', -> - restrict: 'E' - replace: true - templateUrl: 'projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.tpl.html' -) diff --git a/src/app/projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.component.html b/src/app/projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.component.html new file mode 100644 index 0000000000..c5fd1a5cf8 --- /dev/null +++ b/src/app/projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.component.html @@ -0,0 +1,24 @@ + + + Portfolio Preparation + + +

Preparing your portfolio involves 5 steps:

+
    +
  1. Select your Grade you are applying for
  2. +
  3. Upload your Learning Summary Report
  4. +
  5. Select the Tasks you want included
  6. +
  7. Upload any Other Resources you want to add
  8. +
  9. Compile your resources into your portfolio and review
  10. +
+

+ Once you have completed all of these steps, your portfolio will be prepared by + {{ externalName }} and you will be notified when it is ready. You can then check your work, + and if you want to make any corrections repeat these steps to create a new version of your + portfolio. +

+
+ + + +
diff --git a/src/app/projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.component.scss b/src/app/projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.component.ts b/src/app/projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.component.ts new file mode 100644 index 0000000000..41f953ea73 --- /dev/null +++ b/src/app/projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.component.ts @@ -0,0 +1,27 @@ +import {Component, OnInit, Injector} from '@angular/core'; +import {DoubtfireConstants} from 'src/app/config/constants/doubtfire-constants'; + +@Component({ + selector: 'f-portfolio-welcome-step', + templateUrl: 'portfolio-welcome-step.component.html', + styleUrls: ['portfolio-welcome-step.component.scss'], +}) +export class PortfolioWelcomeStepComponent implements OnInit { + public externalName: string = 'OnTrack'; + + constructor( + private constants: DoubtfireConstants, + private injector: Injector, + ) {} + + ngOnInit(): void { + this.constants.ExternalName.subscribe((name) => { + this.externalName = name; + }); + } + + goNextStep() { + // TODO: remove this once parent component is migrated + this.injector.get('$scope').advanceActiveTab(1); + } +} diff --git a/src/app/projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.tpl.html b/src/app/projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.tpl.html deleted file mode 100644 index 524101329b..0000000000 --- a/src/app/projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.tpl.html +++ /dev/null @@ -1,21 +0,0 @@ -
-
-

Portfolio Preparation

-
-
-

Preparing your portfolio involves 5 steps:

-
    -
  1. Select your Grade you are applying for
  2. -
  3. Upload your Learning Summary Report
  4. -
  5. Select the Tasks you want included
  6. -
  7. Upload any Other Resources you want to add
  8. -
  9. Compile your resources into your portfolio and review
  10. -
-

- Once you have completed all of these steps, your portfolio will be prepared by {{externalName.value}} and you will be notified when it is ready. You can then check your work, and if you want to make any corrections repeat these steps to create a new version of your portfolio. -

-
- -
diff --git a/src/app/projects/states/portfolio/portfolio.tpl.html b/src/app/projects/states/portfolio/portfolio.tpl.html index 1c009b47ab..2cc7b69080 100644 --- a/src/app/projects/states/portfolio/portfolio.tpl.html +++ b/src/app/projects/states/portfolio/portfolio.tpl.html @@ -6,10 +6,14 @@ - - - + + + + - +
diff --git a/src/app/projects/states/states.coffee b/src/app/projects/states/states.coffee index 01b9d42dd4..a2a24c4bc4 100644 --- a/src/app/projects/states/states.coffee +++ b/src/app/projects/states/states.coffee @@ -1,7 +1,6 @@ angular.module('doubtfire.projects.states', [ 'doubtfire.projects.states.index' 'doubtfire.projects.states.dashboard' - 'doubtfire.projects.states.tutorials' 'doubtfire.projects.states.portfolio' 'doubtfire.projects.states.groups' 'doubtfire.projects.states.outcomes' diff --git a/src/app/projects/states/tutorials/tutorials.coffee b/src/app/projects/states/tutorials/tutorials.coffee deleted file mode 100644 index 5c22b609e3..0000000000 --- a/src/app/projects/states/tutorials/tutorials.coffee +++ /dev/null @@ -1,23 +0,0 @@ -angular.module('doubtfire.projects.states.tutorials', []) - -# -# Tasks state for projects -# -.config(($stateProvider) -> - $stateProvider.state 'projects/tutorials', { - parent: 'projects/index' - url: '/tutorials' - controller: 'ProjectsTutorialsStateCtrl' - templateUrl: 'projects/states/tutorials/tutorials.tpl.html' - data: - task: "Tutorial List" - pageTitle: "_Home_" - } -) - -.controller("ProjectsTutorialsStateCtrl", ($scope) -> - if $scope.unit.tutorialStreamsCache.size > 0 - $scope.sortOrder = 'tutorialStream.name' - else - $scope.sortOrder = 'abbreviation' -) diff --git a/src/app/projects/states/tutorials/tutorials.component.html b/src/app/projects/states/tutorials/tutorials.component.html new file mode 100644 index 0000000000..39ec57fbf8 --- /dev/null +++ b/src/app/projects/states/tutorials/tutorials.component.html @@ -0,0 +1,104 @@ +
+
+

Tutorials

+

+ View available tutorials and manage your enrolment. Note that availability is subject to + capacity. If you are unable to enrol in a tutorial, please contact your unit coordinator. +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Stream + @if (unit.tutorialStreamsCache.size > 0) { +
{{ tutorial.tutorialStream?.name || 'All' }}
+ } @else { +
N/A
+ } +
Campus + {{ tutorial.campus?.name || 'All' }} + Code + {{ tutorial.abbreviation }} + Day + {{ tutorial.meetingDay }} + Time + {{ shortTime(tutorial.meetingTime) }} + Room + {{ tutorial.meetingLocation }} + Tutor + {{ tutorial.tutorName }} + Actions + @if (project.isEnrolledIn(tutorial)) { + @if (unit.allowStudentChangeTutorial) { + + } @else { +
+ Enrolled +
+ } + } @else if (unit.allowStudentChangeTutorial) { + + } @else { +
+ + } +
+
diff --git a/src/app/projects/states/tutorials/tutorials.component.scss b/src/app/projects/states/tutorials/tutorials.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/projects/states/tutorials/tutorials.component.ts b/src/app/projects/states/tutorials/tutorials.component.ts new file mode 100644 index 0000000000..6a28239240 --- /dev/null +++ b/src/app/projects/states/tutorials/tutorials.component.ts @@ -0,0 +1,149 @@ +import {Component, Input, OnInit} from '@angular/core'; +import {Sort} from '@angular/material/sort'; +import {MatTableDataSource} from '@angular/material/table'; +import {Tutorial, UnitService} from 'src/app/api/models/doubtfire-model'; +import {Project} from 'src/app/api/models/project'; +import {Unit} from 'src/app/api/models/unit'; +import {ProjectService} from 'src/app/api/services/project.service'; + +@Component({ + selector: 'f-tutorials', + templateUrl: './tutorials.component.html', + styleUrls: ['./tutorials.component.scss'], +}) +export class TutorialsComponent implements OnInit { + @Input() projectId: number; + + filteredTutorials: Tutorial[] = []; + + project: Project; + unit: Unit; + + displayedColumns: string[] = [ + 'stream', + 'campus', + 'code', + 'day', + 'time', + 'room', + 'tutor', + 'actions', + ]; + + dataSource = new MatTableDataSource([]); + + constructor( + private projectService: ProjectService, + private unitService: UnitService, + ) {} + + ngOnInit(): void { + this.projectService.fetch(this.projectId).subscribe({ + next: (project) => { + this.unitService.get(project.unit.id).subscribe({ + next: (unit) => { + this.unit = unit; + this.project = project; + this.filteredTutorials = this.tutorialCampusFilter([...unit.tutorials], this.project); + this.dataSource.data = this.filteredTutorials; + }, + error: (error) => { + console.error('Error fetching unit:', error); + }, + }); + }, + error: (error) => { + console.error('Error fetching project:', error); + }, + }); + } + + /** + * Switches to the passed-in tutorial. + * + * @param tutorial + * + * @returns void + */ + switchToTutorial(tutorial: Tutorial): void { + this.project.switchToTutorial(tutorial); + } + + /** + * Filters a collection of passed-in tutorials based on the campus_id of the passed-in project. + * + * @param tutorials + * @param project + * + * @returns Tutorial[] + */ + tutorialCampusFilter(tutorials: Tutorial[], project: Project): Tutorial[] { + if (!project) { + return tutorials; + } + return tutorials.filter((tutorial) => { + return ( + !project.campus?.id || + !tutorial.campus || + tutorial.campus.id === project.campus.id || + project.isEnrolledIn(tutorial) + ); + }); + } + + /** + * Formats the passed-in time string to the format of: HH:mm + * Todo: Add date validation + * @param meetingTime + * + * @returns string + */ + shortTime(meetingTime: string): string { + const [hours, minutes] = meetingTime.split(':'); + const formattedHours = hours.padStart(2, '0'); + const formattedMinutes = minutes.padStart(2, '0'); + + return `${formattedHours}:${formattedMinutes}`; + } + + private sortCompare(aValue: number | string, bValue: number | string, isAsc: boolean) { + return (aValue < bValue ? -1 : 1) * (isAsc ? 1 : -1); + } + + sortTableData(sort: Sort) { + if (!sort.active || sort.direction === '') { + return; + } + this.dataSource.data = this.dataSource.data.sort((a, b) => { + switch (sort.active) { + case 'stream': + return this.sortCompare( + a.tutorialStream?.name, + b.tutorialStream?.name, + sort.direction === 'asc', + ); + case 'campus': + return this.sortCompare(a.campus?.name, b.campus?.name, sort.direction === 'asc'); + case 'code': + return this.sortCompare(a.abbreviation, b.abbreviation, sort.direction === 'asc'); + case 'day': { + return this.sortCompare(a.meetingDay, b.meetingDay, sort.direction === 'asc'); + } + case 'time': { + return this.sortCompare( + this.shortTime(a.meetingTime), + this.shortTime(b.meetingTime), + sort.direction === 'asc', + ); + } + case 'room': { + return this.sortCompare(a.meetingLocation, b.meetingLocation, sort.direction === 'asc'); + } + case 'tutor': + return this.sortCompare(a.tutorName, b.tutorName, sort.direction === 'asc'); + default: + return 0; + } + }); + } +} diff --git a/src/app/projects/states/tutorials/tutorials.scss b/src/app/projects/states/tutorials/tutorials.scss deleted file mode 100644 index d402eae6a6..0000000000 --- a/src/app/projects/states/tutorials/tutorials.scss +++ /dev/null @@ -1,10 +0,0 @@ -#tutorials-state table { - th.stream { width: 10%; } - th.campus { width: 20%; } - th.code { width: 10%; } - th.day { width: 10%; } - th.time { width: 10%; } - th.room { width: 10%; } - th.tutor { width: 15%; } - th.actions { width: 15%; } -} diff --git a/src/app/projects/states/tutorials/tutorials.tpl.html b/src/app/projects/states/tutorials/tutorials.tpl.html deleted file mode 100644 index fae91d6ae3..0000000000 --- a/src/app/projects/states/tutorials/tutorials.tpl.html +++ /dev/null @@ -1,71 +0,0 @@ -
-
-
-

Select a Tutorial

-
-
-

- Click the plus on the specific tutorial to enrol in that tutorial, or click the minus icon to withdraw from your - current tutorial. -

-
- - - - - - - - - - - - - - - - - - - - - - - - - -
- Stream - - Campus - - Code - - Day - - Time - - Room - - Tutor - Actions
{{tutorial.tutorialStream.name || 'All'}}{{tutorial.campus ? tutorial.campus.name : 'All'}}{{tutorial.abbreviation}}{{tutorial.meetingDay}}{{tutorial.meetingTime | date: 'shortTime'}}{{tutorial.meetingLocation}}{{tutorial.tutorName}} - - -
-
-
diff --git a/src/app/sessions/auth/http-auth-injector.coffee b/src/app/sessions/auth/http-auth-injector.coffee deleted file mode 100644 index 56ef98961e..0000000000 --- a/src/app/sessions/auth/http-auth-injector.coffee +++ /dev/null @@ -1,37 +0,0 @@ -angular.module("doubtfire.sessions.auth.http-auth-injector", []) -# -# This module is responsible for injecting the auth credentials to -# all -# -.config(($httpProvider) -> - $httpProvider.interceptors.push ($q, $rootScope, DoubtfireConstants, newUserService) -> - # - # Inject authentication token for requests - # - injectAuthForRequest = (request) -> - # Intercept API requests and inject the auth token. - if _.startsWith(request.url, DoubtfireConstants.API_URL) and newUserService.currentUser.authenticationToken? - request.headers = {} unless _.has(request, "headers") - request.headers.Auth_Token = newUserService.currentUser.authenticationToken - request.headers.Username = newUserService.currentUser.username - request or $q.when request - - # - # Inject handlers for 419 and 401 response errors - # - injectAuthForResponseWithError = (response) -> - # Intercept unauthorised API responses and fire an event. - if response.config && response.config.url and _.startsWith(response.config.url, DoubtfireConstants.API_URL) - # Timeout? - if response.status is 419 - $rootScope.$broadcast "tokenTimeout" - # Unauthorised? - else if response.status is 401 - $rootScope.$broadcast "unauthorisedRequestIntercepted" - $q.reject response - - { - request: injectAuthForRequest - responseError: injectAuthForResponseWithError - } -) diff --git a/src/app/sessions/sessions.coffee b/src/app/sessions/sessions.coffee deleted file mode 100644 index ccf6e6b5f1..0000000000 --- a/src/app/sessions/sessions.coffee +++ /dev/null @@ -1,3 +0,0 @@ -angular.module('doubtfire.sessions', [ - "doubtfire.sessions.auth.http-auth-injector" -]) diff --git a/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.coffee b/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.coffee index b9c9ebd2b9..7646c70ff4 100644 --- a/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.coffee +++ b/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.coffee @@ -55,6 +55,12 @@ angular.module('doubtfire.tasks.modals.upload-submission-modal', []) $scope.submissionTypes = submissionTypes + $scope.isReadyChange = (ready) -> + $scope.uploader.isReady = ready + + $scope.uploadIsReady = (callback) -> + $scope.uploader.start = callback + # Upload files $scope.uploader = { # url: Task.generateSubmissionUrl($scope.task.project, $scope.task) diff --git a/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.tpl.html b/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.tpl.html index 92070ac30f..9b96abcd0b 100644 --- a/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.tpl.html +++ b/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.tpl.html @@ -60,20 +60,21 @@

- +
diff --git a/src/app/tasks/task-ilo-alignment/task-ilo-alignment-editor/task-ilo-alignment-editor.tpl.html b/src/app/tasks/task-ilo-alignment/task-ilo-alignment-editor/task-ilo-alignment-editor.tpl.html index e95245bd46..20f3664c5c 100644 --- a/src/app/tasks/task-ilo-alignment/task-ilo-alignment-editor/task-ilo-alignment-editor.tpl.html +++ b/src/app/tasks/task-ilo-alignment/task-ilo-alignment-editor/task-ilo-alignment-editor.tpl.html @@ -105,12 +105,13 @@

Import Task Outcome Alignments

Import links between tasks and outcomes from a CSV containing: unit_code, learning_outcome, task_abbr, rating.
- +

Export Task Outcome Alignments

diff --git a/src/app/units/states/edit/directives/directives.coffee b/src/app/units/states/edit/directives/directives.coffee index bfb12ed317..fe06bf4510 100644 --- a/src/app/units/states/edit/directives/directives.coffee +++ b/src/app/units/states/edit/directives/directives.coffee @@ -2,5 +2,4 @@ angular.module('doubtfire.units.states.edit.directives', [ 'doubtfire.units.states.edit.directives.unit-details-editor' 'doubtfire.units.states.edit.directives.unit-group-set-editor' 'doubtfire.units.states.edit.directives.unit-ilo-editor' - 'doubtfire.units.states.edit.directives.unit-staff-editor' ]) diff --git a/src/app/units/states/edit/directives/unit-group-set-editor/unit-group-set-editor.tpl.html b/src/app/units/states/edit/directives/unit-group-set-editor/unit-group-set-editor.tpl.html index db550ef42d..90313b86b6 100644 --- a/src/app/units/states/edit/directives/unit-group-set-editor/unit-group-set-editor.tpl.html +++ b/src/app/units/states/edit/directives/unit-group-set-editor/unit-group-set-editor.tpl.html @@ -123,18 +123,21 @@

No Group Sets Created

New Group Set -
- - -
- +
+ + + + + + +

@@ -159,14 +162,14 @@

Import Groups for {{selectedGroupSet.name}} Download CSV
- - + +

@@ -192,15 +195,17 @@

Download CSV
- - -

+ + + + diff --git a/src/app/units/states/edit/directives/unit-ilo-editor/unit-ilo-editor.tpl.html b/src/app/units/states/edit/directives/unit-ilo-editor/unit-ilo-editor.tpl.html index 3ce30ff314..84bd7ef0ab 100644 --- a/src/app/units/states/edit/directives/unit-ilo-editor/unit-ilo-editor.tpl.html +++ b/src/app/units/states/edit/directives/unit-ilo-editor/unit-ilo-editor.tpl.html @@ -54,7 +54,11 @@

Batch Upload Outcome Definitions

description.
- +
diff --git a/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.coffee b/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.coffee deleted file mode 100644 index 3856daefe5..0000000000 --- a/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.coffee +++ /dev/null @@ -1,58 +0,0 @@ -angular.module('doubtfire.units.states.edit.directives.unit-staff-editor', []) - -# -# Editor for adding new staff to a unit and assigning those staff -# members new unit roles within the unit -# -.directive('unitStaffEditor', -> - replace: true - restrict: 'E' - templateUrl: 'units/states/edit/directives/unit-staff-editor/unit-staff-editor.tpl.html' - controller: ($scope, $rootScope, alertService, newUnitService, newUnitRoleService) -> - temp = [] - users = [] - - $scope.unit.staffCache.values.subscribe( (staff) -> $scope.unitStaff = staff ) - - $scope.changeRole = (unitRole, role_id) -> - unitRole.roleId = role_id - newUnitRoleService.update(unitRole).subscribe({ - next: (response) -> alertService.success( "Role changed", 2000) - error: (response) -> alertService.error( response, 6000) - }) - - $scope.changeMainConvenor = (staff) -> - $scope.unit.changeMainConvenor(staff).subscribe({ - next: (response) -> - alertService.success( "Main convenor changed", 2000) - error: (response) -> - alertService.error( response, 6000) - }) - - $scope.addSelectedStaff = -> - staff = $scope.selectedStaff - $scope.selectedStaff = null - $scope.unit.staff = [] unless $scope.unit.staff - - if staff.id? - $scope.unit.addStaff(staff).subscribe({ - next: (response) -> alertService.success( "Staff member added", 2000) - error: (response) -> alertService.error( response, 6000) - }) - else - alertService.error( "Unable to add staff member. Ensure they have a tutor or convenor account in User admin first.", 6000) - - # Used in the typeahead to filter staff already in unit - $scope.filterStaff = (staff) -> - not _.find($scope.unit.staff, (listStaff) -> staff.id == listStaff.user.id) - - $scope.removeStaff = (staff) -> - newUnitRoleService.delete(staff, {cache: $scope.unit.staffCache}).subscribe({ - next: (response) -> alertService.success( "Staff member removed", 2000) - error: (response) -> alertService.error( response, 6000) - }) - - $scope.groupSetName = (id) -> - $scope.unit.groupSetsCache.get(id)?.name || "Individual Work" - -) diff --git a/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.html b/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.html new file mode 100644 index 0000000000..9f3a65b727 --- /dev/null +++ b/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.html @@ -0,0 +1,89 @@ +
+
+

Unit Staff

+

Manage unit staff by adding members and assigning them as convenors or tutors.

+
+ + + + + + + + + + + + + + + + + + + + +
Name +
+ {{ unitRole.user.name }} +
+
Role + + Tutor + Convenor + + Main Convenor + @if (unitRole?.role === 'Convenor') { + + } + Actions + +
+ + + + + {{ staff.name }} + + + +
diff --git a/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.ts b/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.ts new file mode 100644 index 0000000000..7c53a04671 --- /dev/null +++ b/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.ts @@ -0,0 +1,166 @@ +import {Component, Input, OnInit} from '@angular/core'; +import {AlertService} from 'src/app/common/services/alert.service'; +import {UnitRoleService} from 'src/app/api/services/unit-role.service'; +import {Unit} from 'src/app/api/models/unit'; +import {User} from 'src/app/api/models/doubtfire-model'; +import {UnitRole} from 'src/app/api/models/unit-role'; +import {MatTableDataSource} from '@angular/material/table'; +import {MatButtonToggleChange} from '@angular/material/button-toggle'; +import {ConfirmationModalService} from 'src/app/common/modals/confirmation-modal/confirmation-modal.service'; + +@Component({ + selector: 'unit-staff-editor', + templateUrl: 'unit-staff-editor.component.html', +}) +export class UnitStaffEditorComponent implements OnInit { + @Input() unit: Unit; + @Input() staff: User[]; + + temp = []; + users = []; + unitStaff: UnitRole[]; + filteredStaff: User[] = []; // Filtered staff members + searchTerm: string = ''; // Search term entered by the user + + displayedColumns: string[] = ['name', 'role', 'main-convenor', 'actions']; + dataSource = new MatTableDataSource(); + + // Inject services here + constructor( + private alertService: AlertService, + private unitRoleService: UnitRoleService, + private confirmationModalService: ConfirmationModalService, + ) {} + + ngOnInit(): void { + // Subscribe to staff cache + this.unit.staffCache.values.subscribe((staff: UnitRole[]) => { + this.unitStaff = staff; + this.dataSource.data = staff; + }); + } + + onRoleChange(unitRole: UnitRole, event: MatButtonToggleChange) { + const role = event.value; + if (role !== 'Tutor' && role !== 'Convenor') { + return; + } + const roleId = role === 'Tutor' ? 2 : 3; // map however you like + this.changeRole(unitRole, roleId, role); + } + /** + * Changes the role of a staff member. + * + * @param UnitRole unitRole + * @param number role_id + * + * @returns void + */ + changeRole(unitRole: UnitRole, roleId: number, role: string) { + const previousRoleId = unitRole.roleId; + const previousRole = unitRole.role; + + unitRole.roleId = roleId; + unitRole.role = role; + this.unitRoleService.update(unitRole).subscribe({ + next: () => this.alertService.success('Role changed', 2000), + error: (response) => { + // Revert changes on error + unitRole.roleId = previousRoleId; + unitRole.role = previousRole; + this.alertService.error(response, 6000); + }, + }); + } + + /** + * Changes who the `Main Convenor` of the unit is. + * + * @param UnitRole staff + * + * @returns void + */ + changeMainConvenor(staff: UnitRole) { + this.confirmationModalService.show( + 'Set Main Convenor', + `Do you want to make ${staff.user.name} the main convenor for this unit?`, + () => { + this.unit.changeMainConvenor(staff).subscribe({ + next: (_response) => this.alertService.success('Main convenor changed', 2000), + error: (response) => this.alertService.error(response, 6000), + }); + }, + ); + } + + /** + * Adds a staff member to the unit. + * + * @param User selectedStaff + * + * @returns void + */ + addSelectedStaff(selectedStaff: User) { + if (selectedStaff?.id) { + this.unit.addStaff(selectedStaff).subscribe({ + next: () => { + this.alertService.success('Staff member added', 2000); + this.searchTerm = ''; // Clear the input field + this.filterStaffList(); // Refilter the list + }, + error: (response) => this.alertService.error(response, 6000), + }); + } else { + this.alertService.error( + 'Unable to add staff member. Ensure they have a tutor or convenor account in User admin first', + ); + } + } + + /** + * Used in filtering the staff list. The `searchTerm` is bound to the auto-complete input in this class's template. + * + * @returns void + */ + filterStaffList(): void { + // `this.searchTerm` holds the selected staff member object from the dropdown OR the auto-complete input searchTerm (never at the same time). + // Thus, check the type here and exit early if string filtering is not needed. + if (typeof this.searchTerm !== 'string') { + return; + } + this.filteredStaff = this.staff.filter( + (staff) => + staff.matches(this.searchTerm.toLowerCase()) && // Find by name + !this.unit.staff.find((listStaff) => staff.id === listStaff.user.id), // Not already assigned to the unit + ); + } + + /** + * Generates a human-readable name made up of the passed-in staff member's `first` and `last` names. + * + * @param User staff + * + * @returns void + */ + displayStaffName(staff: User): string { + return staff ? staff.name : ''; + } + + /** + * Removes a staff member from the unit. + * + * @param UnitRole staff + * + * @returns void + */ + removeStaff(staff: UnitRole) { + this.unitRoleService.delete(staff, {cache: this.unit.staffCache}).subscribe({ + next: () => this.alertService.success('Staff member removed', 2000), + error: (response) => this.alertService.error(response, 6000), + }); + } + + groupSetName(id: number) { + this.unit.groupSetsCache.get(id).name || 'Individual Work'; + } +} diff --git a/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.tpl.html b/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.tpl.html deleted file mode 100644 index f8b38cecb0..0000000000 --- a/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.tpl.html +++ /dev/null @@ -1,101 +0,0 @@ -
- -
-
-

Modify Unit Staff

- Add staff members to the unit, assigning them a convenor or tutor role. -
-
-
-
This unit has no staff assigned
-
- - - - - - - - - - - - - - - - - - -
NameRoleMain ConvenorActions
- - {{staff.user.name}} -
- - -
-
- - - -
-
-
-
- -
-
diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/unit-task-editor.component.ts b/src/app/units/states/edit/directives/unit-tasks-editor/unit-task-editor.component.ts index 72c309371d..bf4c3b626b 100644 --- a/src/app/units/states/edit/directives/unit-tasks-editor/unit-task-editor.component.ts +++ b/src/app/units/states/edit/directives/unit-tasks-editor/unit-task-editor.component.ts @@ -138,9 +138,7 @@ export class UnitTaskEditorComponent implements AfterViewInit { () => { this.unit.deleteTaskDefinition(taskDefinition); //TODO: reinstate ProgressModal.show "Deleting Task #{task.abbreviation}", 'Please wait while student projects are updated.', promise - - this.alerts.success('Task deleted'); - } + }, ); } diff --git a/src/app/units/states/edit/edit.tpl.html b/src/app/units/states/edit/edit.tpl.html index 3ba73129a5..a49ba3561d 100644 --- a/src/app/units/states/edit/edit.tpl.html +++ b/src/app/units/states/edit/edit.tpl.html @@ -6,7 +6,7 @@ - + diff --git a/src/app/units/states/groups/groups.tpl.html b/src/app/units/states/groups/groups.tpl.html index 319f46e08f..6d79bdb9a3 100644 --- a/src/app/units/states/groups/groups.tpl.html +++ b/src/app/units/states/groups/groups.tpl.html @@ -1,11 +1,8 @@ -
- - -
+
+ + +
+
diff --git a/src/app/visualisations/progress-burndown-chart/progressburndownchart.component.html b/src/app/visualisations/progress-burndown-chart/progressburndownchart.component.html index 3f51c0d7dd..374cee4ce8 100644 --- a/src/app/visualisations/progress-burndown-chart/progressburndownchart.component.html +++ b/src/app/visualisations/progress-burndown-chart/progressburndownchart.component.html @@ -13,7 +13,9 @@ [yAxisLabel]="yAxisLabel" [yAxisTickFormatting]="formatPerc" [scheme]="colorScheme" + [yScaleMin]="yScaleMin" + [yScaleMax]="yScaleMax" (select)="onSelect($event)" - > + >
diff --git a/src/app/visualisations/progress-burndown-chart/progressburndownchart.component.ts b/src/app/visualisations/progress-burndown-chart/progressburndownchart.component.ts index 4c1920154a..e49ca853dc 100644 --- a/src/app/visualisations/progress-burndown-chart/progressburndownchart.component.ts +++ b/src/app/visualisations/progress-burndown-chart/progressburndownchart.component.ts @@ -1,14 +1,13 @@ -import { Component, OnInit, Input, SimpleChanges, LOCALE_ID, ViewContainerRef } from '@angular/core'; -import { Project, Unit } from 'src/app/api/models/doubtfire-model'; -import { formatDate } from '@angular/common'; -import { MappingFunctions } from 'src/app/api/services/mapping-fn'; -import { AppInjector } from 'src/app/app-injector'; -import { ChartBaseComponent } from 'src/app/common/chart-base/chart-base-component/chart-base-component.component'; +import {Component, OnInit, Input, SimpleChanges, LOCALE_ID, ViewContainerRef} from '@angular/core'; +import {Project, Unit} from 'src/app/api/models/doubtfire-model'; +import {formatDate} from '@angular/common'; +import {AppInjector} from 'src/app/app-injector'; +import {ChartBaseComponent} from 'src/app/common/chart-base/chart-base-component/chart-base-component.component'; @Component({ selector: 'f-progress-burndown-chart', templateUrl: './progressburndownchart.component.html', - styleUrls: ['./progressburndownchart.component.scss'] + styleUrls: ['./progressburndownchart.component.scss'], }) export class ProgressBurndownChartComponent extends ChartBaseComponent implements OnInit { @Input() project: Project; @@ -28,9 +27,11 @@ export class ProgressBurndownChartComponent extends ChartBaseComponent implement showXAxisLabel: boolean = true; xAxisLabel: string = 'Time'; yAxisLabel: string = 'Tasks Remaining'; - colorScheme = { domain: ['#AAAAAA', '#777777', '#0079d8', '#E01B5D'] }; + colorScheme = {domain: ['#AAAAAA', '#777777', '#0079d8', '#E01B5D', 'transparent']}; + yScaleMin: number = 0; + yScaleMax: number = 100; - private seriesVisibility: { [key: string]: boolean } = {}; + private seriesVisibility: {[key: string]: boolean} = {}; constructor(public viewContainerRef: ViewContainerRef) { super(viewContainerRef); @@ -39,9 +40,6 @@ export class ProgressBurndownChartComponent extends ChartBaseComponent implement } ngOnInit(): void { - console.log('ProgressBurndownChartComponent: ngOnInit'); - console.log(this.project); - this.project.refreshBurndownChartData(); this.updateData(); this.data.forEach((item) => { @@ -56,49 +54,46 @@ export class ProgressBurndownChartComponent extends ChartBaseComponent implement } } - generateDates() { - const startDate: Date = this.project.unit.startDate; - const endDate: Date = this.project.unit.endDate; - const locale: string = AppInjector.get(LOCALE_ID); - const numberPoints = 10; - // Get the number of days between dates - const totalDays = MappingFunctions.daysBetween(startDate, endDate); - const interval = totalDays / (numberPoints - 1); // get gaps between points - - const dates = []; - for (let i = 0; i < numberPoints; i++) { - const date = MappingFunctions.daysAfter(startDate, interval * i); - dates.push(formatDate(date, 'd MMM', locale)); - } - - return dates; - } - updateData(): void { const chartData = this.project?.burndownChartData; - const dates = this.generateDates(); - - const formattedData = chartData.map((dataset) => { - const values = Array(10) - .fill(0) - .map((_, index) => dataset.values[index] || 0); - - const series = dates.map((date, index) => { - let value = values[index][1] ?? 0; - value = value * 100; - - if (value < 0) { - value = 0; - } + const locale: string = AppInjector.get(LOCALE_ID); + const startDate: Date = this.project.unit.startDate; + const endDate: Date = this.project.unit.endDate; - return { name: date, value }; - }); + if (!chartData) { + this.data = []; + return; + } - return { - name: dataset.key, - series, - }; - }); + const formattedData = chartData.map((series) => ({ + name: series.key, // Use the "key" as the "name" + series: series.values + .filter((value) => value[0] >= startDate.getTime() && value[0] <= endDate.getTime()) // Filter values based on the date range + .map((value) => { + if (value[1] < 0) { + value[1] = 0; // If the value is negative, set it to 0 + } + value[1] = Math.round(value[1] * 100); // Round the value to 2 decimal places + return { + name: formatDate(new Date(value[0]), 'd MMM', locale), // Format the timestamp as a date + value: value[1], + }; + }), + })); + + // Hack to get around yScaleMin and yScaleMax not working. + const target = formattedData.find((series) => series.name === 'Target'); + if (target) { + const start = target.series.find( + (point) => point.name === formatDate(new Date(startDate), 'd MMM', locale), + ); + const end = target.series.find( + (point) => point.name === formatDate(new Date(endDate), 'd MMM', locale), + ); + + if (start) start.value = 100; // Update start + if (end) end.value = 0; // Update end + } this.temp = JSON.parse(JSON.stringify(formattedData)); this.data = formattedData; diff --git a/src/app/visualisations/task-status-pie-chart/taskstatuspiechart.component.html b/src/app/visualisations/task-status-pie-chart/taskstatuspiechart.component.html new file mode 100644 index 0000000000..02f2e78dee --- /dev/null +++ b/src/app/visualisations/task-status-pie-chart/taskstatuspiechart.component.html @@ -0,0 +1,12 @@ +
+ + +
diff --git a/src/app/visualisations/task-status-pie-chart/taskstatuspiechart.component.scss b/src/app/visualisations/task-status-pie-chart/taskstatuspiechart.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/visualisations/task-status-pie-chart/taskstatuspiechart.component.ts b/src/app/visualisations/task-status-pie-chart/taskstatuspiechart.component.ts new file mode 100644 index 0000000000..1e9bb142d8 --- /dev/null +++ b/src/app/visualisations/task-status-pie-chart/taskstatuspiechart.component.ts @@ -0,0 +1,76 @@ +import {Component, OnInit, Input, SimpleChanges} from '@angular/core'; +import {Project, TaskStatus} from 'src/app/api/models/doubtfire-model'; +import {ChartBaseComponent} from 'src/app/common/chart-base/chart-base-component/chart-base-component.component'; + +@Component({ + selector: 'f-task-status-pie-chart', + templateUrl: './taskstatuspiechart.component.html', + styleUrls: ['./taskstatuspiechart.component.scss'], +}) +export class TaskStatusPieChartComponent extends ChartBaseComponent implements OnInit { + @Input() project: Project; + @Input() grade: number; + + data: {name: string; value: number}[] = []; + colors: {name: string; value: string}[]; + view: number[] = [700, 400]; + + ngOnInit(): void { + this.updateData(); + } + + ngOnChanges(changes: SimpleChanges): void { + if ('grade' in changes && changes.grade.currentValue !== undefined) { + this.updateData(); + } + } + + updateData(): void { + if (this.project) { + const taskCounts = new Map(TaskStatus.STATUS_KEYS.map((status) => [status, 0])); + const activeTasks = this.project.activeTasks(); + activeTasks.forEach((task) => { + if (task.status) { + taskCounts.set(task.status, (taskCounts.get(task.status) || 0) + 1); + } + }); + + const sortOrder = [ + 'not_started', + 'feedback_exceeded', + 'redo', + 'need_help', + 'working_on_it', + 'fix_and_resubmit', + 'ready_for_feedback', + 'discuss', + 'demonstrate', + 'complete', + 'fail', + 'time_exceeded', + ]; + + this.data = Array.from(taskCounts) + .map(([status, count]) => { + return { + name: TaskStatus.STATUS_LABELS.get(status), + value: count, + }; + }) + .filter((task) => task.value > 0 || sortOrder.includes(task.name)) + .sort((a, b) => { + let aIndex = sortOrder.indexOf(a.name); + let bIndex = sortOrder.indexOf(b.name); + + aIndex = aIndex === -1 ? sortOrder.length : aIndex; + bIndex = bIndex === -1 ? sortOrder.length : bIndex; + + return aIndex - bIndex; + }); + + this.colors = Array.from(TaskStatus.STATUS_COLORS).map(([status, color]) => { + return {name: TaskStatus.STATUS_LABELS.get(status), value: color}; + }); + } + } +}