Part of #145. Twenty-three files across the task list zone: the list and card views, sorting and grouping, multi-select and its toolbar and help sheet, archived tasks, the assignee card, the chore action menu, the nudge/NFC/photo modals, the rich text editor, the scan panel, the notification templates and the keyboard-shortcut toasts. Extends the existing `chores` namespace, so `src/i18n/config.js` is untouched. English only — no translations, no behaviour change. Every t() value is checked against this branch's base: the string must appear character-for-character in the code it replaces (247 call sites). Two values are matched loosely and worth naming: archived.closeMultiSelect in both the archived view and the toolbar. The base builds that tooltip as `${size === 0 ? 'Close' : 'Clear'} multi-select (Esc)`, so only one branch of the ternary exists contiguously in the source. Both keys hold exactly what each branch renders; the tooltip is kept whole so a translator can reorder it. Rebased on current `develop` again after #215 and #216 landed — the dictionary conflict was theirs, not the code's. No code file in this PR was touched upstream in the meantime.
176 lines
5.1 KiB
JavaScript
176 lines
5.1 KiB
JavaScript
import {
|
|
Alert,
|
|
Box,
|
|
FormControl,
|
|
FormLabel,
|
|
Switch,
|
|
Textarea,
|
|
Typography,
|
|
} from '@mui/joy'
|
|
import { useCallback, useEffect, useState } from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import KeyboardShortcutHint from '../../../components/common/KeyboardShortcutHint'
|
|
import ModalActions from '../../../components/common/ModalActions'
|
|
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
|
|
import { isOfficialDonetickInstanceSync } from '../../../utils/FeatureToggle'
|
|
|
|
function NudgeModal({ config }) {
|
|
const { t } = useTranslation('chores')
|
|
const { ResponsiveModal } = useResponsiveModal()
|
|
const [showKeyboardShortcuts, setShowKeyboardShortcuts] = useState(false)
|
|
const [message, setMessage] = useState('')
|
|
const [notifyAllAssignees, setNotifyAllAssignees] = useState(false)
|
|
const [isOfficialInstance, setIsOfficialInstance] = useState(false)
|
|
|
|
const handleAction = useCallback(
|
|
isConfirmed => {
|
|
if (isConfirmed) {
|
|
config.onConfirm({
|
|
choreId: config.choreId,
|
|
message,
|
|
notifyAllAssignees,
|
|
})
|
|
} else {
|
|
config.onClose()
|
|
}
|
|
},
|
|
[config, message, notifyAllAssignees],
|
|
)
|
|
|
|
// Reset form when modal opens
|
|
useEffect(() => {
|
|
if (config?.isOpen) {
|
|
setMessage('')
|
|
setNotifyAllAssignees(false)
|
|
|
|
// Check if this is the official donetick.com instance
|
|
try {
|
|
setIsOfficialInstance(isOfficialDonetickInstanceSync())
|
|
} catch (error) {
|
|
console.warn('Error checking instance type:', error)
|
|
setIsOfficialInstance(false)
|
|
}
|
|
}
|
|
}, [config?.isOpen])
|
|
|
|
// Keyboard shortcuts for nudge modal
|
|
useEffect(() => {
|
|
const handleKeyDown = event => {
|
|
if (!config?.isOpen) return
|
|
|
|
// Show keyboard shortcuts when Ctrl/Cmd is pressed
|
|
if (event.ctrlKey || event.metaKey) {
|
|
setShowKeyboardShortcuts(true)
|
|
}
|
|
|
|
// Ctrl/Cmd + Y for confirm
|
|
if ((event.ctrlKey || event.metaKey) && event.key === 'y') {
|
|
event.preventDefault()
|
|
handleAction(true)
|
|
return
|
|
}
|
|
|
|
// Ctrl/Cmd + X for cancel
|
|
if ((event.ctrlKey || event.metaKey) && event.key === 'x') {
|
|
event.preventDefault()
|
|
handleAction(false)
|
|
return
|
|
}
|
|
|
|
// Escape key for cancel
|
|
if (event.key === 'Escape') {
|
|
event.preventDefault()
|
|
handleAction(false)
|
|
return
|
|
}
|
|
}
|
|
|
|
const handleKeyUp = event => {
|
|
if (!event.ctrlKey && !event.metaKey) {
|
|
setShowKeyboardShortcuts(false)
|
|
}
|
|
}
|
|
|
|
if (config?.isOpen) {
|
|
document.addEventListener('keydown', handleKeyDown)
|
|
document.addEventListener('keyup', handleKeyUp)
|
|
}
|
|
|
|
return () => {
|
|
document.removeEventListener('keydown', handleKeyDown)
|
|
document.removeEventListener('keyup', handleKeyUp)
|
|
}
|
|
}, [config?.isOpen, handleAction])
|
|
|
|
return (
|
|
<ResponsiveModal
|
|
open={config?.isOpen}
|
|
onClose={config?.onClose}
|
|
size='lg'
|
|
fullWidth={true}
|
|
unmountDelay={250}
|
|
title={t('nudge.title')}
|
|
description='Send a gentle reminder to the people assigned to this task.'
|
|
footer={
|
|
<ModalActions
|
|
secondary={{
|
|
label: t('choreView.cancel'),
|
|
onClick: () => handleAction(false),
|
|
endDecorator: showKeyboardShortcuts ? (
|
|
<KeyboardShortcutHint shortcut='X' />
|
|
) : undefined,
|
|
}}
|
|
primary={{
|
|
label: t('nudge.title'),
|
|
onClick: () => handleAction(true),
|
|
disabled: !isOfficialInstance,
|
|
endDecorator: showKeyboardShortcuts ? (
|
|
<KeyboardShortcutHint shortcut='Y' />
|
|
) : undefined,
|
|
}}
|
|
/>
|
|
}
|
|
>
|
|
{!isOfficialInstance && (
|
|
<Alert color='warning' sx={{ mb: 2 }}>
|
|
<Typography level='body-sm'>
|
|
<strong>Heads up!</strong>This feature avaiable on Donetick Cloud!
|
|
Since you're using a self-hosted instance, nudges will requires
|
|
you to setup Google cloud account and Firebase Cloud Messaging
|
|
(FCM). and build the Android or the iOS app by yourself.
|
|
<br />
|
|
Will update if we come up with a solution to make this easier for to
|
|
configure. for selfhosters
|
|
</Typography>
|
|
</Alert>
|
|
)}
|
|
|
|
<FormControl mb={2}>
|
|
<FormLabel>{t('nudge.customMessage')}</FormLabel>
|
|
<Textarea
|
|
placeholder={t('nudge.messagePlaceholder')}
|
|
value={message}
|
|
onChange={e => setMessage(e.target.value)}
|
|
minRows={3}
|
|
maxRows={5}
|
|
/>
|
|
</FormControl>
|
|
|
|
<FormControl orientation='horizontal' sx={{ mb: 3 }}>
|
|
<Box sx={{ flex: 1 }}>
|
|
<FormLabel>{t('nudge.notifyAll')}</FormLabel>
|
|
<Typography level='body-sm' color='text.secondary'>
|
|
{t('nudge.notifyAllHint')}
|
|
</Typography>
|
|
</Box>
|
|
<Switch
|
|
checked={notifyAllAssignees}
|
|
onChange={e => setNotifyAllAssignees(e.target.checked)}
|
|
/>
|
|
</FormControl>
|
|
</ResponsiveModal>
|
|
)
|
|
}
|
|
|
|
export default NudgeModal
|