diff --git a/src/cloud/components/Props/Pickers/TimePeriodPicker.tsx b/src/cloud/components/Props/Pickers/TimePeriodPicker.tsx
new file mode 100644
index 0000000000..a639c78dea
--- /dev/null
+++ b/src/cloud/components/Props/Pickers/TimePeriodPicker.tsx
@@ -0,0 +1,233 @@
+import { mdiClockOutline } from '@mdi/js'
+import React, { useCallback, useMemo, useRef, useState } from 'react'
+import { useModal } from '../../../../design/lib/stores/modal'
+import styled from '../../../../design/lib/styled'
+import PropertyValueButton from './PropertyValueButton'
+import cc from 'classcat'
+import Form from '../../../../design/components/molecules/Form'
+import FormRow from '../../../../design/components/molecules/Form/templates/FormRow'
+import FormRowItem from '../../../../design/components/molecules/Form/templates/FormRowItem'
+import FormInput from '../../../../design/components/molecules/Form/atoms/FormInput'
+import { SimpleFormSelect } from '../../../../design/components/molecules/Form/atoms/FormSelect'
+import LeftRightList from '../../../../design/components/atoms/LeftRightList'
+import { useEffectOnce } from 'react-use'
+import Button from '../../../../design/components/atoms/Button'
+import plur from 'plur'
+
+interface TimePeriodPickerProps {
+ label: string
+ sending?: boolean
+ value?: number | null
+ disabled?: boolean
+ isReadOnly: boolean
+ onPeriodChange: (newVal: number | null) => void
+ popupAlignment?: 'bottom-left' | 'top-left'
+}
+
+const TimePeriodPicker = ({
+ label,
+ disabled,
+ sending,
+ value,
+ isReadOnly,
+ popupAlignment = 'bottom-left',
+ onPeriodChange,
+}: TimePeriodPickerProps) => {
+ const { openContextModal, closeAllModals } = useModal()
+
+ const parsedValue:
+ | {
+ value: string
+ reason: ReasonType
+ }
+ | undefined = useMemo(() => {
+ if (value == null || typeof value !== 'number') {
+ return undefined
+ }
+
+ let parsedValue
+
+ for (let i = 0; i < reasons.length; i++) {
+ const reasonMultipler = getReasonMultiplier(reasons[i])
+ if (value % reasonMultipler === 0) {
+ parsedValue = {
+ value: (value / reasonMultipler).toString(),
+ reason: reasons[i],
+ }
+ }
+ }
+
+ if (parsedValue == null) {
+ return undefined
+ }
+
+ return parsedValue
+ }, [value])
+
+ return (
+
+
+ openContextModal(
+ e,
+ ,
+ {
+ alignment: popupAlignment,
+ }
+ )
+ }
+ >
+
+ {parsedValue == null
+ ? 'Add'
+ : `${parsedValue.value} ${plur(
+ parsedValue.reason.slice(0, -1),
+ parseInt(parsedValue.value)
+ )}`}
+
+
+
+ )
+}
+
+const Container = styled.div``
+
+export default TimePeriodPicker
+
+const reasons: ReasonType[] = ['Seconds', 'Minutes', 'Hours', 'Days', 'Weeks']
+
+type ReasonType = 'Seconds' | 'Minutes' | 'Hours' | 'Days' | 'Weeks'
+
+const TimePeriodModal = ({
+ defaultValue,
+ defaultReason = 'Hours',
+ label,
+ submitUpdate,
+ closeModal,
+}: {
+ label: string
+ defaultValue?: string
+ defaultReason?: ReasonType
+ submitUpdate: (val: number | null) => void
+ closeModal: () => void
+}) => {
+ const [value, setValue] = useState(defaultValue)
+ const [reason, setReason] = useState(defaultReason)
+ const inputRef = useRef(null)
+
+ useEffectOnce(() => {
+ if (inputRef.current != null) {
+ inputRef.current.focus()
+ }
+ })
+
+ const isValidValue = useMemo(() => {
+ return value != null && typeof parseInt(value) === 'number'
+ }, [value])
+
+ const submit: React.FormEventHandler = useCallback(
+ (event) => {
+ event.preventDefault()
+ if (value == null) {
+ submitUpdate(null)
+ closeModal()
+ return
+ }
+
+ const parsedValue = parseInt(value)
+ if (typeof parsedValue !== 'number') {
+ return
+ }
+
+ const valueToSeconds = parsedValue * getReasonMultiplier(reason)
+ submitUpdate(valueToSeconds)
+ closeModal()
+ return
+ },
+ [value, reason, closeModal, submitUpdate]
+ )
+
+ return (
+
+
+
+
+
+ )
+}
+
+function getReasonMultiplier(reason: ReasonType) {
+ switch (reason) {
+ case 'Seconds':
+ return 1
+ case 'Minutes':
+ return 60
+ case 'Hours':
+ return 60 * 60
+ case 'Days':
+ return 60 * 60 * 24
+ case 'Weeks':
+ return 60 * 60 * 24 * 7
+ }
+}
+
+const ModalContainer = styled.div`
+ .prop__label {
+ display: flexbox;
+ align-items: center;
+ white-space: nowrap;
+ }
+
+ .prop__number {
+ width: 90px;
+ }
+`
diff --git a/src/cloud/components/Props/PropPicker.tsx b/src/cloud/components/Props/PropPicker.tsx
index 3cfaf11434..47015b0abb 100644
--- a/src/cloud/components/Props/PropPicker.tsx
+++ b/src/cloud/components/Props/PropPicker.tsx
@@ -10,6 +10,8 @@ import DueDateSelect from './Pickers/DueDateSelect'
import { format as formatDate } from 'date-fns'
import { toLower } from 'lodash'
import StatusSelect from './Pickers/StatusSelect'
+import TimePeriodPicker from './Pickers/TimePeriodPicker'
+import { getLabelOfProp } from '../../lib/props'
interface PropPickerProps {
parent: { type: 'doc'; target: SerializedDocWithSupplemental }
@@ -113,6 +115,29 @@ const PropPicker = ({
} else {
return null
}
+ case 'json':
+ if (
+ propData.data != null &&
+ propData.data.dataType === 'timeperiod' &&
+ (propData.data.data == null || typeof propData.data.data === 'number')
+ ) {
+ return (
+ {
+ updateProp({
+ type: 'json',
+ data: { dataType: 'timeperiod', data: val },
+ })
+ }}
+ />
+ )
+ }
+ return null
default:
return null
}
diff --git a/src/cloud/lib/props.ts b/src/cloud/lib/props.ts
index 3085bbc020..500acf0415 100644
--- a/src/cloud/lib/props.ts
+++ b/src/cloud/lib/props.ts
@@ -83,7 +83,7 @@ export function getInitialPropDataOfProp(propName: string): NullablePropData {
return { type: 'date', data: null }
case 'timeEstimate':
case 'timeTracked':
- return { type: 'number', data: null }
+ return { type: 'json', data: { dataType: 'timeperiod', data: null } }
case 'reviewers':
case 'assignees':
return { type: 'user', data: null }
diff --git a/src/design/components/molecules/Form/atoms/FormSelect.tsx b/src/design/components/molecules/Form/atoms/FormSelect.tsx
index 73daf6c315..d7ede83200 100644
--- a/src/design/components/molecules/Form/atoms/FormSelect.tsx
+++ b/src/design/components/molecules/Form/atoms/FormSelect.tsx
@@ -178,6 +178,7 @@ const Container = styled.div`
border: none;
&.form__select__control--is-focused {
box-shadow: ${({ theme }) => theme.colors.shadow};
+ border-color: #33B5E5;
}
}
.form__select {