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])
|
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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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)}>
|
||||||
|
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -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,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue