From 419a4906e2ffd570c5fbde9a6ccfa62adb332c9c Mon Sep 17 00:00:00 2001 From: Aaron Shafovaloff Date: Fri, 10 Feb 2023 21:39:38 -0700 Subject: [PATCH] Specify types for convert-case and html-escape Test plan: - Existing tests pass flag=none Change-Id: Ib4ff7609f4ca71242cef7ed91de53a5ef177112c Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/310891 Tested-by: Service Cloud Jenkins Reviewed-by: Kai Bjorkman Reviewed-by: Derek Williams QA-Review: Cameron Ray Product-Review: Cameron Ray Build-Review: Andrea Cirulli --- .storybook/main.js | 2 ++ packages/convert-case/{index.js => index.ts} | 19 +++++++++++-------- packages/convert-case/package.json | 7 ++++++- packages/html-escape/{index.js => index.ts} | 17 ++++++++++------- packages/html-escape/package.json | 7 ++++++- ui-build/webpack-for-karma/index.js | 2 ++ ui-build/webpack/index.js | 2 ++ ui/api.d.ts | 6 +++--- .../default_gradebook/Gradebook.utils.ts | 5 +++-- .../apis/GradebookSettingsModalApi.ts | 4 +++- .../components/SubmissionTray.tsx | 2 +- .../react/default_gradebook/gradebook.d.ts | 2 +- .../react/default_gradebook/initialState.ts | 5 ++++- .../jquery/speed_grader.utils.tsx | 2 +- ui/imports.d.ts | 12 ++++++++++++ ui/shared/grading/SubmissionHelper.ts | 10 +++++++--- 16 files changed, 74 insertions(+), 30 deletions(-) rename packages/convert-case/{index.js => index.ts} (78%) rename packages/html-escape/{index.js => index.ts} (85%) diff --git a/.storybook/main.js b/.storybook/main.js index a92bc6aa51f..e89a4325f3c 100644 --- a/.storybook/main.js +++ b/.storybook/main.js @@ -61,6 +61,8 @@ module.exports = { path.resolve(canvasDir, 'packages/jquery-sticky'), path.resolve(canvasDir, 'packages/mathml'), path.resolve(canvasDir, 'packages/defer-promise'), + path.resolve(canvasDir, 'packages/convert-case'), + path.resolve(canvasDir, 'packages/html-escape'), path.resolve(canvasDir, 'packages/persistent-array'), path.resolve(canvasDir, 'packages/slickgrid'), path.resolve(canvasDir, 'packages/with-breakpoints'), diff --git a/packages/convert-case/index.js b/packages/convert-case/index.ts similarity index 78% rename from packages/convert-case/index.js rename to packages/convert-case/index.ts index f8d47977cac..4fd78839e6e 100644 --- a/packages/convert-case/index.js +++ b/packages/convert-case/index.ts @@ -16,7 +16,7 @@ * with this program. If not, see . */ -function camelizeString(str, lowerFirst) { +function camelizeString(str: string, lowerFirst: boolean = false) { return (str || '').replace(/(?:^|[-_])(\w)/g, (_, c, index) => { if (index === 0 && lowerFirst) { return c ? c.toLowerCase() : '' @@ -26,16 +26,17 @@ function camelizeString(str, lowerFirst) { }) } -function underscoreString(str) { +function underscoreString(str: string) { return str.replace(/([A-Z])/g, $1 => `_${$1.toLowerCase()}`) } // Convert all property keys in an object to camelCase -export function camelize(props) { - let prop - const attrs = {} +export function camelize(props: {[key: string]: any}) { + const attrs: { + [key: string]: any + } = {} - for (prop in props) { + for (const prop in props) { if (props.hasOwnProperty(prop)) { attrs[camelizeString(prop, true)] = props[prop] } @@ -44,9 +45,11 @@ export function camelize(props) { return attrs } -export function underscore(props) { +export function underscore(props: {[key: string]: any}) { let prop - const attrs = {} + const attrs: { + [key: string]: any + } = {} for (prop in props) { if (props.hasOwnProperty(prop)) { diff --git a/packages/convert-case/package.json b/packages/convert-case/package.json index 234715cc951..ad94da27d1a 100644 --- a/packages/convert-case/package.json +++ b/packages/convert-case/package.json @@ -3,5 +3,10 @@ "private": true, "version": "1.0.0", "author": "neme", - "main": "./index.js" + "main": "./index.ts", + "babel": { + "presets": [ + "@babel/preset-typescript" + ] + } } diff --git a/packages/html-escape/index.js b/packages/html-escape/index.ts similarity index 85% rename from packages/html-escape/index.js rename to packages/html-escape/index.ts index 84f6579008e..31b9d7b054c 100644 --- a/packages/html-escape/index.js +++ b/packages/html-escape/index.ts @@ -16,10 +16,13 @@ * with this program. If not, see . */ +// eslint-disable-next-line import/no-extraneous-dependencies import INST from 'browser-sniffer' class SafeString { - constructor(string) { + 'string': string + + constructor(string: any) { this.string = typeof string === 'string' ? string : `${string}` } @@ -36,10 +39,10 @@ const ENTITIES = { "'": ''', '/': '/', '`': '`', // for old versions of IE - '=': '=' // in case of unquoted attributes -} + '=': '=', // in case of unquoted attributes +} as const -function htmlEscape(str) { +export function htmlEscape(str: string): string { // ideally we should wrap this in a SafeString, but this is how it has // always worked :-/ return str.replace(/[&<>"'\/`=]/g, c => ENTITIES[c]) @@ -47,7 +50,7 @@ function htmlEscape(str) { // Escapes HTML tags from string, or object string props of `strOrObject`. // returns the new string, or the object with escaped properties -export default function escape(strOrObject) { +export default function escape(strOrObject: string | SafeString | Object) { if (typeof strOrObject === 'string') { return htmlEscape(strOrObject) } else if (strOrObject instanceof SafeString) { @@ -62,7 +65,7 @@ export default function escape(strOrObject) { strOrObject[k] = escape(v) } } - return strOrObject + return strOrObject as T } escape.SafeString = SafeString @@ -78,7 +81,7 @@ const UNESCAPE_ENTITIES = Object.keys(ENTITIES).reduce((map, key) => { const unescapeSource = `(?:${Object.keys(UNESCAPE_ENTITIES).join('|')})` const UNESCAPE_REGEX = new RegExp(unescapeSource, 'g') -function unescape(str) { +function unescape(str: string) { return str.replace(UNESCAPE_REGEX, match => UNESCAPE_ENTITIES[match]) } diff --git a/packages/html-escape/package.json b/packages/html-escape/package.json index 0b9a7dcf331..cac0f03bce5 100644 --- a/packages/html-escape/package.json +++ b/packages/html-escape/package.json @@ -3,5 +3,10 @@ "private": true, "version": "1.0.0", "author": "neme", - "main": "./index.js" + "main": "./index.ts", + "babel": { + "presets": [ + "@babel/preset-typescript" + ] + } } diff --git a/ui-build/webpack-for-karma/index.js b/ui-build/webpack-for-karma/index.js index 0b4009831b2..e9792bf1a39 100644 --- a/ui-build/webpack-for-karma/index.js +++ b/ui-build/webpack-for-karma/index.js @@ -66,6 +66,8 @@ module.exports = { path.join(canvasDir, 'packages/jquery-selectmenu'), path.join(canvasDir, 'packages/mathml'), path.join(canvasDir, 'packages/defer-promise'), + path.resolve(canvasDir, 'packages/convert-case'), + path.resolve(canvasDir, 'packages/html-escape'), path.join(canvasDir, 'packages/persistent-array'), path.join(canvasDir, 'packages/slickgrid'), path.join(canvasDir, 'packages/with-breakpoints'), diff --git a/ui-build/webpack/index.js b/ui-build/webpack/index.js index 4660d79618b..6d9a58af0c5 100644 --- a/ui-build/webpack/index.js +++ b/ui-build/webpack/index.js @@ -263,6 +263,8 @@ module.exports = { path.resolve(canvasDir, 'packages/jquery-sticky'), path.resolve(canvasDir, 'packages/mathml'), path.resolve(canvasDir, 'packages/defer-promise'), + path.resolve(canvasDir, 'packages/convert-case'), + path.resolve(canvasDir, 'packages/html-escape'), path.resolve(canvasDir, 'packages/persistent-array'), path.resolve(canvasDir, 'packages/slickgrid'), path.resolve(canvasDir, 'packages/with-breakpoints'), diff --git a/ui/api.d.ts b/ui/api.d.ts index 33602a8cae9..e1e30444e97 100644 --- a/ui/api.d.ts +++ b/ui/api.d.ts @@ -63,18 +63,18 @@ export type Student = Readonly<{ avatar_url?: string created_at: string email: null | string - first_name: string group_ids: string[] id: string integration_id: null | string - last_name: string login_id: string - name: string short_name: string sis_import_id: null | string sis_user_id: null | string }> & { enrollments: Enrollment[] + first_name: string + last_name: string + name: string } & Partial<{ computed_current_score: number computed_final_score: number diff --git a/ui/features/gradebook/react/default_gradebook/Gradebook.utils.ts b/ui/features/gradebook/react/default_gradebook/Gradebook.utils.ts index b06bb78be1a..d716d3c6ee9 100644 --- a/ui/features/gradebook/react/default_gradebook/Gradebook.utils.ts +++ b/ui/features/gradebook/react/default_gradebook/Gradebook.utils.ts @@ -44,6 +44,7 @@ import type { GradingPeriod, Module, Section, + Student, StudentGroup, StudentGroupCategory, StudentGroupCategoryMap, @@ -490,14 +491,14 @@ export function maxAssignmentCount( } // mutative -export function escapeStudentContent(student) { +export function escapeStudentContent(student: Student) { const unescapedName = student.name const unescapedSortableName = student.sortable_name const unescapedFirstName = student.first_name const unescapedLastName = student.last_name // TODO: selectively escape fields - const escapedStudent = htmlEscape(student) + const escapedStudent = htmlEscape(student) escapedStudent.name = unescapedName escapedStudent.sortable_name = unescapedSortableName escapedStudent.first_name = unescapedFirstName diff --git a/ui/features/gradebook/react/default_gradebook/apis/GradebookSettingsModalApi.ts b/ui/features/gradebook/react/default_gradebook/apis/GradebookSettingsModalApi.ts index cc3858ad4d6..badb2b6c0db 100644 --- a/ui/features/gradebook/react/default_gradebook/apis/GradebookSettingsModalApi.ts +++ b/ui/features/gradebook/react/default_gradebook/apis/GradebookSettingsModalApi.ts @@ -84,5 +84,7 @@ export function updateCourseSettings( } ) { const url = `/api/v1/courses/${courseId}/settings` - return axios.put(url, underscore(settings)).then(response => ({data: camelize(response.data)})) + return axios.put(url, underscore(settings)).then(response => ({ + data: camelize<{allowFinalGradeOverride: boolean}>(response.data), + })) } diff --git a/ui/features/gradebook/react/default_gradebook/components/SubmissionTray.tsx b/ui/features/gradebook/react/default_gradebook/components/SubmissionTray.tsx index d64b9a03b33..0f9a110fd9c 100644 --- a/ui/features/gradebook/react/default_gradebook/components/SubmissionTray.tsx +++ b/ui/features/gradebook/react/default_gradebook/components/SubmissionTray.tsx @@ -104,7 +104,7 @@ export type SubmissionTrayProps = { isOpen: boolean isFirstStudent: boolean isLastStudent: boolean - latePolicy: LatePolicyCamelized + latePolicy?: LatePolicyCamelized locale: string editSubmissionComment: (commentId: string | null) => void onClose: () => void diff --git a/ui/features/gradebook/react/default_gradebook/gradebook.d.ts b/ui/features/gradebook/react/default_gradebook/gradebook.d.ts index 72407b205c7..bd64f589e6b 100644 --- a/ui/features/gradebook/react/default_gradebook/gradebook.d.ts +++ b/ui/features/gradebook/react/default_gradebook/gradebook.d.ts @@ -213,7 +213,7 @@ export type CourseContent = { gradingSchemes: GradingScheme[] gradingPeriodAssignments: GradingPeriodAssignmentMap assignmentStudentVisibility: {[assignmentId: string]: null | StudentMap} - latePolicy: LatePolicyCamelized + latePolicy?: LatePolicyCamelized students: StudentDatastore modulesById: ModuleMap } diff --git a/ui/features/gradebook/react/default_gradebook/initialState.ts b/ui/features/gradebook/react/default_gradebook/initialState.ts index 053cbc7ea03..c0416eaf72d 100644 --- a/ui/features/gradebook/react/default_gradebook/initialState.ts +++ b/ui/features/gradebook/react/default_gradebook/initialState.ts @@ -24,6 +24,7 @@ import type { GradebookOptions, GradebookSettings, InitialActionStates, + LatePolicyCamelized, } from './gradebook.d' import type {GridDisplaySettings, FilterColumnsOptions} from './grid.d' import {camelize} from 'convert-case' @@ -132,7 +133,9 @@ export function getInitialCourseContent(options: GradebookOptions): CourseConten gradingSchemes: options.grading_schemes.map(camelize), gradingPeriodAssignments: {}, assignmentStudentVisibility: {}, - latePolicy: options.late_policy ? camelize(options.late_policy) : undefined, + latePolicy: options.late_policy + ? camelize(options.late_policy) + : undefined, students: new StudentDatastore({}, {}), modulesById: {}, } diff --git a/ui/features/speed_grader/jquery/speed_grader.utils.tsx b/ui/features/speed_grader/jquery/speed_grader.utils.tsx index 367a9bc1df8..1896eff71d2 100644 --- a/ui/features/speed_grader/jquery/speed_grader.utils.tsx +++ b/ui/features/speed_grader/jquery/speed_grader.utils.tsx @@ -136,7 +136,7 @@ export const configureRecognition = (recognition, messages) => { } // xsslint safeString.function linebreak - function linebreak(transcript) { + function linebreak(transcript: string) { return htmlEscape(transcript).replace(/\n\n/g, '

').replace(/\n/g, '
') } } diff --git a/ui/imports.d.ts b/ui/imports.d.ts index 7187fed51aa..2fc36f9581a 100644 --- a/ui/imports.d.ts +++ b/ui/imports.d.ts @@ -209,3 +209,15 @@ declare module '@instructure/ui-badge' { } } } + +declare module 'convert-case' { + export function camelize(props: {[key: string]: unknown}): T + export function underscore(props: {[key: string]: unknown}): T +} + +declare module 'html-escape' { + type Escapeable = string | number | {[key: string]: Escapeable} + export function escape(strOrObject: Escapeable): T + export function htmlEscape(str: string): string + export function unescape(str: string): string +} diff --git a/ui/shared/grading/SubmissionHelper.ts b/ui/shared/grading/SubmissionHelper.ts index 03d5bbd84d2..2fbc0c38e96 100644 --- a/ui/shared/grading/SubmissionHelper.ts +++ b/ui/shared/grading/SubmissionHelper.ts @@ -18,7 +18,11 @@ import {camelize, underscore} from 'convert-case' import {originalityReportSubmissionKey} from './originalityReportHelper' -import type {PlagiarismData, SimilarityEntry} from '@canvas/grading/grading.d' +import type { + PlagiarismData, + SimilarityEntry, + CamelizedSubmissionWithOriginalityReport, +} from '@canvas/grading/grading.d' export function isGraded(submission) { // TODO: remove when we no longer camelize data in Gradebook @@ -52,9 +56,9 @@ export function isHideable(submission) { // - "pending" reports (reports still being processed) // - scored reports, with higher scores (indicating more likely plagiarism) first export function extractSimilarityInfo(submission) { - const sub = camelize(submission) + const sub = camelize(submission) as CamelizedSubmissionWithOriginalityReport let plagiarismData - let type + let type: 'vericite' | 'turnitin' | 'originality_report' | null = null if (sub.vericiteData?.provider === 'vericite') { type = 'vericite'