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:
Mo Tarbin
2026-07-09 14:44:05 -04:00
parent 2fd6d5cd2e
commit 6612e77579
5 changed files with 251 additions and 135 deletions

10
src/utils/UUID.js Normal file
View 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)
})
}

View File

@@ -39,13 +39,14 @@ const LoginSettings = () => {
}
const testConnection = async url => {
const testURL = url.replace(/\/+$/, '') + '/api/v1/resource'
const controller = new AbortController()
const timeoutId = setTimeout(
() => controller.abort(),
CONNECTION_TIMEOUT_MS,
)
try {
const testURL = url.replace(/\/+$/, '') + '/api/v1/resource'
const response = await fetch(testURL, {
method: 'GET',
signal: controller.signal,
@@ -55,18 +56,79 @@ const LoginSettings = () => {
if (response.status < 500) {
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 {
ok: false,
message: `Server responded with error ${response.status}. Please check your Donetick server.`,
}
} catch (err) {
clearTimeout(timeoutId)
if (err.name === 'AbortError') {
return {
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 {
ok: false,
message:

View File

@@ -58,6 +58,7 @@ import {
UploadChoreAttachment,
} from '../../utils/Fetcher'
import { isPlusAccount, resolvePhotoURL } from '../../utils/Helpers'
import { generateUUID } from '../../utils/UUID'
import Priorities from '../../utils/Priorities.jsx'
import { getIconComponent } from '../../utils/ProjectIcons'
import { getSafeBottomPadding } from '../../utils/SafeAreaUtils.js'
@@ -131,7 +132,7 @@ const ChoreEdit = () => {
const [createdBy, setCreatedBy] = useState(0)
const [errors, setErrors] = useState({})
const [attemptToSave, setAttemptToSave] = useState(false)
const [draftId] = useState(() => crypto.randomUUID())
const [draftId] = useState(() => generateUUID())
const [attachments, setAttachments] = useState([])
const [isUploadingAttachment, setIsUploadingAttachment] = useState(false)
const [addLabelModalOpen, setAddLabelModalOpen] = useState(false)

View File

@@ -99,12 +99,42 @@ const ChoreHistory = () => {
type: 'multi-select',
icon: <FilterList />,
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.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 }} /> },
{
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.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),
},
@@ -143,8 +173,23 @@ const ChoreHistory = () => {
[performers],
)
const { filteredData: filteredHistory, activeFilters, setFilter, clearAll, activeFilterCount } =
useFilter(choreHistory, filterDefs)
const {
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 => {
showConfirmation(
@@ -376,7 +421,6 @@ const ChoreHistory = () => {
{/* History Section Header */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, p: 2 }}>
<Analytics sx={{ fontSize: '1.5rem' }} />
<Typography
level='title-md'
@@ -386,7 +430,7 @@ const ChoreHistory = () => {
</Typography>
</Box>
<Box sx={{ px: 2 }}>
<Box sx={{ px: 2 }}>
<FilterBar
filterDefs={filterDefs}
activeFilters={activeFilters}
@@ -396,7 +440,7 @@ const ChoreHistory = () => {
totalCount={choreHistory.length}
/>
</Box>
{filteredHistory.length === 0 && activeFilterCount > 0 && (
{sortedHistory.length === 0 && activeFilterCount > 0 && (
<Box
sx={{
textAlign: 'center',
@@ -420,15 +464,12 @@ const ChoreHistory = () => {
</Box>
)}
{filteredHistory.length > 0 && (
<Sheet
variant='plain'
sx={{ borderRadius: 'sm', overflow: 'hidden' }}
>
{sortedHistory.length > 0 && (
<Sheet variant='plain' sx={{ borderRadius: 'sm', overflow: 'hidden' }}>
{/* Chore History List (Updated Style) */}
<SwipeableList type={ListType.IOS} fullSwipe={false}>
{filteredHistory.map((historyEntry, index) => (
{sortedHistory.map((historyEntry, index) => (
<SwipeableListItem
key={historyEntry.id || index}
swipeActionOpen={
@@ -604,7 +645,7 @@ const ChoreHistory = () => {
<ConfirmationModal config={confirmModalConfig} />
<NoteViewerModal config={noteViewerConfig} />
<HistoryDetailModal config={detailModalConfig} />
</Container>
</Container>
)
}

View File

@@ -8,6 +8,7 @@ import { useResponsiveModal } from '../../hooks/useResponsiveModal'
import { useCreateChore } from '../../queries/ChoreQueries'
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries'
import { isPlusAccount } from '../../utils/Helpers'
import { generateUUID } from '../../utils/UUID'
import { useLabels } from '../Labels/LabelQueries'
import { useProjects } from '../Projects/ProjectQueries'
import {
@@ -106,7 +107,8 @@ const TaskInput = ({ onChoreUpdate, isModalOpen, onClose }) => {
const [showKeyboardShortcuts, setShowKeyboardShortcuts] = useState(false)
const [projectId, setProjectId] = useState(getInitialProject())
const [attachments, setAttachments] = useState([])
const [draftId, setDraftId] = useState(() => crypto.randomUUID())
const [draftId, setDraftId] = useState(() => generateUUID())
const [showScan, setShowScan] = useState(false)
const [scanAutoCapture, setScanAutoCapture] = useState(false)
const [pendingPhotoUrl, setPendingPhotoUrl] = useState(null)
@@ -595,7 +597,7 @@ const TaskInput = ({ onChoreUpdate, isModalOpen, onClose }) => {
setDueTime(null)
setUseCustomTime(false)
setAttachments([])
setDraftId(crypto.randomUUID())
setDraftId(generateUUID())
}
const createChore = () => {