Use course query to fetch sections/group categories

closes VICE-2465
flag=discussion_create

Test plan:
1. Open discussion create page in local environment
2. See that your sections + group categories
appear in their respective dropdowns (Post To
and Group Discussion)

Change-Id: Ida191d8146fd1760bff0532456ac6afd98cdcbbf
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/325524
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Reviewed-by: Drake Harper <drake.harper@instructure.com>
QA-Review: Caleb Guanzon <cguanzon@instructure.com>
Product-Review: Caleb Guanzon <cguanzon@instructure.com>
This commit is contained in:
Alexander Youngblood 2023-08-17 15:49:55 -05:00 committed by Caleb Guanzon
parent 5d8ff541a5
commit c6221ffa34
5 changed files with 187 additions and 121 deletions

View File

@ -41,6 +41,24 @@ class Mutations::CreateDiscussionTopic < Mutations::DiscussionBase
return validation_error(I18n.t("Invalid context")) unless discussion_topic_context
# TODO: these were taken from the front-end, they still need to be implemented here
# isAnnouncement: false,
# discussionType: 'side_comment',
# delayedPostAt: availableFrom,
# lockAt: availableUntil,
# podcastEnabled: enablePodcastFeed,
# podcastHasStudentPosts: includeRepliesInFeed,
# requireInitialPost: respondBeforeReply,
# pinned: false,
# todoDate,
# groupCategoryId: null,
# allowRating: allowLiking,
# onlyGradersCanRate: onlyGradersCanLike,
# anonymousState: discussionAnonymousState === 'off' ? null : discussionAnonymousState,
# isAnonymousAuthor: anonymousAuthorState,
# specificSections: sectionIdsToPostTo,
# locked: false,
discussion_topic = DiscussionTopic.new(
{
context_id: discussion_topic_context.id,

View File

@ -48,7 +48,7 @@ describe "discussions" do
stub_rcs_config
end
context "on the new page" do
context "when discussion_create feature flag is OFF" do
let(:url) { "/courses/#{course.id}/discussion_topics/new" }
context "as a teacher" do
@ -408,4 +408,48 @@ describe "discussions" do
end
end
end
context "when discussion_create feature flag is ON", ignore_js_errors: true do
before do
Account.site_admin.enable_feature! :discussion_create
# we will turn react_discussions_post on here as well (altough it is not required)
Account.site_admin.enable_feature! :react_discussions_post
end
context "as a teacher" do
before do
user_session(teacher)
end
it "creates a new discussion topic successfully" do
title = "My Test Topic"
message = "replying to topic"
get "/courses/#{course.id}/discussion_topics/new"
f("input[placeholder='Topic Title']").send_keys title
type_in_tiny("textarea", message)
f("button[data-testid='save-and-publish-button']").click
wait_for_ajaximations
dt = DiscussionTopic.last
expect(dt.title).to eq title
expect(dt.message).to include message
expect(dt).to be_published
expect(fj("div[data-testid='discussion-topic-container'] span:contains('#{title}')")).to be_present
end
it "shows course sections or course group categories" do
new_section
group_category
group
get "/courses/#{course.id}/discussion_topics/new"
f("[data-testid='section-select']").click
# verify all sections exist in the dropdown
expect(f("[data-testid='section-opt-#{default_section.id}']")).to be_present
expect(f("[data-testid='section-opt-#{new_section.id}']")).to be_present
force_click("input[data-testid='group-discussion-checkbox']")
f("input[placeholder='Select Group']").click
# very group category exists in the dropdown
expect(f("[data-testid='group-category-opt-#{group_category.id}']")).to be_present
end
end
end
end

View File

@ -23,50 +23,16 @@ export const CREATE_DISCUSSION_TOPIC = gql`
mutation CreateDiscussionTopic(
$contextId: ID!
$contextType: String!
$isAnnouncement: Boolean!
$title: String
$message: String
$discussionType: String
$delayedPostAt: ISO8601DateTime
$lockAt: ISO8601DateTime
$podcastEnabled: Boolean
$podcastHasStudentPosts: Boolean
$requireInitialPost: Boolean
$pinned: Boolean
$todoDate: ISO8601DateTime
$groupCategoryId: ID
$allowRating: Boolean
$onlyGradersCanRate: Boolean
$sortByRating: Boolean
$anonymousState: String
$isAnonymousAuthor: Boolean
$specificSections: [String!]
$locked: Boolean
$published: Boolean
) {
createDiscussionTopic(
input: {
contextId: $contextId
contextType: $contextType
isAnnouncement: $isAnnouncement
title: $title
message: $message
discussionType: $discussionType
delayedPostAt: $delayedPostAt
lockAt: $lockAt
podcastEnabled: $podcastEnabled
podcastHasStudentPosts: $podcastHasStudentPosts
requireInitialPost: $requireInitialPost
pinned: $pinned
todoDate: $todoDate
groupCategoryId: $groupCategoryId
allowRating: $allowRating
onlyGradersCanRate: $onlyGradersCanRate
sortByRating: $sortByRating
anonymousState: $anonymousState
isAnonymousAuthor: $isAnonymousAuthor
specificSections: $specificSections
locked: $locked
published: $published
}
) {

View File

@ -20,7 +20,7 @@ import React, {useState, useRef, useEffect} from 'react'
import PropTypes from 'prop-types'
import AnonymousResponseSelector from '@canvas/discussions/react/components/AnonymousResponseSelector/AnonymousResponseSelector'
import GroupCategoryModalContainer from '../../containers/GroupCategoryModalContainer/GroupCategoryModalContainer'
import {useScope as usei18NScope} from '@canvas/i18n'
import {useScope as useI18nScope} from '@canvas/i18n'
import {View} from '@instructure/ui-view'
import {TextInput} from '@instructure/ui-text-input'
@ -35,7 +35,7 @@ import {DateTimeInput} from '@instructure/ui-date-time-input'
import CanvasMultiSelect from '@canvas/multi-select'
import CanvasRce from '@canvas/rce/react/CanvasRce'
const I18N = usei18NScope('discussion_create')
const I18n = useI18nScope('discussion_create')
export default function DiscussionTopicForm({
isEditing,
@ -122,11 +122,11 @@ export default function DiscussionTopicForm({
const validateTitle = newTitle => {
if (newTitle.length > 255) {
setTitleValidationMessages([
{text: I18N.t('Title must be less than 255 characters.'), type: 'error'},
{text: I18n.t('Title must be less than 255 characters.'), type: 'error'},
])
return false
} else if (newTitle.length === 0) {
setTitleValidationMessages([{text: I18N.t('Title must not be empty.'), type: 'error'}])
setTitleValidationMessages([{text: I18n.t('Title must not be empty.'), type: 'error'}])
return false
} else {
setTitleValidationMessages([{text: '', type: 'success'}])
@ -140,7 +140,7 @@ export default function DiscussionTopicForm({
return true
} else if (newAvailableUntil < newAvailableFrom) {
setAvailabilityValidationMessages([
{text: I18N.t('Date must be after date available.'), type: 'error'},
{text: I18n.t('Date must be after date available.'), type: 'error'},
])
return false
} else {
@ -184,9 +184,9 @@ export default function DiscussionTopicForm({
<>
<FormFieldGroup description="" rowSpacing="small">
<TextInput
renderLabel={I18N.t('Topic Title')}
type={I18N.t('text')}
placeholder={I18N.t('Topic Title')}
renderLabel={I18n.t('Topic Title')}
type={I18n.t('text')}
placeholder={I18n.t('Topic Title')}
value={title}
onChange={(_event, value) => {
validateTitle(value)
@ -215,8 +215,9 @@ export default function DiscussionTopicForm({
{!isGraded && !isGroupDiscussion && (
<View display="block" padding="medium none">
<CanvasMultiSelect
label={I18N.t('Post to')}
assistiveText={I18N.t(
data-testid="section-select"
label={I18n.t('Post to')}
assistiveText={I18n.t(
'Select sections to post to. Type or use arrow keys to navigate. Multiple selections are allowed.'
)}
selectedOptionIds={sectionIdsToPostTo}
@ -241,18 +242,23 @@ export default function DiscussionTopicForm({
width={inputWidth}
>
{[allSectionsOption, ...sections].map(({_id: id, name: label}) => (
<CanvasMultiSelect.Option id={id} value={`opt-${id}`} key={id}>
<CanvasMultiSelect.Option
id={id}
value={`opt-${id}`}
key={id}
data-testid={`section-opt-${id}`}
>
{label}
</CanvasMultiSelect.Option>
))}
</CanvasMultiSelect>
</View>
)}
<Text size="large">{I18N.t('Options')}</Text>
<Text size="large">{I18n.t('Options')}</Text>
<View display="block" margin="medium 0">
<RadioInputGroup
name="anonymous"
description={I18N.t('Anonymous Discussion')}
description={I18n.t('Anonymous Discussion')}
value={discussionAnonymousState}
onChange={(_event, value) => {
if (value !== 'off') {
@ -267,21 +273,21 @@ export default function DiscussionTopicForm({
<RadioInput
key="off"
value="off"
label={I18N.t(
label={I18n.t(
'Off: student names and profile pictures will be visible to other members of this course'
)}
/>
<RadioInput
key="partial_anonymity"
value="partial_anonymity"
label={I18N.t(
label={I18n.t(
'Partial: students can choose to reveal their name and profile picture'
)}
/>
<RadioInput
key="full_anonymity"
value="full_anonymity"
label={I18N.t('Full: student names and profile pictures will be hidden')}
label={I18n.t('Full: student names and profile pictures will be hidden')}
/>
</RadioInputGroup>
{!isEditing && discussionAnonymousState === 'partial_anonymity' && isStudent && (
@ -296,13 +302,13 @@ export default function DiscussionTopicForm({
</View>
<FormFieldGroup description="" rowSpacing="small">
<Checkbox
label={I18N.t('Participants must respond to the topic before viewing other replies')}
label={I18n.t('Participants must respond to the topic before viewing other replies')}
value="must-respond-before-viewing-replies"
checked={respondBeforeReply}
onChange={() => setRespondBeforeReply(!respondBeforeReply)}
/>
<Checkbox
label={I18N.t('Enable podcast feed')}
label={I18n.t('Enable podcast feed')}
value="enable-podcast-feed"
checked={enablePodcastFeed}
onChange={() => {
@ -313,7 +319,7 @@ export default function DiscussionTopicForm({
{enablePodcastFeed && (
<View display="block" padding="none none none large">
<Checkbox
label={I18N.t('Include student replies in podcast feed')}
label={I18n.t('Include student replies in podcast feed')}
value="include-student-replies-in-podcast-feed"
checked={includeRepliesInFeed}
onChange={() => setIncludeRepliesInFeed(!includeRepliesInFeed)}
@ -322,7 +328,7 @@ export default function DiscussionTopicForm({
)}
{discussionAnonymousState === 'off' && (
<Checkbox
label={I18N.t('Graded')}
label={I18n.t('Graded')}
value="graded"
checked={isGraded}
onChange={() => setIsGraded(!isGraded)}
@ -330,7 +336,7 @@ export default function DiscussionTopicForm({
/>
)}
<Checkbox
label={I18N.t('Allow liking')}
label={I18n.t('Allow liking')}
value="allow-liking"
checked={allowLiking}
onChange={() => {
@ -342,7 +348,7 @@ export default function DiscussionTopicForm({
<View display="block" padding="none none none large">
<FormFieldGroup description="" rowSpacing="small">
<Checkbox
label={I18N.t('Only graders can like')}
label={I18n.t('Only graders can like')}
value="only-graders-can-like"
checked={onlyGradersCanLike}
onChange={() => setOnlyGradersCanLike(!onlyGradersCanLike)}
@ -352,7 +358,7 @@ export default function DiscussionTopicForm({
)}
{!isGraded && (
<Checkbox
label={I18N.t('Add to student to-do')}
label={I18n.t('Add to student to-do')}
value="add-to-student-to-do"
checked={addToTodo}
onChange={() => {
@ -367,18 +373,19 @@ export default function DiscussionTopicForm({
description=""
dateRenderLabel=""
timeRenderLabel=""
prevMonthLabel={I18N.t('previous')}
nextMonthLabel={I18N.t('next')}
prevMonthLabel={I18n.t('previous')}
nextMonthLabel={I18n.t('next')}
onChange={(_event, newDate) => setTodoDate(newDate)}
value={todoDate}
invalidDateTimeMessage={I18N.t('Invalid date and time')}
invalidDateTimeMessage={I18n.t('Invalid date and time')}
layout="columns"
/>
</View>
)}
{discussionAnonymousState === 'off' && (
<Checkbox
label={I18N.t('This is a Group Discussion')}
data-testid="group-discussion-checkbox"
label={I18n.t('This is a Group Discussion')}
value="group-discussion"
checked={isGroupDiscussion}
onChange={() => {
@ -390,7 +397,7 @@ export default function DiscussionTopicForm({
{discussionAnonymousState === 'off' && isGroupDiscussion && (
<View display="block" padding="none none none large">
<SimpleSelect
renderLabel={I18N.t('Group Set')}
renderLabel={I18n.t('Group Set')}
defaultValue=""
value={groupCategoryId}
onChange={(_event, newChoice) => {
@ -402,11 +409,16 @@ export default function DiscussionTopicForm({
setGroupCategoryId(value)
}
}}
placeholder={I18N.t('Select Group')}
placeholder={I18n.t('Select Group')}
width={inputWidth}
>
{groupCategories.map(({_id: id, name: label}) => (
<SimpleSelect.Option key={id} id={`opt-${id}`} value={id}>
<SimpleSelect.Option
key={id}
id={`opt-${id}`}
value={id}
data-testid={`group-category-opt-${id}`}
>
{label}
</SimpleSelect.Option>
))}
@ -416,7 +428,7 @@ export default function DiscussionTopicForm({
value="new-group-category"
renderBeforeLabel={IconAddLine}
>
New Group Category
{I18n.t('New Group Category')}
</SimpleSelect.Option>
</SimpleSelect>
@ -433,33 +445,33 @@ export default function DiscussionTopicForm({
) : (
<FormFieldGroup description="" width={inputWidth}>
<DateTimeInput
description={I18N.t('Available from')}
description={I18n.t('Available from')}
dateRenderLabel=""
timeRenderLabel=""
prevMonthLabel={I18N.t('previous')}
nextMonthLabel={I18N.t('next')}
prevMonthLabel={I18n.t('previous')}
nextMonthLabel={I18n.t('next')}
value={availableFrom}
onChange={(_event, newAvailableFrom) => {
validateAvailability(newAvailableFrom, availableUntil)
setAvailableFrom(newAvailableFrom)
}}
datePlaceholder={I18N.t('Select Date')}
invalidDateTimeMessage={I18N.t('Invalid date and time')}
datePlaceholder={I18n.t('Select Date')}
invalidDateTimeMessage={I18n.t('Invalid date and time')}
layout="columns"
/>
<DateTimeInput
description={I18N.t('Until')}
description={I18n.t('Until')}
dateRenderLabel=""
timeRenderLabel=""
prevMonthLabel={I18N.t('previous')}
nextMonthLabel={I18N.t('next')}
prevMonthLabel={I18n.t('previous')}
nextMonthLabel={I18n.t('next')}
value={availableUntil}
onChange={(_event, newAvailableUntil) => {
validateAvailability(availableFrom, newAvailableUntil)
setAvailableUntil(newAvailableUntil)
}}
datePlaceholder={I18N.t('Select Date')}
invalidDateTimeMessage={I18N.t('Invalid date and time')}
datePlaceholder={I18n.t('Select Date')}
invalidDateTimeMessage={I18n.t('Invalid date and time')}
messages={availabiltyValidationMessages}
layout="columns"
/>
@ -473,18 +485,24 @@ export default function DiscussionTopicForm({
padding="large none"
>
<Button type="button" color="secondary">
{I18N.t('Cancel')}
{I18n.t('Cancel')}
</Button>
<Button
type="submit"
onClick={() => submitForm(true)}
color="secondary"
margin="xxx-small"
data-testid="save-and-publish-button"
>
{I18N.t('Save and Publish')}
{I18n.t('Save and Publish')}
</Button>
<Button type="submit" onClick={() => submitForm(false)} color="primary">
{I18N.t('Save')}
<Button
type="submit"
data-testid="save-button"
onClick={() => submitForm(false)}
color="primary"
>
{I18n.t('Save')}
</Button>
</View>
</FormFieldGroup>

View File

@ -19,20 +19,29 @@
import React, {useCallback} from 'react'
import {useQuery, useMutation} from 'react-apollo'
import {DISCUSSION_TOPIC_QUERY} from '../../../graphql/Queries'
import {COURSE_QUERY, DISCUSSION_TOPIC_QUERY} from '../../../graphql/Queries'
import {CREATE_DISCUSSION_TOPIC} from '../../../graphql/Mutations'
import LoadingIndicator from '@canvas/loading-indicator'
import DiscussionTopicForm from '../../components/DiscussionTopicForm/DiscussionTopicForm'
export default function DiscussionTopicFormContainer() {
const is_editing = !!ENV.discussion_topic_id
const {data: topic_data, loading: topic_is_loading} = useQuery(DISCUSSION_TOPIC_QUERY, {
const {data: courseData, loading: courseIsLoading} = useQuery(COURSE_QUERY, {
variables: {
discussionTopicId: 120 /* ENV.disscusion_topic_id, */,
courseId: ENV.context_id, // TODO: what if it's a group?
},
})
const currentDiscussionTopic = topic_data?.legacyNode
const currentCourse = courseData?.legacyNode
const sections = currentCourse?.sectionsConnection?.nodes
const groupCategories = currentCourse?.groupSetsConnection?.nodes
const isEditing = !!ENV.discussion_topic_id
const {data: topicData, loading: topicIsLoading} = useQuery(DISCUSSION_TOPIC_QUERY, {
variables: {
discussionTopicId: ENV.discussion_topic_id,
},
})
const currentDiscussionTopic = topicData?.legacyNode
const [createDiscussionTopic] = useMutation(CREATE_DISCUSSION_TOPIC, {
onCompleted: completionData => {
@ -42,10 +51,12 @@ export default function DiscussionTopicFormContainer() {
if (discussion_topic_id && context_type) {
if (context_type === 'Course') {
window.location.assign(
`/courses/${ENV.course_id}/discussion_topics/${discussion_topic_id}`
`/courses/${ENV.context_id}/discussion_topics/${discussion_topic_id}`
)
} else if (context_type === 'Group') {
window.location.assign(`/groups/${ENV.group_id}/discussion_topics/${discussion_topic_id}`)
window.location.assign(
`/groups/${ENV.context_id}/discussion_topics/${discussion_topic_id}`
)
} else {
// TODO: show error page and/or redirect
// eslint-disable-next-line no-console
@ -68,45 +79,48 @@ export default function DiscussionTopicFormContainer() {
({
title,
message,
sectionIdsToPostTo,
discussionAnonymousState,
anonymousAuthorState,
respondBeforeReply,
enablePodcastFeed,
includeRepliesInFeed,
// TODO: implement these as the backend becomes ready for them
// isAnnouncement,
// sectionIdsToPostTo,
// discussionAnonymousState,
// anonymousAuthorState,
// respondBeforeReply,
// enablePodcastFeed,
// includeRepliesInFeed,
// isGraded, (phase 2)
allowLiking,
onlyGradersCanLike,
// allowLiking,
// onlyGradersCanLike,
// addToTodo,
todoDate,
// todoDate,
// isGroupDiscussion,
// groupCategoryId,
availableFrom,
availableUntil,
// availableFrom,
// availableUntil,
shouldPublish,
}) => {
createDiscussionTopic({
variables: {
contextId: ENV.course_id,
contextId: ENV.context_id,
contextType: 'Course',
isAnnouncement: false,
title,
message,
discussionType: 'side_comment',
delayedPostAt: availableFrom,
lockAt: availableUntil,
podcastEnabled: enablePodcastFeed,
podcastHasStudentPosts: includeRepliesInFeed,
requireInitialPost: respondBeforeReply,
pinned: false,
todoDate,
groupCategoryId: null,
allowRating: allowLiking,
onlyGradersCanRate: onlyGradersCanLike,
anonymousState: discussionAnonymousState === 'off' ? null : discussionAnonymousState,
isAnonymousAuthor: anonymousAuthorState,
specificSections: sectionIdsToPostTo,
locked: false,
// TODO: implement these as the backend becomes ready for them
// isAnnouncement:,
// discussionType: 'side_comment',
// delayedPostAt: availableFrom,
// lockAt: availableUntil,
// podcastEnabled: enablePodcastFeed,
// podcastHasStudentPosts: includeRepliesInFeed,
// requireInitialPost: respondBeforeReply,
// pinned: false,
// todoDate,
// groupCategoryId: null,
// allowRating: allowLiking,
// onlyGradersCanRate: onlyGradersCanLike,
// anonymousState: discussionAnonymousState === 'off' ? null : discussionAnonymousState,
// isAnonymousAuthor: anonymousAuthorState,
// specificSections: sectionIdsToPostTo,
// locked: false,
published: shouldPublish,
},
})
@ -114,6 +128,7 @@ export default function DiscussionTopicFormContainer() {
[createDiscussionTopic]
)
// TODO implement this update discussion mutation when the backend is ready
// const updateDiscussionTopicOnSubmit = useCallback(
// ({
// title,
@ -166,19 +181,24 @@ export default function DiscussionTopicFormContainer() {
// [updateDiscussionTopic]
// )
if (topic_is_loading) {
if (courseIsLoading || topicIsLoading) {
return <LoadingIndicator />
}
return (
<DiscussionTopicForm
isEditing={is_editing}
isEditing={isEditing}
currentDiscussionTopic={currentDiscussionTopic}
isStudent={false /* ENV.is_student */}
sections={[]}
groupCategories={[]}
isStudent={ENV.is_student}
sections={sections}
groupCategories={groupCategories}
onSubmit={
createDiscussionTopicOnSubmit /* is_editing ? updateDiscussionTopicOnSubmit : ... */
isEditing
? () => {
// eslint-disable-next-line no-console
console.log('change this to call updateDiscussionTopicOnSubmit later')
}
: createDiscussionTopicOnSubmit
}
/>
)