Show upload progress — spinner placeholder in photo grid, status text on submit
TaskModal: grey placeholder thumb with spinning loader appears in the photo grid immediately on file pick, replaced by the real thumb once upload+resize completes. NewTaskModal: submit button shows spinning icon with 'Creating task…' then 'Uploading photo N of M…' instead of just greying out. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
b3f4adeb3e
commit
9f402bd2d9
3 changed files with 27 additions and 6 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
import { X, Camera } from 'lucide-react'
|
import { X, Camera, Loader2 } from 'lucide-react'
|
||||||
import type { Category, Location, Task, Priority, AppConfig, Asset } from '../types'
|
import type { Category, Location, Task, Priority, AppConfig, Asset } from '../types'
|
||||||
import { PRIORITIES, PRIORITY_LABELS } from '../types'
|
import { PRIORITIES, PRIORITY_LABELS } from '../types'
|
||||||
import { createTask, fetchTasks, uploadTaskPhoto, fetchAssets } from '../api'
|
import { createTask, fetchTasks, uploadTaskPhoto, fetchAssets } from '../api'
|
||||||
|
|
@ -25,6 +25,7 @@ export default function NewTaskModal({ categories, locations, config, onClose, o
|
||||||
const [assets, setAssets] = useState<Asset[]>([])
|
const [assets, setAssets] = useState<Asset[]>([])
|
||||||
const [existing, setExisting] = useState<Task[]>([])
|
const [existing, setExisting] = useState<Task[]>([])
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
|
const [saveStatus, setSaveStatus] = useState('')
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
const [assignment, setAssignment] = useState<Assignment>({
|
const [assignment, setAssignment] = useState<Assignment>({
|
||||||
|
|
@ -58,6 +59,7 @@ export default function NewTaskModal({ categories, locations, config, onClose, o
|
||||||
async function submit() {
|
async function submit() {
|
||||||
if (!title.trim() || !locationId) { setError('Title and location are required'); return }
|
if (!title.trim() || !locationId) { setError('Title and location are required'); return }
|
||||||
setSaving(true)
|
setSaving(true)
|
||||||
|
setSaveStatus('Creating task…')
|
||||||
setError(null)
|
setError(null)
|
||||||
try {
|
try {
|
||||||
const task = await createTask({
|
const task = await createTask({
|
||||||
|
|
@ -74,8 +76,9 @@ export default function NewTaskModal({ categories, locations, config, onClose, o
|
||||||
contractor_id: assignment.contractor_id,
|
contractor_id: assignment.contractor_id,
|
||||||
})
|
})
|
||||||
|
|
||||||
for (const file of files) {
|
for (let i = 0; i < files.length; i++) {
|
||||||
await uploadTaskPhoto(task.id, file, 'report').catch(() => {})
|
setSaveStatus(`Uploading photo ${i + 1} of ${files.length}…`)
|
||||||
|
await uploadTaskPhoto(task.id, files[i], 'report').catch(() => {})
|
||||||
}
|
}
|
||||||
|
|
||||||
onCreated()
|
onCreated()
|
||||||
|
|
@ -84,6 +87,7 @@ export default function NewTaskModal({ categories, locations, config, onClose, o
|
||||||
setError(err instanceof Error ? err.message : 'Failed to create task')
|
setError(err instanceof Error ? err.message : 'Failed to create task')
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false)
|
setSaving(false)
|
||||||
|
setSaveStatus('')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -193,7 +197,7 @@ export default function NewTaskModal({ categories, locations, config, onClose, o
|
||||||
<div className="modal-actions">
|
<div className="modal-actions">
|
||||||
<button className="btn" onClick={onClose}>Cancel</button>
|
<button className="btn" onClick={onClose}>Cancel</button>
|
||||||
<button className="btn btn-primary" onClick={submit} disabled={saving}>
|
<button className="btn btn-primary" onClick={submit} disabled={saving}>
|
||||||
{saving ? 'Saving…' : 'Submit'}
|
{saving ? <><Loader2 size={14} strokeWidth={1.75} className="spin" /> {saveStatus}</> : 'Submit'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import {
|
import {
|
||||||
X, Camera, MessageSquare, ArrowRight, RotateCcw, PlusCircle,
|
X, Camera, MessageSquare, ArrowRight, RotateCcw, PlusCircle,
|
||||||
CheckCircle2, Image as ImageIcon, PoundSterling, UserRound, Pencil, Trash2,
|
CheckCircle2, Image as ImageIcon, PoundSterling, UserRound, Pencil, Trash2, Loader2,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import type { TaskDetail, TaskStatus, TaskEvent, AuthUser, Priority } from '../types'
|
import type { TaskDetail, TaskStatus, TaskEvent, AuthUser, Priority } from '../types'
|
||||||
import { STATUS_LABELS, TRANSITIONS, PRIORITIES, PRIORITY_LABELS, can } from '../types'
|
import { STATUS_LABELS, TRANSITIONS, PRIORITIES, PRIORITY_LABELS, can } from '../types'
|
||||||
|
|
@ -52,6 +52,7 @@ export default function TaskModal({ taskId, onClose, onChanged }: {
|
||||||
const [assignment, setAssignment] = useState<Assignment | null>(null)
|
const [assignment, setAssignment] = useState<Assignment | null>(null)
|
||||||
|
|
||||||
const [lightboxSrc, setLightboxSrc] = useState<string | null>(null)
|
const [lightboxSrc, setLightboxSrc] = useState<string | null>(null)
|
||||||
|
const [photoUploading, setPhotoUploading] = useState(false)
|
||||||
|
|
||||||
const [showEdit, setShowEdit] = useState(false)
|
const [showEdit, setShowEdit] = useState(false)
|
||||||
const [editTitle, setEditTitle] = useState('')
|
const [editTitle, setEditTitle] = useState('')
|
||||||
|
|
@ -276,6 +277,13 @@ export default function TaskModal({ taskId, onClose, onChanged }: {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
{photoUploading && (
|
||||||
|
<div className="photo-thumb-wrap">
|
||||||
|
<div className="photo-thumb photo-thumb-uploading">
|
||||||
|
<Loader2 size={22} strokeWidth={1.5} className="spin" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{canReport && (
|
{canReport && (
|
||||||
<label className="btn btn-sm" style={{ cursor: 'pointer', alignSelf: 'center' }}>
|
<label className="btn btn-sm" style={{ cursor: 'pointer', alignSelf: 'center' }}>
|
||||||
<Camera size={14} strokeWidth={1.75} /> Add
|
<Camera size={14} strokeWidth={1.75} /> Add
|
||||||
|
|
@ -283,7 +291,10 @@ export default function TaskModal({ taskId, onClose, onChanged }: {
|
||||||
type="file" accept="image/*" capture="environment" style={{ display: 'none' }}
|
type="file" accept="image/*" capture="environment" style={{ display: 'none' }}
|
||||||
onChange={e => {
|
onChange={e => {
|
||||||
const f = e.target.files?.[0]
|
const f = e.target.files?.[0]
|
||||||
if (f) run(() => uploadTaskPhoto(task.id, f, task.status === 'submitted' ? 'report' : 'progress'))
|
if (!f) return
|
||||||
|
setPhotoUploading(true)
|
||||||
|
run(() => uploadTaskPhoto(task.id, f, task.status === 'submitted' ? 'report' : 'progress'))
|
||||||
|
.finally(() => setPhotoUploading(false))
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
|
||||||
|
|
@ -374,6 +374,12 @@ html, body, #root { height: 100%; margin: 0; font-size: 14px; }
|
||||||
border: 1px solid var(--card-border);
|
border: 1px solid var(--card-border);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
.photo-thumb-uploading {
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
background: var(--card-border); color: var(--text-mid);
|
||||||
|
}
|
||||||
|
@keyframes spin { to { transform: rotate(360deg); } }
|
||||||
|
.spin { animation: spin .75s linear infinite; }
|
||||||
.photo-thumb-wrap { position: relative; }
|
.photo-thumb-wrap { position: relative; }
|
||||||
.photo-del {
|
.photo-del {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue