fix: implement UUID generation utility and replace crypto.randomUUID usage (to support genertaion when we are in http )in task and chore components
This commit is contained in:
10
src/utils/UUID.js
Normal file
10
src/utils/UUID.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
export function generateUUID() {
|
||||||
|
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
||||||
|
return crypto.randomUUID()
|
||||||
|
}
|
||||||
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
||||||
|
const r = (Math.random() * 16) | 0
|
||||||
|
const v = c === 'x' ? r : (r & 0x3) | 0x8
|
||||||
|
return v.toString(16)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -39,13 +39,14 @@ const LoginSettings = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const testConnection = async url => {
|
const testConnection = async url => {
|
||||||
|
const testURL = url.replace(/\/+$/, '') + '/api/v1/resource'
|
||||||
const controller = new AbortController()
|
const controller = new AbortController()
|
||||||
const timeoutId = setTimeout(
|
const timeoutId = setTimeout(
|
||||||
() => controller.abort(),
|
() => controller.abort(),
|
||||||
CONNECTION_TIMEOUT_MS,
|
CONNECTION_TIMEOUT_MS,
|
||||||
)
|
)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const testURL = url.replace(/\/+$/, '') + '/api/v1/resource'
|
|
||||||
const response = await fetch(testURL, {
|
const response = await fetch(testURL, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
@@ -55,18 +56,79 @@ const LoginSettings = () => {
|
|||||||
if (response.status < 500) {
|
if (response.status < 500) {
|
||||||
return { ok: true }
|
return { ok: true }
|
||||||
}
|
}
|
||||||
|
if (response.status === 503) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
message:
|
||||||
|
'Server is starting up or temporarily unavailable (503). Try again in a moment.',
|
||||||
|
}
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
message: `Server responded with error ${response.status}. Please check your Donetick server.`,
|
message: `Server responded with error ${response.status}. Please check your Donetick server.`,
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
clearTimeout(timeoutId)
|
clearTimeout(timeoutId)
|
||||||
|
|
||||||
if (err.name === 'AbortError') {
|
if (err.name === 'AbortError') {
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
message: `Connection timed out after ${CONNECTION_TIMEOUT_MS / 1000}s. Check the URL and ensure the server is running.`,
|
message: `Connection timed out after ${CONNECTION_TIMEOUT_MS / 1000}s. The host may be unreachable or behind a firewall — check the IP/hostname and network.`,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Try no-cors to distinguish CORS misconfiguration from server being down
|
||||||
|
const noCorsController = new AbortController()
|
||||||
|
const noCorsTimeout = setTimeout(() => noCorsController.abort(), 3000)
|
||||||
|
try {
|
||||||
|
const probeStart = Date.now()
|
||||||
|
const probe = await fetch(testURL, {
|
||||||
|
method: 'GET',
|
||||||
|
mode: 'no-cors',
|
||||||
|
signal: noCorsController.signal,
|
||||||
|
})
|
||||||
|
clearTimeout(noCorsTimeout)
|
||||||
|
if (probe.type === 'opaque') {
|
||||||
|
// Server responded but CORS headers blocked the real request
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
message:
|
||||||
|
'Server is reachable but blocked the request (CORS). Ensure your Donetick server allows requests from this origin, or check the server CORS config.',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Opaque is the only expected type for no-cors success; anything else is odd
|
||||||
|
void probeStart
|
||||||
|
} catch (probeErr) {
|
||||||
|
clearTimeout(noCorsTimeout)
|
||||||
|
if (probeErr.name !== 'AbortError') {
|
||||||
|
// Both normal and no-cors fetch threw immediately → port refused
|
||||||
|
const msg = probeErr.message?.toLowerCase() ?? ''
|
||||||
|
if (
|
||||||
|
msg.includes('getaddrinfo') ||
|
||||||
|
msg.includes('name not resolved') ||
|
||||||
|
msg.includes('err_name')
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
message:
|
||||||
|
'Hostname could not be resolved. Check the URL for typos or verify DNS.',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
message:
|
||||||
|
'Connection refused. The port may be wrong or nothing is listening — verify the URL and port (default Donetick port is 2021).',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// no-cors also timed out → server/host truly unreachable
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
message:
|
||||||
|
'Unable to reach the server. Check the URL, port, and network connection.',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback (should rarely hit)
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
message:
|
message:
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ import {
|
|||||||
UploadChoreAttachment,
|
UploadChoreAttachment,
|
||||||
} from '../../utils/Fetcher'
|
} from '../../utils/Fetcher'
|
||||||
import { isPlusAccount, resolvePhotoURL } from '../../utils/Helpers'
|
import { isPlusAccount, resolvePhotoURL } from '../../utils/Helpers'
|
||||||
|
import { generateUUID } from '../../utils/UUID'
|
||||||
import Priorities from '../../utils/Priorities.jsx'
|
import Priorities from '../../utils/Priorities.jsx'
|
||||||
import { getIconComponent } from '../../utils/ProjectIcons'
|
import { getIconComponent } from '../../utils/ProjectIcons'
|
||||||
import { getSafeBottomPadding } from '../../utils/SafeAreaUtils.js'
|
import { getSafeBottomPadding } from '../../utils/SafeAreaUtils.js'
|
||||||
@@ -131,7 +132,7 @@ const ChoreEdit = () => {
|
|||||||
const [createdBy, setCreatedBy] = useState(0)
|
const [createdBy, setCreatedBy] = useState(0)
|
||||||
const [errors, setErrors] = useState({})
|
const [errors, setErrors] = useState({})
|
||||||
const [attemptToSave, setAttemptToSave] = useState(false)
|
const [attemptToSave, setAttemptToSave] = useState(false)
|
||||||
const [draftId] = useState(() => crypto.randomUUID())
|
const [draftId] = useState(() => generateUUID())
|
||||||
const [attachments, setAttachments] = useState([])
|
const [attachments, setAttachments] = useState([])
|
||||||
const [isUploadingAttachment, setIsUploadingAttachment] = useState(false)
|
const [isUploadingAttachment, setIsUploadingAttachment] = useState(false)
|
||||||
const [addLabelModalOpen, setAddLabelModalOpen] = useState(false)
|
const [addLabelModalOpen, setAddLabelModalOpen] = useState(false)
|
||||||
|
|||||||
@@ -99,12 +99,42 @@ const ChoreHistory = () => {
|
|||||||
type: 'multi-select',
|
type: 'multi-select',
|
||||||
icon: <FilterList />,
|
icon: <FilterList />,
|
||||||
options: [
|
options: [
|
||||||
{ value: ChoreHistoryStatus.COMPLETED, label: 'Completed', color: 'success', icon: <Check sx={{ fontSize: 14 }} /> },
|
{
|
||||||
{ value: ChoreHistoryStatus.SKIPPED, label: 'Skipped', color: 'warning', icon: <Redo sx={{ fontSize: 14 }} /> },
|
value: ChoreHistoryStatus.COMPLETED,
|
||||||
{ value: ChoreHistoryStatus.PENDING_APPROVAL, label: 'Pending', color: 'neutral', icon: <HourglassEmpty sx={{ fontSize: 14 }} /> },
|
label: 'Completed',
|
||||||
{ value: ChoreHistoryStatus.REJECTED, label: 'Rejected', color: 'danger', icon: <ThumbDown sx={{ fontSize: 14 }} /> },
|
color: 'success',
|
||||||
{ value: 5, label: 'Missed', color: 'danger', icon: <RunningWithErrors sx={{ fontSize: 14 }} /> },
|
icon: <Check sx={{ fontSize: 14 }} />,
|
||||||
{ value: 6, label: 'Rescheduled', color: 'warning', icon: <Schedule sx={{ fontSize: 14 }} /> },
|
},
|
||||||
|
{
|
||||||
|
value: ChoreHistoryStatus.SKIPPED,
|
||||||
|
label: 'Skipped',
|
||||||
|
color: 'warning',
|
||||||
|
icon: <Redo sx={{ fontSize: 14 }} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: ChoreHistoryStatus.PENDING_APPROVAL,
|
||||||
|
label: 'Pending',
|
||||||
|
color: 'neutral',
|
||||||
|
icon: <HourglassEmpty sx={{ fontSize: 14 }} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: ChoreHistoryStatus.REJECTED,
|
||||||
|
label: 'Rejected',
|
||||||
|
color: 'danger',
|
||||||
|
icon: <ThumbDown sx={{ fontSize: 14 }} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 5,
|
||||||
|
label: 'Missed',
|
||||||
|
color: 'danger',
|
||||||
|
icon: <RunningWithErrors sx={{ fontSize: 14 }} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 6,
|
||||||
|
label: 'Rescheduled',
|
||||||
|
color: 'warning',
|
||||||
|
icon: <Schedule sx={{ fontSize: 14 }} />,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
filterFn: (item, values) => values.includes(item.status),
|
filterFn: (item, values) => values.includes(item.status),
|
||||||
},
|
},
|
||||||
@@ -143,8 +173,23 @@ const ChoreHistory = () => {
|
|||||||
[performers],
|
[performers],
|
||||||
)
|
)
|
||||||
|
|
||||||
const { filteredData: filteredHistory, activeFilters, setFilter, clearAll, activeFilterCount } =
|
const {
|
||||||
useFilter(choreHistory, filterDefs)
|
filteredData: filteredHistory,
|
||||||
|
activeFilters,
|
||||||
|
setFilter,
|
||||||
|
clearAll,
|
||||||
|
activeFilterCount,
|
||||||
|
} = useFilter(choreHistory, filterDefs)
|
||||||
|
|
||||||
|
const sortedHistory = useMemo(
|
||||||
|
() =>
|
||||||
|
[...filteredHistory].sort(
|
||||||
|
(a, b) =>
|
||||||
|
new Date(b.performedAt || b.updatedAt) -
|
||||||
|
new Date(a.performedAt || a.updatedAt),
|
||||||
|
),
|
||||||
|
[filteredHistory],
|
||||||
|
)
|
||||||
|
|
||||||
const handleDelete = historyEntry => {
|
const handleDelete = historyEntry => {
|
||||||
showConfirmation(
|
showConfirmation(
|
||||||
@@ -296,7 +341,7 @@ const ChoreHistory = () => {
|
|||||||
<Container maxWidth='md' sx={{ px: 0 }}>
|
<Container maxWidth='md' sx={{ px: 0 }}>
|
||||||
{/* Enhanced Header Section */}
|
{/* Enhanced Header Section */}
|
||||||
<Box sx={{ gap: 2, p: 2 }}>
|
<Box sx={{ gap: 2, p: 2 }}>
|
||||||
{/* <Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2, p: 2 }}> */}
|
{/* <Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2, p: 2 }}> */}
|
||||||
{/* Statistics Cards Grid - Compact Design */}
|
{/* Statistics Cards Grid - Compact Design */}
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 3 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 3 }}>
|
||||||
<History sx={{ fontSize: '1.5rem' }} />
|
<History sx={{ fontSize: '1.5rem' }} />
|
||||||
@@ -376,7 +421,6 @@ const ChoreHistory = () => {
|
|||||||
{/* History Section Header */}
|
{/* History Section Header */}
|
||||||
|
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, p: 2 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, p: 2 }}>
|
||||||
|
|
||||||
<Analytics sx={{ fontSize: '1.5rem' }} />
|
<Analytics sx={{ fontSize: '1.5rem' }} />
|
||||||
<Typography
|
<Typography
|
||||||
level='title-md'
|
level='title-md'
|
||||||
@@ -386,17 +430,17 @@ const ChoreHistory = () => {
|
|||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box sx={{ px: 2 }}>
|
<Box sx={{ px: 2 }}>
|
||||||
<FilterBar
|
<FilterBar
|
||||||
filterDefs={filterDefs}
|
filterDefs={filterDefs}
|
||||||
activeFilters={activeFilters}
|
activeFilters={activeFilters}
|
||||||
onSetFilter={setFilter}
|
onSetFilter={setFilter}
|
||||||
onClearAll={clearAll}
|
onClearAll={clearAll}
|
||||||
resultCount={filteredHistory.length}
|
resultCount={filteredHistory.length}
|
||||||
totalCount={choreHistory.length}
|
totalCount={choreHistory.length}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
{filteredHistory.length === 0 && activeFilterCount > 0 && (
|
{sortedHistory.length === 0 && activeFilterCount > 0 && (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
@@ -420,114 +464,111 @@ const ChoreHistory = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{filteredHistory.length > 0 && (
|
{sortedHistory.length > 0 && (
|
||||||
<Sheet
|
<Sheet variant='plain' sx={{ borderRadius: 'sm', overflow: 'hidden' }}>
|
||||||
variant='plain'
|
{/* Chore History List (Updated Style) */}
|
||||||
sx={{ borderRadius: 'sm', overflow: 'hidden' }}
|
|
||||||
>
|
|
||||||
{/* Chore History List (Updated Style) */}
|
|
||||||
|
|
||||||
<SwipeableList type={ListType.IOS} fullSwipe={false}>
|
<SwipeableList type={ListType.IOS} fullSwipe={false}>
|
||||||
{filteredHistory.map((historyEntry, index) => (
|
{sortedHistory.map((historyEntry, index) => (
|
||||||
<SwipeableListItem
|
<SwipeableListItem
|
||||||
key={historyEntry.id || index}
|
key={historyEntry.id || index}
|
||||||
swipeActionOpen={
|
swipeActionOpen={
|
||||||
showMoreInfoId === (historyEntry.id || index)
|
showMoreInfoId === (historyEntry.id || index)
|
||||||
? 'trailing'
|
? 'trailing'
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
trailingActions={
|
trailingActions={
|
||||||
<TrailingActions>
|
<TrailingActions>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
boxShadow: 'inset 2px 0 4px rgba(0,0,0,0.06)',
|
boxShadow: 'inset 2px 0 4px rgba(0,0,0,0.06)',
|
||||||
zIndex: 0,
|
zIndex: 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SwipeAction onClick={() => handleEdit(historyEntry)}>
|
<SwipeAction onClick={() => handleEdit(historyEntry)}>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
bgcolor: 'neutral.softBg',
|
bgcolor: 'neutral.softBg',
|
||||||
color: 'neutral.700',
|
color: 'neutral.700',
|
||||||
px: 3,
|
px: 3,
|
||||||
height: '100%',
|
height: '100%',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<EditIcon sx={{ fontSize: 20 }} />
|
<EditIcon sx={{ fontSize: 20 }} />
|
||||||
<Typography level='body-xs' sx={{ mt: 0.5 }}>
|
<Typography level='body-xs' sx={{ mt: 0.5 }}>
|
||||||
Edit
|
Edit
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</SwipeAction>
|
</SwipeAction>
|
||||||
<SwipeAction onClick={() => handleDelete(historyEntry)}>
|
<SwipeAction onClick={() => handleDelete(historyEntry)}>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
bgcolor: 'danger.softBg',
|
bgcolor: 'danger.softBg',
|
||||||
color: 'danger.700',
|
color: 'danger.700',
|
||||||
px: 3,
|
px: 3,
|
||||||
height: '100%',
|
height: '100%',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DeleteIcon sx={{ fontSize: 20 }} />
|
<DeleteIcon sx={{ fontSize: 20 }} />
|
||||||
<Typography level='body-xs' sx={{ mt: 0.5 }}>
|
<Typography level='body-xs' sx={{ mt: 0.5 }}>
|
||||||
Delete
|
Delete
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</SwipeAction>
|
</SwipeAction>
|
||||||
</Box>
|
</Box>
|
||||||
</TrailingActions>
|
</TrailingActions>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<HistoryCard
|
<HistoryCard
|
||||||
historyEntry={historyEntry}
|
historyEntry={historyEntry}
|
||||||
performers={performers}
|
performers={performers}
|
||||||
allHistory={choreHistory}
|
allHistory={choreHistory}
|
||||||
index={index}
|
index={index}
|
||||||
onViewDetails={() => {
|
onViewDetails={() => {
|
||||||
setDetailModalConfig({
|
setDetailModalConfig({
|
||||||
isOpen: true,
|
isOpen: true,
|
||||||
entry: historyEntry,
|
entry: historyEntry,
|
||||||
performers,
|
performers,
|
||||||
onClose: () => setDetailModalConfig({ isOpen: false }),
|
onClose: () => setDetailModalConfig({ isOpen: false }),
|
||||||
onEdit: record => {
|
onEdit: record => {
|
||||||
setDetailModalConfig({ isOpen: false })
|
setDetailModalConfig({ isOpen: false })
|
||||||
setEditHistory(record)
|
setEditHistory(record)
|
||||||
setIsEditModalOpen(true)
|
setIsEditModalOpen(true)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
pendingCommands={pendingByHistoryId[historyEntry.id] || []}
|
pendingCommands={pendingByHistoryId[historyEntry.id] || []}
|
||||||
onViewNote={notes => {
|
onViewNote={notes => {
|
||||||
setNoteViewerConfig({
|
setNoteViewerConfig({
|
||||||
isOpen: true,
|
isOpen: true,
|
||||||
title: `Updated at ${fmt.dateTime(historyEntry.updatedAt)}`,
|
title: `Updated at ${fmt.dateTime(historyEntry.updatedAt)}`,
|
||||||
content: notes,
|
content: notes,
|
||||||
onClose: () => setNoteViewerConfig({ isOpen: false }),
|
onClose: () => setNoteViewerConfig({ isOpen: false }),
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
onToggleActions={() => {
|
onToggleActions={() => {
|
||||||
const id = historyEntry.id || index
|
const id = historyEntry.id || index
|
||||||
if (showMoreInfoId === id) {
|
if (showMoreInfoId === id) {
|
||||||
setShowMoreInfoId(null)
|
setShowMoreInfoId(null)
|
||||||
} else {
|
} else {
|
||||||
setShowMoreInfoId(id)
|
setShowMoreInfoId(id)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</SwipeableListItem>
|
</SwipeableListItem>
|
||||||
))}
|
))}
|
||||||
</SwipeableList>
|
</SwipeableList>
|
||||||
</Sheet>
|
</Sheet>
|
||||||
)}
|
)}
|
||||||
<EditHistoryModal
|
<EditHistoryModal
|
||||||
config={{
|
config={{
|
||||||
@@ -604,7 +645,7 @@ const ChoreHistory = () => {
|
|||||||
<ConfirmationModal config={confirmModalConfig} />
|
<ConfirmationModal config={confirmModalConfig} />
|
||||||
<NoteViewerModal config={noteViewerConfig} />
|
<NoteViewerModal config={noteViewerConfig} />
|
||||||
<HistoryDetailModal config={detailModalConfig} />
|
<HistoryDetailModal config={detailModalConfig} />
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { useResponsiveModal } from '../../hooks/useResponsiveModal'
|
|||||||
import { useCreateChore } from '../../queries/ChoreQueries'
|
import { useCreateChore } from '../../queries/ChoreQueries'
|
||||||
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries'
|
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries'
|
||||||
import { isPlusAccount } from '../../utils/Helpers'
|
import { isPlusAccount } from '../../utils/Helpers'
|
||||||
|
import { generateUUID } from '../../utils/UUID'
|
||||||
import { useLabels } from '../Labels/LabelQueries'
|
import { useLabels } from '../Labels/LabelQueries'
|
||||||
import { useProjects } from '../Projects/ProjectQueries'
|
import { useProjects } from '../Projects/ProjectQueries'
|
||||||
import {
|
import {
|
||||||
@@ -106,7 +107,8 @@ const TaskInput = ({ onChoreUpdate, isModalOpen, onClose }) => {
|
|||||||
const [showKeyboardShortcuts, setShowKeyboardShortcuts] = useState(false)
|
const [showKeyboardShortcuts, setShowKeyboardShortcuts] = useState(false)
|
||||||
const [projectId, setProjectId] = useState(getInitialProject())
|
const [projectId, setProjectId] = useState(getInitialProject())
|
||||||
const [attachments, setAttachments] = useState([])
|
const [attachments, setAttachments] = useState([])
|
||||||
const [draftId, setDraftId] = useState(() => crypto.randomUUID())
|
|
||||||
|
const [draftId, setDraftId] = useState(() => generateUUID())
|
||||||
const [showScan, setShowScan] = useState(false)
|
const [showScan, setShowScan] = useState(false)
|
||||||
const [scanAutoCapture, setScanAutoCapture] = useState(false)
|
const [scanAutoCapture, setScanAutoCapture] = useState(false)
|
||||||
const [pendingPhotoUrl, setPendingPhotoUrl] = useState(null)
|
const [pendingPhotoUrl, setPendingPhotoUrl] = useState(null)
|
||||||
@@ -595,7 +597,7 @@ const TaskInput = ({ onChoreUpdate, isModalOpen, onClose }) => {
|
|||||||
setDueTime(null)
|
setDueTime(null)
|
||||||
setUseCustomTime(false)
|
setUseCustomTime(false)
|
||||||
setAttachments([])
|
setAttachments([])
|
||||||
setDraftId(crypto.randomUUID())
|
setDraftId(generateUUID())
|
||||||
}
|
}
|
||||||
|
|
||||||
const createChore = () => {
|
const createChore = () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user