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
2 changes: 1 addition & 1 deletion backend/api/inputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ class CreateSavedViewInput:
filter_definition: str
sort_definition: str
parameters: str
visibility: SavedViewVisibility = SavedViewVisibility.PRIVATE
visibility: SavedViewVisibility = SavedViewVisibility.LINK_SHARED


@strawberry.input
Expand Down
4 changes: 2 additions & 2 deletions web/api/gql/generated.ts

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions web/api/graphql/GetOverviewData.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ query GetOverviewData($rootLocationIds: [ID!], $recentPatientsFilters: [QueryFil
lastOnline
isOnline
}
assigneeTeam {
id
title
kind
}
patient {
id
name
Expand Down
53 changes: 52 additions & 1 deletion web/api/mutations/tasks/updateTask.plan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { ApolloCache } from '@apollo/client/cache'
import type { Reference } from '@apollo/client/utilities'
import {
GetTaskDocument,
LocationType,
type GetTaskQuery,
type UpdateTaskInput
} from '@/api/gql/generated'
Expand All @@ -15,6 +16,44 @@ type UpdateTaskVariables = {
clientMutationId?: string,
}

type TaskEntity = NonNullable<GetTaskQuery['task']>

function optimisticAssignees(
assigneeIds: string[] | null | undefined,
previous: TaskEntity['assignees'] | undefined
): TaskEntity['assignees'] | undefined {
if (assigneeIds === undefined) return undefined
const prev = previous ?? []
const ids = assigneeIds ?? []
return ids.map((userId) => {
const found = prev.find((u) => u.id === userId)
if (found) return found
return {
__typename: 'UserType' as const,
id: userId,
name: '',
avatarUrl: null,
lastOnline: null,
isOnline: false,
}
})
}

function optimisticAssigneeTeam(
assigneeTeamId: string | null | undefined,
previous: TaskEntity['assigneeTeam'] | undefined
): TaskEntity['assigneeTeam'] | null | undefined {
if (assigneeTeamId === undefined) return undefined
if (assigneeTeamId === null) return null
if (previous?.id === assigneeTeamId) return previous
return {
__typename: 'LocationNodeType' as const,
id: assigneeTeamId,
title: '',
kind: LocationType.Team,
}
}

export const updateTaskOptimisticPlanKey = 'UpdateTask'

export const updateTaskOptimisticPlan: OptimisticPlan<UpdateTaskVariables> = {
Expand All @@ -35,7 +74,8 @@ export const updateTaskOptimisticPlan: OptimisticPlan<UpdateTaskVariables> = {
snapshotRef.current = existing ?? null

const id = cache.identify({ __typename: 'TaskType', id: taskId })
const existingProps = existing?.task?.properties ?? []
const existingTask = existing?.task
const existingProps = existingTask?.properties ?? []
const mergeProperties = (_prev: Reference | readonly unknown[]) => {
if (!data.properties) return existingProps
return data.properties.map((inp) => {
Expand Down Expand Up @@ -87,6 +127,17 @@ export const updateTaskOptimisticPlan: OptimisticPlan<UpdateTaskVariables> = {
estimatedTime: (prev: number | null) =>
data.estimatedTime !== undefined ? data.estimatedTime : prev,
properties: mergeProperties,
assignees: (prev) => {
const next = optimisticAssignees(data.assigneeIds, existingTask?.assignees)
return next !== undefined ? next : prev
},
assigneeTeam: (prev) => {
const next = optimisticAssigneeTeam(
data.assigneeTeamId,
existingTask?.assigneeTeam
)
return next !== undefined ? next : prev
},
},
})
cache.modify({
Expand Down
14 changes: 8 additions & 6 deletions web/components/layout/Page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -416,7 +416,7 @@ export const Header = ({ onMenuClick, isMenuOpen, ...props }: HeaderProps) => {
color="neutral"
coloringStyle="text"
onClick={onMenuClick}
className="lg:hidden"
className="min-h-11 min-w-11 lg:hidden"
>
{isMenuOpen ? <X className="size-6" /> : <MenuIcon className="size-6" />}
</IconButton>
Expand Down Expand Up @@ -505,15 +505,17 @@ export const Sidebar = ({ isOpen, onClose, ...props }: SidebarProps) => {
<>
{isOpen && (
<div
className="fixed inset-0 bg-overlay-shadow z-40 lg:hidden"
className="fixed inset-0 z-40 touch-manipulation bg-overlay-shadow lg:hidden"
onClick={onClose}
role="presentation"
/>
)}
<aside
{...props}
className={clsx(
'flex-col-4 w-50 min-w-56 rounded-lg bg-surface text-on-surface overflow-hidden shadow-md',
'fixed lg:relative inset-y-0 z-50 lg:z-auto',
'pt-[env(safe-area-inset-top)] pb-[env(safe-area-inset-bottom)]',
'w-screen max-w-sm lg:w-50 lg:min-w-56',
'transform transition-transform duration-300 ease-out',
isOpen
Expand All @@ -535,7 +537,7 @@ export const Sidebar = ({ isOpen, onClose, ...props }: SidebarProps) => {
color="neutral"
coloringStyle="text"
onClick={onClose}
className="lg:hidden"
className="min-h-11 min-w-11 lg:hidden"
>
<X className="size-6" />
</IconButton>
Expand Down Expand Up @@ -683,7 +685,7 @@ export const Page = ({
})

return (
<div className="flex-row-0 h-screen w-screen overflow-hidden overflow-x-hidden">
<div className="flex-row-0 h-dvh min-h-dvh max-h-dvh w-screen overflow-hidden overflow-x-hidden">
<Head>
<title>{titleWrapper(pageTitle)}</title>
</Head>
Expand All @@ -696,10 +698,10 @@ export const Page = ({
/>
<div
ref={mainContentRef as React.RefObject<HTMLDivElement>}
className="flex-col-4 lg:pl-8 grow overflow-y-scroll"
className="flex-col-4 lg:pl-8 grow overflow-y-auto overscroll-y-contain"
>
<Header
className="sticky top-0 right-0 p-4 bg-background text-on-background"
className="sticky top-0 right-0 z-20 p-4 bg-background text-on-background"
onMenuClick={() => setIsSidebarOpen(!isSidebarOpen)}
isMenuOpen={isSidebarOpen}
/>
Expand Down
14 changes: 7 additions & 7 deletions web/components/properties/PropertyDetailView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -200,10 +200,10 @@ export const PropertyDetailView = ({

return (
<FormProvider state={form}>
<div className="flex-col-0 h-full overflow-hidden">
<div className="flex-col-0 h-full min-h-0 overflow-hidden px-1 sm:px-0">
<form
onSubmit={event => { event.preventDefault(); form.submit() }}
className="flex-col-6 flex-1 overflow-y-auto"
className="flex flex-col flex-1 gap-6 overflow-y-auto sm:gap-6"
>
<FormField<PropertyFormValues, 'name'>
name="name"
Expand Down Expand Up @@ -289,7 +289,7 @@ export const PropertyDetailView = ({
<span className="typography-title-md">
{translation('selectOptions')}
</span>
<div className="flex-col-2 min-h-64 max-h-64 overflow-y-auto">
<div className="flex-col-2 min-h-48 max-h-64 sm:min-h-64 overflow-y-auto">
<FormObserverKey<PropertyFormValues, 'selectData'> formKey="selectData">
{({ value: selectData }) => {
return selectData?.options.map((option) => (
Expand Down Expand Up @@ -445,8 +445,8 @@ export const PropertyDetailView = ({
label={translation('archiveProperty')}
>
{({ dataProps: { value, onValueChange }, focusableElementProps, interactionStates }) => (
<div className="flex-row-4 justify-between items-center">
<div className="flex-col-1">
<div className="flex flex-col gap-4 sm:flex-row sm:justify-between sm:items-center">
<div className="flex-col-1 min-w-0">
<span className="typography-title-md">
{translation('archiveProperty')}
</span>
Expand All @@ -472,12 +472,12 @@ export const PropertyDetailView = ({
</form>

{!isEditMode && (
<div className="flex-row-0 pt-2 justify-end">
<div className="flex flex-row pt-2 justify-stretch sm:justify-end">
<Button
type="submit"
onClick={() => form.submit()}
disabled={isLoading}
className="w-fit"
className="min-h-11 w-full sm:w-fit"
>
{translation('create')}
</Button>
Expand Down
85 changes: 85 additions & 0 deletions web/components/tables/AssigneeFilterActiveLabel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import type { FilterValue } from '@helpwave/hightide'
import { Chip } from '@helpwave/hightide'
import { User } from 'lucide-react'
import { useMemo } from 'react'
import { useUsers } from '@/data'
import { useTasksTranslation } from '@/i18n/useTasksTranslation'
import { FilterPreviewAvatar } from '@/components/tables/FilterPreviewMedia'

export function AssigneeFilterActiveLabel({ value }: { value: FilterValue }) {
const translation = useTasksTranslation()
const { data } = useUsers()
const users = data?.users

const content = useMemo(() => {
const param = value?.parameter ?? {}
const op = value?.operator ?? 'equals'
if (op === 'contains') {
const ids = (param.uuidValues as string[] | undefined) ?? []
if (ids.length === 0) {
return <span className="text-sm">{translation('selectAssignee')}</span>
}
if (ids.length <= 2) {
return (
<span className="flex flex-wrap items-center gap-1">
{ids.map(id => {
const user = users?.find(u => u.id === id)
const title = user?.name ?? id
return (
<Chip key={id} size="sm" color="neutral" className="max-w-[11rem] py-0.5">
<span className="flex items-center gap-1 min-w-0">
{user ? (
<FilterPreviewAvatar name={user.name} avatarUrl={user.avatarUrl} />
) : (
<User className="size-3 shrink-0 scale-90" />
)}
<span className="truncate text-sm">{title}</span>
</span>
</Chip>
)
})}
</span>
)
}
return (
<Chip size="sm" color="neutral" className="py-0.5">
<span className="flex items-center gap-1.5 min-w-0">
<span className="flex items-center -space-x-1 shrink-0">
{ids.slice(0, 3).map(id => {
const user = users?.find(u => u.id === id)
return user ? (
<FilterPreviewAvatar key={id} name={user.name} avatarUrl={user.avatarUrl} />
) : (
<User key={id} className="size-3 shrink-0 scale-90 opacity-70" />
)
})}
</span>
<span className="truncate text-sm">
{ids.length} {translation('users')}
</span>
</span>
</Chip>
)
}
const uid = param.uuidValue != null ? String(param.uuidValue) : ''
if (!uid) {
return <span className="text-sm">{translation('selectAssignee')}</span>
}
const user = users?.find(u => u.id === uid)
const title = user?.name ?? uid
return (
<Chip size="sm" color="neutral" className="max-w-[15rem] py-0.5">
<span className="flex items-center gap-1 min-w-0">
{user ? (
<FilterPreviewAvatar name={user.name} avatarUrl={user.avatarUrl} />
) : (
<User className="size-3 shrink-0 scale-90" />
)}
<span className="truncate text-sm">{title}</span>
</span>
</Chip>
)
}, [users, translation, value])

return content
}
48 changes: 48 additions & 0 deletions web/components/tables/FilterPreviewMedia.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { LocationType } from '@/api/gql/generated'
import { LocationChips } from '@/components/locations/LocationChips'
import { Avatar } from '@helpwave/hightide'
import clsx from 'clsx'

export type FilterPreviewLocationItem = {
id: string,
title: string,
kind?: LocationType,
}

export function FilterPreviewAvatar({
name,
avatarUrl,
className,
}: {
name: string,
avatarUrl?: string | null,
className?: string,
}) {
return (
<span
className={clsx(
'inline-flex shrink-0 origin-center scale-[0.82] rounded-full ring-1 ring-border/50 overflow-hidden align-middle',
className
)}
>
<Avatar
size="xs"
image={avatarUrl ? { avatarUrl, alt: name } : undefined}
/>
</span>
)
}

export function FilterPreviewLocationChips({
locations,
className,
}: {
locations: FilterPreviewLocationItem[],
className?: string,
}) {
return (
<span className={clsx('inline-flex min-w-0 max-w-full origin-left scale-[0.82]', className)}>
<LocationChips locations={locations} small disableLink className="max-w-full" />
</span>
)
}
Loading
Loading