Refactor docker-compose setup and enhance scheduling components
- Re-enabled Vision API and Media API services in docker-compose.yml, providing necessary configurations for development. - Improved scheduling logic in HorizontalTimelineCalendar and Scheduling components to better manage repetition visibility and database scheduling status. - Updated docker-compose-reset.sh to conditionally wait for Supabase services, enhancing the setup process for local development. - Added isScheduledInDb prop to manage UI states for scheduled repetitions, improving user experience in the scheduling interface.
This commit is contained in:
@@ -67,6 +67,7 @@ function RepetitionBorder({
|
||||
onScheduleRepetition,
|
||||
visibleMarkers,
|
||||
getTimePosition,
|
||||
isScheduledInDb = false,
|
||||
children
|
||||
}: {
|
||||
left: number
|
||||
@@ -91,6 +92,7 @@ function RepetitionBorder({
|
||||
onScheduleRepetition?: (repId: string, experimentId: string) => void
|
||||
visibleMarkers: Array<{ id: string; startTime: Date; assignedConductors: string[] }>
|
||||
getTimePosition: (time: Date) => number
|
||||
isScheduledInDb?: boolean
|
||||
children?: React.ReactNode
|
||||
}) {
|
||||
const [isHovered, setIsHovered] = useState(false)
|
||||
@@ -148,6 +150,9 @@ function RepetitionBorder({
|
||||
? '8px 0 0 8px' // No radius on right side (markers extend to future)
|
||||
: '8px' // Full radius (default)
|
||||
|
||||
// Muted styling for repetitions that have been fully scheduled in DB (gray out, but don't collapse)
|
||||
const isMuted = isScheduledInDb && !isHovered && !isDragging
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`absolute transition-all duration-200 ease-in-out ${isLocked ? 'cursor-not-allowed' : 'cursor-grab'} active:cursor-grabbing`}
|
||||
@@ -159,11 +164,13 @@ function RepetitionBorder({
|
||||
border: '2px solid',
|
||||
borderLeft: extendLeft ? 'none' : '2px solid',
|
||||
borderRight: extendRight ? 'none' : '2px solid',
|
||||
borderColor: isHovered
|
||||
borderColor: isMuted
|
||||
? 'rgba(156, 163, 175, 0.4)'
|
||||
: isHovered
|
||||
? (isLocked ? 'rgba(156, 163, 175, 0.9)' : 'rgba(59, 130, 246, 0.9)')
|
||||
: (isLocked ? 'rgba(156, 163, 175, 0.3)' : 'rgba(59, 130, 246, 0.3)'),
|
||||
borderRadius: borderRadius,
|
||||
backgroundColor: 'transparent',
|
||||
backgroundColor: isMuted ? 'rgba(249, 250, 251, 0.6)' : 'transparent',
|
||||
opacity: isDragging ? 0.7 : 1,
|
||||
transition: isDragging ? 'none' : 'left 0.2s ease-out, border-color 0.2s ease-in-out',
|
||||
pointerEvents: 'auto',
|
||||
@@ -183,20 +190,30 @@ function RepetitionBorder({
|
||||
style={{
|
||||
left: `${textContainerLeft}px`,
|
||||
width: `${textContainerWidth}px`,
|
||||
height: `${height - 2}px`
|
||||
height: `${height - 2}px`,
|
||||
opacity: isMuted ? 0.5 : 1
|
||||
}}
|
||||
>
|
||||
{/* Text content (non-clickable) */}
|
||||
<div
|
||||
className="text-xs font-medium text-gray-700 dark:text-gray-300 flex-shrink-0"
|
||||
style={{
|
||||
lineHeight: '1.2',
|
||||
lineHeight: isScheduledInDb ? '1' : '1.2',
|
||||
whiteSpace: 'pre-line'
|
||||
}}
|
||||
>
|
||||
{phaseName && <div>{phaseName}</div>}
|
||||
{experimentNumber !== undefined && <div>Exp {experimentNumber}</div>}
|
||||
{repetitionNumber !== undefined && <div>Rep {repetitionNumber}</div>}
|
||||
{isScheduledInDb ? (
|
||||
<>
|
||||
{experimentNumber !== undefined && <div>{`Exp ${experimentNumber}`}</div>}
|
||||
{repetitionNumber !== undefined && <div>{`Rep ${repetitionNumber}`}</div>}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{phaseName && <div>{phaseName}</div>}
|
||||
{experimentNumber !== undefined && <div>Exp {experimentNumber}</div>}
|
||||
{repetitionNumber !== undefined && <div>Rep {repetitionNumber}</div>}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Go to repetition button */}
|
||||
@@ -215,7 +232,7 @@ function RepetitionBorder({
|
||||
<button
|
||||
onClick={handleSchedule}
|
||||
disabled={!allMarkersHaveConductors}
|
||||
className="flex-shrink-0 px-2 py-1 text-xs bg-green-600 hover:bg-green-700 disabled:bg-gray-400 disabled:cursor-not-allowed text-white rounded transition-colors"
|
||||
className="flex-shrink-0 px-2 py-1 text-xs bg-green-600 hover:bg-green-700 disabled:bg-gray-400 text-white rounded transition-colors"
|
||||
title={allMarkersHaveConductors ? "Schedule repetition" : "All markers must have at least one conductor assigned"}
|
||||
>
|
||||
Schedule
|
||||
@@ -1214,6 +1231,7 @@ export function HorizontalTimelineCalendar({
|
||||
experimentNumber={metadata?.experimentNumber}
|
||||
repetitionNumber={metadata?.repetitionNumber}
|
||||
experimentId={metadata?.experimentId}
|
||||
isScheduledInDb={metadata?.isScheduledInDb}
|
||||
onScrollToRepetition={onScrollToRepetition}
|
||||
onScheduleRepetition={onScheduleRepetition}
|
||||
visibleMarkers={borderMarkers}
|
||||
|
||||
@@ -251,29 +251,17 @@ export function ScheduleExperiment({ user, onBack }: { user: User; onBack: () =>
|
||||
}
|
||||
|
||||
const toggleRepetition = (repId: string) => {
|
||||
// Checking/unchecking should only control visibility on the timeline.
|
||||
// It must NOT clear scheduling info or conductor assignments.
|
||||
setSelectedRepetitionIds(prev => {
|
||||
const next = new Set(prev)
|
||||
if (next.has(repId)) {
|
||||
// Hide this repetition from the timeline
|
||||
next.delete(repId)
|
||||
// Remove from scheduled repetitions when unchecked
|
||||
setScheduledRepetitions(prevScheduled => {
|
||||
const newScheduled = { ...prevScheduled }
|
||||
delete newScheduled[repId]
|
||||
return newScheduled
|
||||
})
|
||||
// Clear all related state when unchecked
|
||||
setRepetitionsWithTimes(prev => {
|
||||
const next = new Set(prev)
|
||||
next.delete(repId)
|
||||
return next
|
||||
})
|
||||
setSchedulingRepetitions(prev => {
|
||||
const next = new Set(prev)
|
||||
next.delete(repId)
|
||||
return next
|
||||
})
|
||||
// Don't re-stagger remaining repetitions - they should keep their positions
|
||||
// Keep scheduledRepetitions and repetitionsWithTimes intact so that
|
||||
// re-checking the box restores the repetition in the correct spot.
|
||||
} else {
|
||||
// Show this repetition on the timeline
|
||||
next.add(repId)
|
||||
// Auto-spawn when checked - pass the updated set to ensure correct stagger calculation
|
||||
// spawnSingleRepetition will position the new repetition relative to existing ones
|
||||
@@ -293,20 +281,14 @@ export function ScheduleExperiment({ user, onBack }: { user: User; onBack: () =>
|
||||
const allSelected = allRepetitions.every(rep => prev.has(rep.id))
|
||||
|
||||
if (allSelected) {
|
||||
// Deselect all repetitions in this phase
|
||||
// Deselect all repetitions in this phase (hide from timeline only)
|
||||
const next = new Set(prev)
|
||||
allRepetitions.forEach(rep => {
|
||||
next.delete(rep.id)
|
||||
// Remove from scheduled repetitions
|
||||
setScheduledRepetitions(prevScheduled => {
|
||||
const newScheduled = { ...prevScheduled }
|
||||
delete newScheduled[rep.id]
|
||||
return newScheduled
|
||||
})
|
||||
})
|
||||
return next
|
||||
} else {
|
||||
// Select all repetitions in this phase
|
||||
// Select all repetitions in this phase (show on timeline)
|
||||
const next = new Set(prev)
|
||||
allRepetitions.forEach(rep => {
|
||||
next.add(rep.id)
|
||||
@@ -738,6 +720,51 @@ export function ScheduleExperiment({ user, onBack }: { user: User; onBack: () =>
|
||||
}
|
||||
}
|
||||
|
||||
// Unschedule a repetition: clear its scheduling info and unassign all conductors.
|
||||
const unscheduleRepetition = async (repId: string, experimentId: string) => {
|
||||
setSchedulingRepetitions(prev => new Set(prev).add(repId))
|
||||
|
||||
try {
|
||||
// Remove all conductor assignments for this repetition
|
||||
removeRepetitionAssignments(repId)
|
||||
|
||||
// Clear scheduled_date on the repetition in local state
|
||||
setRepetitionsByExperiment(prev => ({
|
||||
...prev,
|
||||
[experimentId]: prev[experimentId]?.map(r =>
|
||||
r.id === repId ? { ...r, scheduled_date: null } : r
|
||||
) || []
|
||||
}))
|
||||
|
||||
// Clear scheduled times for this repetition so it disappears from the timeline
|
||||
setScheduledRepetitions(prev => {
|
||||
const next = { ...prev }
|
||||
delete next[repId]
|
||||
return next
|
||||
})
|
||||
|
||||
// This repetition no longer has active times
|
||||
setRepetitionsWithTimes(prev => {
|
||||
const next = new Set(prev)
|
||||
next.delete(repId)
|
||||
return next
|
||||
})
|
||||
|
||||
// Also clear scheduled_date in the database for this repetition
|
||||
await repetitionManagement.updateRepetition(repId, {
|
||||
scheduled_date: null
|
||||
})
|
||||
} catch (error: any) {
|
||||
setError(error?.message || 'Failed to unschedule repetition')
|
||||
} finally {
|
||||
setSchedulingRepetitions(prev => {
|
||||
const next = new Set(prev)
|
||||
next.delete(repId)
|
||||
return next
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Restore scroll position after scheduledRepetitions changes
|
||||
useEffect(() => {
|
||||
if (scrollPositionRef.current) {
|
||||
@@ -792,6 +819,11 @@ export function ScheduleExperiment({ user, onBack }: { user: User; onBack: () =>
|
||||
|
||||
Object.values(scheduledRepetitions).forEach(scheduled => {
|
||||
const repId = scheduled.repetitionId
|
||||
// Only include markers for repetitions that are checked (selected)
|
||||
if (!selectedRepetitionIds.has(repId)) {
|
||||
return
|
||||
}
|
||||
|
||||
const markerIdPrefix = repId
|
||||
|
||||
if (scheduled.soakingStart) {
|
||||
@@ -834,14 +866,19 @@ export function ScheduleExperiment({ user, onBack }: { user: User; onBack: () =>
|
||||
conductorAvailabilities,
|
||||
phaseMarkers
|
||||
}
|
||||
}, [selectedConductorIds, conductors, conductorColorMap, colorPalette, availabilityEvents, scheduledRepetitions, conductorAssignments, calendarStartDate, calendarZoom])
|
||||
}, [selectedConductorIds, conductors, conductorColorMap, colorPalette, availabilityEvents, scheduledRepetitions, conductorAssignments, calendarStartDate, calendarZoom, selectedRepetitionIds])
|
||||
|
||||
// Build repetition metadata mapping for timeline display
|
||||
const repetitionMetadata = useMemo(() => {
|
||||
const metadata: Record<string, { phaseName: string; experimentNumber: number; repetitionNumber: number }> = {}
|
||||
const metadata: Record<string, { phaseName: string; experimentNumber: number; repetitionNumber: number; experimentId: string; isScheduledInDb: boolean }> = {}
|
||||
|
||||
Object.values(scheduledRepetitions).forEach(scheduled => {
|
||||
const repId = scheduled.repetitionId
|
||||
// Only include metadata for repetitions that are checked (selected)
|
||||
if (!selectedRepetitionIds.has(repId)) {
|
||||
return
|
||||
}
|
||||
|
||||
const experiment = Object.values(experimentsByPhase).flat().find(e => e.id === scheduled.experimentId)
|
||||
const repetition = Object.values(repetitionsByExperiment).flat().find(r => r.id === repId)
|
||||
const phase = phases.find(p =>
|
||||
@@ -853,13 +890,15 @@ export function ScheduleExperiment({ user, onBack }: { user: User; onBack: () =>
|
||||
phaseName: phase.name,
|
||||
experimentNumber: experiment.experiment_number,
|
||||
repetitionNumber: repetition.repetition_number,
|
||||
experimentId: scheduled.experimentId
|
||||
experimentId: scheduled.experimentId,
|
||||
// Consider a repetition \"scheduled\" in DB if it has a non-null scheduled_date
|
||||
isScheduledInDb: Boolean(repetition.scheduled_date)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return metadata
|
||||
}, [scheduledRepetitions, experimentsByPhase, repetitionsByExperiment, phases])
|
||||
}, [scheduledRepetitions, experimentsByPhase, repetitionsByExperiment, phases, selectedRepetitionIds])
|
||||
|
||||
// Scroll to repetition in accordion
|
||||
const handleScrollToRepetition = useCallback(async (repetitionId: string) => {
|
||||
@@ -1239,8 +1278,8 @@ export function ScheduleExperiment({ user, onBack }: { user: User; onBack: () =>
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">Rep {rep.repetition_number}</span>
|
||||
</label>
|
||||
|
||||
{/* Time points (shown only if has been dropped/moved) */}
|
||||
{hasTimes && scheduled && (
|
||||
{/* Time points (shown whenever the repetition has scheduled times) */}
|
||||
{scheduled && (
|
||||
<div className="mt-2 ml-6 text-xs space-y-1">
|
||||
{(() => {
|
||||
const repId = rep.id
|
||||
@@ -1293,23 +1332,35 @@ export function ScheduleExperiment({ user, onBack }: { user: User; onBack: () =>
|
||||
)
|
||||
})()}
|
||||
|
||||
{/* Remove Assignments button and Schedule button */}
|
||||
{/* Remove Assignments button and Schedule/Unschedule button */}
|
||||
<div className="flex items-center gap-3 mt-3 pt-2 border-t border-gray-200 dark:border-gray-600">
|
||||
{hasAssignments && (
|
||||
<button
|
||||
onClick={() => removeRepetitionAssignments(rep.id)}
|
||||
className="px-3 py-1 bg-red-600 hover:bg-red-700 text-white rounded text-xs transition-colors"
|
||||
disabled={Boolean(rep.scheduled_date)}
|
||||
className="px-3 py-1 bg-red-600 hover:bg-red-700 disabled:bg-gray-400 text-white rounded text-xs transition-colors"
|
||||
title={rep.scheduled_date ? "Unschedule the repetition first before removing assignments" : "Remove all conductor assignments from this repetition"}
|
||||
>
|
||||
Remove Assignments
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => scheduleRepetition(rep.id, exp.id)}
|
||||
disabled={isScheduling}
|
||||
className="px-3 py-1 bg-green-600 hover:bg-green-700 disabled:bg-gray-400 text-white rounded text-xs transition-colors"
|
||||
>
|
||||
{isScheduling ? 'Scheduling...' : 'Schedule'}
|
||||
</button>
|
||||
{rep.scheduled_date ? (
|
||||
<button
|
||||
onClick={() => unscheduleRepetition(rep.id, exp.id)}
|
||||
disabled={isScheduling}
|
||||
className="px-3 py-1 bg-yellow-600 hover:bg-yellow-700 disabled:bg-gray-400 text-white rounded text-xs transition-colors"
|
||||
>
|
||||
{isScheduling ? 'Unscheduling...' : 'Unschedule'}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => scheduleRepetition(rep.id, exp.id)}
|
||||
disabled={isScheduling}
|
||||
className="px-3 py-1 bg-green-600 hover:bg-green-700 disabled:bg-gray-400 text-white rounded text-xs transition-colors"
|
||||
>
|
||||
{isScheduling ? 'Scheduling...' : 'Schedule'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user