add rubric save as draft button
this commit adds a save as draft button to the rubric form. This button is only displayed when a rubric has no current associations. When clicked, the rubric is saved with a new workflow_state value of "draft". closes EVAL-3637 flag=enhanced_rubrics test plan: - make sure graphql is updated - create a rubric. you should see the "Save as Draft" button - add a title and save as draft. you should be navigated back to the rubric list and see the rubric with a draft status - edit the rubric and the click the "Save" button. you should be navigated back to the rubric list and the rubric should no longer have the draft status. - edit the rubric. you should see the "Save as Draft" button still available. - add that same rubric to an assignment. - navigate back to the edit rubric page. you should no longer see the "Save as Draft" button. Change-Id: Id1e68f1432cd17fab618b2c8dd0350acf0408e12 Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/341595 Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com> Reviewed-by: Derek Williams <derek.williams@instructure.com> Reviewed-by: Kai Bjorkman <kbjorkman@instructure.com> QA-Review: Kai Bjorkman <kbjorkman@instructure.com> Product-Review: Ravi Koll <ravi.koll@instructure.com>
This commit is contained in:
parent
ec394e19ba
commit
5bee58bc2e
|
@ -50,9 +50,20 @@ module Types
|
|||
|
||||
field :unassessed, Boolean, null: false
|
||||
def unassessed
|
||||
if object.workflow_state == "draft"
|
||||
return true
|
||||
end
|
||||
|
||||
Rubric.active.unassessed.where(id: object.id).exists?
|
||||
end
|
||||
|
||||
field :has_rubric_associations, Boolean, null: false, resolver_method: :rubric_assignment_associations?
|
||||
def rubric_assignment_associations?
|
||||
load_association(:rubric_associations).then do
|
||||
object.rubric_assignment_associations?
|
||||
end
|
||||
end
|
||||
|
||||
field :button_display, String, null: false
|
||||
field :hide_points, Boolean, null: true
|
||||
field :rating_order, String, null: false
|
||||
|
|
|
@ -78,7 +78,7 @@ class Rubric < ActiveRecord::Base
|
|||
scope :publicly_reusable, -> { where(reusable: true).order(best_unicode_collation_key("title")) }
|
||||
scope :matching, ->(search) { where(wildcard("rubrics.title", search)).order("rubrics.association_count DESC") }
|
||||
scope :before, ->(date) { where("rubrics.created_at<?", date) }
|
||||
scope :active, -> { where.not(workflow_state: "deleted") }
|
||||
scope :active, -> { where.not(workflow_state: ["deleted", "draft"]) }
|
||||
|
||||
set_policy do
|
||||
given { |user, session| context.grants_right?(user, session, :manage_rubrics) }
|
||||
|
@ -117,6 +117,7 @@ class Rubric < ActiveRecord::Base
|
|||
state :archived do
|
||||
event :unarchive, transitions_to: :active
|
||||
end
|
||||
state :draft
|
||||
state :deleted
|
||||
end
|
||||
|
||||
|
@ -130,6 +131,10 @@ class Rubric < ActiveRecord::Base
|
|||
super if enhanced_rubrics_enabled?
|
||||
end
|
||||
|
||||
def draft
|
||||
super if enhanced_rubrics_enabled?
|
||||
end
|
||||
|
||||
def self.aligned_to_outcomes
|
||||
where(
|
||||
ContentTag.learning_outcome_alignments
|
||||
|
@ -336,6 +341,7 @@ class Rubric < ActiveRecord::Base
|
|||
self.points_possible = data.points_possible
|
||||
self.hide_points = params[:hide_points]
|
||||
self.rating_order = params[:rating_order] if params.key?(:rating_order)
|
||||
self.workflow_state = params[:workflow_state] if params[:workflow_state]
|
||||
save
|
||||
self
|
||||
end
|
||||
|
@ -548,4 +554,8 @@ class Rubric < ActiveRecord::Base
|
|||
def learning_outcome_ids_from_results
|
||||
learning_outcome_results.select(:learning_outcome_id).distinct.pluck(:learning_outcome_id)
|
||||
end
|
||||
|
||||
def rubric_assignment_associations?
|
||||
rubric_associations.where(association_type: "Assignment", workflow_state: "active").any?
|
||||
end
|
||||
end
|
||||
|
|
|
@ -26,8 +26,6 @@ describe Types::RubricType do
|
|||
let(:rubric) { rubric_for_course }
|
||||
let(:rubric_type) { GraphQLTypeTester.new(rubric, current_user: student) }
|
||||
let(:assignment) { assignment_model(course: @course) }
|
||||
let(:association) { rubric_association_model(rubric:, association_object: assignment, purpose: "grading") }
|
||||
let(:assessment) { rubric_assessment_model(rubric:, rubric_association: association) }
|
||||
|
||||
it "works" do
|
||||
expect(rubric_type.resolve("_id")).to eq rubric.id.to_s
|
||||
|
@ -85,6 +83,17 @@ describe Types::RubricType do
|
|||
|
||||
it "unassessed" do
|
||||
expect(rubric_type.resolve("unassessed")).to be true
|
||||
|
||||
association = rubric_association_model(rubric:, association_object: assignment, purpose: "grading")
|
||||
rubric_assessment_model(rubric:, rubric_association: association, user: student)
|
||||
expect(rubric_type.resolve("unassessed")).to be false
|
||||
end
|
||||
|
||||
it "has_rubric_associations" do
|
||||
expect(rubric_type.resolve("hasRubricAssociations")).to be false
|
||||
|
||||
rubric_association_model(rubric:, association_object: assignment, purpose: "grading")
|
||||
expect(rubric_type.resolve("hasRubricAssociations")).to be true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -66,6 +66,7 @@ describe('RubricForm Tests', () => {
|
|||
expect(getByTestId('rubric-form-title')).toHaveValue('')
|
||||
expect(getByTestId('rubric-hide-points-select')).toBeInTheDocument()
|
||||
expect(getByTestId('rubric-rating-order-select')).toBeInTheDocument()
|
||||
expect(getByTestId('save-as-draft-button')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -123,6 +124,7 @@ describe('RubricForm Tests', () => {
|
|||
buttonDisplay: 'numeric',
|
||||
ratingOrder: 'descending',
|
||||
unassessed: true,
|
||||
hasRubricAssociations: false,
|
||||
})
|
||||
)
|
||||
const {getByTestId} = renderComponent()
|
||||
|
@ -133,6 +135,18 @@ describe('RubricForm Tests', () => {
|
|||
await new Promise(resolve => setTimeout(resolve, 0))
|
||||
expect(getSRAlert()).toEqual('Rubric saved successfully')
|
||||
})
|
||||
|
||||
it('does not display save as draft button if rubric has associations', () => {
|
||||
jest.spyOn(Router, 'useParams').mockReturnValue({accountId: '1', rubricId: '1'})
|
||||
|
||||
queryClient.setQueryData(['fetch-rubric-1'], {
|
||||
...RUBRICS_QUERY_RESPONSE,
|
||||
hasRubricAssociations: true,
|
||||
})
|
||||
|
||||
const {queryByTestId} = renderComponent()
|
||||
expect(queryByTestId('save-as-draft-button')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('rubric criteria', () => {
|
||||
|
|
|
@ -25,19 +25,13 @@ import LoadingIndicator from '@canvas/loading-indicator/react'
|
|||
import {useQuery, useMutation, queryClient} from '@canvas/query'
|
||||
import type {RubricCriterion} from '@canvas/rubrics/react/types/rubric'
|
||||
import {Alert} from '@instructure/ui-alerts'
|
||||
import {ScreenReaderContent} from '@instructure/ui-a11y-content'
|
||||
import {View} from '@instructure/ui-view'
|
||||
import {TextInput} from '@instructure/ui-text-input'
|
||||
import {Heading} from '@instructure/ui-heading'
|
||||
import {Text} from '@instructure/ui-text'
|
||||
import {SimpleSelect} from '@instructure/ui-simple-select'
|
||||
import {Flex} from '@instructure/ui-flex'
|
||||
import {
|
||||
IconEyeLine,
|
||||
IconTableLeftHeaderSolid,
|
||||
IconTableRowPropertiesSolid,
|
||||
IconTableTopHeaderSolid,
|
||||
} from '@instructure/ui-icons'
|
||||
import {IconEyeLine} from '@instructure/ui-icons'
|
||||
import {Button} from '@instructure/ui-buttons'
|
||||
import {Link} from '@instructure/ui-link'
|
||||
import {RubricCriteriaRow} from './RubricCriteriaRow'
|
||||
|
@ -52,24 +46,28 @@ const {Option: SimpleSelectOption} = SimpleSelect
|
|||
|
||||
const defaultRubricForm: RubricFormProps = {
|
||||
title: '',
|
||||
hasRubricAssociations: false,
|
||||
hidePoints: false,
|
||||
criteria: [],
|
||||
pointsPossible: 0,
|
||||
buttonDisplay: 'numeric',
|
||||
ratingOrder: 'descending',
|
||||
unassessed: true,
|
||||
workflowState: 'active',
|
||||
}
|
||||
|
||||
const translateRubricData = (fields: RubricQueryResponse): RubricFormProps => {
|
||||
return {
|
||||
id: fields.id,
|
||||
title: fields.title ?? '',
|
||||
hasRubricAssociations: fields.hasRubricAssociations ?? false,
|
||||
hidePoints: fields.hidePoints ?? false,
|
||||
criteria: fields.criteria ?? [],
|
||||
pointsPossible: fields.pointsPossible ?? 0,
|
||||
buttonDisplay: fields.buttonDisplay ?? 'numeric',
|
||||
ratingOrder: fields.ratingOrder ?? 'descending',
|
||||
unassessed: fields.unassessed ?? true,
|
||||
workflowState: fields.workflowState ?? 'active',
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -157,6 +155,16 @@ export const RubricForm = () => {
|
|||
setIsCriterionModalOpen(false)
|
||||
}
|
||||
|
||||
const handleSaveAsDraft = () => {
|
||||
setRubricFormField('workflowState', 'draft')
|
||||
mutate()
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
setRubricFormField('workflowState', 'active')
|
||||
mutate()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
const rubricFormData = translateRubricData(data)
|
||||
|
@ -314,10 +322,21 @@ export const RubricForm = () => {
|
|||
<Flex.Item margin="0 medium 0 0">
|
||||
<Button onClick={() => navigate(navigateUrl)}>{I18n.t('Cancel')}</Button>
|
||||
|
||||
{!rubricForm.hasRubricAssociations && (
|
||||
<Button
|
||||
margin="0 0 0 small"
|
||||
disabled={saveLoading || !formValid()}
|
||||
onClick={handleSaveAsDraft}
|
||||
data-testid="save-as-draft-button"
|
||||
>
|
||||
{I18n.t('Save as Draft')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
margin="0 0 0 small"
|
||||
color="primary"
|
||||
onClick={() => mutate()}
|
||||
onClick={handleSave}
|
||||
disabled={saveLoading || !formValid()}
|
||||
data-testid="save-rubric-button"
|
||||
>
|
||||
|
|
|
@ -25,6 +25,7 @@ import {TruncateText} from '@instructure/ui-truncate-text'
|
|||
import {Link} from '@instructure/ui-link'
|
||||
import {ScreenReaderContent} from '@instructure/ui-a11y-content'
|
||||
import {RubricPopover} from './RubricPopover'
|
||||
import {Pill} from '@instructure/ui-pill'
|
||||
|
||||
const I18n = useI18nScope('rubrics-list-table')
|
||||
|
||||
|
@ -88,6 +89,7 @@ export const RubricTable = ({rubrics}: RubricTableProps) => {
|
|||
>
|
||||
{rubric.title}
|
||||
</Link>
|
||||
{rubric.workflowState === 'draft' && <Pill margin="x-small">{I18n.t('Draft')}</Pill>}
|
||||
</Cell>
|
||||
<Cell data-testid={`rubric-points-${rubric.id}`}>{rubric.pointsPossible}</Cell>
|
||||
<Cell data-testid={`rubric-criterion-count-${rubric.id}`}>{rubric.criteriaCount}</Cell>
|
||||
|
|
|
@ -96,7 +96,8 @@ export const ViewRubrics = () => {
|
|||
criteria: curr.criteria,
|
||||
}
|
||||
|
||||
curr.workflowState === 'active'
|
||||
const activeStates = ['active', 'draft']
|
||||
activeStates.includes(curr.workflowState ?? '')
|
||||
? prev.activeRubrics.push(rubric)
|
||||
: prev.archivedRubrics.push(rubric)
|
||||
return prev
|
||||
|
|
|
@ -28,6 +28,7 @@ const RUBRIC_QUERY = gql`
|
|||
rubric(id: $id) {
|
||||
id: _id
|
||||
title
|
||||
hasRubricAssociations
|
||||
hidePoints
|
||||
buttonDisplay
|
||||
ratingOrder
|
||||
|
@ -60,7 +61,10 @@ export type RubricQueryResponse = Pick<
|
|||
| 'buttonDisplay'
|
||||
| 'ratingOrder'
|
||||
| 'workflowState'
|
||||
> & {unassessed: boolean}
|
||||
> & {
|
||||
unassessed: boolean
|
||||
hasRubricAssociations: boolean
|
||||
}
|
||||
|
||||
type FetchRubricResponse = {
|
||||
rubric: RubricQueryResponse
|
||||
|
@ -76,7 +80,8 @@ export const fetchRubric = async (id?: string): Promise<RubricQueryResponse | nu
|
|||
}
|
||||
|
||||
export const saveRubric = async (rubric: RubricFormProps): Promise<RubricQueryResponse> => {
|
||||
const {id, title, hidePoints, accountId, courseId, ratingOrder, buttonDisplay} = rubric
|
||||
const {id, title, hidePoints, accountId, courseId, ratingOrder, buttonDisplay, workflowState} =
|
||||
rubric
|
||||
const urlPrefix = accountId ? `/accounts/${accountId}` : `/courses/${courseId}`
|
||||
const url = `${urlPrefix}/rubrics/${id ?? ''}`
|
||||
const method = id ? 'PATCH' : 'POST'
|
||||
|
@ -112,6 +117,7 @@ export const saveRubric = async (rubric: RubricFormProps): Promise<RubricQueryRe
|
|||
criteria,
|
||||
button_display: buttonDisplay,
|
||||
rating_order: ratingOrder,
|
||||
workflow_state: workflowState,
|
||||
},
|
||||
rubric_association: {
|
||||
association_id: accountId ?? courseId,
|
||||
|
@ -140,5 +146,6 @@ export const saveRubric = async (rubric: RubricFormProps): Promise<RubricQueryRe
|
|||
ratingOrder: savedRubric.rating_order,
|
||||
workflowState: savedRubric.workflow_state,
|
||||
unassessed: rubric.unassessed,
|
||||
hasRubricAssociations: rubric.hasRubricAssociations,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ import type {RubricCriterion} from '@canvas/rubrics/react/types/rubric'
|
|||
export type RubricFormProps = {
|
||||
id?: string
|
||||
title: string
|
||||
hasRubricAssociations: boolean
|
||||
hidePoints: boolean
|
||||
accountId?: string
|
||||
courseId?: string
|
||||
|
@ -29,4 +30,5 @@ export type RubricFormProps = {
|
|||
buttonDisplay: string
|
||||
ratingOrder: string
|
||||
unassessed: boolean
|
||||
workflowState: string
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue