Rework notification input fields, fix verification happens too early. Adjust ranges.

This commit is contained in:
Mihály Hóbor
2026-03-28 02:41:13 +01:00
parent 3c29d4a19f
commit 42549bf1a6

View File

@@ -12,7 +12,7 @@ import Input from '@mui/joy/Input'
import Option from '@mui/joy/Option'
import Select from '@mui/joy/Select'
import Typography from '@mui/joy/Typography'
import { useCallback, useEffect, useState } from 'react'
import { useCallback, useEffect, useState, useRef } from 'react'
import { NOTIFICATION_TYPE, TASK_COLOR } from '../utils/Colors'
import { TIME_UNITS } from '../utils/DurationUtils'
@@ -73,6 +73,9 @@ const NotificationTemplate = ({
[],
)
const notificationsRef = useRef(notifications)
const [draftValues, setDraftValues] = useState({})
const [error, setError] = useState(null)
const [showSaveDefault, setShowSaveDefault] = useState(false)
// Create a map of notification indices for timeline display
@@ -114,7 +117,6 @@ const NotificationTemplate = ({
// Sort notifications and update the index mapping
useEffect(() => {
updateNotificationIndices()
setError(null)
}, [updateNotificationIndices])
// Notify parent component of changes including the template name
@@ -125,8 +127,8 @@ const NotificationTemplate = ({
}, [notifications, onChange])
// Validates if a notification configuration already exists
const isDuplicate = (notification, currentIdx = -1) => {
return notifications.some((n, idx) => {
const isDuplicate = (notification, currentIdx = -1, list = notifications) => {
return list.some((n, idx) => {
if (idx === currentIdx) return false
return (
@@ -136,6 +138,26 @@ const NotificationTemplate = ({
})
}
const getSmartSuggestion = type => {
let suggestions = []
if (type === 'reminder' || type === 'before') {
suggestions = [
{ value: -1, unit: 'd' },
{ value: -3, unit: 'h' },
{ value: -30, unit: 'm' },
]
} else if (type === 'followup' || type === 'after') {
suggestions = [
{ value: 1, unit: 'd' },
{ value: 3, unit: 'd' },
{ value: 7, unit: 'd' },
]
}
return suggestions.find(
suggestion => !isDuplicate(suggestion, -1, notificationsRef.current),
)
}
const handleChange = (idx, field, value) => {
const currentNotification = notifications[idx]
const uiRep = getUIRepresentation(currentNotification)
@@ -149,6 +171,15 @@ const NotificationTemplate = ({
// Reset display value when switching to "On Due"
if (value === 'ondue') {
updatedUIRep.displayValue = 0
} else if (Number(currentNotification.value) === 0) {
const suggestion = getSmartSuggestion(value)
if (suggestion) {
updatedUIRep.displayValue = Math.abs(suggestion.value)
updatedUIRep.unit = suggestion.unit
} else {
updatedUIRep.displayValue = 1
updatedUIRep.unit = 'h'
}
}
} else if (field === 'displayValue') {
updatedUIRep.displayValue = Math.max(0, Number(value))
@@ -168,71 +199,41 @@ const NotificationTemplate = ({
unit: updatedUIRep.unit,
}
// Check if another notification is already "On Due" (value = 0)
if (newInternalValue === 0) {
const existingOnDue = notifications.findIndex(
(n, i) => i !== idx && Number(n.value) === 0,
const updated = notifications.map((n, i) =>
i === idx ? updatedNotification : n,
)
if (existingOnDue !== -1) {
setError(
'Only one notification can be set to "On Due". Please choose a different timing.',
)
return
}
setNotifications(updated)
notificationsRef.current = updated
setError(null)
}
if (isDuplicate(updatedNotification, idx)) {
const handleBlur = idx => {
const currentList = notificationsRef.current
const currentNotification = currentList[idx]
if (!currentNotification) return
if (isDuplicate(currentNotification, idx, currentList)) {
setError(
'This notification setting already exists. Please use a different timing.',
)
return
}
const updated = notifications.map((n, i) =>
i === idx ? updatedNotification : n,
)
setNotifications(updated)
setError(null)
}
const addSmartNotification = type => {
if (notifications.length >= maxNotifications) return
setShowSaveDefault(true)
let newNotification
let suggestions = []
switch (type) {
case 'reminder':
// Suggest common reminder times that don't exist
suggestions = [
{ value: -1, unit: 'd' }, // 1 day before
{ value: -3, unit: 'h' }, // 3 hours before
{ value: -30, unit: 'm' }, // 3 days before
]
break
case 'due':
if (notifications.some(n => Number(n.value) === 0)) {
if (type === 'due') {
if (notificationsRef.current.some(n => Number(n.value) === 0)) {
setError('Only one "Due Alert" notification is allowed.')
return
}
newNotification = { value: 0, unit: 'm' }
break
case 'followup':
suggestions = [
{ value: 1, unit: 'd' }, // 1 day after
{ value: 3, unit: 'd' }, // 3 days after
{ value: 7, unit: 'd' }, // 1 week after
]
break
}
// For reminder/followup, find first non-duplicate suggestion
if (suggestions.length > 0) {
newNotification = suggestions.find(suggestion => !isDuplicate(suggestion))
} else {
newNotification = getSmartSuggestion(type)
if (!newNotification) {
setError(`All common ${type} times are already configured.`)
return
@@ -243,15 +244,25 @@ const NotificationTemplate = ({
const updatedNotifications = [...notifications, newNotification]
setNotifications(updatedNotifications)
notificationsRef.current = updatedNotifications
setError(null)
}
const removeNotification = idx => {
const updated = notifications.filter((_, i) => i !== idx)
setNotifications(updated)
notificationsRef.current = updated
setDraftValues(prev => {
const next = { ...prev }
delete next[idx]
return next
})
onChange && onChange(updated)
setShowSaveDefault(true)
}
const renderTimeline = () => {
// Convert notifications to minutes for proper chronological sorting
const convertToMinutes = (value, unit) => {
@@ -459,6 +470,11 @@ const NotificationTemplate = ({
const badgeNumber = notificationIndexMap[idx]
const uiRep = getUIRepresentation(n)
// Check if an "On Due" notification exists anywhere else in the list
const hasOnDueElsewhere = notificationsRef.current.some(
(notif, i) => i !== idx && Number(notif.value) === 0,
)
const getNotificationColors = value => {
if (Number(value) < 0) {
return {
@@ -487,7 +503,7 @@ const NotificationTemplate = ({
const colors = getNotificationColors(n.value)
return (
<>
<Box key={idx} sx={{ position: 'relative' }}>
<Badge
badgeContent={badgeNumber}
size={'sm'}
@@ -495,7 +511,6 @@ const NotificationTemplate = ({
'--Badge-minHeight': '16px',
'--Badge-fontSize': '0.7rem',
'--Badge-paddingX': '5px',
top: 10,
'& .MuiBadge-badge': {
background: colors.bgColor,
@@ -504,7 +519,6 @@ const NotificationTemplate = ({
}}
/>
<Box
key={idx}
sx={{
mb: 1.5,
p: 2,
@@ -548,11 +562,16 @@ const NotificationTemplate = ({
<Select
value={uiRep.timing}
onChange={(_, value) => handleChange(idx, 'timing', value)}
onBlur={() => handleBlur(idx)}
sx={{ minWidth: 80 }}
size={'sm'}
>
{timingOptions.map(opt => (
<Option key={opt.value} value={opt.value}>
<Option
key={opt.value}
value={opt.value}
disabled={opt.value === 'ondue' && hasOnDueElsewhere}
>
{opt.label}
</Option>
))}
@@ -560,11 +579,62 @@ const NotificationTemplate = ({
<Input
type={'number'}
min={0}
value={uiRep.displayValue}
disabled={uiRep.timing === 'ondue'}
onChange={e =>
handleChange(idx, 'displayValue', e.target.value)
value={
draftValues[idx] !== undefined
? draftValues[idx]
: uiRep.displayValue
}
disabled={uiRep.timing === 'ondue'}
onChange={e => {
const val = e.target.value
if (val.includes('-')) return
// Prevent setting to '0' if an 'On Due' already exists elsewhere
if (val === '0' || val === '') {
if (hasOnDueElsewhere) return
}
setDraftValues(prev => ({ ...prev, [idx]: val }))
}}
onKeyDown={e => {
if (['-', 'e', '+', '.'].includes(e.key)) {
e.preventDefault()
return
}
// Physically block typing '0' if 'On Due' exists
if (
e.key === '0' ||
e.key === 'Backspace' ||
e.key === 'Delete'
) {
const currentVal =
draftValues[idx] !== undefined
? draftValues[idx]
: uiRep.displayValue.toString()
// If typing 0 into an empty input, or deleting the last character
const willBeZero =
(e.key === '0' && currentVal === '') ||
((e.key === 'Backspace' || e.key === 'Delete') &&
currentVal.length <= 1)
if (willBeZero && hasOnDueElsewhere) {
e.preventDefault()
}
}
}}
onBlur={e => {
let val = e.target.value
if (val === '' || Number(val) < 0) val = 0
handleChange(idx, 'displayValue', val)
setDraftValues(prev => {
const next = { ...prev }
delete next[idx]
return next
})
handleBlur(idx)
}}
sx={{
width: 60,
opacity: uiRep.timing === 'ondue' ? 0.6 : 1,
@@ -576,6 +646,7 @@ const NotificationTemplate = ({
value={n.unit}
disabled={uiRep.timing === 'ondue'}
onChange={(_, value) => handleChange(idx, 'unit', value)}
onBlur={() => handleBlur(idx)}
sx={{
minWidth: 70,
opacity: uiRep.timing === 'ondue' ? 0.6 : 1,
@@ -605,7 +676,7 @@ const NotificationTemplate = ({
</IconButton>
</Box>
</Box>
</>
</Box>
)
})}
<Box