diff --git a/client/src/components/CustomDateInput.js b/client/src/components/CustomDateInput.js
index 45399c5b41..2d8d17da35 100644
--- a/client/src/components/CustomDateInput.js
+++ b/client/src/components/CustomDateInput.js
@@ -30,18 +30,21 @@ const CustomDateInput = ({
withTime,
value,
onChange,
- onBlur
+ onBlur,
+ fullWidth,
+ allDay
}) => {
const inputRef = useRef()
const rightElement = showIcon && CalendarIcon(inputRef.current)
const width = 8 + (showIcon ? 3 : 0) + (withTime ? 3 : 0)
- const style = { width: `${width}em`, fontSize: "1.1em" }
- const dateFormats = withTime
- ? Settings.dateFormats.forms.input.withTime
- : Settings.dateFormats.forms.input.date
+ const style = { width: fullWidth ? "100%" : `${width}em`, fontSize: "1.1em" }
+ const dateFormats =
+ withTime && !allDay
+ ? Settings.dateFormats.forms.input.withTime
+ : Settings.dateFormats.forms.input.date
const inputFormat = dateFormats[0]
const timePickerProps = !withTime
- ? {}
+ ? undefined
: {
precision: TimePrecision.MINUTE,
selectAllOnFocus: true
@@ -70,6 +73,7 @@ const CustomDateInput = ({
timePickerProps={timePickerProps}
popoverProps={{ usePortal: false }}
disabled={disabled}
+ fill={fullWidth}
/>
)
}
@@ -84,12 +88,16 @@ CustomDateInput.propTypes = {
PropTypes.instanceOf(Date)
]),
onChange: PropTypes.func,
- onBlur: PropTypes.func
+ onBlur: PropTypes.func,
+ fullWidth: PropTypes.bool,
+ allDay: PropTypes.bool
}
CustomDateInput.defaultProps = {
disabled: false,
showIcon: true,
- withTime: false
+ withTime: false,
+ fullWidth: false,
+ allDay: true
}
export default CustomDateInput
diff --git a/client/src/components/ReportSummary.js b/client/src/components/ReportSummary.js
index 33151a7a2e..292b855430 100644
--- a/client/src/components/ReportSummary.js
+++ b/client/src/components/ReportSummary.js
@@ -245,9 +245,7 @@ const ReportSummaryRow = ({ report }) => {
{report.engagementDate && (
)}
diff --git a/client/src/components/ReportTable.js b/client/src/components/ReportTable.js
index 9095eb59e2..cde595694d 100644
--- a/client/src/components/ReportTable.js
+++ b/client/src/components/ReportTable.js
@@ -6,7 +6,6 @@ import UltimatePaginationTopDown from "components/UltimatePaginationTopDown"
import _get from "lodash/get"
import _isEqual from "lodash/isEqual"
import { Report } from "models"
-import moment from "moment"
import PropTypes from "prop-types"
import React, { useEffect, useRef, useState } from "react"
import { Table } from "react-bootstrap"
@@ -172,11 +171,7 @@ const ReportTable = ({
/>
{showStatus && {report.state} | }
-
- {moment(report.engagementDate).format(
- Report.getEngagementDateFormat()
- )}
- |
+ {Report.getFormattedEngagementDate(report)} |
))}
diff --git a/client/src/components/aggregations/ReportsMapWidget.js b/client/src/components/aggregations/ReportsMapWidget.js
index 5229366153..4d53dff653 100644
--- a/client/src/components/aggregations/ReportsMapWidget.js
+++ b/client/src/components/aggregations/ReportsMapWidget.js
@@ -5,7 +5,7 @@ import {
import Leaflet from "components/Leaflet"
import _escape from "lodash/escape"
import _isEmpty from "lodash/isEmpty"
-import { Location } from "models"
+import { Location, Report } from "models"
import PropTypes from "prop-types"
import React, { useMemo } from "react"
@@ -25,7 +25,9 @@ const ReportsMapWidget = ({
values.forEach(report => {
if (Location.hasCoordinates(report.location)) {
let label = _escape(report.intent || "") // escape HTML in intent!
- label += `
@ ${_escape(report.location.name)}` // escape HTML in locationName!
+ label += `
@ ${_escape(
+ report.location.name
+ )} ${Report.getAllDayIndicator(report)}` // escape HTML in locationName!
markerArray.push({
id: report.uuid,
lat: report.location.lat,
diff --git a/client/src/components/aggregations/utils.js b/client/src/components/aggregations/utils.js
index 52650f16ff..6e1b62e273 100644
--- a/client/src/components/aggregations/utils.js
+++ b/client/src/components/aggregations/utils.js
@@ -210,7 +210,7 @@ export function reportsToEvents(reports) {
(r.location && r.location.name) ||
""
return {
- title: who + "@" + where,
+ title: `${who} @ ${where} - ${Report.getAllDayIndicator(r)}`,
start: moment(r.engagementDate).format("YYYY-MM-DD HH:mm"),
end: moment(r.engagementDate)
.add(r.duration, "minutes")
diff --git a/client/src/index.css b/client/src/index.css
index af772f7e44..c9c1981ed1 100644
--- a/client/src/index.css
+++ b/client/src/index.css
@@ -148,7 +148,7 @@ fieldset.danger {
}
#duration {
- width: 10em;
+ width: 100%;
}
.shortcut-list {
diff --git a/client/src/models/Report.js b/client/src/models/Report.js
index 2136401375..05af171b40 100644
--- a/client/src/models/Report.js
+++ b/client/src/models/Report.js
@@ -106,7 +106,26 @@ export default class Report extends Model {
.nullable()
.required("You must provide the Date of Engagement")
.default(null),
- duration: yup.number().nullable().default(null),
+ duration: yup
+ .number()
+ .nullable()
+ .test(
+ "duration",
+ "You must provide duration when engagement time(hour:minute) is provided",
+ function(duration) {
+ const { engagementDate } = this.parent
+ if (
+ !engagementDate ||
+ moment(engagementDate).isSame(
+ moment(engagementDate).startOf("day")
+ )
+ ) {
+ return true
+ }
+ return !!duration
+ }
+ )
+ .default(null),
// not actually in the database, but used for validation:
cancelled: yup
.boolean()
@@ -570,16 +589,24 @@ export default class Report extends Model {
)
}
+ static isEngagementAllDay(report) {
+ return !report.duration
+ }
+
+ static getAllDayIndicator(report) {
+ return Report.isEngagementAllDay(report) ? " (all day)" : ""
+ }
+
static getFormattedEngagementDate(report) {
if (!report?.engagementDate) {
return ""
}
const start = moment(report.engagementDate)
- if (!report.duration) {
+ if (Report.isEngagementAllDay(report)) {
return Settings.engagementsIncludeTimeAndDuration
? start.format(Settings.dateFormats.forms.displayLong.date) +
- " (all day)"
+ Report.getAllDayIndicator(report)
: start.format(Report.getEngagementDateFormat())
}
diff --git a/client/src/pages/reports/EngagementDateFormPartial.js b/client/src/pages/reports/EngagementDateFormPartial.js
new file mode 100644
index 0000000000..aaadb4d901
--- /dev/null
+++ b/client/src/pages/reports/EngagementDateFormPartial.js
@@ -0,0 +1,187 @@
+import { Checkbox } from "@blueprintjs/core"
+import CustomDateInput from "components/CustomDateInput"
+import * as FieldHelper from "components/FieldHelper"
+import { FastField, Field } from "formik"
+import { Report } from "models"
+import moment from "moment"
+import PropTypes from "prop-types"
+import React, { useState } from "react"
+import { Col, ControlLabel, FormGroup, HelpBlock } from "react-bootstrap"
+import Settings from "settings"
+import utils from "utils"
+
+const futureEngagementHint = (
+
+ This will create a planned engagement
+
+)
+
+function isStartOfDay(date) {
+ return date && moment(date).isSame(moment(date).startOf("day"))
+}
+
+const EngagementDatePartialFormWithDuration = ({
+ setFieldValue,
+ setFieldTouched,
+ validateFieldDebounced,
+ initialValues,
+ edit,
+ values
+}) => {
+ const [isAllDay, setIsAllDay] = useState(() =>
+ getInitalAllDayState(edit, initialValues)
+ )
+
+ return (
+
+
+ Engagement planning
+
+
+ {
+ const sval = value ? moment(value).startOf("minute").toDate() : null
+ setFieldTouched("engagementDate", true, false) // onBlur doesn't work when selecting a date
+ setFieldValue("engagementDate", sval, true)
+ if (!sval) {
+ setIsAllDay(true)
+ } else if (!isStartOfDay(sval)) {
+ setIsAllDay(false)
+ }
+ }}
+ onBlur={() => setFieldTouched("engagementDate")}
+ widget={
+
+ }
+ vertical
+ >
+ {Report.isFuture(values.engagementDate) && futureEngagementHint}
+
+
+
+ event.currentTarget.blur()} // Prevent scroll action on number input
+ onChange={event => {
+ const safeVal =
+ utils.preventNonPositiveAndLongDigits(event.target.value, 4) ||
+ null
+ setFieldTouched("duration", true, false)
+ setFieldValue("duration", safeVal, false)
+ validateFieldDebounced("duration")
+ setIsAllDay(false)
+ }}
+ vertical
+ disabled={isAllDay}
+ />
+
+
+ {
+ setIsAllDay(e.target.checked)
+ if (e.target.checked) {
+ setFieldValue("duration", null, true)
+ validateFieldDebounced("duration")
+ if (values.engagementDate) {
+ setFieldValue(
+ "engagementDate",
+ moment(values.engagementDate).startOf("day").toDate(),
+ false
+ )
+ }
+ }
+ }}
+ />
+
+
+ )
+}
+
+function getInitalAllDayState(edit, initialValues) {
+ if (!edit || !initialValues.engagementDate) {
+ return true
+ } else if (!isStartOfDay(initialValues.engagementDate)) {
+ return false
+ } else {
+ return initialValues.duration === null
+ }
+}
+
+EngagementDatePartialFormWithDuration.propTypes = {
+ setFieldValue: PropTypes.func.isRequired,
+ setFieldTouched: PropTypes.func.isRequired,
+ validateFieldDebounced: PropTypes.func.isRequired,
+ values: PropTypes.object.isRequired,
+ initialValues: PropTypes.instanceOf(Report).isRequired,
+ edit: PropTypes.bool.isRequired
+}
+
+const EngagementDateFormPartial = ({
+ setFieldValue,
+ setFieldTouched,
+ validateFieldDebounced,
+ initialValues,
+ edit,
+ values
+}) => {
+ if (!Settings.engagementsIncludeTimeAndDuration) {
+ return (
+ {
+ const val = value ? moment(value).startOf("day").toDate() : null
+ setFieldValue("engagementDate", val, true)
+ }}
+ widget={}
+ >
+ {Report.isFuture(values.engagementDate) && futureEngagementHint}
+
+ )
+ }
+
+ return (
+
+ )
+}
+
+EngagementDateFormPartial.propTypes = {
+ setFieldValue: PropTypes.func.isRequired,
+ setFieldTouched: PropTypes.func.isRequired,
+ validateFieldDebounced: PropTypes.func.isRequired,
+ values: PropTypes.object.isRequired,
+ initialValues: PropTypes.instanceOf(Report).isRequired,
+ edit: PropTypes.bool.isRequired
+}
+
+export default EngagementDateFormPartial
diff --git a/client/src/pages/reports/Form.js b/client/src/pages/reports/Form.js
index e715b70f80..59f6868f31 100644
--- a/client/src/pages/reports/Form.js
+++ b/client/src/pages/reports/Form.js
@@ -13,7 +13,6 @@ import AdvancedSingleSelect from "components/advancedSelectWidget/AdvancedSingle
import AppContext from "components/AppContext"
import InstantAssessmentsContainerField from "components/assessments/InstantAssessmentsContainerField"
import ConfirmDelete from "components/ConfirmDelete"
-import CustomDateInput from "components/CustomDateInput"
import {
CustomFieldsContainer,
customFieldsJSONString
@@ -45,11 +44,12 @@ import _isEqual from "lodash/isEqual"
import _upperFirst from "lodash/upperFirst"
import { AuthorizationGroup, Location, Person, Report, Tag, Task } from "models"
import moment from "moment"
+import EngagementDateFormPartial from "pages/reports/EngagementDateFormPartial"
import { RECURRENCE_TYPE } from "periodUtils"
import pluralize from "pluralize"
import PropTypes from "prop-types"
import React, { useContext, useEffect, useRef, useState } from "react"
-import { Button, Checkbox, Collapse, HelpBlock } from "react-bootstrap"
+import { Button, Checkbox, Collapse } from "react-bootstrap"
import { connect } from "react-redux"
import { useHistory } from "react-router-dom"
import { toast } from "react-toastify"
@@ -500,49 +500,14 @@ const ReportForm = ({
className="meeting-goal"
/>
- {
- setFieldTouched("engagementDate", true, false) // onBlur doesn't work when selecting a date
- setFieldValue("engagementDate", value, true)
- }}
- onBlur={() => setFieldTouched("engagementDate")}
- widget={
-
- }
- >
- {isFutureEngagement && (
-
-
- This will create a planned engagement
-
-
- )}
-
-
- {Settings.engagementsIncludeTimeAndDuration && (
- event.currentTarget.blur()} // Prevent scroll action on number input
- onChange={event => {
- const safeVal =
- utils.preventNegativeAndLongDigits(
- event.target.value,
- 4
- ) || null
- setFieldTouched("duration", true, false)
- setFieldValue("duration", safeVal, false)
- validateFieldDebounced("duration")
- }}
- />
- )}
+
{
component={FieldHelper.ReadonlyField}
humanValue={
<>
- {report.engagementDate &&
- moment(report.engagementDate).format(
- Report.getEngagementDateFormat()
- )}
+ {Report.getFormattedEngagementDate(report)}
>
}
diff --git a/client/src/utils.js b/client/src/utils.js
index 825dd60317..816423f9cd 100644
--- a/client/src/utils.js
+++ b/client/src/utils.js
@@ -199,11 +199,11 @@ export default {
)
},
- preventNegativeAndLongDigits: function(valueStr, maxLen) {
+ preventNonPositiveAndLongDigits: function(valueStr, maxLen) {
let safeVal
const dangerVal = Number(valueStr)
- if (!isNaN(dangerVal) && dangerVal < 0) {
- safeVal = "0"
+ if (!isNaN(dangerVal) && dangerVal <= 0) {
+ safeVal = null
} else {
const nonDigitsRemoved = valueStr.replace(/\D/g, "")
safeVal =
diff --git a/client/tests/e2e/report.js b/client/tests/e2e/report.js
index 357b6b1565..2bffd4a184 100644
--- a/client/tests/e2e/report.js
+++ b/client/tests/e2e/report.js
@@ -32,8 +32,11 @@ test.serial("Draft and submit a report", async t => {
await pageHelpers.clickTodayButton()
- const $intent = await $("#intent")
- await $intent.click() // click intent to make sure the date picker is being closed
+ const $intent = await $('label[for="intent"]')
+ await $intent.click() // click intent label to make sure the date picker is being closed
+
+ await pageHelpers.writeInForm("#duration", "30")
+ await t.context.driver.sleep(shortWaitMs) // wait for the datepicker to pop up
const $locationAdvancedSelect = await pageHelpers.chooseAdvancedSelectOption(
"#location",
@@ -445,7 +448,7 @@ test.serial(
)
await $input.sendKeys("user input")
- await $input.sendKeys(t.context.Key.TAB) // fire blur event
+ await $searchBarInput.click() // fire blur event
t.false(
_includes(await $fieldGroup.getAttribute("class"), warningClass),
`After typing in ${fieldName} field, warning state goes away`
diff --git a/client/tests/webdriver/baseSpecs/createReportWithPlanningConflict.spec.js b/client/tests/webdriver/baseSpecs/createReportWithPlanningConflict.spec.js
index ec9a8d6e9f..a84b7fd708 100644
--- a/client/tests/webdriver/baseSpecs/createReportWithPlanningConflict.spec.js
+++ b/client/tests/webdriver/baseSpecs/createReportWithPlanningConflict.spec.js
@@ -4,13 +4,53 @@ import CreateReport from "../pages/report/createReport.page"
import EditReport from "../pages/report/editReport.page"
import ShowReport from "../pages/report/showReport.page"
+// NOTE: Copied Report model logic here because importing issues
+
+function getFormattedDateInput(report) {
+ return report.engagementDate.format("DD-MM-YYYY HH:mm")
+}
+
+function isEngagementAllDay(report) {
+ return !report.duration
+}
+
+function getEngagementDateFormat() {
+ return "dddd, D MMMM YYYY @ HH:mm"
+}
+
+function getAllDayIndicator(report) {
+ return isEngagementAllDay(report) ? " (all day)" : ""
+}
+
+function getFormattedEngagementDate(report) {
+ if (!report?.engagementDate) {
+ return ""
+ }
+
+ const start = moment(report.engagementDate)
+ if (isEngagementAllDay(report)) {
+ return start.format("dddd, D MMMM YYYY") + getAllDayIndicator(report)
+ }
+
+ const end = moment(report.engagementDate).add(report.duration, "minutes")
+
+ return (
+ start.format(getEngagementDateFormat()) +
+ end.format(
+ start.isSame(end, "day")
+ ? " - HH:mm"
+ : " >>> " + getEngagementDateFormat()
+ )
+ )
+}
+
describe("When creating a Report with conflicts", () => {
let firstReportUUID
let secondReportUUID
const report01 = {
intent: "111111111111",
engagementDate: moment()
- .add(1, "day")
+ .add(2, "day")
.hours(1)
.minutes(0)
.seconds(0)
@@ -22,7 +62,7 @@ describe("When creating a Report with conflicts", () => {
const report02 = {
intent: "2222222222",
engagementDate: moment()
- .add(1, "day")
+ .add(2, "day")
.hours(1)
.minutes(10)
.seconds(0)
@@ -38,7 +78,7 @@ describe("When creating a Report with conflicts", () => {
expect(CreateReport.intent.getValue()).to.equal(report01.intent)
expect(CreateReport.engagementDate.getValue()).to.equal(
- report01.engagementDate.format("DD-MM-YYYY HH:mm")
+ getFormattedDateInput(report01)
)
expect(CreateReport.duration.getValue()).to.equal(report01.duration)
const advisor01 = CreateReport.getPersonByName("CIV ERINSON, Erin")
@@ -70,7 +110,7 @@ describe("When creating a Report with conflicts", () => {
expect(CreateReport.intent.getValue()).to.equal(report02.intent)
expect(CreateReport.engagementDate.getValue()).to.equal(
- report02.engagementDate.format("DD-MM-YYYY HH:mm")
+ getFormattedDateInput(report02)
)
expect(CreateReport.duration.getValue()).to.equal(report02.duration)
const advisor01 = CreateReport.getPersonByName("CIV ERINSON, Erin")
@@ -116,7 +156,7 @@ describe("When creating a Report with conflicts", () => {
expect(ShowReport.intent).to.equal(report01.intent)
expect(ShowReport.engagementDate).to.equal(
- report01.engagementDate.format("dddd, D MMMM YYYY @ HH:mm")
+ getFormattedEngagementDate(report01)
)
expect(ShowReport.reportConflictIcon.isExisting()).to.equal(true)
@@ -157,7 +197,7 @@ describe("When creating a Report with conflicts", () => {
expect(ShowReport.intent).to.equal(report02.intent)
expect(ShowReport.engagementDate).to.equal(
- report02.engagementDate.format("dddd, D MMMM YYYY @ HH:mm")
+ getFormattedEngagementDate(report02)
)
expect(ShowReport.reportConflictIcon.isExisting()).to.equal(true)
diff --git a/client/tests/webdriver/customFieldsSpecs/createReport.spec.js b/client/tests/webdriver/customFieldsSpecs/createReport.spec.js
index 050c77a204..48301ad5cf 100644
--- a/client/tests/webdriver/customFieldsSpecs/createReport.spec.js
+++ b/client/tests/webdriver/customFieldsSpecs/createReport.spec.js
@@ -9,7 +9,7 @@ const INVALID_ENGAGEMENT_DURATION_1 = "123456"
const INVALID_ENGAGEMENT_DURATION_2 = "-1"
// positive sliced at 4th digit, negative should turn into 0
const VALID_ENGAGEMENT_DURATION_1 = "1234"
-const VALID_ENGAGEMENT_DURATION_2 = "0"
+const VALID_ENGAGEMENT_DURATION_2 = ""
const PERSON = "EF 2.1"
const PERSON_VALUE_1 = "HENDERSON, Henry"
@@ -36,6 +36,10 @@ describe("Create report form page", () => {
})
it("Should be able to prevent invalid duration values", () => {
+ // make it not an all day first to enable duration input
+ CreateReport.allDayCheckbox.click()
+ CreateReport.duration.waitForClickable()
+
CreateReport.duration.setValue(INVALID_ENGAGEMENT_DURATION_1)
browser.waitUntil(
() => {
@@ -48,7 +52,13 @@ describe("Create report form page", () => {
timeoutMsg: "Large positive duration value was not sliced "
}
)
- CreateReport.duration.setValue(INVALID_ENGAGEMENT_DURATION_2)
+
+ // remove first value, otherwise appends the value
+ CreateReport.duration.setValue(
+ "\uE003".repeat(VALID_ENGAGEMENT_DURATION_1) +
+ INVALID_ENGAGEMENT_DURATION_2
+ )
+
browser.waitUntil(
() => {
return (
diff --git a/client/tests/webdriver/pages/createReport.page.js b/client/tests/webdriver/pages/createReport.page.js
index abea110f4b..862a02c461 100644
--- a/client/tests/webdriver/pages/createReport.page.js
+++ b/client/tests/webdriver/pages/createReport.page.js
@@ -40,6 +40,10 @@ export class CreateReport extends Page {
return browser.$(`div[id="fg-${id}"]`)
}
+ get allDayCheckbox() {
+ return browser.$("#all-day-col label")
+ }
+
get engagementInformationTitle() {
return browser.$('//span[text()="Engagement information"]')
}
diff --git a/client/tests/webdriver/pages/report/createReport.page.js b/client/tests/webdriver/pages/report/createReport.page.js
index 222425dc0a..7651788b91 100644
--- a/client/tests/webdriver/pages/report/createReport.page.js
+++ b/client/tests/webdriver/pages/report/createReport.page.js
@@ -24,15 +24,20 @@ class CreateReport extends Page {
return browser.$("#engagementDate")
}
- get tomorrow() {
- const tomorrow = moment().add(1, "day").format("ddd MMM DD YYYY")
- return browser.$(`div[aria-label="${tomorrow}"]`)
+ get datePickPopover() {
+ // check the today button
+ const today = moment().format("ddd MMM DD YYYY")
+ return browser.$(`div[aria-label="${today}"]`)
}
get duration() {
return browser.$("#duration")
}
+ get allDayCheckbox() {
+ return browser.$("#all-day-col label")
+ }
+
get reportPeople() {
return browser.$("#reportPeople")
}
@@ -91,13 +96,17 @@ class CreateReport extends Page {
this.intentHelpBlock.waitForExist({ reverse: true })
if (moment.isMoment(fields.engagementDate)) {
+ // remove all day as it would block duration adding
+ if (!this.duration.isClickable()) {
+ this.allDayCheckbox.click()
+ }
this.engagementDate.waitForClickable()
this.engagementDate.click()
- this.tomorrow.waitForDisplayed()
+ this.datePickPopover.waitForDisplayed()
browser.keys(fields.engagementDate.format("DD-MM-YYYY HH:mm"))
this.title.click()
- this.tomorrow.waitForExist({ reverse: true, timeout: 3000 })
+ this.datePickPopover.waitForExist({ reverse: true, timeout: 3000 })
}
if (fields.duration !== undefined) {