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 ( + + +
+ + + {label} + + + setValue(event.target.value)} + /> + + + setReason(val as ReasonType)} + /> + + + + + +
+
+
+ ) +} + +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 {