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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,7 @@ htmlcov/
# feedback
**/feedback/
feedback/

# profile pictures
**/profile/
profile/
5 changes: 5 additions & 0 deletions backend/api/inputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,3 +165,8 @@ class UpdatePropertyDefinitionInput:
options: list[str] | None = None
is_active: bool | None = None
allowed_entities: list[PropertyEntity] | None = None


@strawberry.input
class UpdateProfilePictureInput:
avatar_url: str
3 changes: 2 additions & 1 deletion backend/api/resolvers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from .patient import PatientMutation, PatientQuery, PatientSubscription
from .property import PropertyDefinitionMutation, PropertyDefinitionQuery
from .task import TaskMutation, TaskQuery, TaskSubscription
from .user import UserQuery
from .user import UserMutation, UserQuery


@strawberry.type
Expand All @@ -26,6 +26,7 @@ class Mutation(
TaskMutation,
PropertyDefinitionMutation,
LocationMutation,
UserMutation,
):
pass

Expand Down
30 changes: 30 additions & 0 deletions backend/api/resolvers/user.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import strawberry
from api.context import Info
from api.inputs import UpdateProfilePictureInput
from api.resolvers.base import BaseMutationResolver
from api.types.user import UserType
from database import models
from graphql import GraphQLError
from sqlalchemy import select


Expand All @@ -22,3 +25,30 @@ async def users(self, info: Info) -> list[UserType]:
@strawberry.field
def me(self, info: Info) -> UserType | None:
return info.context.user


@strawberry.type
class UserMutation(BaseMutationResolver[models.User]):
@strawberry.mutation
async def update_profile_picture(
self,
info: Info,
data: UpdateProfilePictureInput,
) -> UserType:
if not info.context.user:
raise GraphQLError(
"Authentication required. Please log in to update your profile picture.",
extensions={"code": "UNAUTHENTICATED"},
)

user = info.context.user
user.avatar_url = data.avatar_url

await BaseMutationResolver.update_and_notify(
info,
user,
models.User,
"user",
)

return user
1 change: 1 addition & 0 deletions docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ services:
RUNTIME_CLIENT_ID: "tasks-web"
volumes:
- "./feedback:/feedback"
- "./profile:/profile"
depends_on:
- backend

Expand Down
2 changes: 2 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ services:
RUNTIME_CLIENT_ID: "tasks-web"
volumes:
- "./feedback:/feedback"
- "profile-data:/profile"
depends_on:
- backend

Expand Down Expand Up @@ -128,3 +129,4 @@ volumes:
keycloak-data:
postgres-data:
influxdb-data:
profile-data:
5 changes: 4 additions & 1 deletion web/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,12 @@ RUN apk add --no-cache libcap=2.77-r0 && \
addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 tasks && \
mkdir -p /feedback && \
chown tasks:nodejs /feedback
mkdir -p /profile && \
chown tasks:nodejs /feedback && \
chown tasks:nodejs /profile

ENV FEEDBACK_DIR=/feedback
ENV PROFILE_PICTURE_DIRECTORY=/profile

COPY --from=builder --chown=tasks:nodejs /app/public ./public
COPY --from=builder --chown=tasks:nodejs /app/build/standalone ./
Expand Down
47 changes: 47 additions & 0 deletions web/api/gql/generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ export type Mutation = {
unassignTaskFromTeam: TaskType;
updateLocationNode: LocationNodeType;
updatePatient: PatientType;
updateProfilePicture: UserType;
updatePropertyDefinition: PropertyDefinitionType;
updateTask: TaskType;
waitPatient: PatientType;
Expand Down Expand Up @@ -230,6 +231,11 @@ export type MutationUpdatePatientArgs = {
};


export type MutationUpdateProfilePictureArgs = {
data: UpdateProfilePictureInput;
};


export type MutationUpdatePropertyDefinitionArgs = {
data: UpdatePropertyDefinitionInput;
id: Scalars['ID']['input'];
Expand Down Expand Up @@ -518,6 +524,10 @@ export type UpdatePatientInput = {
teamIds?: InputMaybe<Array<Scalars['ID']['input']>>;
};

export type UpdateProfilePictureInput = {
avatarUrl: Scalars['String']['input'];
};

export type UpdatePropertyDefinitionInput = {
allowedEntities?: InputMaybe<Array<PropertyEntity>>;
description?: InputMaybe<Scalars['String']['input']>;
Expand Down Expand Up @@ -864,6 +874,13 @@ export type UnassignTaskFromTeamMutationVariables = Exact<{

export type UnassignTaskFromTeamMutation = { __typename?: 'Mutation', unassignTaskFromTeam: { __typename?: 'TaskType', id: string, assigneeTeam?: { __typename?: 'LocationNodeType', id: string, title: string, kind: LocationType } | null } };

export type UpdateProfilePictureMutationVariables = Exact<{
data: UpdateProfilePictureInput;
}>;


export type UpdateProfilePictureMutation = { __typename?: 'Mutation', updateProfilePicture: { __typename?: 'UserType', id: string, username: string, name: string, email?: string | null, firstname?: string | null, lastname?: string | null, title?: string | null, avatarUrl?: string | null, lastOnline?: any | null, isOnline: boolean } };



export const GetAuditLogsDocument = `
Expand Down Expand Up @@ -2340,3 +2357,33 @@ export const useUnassignTaskFromTeamMutation = <
...options
}
)};

export const UpdateProfilePictureDocument = `
mutation UpdateProfilePicture($data: UpdateProfilePictureInput!) {
updateProfilePicture(data: $data) {
id
username
name
email
firstname
lastname
title
avatarUrl
lastOnline
isOnline
}
}
`;

export const useUpdateProfilePictureMutation = <
TError = unknown,
TContext = unknown
>(options?: UseMutationOptions<UpdateProfilePictureMutation, TError, UpdateProfilePictureMutationVariables, TContext>) => {

return useMutation<UpdateProfilePictureMutation, TError, UpdateProfilePictureMutationVariables, TContext>(
{
mutationKey: ['UpdateProfilePicture'],
mutationFn: (variables?: UpdateProfilePictureMutationVariables) => fetcher<UpdateProfilePictureMutation, UpdateProfilePictureMutationVariables>(UpdateProfilePictureDocument, variables)(),
...options
}
)};
15 changes: 15 additions & 0 deletions web/api/graphql/UserMutations.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
mutation UpdateProfilePicture($data: UpdateProfilePictureInput!) {
updateProfilePicture(data: $data) {
id
username
name
email
firstname
lastname
title
avatarUrl
lastOnline
isOnline
}
}

6 changes: 6 additions & 0 deletions web/components/UserInfoPopup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { fetcher } from '@/api/gql/fetcher'
import clsx from 'clsx'
import { AvatarStatusComponent } from '@/components/AvatarStatusComponent'
import { useTasksTranslation } from '@/i18n/useTasksTranslation'
import { SmartDate } from '@/utils/date'

const GET_USER_QUERY = `
query GetUser($id: ID!) {
Expand Down Expand Up @@ -112,6 +113,11 @@ export const UserInfoPopup: React.FC<UserInfoPopupProps> = ({ userId, isOpen, on
{user.isOnline ? 'Online' : 'Offline'}
</span>
</div>
{user.lastOnline && (
<div className="text-xs text-description mt-1">
<SmartDate date={new Date(user.lastOnline)} />
</div>
)}
</div>
</div>
) : (
Expand Down
Loading
Loading