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 <svc.cloudjenkins@instructure.com>
Reviewed-by: Kai Bjorkman <kbjorkman@instructure.com>
Reviewed-by: Derek Williams <derek.williams@instructure.com>
QA-Review: Cameron Ray <cameron.ray@instructure.com>
Product-Review: Cameron Ray <cameron.ray@instructure.com>
Build-Review: Andrea Cirulli <andrea.cirulli@instructure.com>
This commit is contained in:
Aaron Shafovaloff 2023-02-10 21:39:38 -07:00
parent 01b718aa98
commit 419a4906e2
16 changed files with 74 additions and 30 deletions

View File

@ -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'),

View File

@ -16,7 +16,7 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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)) {

View File

@ -3,5 +3,10 @@
"private": true,
"version": "1.0.0",
"author": "neme",
"main": "./index.js"
"main": "./index.ts",
"babel": {
"presets": [
"@babel/preset-typescript"
]
}
}

View File

@ -16,10 +16,13 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
// 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 = {
"'": '&#x27;',
'/': '&#x2F;',
'`': '&#x60;', // for old versions of IE
'=': '&#x3D;' // in case of unquoted attributes
}
'=': '&#x3D;', // 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<T>(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])
}

View File

@ -3,5 +3,10 @@
"private": true,
"version": "1.0.0",
"author": "neme",
"main": "./index.js"
"main": "./index.ts",
"babel": {
"presets": [
"@babel/preset-typescript"
]
}
}

View File

@ -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'),

View File

@ -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'),

6
ui/api.d.ts vendored
View File

@ -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

View File

@ -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>(student)
escapedStudent.name = unescapedName
escapedStudent.sortable_name = unescapedSortableName
escapedStudent.first_name = unescapedFirstName

View File

@ -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),
}))
}

View File

@ -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

View File

@ -213,7 +213,7 @@ export type CourseContent = {
gradingSchemes: GradingScheme[]
gradingPeriodAssignments: GradingPeriodAssignmentMap
assignmentStudentVisibility: {[assignmentId: string]: null | StudentMap}
latePolicy: LatePolicyCamelized
latePolicy?: LatePolicyCamelized
students: StudentDatastore
modulesById: ModuleMap
}

View File

@ -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<LatePolicyCamelized>(options.late_policy)
: undefined,
students: new StudentDatastore({}, {}),
modulesById: {},
}

View File

@ -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, '<p></p>').replace(/\n/g, '<br>')
}
}

12
ui/imports.d.ts vendored
View File

@ -209,3 +209,15 @@ declare module '@instructure/ui-badge' {
}
}
}
declare module 'convert-case' {
export function camelize<T>(props: {[key: string]: unknown}): T
export function underscore<T>(props: {[key: string]: unknown}): T
}
declare module 'html-escape' {
type Escapeable = string | number | {[key: string]: Escapeable}
export function escape<T>(strOrObject: Escapeable): T
export function htmlEscape(str: string): string
export function unescape(str: string): string
}

View File

@ -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'