diff --git a/src/actions/event-actions.js b/src/actions/event-actions.js index b59193771..795915f49 100644 --- a/src/actions/event-actions.js +++ b/src/actions/event-actions.js @@ -12,7 +12,7 @@ * */ import T from "i18n-react/dist/i18n-react"; -import debounce from "lodash/debounce" +import debounce from "lodash/debounce"; import { getRequest, putRequest, @@ -908,6 +908,50 @@ export const saveEvent = (entity, publish) => async (dispatch, getState) => { }); }; +export const saveEventAsDraft = (entity) => async (dispatch, getState) => { + if (!entity.id) { + console.error("saveEventAsDraft: entity.id is required"); + return; + } + + const { currentSummitState } = getState(); + const accessToken = await getAccessTokenSafely(); + const { currentSummit } = currentSummitState; + const { type_id } = entity; + const type = currentSummit.event_types.find((e) => e.id === type_id); + + dispatch(startLoading()); + + const normalizedEntity = normalizeEvent(entity, type, currentSummit); + + const params = { + access_token: accessToken, + expand: + "creator,speakers,moderator,sponsors,groups,type,type.allowed_media_upload_types,type.allowed_media_upload_types.type, slides, links, videos, media_uploads, tags, media_uploads.media_upload_type, media_uploads.media_upload_type.type,extra_questions,selection_plan,selection_plan.track_chair_rating_types,selection_plan.track_chair_rating_types.score_types,selection_plan.extra_questions,selection_plan.extra_questions.values,created_by,track_chair_scores_avg.ranking_type,actions,allowed_ticket_types", + fields: "allowed_ticket_types.id,allowed_ticket_types.name" + }; + + return putRequest( + createAction(UPDATE_EVENT), + createAction(EVENT_UPDATED), + `${window.API_BASE_URL}/api/v1/summits/${currentSummit.id}/events/${entity.id}/draft`, + normalizedEntity, + authErrorHandler, + entity + )(params)(dispatch).then(() => { + const success_message = { + title: T.translate("general.done"), + html: T.translate("edit_event.event_saved_as_draft"), + type: "success" + }; + dispatch( + showMessage(success_message, () => { + location.reload(); + }) + ); + }); +}; + export const saveEventFieldWithoutRefresh = (field) => async (dispatch, getState) => { const { currentSummitState, currentSummitEventState } = getState(); diff --git a/src/components/forms/event-form.js b/src/components/forms/event-form.js index 9d83656f1..1d66091c7 100644 --- a/src/components/forms/event-form.js +++ b/src/components/forms/event-form.js @@ -17,20 +17,20 @@ import "awesome-bootstrap-checkbox/awesome-bootstrap-checkbox.css"; import Swal from "sweetalert2"; import { Tooltip } from "react-tooltip"; import { epochToMomentTimeZone } from "openstack-uicore-foundation/lib/utils/methods"; -import Dropdown from "openstack-uicore-foundation/lib/components/inputs/dropdown" -import GroupedDropdown from "openstack-uicore-foundation/lib/components/inputs/grouped-dropdown" -import DateTimePicker from "openstack-uicore-foundation/lib/components/inputs/datetimepicker" -import TagInput from "openstack-uicore-foundation/lib/components/inputs/tag-input" -import SpeakerInput from "openstack-uicore-foundation/lib/components/inputs/speaker-input" -import CompanyInput from "openstack-uicore-foundation/lib/components/inputs/company-input" -import GroupInput from "openstack-uicore-foundation/lib/components/inputs/group-input" -import UploadInput from "openstack-uicore-foundation/lib/components/inputs/upload-input" -import Input from "openstack-uicore-foundation/lib/components/inputs/text-input" -import Panel from "openstack-uicore-foundation/lib/components/sections/panel" -import Table from "openstack-uicore-foundation/lib/components/table" -import MemberInput from "openstack-uicore-foundation/lib/components/inputs/member-input" -import FreeTextSearch from "openstack-uicore-foundation/lib/components/free-text-search" -import TicketTypesInput from "openstack-uicore-foundation/lib/components/inputs/ticket-types-input" +import Dropdown from "openstack-uicore-foundation/lib/components/inputs/dropdown"; +import GroupedDropdown from "openstack-uicore-foundation/lib/components/inputs/grouped-dropdown"; +import DateTimePicker from "openstack-uicore-foundation/lib/components/inputs/datetimepicker"; +import TagInput from "openstack-uicore-foundation/lib/components/inputs/tag-input"; +import SpeakerInput from "openstack-uicore-foundation/lib/components/inputs/speaker-input"; +import CompanyInput from "openstack-uicore-foundation/lib/components/inputs/company-input"; +import GroupInput from "openstack-uicore-foundation/lib/components/inputs/group-input"; +import UploadInput from "openstack-uicore-foundation/lib/components/inputs/upload-input"; +import Input from "openstack-uicore-foundation/lib/components/inputs/text-input"; +import Panel from "openstack-uicore-foundation/lib/components/sections/panel"; +import Table from "openstack-uicore-foundation/lib/components/table"; +import MemberInput from "openstack-uicore-foundation/lib/components/inputs/member-input"; +import FreeTextSearch from "openstack-uicore-foundation/lib/components/free-text-search"; +import TicketTypesInput from "openstack-uicore-foundation/lib/components/inputs/ticket-types-input"; import SortableTable from "openstack-uicore-foundation/lib/components/table-sortable"; import TextEditorV3 from "openstack-uicore-foundation/lib/components/inputs/editor-input-v3"; import { Pagination } from "react-bootstrap"; @@ -125,6 +125,7 @@ class EventForm extends React.Component { this.handleCloneEvent = this.handleCloneEvent.bind(this); this.handleEventTypeChange = this.handleEventTypeChange.bind(this); this.handleRSVPTypeChange = this.handleRSVPTypeChange.bind(this); + this.handleSaveIncomplete = this.handleSaveIncomplete.bind(this); } componentDidMount() { @@ -721,6 +722,44 @@ class EventForm extends React.Component { ); } + handleSaveIncomplete(ev) { + ev.preventDefault(); + const { onSaveIncomplete } = this.props; + const { entity } = this.state; + onSaveIncomplete({ ...entity }); + } + + isNew() { + const { entity } = this.state; + return !entity.id; + } + + isComplete() { + const { entity } = this.state; + return entity?.status === "Received" && entity?.progress === "COMPLETE"; + } + + getMissingDraftFields() { + const { entity } = this.state; + const missing = []; + + if (!entity.title) missing.push("Title"); + if (!entity.type_id) missing.push("Activity Type"); + if (!entity.track_id) missing.push("Activity Category"); + + if (!entity.type_id || this.shouldShowField("allows_publishing_dates")) { + if (!entity.start_date) missing.push("Start Date"); + if (!entity.end_date) missing.push("End Date"); + if (!entity.duration) missing.push("Duration"); + } + + if (!entity.type_id || this.isEventType(EVENT_TYPE_PRESENTATION)) { + if (!entity.disclaimer_accepted) missing.push("Disclaimer Accepted"); + } + + return missing; + } + handleEventTypeChange(oldEntity, newEntity) { const isEventUpgrade = !this.isEventType(EVENT_TYPE_PRESENTATION, oldEntity) && @@ -1038,9 +1077,47 @@ class EventForm extends React.Component { { label: "Submission", value: "Submission" } ]; + const missingDraftFields = + this.isNew() || this.isComplete() ? [] : this.getMissingDraftFields(); + return (
+ {!this.isNew() && !this.isComplete() && ( +
+
+ {T.translate("edit_event.draft_state_label")} + + {T.translate("edit_event.draft_state_badge")} + +
+ {missingDraftFields.length > 0 && ( +

+ + {T.translate("edit_event.draft_state_missing_fields")} + {" "} + {missingDraftFields.join(", ")} +

+ )} +

+ {T.translate("edit_event.draft_state_note")} +

+
+ )}
  @@ -2059,7 +2136,7 @@ class EventForm extends React.Component { type="button" onClick={(ev) => this.triggerFormSubmit(ev, false)} className="btn btn-primary pull-right" - value={T.translate("general.save")} + value={T.translate("edit_event.save_and_mark_complete")} /> + {!this.isNew() && !this.isComplete() && ( + + )}
)} diff --git a/src/i18n/en.json b/src/i18n/en.json index 0bb5e83e8..e73973b47 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -681,7 +681,14 @@ "search_comment": "Search by Comment, Creator ID", "allowed_ticket_types": "Select ticket types", "select_submission_source": "Select a Submission Source" - } + }, + "draft_state_label": "DRAFT STATE: NOT SUBMITTED BY SUBMITTER", + "draft_state_badge": "Pending Complete Details", + "draft_state_missing_fields": "Missing required fields:", + "draft_state_note": "This activity was saved as a draft by the submitter and contains missing required fields. To make partial changes (e.g., re-categorize or assign tags) without filling out missing fields, use the Save as Incomplete button below. This preserves the draft status.", + "save_as_incomplete": "Save as Incomplete", + "save_and_mark_complete": "Save & Mark Complete", + "event_saved_as_draft": "Activity saved as draft successfully." }, "edit_event_material": { "material": "Material", diff --git a/src/pages/events/edit-summit-event-page.js b/src/pages/events/edit-summit-event-page.js index b71b43357..912646db7 100644 --- a/src/pages/events/edit-summit-event-page.js +++ b/src/pages/events/edit-summit-event-page.js @@ -17,6 +17,7 @@ import T from "i18n-react/dist/i18n-react"; import EventForm from "../../components/forms/event-form"; import { saveEvent, + saveEventAsDraft, saveEventFieldWithoutRefresh, attachFile, getEvents, @@ -225,6 +226,7 @@ function EditSummitEventPage(props) { loading, history, saveEvent, + saveEventAsDraft, saveEventFieldWithoutRefresh, attachFile, unPublishEvent, @@ -289,6 +291,7 @@ function EditSummitEventPage(props) { entity={entity} errors={errors} onSubmit={saveEvent} + onSaveIncomplete={saveEventAsDraft} onUpdate={saveEventFieldWithoutRefresh} onEventUpgrade={upgradeEvent} onAttach={attachFile} @@ -336,6 +339,7 @@ const mapStateToProps = ({ export default connect(mapStateToProps, { saveEvent, + saveEventAsDraft, saveEventFieldWithoutRefresh, attachFile, unPublishEvent,