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:
Mohamad Tarbin
2026-08-02 16:14:52 -04:00
committed by GitHub
parent 9478f7d0f5
commit b2949b2dd9
20 changed files with 1587 additions and 954 deletions

View File

@@ -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 &quot;Scan Document&quot; 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>
)