Enhance project management UI and handle bulk changes (#186)
* enable hold to select, remove once x1 chip, unify reschedule modal * project title still show fromwhen login from different account it's was cache in local storage so makdikngsure we do clean up * Fix: show the button for creating task on the modal instead of the panel * Bulk moving project, Move the button in scanner to the modal * - Handle bulk change for projects - allow no assignee being selected - choreactionmenu become modal on mobile
This commit is contained in:
@@ -12,7 +12,7 @@ import {
|
||||
LinearProgress,
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import { useEffect } from 'react'
|
||||
import { useCallback, useEffect, useMemo } from 'react'
|
||||
import { useScanToTask } from './useScanToTask'
|
||||
|
||||
/**
|
||||
@@ -20,8 +20,20 @@ import { useScanToTask } from './useScanToTask'
|
||||
*
|
||||
* Flow: capture → (auto) processing → done [calls onTaskExtracted + onClose]
|
||||
* → error [retake or cancel]
|
||||
*
|
||||
* The primary action (Capture / Scan Document / Retake) lives in the modal
|
||||
* footer alongside Cancel — the panel reports it up through onStateChange
|
||||
* rather than rendering its own button row. Upload stays inline because it
|
||||
* belongs to the capture surface and drives a hidden input in this subtree.
|
||||
*/
|
||||
const ScanPanel = ({ open, onTaskExtracted, onClose, initialImageUrl, autoCapture }) => {
|
||||
const ScanPanel = ({
|
||||
open,
|
||||
onTaskExtracted,
|
||||
onClose,
|
||||
onStateChange,
|
||||
initialImageUrl,
|
||||
autoCapture,
|
||||
}) => {
|
||||
const {
|
||||
isNativeScanner,
|
||||
phase,
|
||||
@@ -76,6 +88,51 @@ const ScanPanel = ({ open, onTaskExtracted, onClose, initialImageUrl, autoCaptur
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [phase, taskResult])
|
||||
|
||||
const openFilePicker = useCallback(
|
||||
() => fileInputRef.current?.click(),
|
||||
[fileInputRef],
|
||||
)
|
||||
|
||||
// The one action the footer renders for the current phase; null while
|
||||
// processing (nothing to do but wait) and when done (the panel closes)
|
||||
const primaryAction = useMemo(() => {
|
||||
if (phase === 'capture') {
|
||||
if (isNativeScanner) {
|
||||
return {
|
||||
label: 'Scan Document',
|
||||
icon: <DocumentScanner />,
|
||||
onClick: handleNativeScan,
|
||||
}
|
||||
}
|
||||
if (cameraAvailable) {
|
||||
return { label: 'Capture', icon: <CameraAlt />, onClick: capture }
|
||||
}
|
||||
// No camera on this device — Upload is the only way forward, so it
|
||||
// graduates from the inline secondary to the footer's primary
|
||||
return {
|
||||
label: 'Upload Photo',
|
||||
icon: <PhotoCamera />,
|
||||
onClick: openFilePicker,
|
||||
}
|
||||
}
|
||||
if (phase === 'error') {
|
||||
return { label: 'Retake', icon: <Replay />, onClick: retake }
|
||||
}
|
||||
return null
|
||||
}, [
|
||||
phase,
|
||||
isNativeScanner,
|
||||
cameraAvailable,
|
||||
capture,
|
||||
handleNativeScan,
|
||||
retake,
|
||||
openFilePicker,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
onStateChange?.({ phase, primaryAction })
|
||||
}, [phase, primaryAction, onStateChange])
|
||||
|
||||
if (!open) return null
|
||||
|
||||
const isProcessing = phase === 'processing'
|
||||
@@ -111,7 +168,10 @@ const ScanPanel = ({ open, onTaskExtracted, onClose, initialImageUrl, autoCaptur
|
||||
<DocumentScanner
|
||||
sx={{ fontSize: 56, color: 'white', opacity: 0.5, mb: 1 }}
|
||||
/>
|
||||
<Typography level='body-sm' sx={{ color: 'white', opacity: 0.6 }}>
|
||||
<Typography
|
||||
level='body-sm'
|
||||
sx={{ color: 'white', opacity: 0.6 }}
|
||||
>
|
||||
Tap "Scan Document" to open the scanner
|
||||
</Typography>
|
||||
</Box>
|
||||
@@ -137,65 +197,38 @@ const ScanPanel = ({ open, onTaskExtracted, onClose, initialImageUrl, autoCaptur
|
||||
<CameraAlt
|
||||
sx={{ fontSize: 48, color: 'white', opacity: 0.4, mb: 1 }}
|
||||
/>
|
||||
<Typography level='body-sm' sx={{ color: 'white', opacity: 0.6 }}>
|
||||
<Typography
|
||||
level='body-sm'
|
||||
sx={{ color: 'white', opacity: 0.6 }}
|
||||
>
|
||||
Camera not available — use Upload instead
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
px: 1.5,
|
||||
py: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
size='sm'
|
||||
variant='plain'
|
||||
color='neutral'
|
||||
startDecorator={<PhotoCamera fontSize='small' />}
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
{/* Hidden when Upload is already the footer's primary action */}
|
||||
{(isNativeScanner || cameraAvailable) && (
|
||||
<Box
|
||||
sx={{
|
||||
px: 1.5,
|
||||
py: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
Upload
|
||||
</Button>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type='file'
|
||||
accept='image/*'
|
||||
style={{ display: 'none' }}
|
||||
onChange={handleFileSelect}
|
||||
/>
|
||||
|
||||
<Box sx={{ ml: 'auto', display: 'flex', gap: 1 }}>
|
||||
{isNativeScanner ? (
|
||||
<Button
|
||||
size='sm'
|
||||
variant='solid'
|
||||
color='primary'
|
||||
startDecorator={<DocumentScanner fontSize='small' />}
|
||||
onClick={handleNativeScan}
|
||||
>
|
||||
Scan Document
|
||||
</Button>
|
||||
) : (
|
||||
cameraAvailable && (
|
||||
<Button
|
||||
size='sm'
|
||||
variant='solid'
|
||||
color='primary'
|
||||
startDecorator={<CameraAlt fontSize='small' />}
|
||||
onClick={capture}
|
||||
>
|
||||
Capture
|
||||
</Button>
|
||||
)
|
||||
)}
|
||||
<Button
|
||||
size='sm'
|
||||
variant='plain'
|
||||
color='neutral'
|
||||
startDecorator={<PhotoCamera fontSize='small' />}
|
||||
onClick={openFilePicker}
|
||||
>
|
||||
Upload
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -280,24 +313,22 @@ const ScanPanel = ({ open, onTaskExtracted, onClose, initialImageUrl, autoCaptur
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1, mb: 2 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1 }}>
|
||||
<WarningAmber color='warning' sx={{ mt: 0.25, flexShrink: 0 }} />
|
||||
<Typography level='body-sm'>{errorMsg}</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<Button
|
||||
size='sm'
|
||||
variant='outlined'
|
||||
color='neutral'
|
||||
startDecorator={<Replay fontSize='small' />}
|
||||
onClick={retake}
|
||||
>
|
||||
Retake
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Kept outside the phase branches so the footer's Upload action can
|
||||
reach it even when no capture surface is rendered */}
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type='file'
|
||||
accept='image/*'
|
||||
style={{ display: 'none' }}
|
||||
onChange={handleFileSelect}
|
||||
/>
|
||||
<canvas ref={canvasRef} style={{ display: 'none' }} />
|
||||
</Box>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user