| @@ -8,27 +8,24 @@ import Header from '@/app/components/header' | |||
| import { EventEmitterContextProvider } from '@/context/event-emitter' | |||
| import { ProviderContextProvider } from '@/context/provider-context' | |||
| import { ModalContextProvider } from '@/context/modal-context' | |||
| import { TanstackQueryIniter } from '@/context/query-client' | |||
| const Layout = ({ children }: { children: ReactNode }) => { | |||
| return ( | |||
| <> | |||
| <GA gaType={GaType.admin} /> | |||
| <SwrInitor> | |||
| <TanstackQueryIniter> | |||
| <AppContextProvider> | |||
| <EventEmitterContextProvider> | |||
| <ProviderContextProvider> | |||
| <ModalContextProvider> | |||
| <HeaderWrapper> | |||
| <Header /> | |||
| </HeaderWrapper> | |||
| {children} | |||
| </ModalContextProvider> | |||
| </ProviderContextProvider> | |||
| </EventEmitterContextProvider> | |||
| </AppContextProvider> | |||
| </TanstackQueryIniter> | |||
| <AppContextProvider> | |||
| <EventEmitterContextProvider> | |||
| <ProviderContextProvider> | |||
| <ModalContextProvider> | |||
| <HeaderWrapper> | |||
| <Header /> | |||
| </HeaderWrapper> | |||
| {children} | |||
| </ModalContextProvider> | |||
| </ProviderContextProvider> | |||
| </EventEmitterContextProvider> | |||
| </AppContextProvider> | |||
| </SwrInitor> | |||
| </> | |||
| ) | |||
| @@ -3,11 +3,11 @@ import { useState } from 'react' | |||
| import { useTranslation } from 'react-i18next' | |||
| import { useContext } from 'use-context-selector' | |||
| import DeleteAccount from '../delete-account' | |||
| import s from './index.module.css' | |||
| import Collapse 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 Confirm from '@/app/components/base/confirm' | |||
| import Button from '@/app/components/base/button' | |||
| import { updateUserProfile } from '@/service/common' | |||
| import { useAppContext } from '@/context/app-context' | |||
| @@ -296,37 +296,9 @@ export default function AccountPage() { | |||
| } | |||
| { | |||
| showDeleteAccountModal && ( | |||
| <Confirm | |||
| isShow | |||
| <DeleteAccount | |||
| onCancel={() => setShowDeleteAccountModal(false)} | |||
| onConfirm={() => setShowDeleteAccountModal(false)} | |||
| showCancel={false} | |||
| type='warning' | |||
| title={t('common.account.delete')} | |||
| content={ | |||
| <> | |||
| <div className='my-1 text-text-destructive body-md-medium'> | |||
| {t('common.account.deleteTip')} | |||
| </div> | |||
| <div className='mt-3 text-sm leading-5'> | |||
| <span>{t('common.account.deleteConfirmTip')}</span> | |||
| <a | |||
| className='text-text-accent cursor' | |||
| href={`mailto:support@dify.ai?subject=Delete Account Request&body=Delete Account: ${userProfile.email}`} | |||
| target='_blank' | |||
| rel='noreferrer noopener' | |||
| onClick={(e) => { | |||
| e.preventDefault() | |||
| window.location.href = e.currentTarget.href | |||
| }} | |||
| > | |||
| support@dify.ai | |||
| </a> | |||
| </div> | |||
| <div className='my-2 px-3 py-2 rounded-lg bg-components-input-bg-active border border-components-input-border-active system-sm-regular text-components-input-text-filled'>{`${t('common.account.delete')}: ${userProfile.email}`}</div> | |||
| </> | |||
| } | |||
| confirmText={t('common.operation.ok') as string} | |||
| /> | |||
| ) | |||
| } | |||
| @@ -0,0 +1,48 @@ | |||
| 'use client' | |||
| import { useTranslation } from 'react-i18next' | |||
| import { useCallback, useState } from 'react' | |||
| import Link from 'next/link' | |||
| import { useSendDeleteAccountEmail } from '../state' | |||
| import { useAppContext } from '@/context/app-context' | |||
| import Input from '@/app/components/base/input' | |||
| import Button from '@/app/components/base/button' | |||
| type DeleteAccountProps = { | |||
| onCancel: () => void | |||
| onConfirm: () => void | |||
| } | |||
| export default function CheckEmail(props: DeleteAccountProps) { | |||
| const { t } = useTranslation() | |||
| const { userProfile } = useAppContext() | |||
| const [userInputEmail, setUserInputEmail] = useState('') | |||
| const { isPending: isSendingEmail, mutateAsync: getDeleteEmailVerifyCode } = useSendDeleteAccountEmail() | |||
| const handleConfirm = useCallback(async () => { | |||
| try { | |||
| const ret = await getDeleteEmailVerifyCode() | |||
| if (ret.result === 'success') | |||
| props.onConfirm() | |||
| } | |||
| catch (error) { console.error(error) } | |||
| }, [getDeleteEmailVerifyCode, props]) | |||
| return <> | |||
| <div className='py-1 text-text-destructive body-md-medium'> | |||
| {t('common.account.deleteTip')} | |||
| </div> | |||
| <div className='pt-1 pb-2 text-text-secondary body-md-regular'> | |||
| {t('common.account.deletePrivacyLinkTip')} | |||
| <Link href='https://dify.ai/privacy' className='text-text-accent'>{t('common.account.deletePrivacyLink')}</Link> | |||
| </div> | |||
| <label className='mt-3 mb-1 h-6 flex items-center system-sm-semibold text-text-secondary'>{t('common.account.deleteLabel')}</label> | |||
| <Input placeholder={t('common.account.deletePlaceholder') as string} onChange={(e) => { | |||
| setUserInputEmail(e.target.value) | |||
| }} /> | |||
| <div className='w-full flex flex-col mt-3 gap-2'> | |||
| <Button className='w-full' disabled={userInputEmail !== userProfile.email || isSendingEmail} loading={isSendingEmail} variant='primary' onClick={handleConfirm}>{t('common.account.sendVerificationButton')}</Button> | |||
| <Button className='w-full' onClick={props.onCancel}>{t('common.operation.cancel')}</Button> | |||
| </div> | |||
| </> | |||
| } | |||
| @@ -0,0 +1,68 @@ | |||
| 'use client' | |||
| import { useTranslation } from 'react-i18next' | |||
| import { useCallback, useState } from 'react' | |||
| import { useRouter } from 'next/navigation' | |||
| import { useDeleteAccountFeedback } from '../state' | |||
| import { useAppContext } from '@/context/app-context' | |||
| import Button from '@/app/components/base/button' | |||
| import CustomDialog from '@/app/components/base/dialog' | |||
| import Textarea from '@/app/components/base/textarea' | |||
| import Toast from '@/app/components/base/toast' | |||
| import { logout } from '@/service/common' | |||
| type DeleteAccountProps = { | |||
| onCancel: () => void | |||
| onConfirm: () => void | |||
| } | |||
| export default function FeedBack(props: DeleteAccountProps) { | |||
| const { t } = useTranslation() | |||
| const { userProfile } = useAppContext() | |||
| const router = useRouter() | |||
| const [userFeedback, setUserFeedback] = useState('') | |||
| const { isPending, mutateAsync: sendFeedback } = useDeleteAccountFeedback() | |||
| const handleSuccess = useCallback(async () => { | |||
| try { | |||
| await logout({ | |||
| url: '/logout', | |||
| params: {}, | |||
| }) | |||
| localStorage.removeItem('refresh_token') | |||
| localStorage.removeItem('console_token') | |||
| router.push('/signin') | |||
| Toast.notify({ type: 'info', message: t('common.account.deleteSuccessTip') }) | |||
| } | |||
| catch (error) { console.error(error) } | |||
| }, [router, t]) | |||
| const handleSubmit = useCallback(async () => { | |||
| try { | |||
| await sendFeedback({ feedback: userFeedback, email: userProfile.email }) | |||
| props.onConfirm() | |||
| await handleSuccess() | |||
| } | |||
| catch (error) { console.error(error) } | |||
| }, [handleSuccess, userFeedback, sendFeedback, userProfile, props]) | |||
| const handleSkip = useCallback(() => { | |||
| props.onCancel() | |||
| handleSuccess() | |||
| }, [handleSuccess, props]) | |||
| return <CustomDialog | |||
| show={true} | |||
| onClose={props.onCancel} | |||
| title={t('common.account.feedbackTitle')} | |||
| className="max-w-[480px]" | |||
| footer={false} | |||
| > | |||
| <label className='mt-3 mb-1 flex items-center system-sm-semibold text-text-secondary'>{t('common.account.feedbackLabel')}</label> | |||
| <Textarea rows={6} value={userFeedback} placeholder={t('common.account.feedbackPlaceholder') as string} onChange={(e) => { | |||
| setUserFeedback(e.target.value) | |||
| }} /> | |||
| <div className='w-full flex flex-col mt-3 gap-2'> | |||
| <Button className='w-full' loading={isPending} variant='primary' onClick={handleSubmit}>{t('common.operation.submit')}</Button> | |||
| <Button className='w-full' onClick={handleSkip}>{t('common.operation.skip')}</Button> | |||
| </div> | |||
| </CustomDialog> | |||
| } | |||
| @@ -0,0 +1,55 @@ | |||
| 'use client' | |||
| import { useTranslation } from 'react-i18next' | |||
| import { useCallback, useEffect, useState } from 'react' | |||
| import Link from 'next/link' | |||
| import { useAccountDeleteStore, useConfirmDeleteAccount, useSendDeleteAccountEmail } from '../state' | |||
| import Input from '@/app/components/base/input' | |||
| import Button from '@/app/components/base/button' | |||
| import Countdown from '@/app/components/signin/countdown' | |||
| const CODE_EXP = /[A-Za-z\d]{6}/gi | |||
| type DeleteAccountProps = { | |||
| onCancel: () => void | |||
| onConfirm: () => void | |||
| } | |||
| export default function VerifyEmail(props: DeleteAccountProps) { | |||
| const { t } = useTranslation() | |||
| const emailToken = useAccountDeleteStore(state => state.sendEmailToken) | |||
| const [verificationCode, setVerificationCode] = useState<string>() | |||
| const [shouldButtonDisabled, setShouldButtonDisabled] = useState(true) | |||
| const { mutate: sendEmail } = useSendDeleteAccountEmail() | |||
| const { isPending: isDeleting, mutateAsync: confirmDeleteAccount } = useConfirmDeleteAccount() | |||
| useEffect(() => { | |||
| setShouldButtonDisabled(!(verificationCode && CODE_EXP.test(verificationCode)) || isDeleting) | |||
| }, [verificationCode, isDeleting]) | |||
| const handleConfirm = useCallback(async () => { | |||
| try { | |||
| const ret = await confirmDeleteAccount({ code: verificationCode!, token: emailToken }) | |||
| if (ret.result === 'success') | |||
| props.onConfirm() | |||
| } | |||
| catch (error) { console.error(error) } | |||
| }, [emailToken, verificationCode, confirmDeleteAccount, props]) | |||
| return <> | |||
| <div className='pt-1 text-text-destructive body-md-medium'> | |||
| {t('common.account.deleteTip')} | |||
| </div> | |||
| <div className='pt-1 pb-2 text-text-secondary body-md-regular'> | |||
| {t('common.account.deletePrivacyLinkTip')} | |||
| <Link href='https://dify.ai/privacy' className='text-text-accent'>{t('common.account.deletePrivacyLink')}</Link> | |||
| </div> | |||
| <label className='mt-3 mb-1 h-6 flex items-center system-sm-semibold text-text-secondary'>{t('common.account.verificationLabel')}</label> | |||
| <Input minLength={6} maxLength={6} placeholder={t('common.account.verificationPlaceholder') as string} onChange={(e) => { | |||
| setVerificationCode(e.target.value) | |||
| }} /> | |||
| <div className='w-full flex flex-col mt-3 gap-2'> | |||
| <Button className='w-full' disabled={shouldButtonDisabled} loading={isDeleting} variant='warning' onClick={handleConfirm}>{t('common.account.permanentlyDeleteButton')}</Button> | |||
| <Button className='w-full' onClick={props.onCancel}>{t('common.operation.cancel')}</Button> | |||
| <Countdown onResend={sendEmail} /> | |||
| </div> | |||
| </> | |||
| } | |||
| @@ -0,0 +1,44 @@ | |||
| 'use client' | |||
| import { useTranslation } from 'react-i18next' | |||
| import { useCallback, useState } from 'react' | |||
| import CheckEmail from './components/check-email' | |||
| import VerifyEmail from './components/verify-email' | |||
| import FeedBack from './components/feed-back' | |||
| import CustomDialog from '@/app/components/base/dialog' | |||
| import { COUNT_DOWN_KEY, COUNT_DOWN_TIME_MS } from '@/app/components/signin/countdown' | |||
| type DeleteAccountProps = { | |||
| onCancel: () => void | |||
| onConfirm: () => void | |||
| } | |||
| export default function DeleteAccount(props: DeleteAccountProps) { | |||
| const { t } = useTranslation() | |||
| const [showVerifyEmail, setShowVerifyEmail] = useState(false) | |||
| const [showFeedbackDialog, setShowFeedbackDialog] = useState(false) | |||
| const handleEmailCheckSuccess = useCallback(async () => { | |||
| try { | |||
| setShowVerifyEmail(true) | |||
| localStorage.setItem(COUNT_DOWN_KEY, `${COUNT_DOWN_TIME_MS}`) | |||
| } | |||
| catch (error) { console.error(error) } | |||
| }, []) | |||
| if (showFeedbackDialog) | |||
| return <FeedBack onCancel={props.onCancel} onConfirm={props.onConfirm} /> | |||
| return <CustomDialog | |||
| show={true} | |||
| onClose={props.onCancel} | |||
| title={t('common.account.delete')} | |||
| className="max-w-[480px]" | |||
| footer={false} | |||
| > | |||
| {!showVerifyEmail && <CheckEmail onCancel={props.onCancel} onConfirm={handleEmailCheckSuccess} />} | |||
| {showVerifyEmail && <VerifyEmail onCancel={props.onCancel} onConfirm={() => { | |||
| setShowFeedbackDialog(true) | |||
| }} />} | |||
| </CustomDialog> | |||
| } | |||
| @@ -0,0 +1,39 @@ | |||
| import { useMutation } from '@tanstack/react-query' | |||
| import { create } from 'zustand' | |||
| import { sendDeleteAccountCode, submitDeleteAccountFeedback, verifyDeleteAccountCode } from '@/service/common' | |||
| type State = { | |||
| sendEmailToken: string | |||
| setSendEmailToken: (token: string) => void | |||
| } | |||
| export const useAccountDeleteStore = create<State>(set => ({ | |||
| sendEmailToken: '', | |||
| setSendEmailToken: (token: string) => set({ sendEmailToken: token }), | |||
| })) | |||
| export function useSendDeleteAccountEmail() { | |||
| const updateEmailToken = useAccountDeleteStore(state => state.setSendEmailToken) | |||
| return useMutation({ | |||
| mutationKey: ['delete-account'], | |||
| mutationFn: sendDeleteAccountCode, | |||
| onSuccess: (ret) => { | |||
| if (ret.result === 'success') | |||
| updateEmailToken(ret.data) | |||
| }, | |||
| }) | |||
| } | |||
| export function useConfirmDeleteAccount() { | |||
| return useMutation({ | |||
| mutationKey: ['confirm-delete-account'], | |||
| mutationFn: verifyDeleteAccountCode, | |||
| }) | |||
| } | |||
| export function useDeleteAccountFeedback() { | |||
| return useMutation({ | |||
| mutationKey: ['delete-account-feedback'], | |||
| mutationFn: submitDeleteAccountFeedback, | |||
| }) | |||
| } | |||
| @@ -47,7 +47,7 @@ const CustomDialog = ({ | |||
| </Transition.Child> | |||
| <div className="fixed inset-0 overflow-y-auto"> | |||
| <div className="flex items-center justify-center min-h-full p-4 text-center"> | |||
| <div className="flex items-center justify-center min-h-full"> | |||
| <Transition.Child | |||
| as={Fragment} | |||
| enter="ease-out duration-300" | |||
| @@ -57,20 +57,20 @@ const CustomDialog = ({ | |||
| leaveFrom="opacity-100 scale-100" | |||
| leaveTo="opacity-0 scale-95" | |||
| > | |||
| <Dialog.Panel className={classNames('w-full max-w-[800px] p-0 overflow-hidden text-left text-gray-900 align-middle transition-all transform bg-white shadow-xl rounded-2xl', className)}> | |||
| <Dialog.Panel className={classNames('w-full max-w-[800px] p-6 overflow-hidden transition-all transform bg-components-panel-bg border-[0.5px] border-components-panel-border shadow-xl rounded-2xl', className)}> | |||
| {Boolean(title) && ( | |||
| <Dialog.Title | |||
| as={titleAs || 'h3'} | |||
| className={classNames('px-8 py-6 text-lg font-medium leading-6 text-gray-900', titleClassName)} | |||
| className={classNames('pr-8 pb-3 title-2xl-semi-bold text-text-primary', titleClassName)} | |||
| > | |||
| {title} | |||
| </Dialog.Title> | |||
| )} | |||
| <div className={classNames('px-8 text-lg font-medium leading-6', bodyClassName)}> | |||
| <div className={classNames(bodyClassName)}> | |||
| {children} | |||
| </div> | |||
| {Boolean(footer) && ( | |||
| <div className={classNames('flex items-center justify-end gap-2 px-8 py-6', footerClassName)}> | |||
| <div className={classNames('flex items-center justify-end gap-2 px-6 pb-6 pt-3', footerClassName)}> | |||
| {footer} | |||
| </div> | |||
| )} | |||
| @@ -1,9 +0,0 @@ | |||
| .modal { | |||
| padding: 24px 32px !important; | |||
| width: 400px !important; | |||
| } | |||
| .bg { | |||
| background: linear-gradient(180deg, rgba(217, 45, 32, 0.05) 0%, rgba(217, 45, 32, 0.00) 24.02%), #F9FAFB; | |||
| } | |||
| @@ -1,282 +0,0 @@ | |||
| 'use client' | |||
| import { useState } from 'react' | |||
| import { useTranslation } from 'react-i18next' | |||
| import { useContext, useContextSelector } from 'use-context-selector' | |||
| import Collapse from '../collapse' | |||
| import type { IItem } from '../collapse' | |||
| import s from './index.module.css' | |||
| import classNames from '@/utils/classnames' | |||
| import Modal from '@/app/components/base/modal' | |||
| import Confirm from '@/app/components/base/confirm' | |||
| import Button from '@/app/components/base/button' | |||
| import { updateUserProfile } from '@/service/common' | |||
| import AppContext, { useAppContext } from '@/context/app-context' | |||
| import { ToastContext } from '@/app/components/base/toast' | |||
| import AppIcon from '@/app/components/base/app-icon' | |||
| import Avatar from '@/app/components/base/avatar' | |||
| import { IS_CE_EDITION } from '@/config' | |||
| const titleClassName = ` | |||
| text-sm font-medium text-gray-900 | |||
| ` | |||
| const descriptionClassName = ` | |||
| mt-1 text-xs font-normal text-gray-500 | |||
| ` | |||
| const inputClassName = ` | |||
| mt-2 w-full px-3 py-2 bg-gray-100 rounded | |||
| text-sm font-normal text-gray-800 | |||
| ` | |||
| const validPassword = /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/ | |||
| export default function AccountPage() { | |||
| const { t } = useTranslation() | |||
| const { mutateUserProfile, userProfile, apps } = useAppContext() | |||
| const { notify } = useContext(ToastContext) | |||
| const [editNameModalVisible, setEditNameModalVisible] = useState(false) | |||
| const [editName, setEditName] = useState('') | |||
| const [editing, setEditing] = useState(false) | |||
| const [editPasswordModalVisible, setEditPasswordModalVisible] = useState(false) | |||
| const [currentPassword, setCurrentPassword] = useState('') | |||
| const [password, setPassword] = useState('') | |||
| const [confirmPassword, setConfirmPassword] = useState('') | |||
| const [showDeleteAccountModal, setShowDeleteAccountModal] = useState(false) | |||
| const systemFeatures = useContextSelector(AppContext, state => state.systemFeatures) | |||
| const handleEditName = () => { | |||
| setEditNameModalVisible(true) | |||
| setEditName(userProfile.name) | |||
| } | |||
| const handleSaveName = async () => { | |||
| try { | |||
| setEditing(true) | |||
| await updateUserProfile({ url: 'account/name', body: { name: editName } }) | |||
| notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') }) | |||
| mutateUserProfile() | |||
| setEditNameModalVisible(false) | |||
| setEditing(false) | |||
| } | |||
| catch (e) { | |||
| notify({ type: 'error', message: (e as Error).message }) | |||
| setEditNameModalVisible(false) | |||
| setEditing(false) | |||
| } | |||
| } | |||
| const showErrorMessage = (message: string) => { | |||
| notify({ | |||
| type: 'error', | |||
| message, | |||
| }) | |||
| } | |||
| const valid = () => { | |||
| if (!password.trim()) { | |||
| showErrorMessage(t('login.error.passwordEmpty')) | |||
| return false | |||
| } | |||
| if (!validPassword.test(password)) { | |||
| showErrorMessage(t('login.error.passwordInvalid')) | |||
| return false | |||
| } | |||
| if (password !== confirmPassword) { | |||
| showErrorMessage(t('common.account.notEqual')) | |||
| return false | |||
| } | |||
| return true | |||
| } | |||
| const resetPasswordForm = () => { | |||
| setCurrentPassword('') | |||
| setPassword('') | |||
| setConfirmPassword('') | |||
| } | |||
| const handleSavePassword = async () => { | |||
| if (!valid()) | |||
| return | |||
| try { | |||
| setEditing(true) | |||
| await updateUserProfile({ | |||
| url: 'account/password', | |||
| body: { | |||
| password: currentPassword, | |||
| new_password: password, | |||
| repeat_new_password: confirmPassword, | |||
| }, | |||
| }) | |||
| notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') }) | |||
| mutateUserProfile() | |||
| setEditPasswordModalVisible(false) | |||
| resetPasswordForm() | |||
| setEditing(false) | |||
| } | |||
| catch (e) { | |||
| notify({ type: 'error', message: (e as Error).message }) | |||
| setEditPasswordModalVisible(false) | |||
| setEditing(false) | |||
| } | |||
| } | |||
| const renderAppItem = (item: IItem) => { | |||
| return ( | |||
| <div className='flex px-3 py-1'> | |||
| <div className='mr-3'> | |||
| <AppIcon size='tiny' /> | |||
| </div> | |||
| <div className='mt-[3px] text-xs font-medium text-gray-700 leading-[18px]'>{item.name}</div> | |||
| </div> | |||
| ) | |||
| } | |||
| return ( | |||
| <> | |||
| <div className='mb-8'> | |||
| <div className={titleClassName}>{t('common.account.avatar')}</div> | |||
| <Avatar name={userProfile.name} size={64} className='mt-2' /> | |||
| </div> | |||
| <div className='mb-8'> | |||
| <div className={titleClassName}>{t('common.account.name')}</div> | |||
| <div className={classNames('flex items-center justify-between mt-2 w-full h-9 px-3 bg-gray-100 rounded text-sm font-normal text-gray-800 cursor-pointer group')}> | |||
| {userProfile.name} | |||
| <div className='items-center hidden h-6 px-2 text-xs font-normal bg-white border border-gray-200 rounded-md group-hover:flex' onClick={handleEditName}>{t('common.operation.edit')}</div> | |||
| </div> | |||
| </div> | |||
| <div className='mb-8'> | |||
| <div className={titleClassName}>{t('common.account.email')}</div> | |||
| <div className={classNames(inputClassName, 'cursor-pointer')}>{userProfile.email}</div> | |||
| </div> | |||
| {systemFeatures.enable_email_password_login && ( | |||
| <div className='mb-8'> | |||
| <div className='mb-1 text-sm font-medium text-gray-900'>{t('common.account.password')}</div> | |||
| <div className='mb-2 text-xs text-gray-500'>{t('common.account.passwordTip')}</div> | |||
| <Button onClick={() => setEditPasswordModalVisible(true)}>{userProfile.is_password_set ? t('common.account.resetPassword') : t('common.account.setPassword')}</Button> | |||
| </div> | |||
| )} | |||
| <div className='mb-6 border-[0.5px] border-gray-100' /> | |||
| <div className='mb-8'> | |||
| <div className={titleClassName}>{t('common.account.langGeniusAccount')}</div> | |||
| <div className={descriptionClassName}>{t('common.account.langGeniusAccountTip')}</div> | |||
| {!!apps.length && ( | |||
| <Collapse | |||
| title={`${t('common.account.showAppLength', { length: apps.length })}`} | |||
| items={apps.map(app => ({ key: app.id, name: app.name }))} | |||
| renderItem={renderAppItem} | |||
| wrapperClassName='mt-2' | |||
| /> | |||
| )} | |||
| {!IS_CE_EDITION && <Button className='mt-2 text-[#D92D20]' onClick={() => setShowDeleteAccountModal(true)}>{t('common.account.delete')}</Button>} | |||
| </div> | |||
| {editNameModalVisible && ( | |||
| <Modal | |||
| isShow | |||
| onClose={() => setEditNameModalVisible(false)} | |||
| className={s.modal} | |||
| > | |||
| <div className='mb-6 text-lg font-medium text-gray-900'>{t('common.account.editName')}</div> | |||
| <div className={titleClassName}>{t('common.account.name')}</div> | |||
| <input | |||
| className={inputClassName} | |||
| value={editName} | |||
| onChange={e => setEditName(e.target.value)} | |||
| /> | |||
| <div className='flex justify-end mt-10'> | |||
| <Button className='mr-2' onClick={() => setEditNameModalVisible(false)}>{t('common.operation.cancel')}</Button> | |||
| <Button | |||
| disabled={editing || !editName} | |||
| variant='primary' | |||
| onClick={handleSaveName} | |||
| > | |||
| {t('common.operation.save')} | |||
| </Button> | |||
| </div> | |||
| </Modal> | |||
| )} | |||
| {editPasswordModalVisible && ( | |||
| <Modal | |||
| isShow | |||
| onClose={() => { | |||
| setEditPasswordModalVisible(false) | |||
| resetPasswordForm() | |||
| }} | |||
| className={s.modal} | |||
| > | |||
| <div className='mb-6 text-lg font-medium text-gray-900'>{userProfile.is_password_set ? t('common.account.resetPassword') : t('common.account.setPassword')}</div> | |||
| {userProfile.is_password_set && ( | |||
| <> | |||
| <div className={titleClassName}>{t('common.account.currentPassword')}</div> | |||
| <input | |||
| type="password" | |||
| className={inputClassName} | |||
| value={currentPassword} | |||
| onChange={e => setCurrentPassword(e.target.value)} | |||
| /> | |||
| </> | |||
| )} | |||
| <div className='mt-8 text-sm font-medium text-gray-900'> | |||
| {userProfile.is_password_set ? t('common.account.newPassword') : t('common.account.password')} | |||
| </div> | |||
| <input | |||
| type="password" | |||
| className={inputClassName} | |||
| value={password} | |||
| onChange={e => setPassword(e.target.value)} | |||
| /> | |||
| <div className='mt-8 text-sm font-medium text-gray-900'>{t('common.account.confirmPassword')}</div> | |||
| <input | |||
| type="password" | |||
| className={inputClassName} | |||
| value={confirmPassword} | |||
| onChange={e => setConfirmPassword(e.target.value)} | |||
| /> | |||
| <div className='flex justify-end mt-10'> | |||
| <Button className='mr-2' onClick={() => { | |||
| setEditPasswordModalVisible(false) | |||
| resetPasswordForm() | |||
| }}>{t('common.operation.cancel')}</Button> | |||
| <Button | |||
| disabled={editing} | |||
| variant='primary' | |||
| onClick={handleSavePassword} | |||
| > | |||
| {userProfile.is_password_set ? t('common.operation.reset') : t('common.operation.save')} | |||
| </Button> | |||
| </div> | |||
| </Modal> | |||
| )} | |||
| {showDeleteAccountModal && ( | |||
| <Confirm | |||
| isShow | |||
| onCancel={() => setShowDeleteAccountModal(false)} | |||
| onConfirm={() => setShowDeleteAccountModal(false)} | |||
| showCancel={false} | |||
| type='warning' | |||
| title={t('common.account.delete')} | |||
| content={ | |||
| <> | |||
| <div className='my-1 text-[#D92D20] text-sm leading-5'> | |||
| {t('common.account.deleteTip')} | |||
| </div> | |||
| <div className='mt-3 text-sm leading-5'> | |||
| <span>{t('common.account.deleteConfirmTip')}</span> | |||
| <a | |||
| className='text-primary-600 cursor' | |||
| href={`mailto:support@dify.ai?subject=Delete Account Request&body=Delete Account: ${userProfile.email}`} | |||
| target='_blank' | |||
| rel='noreferrer noopener' | |||
| onClick={(e) => { | |||
| e.preventDefault() | |||
| window.location.href = e.currentTarget.href | |||
| }} | |||
| > | |||
| support@dify.ai | |||
| </a> | |||
| </div> | |||
| <div className='my-2 px-3 py-2 rounded-lg bg-gray-100 text-sm font-medium leading-5 text-gray-800'>{`${t('common.account.delete')}: ${userProfile.email}`}</div> | |||
| </> | |||
| } | |||
| confirmText={t('common.operation.ok') as string} | |||
| /> | |||
| )} | |||
| </> | |||
| ) | |||
| } | |||
| @@ -3,6 +3,7 @@ import I18nServer from './components/i18n-server' | |||
| import BrowserInitor from './components/browser-initor' | |||
| import SentryInitor from './components/sentry-initor' | |||
| import { getLocaleOnServer } from '@/i18n/server' | |||
| import { TanstackQueryIniter } from '@/context/query-client' | |||
| import './styles/globals.css' | |||
| import './styles/markdown.scss' | |||
| @@ -46,7 +47,9 @@ const LocaleLayout = ({ | |||
| > | |||
| <BrowserInitor> | |||
| <SentryInitor> | |||
| <I18nServer>{children}</I18nServer> | |||
| <TanstackQueryIniter> | |||
| <I18nServer>{children}</I18nServer> | |||
| </TanstackQueryIniter> | |||
| </SentryInitor> | |||
| </BrowserInitor> | |||
| </body> | |||
| @@ -47,6 +47,8 @@ const translation = { | |||
| view: 'View', | |||
| viewMore: 'VIEW MORE', | |||
| regenerate: 'Regenerate', | |||
| submit: 'Submit', | |||
| skip: 'Skip', | |||
| }, | |||
| errorMsg: { | |||
| fieldRequired: '{{field}} is required', | |||
| @@ -181,8 +183,19 @@ const translation = { | |||
| editName: 'Edit Name', | |||
| showAppLength: 'Show {{length}} apps', | |||
| delete: 'Delete Account', | |||
| deleteTip: 'Deleting your account will permanently erase all your data and it cannot be recovered.', | |||
| deleteConfirmTip: 'To confirm, please send the following from your registered email to ', | |||
| deleteTip: 'Please note, once confirmed, as the Owner of any Workspaces, your workspaces will be scheduled in a queue for permanent deletion, and all your user data will be queued for permanent deletion.', | |||
| deletePrivacyLinkTip: 'For more information about how we handle your data, please see our ', | |||
| deletePrivacyLink: 'Privacy Policy.', | |||
| deleteSuccessTip: 'Your account needs time to finish deleting. We\'ll email you when it\'s all done.', | |||
| deleteLabel: 'To confirm, please type in your email below', | |||
| deletePlaceholder: 'Please enter your email', | |||
| sendVerificationButton: 'Send Verification Code', | |||
| verificationLabel: 'Verification Code', | |||
| verificationPlaceholder: 'Paste the 6-digit code', | |||
| permanentlyDeleteButton: 'Permanently Delete Account', | |||
| feedbackTitle: 'Feedback', | |||
| feedbackLabel: 'Tell us why you deleted your account?', | |||
| feedbackPlaceholder: 'Optional', | |||
| }, | |||
| members: { | |||
| team: 'Team', | |||
| @@ -47,6 +47,8 @@ const translation = { | |||
| view: '查看', | |||
| viewMore: '查看更多', | |||
| regenerate: '重新生成', | |||
| submit: '提交', | |||
| skip: '跳过', | |||
| }, | |||
| errorMsg: { | |||
| fieldRequired: '{{field}} 为必填项', | |||
| @@ -181,8 +183,19 @@ const translation = { | |||
| editName: '编辑名字', | |||
| showAppLength: '显示 {{length}} 个应用', | |||
| delete: '删除账户', | |||
| deleteTip: '删除账户后,所有数据将被永久删除且不可恢复。', | |||
| deleteConfirmTip: '请将以下内容通过您的账户邮箱发送到 ', | |||
| deleteTip: '请注意,一旦确认,作为任何空间的所有者,您的空间将被安排进入永久删除队列,您的所有用户数据也将被排入永久删除队列。', | |||
| deletePrivacyLinkTip: '有关我们如何处理您的数据的更多信息,请参阅我们的', | |||
| deletePrivacyLink: '隐私政策', | |||
| deleteSuccessTip: '删除账户需要一些时间。完成后,我们会通过邮件通知您。', | |||
| deleteLabel: '请输入您的邮箱以确认', | |||
| deletePlaceholder: '输入您的邮箱...', | |||
| sendVerificationButton: '发送验证码', | |||
| verificationLabel: '验证码', | |||
| verificationPlaceholder: '输入 6 位数字验证码', | |||
| permanentlyDeleteButton: '永久删除', | |||
| feedbackTitle: '反馈', | |||
| feedbackLabel: '请告诉我们您为什么删除账户?', | |||
| feedbackPlaceholder: '选填', | |||
| }, | |||
| members: { | |||
| team: '团队', | |||
| @@ -339,3 +339,12 @@ export const sendResetPasswordCode = (email: string, language = 'en-US') => | |||
| export const verifyResetPasswordCode = (body: { email: string;code: string;token: string }) => | |||
| post<CommonResponse & { is_valid: boolean }>('/forgot-password/validity', { body }) | |||
| export const sendDeleteAccountCode = () => | |||
| get<CommonResponse & { data: string }>('/account/delete/verify') | |||
| export const verifyDeleteAccountCode = (body: { code: string;token: string }) => | |||
| post<CommonResponse & { is_valid: boolean }>('/account/delete', { body }) | |||
| export const submitDeleteAccountFeedback = (body: { feedback: string;email: string }) => | |||
| post<CommonResponse>('/account/delete/feedback', { body }) | |||