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:
parent
04adf804f5
commit
9bb60b874c
|
@ -48,6 +48,7 @@ module Types
|
|||
Loaders::IDLoader.for(LearningOutcome).load(object[:learning_outcome_id])
|
||||
end
|
||||
|
||||
field :learning_outcome_id, ID, null: true
|
||||
field :long_description, String, null: true
|
||||
field :mastery_points, Float, null: true
|
||||
field :points, Float, null: true
|
||||
|
|
|
@ -80,5 +80,14 @@ describe Types::RubricCriterionType do
|
|||
rubric_type.resolve("criteria { ratings { _id }}")
|
||||
).to eq(rubric.criteria.map { |c| c[:ratings].map { |r| r[:id].to_s } })
|
||||
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
|
||||
|
|
|
@ -34,31 +34,31 @@ const I18n = useI18nScope('rubrics-criterion-modal')
|
|||
|
||||
export const DEFAULT_RUBRIC_RATINGS: RubricRating[] = [
|
||||
{
|
||||
id: '-1',
|
||||
id: '',
|
||||
points: 4,
|
||||
description: I18n.t('Exceeds'),
|
||||
longDescription: '',
|
||||
},
|
||||
{
|
||||
id: '-1',
|
||||
id: '',
|
||||
points: 3,
|
||||
description: I18n.t('Mastery'),
|
||||
longDescription: '',
|
||||
},
|
||||
{
|
||||
id: '-1',
|
||||
id: '',
|
||||
points: 2,
|
||||
description: I18n.t('Near'),
|
||||
longDescription: '',
|
||||
},
|
||||
{
|
||||
id: '-1',
|
||||
id: '',
|
||||
points: 1,
|
||||
description: I18n.t('Below'),
|
||||
longDescription: '',
|
||||
},
|
||||
{
|
||||
id: '-1',
|
||||
id: '',
|
||||
points: 0,
|
||||
description: I18n.t('No Evidence'),
|
||||
longDescription: '',
|
||||
|
@ -110,8 +110,8 @@ export const CriterionModal = ({isOpen, onDismiss}: CriterionModalProps) => {
|
|||
<Heading>{I18n.t('Create New Criterion')}</Heading>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<View as="div" margin="x-small 0">
|
||||
<View as="span" margin="0 small 0 0">
|
||||
<View as="div" margin="0">
|
||||
<View as="span" margin="0 small 0 0" themeOverride={{marginSmall: '1rem'}}>
|
||||
<TextInput
|
||||
renderLabel={I18n.t('Criterion Name')}
|
||||
placeholder={I18n.t('Enter the name')}
|
||||
|
@ -148,8 +148,10 @@ export const CriterionModal = ({isOpen, onDismiss}: CriterionModalProps) => {
|
|||
{I18n.t('Rating Name')}
|
||||
</View>
|
||||
</Flex.Item>
|
||||
<Flex.Item margin="0 0 0 small">
|
||||
<View as="div">{I18n.t('Rating Description')}</View>
|
||||
<Flex.Item>
|
||||
<View as="div" margin="0 0 0 small" themeOverride={{marginSmall: '1rem'}}>
|
||||
{I18n.t('Rating Description')}
|
||||
</View>
|
||||
</Flex.Item>
|
||||
</Flex>
|
||||
</View>
|
||||
|
@ -239,7 +241,7 @@ const RatingRow = ({rating, scale, onRemove}: RatingRowProps) => {
|
|||
</View>
|
||||
</Flex.Item>
|
||||
<Flex.Item shouldGrow={true} shouldShrink={true}>
|
||||
<View as="div" margin="0 small">
|
||||
<View as="div" margin="0 small" themeOverride={{marginSmall: '1rem'}}>
|
||||
<TextInput
|
||||
renderLabel={<ScreenReaderContent>{I18n.t('Rating Description')}</ScreenReaderContent>}
|
||||
display="inline-block"
|
||||
|
|
|
@ -32,9 +32,13 @@ import {Pill} from '@instructure/ui-pill'
|
|||
import {View} from '@instructure/ui-view'
|
||||
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)
|
||||
|
||||
return (
|
||||
|
@ -45,7 +49,7 @@ export const NewCriteriaRow = () => {
|
|||
/>
|
||||
<Flex>
|
||||
<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 margin="0 small" align="start" shouldGrow={true}>
|
||||
<Button renderIcon={IconEditLine} onClick={() => setIsCriterionModalOpen(true)}>
|
||||
|
|
|
@ -16,7 +16,10 @@
|
|||
* 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 {Flex} from '@instructure/ui-flex'
|
||||
import {Tag} from '@instructure/ui-tag'
|
||||
|
@ -25,20 +28,35 @@ import {View} from '@instructure/ui-view'
|
|||
import {Pill} from '@instructure/ui-pill'
|
||||
import {IconButton} from '@instructure/ui-buttons'
|
||||
import {
|
||||
IconArrowOpenDownLine,
|
||||
IconArrowOpenEndLine,
|
||||
IconDragHandleLine,
|
||||
IconDuplicateLine,
|
||||
IconEditLine,
|
||||
IconTrashLine,
|
||||
} 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 (
|
||||
<>
|
||||
<Flex>
|
||||
<Flex.Item align="start">
|
||||
<Text weight="bold">1.</Text>
|
||||
<Flex data-testid="rubric-criteria-row">
|
||||
<Flex.Item align="start" shouldShrink={true}>
|
||||
<Text weight="bold" data-testid="rubric-criteria-row-index">
|
||||
{rowIndex}.
|
||||
</Text>
|
||||
</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">
|
||||
<Tag
|
||||
text={
|
||||
|
@ -56,13 +74,11 @@ export const RubricCriteriaRow = () => {
|
|||
}}
|
||||
/>
|
||||
</View>
|
||||
<View as="div" margin="small 0 0 0">
|
||||
<Text weight="bold">Effective Use of Space</Text>
|
||||
<View as="div" margin="small 0 0 0" data-testid="rubric-criteria-row-description">
|
||||
<Text weight="bold">{description}</Text>
|
||||
</View>
|
||||
<View as="div">
|
||||
<Text>
|
||||
Great use of space to show depth with use of foreground, middleground, and background.{' '}
|
||||
</Text>
|
||||
<View as="div" data-testid="rubric-criteria-row-long-description">
|
||||
<Text>{longDescription}</Text>
|
||||
</View>
|
||||
</Flex.Item>
|
||||
<Flex.Item align="start">
|
||||
|
@ -75,24 +91,146 @@ export const RubricCriteriaRow = () => {
|
|||
infoColor: 'white',
|
||||
}}
|
||||
>
|
||||
<Text size="x-small">10 pts</Text>
|
||||
<Text data-testid="rubric-criteria-row-points" size="x-small">
|
||||
{possibleString(points)}
|
||||
</Text>
|
||||
</Pill>
|
||||
<IconButton withBackground={false} withBorder={false} screenReaderLabel="" size="small">
|
||||
<IconButton
|
||||
withBackground={false}
|
||||
withBorder={false}
|
||||
screenReaderLabel={I18n.t('Move Criterion')}
|
||||
size="small"
|
||||
>
|
||||
<IconDragHandleLine />
|
||||
</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 />
|
||||
</IconButton>
|
||||
<IconButton withBackground={false} withBorder={false} screenReaderLabel="" size="small">
|
||||
<IconButton
|
||||
withBackground={false}
|
||||
withBorder={false}
|
||||
screenReaderLabel={I18n.t('Delete Criterion')}
|
||||
size="small"
|
||||
>
|
||||
<IconTrashLine />
|
||||
</IconButton>
|
||||
<IconButton withBackground={false} withBorder={false} screenReaderLabel="" size="small">
|
||||
<IconButton
|
||||
withBackground={false}
|
||||
withBorder={false}
|
||||
screenReaderLabel={I18n.t('Duplicate Criterion')}
|
||||
size="small"
|
||||
>
|
||||
<IconDuplicateLine />
|
||||
</IconButton>
|
||||
</Flex.Item>
|
||||
</Flex>
|
||||
|
||||
<RatingScaleAccordion ratings={criterion.ratings} />
|
||||
<CriterionModal
|
||||
isOpen={isCriterionModalOpen}
|
||||
onDismiss={() => setIsCriterionModalOpen(false)}
|
||||
/>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -115,7 +115,7 @@ describe('RubricForm Tests', () => {
|
|||
it('will navigate back to /rubrics after successfully saving', async () => {
|
||||
jest
|
||||
.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 titleInput = getByTestId('rubric-form-title')
|
||||
fireEvent.change(titleInput, {target: {value: 'Rubric 1'}})
|
||||
|
@ -125,4 +125,72 @@ describe('RubricForm Tests', () => {
|
|||
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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -16,7 +16,61 @@
|
|||
* 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',
|
||||
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,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
|
@ -50,6 +50,8 @@ const {Option: SimpleSelectOption} = SimpleSelect
|
|||
const defaultRubricForm: RubricFormProps = {
|
||||
title: '',
|
||||
hidePoints: false,
|
||||
criteria: [],
|
||||
pointsPossible: 0,
|
||||
}
|
||||
|
||||
const translateRubricData = (fields: RubricQueryResponse): RubricFormProps => {
|
||||
|
@ -57,6 +59,8 @@ const translateRubricData = (fields: RubricQueryResponse): RubricFormProps => {
|
|||
id: fields.id,
|
||||
title: fields.title ?? '',
|
||||
hidePoints: fields.hidePoints ?? false,
|
||||
criteria: fields.criteria ?? [],
|
||||
pointsPossible: fields.pointsPossible ?? 0,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -194,7 +198,7 @@ export const RubricForm = () => {
|
|||
</Flex.Item>
|
||||
<Flex.Item>
|
||||
<Text weight="bold" size="xx-large" themeOverride={{fontWeightBold: 400}}>
|
||||
10
|
||||
{rubricForm.pointsPossible}
|
||||
</Text>
|
||||
<View as="span" margin="0 0 0 small">
|
||||
<Text weight="light" size="x-large">
|
||||
|
@ -213,9 +217,11 @@ export const RubricForm = () => {
|
|||
overflowX="hidden"
|
||||
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>
|
||||
|
||||
|
|
|
@ -30,11 +30,26 @@ const RUBRIC_QUERY = gql`
|
|||
title
|
||||
hidePoints
|
||||
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 = {
|
||||
rubric: RubricQueryResponse
|
||||
|
@ -55,6 +70,22 @@ export const saveRubric = async (rubric: RubricFormProps): Promise<RubricQueryRe
|
|||
const url = `${urlPrefix}/rubrics/${id ?? ''}`
|
||||
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, {
|
||||
method,
|
||||
headers: {
|
||||
|
@ -66,6 +97,7 @@ export const saveRubric = async (rubric: RubricFormProps): Promise<RubricQueryRe
|
|||
rubric: {
|
||||
title,
|
||||
hide_points: hidePoints,
|
||||
criteria,
|
||||
},
|
||||
rubric_association: {
|
||||
association_id: accountId ?? courseId,
|
||||
|
@ -89,5 +121,6 @@ export const saveRubric = async (rubric: RubricFormProps): Promise<RubricQueryRe
|
|||
title: savedRubric.title,
|
||||
hidePoints: savedRubric.hide_points,
|
||||
criteria: savedRubric.criteria,
|
||||
pointsPossible: savedRubric.points_possible,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,12 +16,16 @@
|
|||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type {RubricCriterion} from '@canvas/rubrics/react/types/rubric'
|
||||
|
||||
export type RubricFormProps = {
|
||||
id?: string
|
||||
title: string
|
||||
hidePoints: boolean
|
||||
accountId?: string
|
||||
courseId?: string
|
||||
criteria: RubricCriterion[]
|
||||
pointsPossible: number
|
||||
}
|
||||
|
||||
export type RubricFormValueTypes = string | boolean
|
||||
|
|
|
@ -18,14 +18,7 @@
|
|||
|
||||
export type Rubric = {
|
||||
id: string
|
||||
criteria?: {
|
||||
points: number
|
||||
description: string
|
||||
longDescription: string
|
||||
ignoreForScoring: boolean
|
||||
masteryPoints: number
|
||||
criterionUseRange: boolean
|
||||
}[]
|
||||
criteria?: RubricCriterion[]
|
||||
criteriaCount: number
|
||||
hidePoints?: boolean
|
||||
locations: string[]
|
||||
|
@ -34,6 +27,18 @@ export type Rubric = {
|
|||
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 = {
|
||||
id: string
|
||||
description: string
|
||||
|
|
Loading…
Reference in New Issue