diff --git a/MIGRATION-GUIDE.md b/MIGRATION-GUIDE.md new file mode 100644 index 000000000..2a1bb1069 --- /dev/null +++ b/MIGRATION-GUIDE.md @@ -0,0 +1,200 @@ +![Doubtfire Logo](http://puu.sh/lyClF/fde5bfbbe7.png) + +# Doubtfire Web - Angular.js to Angular migration guide + +This guide will provide the basic steps you can follow in order to successfully migrate a Doubtfire component from CoffeeScript and Angular.js to TypeScript and Angular. + +Here we will demonstrate what to do using the changes associated with the Task Description card. + +## Step 0 - Come up with a plan + +Before you really get started you need to identify a suitable component to migrate, and come up with a plan for the components new styling. + +### Identifying a suitable component to migrate + +When picking a component make sure your component **DOES NOT** have any other Angular.js components nested within it. Always start at the bottom and work our way up to the "bigger" components. + +Look for components that should be easy to migrate. It will be easier to migrate things that just present data, but in general remember you already have a working component. So it should mostly be a matter of mapping the code across, and changing the styling to switch from bootstrap to material design. + +When in doubt... ask. We are happy to make suggestions for components that should be beneficial to migrate. + +We picked the Task Description Card for this guide as it was relatively straightforward in terms of functionality, and it does not have any other Doubtfire components nested within it. + +### Plan your components new styling + +Explore the [Angular Material](https://material.angular.io) details. Look for similar [components](https://material.angular.io/components/categories) to the bootstrap components being used in the component you are migrating. Where there is a simple mapping you can probably just proceed, but if you want to make bigger changes (which is good) then please mock something up and discuss with the team first. We do not want you wasting effort if your ideas are not in line with what we will accept. + +For the Task Description Card there is a matching [card component](https://material.angular.io/components/card/overview) in Angular Material with a suitable [example](https://material.angular.io/components/card/examples) we can build from. We can do a first pass and then make any styling fixes if needed at the end. + +## Step 1 - Create a branch + +Ok, this should be a given but it is always going to be important to isolate the changes for these migrations. So start by checking out a new branch for this change. + +For the task description card this was done using the following command in the Terminal: + +```bash +git checkout -b migrate/task-description-card +``` + +## Step 2 - Create replacement files + +Create a typescript, scss, and html file to replace the coffeescript, scss, and html files from the angular.js project. + +For the Task Description Card we had the files: + +- task-description-card.coffee +- task-description-card.tpl.html +- task-description-card.scss + +In the same folder we can start by creating the following files: + +- task-description-card.component.ts +- task-description-card.component.html +- task-description-card.component.scss + +Notice the naming convention. When migrating a component we use the format *name*.**component**.*extension*. + +Add the start of the typescript using something based on the following: + +```typescript +import { Component, Input, Inject } from '@angular/core'; + +@Component({ + selector: 'task-description-card', + templateUrl: 'task-description-card.component.html', + styleUrls: ['task-description-card.component.scss'] +}) +export class TaskDescriptionCardComponent { + + constructor( + ) { + } + +} +``` + +We cant see any of these changes yet, but it is a good clean start so lets commit this before we move on. + +```bash +git add task-description-card.component.ts +git add task-description-card.component.html +git add task-description-card.component.scss +# or "git add ." if you have a clean repository and only these files are changed. +git commit -m "NEW: Create initial files for migration of task-description-card" +``` + +Then we should make sure to push this back to GitHub so that others can see our progress. As this is a new branch you will need set the upstream branch, but if you forget the `git push` will remind you anyway. + +```bash +git push --set-upstream origin migrate/task-description-card +``` + +You should see a suggestion to create a pull request when you add this branch. This is also a good idea. At this stage you can create a [Draft Pull Request](https://github.blog/2019-02-14-introducing-draft-pull-requests/) which will get updated as you push your changes. Use this to get a quick screenshot of the component in action currently. Then you can include this as a "from" image. + +See the resulting [commit](933df7b673b5d57dc162fb42f79c511444f5fbe3) and [pull request](https://github.com/doubtfire-lms/doubtfire-web/pull/321). Its great that you can go back and see how things evolved. + +## Step 3 - Unlink old component and add in new + +We want to make sure we can see our progress as quickly as possible. So lets start by replacing the old component with the new one. + +There are a few files we need to update to achieve this. + +- Remove link to component from the angular module. + - Open the component's CoffeeScript file and make a note of the name of the module. + ```coffeescript + angular.module('doubtfire.projects.states.dashboard.directives.task-dashboard.directives.task-description-card', []) + ``` + + The name is `doubtfire.projects.states.dashboard.directives.task-dashboard.directives.task-description-card` + - Search for where this is referenced. For the task-description-card this was in file `src/app/projects/states/dashboard/directives/task-dashboard/directives/directives.coffee`... hence the suggestion to search :satisfied:. + - Remove the reference. This will mean that the component is no longer loaded by angular.js. +- Setup the new component in **doubtfire-angular.module.ts** + - Import like this: + ```ts + import { TaskDescriptionCardComponent } from './projects/states/dashboard/directives/task-dashboard/directives/task-description-card/task-description-card.component'; + ``` + - Then add the component name to the list of **declarations**. Now the component will be available in Angular. +- Remove the old and downgrade the new in **doubtfire-angularjs.module.ts** + - Remove the line importing the old javascript file: + ```typescript + import 'build/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-description-card/task-description-card.js'; + ``` + - Import the new component... *it is the same code as importing to the angular module*. + - Downgrade to make the new component available to Angular.js + ```typescript + DoubtfireAngularJSModule.directive('taskDescriptionCard', downgradeComponent({ component: TaskDescriptionCardComponent})); + ``` +- Update attributes on the new component usage. + - Search for all of the places where the component was already used (i.e. search for the component HTML tag). + - Update the property binding style to use the Angular form which is `[property]="value"`. + + For example: + ```html + + ``` + + Needs to change to: + ```html + + ``` +- Add matching inputs into your components typescript declaration. These use the syntax `@Input() name: type;`. For the task description card we use: + + ```ts + export class TaskDescriptionCardComponent { + @Input() task: any; + @Input() taskDef: any; + @Input() unit: any; + ... + } + ``` + +Now we can compile to see if this has all worked... :crossed_fingers:. When you change the config in this way I generally find you need to kill any old build processes and start them again. So we can compile now using: + +```bash +npm start +``` + +Open your local copy of Doubtfire and navigate somewhere that you can see the lack of the old component. As we have no HTML yet, there should be nothing in its place but we are mostly checking that we have connected this all up correctly. + +See the resulting [commit](81563fa3bc04ba4c1afc40a682dbce0703dfc10b) for all of these changes. + +Once things are working make sure to commit and push your changes. This will update your draft pull request as well. + +Now we are on to the "real" work. + +## Step 4 - Migrate the HTML, CoffeeScript, and SCSS + +This is where things are going to depend on what you are migrating. Hopefully you have already got a plan for this... + +For the task description card I used the [mat-card](https://material.angular.io/components/card/examples) as the main component I needed to migrate to. I did this with the following steps: + +1. Copy in the current bootstrap HTML +2. Identify update to wrap in the material card component (save and test) +3. Update the header (save and test) +4. Update the footer to actions (save and test) +5. Scan for other bootstrap tags and replace. + +As I needed it I copied across the CoffeeScript code into the TypeScript component. This included switching the hyperlinks to use buttons, and including the addition of the file saver to make it easy to save the downloaded task sheets and resources as a click action rather than opening a new page with the links. + +At the end I made sure to check that **all functionality** from the old component had a matching set of code in the new component. + +Here are a few things to watch out for when doing this migration: + +- Switching ```ng-show``` to ```[hidden]``` remember to change the boolean expression. +- Font awesome icons needs to change to Material Design icons using [mat-icon](https://material.angular.io/components/icon/overview) - check out the [icon list]. +- Make sure to add [aria-hidden](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_aria-hidden_attribute) and [aria-label](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_aria-label_attribute) to ensure icons and images are screen reader friendly. +- Make sure to run ```ng lint``` at the end of this process to make sure your TypeScript code is looking good! + +Once things are all working... you can delete the old CoffeeScript, HTML and SCSS files. These are no longer needed... so they can go! + +Check the [final commit](9c8a62c1d70e8f950f8c72b3a7a48c0d7274f670) with all of these changes. + +Now it is time to update the Pull Request. Mostly we need to grab a screenshot of the new component in action. Once you have added this, make sure to note any changes that the team should check. You want to make sure that you dont break things when then is merged in. If you think everything is ready then switch from a draft to full PR... As long as things are tidy, and you have clear screenshots to show that everything is working, you can expect things to be merged quickly or for you to get some instructions on what to change. + +You can check out how this [pull request](https://github.com/doubtfire-lms/doubtfire-web/pull/321) went... :crossed_fingers: + +## Conclusion + +Hopefully this has provided some useful steps that will mean you can quickly and efficiently migrate the Doubtfire components. We are all really looking forward to switching to the new Angular approach, and trying to keep things more up to date going forward. + +Many thanks for your contributions on behalf of the Doubtfire team! diff --git a/angular.json b/angular.json index cc63c834d..b14d8fd01 100644 --- a/angular.json +++ b/angular.json @@ -19,7 +19,10 @@ "main": "src/main.ts", "polyfills": "src/polyfills.ts", "tsConfig": "src/tsconfig.app.json", - "assets": ["src/assets", "src/manifest.webmanifest"], + "assets": [ + "src/assets", + "src/manifest.webmanifest" + ], "styles": [ "./node_modules/@angular/material/prebuilt-themes/indigo-pink.css", "./node_modules/bootstrap/dist/css/bootstrap.css", @@ -106,20 +109,32 @@ "./node_modules/@angular/material/prebuilt-themes/indigo-pink.css" ], "scripts": [], - "assets": ["src/favicon.ico", "src/assets", "src/manifest.webmanifest"] + "assets": [ + "src/favicon.ico", + "src/assets", + "src/manifest.webmanifest" + ] } }, "lint": { "builder": "@angular-devkit/build-angular:tslint", "options": { - "tsConfig": ["src/tsconfig.app.json", "src/tsconfig.spec.json"], - "exclude": ["**/node_modules/**"] + "tsConfig": [ + "src/tsconfig.app.json", + "src/tsconfig.spec.json" + ], + "exclude": [ + "**/node_modules/**" + ] } }, "eslint": { "builder": "@angular-eslint/builder:lint", "options": { - "lintFilePatterns": ["src/**/*.ts", "src/**/*.component.html"] + "lintFilePatterns": [ + "src/**/*.ts", + "src/**/*.component.html" + ] } } } @@ -130,5 +145,8 @@ "@schematics/angular:component": { "style": "scss" } + }, + "cli": { + "analytics": "d9f70e12-eb87-48a0-a953-29ebd31202d7" } -} +} \ No newline at end of file diff --git a/package.json b/package.json index cf6623cfd..48b434eb7 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,8 @@ "ts-md5": "^1.2.7", "tslib": "^2.0.0", "underscore.string": "2.3.3", - "zone.js": "~0.10.3" + "zone.js": "~0.10.3", + "file-saver": "^2.0.5" }, "devDependencies": { "@angular-devkit/build-angular": "~0.1001.2", @@ -143,7 +144,8 @@ "@angular-eslint/schematics": "^0.6.0-beta.0", "@angular-eslint/template-parser": "0.6.0-beta.0", "@typescript-eslint/eslint-plugin": "4.3.0", - "@typescript-eslint/parser": "4.3.0" + "@typescript-eslint/parser": "4.3.0", + "@types/file-saver": "^2.0.1" }, "husky": { "hooks": { diff --git a/src/app/doubtfire-angular.module.ts b/src/app/doubtfire-angular.module.ts index 47a154e94..7adc2a4c8 100644 --- a/src/app/doubtfire-angular.module.ts +++ b/src/app/doubtfire-angular.module.ts @@ -116,6 +116,7 @@ import { EmojiModule } from '@ctrl/ngx-emoji-mart/ngx-emoji'; import { EmojiService } from './common/services/emoji.service'; import { TaskListItemComponent } from './projects/states/dashboard/directives/student-task-list/task-list-item/task-list-item.component'; import { CreatePortfolioTaskListItemComponent } from './projects/states/dashboard/directives/student-task-list/create-portfolio-task-list-item/create-portfolio-task-list-item.component'; +import { TaskDescriptionCardComponent } from './projects/states/dashboard/directives/task-dashboard/directives/task-description-card/task-description-card.component'; import { TaskCommentsViewerComponent } from './tasks/task-comments-viewer/task-comments-viewer.component'; import { MarkedPipe } from './common/pipes/marked.pipe'; import { UserIconComponent } from './common/user-icon/user-icon.component'; @@ -160,6 +161,7 @@ import { ScrollingModule } from '@angular/cdk/scrolling'; StudentCampusSelectComponent, TaskListItemComponent, CreatePortfolioTaskListItemComponent, + TaskDescriptionCardComponent, StatusIconComponent, TaskCommentsViewerComponent, UserIconComponent, diff --git a/src/app/doubtfire-angularjs.module.ts b/src/app/doubtfire-angularjs.module.ts index 798e2bc77..8461e8c42 100644 --- a/src/app/doubtfire-angularjs.module.ts +++ b/src/app/doubtfire-angularjs.module.ts @@ -92,7 +92,6 @@ import 'build/src/app/projects/states/dashboard/directives/task-dashboard/direct import 'build/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-submission-card/task-submission-card.js'; import 'build/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-due-card/task-due-card.js'; import 'build/src/app/projects/states/dashboard/directives/task-dashboard/directives/directives.js'; -import 'build/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-description-card/task-description-card.js'; import 'build/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-status-card/task-status-card.js'; import 'build/src/app/projects/states/dashboard/directives/task-dashboard/task-dashboard.js'; import 'build/src/app/projects/states/dashboard/dashboard.js'; @@ -280,6 +279,7 @@ import { StudentCampusSelectComponent } from './units/states/edit/directives/uni import { EmojiService } from './common/services/emoji.service'; import { TaskListItemComponent } from './projects/states/dashboard/directives/student-task-list/task-list-item/task-list-item.component'; import { CreatePortfolioTaskListItemComponent } from './projects/states/dashboard/directives/student-task-list/create-portfolio-task-list-item/create-portfolio-task-list-item.component'; +import { TaskDescriptionCardComponent } from './projects/states/dashboard/directives/task-dashboard/directives/task-description-card/task-description-card.component' import { TaskCommentsViewerComponent } from './tasks/task-comments-viewer/task-comments-viewer.component'; import { UserIconComponent } from './common/user-icon/user-icon.component'; import { PdfViewerComponent } from './common/pdf-viewer/pdf-viewer.component'; @@ -354,6 +354,8 @@ DoubtfireAngularJSModule.directive('taskListItem', downgradeComponent({ component: TaskListItemComponent })); DoubtfireAngularJSModule.directive('createPortfolioTaskListItem', downgradeComponent({ component: CreatePortfolioTaskListItemComponent })); +DoubtfireAngularJSModule.directive('taskDescriptionCard', + downgradeComponent({ component: TaskDescriptionCardComponent})); // Global configuration DoubtfireAngularJSModule.directive('taskCommentsViewer', diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/directives.coffee b/src/app/projects/states/dashboard/directives/task-dashboard/directives/directives.coffee index 628614aed..a7bbe48e8 100644 --- a/src/app/projects/states/dashboard/directives/task-dashboard/directives/directives.coffee +++ b/src/app/projects/states/dashboard/directives/task-dashboard/directives/directives.coffee @@ -1,5 +1,4 @@ angular.module('doubtfire.projects.states.dashboard.directives.task-dashboard.directives', [ - 'doubtfire.projects.states.dashboard.directives.task-dashboard.directives.task-description-card' 'doubtfire.projects.states.dashboard.directives.task-dashboard.directives.task-status-card' 'doubtfire.projects.states.dashboard.directives.task-dashboard.directives.task-assessment-card' 'doubtfire.projects.states.dashboard.directives.task-dashboard.directives.task-submission-card' diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-description-card/task-description-card.coffee b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-description-card/task-description-card.coffee deleted file mode 100644 index fa874d0d8..000000000 --- a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-description-card/task-description-card.coffee +++ /dev/null @@ -1,47 +0,0 @@ -angular.module('doubtfire.projects.states.dashboard.directives.task-dashboard.directives.task-description-card', []) -# -# Description of task information -# -.directive('taskDescriptionCard', -> - restrict: 'E' - templateUrl: 'projects/states/dashboard/directives/task-dashboard/directives/task-description-card/task-description-card.tpl.html' - scope: - task: '=' - taskDef: '=' - unit: '=' - controller: ($scope, Task, ExtensionModal, listenerService, analyticsService, gradeService, alertService) -> - # Cleanup - listeners = listenerService.listenTo($scope) - # Required changes when task changes - listeners.push $scope.$watch('taskDef.id', -> - return unless $scope.taskDef? - # Resource download URLs - $scope.urls = - taskSheet: "#{Task.getTaskPDFUrl($scope.unit, $scope.taskDef)}&as_attachment=true" - resources: Task.getTaskResourcesUrl($scope.unit, $scope.taskDef) - ) - # Analytics event for when task resource is downloaded - $scope.downloadEvent = (type) -> - analyticsService.event 'Task Sheet', "Downloaded Task #{type}" - # Expose grade names - $scope.grades = - names: gradeService.grades - acronyms: gradeService.gradeAcronyms - - $scope.dueDate = () -> - if $scope.task? - return $scope.task.localDueDateString() - else if $scope.taskDef? - return $scope.taskDef.target_date - else - return "" - - $scope.startDate = () -> - if $scope.taskDef? - return $scope.taskDef.start_date - else - return "" - - $scope.shouldShowDeadline = () -> - $scope.task?.daysUntilDeadlineDate() <= 14 || false -) diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-description-card/task-description-card.component.html b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-description-card/task-description-card.component.html new file mode 100644 index 000000000..0912fc41e --- /dev/null +++ b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-description-card/task-description-card.component.html @@ -0,0 +1,36 @@ + + + Task Details + + +
+
    +
  • + {{grades.names[taskDef.target_grade]}} Task +
  • +
  • + Start Date + — + Aim to start this task by {{startDate() | date : 'EEE d MMM' : '+0000'}}. +
  • +
  • + Extension + — + You have an extension for {{task.extensions}} week{{task.extensions > 1 ? "s" : ""}}. +
  • +
  • + Due Date + — + Aim to complete by {{dueDate() | date : 'EEE d MMM' : '+0000'}}. +
  • +
+
+ + + + +
diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-description-card/task-description-card.component.scss b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-description-card/task-description-card.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-description-card/task-description-card.component.ts b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-description-card/task-description-card.component.ts new file mode 100644 index 000000000..b905823f5 --- /dev/null +++ b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-description-card/task-description-card.component.ts @@ -0,0 +1,52 @@ +import { Component, Input, Inject } from '@angular/core'; +import { Moment } from 'moment'; +import { gradeService, Task } from 'src/app/ajs-upgraded-providers'; +import { saveAs } from 'file-saver'; + +@Component({ + selector: 'task-description-card', + templateUrl: 'task-description-card.component.html', + styleUrls: ['task-description-card.component.scss'] +}) +export class TaskDescriptionCardComponent { + @Input() task: any; + @Input() taskDef: any; + @Input() unit: any; + + public grades: {names: any, acronyms: any}; + + constructor( + @Inject(gradeService) private GradeService: any, + @Inject(Task) private taskAPI: any, + ) { + this.grades = { + names: GradeService.grades, + acronyms: GradeService.gradeAcronyms + }; + } + + public downloadTaskSheet() { + saveAs(this.taskAPI.getTaskPDFUrl(this.unit, this.taskDef), `${this.unit.code}-${this.taskDef.abbreviation}-TaskSheet.pdf`); + } + + public downloadResources() { + saveAs(this.taskAPI.getTaskResourcesUrl(this.unit, `${this.unit.code}-${this.taskDef.abbreviation}-TaskResources.zip`)); + } + + public dueDate() : Moment { + if (this.task) + return this.task.localDueDate(); + else if (this.taskDef) + return this.taskDef.target_date; + else + return undefined; + } + + public startDate() : Moment { + return this.taskDef?.start_date; + } + + public shouldShowDeadline() : boolean { + return this.task && this.task.daysUntilDeadlineDate() <= 14 + } +} diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-description-card/task-description-card.scss b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-description-card/task-description-card.scss deleted file mode 100644 index 6b94e3050..000000000 --- a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-description-card/task-description-card.scss +++ /dev/null @@ -1,8 +0,0 @@ -task-description-card { - .card-footer { - .btn { - width: 13em; - margin-top: 3px; - } - } -} diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-description-card/task-description-card.tpl.html b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-description-card/task-description-card.tpl.html deleted file mode 100644 index a6f05bc8f..000000000 --- a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-description-card/task-description-card.tpl.html +++ /dev/null @@ -1,51 +0,0 @@ -
-
-

Description

-
-
-
-
- - -
diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/task-dashboard.tpl.html b/src/app/projects/states/dashboard/directives/task-dashboard/task-dashboard.tpl.html index ad611b779..52516a1fa 100644 --- a/src/app/projects/states/dashboard/directives/task-dashboard/task-dashboard.tpl.html +++ b/src/app/projects/states/dashboard/directives/task-dashboard/task-dashboard.tpl.html @@ -36,7 +36,7 @@ - +
diff --git a/src/app/units/states/tasks/viewer/directives/task-details-view/task-details-view.tpl.html b/src/app/units/states/tasks/viewer/directives/task-details-view/task-details-view.tpl.html index 8167d2692..b44d13e3b 100644 --- a/src/app/units/states/tasks/viewer/directives/task-details-view/task-details-view.tpl.html +++ b/src/app/units/states/tasks/viewer/directives/task-details-view/task-details-view.tpl.html @@ -6,6 +6,6 @@
- +