Add user profile image

This commit is contained in:
jeffvli
2022-11-13 20:18:23 -08:00
parent 14c22c63a0
commit 1babcc40ee
19 changed files with 1457 additions and 118 deletions
+9
View File
@@ -134,7 +134,16 @@ export type RelatedServerPermission = {
type: ServerPermissionType;
};
export type ServerFile = {
id: string;
mimetype: string;
name: string;
path: string;
type: string;
};
export type User = {
avatar: ServerFile | null;
createdAt: string;
displayName?: string;
enabled: boolean;
+20 -1
View File
@@ -1,3 +1,4 @@
import { FileWithPath } from '@mantine/dropzone';
import { BaseResponse, NullResponse, User } from '@/renderer/api/types';
import { ax } from '@/renderer/lib/axios';
@@ -15,6 +16,8 @@ const getUserList = async (signal?: AbortSignal) => {
};
export type CreateUserBody = {
displayName?: string;
image?: FileWithPath;
password: string;
username: string;
};
@@ -32,9 +35,25 @@ const deleteUser = async (query: { userId: string }) => {
export type UpdateUserBody = Partial<CreateUserBody>;
const updateUser = async (query: { userId: string }, body: UpdateUserBody) => {
if (body.image) {
const formData = new FormData();
formData.append('image', body.image);
if (body.username) formData.append('username', body.username);
if (body.displayName) formData.append('displayName', body.displayName);
const { data } = await ax.patch<UserDetailResponse>(
`/users/${query.userId}`,
formData,
{ headers: { 'Content-Type': 'multipart/form-data' } }
);
return data;
}
const { data } = await ax.patch<UserDetailResponse>(
`/users/${query.userId}`,
body
body,
{}
);
return data;
};
@@ -63,7 +63,7 @@ export const AddUserForm = ({ onCancel }: AddUserFormProps) => {
return (
<form onSubmit={handleAddUser}>
<Stack ref={focusTrapRef}>
<Stack ref={focusTrapRef} m={5}>
<TextInput
data-autofocus
required
@@ -1,6 +1,8 @@
import { Stack, Group } from '@mantine/core';
import { Stack, Group, Grid, Image } from '@mantine/core';
import { FileWithPath, IMAGE_MIME_TYPE } from '@mantine/dropzone';
import { useForm } from '@mantine/form';
import { useClipboard, useFocusTrap } from '@mantine/hooks';
import { RiImage2Line } from 'react-icons/ri';
import { User } from '@/renderer/api/types';
import {
Button,
@@ -8,6 +10,8 @@ import {
TextInput,
Switch,
toast,
Dropzone,
Text,
} from '@/renderer/components';
import { usePermissions } from '@/renderer/features/shared';
import { useUpdateUser } from '@/renderer/features/users/mutations/update-user';
@@ -31,6 +35,7 @@ export const EditUserForm = ({
const form = useForm({
initialValues: {
displayName: user?.displayName || '',
image: null as FileWithPath | null,
isAdmin: user?.isAdmin || false,
password: '',
repeatPassword: '',
@@ -58,6 +63,7 @@ export const EditUserForm = ({
const body = {
...values,
displayName: values.displayName || undefined,
image: values.image || undefined,
password: values.password || undefined,
repeatPassword: values.repeatPassword || undefined,
};
@@ -81,62 +87,126 @@ export const EditUserForm = ({
);
});
const getPreview = () => {
if (form.values.image instanceof File) {
const imageUrl = URL.createObjectURL(form.values.image);
return (
<Image
height={150}
imageProps={{ onLoad: () => URL.revokeObjectURL(imageUrl) }}
radius={100}
src={imageUrl}
sx={{ objectFit: 'contain' }}
width={150}
/>
);
}
return null;
};
const handleRemoveImage = () => form.setFieldValue('image', null);
return (
<form onSubmit={handleUpdateUser}>
<Stack ref={focusTrapRef} spacing="xl">
<TextInput
data-autofocus
label="Username"
{...form.getInputProps('username')}
/>
<TextInput
label="Display name"
{...form.getInputProps('displayName')}
/>
<PasswordInput label="Password" {...form.getInputProps('password')} />
{repeatPassword && (
<PasswordInput
label="Repeat password"
{...form.getInputProps('repeatPassword')}
/>
)}
<Group position="apart">
{permissions.isAdmin && !user?.isSuperAdmin ? (
<Group>
Admin
<Switch
{...form.getInputProps('isAdmin', { type: 'checkbox' })}
<Stack ref={focusTrapRef} spacing="xs" sx={{ margin: '5px' }}>
<Grid>
<Grid.Col span={4}>
<Stack spacing="xs" sx={{ height: '100%' }}>
<Dropzone
accept={IMAGE_MIME_TYPE}
maxSize={3 * 1024 ** 2}
multiple={false}
onDrop={(file) => form.setFieldValue('image', file[0])}
onReject={(err) =>
toast.error({
message: `${err[0].errors[0].message}`,
title: 'Invalid Image',
})
}
>
<Group>
<Dropzone.Idle>
{form.values.image ? (
<Group position="center">{getPreview()}</Group>
) : (
<Group position="center">
<RiImage2Line color="var(--primary-color)" size={30} />
<Stack spacing="xs">
<Text>Profile image</Text>
<Text>Max size: 5MB</Text>
</Stack>
</Group>
)}
</Dropzone.Idle>
</Group>
</Dropzone>
<Button compact variant="subtle" onClick={handleRemoveImage}>
Remove image
</Button>
</Stack>
</Grid.Col>
<Grid.Col span={8}>
<Stack>
<TextInput
data-autofocus
label="Username"
{...form.getInputProps('username')}
/>
</Group>
) : (
<Group />
)}
{!repeatPassword && (
<Button
compact
sx={{ height: '1.5rem' }}
variant="subtle"
onClick={handleGeneratePassword}
>
Generate password
</Button>
)}
</Group>
<TextInput
label="Display Name"
{...form.getInputProps('displayName')}
/>
<PasswordInput
label="Password"
{...form.getInputProps('password')}
/>
{repeatPassword && (
<PasswordInput
label="Repeat Password"
{...form.getInputProps('repeatPassword')}
/>
)}
<Group mt={10} position="right">
<Button variant="subtle" onClick={onCancel}>
Cancel
</Button>
<Button
disabled={!isSubmitValid()}
loading={updateUserMutation.isLoading}
type="submit"
variant="filled"
>
Submit
</Button>
</Group>
<Group position="apart">
{permissions.isAdmin && !user?.isSuperAdmin ? (
<Group>
Admin
<Switch
{...form.getInputProps('isAdmin', { type: 'checkbox' })}
/>
</Group>
) : (
<Group />
)}
{!repeatPassword && (
<Button
compact
sx={{ height: '1.5rem' }}
variant="subtle"
onClick={handleGeneratePassword}
>
Generate password
</Button>
)}
</Group>
<Group mt={10} position="right">
<Button variant="subtle" onClick={onCancel}>
Cancel
</Button>
<Button
disabled={!isSubmitValid()}
loading={updateUserMutation.isLoading}
type="submit"
variant="filled"
>
Submit
</Button>
</Group>
</Stack>
</Grid.Col>
</Grid>
</Stack>
</form>
);
@@ -56,6 +56,7 @@ export const UserList = () => {
},
modal: 'base',
overflow: 'inside',
size: 'lg',
title: `Edit User`,
transition: 'slide-down',
});
@@ -110,7 +111,7 @@ export const UserList = () => {
}}
>
<Group>
<Avatar radius="xl" />
<Avatar radius="xl" src={u.avatarUrl} />
<Stack spacing="xs">
<Text overflow="hidden">
{u.username}
@@ -3,11 +3,23 @@ import { api } from '@/renderer/api';
import { queryKeys } from '@/renderer/api/query-keys';
import { UserListResponse } from '@/renderer/api/users.api';
import { QueryOptions } from '@/renderer/lib/react-query';
import { useAuthStore } from '@/renderer/store';
import { getFileUrl } from '@/renderer/utils';
export const useUserList = (options?: QueryOptions<UserListResponse>) => {
const serverUrl = useAuthStore((state) => state.serverUrl);
const query = useQuery({
queryFn: () => api.users.getUserList(),
queryKey: queryKeys.users.list(),
select: (data) => {
const users = data.data.map((user) => ({
...user,
avatarUrl: getFileUrl(serverUrl, user?.avatar),
}));
return { ...data, data: users };
},
...options,
});
+6
View File
@@ -0,0 +1,6 @@
import { ServerFile } from '@/renderer/api/types';
export const getFileUrl = (serverUrl: string, file: ServerFile | null) => {
if (!file) return undefined;
return `${serverUrl}/${file.path}`;
};
+1
View File
@@ -6,3 +6,4 @@ export * from './server-folder-auth';
export * from './set-local-storage-setttings';
export * from './constrain-sidebar-width';
export * from './title-case';
export * from './get-file-url';