mirror of
https://git.mirrors.martin98.com/https://github.com/langgenius/dify.git
synced 2025-08-14 05:55:52 +08:00
feat: Add ability to change profile avatar (#12642)
This commit is contained in:
parent
2f41bd495d
commit
f582d4a13e
@ -1,6 +1,6 @@
|
|||||||
from flask_restful import fields # type: ignore
|
from flask_restful import fields # type: ignore
|
||||||
|
|
||||||
from libs.helper import TimestampField
|
from libs.helper import AvatarUrlField, TimestampField
|
||||||
|
|
||||||
simple_account_fields = {"id": fields.String, "name": fields.String, "email": fields.String}
|
simple_account_fields = {"id": fields.String, "name": fields.String, "email": fields.String}
|
||||||
|
|
||||||
@ -8,6 +8,7 @@ account_fields = {
|
|||||||
"id": fields.String,
|
"id": fields.String,
|
||||||
"name": fields.String,
|
"name": fields.String,
|
||||||
"avatar": fields.String,
|
"avatar": fields.String,
|
||||||
|
"avatar_url": AvatarUrlField,
|
||||||
"email": fields.String,
|
"email": fields.String,
|
||||||
"is_password_set": fields.Boolean,
|
"is_password_set": fields.Boolean,
|
||||||
"interface_language": fields.String,
|
"interface_language": fields.String,
|
||||||
@ -22,6 +23,7 @@ account_with_role_fields = {
|
|||||||
"id": fields.String,
|
"id": fields.String,
|
||||||
"name": fields.String,
|
"name": fields.String,
|
||||||
"avatar": fields.String,
|
"avatar": fields.String,
|
||||||
|
"avatar_url": AvatarUrlField,
|
||||||
"email": fields.String,
|
"email": fields.String,
|
||||||
"last_login_at": TimestampField,
|
"last_login_at": TimestampField,
|
||||||
"last_active_at": TimestampField,
|
"last_active_at": TimestampField,
|
||||||
|
@ -41,6 +41,18 @@ class AppIconUrlField(fields.Raw):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class AvatarUrlField(fields.Raw):
|
||||||
|
def output(self, key, obj):
|
||||||
|
if obj is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
from models.account import Account
|
||||||
|
|
||||||
|
if isinstance(obj, Account) and obj.avatar is not None:
|
||||||
|
return file_helpers.get_signed_file_url(obj.avatar)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class TimestampField(fields.Raw):
|
class TimestampField(fields.Raw):
|
||||||
def format(self, value) -> int:
|
def format(self, value) -> int:
|
||||||
return int(value.timestamp())
|
return int(value.timestamp())
|
||||||
|
122
web/app/account/account-page/AvatarWithEdit.tsx
Normal file
122
web/app/account/account-page/AvatarWithEdit.tsx
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import type { Area } from 'react-easy-crop'
|
||||||
|
import React, { useCallback, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useContext } from 'use-context-selector'
|
||||||
|
import { RiPencilLine } from '@remixicon/react'
|
||||||
|
import { updateUserProfile } from '@/service/common'
|
||||||
|
import { ToastContext } from '@/app/components/base/toast'
|
||||||
|
import ImageInput, { type OnImageInput } from '@/app/components/base/app-icon-picker/ImageInput'
|
||||||
|
import Modal from '@/app/components/base/modal'
|
||||||
|
import Divider from '@/app/components/base/divider'
|
||||||
|
import Button from '@/app/components/base/button'
|
||||||
|
import Avatar, { type AvatarProps } from '@/app/components/base/avatar'
|
||||||
|
import { useLocalFileUploader } from '@/app/components/base/image-uploader/hooks'
|
||||||
|
import type { ImageFile } from '@/types/app'
|
||||||
|
import getCroppedImg from '@/app/components/base/app-icon-picker/utils'
|
||||||
|
import { DISABLE_UPLOAD_IMAGE_AS_ICON } from '@/config'
|
||||||
|
|
||||||
|
type InputImageInfo = { file: File } | { tempUrl: string; croppedAreaPixels: Area; fileName: string }
|
||||||
|
type AvatarWithEditProps = AvatarProps & { onSave?: () => void }
|
||||||
|
|
||||||
|
const AvatarWithEdit = ({ onSave, ...props }: AvatarWithEditProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { notify } = useContext(ToastContext)
|
||||||
|
|
||||||
|
const [inputImageInfo, setInputImageInfo] = useState<InputImageInfo>()
|
||||||
|
const [isShowAvatarPicker, setIsShowAvatarPicker] = useState(false)
|
||||||
|
const [uploading, setUploading] = useState(false)
|
||||||
|
|
||||||
|
const handleImageInput: OnImageInput = useCallback(async (isCropped: boolean, fileOrTempUrl: string | File, croppedAreaPixels?: Area, fileName?: string) => {
|
||||||
|
setInputImageInfo(
|
||||||
|
isCropped
|
||||||
|
? { tempUrl: fileOrTempUrl as string, croppedAreaPixels: croppedAreaPixels!, fileName: fileName! }
|
||||||
|
: { file: fileOrTempUrl as File },
|
||||||
|
)
|
||||||
|
}, [setInputImageInfo])
|
||||||
|
|
||||||
|
const handleSaveAvatar = useCallback(async (uploadedFileId: string) => {
|
||||||
|
try {
|
||||||
|
await updateUserProfile({ url: 'account/avatar', body: { avatar: uploadedFileId } })
|
||||||
|
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
|
||||||
|
setIsShowAvatarPicker(false)
|
||||||
|
onSave?.()
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
notify({ type: 'error', message: (e as Error).message })
|
||||||
|
}
|
||||||
|
}, [notify, onSave, t])
|
||||||
|
|
||||||
|
const { handleLocalFileUpload } = useLocalFileUploader({
|
||||||
|
limit: 3,
|
||||||
|
disabled: false,
|
||||||
|
onUpload: (imageFile: ImageFile) => {
|
||||||
|
if (imageFile.progress === 100) {
|
||||||
|
setUploading(false)
|
||||||
|
setInputImageInfo(undefined)
|
||||||
|
handleSaveAvatar(imageFile.fileId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error
|
||||||
|
if (imageFile.progress === -1)
|
||||||
|
setUploading(false)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleSelect = useCallback(async () => {
|
||||||
|
if (!inputImageInfo)
|
||||||
|
return
|
||||||
|
setUploading(true)
|
||||||
|
if ('file' in inputImageInfo) {
|
||||||
|
handleLocalFileUpload(inputImageInfo.file)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const blob = await getCroppedImg(inputImageInfo.tempUrl, inputImageInfo.croppedAreaPixels, inputImageInfo.fileName)
|
||||||
|
const file = new File([blob], inputImageInfo.fileName, { type: blob.type })
|
||||||
|
handleLocalFileUpload(file)
|
||||||
|
}, [handleLocalFileUpload, inputImageInfo])
|
||||||
|
|
||||||
|
if (DISABLE_UPLOAD_IMAGE_AS_ICON)
|
||||||
|
return <Avatar {...props} />
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<div className="relative group">
|
||||||
|
<Avatar {...props} />
|
||||||
|
<div
|
||||||
|
onClick={() => { setIsShowAvatarPicker(true) }}
|
||||||
|
className="absolute inset-0 bg-black bg-opacity-50 rounded-full opacity-0 group-hover:opacity-100 transition-opacity cursor-pointer flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<span className="text-white text-xs">
|
||||||
|
<RiPencilLine />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
closable
|
||||||
|
className="!w-[362px] !p-0"
|
||||||
|
isShow={isShowAvatarPicker}
|
||||||
|
onClose={() => setIsShowAvatarPicker(false)}
|
||||||
|
>
|
||||||
|
<ImageInput onImageInput={handleImageInput} cropShape='round' />
|
||||||
|
<Divider className='m-0' />
|
||||||
|
|
||||||
|
<div className='w-full flex items-center justify-center p-3 gap-2'>
|
||||||
|
<Button className='w-full' onClick={() => setIsShowAvatarPicker(false)}>
|
||||||
|
{t('app.iconPicker.cancel')}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button variant="primary" className='w-full' disabled={uploading || !inputImageInfo} loading={uploading} onClick={handleSelect}>
|
||||||
|
{t('app.iconPicker.ok')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AvatarWithEdit
|
@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import { useContext } from 'use-context-selector'
|
import { useContext } from 'use-context-selector'
|
||||||
import DeleteAccount from '../delete-account'
|
import DeleteAccount from '../delete-account'
|
||||||
import s from './index.module.css'
|
import s from './index.module.css'
|
||||||
|
import AvatarWithEdit from './AvatarWithEdit'
|
||||||
import Collapse from '@/app/components/header/account-setting/collapse'
|
import Collapse from '@/app/components/header/account-setting/collapse'
|
||||||
import type { IItem } from '@/app/components/header/account-setting/collapse'
|
import type { IItem } from '@/app/components/header/account-setting/collapse'
|
||||||
import Modal from '@/app/components/base/modal'
|
import Modal from '@/app/components/base/modal'
|
||||||
@ -13,7 +14,6 @@ import { updateUserProfile } from '@/service/common'
|
|||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
import { ToastContext } from '@/app/components/base/toast'
|
import { ToastContext } from '@/app/components/base/toast'
|
||||||
import AppIcon from '@/app/components/base/app-icon'
|
import AppIcon from '@/app/components/base/app-icon'
|
||||||
import Avatar from '@/app/components/base/avatar'
|
|
||||||
import { IS_CE_EDITION } from '@/config'
|
import { IS_CE_EDITION } from '@/config'
|
||||||
import Input from '@/app/components/base/input'
|
import Input from '@/app/components/base/input'
|
||||||
|
|
||||||
@ -133,7 +133,7 @@ export default function AccountPage() {
|
|||||||
<h4 className='title-2xl-semi-bold text-text-primary'>{t('common.account.myAccount')}</h4>
|
<h4 className='title-2xl-semi-bold text-text-primary'>{t('common.account.myAccount')}</h4>
|
||||||
</div>
|
</div>
|
||||||
<div className='mb-8 p-6 rounded-xl flex items-center bg-gradient-to-r from-background-gradient-bg-fill-chat-bg-2 to-background-gradient-bg-fill-chat-bg-1'>
|
<div className='mb-8 p-6 rounded-xl flex items-center bg-gradient-to-r from-background-gradient-bg-fill-chat-bg-2 to-background-gradient-bg-fill-chat-bg-1'>
|
||||||
<Avatar name={userProfile.name} size={64} />
|
<AvatarWithEdit avatar={userProfile.avatar_url} name={userProfile.name} onSave={ mutateUserProfile } size={64} />
|
||||||
<div className='ml-4'>
|
<div className='ml-4'>
|
||||||
<p className='system-xl-semibold text-text-primary'>{userProfile.name}</p>
|
<p className='system-xl-semibold text-text-primary'>{userProfile.name}</p>
|
||||||
<p className='system-xs-regular text-text-tertiary'>{userProfile.email}</p>
|
<p className='system-xs-regular text-text-tertiary'>{userProfile.email}</p>
|
||||||
|
@ -45,7 +45,7 @@ export default function AppSelector() {
|
|||||||
${open && 'bg-components-panel-bg-blur'}
|
${open && 'bg-components-panel-bg-blur'}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<Avatar name={userProfile.name} size={32} />
|
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} size={32} />
|
||||||
</Menu.Button>
|
</Menu.Button>
|
||||||
</div>
|
</div>
|
||||||
<Transition
|
<Transition
|
||||||
@ -71,7 +71,7 @@ export default function AppSelector() {
|
|||||||
<div className='system-md-medium text-text-primary break-all'>{userProfile.name}</div>
|
<div className='system-md-medium text-text-primary break-all'>{userProfile.name}</div>
|
||||||
<div className='system-xs-regular text-text-tertiary break-all'>{userProfile.email}</div>
|
<div className='system-xs-regular text-text-tertiary break-all'>{userProfile.email}</div>
|
||||||
</div>
|
</div>
|
||||||
<Avatar name={userProfile.name} size={32} />
|
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} size={32} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
@ -149,7 +149,7 @@ const ChatItem: FC<ChatItemProps> = ({
|
|||||||
suggestedQuestions={suggestedQuestions}
|
suggestedQuestions={suggestedQuestions}
|
||||||
onSend={doSend}
|
onSend={doSend}
|
||||||
showPromptLog
|
showPromptLog
|
||||||
questionIcon={<Avatar name={userProfile.name} size={40} />}
|
questionIcon={<Avatar avatar={userProfile.avatar_url} name={userProfile.name} size={40} />}
|
||||||
allToolIcons={allToolIcons}
|
allToolIcons={allToolIcons}
|
||||||
hideLogModal
|
hideLogModal
|
||||||
noSpacing
|
noSpacing
|
||||||
|
@ -175,7 +175,7 @@ const DebugWithSingleModel = forwardRef<DebugWithSingleModelRefType, DebugWithSi
|
|||||||
onRegenerate={doRegenerate}
|
onRegenerate={doRegenerate}
|
||||||
onStopResponding={handleStop}
|
onStopResponding={handleStop}
|
||||||
showPromptLog
|
showPromptLog
|
||||||
questionIcon={<Avatar name={userProfile.name} size={40} />}
|
questionIcon={<Avatar avatar={userProfile.avatar_url} name={userProfile.name} size={40} />}
|
||||||
allToolIcons={allToolIcons}
|
allToolIcons={allToolIcons}
|
||||||
onAnnotationEdited={handleAnnotationEdited}
|
onAnnotationEdited={handleAnnotationEdited}
|
||||||
onAnnotationAdded={handleAnnotationAdded}
|
onAnnotationAdded={handleAnnotationAdded}
|
||||||
|
@ -2,8 +2,7 @@
|
|||||||
|
|
||||||
import type { ChangeEvent, FC } from 'react'
|
import type { ChangeEvent, FC } from 'react'
|
||||||
import { createRef, useEffect, useState } from 'react'
|
import { createRef, useEffect, useState } from 'react'
|
||||||
import type { Area } from 'react-easy-crop'
|
import Cropper, { type Area, type CropperProps } from 'react-easy-crop'
|
||||||
import Cropper from 'react-easy-crop'
|
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
|
|
||||||
import { ImagePlus } from '../icons/src/vender/line/images'
|
import { ImagePlus } from '../icons/src/vender/line/images'
|
||||||
@ -18,11 +17,13 @@ export type OnImageInput = {
|
|||||||
|
|
||||||
type UploaderProps = {
|
type UploaderProps = {
|
||||||
className?: string
|
className?: string
|
||||||
|
cropShape?: CropperProps['cropShape']
|
||||||
onImageInput?: OnImageInput
|
onImageInput?: OnImageInput
|
||||||
}
|
}
|
||||||
|
|
||||||
const ImageInput: FC<UploaderProps> = ({
|
const ImageInput: FC<UploaderProps> = ({
|
||||||
className,
|
className,
|
||||||
|
cropShape,
|
||||||
onImageInput,
|
onImageInput,
|
||||||
}) => {
|
}) => {
|
||||||
const [inputImage, setInputImage] = useState<{ file: File; url: string }>()
|
const [inputImage, setInputImage] = useState<{ file: File; url: string }>()
|
||||||
@ -78,6 +79,7 @@ const ImageInput: FC<UploaderProps> = ({
|
|||||||
crop={crop}
|
crop={crop}
|
||||||
zoom={zoom}
|
zoom={zoom}
|
||||||
aspect={1}
|
aspect={1}
|
||||||
|
cropShape={cropShape}
|
||||||
onCropChange={setCrop}
|
onCropChange={setCrop}
|
||||||
onCropComplete={onCropComplete}
|
onCropComplete={onCropComplete}
|
||||||
onZoomChange={setZoom}
|
onZoomChange={setZoom}
|
||||||
|
@ -2,9 +2,9 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import cn from '@/utils/classnames'
|
import cn from '@/utils/classnames'
|
||||||
|
|
||||||
type AvatarProps = {
|
export type AvatarProps = {
|
||||||
name: string
|
name: string
|
||||||
avatar?: string
|
avatar: string | null
|
||||||
size?: number
|
size?: number
|
||||||
className?: string
|
className?: string
|
||||||
textClassName?: string
|
textClassName?: string
|
||||||
|
@ -74,7 +74,7 @@ const PermissionSelector = ({ disabled, permission, value, memberList, onChange,
|
|||||||
>
|
>
|
||||||
{permission === 'only_me' && (
|
{permission === 'only_me' && (
|
||||||
<div className={cn('flex items-center px-3 py-[6px] rounded-lg bg-gray-100 cursor-pointer hover:bg-gray-200', open && 'bg-gray-200', disabled && 'hover:!bg-gray-100 !cursor-default')}>
|
<div className={cn('flex items-center px-3 py-[6px] rounded-lg bg-gray-100 cursor-pointer hover:bg-gray-200', open && 'bg-gray-200', disabled && 'hover:!bg-gray-100 !cursor-default')}>
|
||||||
<Avatar name={userProfile.name} className='shrink-0 mr-2' size={24} />
|
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} className='shrink-0 mr-2' size={24} />
|
||||||
<div className='grow mr-2 text-gray-900 text-sm leading-5'>{t('datasetSettings.form.permissionsOnlyMe')}</div>
|
<div className='grow mr-2 text-gray-900 text-sm leading-5'>{t('datasetSettings.form.permissionsOnlyMe')}</div>
|
||||||
{!disabled && <RiArrowDownSLine className='shrink-0 w-4 h-4 text-gray-700' />}
|
{!disabled && <RiArrowDownSLine className='shrink-0 w-4 h-4 text-gray-700' />}
|
||||||
</div>
|
</div>
|
||||||
@ -106,7 +106,7 @@ const PermissionSelector = ({ disabled, permission, value, memberList, onChange,
|
|||||||
setOpen(false)
|
setOpen(false)
|
||||||
}}>
|
}}>
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
<Avatar name={userProfile.name} className='shrink-0 mr-2' size={24} />
|
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} className='shrink-0 mr-2' size={24} />
|
||||||
<div className='grow mr-2 text-gray-900 text-sm leading-5'>{t('datasetSettings.form.permissionsOnlyMe')}</div>
|
<div className='grow mr-2 text-gray-900 text-sm leading-5'>{t('datasetSettings.form.permissionsOnlyMe')}</div>
|
||||||
{permission === 'only_me' && <Check className='w-4 h-4 text-primary-600' />}
|
{permission === 'only_me' && <Check className='w-4 h-4 text-primary-600' />}
|
||||||
</div>
|
</div>
|
||||||
@ -149,7 +149,7 @@ const PermissionSelector = ({ disabled, permission, value, memberList, onChange,
|
|||||||
</div>
|
</div>
|
||||||
{showMe && (
|
{showMe && (
|
||||||
<div className='pl-3 pr-[10px] py-1 flex gap-2 items-center rounded-lg'>
|
<div className='pl-3 pr-[10px] py-1 flex gap-2 items-center rounded-lg'>
|
||||||
<Avatar name={userProfile.name} className='shrink-0' size={24} />
|
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} className='shrink-0' size={24} />
|
||||||
<div className='grow'>
|
<div className='grow'>
|
||||||
<div className='text-[13px] text-gray-700 font-medium leading-[18px] truncate'>
|
<div className='text-[13px] text-gray-700 font-medium leading-[18px] truncate'>
|
||||||
{userProfile.name}
|
{userProfile.name}
|
||||||
@ -162,7 +162,7 @@ const PermissionSelector = ({ disabled, permission, value, memberList, onChange,
|
|||||||
)}
|
)}
|
||||||
{filteredMemberList.map(member => (
|
{filteredMemberList.map(member => (
|
||||||
<div key={member.id} className='pl-3 pr-[10px] py-1 flex gap-2 items-center rounded-lg hover:bg-gray-100 cursor-pointer' onClick={() => selectMember(member)}>
|
<div key={member.id} className='pl-3 pr-[10px] py-1 flex gap-2 items-center rounded-lg hover:bg-gray-100 cursor-pointer' onClick={() => selectMember(member)}>
|
||||||
<Avatar name={member.name} className='shrink-0' size={24} />
|
<Avatar avatar={userProfile.avatar_url} name={member.name} className='shrink-0' size={24} />
|
||||||
<div className='grow'>
|
<div className='grow'>
|
||||||
<div className='text-[13px] text-gray-700 font-medium leading-[18px] truncate'>{member.name}</div>
|
<div className='text-[13px] text-gray-700 font-medium leading-[18px] truncate'>{member.name}</div>
|
||||||
<div className='text-xs text-gray-500 leading-[18px] truncate'>{member.email}</div>
|
<div className='text-xs text-gray-500 leading-[18px] truncate'>{member.email}</div>
|
||||||
|
@ -68,7 +68,7 @@ export default function AppSelector({ isMobile }: IAppSelector) {
|
|||||||
${open && 'bg-gray-200'}
|
${open && 'bg-gray-200'}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<Avatar name={userProfile.name} className='sm:mr-2 mr-0' size={32} />
|
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} className='sm:mr-2 mr-0' size={32} />
|
||||||
{!isMobile && <>
|
{!isMobile && <>
|
||||||
{userProfile.name}
|
{userProfile.name}
|
||||||
<RiArrowDownSLine className="w-3 h-3 ml-1 text-gray-700" />
|
<RiArrowDownSLine className="w-3 h-3 ml-1 text-gray-700" />
|
||||||
@ -92,7 +92,7 @@ export default function AppSelector({ isMobile }: IAppSelector) {
|
|||||||
>
|
>
|
||||||
<Menu.Item disabled>
|
<Menu.Item disabled>
|
||||||
<div className='flex flex-nowrap items-center px-4 py-[13px]'>
|
<div className='flex flex-nowrap items-center px-4 py-[13px]'>
|
||||||
<Avatar name={userProfile.name} size={36} className='mr-3' />
|
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} size={36} className='mr-3' />
|
||||||
<div className='grow'>
|
<div className='grow'>
|
||||||
<div className='system-md-medium text-text-primary break-all'>{userProfile.name}</div>
|
<div className='system-md-medium text-text-primary break-all'>{userProfile.name}</div>
|
||||||
<div className='system-xs-regular text-text-tertiary break-all'>{userProfile.email}</div>
|
<div className='system-xs-regular text-text-tertiary break-all'>{userProfile.email}</div>
|
||||||
|
@ -95,7 +95,7 @@ const MembersPage = () => {
|
|||||||
accounts.map(account => (
|
accounts.map(account => (
|
||||||
<div key={account.id} className='flex border-b border-divider-subtle'>
|
<div key={account.id} className='flex border-b border-divider-subtle'>
|
||||||
<div className='grow flex items-center py-2 px-3'>
|
<div className='grow flex items-center py-2 px-3'>
|
||||||
<Avatar size={24} className='mr-2' name={account.name} />
|
<Avatar avatar={account.avatar_url} size={24} className='mr-2' name={account.name} />
|
||||||
<div className=''>
|
<div className=''>
|
||||||
<div className='text-text-secondary system-sm-medium'>
|
<div className='text-text-secondary system-sm-medium'>
|
||||||
{account.name}
|
{account.name}
|
||||||
|
@ -22,6 +22,7 @@ export type UserProfileResponse = {
|
|||||||
name: string
|
name: string
|
||||||
email: string
|
email: string
|
||||||
avatar: string
|
avatar: string
|
||||||
|
avatar_url: string | null
|
||||||
is_password_set: boolean
|
is_password_set: boolean
|
||||||
interface_language?: string
|
interface_language?: string
|
||||||
interface_theme?: string
|
interface_theme?: string
|
||||||
@ -62,7 +63,7 @@ export type TenantInfoResponse = {
|
|||||||
trial_end_reason: null | 'trial_exceeded' | 'using_custom'
|
trial_end_reason: null | 'trial_exceeded' | 'using_custom'
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Member = Pick<UserProfileResponse, 'id' | 'name' | 'email' | 'last_login_at' | 'last_active_at' | 'created_at'> & {
|
export type Member = Pick<UserProfileResponse, 'id' | 'name' | 'email' | 'last_login_at' | 'last_active_at' | 'created_at' | 'avatar_url'> & {
|
||||||
avatar: string
|
avatar: string
|
||||||
status: 'pending' | 'active' | 'banned' | 'closed'
|
status: 'pending' | 'active' | 'banned' | 'closed'
|
||||||
role: 'owner' | 'admin' | 'editor' | 'normal' | 'dataset_operator'
|
role: 'owner' | 'admin' | 'editor' | 'normal' | 'dataset_operator'
|
||||||
|
Loading…
x
Reference in New Issue
Block a user