rubric redesign add load criteria

this commit removes the hard coded rubric criteria data and loads any
existing criteria in the UI. It also allows a user to open the criteria
modal by clicking on the "Edit" icon (ratings not loaded yet). It also
adds the ratings accordion that when clicked will show the rating.
details for a criterion. It also allows the rubric to be saved without
losing any criterion data.

closes EVAL-3940
closes EVAL-3633
flag=enhanced_rubrics

test plan:
- navigate to /rubrics
- click on a rubric for Edit
- verify that any existing criterion is loaded correctly and that the
  "Edit" icon is clickable and opens the criteria modal
- verify that the ratings accordion is clickable and shows the rating
  details for a criterion
- verify that the last option is the correctly numbered row with the
  "Draft New Criterion" button & "Create from Outcome button"
- verify that the rubric can be saved without losing any criterion data
- go back to /rubrics and click on "Create New Rubric"
- verify that no criterion is listed and only the buttons for creating
  new criterion are visible

Change-Id: I34e74b52c06ec25db84947f7c1566c554ec7425e
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/340042
Reviewed-by: Derek Williams <derek.williams@instructure.com>
Reviewed-by: Kai Bjorkman <kbjorkman@instructure.com>
QA-Review: Derek Williams <derek.williams@instructure.com>
Product-Review: Ravi Koll <ravi.koll@instructure.com>
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
This commit is contained in:
Chris Soto 2024-02-08 10:05:27 -07:00 committed by Christopher Soto
parent 04adf804f5
commit 9bb60b874c
11 changed files with 369 additions and 45 deletions

View File

@ -48,6 +48,7 @@ module Types
Loaders::IDLoader.for(LearningOutcome).load(object[:learning_outcome_id]) Loaders::IDLoader.for(LearningOutcome).load(object[:learning_outcome_id])
end end
field :learning_outcome_id, ID, null: true
field :long_description, String, null: true field :long_description, String, null: true
field :mastery_points, Float, null: true field :mastery_points, Float, null: true
field :points, Float, null: true field :points, Float, null: true

View File

@ -80,5 +80,14 @@ describe Types::RubricCriterionType do
rubric_type.resolve("criteria { ratings { _id }}") rubric_type.resolve("criteria { ratings { _id }}")
).to eq(rubric.criteria.map { |c| c[:ratings].map { |r| r[:id].to_s } }) ).to eq(rubric.criteria.map { |c| c[:ratings].map { |r| r[:id].to_s } })
end end
it "learning_outcome_id" do
rubric.criteria[0][:learning_outcome_id] = learning_outcome.id
rubric.save!
expect(
rubric_type.resolve("criteria { learningOutcomeId }")
).to eq rubric.criteria.pluck(:learning_outcome_id).map(&:to_s)
end
end end
end end

View File

