Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 22 additions & 5 deletions web/components/Date/DateDisplay.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { Tooltip, useUpdatingDateString } from '@helpwave/hightide'
import { Tooltip, useLocale } from '@helpwave/hightide'
import clsx from 'clsx'
import { useMemo } from 'react'
import {
formatAbsoluteHightide,
formatRelativeHightide,
type DateTimeFormat
} from '@/utils/hightideDateFormat'

type DateDisplayProps = {
date: Date,
Expand All @@ -8,11 +14,22 @@ type DateDisplayProps = {
mode?: 'relative' | 'absolute',
}

function toAbsoluteFormat(showTime: boolean): DateTimeFormat {
return showTime ? 'dateTime' : 'date'
}

export const DateDisplay = ({ date, className, showTime = true, mode = 'relative' }: DateDisplayProps) => {
const { absolute, relative } = useUpdatingDateString({
date: date ?? new Date(),
absoluteFormat: showTime ? 'dateTime' : 'date',
})
const { locale } = useLocale()
const absoluteFormat = toAbsoluteFormat(showTime)
const absolute = useMemo(
() => (date ? formatAbsoluteHightide(date, locale, absoluteFormat) : ''),
[date, locale, absoluteFormat]
)
const relative = useMemo(
() => (date ? formatRelativeHightide(date, locale) : ''),
[date, locale]
)

if (!date) return null

const displayString = mode === 'relative' ? relative : absolute
Expand Down
3 changes: 1 addition & 2 deletions web/components/patients/PatientTasksView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -166,9 +166,8 @@ export const PatientTasksView = ({
<TaskDetailView
taskId={taskId}
initialPatientId={isCreatingTask ? patientId : undefined}
onSuccess={() => {
onListSync={() => {
onSuccess?.()
setIsCreatingTask(false)
}}
onClose={() => {
setTaskId(null)
Expand Down
4 changes: 2 additions & 2 deletions web/components/properties/PropertyCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export const PropertyCell = ({
return <FillerCell />
}
return (
<DateDisplay date={date} showTime={false} />
<DateDisplay date={date} showTime={false} mode="absolute" />
)
}
case FieldType.FieldTypeDateTime: {
Expand All @@ -58,7 +58,7 @@ export const PropertyCell = ({
return <FillerCell />
}
return (
<DateDisplay date={date} />
<DateDisplay date={date} mode="absolute" />
)
}
case FieldType.FieldTypeSelect: {
Expand Down
3 changes: 2 additions & 1 deletion web/components/properties/PropertyEntry.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
CheckboxProperty,
DateProperty,
MultiSelectOption,
MultiSelectProperty,
NumberProperty,
PropertyBase,
Expand Down Expand Up @@ -121,7 +122,7 @@ export const PropertyEntry = ({
onEditComplete={multiSelectValue => onEditComplete({ ...value, multiSelectValue })}
>
{selectData?.options.map(option => (
<SelectOption key={option.id} value={option.id} label={option.name} />
<MultiSelectOption key={option.id} value={option.id} label={option.name} />
))}
</MultiSelectProperty>
)
Expand Down
4 changes: 2 additions & 2 deletions web/components/tables/RecentTasksTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ export const RecentTasksTable = ({
return (
<DateDisplay
date={new Date(row.original.dueDate)}
mode="relative"
mode="absolute"
className={clsx(colorClass)}
/>
)
Expand Down Expand Up @@ -211,7 +211,7 @@ export const RecentTasksTable = ({
const date = getValue() as Date | undefined
if (!date) return <FillerCell />
return (
<DateDisplay date={date} />
<DateDisplay date={date} mode="absolute" />
)
},
minSize: 220,
Expand Down
32 changes: 19 additions & 13 deletions web/components/tables/TaskList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ const TaskAssigneeTableCell = memo(function TaskAssigneeTableCell({
)
})

function taskListDataSyncKey(tasks: TaskViewModel[]): string {
return tasks.map(t => `${t.id}:${t.done}:${t.updateDate.getTime()}`).join('\0')
}

export type TaskViewModel = {
id: string,
name: string,
Expand Down Expand Up @@ -212,15 +216,22 @@ export const TaskList = forwardRef<TaskListRef, TaskListProps>(({ tasks: initial
}
}))

const initialTaskPresent = Boolean(initialTaskId && initialTasks.some(t => t.id === initialTaskId))

const initialTasksSyncKey = useMemo(
() => taskListDataSyncKey(initialTasks),
[initialTasks]
)

useEffect(() => {
if (initialTaskId && initialTasks.length > 0 && openedTaskId !== initialTaskId) {
if (initialTaskId && initialTaskPresent && openedTaskId !== initialTaskId) {
setTaskDialogState({ isOpen: true, taskId: initialTaskId })
setOpenedTaskId(initialTaskId)
onInitialTaskOpened?.()
} else if (!initialTaskId) {
setOpenedTaskId(null)
}
}, [initialTaskId, initialTasks, openedTaskId, onInitialTaskOpened])
}, [initialTaskId, initialTaskPresent, openedTaskId, onInitialTaskOpened])

useEffect(() => {
setOptimisticUpdates(prev => {
Expand All @@ -237,7 +248,7 @@ export const TaskList = forwardRef<TaskListRef, TaskListProps>(({ tasks: initial

return hasChanges ? next : prev
})
}, [initialTasks])
}, [initialTasksSyncKey, initialTasks])

const isServerDriven = totalCount != null

Expand Down Expand Up @@ -276,7 +287,7 @@ export const TaskList = forwardRef<TaskListRef, TaskListProps>(({ tasks: initial
useEffect(() => {
if (isServerDriven) return
setClientVisibleCount(LIST_PAGE_SIZE)
}, [initialTasks, searchQuery, isServerDriven])
}, [initialTasksSyncKey, searchQuery, isServerDriven])

const displayedTasks = useMemo(() => {
if (isServerDriven) return tasks
Expand Down Expand Up @@ -466,12 +477,10 @@ export const TaskList = forwardRef<TaskListRef, TaskListProps>(({ tasks: initial
if (checked) {
completeTask({
variables: { id: task.id },
onCompleted: () => onRefetch?.(),
})
} else {
reopenTask({
variables: { id: task.id },
onCompleted: () => onRefetch?.(),
})
}
}}
Expand Down Expand Up @@ -528,7 +537,7 @@ export const TaskList = forwardRef<TaskListRef, TaskListProps>(({ tasks: initial
<TaskRowRefreshingGate taskId={row.original.id}>
<DateDisplay
date={row.original.dueDate}
mode="relative"
mode="absolute"
className={clsx(colorClass)}
/>
</TaskRowRefreshingGate>
Expand Down Expand Up @@ -658,7 +667,7 @@ export const TaskList = forwardRef<TaskListRef, TaskListProps>(({ tasks: initial
]
return colsWithRefreshing
},
[translation, completeTask, reopenTask, showAssignee, taskPropertyColumns, onRefetch])
[translation, completeTask, reopenTask, showAssignee, taskPropertyColumns])

const taskCardPrimaryColumnIds = useMemo(() => {
const s = new Set<string>(['done', 'title', 'dueDate', 'patient'])
Expand Down Expand Up @@ -843,7 +852,6 @@ export const TaskList = forwardRef<TaskListRef, TaskListProps>(({ tasks: initial
task={task}
showAssignee={showAssignee}
showPatient={true}
onRefetch={onRefetch}
onClick={() => setTaskDialogState({ isOpen: true, taskId: task.id })}
extraContent={renderTaskCardExtras(task)}
/>
Expand All @@ -866,8 +874,7 @@ export const TaskList = forwardRef<TaskListRef, TaskListProps>(({ tasks: initial
<TaskDetailView
taskId={taskDialogState.taskId ?? null}
onClose={() => setTaskDialogState({ isOpen: false })}
onSuccess={onRefetch || (() => {
})}
onListSync={onRefetch}
/>
</Drawer>
<Drawer
Expand All @@ -881,8 +888,7 @@ export const TaskList = forwardRef<TaskListRef, TaskListProps>(({ tasks: initial
<PatientDetailView
patientId={selectedPatientId}
onClose={() => setSelectedPatientId(null)}
onSuccess={onRefetch || (() => {
})}
onSuccess={() => {}}
/>
)}
</Drawer>
Expand Down
23 changes: 18 additions & 5 deletions web/components/tables/TaskRowRefreshingGate.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createContext, useContext, type ReactNode } from 'react'
import { LoadingContainer } from '@helpwave/hightide'
import { Loader2 } from 'lucide-react'

const EMPTY_TASK_IDS = new Set<string>()

Expand All @@ -12,8 +12,21 @@ type TaskRowRefreshingGateProps = {

export function TaskRowRefreshingGate({ taskId, children }: TaskRowRefreshingGateProps) {
const ids = useContext(RefreshingTaskIdsContext)
if (ids.has(taskId)) {
return <LoadingContainer className="w-full min-h-8" />
}
return children
const refreshing = ids.has(taskId)
return (
<div className="relative min-h-8 min-w-0">
<div className={refreshing ? 'opacity-50' : undefined}>
{children}
</div>
{refreshing && (
<div
className="pointer-events-none absolute inset-0 z-[1] flex items-center justify-center bg-surface/25"
aria-busy
aria-hidden
>
<Loader2 className="size-4 shrink-0 animate-spin text-description" />
</div>
)}
</div>
)
}
15 changes: 12 additions & 3 deletions web/components/tasks/AssigneeSelectDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { Users, Info } from 'lucide-react'
import { useUsers, useLocations } from '@/data'
import clsx from 'clsx'

const EMPTY_MULTI_USER_IDS: string[] = []

interface AssigneeSelectDialogProps {
value: string,
onValueChanged: (value: string) => void,
Expand Down Expand Up @@ -33,12 +35,13 @@ export const AssigneeSelectDialog = ({
onUserInfoClick,
multiUserSelect = false,
onMultiUserIdsSelected,
initialMultiUserIds = [],
initialMultiUserIds,
}: AssigneeSelectDialogProps) => {
const translation = useTasksTranslation()
const [searchQuery, setSearchQuery] = useState('')
const [pendingUserIds, setPendingUserIds] = useState<Set<string>>(new Set())
const searchInputRef = useRef<HTMLDivElement>(null)
const initialMultiIds = initialMultiUserIds ?? EMPTY_MULTI_USER_IDS

const { data: usersData } = useUsers()
const { data: locationsData } = useLocations()
Expand Down Expand Up @@ -96,9 +99,15 @@ export const AssigneeSelectDialog = ({
setSearchQuery('')
}
if (isOpen && multiUserSelect) {
setPendingUserIds(new Set(initialMultiUserIds))
setPendingUserIds(prev => {
const next = new Set(initialMultiIds)
if (prev.size === next.size && [...next].every((id) => prev.has(id))) {
return prev
}
return next
})
}
}, [isOpen, multiUserSelect, initialMultiUserIds])
}, [isOpen, multiUserSelect, initialMultiIds])

const handleSelect = (selectedValue: string) => {
onValueChanged(selectedValue)
Expand Down
9 changes: 3 additions & 6 deletions web/components/tasks/TaskCardView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ type TaskCardViewProps = {
onClick: (task: FlexibleTask | TaskViewModel) => void,
showAssignee?: boolean,
showPatient?: boolean,
onRefetch?: () => void,
className?: string,
fullWidth?: boolean,
extraContent?: ReactNode,
Expand All @@ -75,7 +74,7 @@ const toDate = (date: Date | string | null | undefined): Date | undefined => {
return new Date(date)
}

export const TaskCardView = ({ task, onToggleDone: _onToggleDone, onClick, showAssignee: _showAssignee = false, showPatient = true, onRefetch, className, fullWidth: _fullWidth = false, extraContent }: TaskCardViewProps) => {
export const TaskCardView = ({ task, onToggleDone: _onToggleDone, onClick, showAssignee: _showAssignee = false, showPatient = true, className, fullWidth: _fullWidth = false, extraContent }: TaskCardViewProps) => {
const router = useRouter()
const [selectedUserId, setSelectedUserId] = useState<string | null>(null)
const [optimisticDone, setOptimisticDone] = useState<boolean | null>(null)
Expand Down Expand Up @@ -125,7 +124,6 @@ export const TaskCardView = ({ task, onToggleDone: _onToggleDone, onClick, showA
onCompleted: () => {
pendingCheckedRef.current = null
setOptimisticDone(null)
onRefetch?.()
},
onError: () => {
pendingCheckedRef.current = null
Expand All @@ -138,7 +136,6 @@ export const TaskCardView = ({ task, onToggleDone: _onToggleDone, onClick, showA
onCompleted: () => {
pendingCheckedRef.current = null
setOptimisticDone(null)
onRefetch?.()
},
onError: () => {
pendingCheckedRef.current = null
Expand Down Expand Up @@ -259,14 +256,14 @@ export const TaskCardView = ({ task, onToggleDone: _onToggleDone, onClick, showA
{dueDate && (
<div className={clsx('flex items-center gap-2', dueDateColorClass)}>
<Clock className="size-4 shrink-0" />
<DateDisplay date={dueDate} mode="relative" showTime={true} />
<DateDisplay date={dueDate} mode="absolute" showTime={true} />
</div>
)}
</div>
{expectedFinishDate && (
<div className="flex items-center gap-2 text-xs">
<Flag className="size-4 shrink-0" />
<DateDisplay date={expectedFinishDate} mode="relative" showTime={true} />
<DateDisplay date={expectedFinishDate} mode="absolute" showTime={true} />
</div>
)}
</div>
Expand Down
9 changes: 4 additions & 5 deletions web/components/tasks/TaskDataEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,14 @@ type TaskFormValues = CreateTaskInput & {
interface TaskDataEditorProps {
id: null | string,
initialPatientId?: string,
onSuccess?: () => void,
onListSync?: () => void,
onClose?: () => void,
}

export const TaskDataEditor = ({
id,
initialPatientId,
onSuccess,
onListSync,
onClose,
}: TaskDataEditorProps) => {
const translation = useTasksTranslation()
Expand Down Expand Up @@ -83,7 +83,6 @@ export const TaskDataEditor = ({
const updateTask = (vars: { id: string, data: UpdateTaskInput }) => {
updateTaskMutate({
variables: vars,
onCompleted: () => onSuccess?.(),
onError: (err) => {
setErrorDialog({
isOpen: true,
Expand Down Expand Up @@ -123,7 +122,7 @@ export const TaskDataEditor = ({
} as CreateTaskInput & { priority?: TaskPriority | null, estimatedTime?: number | null }
},
onCompleted: () => {
onSuccess?.()
onListSync?.()
onClose?.()
},
onError: (error) => {
Expand Down Expand Up @@ -501,7 +500,7 @@ export const TaskDataEditor = ({
deleteTask({
variables: { id: taskId },
onCompleted: () => {
onSuccess?.()
onListSync?.()
onClose?.()
},
})
Expand Down
Loading
Loading