improve logic for cancel, save, publish, [save and publish]

flag=discussion_create
fixes VICE-3811

test plan:
Announcement Create
- default: Cancel, Publish
- check delay posting, leve date blank, (must show cancel, publish)
- now set available from date to future, (must show cancel, save)
- set available from date to the past, (must show cancel, publish)
- uncheck delay posting, (must show cancel, publish)

Discussion Create
- as student, must show cancel, save (save publishes the discussion)
- as a teacher, must show cancel, save and publish, save
- - save must save as unpublished
- - save and publish must save as published
- - save with an available from date in the future must
save as unpublished
- - save and publish with an available from date in tge future
must save as workflow_state: post_delayed

Discussion Edit
- you only see cancel and save buttons
for published discussions
- teachers see cancel, save and publish, and
save for unpublished discussions
- teachers only see cancel and save for
published discussions

Discussions/Announcements any state
- cancel sends you to the place you came from

Change-Id: I57945b78afd26f5024351c7bd0b303cd2027a80b
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/330478
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
QA-Review: Jason Gillett <jason.gillett@instructure.com>
Reviewed-by: Drake Harper <drake.harper@instructure.com>
Product-Review: Caleb Guanzon <cguanzon@instructure.com>
This commit is contained in:
Caleb Guanzon 2023-10-16 12:26:59 -06:00
parent 834df61a01
commit 77d748906e
4 changed files with 96 additions and 29 deletions

View File

@ -60,7 +60,8 @@ class Mutations::DiscussionBase < Mutations::BaseMutation
discussion_topic.lock_at = lock_at if lock_at
if discussion_topic.delayed_post_at_changed? || discussion_topic.lock_at_changed?
discussion_topic.workflow_state = discussion_topic.should_not_post_yet ? "post_delayed" : discussion_topic.workflow_state
# only apply post_delayed if the topic is set to published
discussion_topic.workflow_state = (discussion_topic.should_not_post_yet && discussion_topic.workflow_state == "active") ? "post_delayed" : discussion_topic.workflow_state
if discussion_topic.should_lock_yet
discussion_topic.lock(without_save: true)
else

View File

@ -642,7 +642,7 @@ describe Mutations::CreateDiscussionTopic do
end
context "delayed_post_at and lock_at" do
it "successfully creates a discussion topic with delayed_post_at and lock_at" do
it "successfully creates an unpublished discussion topic with delayed_post_at and lock_at" do
context_type = "Course"
title = "Delayed Topic"
message = "Lorem ipsum..."
@ -666,6 +666,37 @@ describe Mutations::CreateDiscussionTopic do
result = execute_with_input(query)
discussion_topic = result.dig("data", "createDiscussionTopic", "discussionTopic")
expect(result["errors"]).to be_nil
expect(result.dig("data", "discussionTopic", "errors")).to be_nil
expect(discussion_topic["delayedPostAt"]).to eq delayed_post_at
expect(discussion_topic["lockAt"]).to eq lock_at
expect(DiscussionTopic.last.workflow_state).to eq "unpublished"
end
it "coerces a created published discussion into post_delayed if delayed_post_at is in the future" do
context_type = "Course"
title = "Delayed Topic"
message = "Lorem ipsum..."
published = true
require_initial_post = true
delayed_post_at = 5.days.from_now.iso8601
lock_at = 10.days.from_now.iso8601
query = <<~GQL
contextId: "#{@course.id}"
contextType: "#{context_type}"
title: "#{title}"
message: "#{message}"
published: #{published}
requireInitialPost: #{require_initial_post}
anonymousState: "off"
delayedPostAt: "#{delayed_post_at}"
lockAt: "#{lock_at}"
GQL
result = execute_with_input(query)
discussion_topic = result.dig("data", "createDiscussionTopic", "discussionTopic")
expect(result["errors"]).to be_nil
expect(result.dig("data", "discussionTopic", "errors")).to be_nil
expect(discussion_topic["delayedPostAt"]).to eq delayed_post_at

View File

