Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
233 changes: 233 additions & 0 deletions src/cloud/components/Props/Pickers/TimePeriodPicker.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Container>
<PropertyValueButton
disabled={disabled}
empty={parsedValue == null}
isReadOnly={isReadOnly}
iconPath={mdiClockOutline}
sending={sending}
onClick={(e) =>
openContextModal(
e,
<TimePeriodModal
label={label}
defaultValue={parsedValue?.value}
defaultReason={parsedValue?.reason}
submitUpdate={onPeriodChange}
closeModal={closeAllModals}
/>,
{
alignment: popupAlignment,
}
)
}
>
<span className={cc([])}>
{parsedValue == null
? 'Add'
: `${parsedValue.value} ${plur(
parsedValue.reason.slice(0, -1),
parseInt(parsedValue.value)
)}`}
</span>
</PropertyValueButton>
</Container>
)
}

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<ReasonType>(defaultReason)
const inputRef = useRef<HTMLInputElement>(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 (
<ModalContainer>
<LeftRightList ignoreFocus>
<Form onSubmit={submit}>
<FormRow>
<FormRowItem>
<span className='prop__label'>{label}</span>
</FormRowItem>
<FormRowItem>
<FormInput
ref={inputRef}
id='prop__number'
type='number'
className='prop__number'
value={value}
onChange={(event) => setValue(event.target.value)}
/>
</FormRowItem>
<FormRowItem>
<SimpleFormSelect
id='prop__reason'
value={reason}
options={reasons}
onChange={(val) => setReason(val as ReasonType)}
/>
</FormRowItem>
<FormRowItem flex='0 0 auto'>
<Button
variant='primary'
disabled={
defaultValue === value &&
reason === defaultReason &&
isValidValue
}
type='submit'
id='prop__save'
>
Save
</Button>
</FormRowItem>
</FormRow>
</Form>
</LeftRightList>
</ModalContainer>
)
}

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;
}
`
25 changes: 25 additions & 0 deletions src/cloud/components/Props/PropPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down Expand Up @@ -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 (
<TimePeriodPicker
label={getLabelOfProp(propName)}
isReadOnly={readOnly}
sending={sendingMap.get(parent.target.id) === propName}
disabled={sendingMap.get(parent.target.id) != null || readOnly}
value={propData.data.data}
onPeriodChange={(val) => {
updateProp({
type: 'json',
data: { dataType: 'timeperiod', data: val },
})
}}
/>
)
}
return null
default:
return null
}
Expand Down
2 changes: 1 addition & 1 deletion src/cloud/lib/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
1 change: 1 addition & 0 deletions src/design/components/molecules/Form/atoms/FormSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down