@ -34,31 +34,31 @@ const I18n = useI18nScope('rubrics-criterion-modal')
export const DEFAULT_RUBRIC_RATINGS: RubricRating[] = [ export const DEFAULT_RUBRIC_RATINGS: RubricRating[] = [
{ {
id: '-1', id: '',
points: 4, points: 4,
description: I18n.t('Exceeds'), description: I18n.t('Exceeds'),
longDescription: '', longDescription: '',
}, },
{ {
id: '-1', id: '',
points: 3, points: 3,
description: I18n.t('Mastery'), description: I18n.t('Mastery'),
longDescription: '', longDescription: '',
}, },
{ {
id: '-1', id: '',
points: 2, points: 2,
description: I18n.t('Near'), description: I18n.t('Near'),
longDescription: '', longDescription: '',
}, },
{ {
id: '-1', id: '',
points: 1, points: 1,
description: I18n.t('Below'), description: I18n.t('Below'),
longDescription: '', longDescription: '',
}, },
{ {
id: '-1', id: '',
points: 0, points: 0,
description: I18n.t('No Evidence'), description: I18n.t('No Evidence'),
longDescription: '', longDescription: '',
@ -110,8 +110,8 @@ export const CriterionModal = ({isOpen, onDismiss}: CriterionModalProps) => {
<Heading>{I18n.t('Create New Criterion')}</Heading> <Heading>{I18n.t('Create New Criterion')}</Heading>
</Modal.Header> </Modal.Header>
<Modal.Body> <Modal.Body>
<View as="div" margin="x-small 0"> <View as="div" margin="0">
<View as="span" margin="0 small 0 0"> <View as="span" margin="0 small 0 0" themeOverride={{marginSmall: '1rem'}}>
<TextInput <TextInput
renderLabel={I18n.t('Criterion Name')} renderLabel={I18n.t('Criterion Name')}
placeholder={I18n.t('Enter the name')} placeholder={I18n.t('Enter the name')}
@ -148,8 +148,10 @@ export const CriterionModal = ({isOpen, onDismiss}: CriterionModalProps) => {
{I18n.t('Rating Name')} {I18n.t('Rating Name')}
</View> </View>
</Flex.Item> </Flex.Item>
<Flex.Item margin="0 0 0 small"> <Flex.Item>
<View as="div">{I18n.t('Rating Description')}</View> <View as="div" margin="0 0 0 small" themeOverride={{marginSmall: '1rem'}}>
{I18n.t('Rating Description')}
</View>
</Flex.Item> </Flex.Item>
</Flex> </Flex>
</View> </View>
@ -239,7 +241,7 @@ const RatingRow = ({rating, scale, onRemove}: RatingRowProps) => {
</View> </View>
</Flex.Item> </Flex.Item>
<Flex.Item shouldGrow={true} shouldShrink={true}> <Flex.Item shouldGrow={true} shouldShrink={true}>
<View as="div" margin="0 small"> <View as="div" margin="0 small" themeOverride={{marginSmall: '1rem'}}>
<TextInput <TextInput
renderLabel={<ScreenReaderContent>{I18n.t('Rating Description')}</ScreenReaderContent>} renderLabel={<ScreenReaderContent>{I18n.t('Rating Description')}</ScreenReaderContent>}
display="inline-block" display="inline-block"

View File

@ -32,9 +32,13 @@ import {Pill} from '@instructure/ui-pill'
import {View} from '@instructure/ui-view' import {View} from '@instructure/ui-view'
import {CriterionModal} from './CriterionModal' import {CriterionModal} from './CriterionModal'
const I18n = useI18nScope('rubrics-criteria-row') const I18n = useI18nScope('rubrics-criteria-new-row')
export const NewCriteriaRow = () => { type NewCriteriaRowProps = {
rowIndex: number
}
export const NewCriteriaRow = ({rowIndex}: NewCriteriaRowProps) => {
const [isCriterionModalOpen, setIsCriterionModalOpen] = React.useState(false) const [isCriterionModalOpen, setIsCriterionModalOpen] = React.useState(false)
return ( return (
@ -45,7 +49,7 @@ export const NewCriteriaRow = () => {
/> />
<Flex> <Flex>
<Flex.Item align="start" margin="small 0 0 0"> <Flex.Item align="start" margin="small 0 0 0">
<Text weight="bold">{I18n.t('2.')}</Text> <Text weight="bold">{rowIndex}.</Text>
</Flex.Item> </Flex.Item>
<Flex.Item margin="0 small" align="start" shouldGrow={true}> <Flex.Item margin="0 small" align="start" shouldGrow={true}>
<Button renderIcon={IconEditLine} onClick={() => setIsCriterionModalOpen(true)}> <Button renderIcon={IconEditLine} onClick={() => setIsCriterionModalOpen(true)}>

View File

@ -16,7 +16,10 @@
* with this program. If not, see <http://www.gnu.org/licenses/>. * with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import React from 'react' import React, {useState} from 'react'
import {useScope as useI18nScope} from '@canvas/i18n'
import type {RubricCriterion, RubricRating} from '@canvas/rubrics/react/types/rubric'
import {possibleString} from '@canvas/rubrics/react/Points'
import {AccessibleContent} from '@instructure/ui-a11y-content' import {AccessibleContent} from '@instructure/ui-a11y-content'
import {Flex} from '@instructure/ui-flex' import {Flex} from '@instructure/ui-flex'
import {Tag} from '@instructure/ui-tag' import {Tag} from '@instructure/ui-tag'
@ -25,20 +28,35 @@ import {View} from '@instructure/ui-view'
import {Pill} from '@instructure/ui-pill' import {Pill} from '@instructure/ui-pill'
import {IconButton} from '@instructure/ui-buttons' import {IconButton} from '@instructure/ui-buttons'
import { import {
IconArrowOpenDownLine,
IconArrowOpenEndLine,
IconDragHandleLine, IconDragHandleLine,
IconDuplicateLine, IconDuplicateLine,
IconEditLine, IconEditLine,
IconTrashLine, IconTrashLine,
} from '@instructure/ui-icons' } from '@instructure/ui-icons'
import {CriterionModal} from './CriterionModal'
const I18n = useI18nScope('rubrics-criteria-row')
type RubricCriteriaRowProps = {
criterion: RubricCriterion
rowIndex: number
}
export const RubricCriteriaRow = ({criterion, rowIndex}: RubricCriteriaRowProps) => {
const {description, longDescription, points} = criterion
const [isCriterionModalOpen, setIsCriterionModalOpen] = useState(false)
export const RubricCriteriaRow = () => {
return ( return (
<> <>
<Flex> <Flex data-testid="rubric-criteria-row">
<Flex.Item align="start"> <Flex.Item align="start" shouldShrink={true}>
<Text weight="bold">1.</Text> <Text weight="bold" data-testid="rubric-criteria-row-index">
{rowIndex}.
</Text>
</Flex.Item> </Flex.Item>
<Flex.Item margin="0 small" align="start" shouldGrow={true}> <Flex.Item margin="0 small" align="start" shouldGrow={true} shouldShrink={true}>
<View as="div"> <View as="div">
<Tag <Tag
text={ text={
@ -56,13 +74,11 @@ export const RubricCriteriaRow = () => {
}} }}
/> />
</View> </View>
<View as="div" margin="small 0 0 0"> <View as="div" margin="small 0 0 0" data-testid="rubric-criteria-row-description">
<Text weight="bold">Effective Use of Space</Text> <Text weight="bold">{description}</Text>
</View> </View>
<View as="div"> <View as="div" data-testid="rubric-criteria-row-long-description">
<Text> <Text>{longDescription}</Text>
Great use of space to show depth with use of foreground, middleground, and background.{' '}
</Text>
</View> </View>
</Flex.Item> </Flex.Item>
<Flex.Item align="start"> <Flex.Item align="start">
@ -75,24 +91,146 @@ export const RubricCriteriaRow = () => {
infoColor: 'white', infoColor: 'white',
}} }}
> >
<Text size="x-small">10 pts</Text> <Text data-testid="rubric-criteria-row-points" size="x-small">
{possibleString(points)}
</Text>
</Pill> </Pill>
<IconButton withBackground={false} withBorder={false} screenReaderLabel="" size="small"> <IconButton
withBackground={false}
withBorder={false}
screenReaderLabel={I18n.t('Move Criterion')}
size="small"
>
<IconDragHandleLine /> <IconDragHandleLine />
</IconButton> </IconButton>
<IconButton withBackground={false} withBorder={false} screenReaderLabel="" size="small"> <IconButton
withBackground={false}
withBorder={false}
screenReaderLabel={I18n.t('Edit Criterion')}
onClick={() => setIsCriterionModalOpen(true)}
size="small"
>
<IconEditLine /> <IconEditLine />
</IconButton> </IconButton>
<IconButton withBackground={false} withBorder={false} screenReaderLabel="" size="small"> <IconButton
withBackground={false}
withBorder={false}
screenReaderLabel={I18n.t('Delete Criterion')}
size="small"
>
<IconTrashLine /> <IconTrashLine />
</IconButton> </IconButton>
<IconButton withBackground={false} withBorder={false} screenReaderLabel="" size="small"> <IconButton
withBackground={false}
withBorder={false}
screenReaderLabel={I18n.t('Duplicate Criterion')}
size="small"
>
<IconDuplicateLine /> <IconDuplicateLine />
</IconButton> </IconButton>
</Flex.Item> </Flex.Item>
</Flex> </Flex>
<RatingScaleAccordion ratings={criterion.ratings} />
<CriterionModal
isOpen={isCriterionModalOpen}
onDismiss={() => setIsCriterionModalOpen(false)}
/>
<View as="hr" margin="medium 0 small 0" /> <View as="hr" margin="medium 0 small 0" />
</> </>
) )
} }
type RatingScaleAccordionProps = {
ratings: RubricRating[]
}
const RatingScaleAccordion = ({ratings}: RatingScaleAccordionProps) => {
const [ratingsOpen, setRatingsOpen] = useState(false)
return (
<View as="div" padding="medium 0 0 medium" themeOverride={{paddingMedium: '1.5rem'}}>
<View
as="button"
cursor="pointer"
onClick={() => setRatingsOpen(!ratingsOpen)}
background="transparent"
display="block"
borderWidth="none"
textAlign="start"
type="button"
position="relative"
>
{ratingsOpen ? (
<IconArrowOpenDownLine width="18" height="18" />
) : (
<IconArrowOpenEndLine width="18" height="18" />
)}
<View as="span" margin="0 0 0 small" data-testid="criterion-row-rating-accordion">
<Text>
{I18n.t('Rating Scale')}: {ratings.length}
</Text>
</View>
</View>
{ratingsOpen &&
ratings.map((rating, index) => {
const scale = ratings.length - (index + 1)
const spacing = index === 0 ? '1.5rem' : '2.25rem'
return (
<RatingScaleAccordionItem
rating={rating}
key={`rating-scale-item-${rating.id}`}
scale={scale}
spacing={spacing}
/>
)
})}
</View>
)
}
type RatingScaleAccordionItemProps = {
rating: RubricRating
scale: number
spacing: string
}
const RatingScaleAccordionItem = ({rating, scale, spacing}: RatingScaleAccordionItemProps) => {
return (
<View
as="div"
margin="small 0 0 0"
themeOverride={{marginSmall: spacing}}
data-testid="rating-scale-accordion-item"
>
<Flex>
<Flex.Item align="start">
<View
as="div"
width="2.25rem"
margin="0 0 0 small"
themeOverride={{marginSmall: '0.25rem'}}
>
<Text width="0.75rem">{scale}</Text>
</View>
</Flex.Item>
<Flex.Item align="start">
<View as="div" width="7.063rem">
<View as="div" maxWidth="5.563rem">
<Text>{rating.description}</Text>
</View>
</View>
</Flex.Item>
<Flex.Item shouldShrink={true} shouldGrow={true} align="start">
<View as="div">
<Text>{rating.longDescription}</Text>
</View>
</Flex.Item>
<Flex.Item align="start">
<View as="div" margin="0 0 0 medium" themeOverride={{marginMedium: '1.5rem'}}>
<Text>{possibleString(rating.points)}</Text>
</View>
</Flex.Item>
</Flex>
</View>
)
}

View File

@ -115,7 +115,7 @@ describe('RubricForm Tests', () => {
it('will navigate back to /rubrics after successfully saving', async () => { it('will navigate back to /rubrics after successfully saving', async () => {
jest jest
.spyOn(RubricFormQueries, 'saveRubric') .spyOn(RubricFormQueries, 'saveRubric')
.mockImplementation(() => Promise.resolve({id: '1', title: 'Rubric 1'})) .mockImplementation(() => Promise.resolve({id: '1', title: 'Rubric 1', pointsPossible: 10}))
const {getByTestId} = renderComponent() const {getByTestId} = renderComponent()
const titleInput = getByTestId('rubric-form-title') const titleInput = getByTestId('rubric-form-title')
fireEvent.change(titleInput, {target: {value: 'Rubric 1'}}) fireEvent.change(titleInput, {target: {value: 'Rubric 1'}})
@ -125,4 +125,72 @@ describe('RubricForm Tests', () => {
expect(getSRAlert()).toEqual('Rubric saved successfully') expect(getSRAlert()).toEqual('Rubric saved successfully')
}) })
}) })
describe('rubric criteria', () => {
beforeEach(() => {
jest.spyOn(Router, 'useParams').mockReturnValue({rubricId: '1'})
})
it('renders all criteria rows for a rubric', () => {
queryClient.setQueryData(['fetch-rubric-1'], RUBRICS_QUERY_RESPONSE)
const {criteria = []} = RUBRICS_QUERY_RESPONSE
const {queryAllByTestId} = renderComponent()
const criteriaRows = queryAllByTestId('rubric-criteria-row')
const criteriaRowDescriptions = queryAllByTestId('rubric-criteria-row-description')
const criteriaRowLongDescriptions = queryAllByTestId('rubric-criteria-row-long-description')
const criteriaRowPoints = queryAllByTestId('rubric-criteria-row-points')
const criteriaRowIndexes = queryAllByTestId('rubric-criteria-row-index')
expect(criteriaRows.length).toEqual(2)
expect(criteriaRowDescriptions[0]).toHaveTextContent(criteria[0].description)
expect(criteriaRowDescriptions[1]).toHaveTextContent(criteria[1].description)
expect(criteriaRowLongDescriptions[0]).toHaveTextContent(criteria[0].longDescription)
expect(criteriaRowLongDescriptions[1]).toHaveTextContent(criteria[1].longDescription)
expect(criteriaRowPoints[0]).toHaveTextContent(criteria[0].points.toString())
expect(criteriaRowPoints[1]).toHaveTextContent(criteria[1].points.toString())
expect(criteriaRowIndexes[0]).toHaveTextContent('1.')
expect(criteriaRowIndexes[1]).toHaveTextContent('2')
})
it('renders the criterion ratings accordion button', () => {
queryClient.setQueryData(['fetch-rubric-1'], RUBRICS_QUERY_RESPONSE)
const {criteria = []} = RUBRICS_QUERY_RESPONSE
const {queryAllByTestId} = renderComponent()
const ratingScaleAccordion = queryAllByTestId('criterion-row-rating-accordion')
expect(ratingScaleAccordion.length).toEqual(2)
expect(ratingScaleAccordion[0]).toHaveTextContent(
`Rating Scale: ${criteria[0].ratings.length}`
)
expect(ratingScaleAccordion[1]).toHaveTextContent(
`Rating Scale: ${criteria[0].ratings.length}`
)
})
it('renders the criterion ratings accordion items when button is clicked', () => {
queryClient.setQueryData(['fetch-rubric-1'], RUBRICS_QUERY_RESPONSE)
const {criteria = []} = RUBRICS_QUERY_RESPONSE
const {queryAllByTestId} = renderComponent()
const ratingScaleAccordion = queryAllByTestId('criterion-row-rating-accordion')
fireEvent.click(ratingScaleAccordion[0])
const ratingScaleAccordionItems = queryAllByTestId('rating-scale-accordion-item')
expect(ratingScaleAccordionItems.length).toEqual(criteria[0].ratings.length)
})
it('does not render the criterion ratings accordion items when accordion is closed', () => {
queryClient.setQueryData(['fetch-rubric-1'], RUBRICS_QUERY_RESPONSE)
const {queryAllByTestId} = renderComponent()
const ratingScaleAccordion = queryAllByTestId('criterion-row-rating-accordion')
fireEvent.click(ratingScaleAccordion[0])
fireEvent.click(ratingScaleAccordion[0])
const ratingScaleAccordionItems = queryAllByTestId('rating-scale-accordion-item')
expect(ratingScaleAccordionItems.length).toEqual(0)
})
})
}) })

View File

@ -16,7 +16,61 @@
* with this program. If not, see <http://www.gnu.org/licenses/>. * with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
export const RUBRICS_QUERY_RESPONSE = { import type {Rubric} from '@canvas/rubrics/react/types/rubric'
export const RUBRICS_QUERY_RESPONSE: Rubric = {
id: '1', id: '1',
title: 'Rubric 1', title: 'Rubric 1',
criteriaCount: 2,
locations: [],
pointsPossible: 10,
workflowState: 'active',
criteria: [
{
id: '1',
points: 5,
description: 'Criterion 1',
longDescription: 'Long description for criterion 1',
ignoreForScoring: false,
masteryPoints: 3,
criterionUseRange: false,
ratings: [
{
id: '1',
description: 'Rating 1',
longDescription: 'Long description for rating 1',
points: 5,
},
{
id: '2',
description: 'Rating 2',
longDescription: 'Long description for rating 2',
points: 0,
},
],
},
{
id: '2',
points: 5,
description: 'Criterion 2',
longDescription: 'Long description for criterion 2',
ignoreForScoring: false,
masteryPoints: 3,
criterionUseRange: false,
ratings: [
{
id: '1',
description: 'Rating 1',
longDescription: 'Long description for rating 1',
points: 5,
},
{
id: '2',
description: 'Rating 2',
longDescription: 'Long description for rating 2',
points: 0,
},
],
},
],
} }

View File

@ -50,6 +50,8 @@ const {Option: SimpleSelectOption} = SimpleSelect
const defaultRubricForm: RubricFormProps = { const defaultRubricForm: RubricFormProps = {
title: '', title: '',
hidePoints: false, hidePoints: false,
criteria: [],
pointsPossible: 0,
} }
const translateRubricData = (fields: RubricQueryResponse): RubricFormProps => { const translateRubricData = (fields: RubricQueryResponse): RubricFormProps => {
@ -57,6 +59,8 @@ const translateRubricData = (fields: RubricQueryResponse): RubricFormProps => {
id: fields.id, id: fields.id,
title: fields.title ?? '', title: fields.title ?? '',
hidePoints: fields.hidePoints ?? false, hidePoints: fields.hidePoints ?? false,
criteria: fields.criteria ?? [],
pointsPossible: fields.pointsPossible ?? 0,
} }
} }
@ -194,7 +198,7 @@ export const RubricForm = () => {
</Flex.Item> </Flex.Item>
<Flex.Item> <Flex.Item>
<Text weight="bold" size="xx-large" themeOverride={{fontWeightBold: 400}}> <Text weight="bold" size="xx-large" themeOverride={{fontWeightBold: 400}}>
10 {rubricForm.pointsPossible}
</Text> </Text>
<View as="span" margin="0 0 0 small"> <View as="span" margin="0 0 0 small">
<Text weight="light" size="x-large"> <Text weight="light" size="x-large">
@ -213,9 +217,11 @@ export const RubricForm = () => {
overflowX="hidden" overflowX="hidden"
as="main" as="main"
> >
<RubricCriteriaRow /> {rubricForm.criteria.map((criterion, index) => (
<RubricCriteriaRow key={criterion.id} criterion={criterion} rowIndex={index + 1} />
))}
<NewCriteriaRow /> <NewCriteriaRow rowIndex={rubricForm.criteria.length + 1} />
</Flex.Item> </Flex.Item>
</Flex> </Flex>

View File

@ -30,11 +30,26 @@ const RUBRIC_QUERY = gql`
title title
hidePoints hidePoints
workflowState workflowState
pointsPossible
criteria {
id: _id
ratings {
description
longDescription
points
}
points
longDescription
description
}
} }
} }
` `
export type RubricQueryResponse = Pick<Rubric, 'id' | 'title' | 'criteria' | 'hidePoints'> export type RubricQueryResponse = Pick<
Rubric,
'id' | 'title' | 'criteria' | 'hidePoints' | 'pointsPossible'
>
type FetchRubricResponse = { type FetchRubricResponse = {
rubric: RubricQueryResponse rubric: RubricQueryResponse
@ -55,6 +70,22 @@ export const saveRubric = async (rubric: RubricFormProps): Promise<RubricQueryRe
const url = `${urlPrefix}/rubrics/${id ?? ''}` const url = `${urlPrefix}/rubrics/${id ?? ''}`
const method = id ? 'PATCH' : 'POST' const method = id ? 'PATCH' : 'POST'
const criteria = rubric.criteria.map(criterion => {
return {
id: criterion.id,
description: criterion.description,
long_description: criterion.longDescription,
points: criterion.points,
learning_outcome_id: criterion.learningOutcomeId,
ratings: criterion.ratings.map(rating => ({
description: rating.description,
long_description: rating.longDescription,
points: rating.points,
id: rating.id,
})),
}
})
const response = await fetch(url, { const response = await fetch(url, {
method, method,
headers: { headers: {
@ -66,6 +97,7 @@ export const saveRubric = async (rubric: RubricFormProps): Promise<RubricQueryRe
rubric: { rubric: {
title, title,
hide_points: hidePoints, hide_points: hidePoints,
criteria,
}, },
rubric_association: { rubric_association: {
association_id: accountId ?? courseId, association_id: accountId ?? courseId,
@ -89,5 +121,6 @@ export const saveRubric = async (rubric: RubricFormProps): Promise<RubricQueryRe
title: savedRubric.title, title: savedRubric.title,
hidePoints: savedRubric.hide_points, hidePoints: savedRubric.hide_points,
criteria: savedRubric.criteria, criteria: savedRubric.criteria,
pointsPossible: savedRubric.points_possible,
} }
} }

View File

@ -16,12 +16,16 @@
* with this program. If not, see <http://www.gnu.org/licenses/>. * with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import type {RubricCriterion} from '@canvas/rubrics/react/types/rubric'
export type RubricFormProps = { export type RubricFormProps = {
id?: string id?: string
title: string title: string
hidePoints: boolean hidePoints: boolean
accountId?: string accountId?: string
courseId?: string courseId?: string
criteria: RubricCriterion[]
pointsPossible: number
} }
export type RubricFormValueTypes = string | boolean export type RubricFormValueTypes = string | boolean

View File

@ -18,14 +18,7 @@
export type Rubric = { export type Rubric = {
id: string id: string
criteria?: { criteria?: RubricCriterion[]
points: number
description: string
longDescription: string
ignoreForScoring: boolean
masteryPoints: number
criterionUseRange: boolean
}[]
criteriaCount: number criteriaCount: number
hidePoints?: boolean hidePoints?: boolean
locations: string[] locations: string[]
@ -34,6 +27,18 @@ export type Rubric = {
workflowState?: string workflowState?: string
} }
export type RubricCriterion = {
id: string
points: number
description: string
longDescription: string
ignoreForScoring: boolean
masteryPoints: number
criterionUseRange: boolean
ratings: RubricRating[]
learningOutcomeId?: string
}
export type RubricRating = { export type RubricRating = {
id: string id: string
description: string description: string