@ -443,7 +443,7 @@ describe "discussions" do
get "/courses/#{course.id}/discussion_topics/new"
f("input[placeholder='Topic Title']").send_keys "This is fully anonymous"
force_click("input[value='full_anonymity']")
f("button[data-testid='save-and-publish-button']").click
f("button[data-testid='save-button']").click
wait_for_ajaximations
expect(f("span[data-testid='anon-conversation']").text).to eq "This is an anonymous Discussion, Your name and profile picture will be hidden from other course members."
expect(f("span[data-testid='author_name']").text).to include "Anonymous"
@ -455,7 +455,7 @@ describe "discussions" do
get "/courses/#{course.id}/discussion_topics/new"
f("input[placeholder='Topic Title']").send_keys "This is partially anonymous"
force_click("input[value='partial_anonymity']")
f("button[data-testid='save-and-publish-button']").click
f("button[data-testid='save-button']").click
wait_for_ajaximations
expect(f("span[data-testid='anon-conversation']").text).to eq "When creating a reply, you will have the option to show your name and profile picture to other course members or remain anonymous."
expect(f("span[data-testid='author_name']").text).to include "Anonymous"
@ -473,7 +473,7 @@ describe "discussions" do
force_click("input[value='Anonymous']")
fj("li:contains('student')").click
f("button[data-testid='save-and-publish-button']").click
f("button[data-testid='save-button']").click
wait_for_ajaximations
expect(f("span[data-testid='anon-conversation']").text).to eq "When creating a reply, you will have the option to show your name and profile picture to other course members or remain anonymous."
expect(f("span[data-testid='author_name']").text).to include "student"
@ -494,7 +494,7 @@ describe "discussions" do
force_click("input[value='student']")
fj("li:contains('Anonymous')").click
f("button[data-testid='save-and-publish-button']").click
f("button[data-testid='save-button']").click
wait_for_ajaximations
expect(f("span[data-testid='anon-conversation']").text).to eq "When creating a reply, you will have the option to show your name and profile picture to other course members or remain anonymous."
expect(f("span[data-testid='author_name']").text).to include "Anonymous"

View File

@ -58,7 +58,7 @@ export default function DiscussionTopicForm({
const isUnpublishedAnnouncement =
isAnnouncement && !ENV.DISCUSSION_TOPIC?.ATTRIBUTES.course_published
const isEditingAnnouncement = isAnnouncement && ENV.DISCUSSION_TOPIC?.ATTRIBUTES.id
const published = currentDiscussionTopic?.published ?? false
const announcementAlertProps = () => {
if (isUnpublishedAnnouncement) {
return {
@ -116,6 +116,7 @@ export default function DiscussionTopicForm({
const [availableFrom, setAvailableFrom] = useState(null)
const [availableUntil, setAvailableUntil] = useState(null)
const [willAnnouncementPostRightAway, setWillAnnouncementPostRightAway] = useState(true)
const [availabiltyValidationMessages, setAvailabilityValidationMessages] = useState([
{text: '', type: 'success'},
])
@ -129,7 +130,6 @@ export default function DiscussionTopicForm({
const [peerReviewDueDate, setPeerReviewDueDate] = useState('')
const [assignTo, setAssignTo] = useState('')
const [dueDate, setDueDate] = useState('')
const [showGroupCategoryModal, setShowGroupCategoryModal] = useState(false)
useEffect(() => {
@ -159,9 +159,18 @@ export default function DiscussionTopicForm({
setAvailableUntil(currentDiscussionTopic.lockAt)
setDelayPosting(!!currentDiscussionTopic.delayedPostAt && isAnnouncement)
setLocked(currentDiscussionTopic.locked && isAnnouncement)
}, [isEditing, currentDiscussionTopic, discussionAnonymousState, isAnnouncement])
useEffect(() => {
if (delayPosting) {
const rightNow = new Date()
const availableFromIntoDate = new Date(availableFrom)
setWillAnnouncementPostRightAway(availableFromIntoDate <= rightNow)
} else {
setWillAnnouncementPostRightAway(true)
}
}, [availableFrom, delayPosting])
const validateTitle = newTitle => {
if (newTitle.length > 255) {
setTitleValidationMessages([
@ -635,28 +644,54 @@ export default function DiscussionTopicForm({
margin="xx-large none"
padding="large none"
>
<Button type="button" color="secondary">
<Button
type="button"
color="secondary"
onClick={() => {
window.location.assign(ENV.CANCEL_TO)
}}
>
{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')}
</Button>
<Button
type="submit"
data-testid="save-button"
onClick={() =>
submitForm(currentDiscussionTopic ? currentDiscussionTopic.published : false)
}
color="primary"
>
{I18n.t('Save')}
</Button>
{/* discussion moderators viewing a new or still unpublished discussion */}
{!isAnnouncement && ENV.DISCUSSION_TOPIC?.PERMISSIONS?.CAN_MODERATE && !published && (
<Button
type="submit"
onClick={() => submitForm(true)}
color="secondary"
margin="xxx-small"
data-testid="save-and-publish-button"
>
{I18n.t('Save and Publish')}
</Button>
)}
{/* for announcements, show publish when the available until da */}
{isAnnouncement ? (
<Button
type="submit"
// we always process announcements as published.
onClick={() => submitForm(true)}
color="primary"
margin="xxx-small"
data-testid="announcement-submit-button"
>
{willAnnouncementPostRightAway ? I18n.t('Publish') : I18n.t('Save')}
</Button>
) : (
<Button
type="submit"
data-testid="save-button"
// when editing, use the current published state, otherwise:
// students will always save as published while for moderators in this case they
// can save as unpublished
onClick={() =>
submitForm(isEditing ? published : !ENV.DISCUSSION_TOPIC?.PERMISSIONS?.CAN_MODERATE)
}
color="primary"
>
{I18n.t('Save')}
</Button>
)}
</View>
</FormFieldGroup>
</>