Add More options to AddTaskModal to handle most of the things we have in editChore
This commit is contained in:
@@ -25,6 +25,9 @@ import KeyboardShortcutHint from '../../components/common/KeyboardShortcutHint'
|
||||
import { useDocumentScanner } from '../../hooks/useDocumentScanner'
|
||||
import { localAIService } from '../../service/LocalAIService'
|
||||
import { TASK_COLOR } from '../../utils/Colors'
|
||||
import AdvancedOptionsSection, {
|
||||
AdvancedOptionsTrigger,
|
||||
} from './AdvancedOptionsSection'
|
||||
import AssigneePickerField from './AssigneePickerField'
|
||||
import AttachmentPickerField from './AttachmentPickerField'
|
||||
import DueDatePickerField from './DueDatePickerField'
|
||||
@@ -101,6 +104,11 @@ const TaskInput = ({ onChoreUpdate, isModalOpen, onClose }) => {
|
||||
const [hasDescription, setHasDescription] = useState(false)
|
||||
const [hasSubTasks, setHasSubTasks] = useState(false)
|
||||
const [deadlineOffset, setDeadlineOffset] = useState(-1)
|
||||
const [requireApproval, setRequireApproval] = useState(false)
|
||||
const [completionWindow, setCompletionWindow] = useState(-1)
|
||||
const [assignStrategy, setAssignStrategy] = useState('keep_last_assigned')
|
||||
const [isPrivate, setIsPrivate] = useState(false)
|
||||
const [showAdvanced, setShowAdvanced] = useState(false)
|
||||
const [dueDateOnly, setDueDateOnly] = useState(null)
|
||||
const [dueTime, setDueTime] = useState(null)
|
||||
const [useCustomTime, setUseCustomTime] = useState(false)
|
||||
@@ -599,6 +607,11 @@ const TaskInput = ({ onChoreUpdate, isModalOpen, onClose }) => {
|
||||
setAssignees([])
|
||||
setProjectId(getInitialProject())
|
||||
setDeadlineOffset(-1)
|
||||
setRequireApproval(false)
|
||||
setCompletionWindow(-1)
|
||||
setAssignStrategy('keep_last_assigned')
|
||||
setIsPrivate(false)
|
||||
setShowAdvanced(false)
|
||||
setDueDateOnly(null)
|
||||
setDueTime(null)
|
||||
setUseCustomTime(false)
|
||||
@@ -610,26 +623,22 @@ const TaskInput = ({ onChoreUpdate, isModalOpen, onClose }) => {
|
||||
// Handle different assignee scenarios
|
||||
let finalAssignees = assignees
|
||||
let finalAssignedTo = null
|
||||
let finalAssignStrategy = 'random'
|
||||
let finalAssignStrategy = assignStrategy
|
||||
|
||||
if (isAnyoneTask) {
|
||||
// @Anyone was explicitly used - anyone can do the task
|
||||
finalAssignees = []
|
||||
finalAssignedTo = null
|
||||
finalAssignStrategy = 'no_assignee'
|
||||
} else if (assignees.length === 0) {
|
||||
// No assignees and no @Anyone - fallback to current user
|
||||
finalAssignees = [{ userId: userProfile?.id }]
|
||||
finalAssignedTo = userProfile?.id
|
||||
finalAssignStrategy = 'keep_last_assigned'
|
||||
finalAssignStrategy = assignStrategy
|
||||
} else if (assignees.length === 1) {
|
||||
// Single assignee
|
||||
finalAssignedTo = assignees[0].userId
|
||||
finalAssignStrategy = 'keep_last_assigned'
|
||||
finalAssignStrategy = assignStrategy
|
||||
} else {
|
||||
// Multiple assignees
|
||||
finalAssignedTo = null
|
||||
finalAssignStrategy = 'random'
|
||||
finalAssignStrategy = assignStrategy
|
||||
}
|
||||
|
||||
const chore = {
|
||||
@@ -644,6 +653,10 @@ const TaskInput = ({ onChoreUpdate, isModalOpen, onClose }) => {
|
||||
priority: priority ? Number(priority) : 0,
|
||||
points: points > -1 ? points : null,
|
||||
deadlineOffset: deadlineOffset < 0 ? null : deadlineOffset,
|
||||
completionWindow:
|
||||
completionWindow < 0 || !dueDate ? null : completionWindow,
|
||||
requireApproval: requireApproval,
|
||||
isPrivate: isPrivate,
|
||||
status: 0,
|
||||
frequencyType: 'once',
|
||||
frequencyMetadata: {},
|
||||
@@ -715,7 +728,6 @@ const TaskInput = ({ onChoreUpdate, isModalOpen, onClose }) => {
|
||||
footer={
|
||||
<Box
|
||||
sx={{
|
||||
marginTop: 2,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'end',
|
||||
@@ -960,41 +972,84 @@ const TaskInput = ({ onChoreUpdate, isModalOpen, onClose }) => {
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box mt={2} sx={{ display: 'flex', flexDirection: 'row', gap: 1 }}>
|
||||
<Box
|
||||
sx={{
|
||||
mt: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
gap: 1.5,
|
||||
flexWrap: 'wrap',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
{!hasDescription && (
|
||||
<Button
|
||||
startDecorator={<Add />}
|
||||
size='sm'
|
||||
variant='outlined'
|
||||
color='neutral'
|
||||
size='md'
|
||||
startDecorator={<Add sx={{ fontSize: 18 }} />}
|
||||
onClick={() => setHasDescription(true)}
|
||||
endDecorator={
|
||||
showKeyboardShortcuts && (
|
||||
<KeyboardShortcutHint shortcut='E' />
|
||||
)
|
||||
}
|
||||
sx={{ borderRadius: '128px', minHeight: 40, px: 1.25 }}
|
||||
>
|
||||
Description
|
||||
</Button>
|
||||
)}
|
||||
{!hasSubTasks && (
|
||||
<Button
|
||||
startDecorator={<Add />}
|
||||
size='sm'
|
||||
variant='outlined'
|
||||
color='neutral'
|
||||
size='md'
|
||||
startDecorator={<Add sx={{ fontSize: 18 }} />}
|
||||
onClick={() => setHasSubTasks(true)}
|
||||
endDecorator={
|
||||
showKeyboardShortcuts && (
|
||||
<KeyboardShortcutHint shortcut='J' />
|
||||
)
|
||||
}
|
||||
sx={{ borderRadius: '128px', minHeight: 40, px: 1.25 }}
|
||||
>
|
||||
Subtasks
|
||||
</Button>
|
||||
)}
|
||||
<AdvancedOptionsTrigger
|
||||
open={showAdvanced}
|
||||
onToggle={() => setShowAdvanced(v => !v)}
|
||||
activeCount={
|
||||
[
|
||||
points > -1,
|
||||
requireApproval,
|
||||
completionWindow > -1,
|
||||
deadlineOffset > -1,
|
||||
].filter(Boolean).length
|
||||
}
|
||||
emptyDisplay={pickerEmptyDisplay}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<AdvancedOptionsSection
|
||||
open={showAdvanced}
|
||||
points={points}
|
||||
onPointsChange={setPoints}
|
||||
requireApproval={requireApproval}
|
||||
onRequireApprovalChange={setRequireApproval}
|
||||
completionWindow={completionWindow}
|
||||
onCompletionWindowChange={setCompletionWindow}
|
||||
deadlineOffset={deadlineOffset}
|
||||
onDeadlineOffsetChange={setDeadlineOffset}
|
||||
assignStrategy={assignStrategy}
|
||||
onAssignStrategyChange={setAssignStrategy}
|
||||
hasDueDate={!!dueDate}
|
||||
hasMultipleAssignees={assignees.length > 1}
|
||||
hasAssignees={assignees.length > 0}
|
||||
isPrivate={isPrivate}
|
||||
onIsPrivateChange={setIsPrivate}
|
||||
/>
|
||||
|
||||
{hasDescription && (
|
||||
<Box>
|
||||
<Typography level='body-sm'>Description:</Typography>
|
||||
|
||||
361
src/views/components/AdvancedOptionsSection.jsx
Normal file
361
src/views/components/AdvancedOptionsSection.jsx
Normal file
@@ -0,0 +1,361 @@
|
||||
import {
|
||||
Add,
|
||||
Approval,
|
||||
HourglassTop,
|
||||
Lock,
|
||||
MoreHoriz,
|
||||
People,
|
||||
Remove,
|
||||
Timer,
|
||||
} from '@mui/icons-material'
|
||||
import {
|
||||
Box,
|
||||
IconButton,
|
||||
Input,
|
||||
Option,
|
||||
Select,
|
||||
Switch,
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
|
||||
const STRATEGY_OPTIONS = [
|
||||
{ value: 'keep_last_assigned', label: 'Keep same assignee' },
|
||||
{ value: 'random', label: 'Random' },
|
||||
{ value: 'least_completed', label: 'Least completed' },
|
||||
{ value: 'round_robin', label: 'Round robin' },
|
||||
]
|
||||
|
||||
const FieldRow = ({ label, description, children }) => (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 2,
|
||||
py: 0.75,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ minWidth: 0, flex: '1 1 auto' }}>
|
||||
<Typography level='body-sm' fontWeight='md'>
|
||||
{label}
|
||||
</Typography>
|
||||
{description && (
|
||||
<Typography level='body-xs' textColor='text.tertiary' sx={{ mt: 0.25 }}>
|
||||
{description}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexShrink: 0 }}>
|
||||
{children}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
|
||||
// Trigger button — place this inside the chip/action row
|
||||
export const AdvancedOptionsTrigger = ({
|
||||
open,
|
||||
onToggle,
|
||||
activeCount = 0,
|
||||
emptyDisplay = 'icon-text',
|
||||
}) => {
|
||||
const showLabel = emptyDisplay === 'icon-text' || open || activeCount > 0
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{ position: 'relative', display: 'inline-flex', alignItems: 'center' }}
|
||||
>
|
||||
<Box
|
||||
component='button'
|
||||
onClick={onToggle}
|
||||
sx={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: showLabel ? 1 : 0,
|
||||
border: '1px solid',
|
||||
borderColor:
|
||||
open || activeCount > 0
|
||||
? 'primary.outlinedBorder'
|
||||
: 'neutral.outlinedBorder',
|
||||
bgcolor:
|
||||
open || activeCount > 0 ? 'primary.softBg' : 'transparent',
|
||||
color:
|
||||
open || activeCount > 0 ? 'primary.softColor' : 'text.secondary',
|
||||
borderRadius: '128px',
|
||||
minHeight: 40,
|
||||
px: showLabel ? 1.5 : 0.75,
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s ease-in-out',
|
||||
'&:hover': {
|
||||
bgcolor:
|
||||
open || activeCount > 0
|
||||
? 'primary.softHoverBg'
|
||||
: 'neutral.softHoverBg',
|
||||
borderColor:
|
||||
open || activeCount > 0
|
||||
? 'primary.outlinedHoverBorder'
|
||||
: 'neutral.outlinedHoverBorder',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<MoreHoriz sx={{ fontSize: 20 }} />
|
||||
<Typography
|
||||
level='body-sm'
|
||||
sx={{
|
||||
color: 'inherit',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
maxWidth: showLabel ? 120 : 0,
|
||||
opacity: showLabel ? 1 : 0,
|
||||
transform: showLabel ? 'translateX(0)' : 'translateX(-4px)',
|
||||
transition:
|
||||
'max-width 0.25s ease-in-out, opacity 0.2s ease-in-out, transform 0.25s ease-in-out',
|
||||
}}
|
||||
>
|
||||
More
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{activeCount > 0 && (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: -6,
|
||||
right: -8,
|
||||
width: 16,
|
||||
height: 16,
|
||||
borderRadius: '50%',
|
||||
bgcolor: 'primary.solidBg',
|
||||
color: 'primary.solidColor',
|
||||
fontSize: 10,
|
||||
fontWeight: 700,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
>
|
||||
{activeCount}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
// Panel — place this as a sibling below the description/subtask sections
|
||||
const AdvancedOptionsSection = ({
|
||||
open,
|
||||
points,
|
||||
onPointsChange,
|
||||
requireApproval,
|
||||
onRequireApprovalChange,
|
||||
completionWindow,
|
||||
onCompletionWindowChange,
|
||||
deadlineOffset,
|
||||
onDeadlineOffsetChange,
|
||||
assignStrategy,
|
||||
onAssignStrategyChange,
|
||||
isPrivate,
|
||||
onIsPrivateChange,
|
||||
hasDueDate,
|
||||
hasMultipleAssignees,
|
||||
hasAssignees,
|
||||
}) => {
|
||||
const displayPoints = points <= 0 ? 0 : points
|
||||
|
||||
const handleDecrement = () => {
|
||||
const next = Math.max(0, displayPoints - 1)
|
||||
onPointsChange(next === 0 ? -1 : next)
|
||||
}
|
||||
|
||||
const handleIncrement = () => {
|
||||
onPointsChange(displayPoints + 1)
|
||||
}
|
||||
|
||||
const handlePointsInput = e => {
|
||||
const v = parseInt(e.target.value)
|
||||
if (isNaN(v) || v <= 0) {
|
||||
onPointsChange(-1)
|
||||
} else {
|
||||
onPointsChange(Math.min(v, 9999))
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'grid',
|
||||
gridTemplateRows: open ? '1fr' : '0fr',
|
||||
opacity: open ? 1 : 0,
|
||||
transition:
|
||||
'grid-template-rows 0.25s ease-in-out, opacity 0.2s ease-in-out',
|
||||
}}
|
||||
>
|
||||
<Box sx={{ overflow: 'hidden' }}>
|
||||
<Box
|
||||
sx={{
|
||||
mt: 2,
|
||||
px: 2,
|
||||
py: 1,
|
||||
borderRadius: 'md',
|
||||
border: '1px solid',
|
||||
borderColor: 'neutral.outlinedBorder',
|
||||
bgcolor: 'background.level1',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
{/* Points */}
|
||||
<FieldRow
|
||||
label='Points'
|
||||
description='Award points for completing this task'
|
||||
>
|
||||
<IconButton
|
||||
size='sm'
|
||||
variant='outlined'
|
||||
color='neutral'
|
||||
onClick={handleDecrement}
|
||||
disabled={displayPoints === 0}
|
||||
>
|
||||
<Remove sx={{ fontSize: 16 }} />
|
||||
</IconButton>
|
||||
<Input
|
||||
type='number'
|
||||
size='sm'
|
||||
value={displayPoints === 0 ? '' : displayPoints}
|
||||
placeholder='0'
|
||||
onChange={handlePointsInput}
|
||||
sx={{ width: 64 }}
|
||||
slotProps={{
|
||||
input: {
|
||||
min: 0,
|
||||
max: 9999,
|
||||
style: { textAlign: 'center' },
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
size='sm'
|
||||
variant='outlined'
|
||||
color='neutral'
|
||||
onClick={handleIncrement}
|
||||
>
|
||||
<Add sx={{ fontSize: 16 }} />
|
||||
</IconButton>
|
||||
</FieldRow>
|
||||
|
||||
{/* Require approval */}
|
||||
<FieldRow
|
||||
label='Require approval'
|
||||
description='Task needs admin sign-off before it can be closed'
|
||||
>
|
||||
<Switch
|
||||
size='sm'
|
||||
checked={requireApproval}
|
||||
onChange={e => onRequireApprovalChange(e.target.checked)}
|
||||
/>
|
||||
</FieldRow>
|
||||
|
||||
{/* Privacy */}
|
||||
<FieldRow
|
||||
label='Limited visibility'
|
||||
description={
|
||||
!hasAssignees
|
||||
? 'Assign someone to enable limited visibility'
|
||||
: 'Only you and assignees can see this task'
|
||||
}
|
||||
>
|
||||
<Switch
|
||||
size='sm'
|
||||
checked={isPrivate}
|
||||
disabled={!hasAssignees}
|
||||
onChange={e => onIsPrivateChange(e.target.checked)}
|
||||
/>
|
||||
</FieldRow>
|
||||
|
||||
{/* Assignment strategy — only shown when there are multiple assignees */}
|
||||
{hasMultipleAssignees && (
|
||||
<FieldRow
|
||||
label='Assign strategy'
|
||||
description='How to pick the next assignee each recurrence'
|
||||
>
|
||||
<Select
|
||||
size='sm'
|
||||
value={assignStrategy}
|
||||
onChange={(_, v) => onAssignStrategyChange(v)}
|
||||
sx={{ minWidth: 190 }}
|
||||
>
|
||||
{STRATEGY_OPTIONS.map(opt => (
|
||||
<Option key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</FieldRow>
|
||||
)}
|
||||
|
||||
{/* Completion window and deadline — only when due date set */}
|
||||
{hasDueDate ? (
|
||||
<>
|
||||
<FieldRow
|
||||
label='Available from'
|
||||
description='Hours before the due date the task becomes available'
|
||||
>
|
||||
<Input
|
||||
type='number'
|
||||
size='sm'
|
||||
placeholder='—'
|
||||
value={completionWindow > -1 ? completionWindow : ''}
|
||||
onChange={e => {
|
||||
const v = parseInt(e.target.value)
|
||||
onCompletionWindowChange(isNaN(v) ? -1 : Math.max(0, v))
|
||||
}}
|
||||
endDecorator={
|
||||
<Typography level='body-xs' textColor='text.tertiary'>
|
||||
hrs
|
||||
</Typography>
|
||||
}
|
||||
sx={{ width: 96 }}
|
||||
slotProps={{ input: { min: 0 } }}
|
||||
/>
|
||||
</FieldRow>
|
||||
|
||||
<FieldRow
|
||||
label='Expires after'
|
||||
description='Hours after the due date when the task can no longer be completed'
|
||||
>
|
||||
<Input
|
||||
type='number'
|
||||
size='sm'
|
||||
placeholder='—'
|
||||
value={deadlineOffset > -1 ? deadlineOffset : ''}
|
||||
onChange={e => {
|
||||
const v = parseInt(e.target.value)
|
||||
onDeadlineOffsetChange(isNaN(v) ? -1 : Math.max(0, v))
|
||||
}}
|
||||
endDecorator={
|
||||
<Typography level='body-xs' textColor='text.tertiary'>
|
||||
hrs
|
||||
</Typography>
|
||||
}
|
||||
sx={{ width: 96 }}
|
||||
slotProps={{ input: { min: 0 } }}
|
||||
/>
|
||||
</FieldRow>
|
||||
</>
|
||||
) : (
|
||||
<Typography
|
||||
level='body-xs'
|
||||
textColor='text.tertiary'
|
||||
sx={{ my: 0.5, fontStyle: 'italic' }}
|
||||
>
|
||||
Set a due date to configure completion window and deadline.
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export default AdvancedOptionsSection
|
||||
Reference in New Issue
Block a user