This is an automated email from the ASF dual-hosted git repository. shuai pushed a commit to branch feat/1.6.1/md in repository https://gitbox.apache.org/repos/asf/answer.git
commit 19eca49d2a8e1a6b6fa67268061567421a2fcd04 Author: shuai <lishuail...@sifou.com> AuthorDate: Thu Jun 26 14:55:35 2025 +0800 fix: move the settings -> users section in admin to Interface #1360 --- ui/src/common/constants.ts | 44 +++++- ui/src/common/interface.ts | 4 +- ui/src/pages/Admin/Interface/index.tsx | 51 +++++- ui/src/pages/Admin/SettingsUsers/index.tsx | 218 -------------------------- ui/src/pages/Admin/Users/index.tsx | 45 +++++- ui/src/pages/Users/Settings/Profile/index.tsx | 24 ++- ui/src/router/routes.ts | 4 - ui/src/services/admin/settings.ts | 2 - ui/src/stores/interface.ts | 1 + ui/src/stores/siteInfo.ts | 2 - 10 files changed, 141 insertions(+), 254 deletions(-) diff --git a/ui/src/common/constants.ts b/ui/src/common/constants.ts index 163d4989..7ca57400 100644 --- a/ui/src/common/constants.ts +++ b/ui/src/common/constants.ts @@ -126,7 +126,6 @@ export const ADMIN_NAV_MENUS = [ { name: 'write' }, { name: 'seo' }, { name: 'login' }, - { name: 'users', path: 'settings-users' }, { name: 'privileges' }, ], }, @@ -660,3 +659,46 @@ export const SYSTEM_AVATAR_OPTIONS = [ export const TAG_SLUG_NAME_MAX_LENGTH = 35; export const DEFAULT_THEME_COLOR = '#0033ff'; + +export const SUSPENSE_USER_TIME = [ + { + label: 'hours', + value: '24', + }, + { + label: 'hours', + value: '48', + }, + { + label: 'hours', + value: '72', + }, + { + label: 'days', + value: '7', + }, + { + label: 'days', + value: '14', + }, + { + label: 'months', + value: '1', + }, + { + label: 'months', + value: '2', + }, + { + label: 'months', + value: '3', + }, + { + label: 'months', + value: '6', + }, + { + label: 'year', + value: '1', + }, +]; diff --git a/ui/src/common/interface.ts b/ui/src/common/interface.ts index 3935c439..114b0e4c 100644 --- a/ui/src/common/interface.ts +++ b/ui/src/common/interface.ts @@ -382,6 +382,8 @@ export interface HelmetUpdate extends Omit<HelmetBase, 'pageTitle'> { export interface AdminSettingsInterface { language: string; time_zone?: string; + default_avatar: string; + gravatar_base_url: string; } export interface AdminSettingsSmtp { @@ -403,8 +405,6 @@ export interface AdminSettingsUsers { allow_update_location: boolean; allow_update_username: boolean; allow_update_website: boolean; - default_avatar: string; - gravatar_base_url: string; } export interface SiteSettings { diff --git a/ui/src/pages/Admin/Interface/index.tsx b/ui/src/pages/Admin/Interface/index.tsx index 9c622048..865029e9 100644 --- a/ui/src/pages/Admin/Interface/index.tsx +++ b/ui/src/pages/Admin/Interface/index.tsx @@ -28,7 +28,7 @@ import { } from '@/common/interface'; import { interfaceStore, loggedUserInfoStore } from '@/stores'; import { JSONSchema, SchemaForm, UISchema } from '@/components'; -import { DEFAULT_TIMEZONE } from '@/common/constants'; +import { DEFAULT_TIMEZONE, SYSTEM_AVATAR_OPTIONS } from '@/common/constants'; import { updateInterfaceSetting, useInterfaceSetting, @@ -59,7 +59,8 @@ const Interface: FC = () => { description: t('language.text'), enum: langs?.map((lang) => lang.value), enumNames: langs?.map((lang) => lang.label), - default: setting?.language || storeInterface.language, + default: + setting?.language || storeInterface.language || langs?.[0]?.value, }, time_zone: { type: 'string', @@ -67,12 +68,26 @@ const Interface: FC = () => { description: t('time_zone.text'), default: setting?.time_zone || DEFAULT_TIMEZONE, }, + default_avatar: { + type: 'string', + title: t('avatar.label'), + description: t('avatar.text'), + enum: SYSTEM_AVATAR_OPTIONS?.map((v) => v.value), + enumNames: SYSTEM_AVATAR_OPTIONS?.map((v) => v.label), + default: setting?.default_avatar || 'system', + }, + gravatar_base_url: { + type: 'string', + title: t('gravatar_base_url.label'), + description: t('gravatar_base_url.text'), + default: setting?.gravatar_base_url || '', + }, }, }; const [formData, setFormData] = useState<FormDataType>({ language: { - value: setting?.language || storeInterface.language, + value: setting?.language || storeInterface.language || langs?.[0]?.value, isInvalid: false, errorMsg: '', }, @@ -81,6 +96,16 @@ const Interface: FC = () => { isInvalid: false, errorMsg: '', }, + default_avatar: { + value: setting?.default_avatar || 'system', + isInvalid: false, + errorMsg: '', + }, + gravatar_base_url: { + value: setting?.gravatar_base_url || '', + isInvalid: false, + errorMsg: '', + }, }); const uiSchema: UISchema = { @@ -90,6 +115,15 @@ const Interface: FC = () => { time_zone: { 'ui:widget': 'timezone', }, + default_avatar: { + 'ui:widget': 'select', + }, + gravatar_base_url: { + 'ui:widget': 'input', + 'ui:options': { + placeholder: 'https://www.gravatar.com/avatar/', + }, + }, }; const getLangs = async () => { const res: LangsType[] = await loadLanguageOptions(true); @@ -122,6 +156,8 @@ const Interface: FC = () => { const reqParams: AdminSettingsInterface = { language: formData.language.value, time_zone: formData.time_zone.value, + default_avatar: formData.default_avatar.value, + gravatar_base_url: formData.gravatar_base_url.value, }; updateInterfaceSetting(reqParams) @@ -151,7 +187,14 @@ const Interface: FC = () => { if (setting) { const formMeta = {}; Object.keys(setting).forEach((k) => { - formMeta[k] = { ...formData[k], value: setting[k] }; + let v = setting[k]; + if (k === 'default_avatar' && !v) { + v = 'system'; + } + if (k === 'gravatar_base_url' && !v) { + v = ''; + } + formMeta[k] = { ...formData[k], value: v }; }); setFormData({ ...formData, ...formMeta }); } diff --git a/ui/src/pages/Admin/SettingsUsers/index.tsx b/ui/src/pages/Admin/SettingsUsers/index.tsx deleted file mode 100644 index e0980980..00000000 --- a/ui/src/pages/Admin/SettingsUsers/index.tsx +++ /dev/null @@ -1,218 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { FC, FormEvent, useEffect, useState } from 'react'; -import { useTranslation } from 'react-i18next'; - -import { useToast } from '@/hooks'; -import { FormDataType } from '@/common/interface'; -import { JSONSchema, SchemaForm, UISchema, initFormData } from '@/components'; -import { SYSTEM_AVATAR_OPTIONS } from '@/common/constants'; -import { - getUsersSetting, - putUsersSetting, - AdminSettingsUsers, -} from '@/services'; -import { handleFormError, scrollToElementTop } from '@/utils'; -import * as Type from '@/common/interface'; -import { siteInfoStore } from '@/stores'; - -const Index: FC = () => { - const { t } = useTranslation('translation', { - keyPrefix: 'admin.settings_users', - }); - const Toast = useToast(); - const { updateUsers: updateUsersStore } = siteInfoStore(); - const schema: JSONSchema = { - title: t('title'), - properties: { - default_avatar: { - type: 'string', - title: t('avatar.label'), - description: t('avatar.text'), - enum: SYSTEM_AVATAR_OPTIONS?.map((v) => v.value), - enumNames: SYSTEM_AVATAR_OPTIONS?.map((v) => v.label), - default: 'system', - }, - gravatar_base_url: { - type: 'string', - title: t('gravatar_base_url.label'), - description: t('gravatar_base_url.text'), - }, - profile_editable: { - type: 'string', - title: t('profile_editable.title'), - }, - allow_update_display_name: { - type: 'boolean', - title: 'allow_update_display_name', - }, - allow_update_username: { - type: 'boolean', - title: 'allow_update_username', - }, - allow_update_avatar: { - type: 'boolean', - title: 'allow_update_avatar', - }, - allow_update_bio: { - type: 'boolean', - title: 'allow_update_bio', - }, - allow_update_website: { - type: 'boolean', - title: 'allow_update_website', - }, - allow_update_location: { - type: 'boolean', - title: 'allow_update_location', - }, - }, - }; - - const [formData, setFormData] = useState<FormDataType>(initFormData(schema)); - - const uiSchema: UISchema = { - default_avatar: { - 'ui:widget': 'select', - }, - gravatar_base_url: { - 'ui:widget': 'input', - 'ui:options': { - placeholder: 'https://www.gravatar.com/avatar/', - }, - }, - profile_editable: { - 'ui:widget': 'legend', - }, - allow_update_display_name: { - 'ui:widget': 'switch', - 'ui:options': { - label: t('allow_update_display_name.label'), - simplify: true, - }, - }, - allow_update_username: { - 'ui:widget': 'switch', - 'ui:options': { - label: t('allow_update_username.label'), - simplify: true, - }, - }, - allow_update_avatar: { - 'ui:widget': 'switch', - 'ui:options': { - label: t('allow_update_avatar.label'), - simplify: true, - }, - }, - allow_update_bio: { - 'ui:widget': 'switch', - 'ui:options': { - label: t('allow_update_bio.label'), - simplify: true, - }, - }, - allow_update_website: { - 'ui:widget': 'switch', - 'ui:options': { - label: t('allow_update_website.label'), - simplify: true, - }, - }, - allow_update_location: { - 'ui:widget': 'switch', - 'ui:options': { - label: t('allow_update_location.label'), - field_class_name: 'mb-3', - simplify: true, - }, - }, - }; - - const onSubmit = (evt: FormEvent) => { - evt.preventDefault(); - evt.stopPropagation(); - const reqParams: AdminSettingsUsers = { - allow_update_avatar: formData.allow_update_avatar.value, - allow_update_bio: formData.allow_update_bio.value, - allow_update_display_name: formData.allow_update_display_name.value, - allow_update_location: formData.allow_update_location.value, - allow_update_username: formData.allow_update_username.value, - allow_update_website: formData.allow_update_website.value, - default_avatar: formData.default_avatar.value, - gravatar_base_url: formData.gravatar_base_url.value, - }; - putUsersSetting(reqParams) - .then(() => { - updateUsersStore(reqParams); - Toast.onShow({ - msg: t('update', { keyPrefix: 'toast' }), - variant: 'success', - }); - }) - .catch((err) => { - if (err.isError) { - const data = handleFormError(err, formData); - setFormData({ ...data }); - const ele = document.getElementById(err.list[0].error_field); - scrollToElementTop(ele); - } - }); - }; - - useEffect(() => { - getUsersSetting().then((resp) => { - if (!resp) { - return; - } - const formMeta: Type.FormDataType = {}; - Object.keys(formData).forEach((k) => { - let v = resp[k]; - if (k === 'default_avatar' && !v) { - v = 'system'; - } - if (k === 'gravatar_base_url' && !v) { - v = ''; - } - formMeta[k] = { ...formData[k], value: v }; - }); - setFormData({ ...formData, ...formMeta }); - }); - }, []); - - const handleOnChange = (data) => { - setFormData(data); - }; - - return ( - <> - <h3 className="mb-4">{t('title')}</h3> - <SchemaForm - schema={schema} - uiSchema={uiSchema} - formData={formData} - onSubmit={onSubmit} - onChange={handleOnChange} - /> - </> - ); -}; - -export default Index; diff --git a/ui/src/pages/Admin/Users/index.tsx b/ui/src/pages/Admin/Users/index.tsx index 2402a839..295cb075 100644 --- a/ui/src/pages/Admin/Users/index.tsx +++ b/ui/src/pages/Admin/Users/index.tsx @@ -47,6 +47,7 @@ import { formatCount } from '@/utils'; import DeleteUserModal from './components/DeleteUserModal'; import Action from './components/Action'; +import SuspenseUserModal from './components/SuspenseUserModal'; const UserFilterKeys: Type.UserFilterBy[] = [ 'normal', @@ -70,6 +71,10 @@ const Users: FC = () => { show: false, userId: '', }); + const [suspenseUserModalState, setSuspenseUserModalState] = useState({ + show: false, + userId: '', + }); const [urlSearchParams, setUrlSearchParams] = useSearchParams(); const curFilter = urlSearchParams.get('filter') || UserFilterKeys[0]; const curPage = Number(urlSearchParams.get('page') || '1'); @@ -172,6 +177,13 @@ const Users: FC = () => { }); }; + const handleSuspenseUserModalState = (modalData: { + show: boolean; + userId: string; + }) => { + setSuspenseUserModalState(modalData); + }; + const showAddUser = !ucAgent?.enabled || (ucAgent?.enabled && adminUcAgent?.allow_create_user); const showActionPassword = @@ -231,17 +243,22 @@ const Users: FC = () => { <tr> <th>{t('name')}</th> <th style={{ width: '12%' }}>{t('reputation')}</th> - <th style={{ width: '20%' }} className="min-w-15"> + <th style={{ width: '15%' }} className="min-w-15"> {t('email')} </th> - <th className="text-nowrap" style={{ width: '15%' }}> + <th className="text-nowrap" style={{ width: '12%' }}> {t('created_at')} </th> {(curFilter === 'deleted' || curFilter === 'suspended') && ( - <th className="text-nowrap" style={{ width: '15%' }}> + <th className="text-nowrap" style={{ width: '12%' }}> {curFilter === 'deleted' ? t('delete_at') : t('suspend_at')} </th> )} + {curFilter === 'suspended' && ( + <th className="text-nowrap" style={{ width: '12%' }}> + {t('suspend_until')} + </th> + )} <th style={{ width: '12%' }}>{t('status')}</th> {curFilter !== 'suspended' && curFilter !== 'deleted' && ( @@ -275,9 +292,14 @@ const Users: FC = () => { <FormatTime time={user.created_at} /> </td> {curFilter === 'suspended' && ( - <td className="text-nowrap"> - <FormatTime time={user.suspended_at} /> - </td> + <> + <td className="text-nowrap"> + <FormatTime time={user.suspended_at} /> + </td> + <td className="text-nowrap"> + <FormatTime time={user.suspended_at} /> + </td> + </> )} {curFilter === 'deleted' && ( <td className="text-nowrap"> @@ -306,6 +328,7 @@ const Users: FC = () => { currentUser={currentUser} refreshUsers={refreshUsers} showDeleteModal={changeDeleteUserModalState} + showSuspenseModal={handleSuspenseUserModalState} /> ) : null} </tr> @@ -332,6 +355,16 @@ const Users: FC = () => { }} onDelete={(val) => handleDelete(val)} /> + <SuspenseUserModal + show={suspenseUserModalState.show} + onClose={() => { + handleSuspenseUserModalState({ + show: false, + userId: '', + }); + }} + onDelete={(val) => handleDelete(val)} + /> </> ); }; diff --git a/ui/src/pages/Users/Settings/Profile/index.tsx b/ui/src/pages/Users/Settings/Profile/index.tsx index 9af28ce6..7fb8247c 100644 --- a/ui/src/pages/Users/Settings/Profile/index.tsx +++ b/ui/src/pages/Users/Settings/Profile/index.tsx @@ -25,7 +25,7 @@ import { sha256 } from 'js-sha256'; import type { FormDataType } from '@/common/interface'; import { UploadImg, Avatar, Icon, ImgViewer } from '@/components'; -import { loggedUserInfoStore, userCenterStore, siteInfoStore } from '@/stores'; +import { loggedUserInfoStore, userCenterStore, interfaceStore } from '@/stores'; import { useToast } from '@/hooks'; import { modifyUserInfo, @@ -42,7 +42,7 @@ const Index: React.FC = () => { const toast = useToast(); const { user, update } = loggedUserInfoStore(); const { agent: ucAgent } = userCenterStore(); - const { users: usersSetting } = siteInfoStore(); + const { interface: interfaceSetting } = interfaceStore(); const [mailHash, setMailHash] = useState(''); const [count] = useState(0); const [profileAgent, setProfileAgent] = useState<UcSettingAgent>(); @@ -309,7 +309,6 @@ const Index: React.FC = () => { <Form.Control required type="text" - disabled={!usersSetting.allow_update_display_name} value={formData.display_name.value} isInvalid={formData.display_name.isInvalid} onChange={(e) => @@ -332,7 +331,6 @@ const Index: React.FC = () => { <Form.Control required type="text" - disabled={!usersSetting.allow_update_username} value={formData.username.value} isInvalid={formData.username.isInvalid} onChange={(e) => @@ -356,7 +354,6 @@ const Index: React.FC = () => { <div className="mb-3"> <Form.Select name="avatar.type" - disabled={!usersSetting.allow_update_avatar} value={formData.avatar.type} onChange={handleAvatarChange}> <option value="gravatar" key="gravatar"> @@ -387,14 +384,18 @@ const Index: React.FC = () => { <span>{t('avatar.gravatar_text')}</span> <a href={ - usersSetting.gravatar_base_url.includes('gravatar.cn') + interfaceSetting.gravatar_base_url.includes( + 'gravatar.cn', + ) ? 'https://gravatar.cn' : 'https://gravatar.com' } className="ms-1" target="_blank" rel="noreferrer"> - {usersSetting.gravatar_base_url.includes('gravatar.cn') + {interfaceSetting.gravatar_base_url.includes( + 'gravatar.cn', + ) ? 'gravatar.cn' : 'gravatar.com'} </a> @@ -413,15 +414,11 @@ const Index: React.FC = () => { alt={formData.display_name.value} /> <ButtonGroup vertical className="fit-content"> - <UploadImg - type="avatar" - disabled={!usersSetting.allow_update_avatar} - uploadCallback={avatarUpload}> + <UploadImg type="avatar" uploadCallback={avatarUpload}> <Icon name="cloud-upload" /> </UploadImg> <Button variant="outline-secondary" - disabled={!usersSetting.allow_update_avatar} onClick={removeCustomAvatar}> <Icon name="trash" /> </Button> @@ -463,7 +460,6 @@ const Index: React.FC = () => { required as="textarea" rows={5} - disabled={!usersSetting.allow_update_bio} value={formData.bio.value} isInvalid={formData.bio.isInvalid} onChange={(e) => @@ -489,7 +485,6 @@ const Index: React.FC = () => { required type="url" placeholder={t('website.placeholder')} - disabled={!usersSetting.allow_update_website} value={formData.website.value} isInvalid={formData.website.isInvalid} onChange={(e) => @@ -515,7 +510,6 @@ const Index: React.FC = () => { required type="text" placeholder={t('location.placeholder')} - disabled={!usersSetting.allow_update_location} value={formData.location.value} isInvalid={formData.location.isInvalid} onChange={(e) => diff --git a/ui/src/router/routes.ts b/ui/src/router/routes.ts index d9d20a44..59907b41 100644 --- a/ui/src/router/routes.ts +++ b/ui/src/router/routes.ts @@ -398,10 +398,6 @@ const routes: RouteNode[] = [ path: 'login', page: 'pages/Admin/Login', }, - { - path: 'settings-users', - page: 'pages/Admin/SettingsUsers', - }, { path: 'privileges', page: 'pages/Admin/Privileges', diff --git a/ui/src/services/admin/settings.ts b/ui/src/services/admin/settings.ts index 7ce57ade..f2b9e598 100644 --- a/ui/src/services/admin/settings.ts +++ b/ui/src/services/admin/settings.ts @@ -29,8 +29,6 @@ export interface AdminSettingsUsers { allow_update_location: boolean; allow_update_username: boolean; allow_update_website: boolean; - default_avatar: string; - gravatar_base_url: string; } interface PrivilegeLevel { diff --git a/ui/src/stores/interface.ts b/ui/src/stores/interface.ts index 7b514eb6..cceb425c 100644 --- a/ui/src/stores/interface.ts +++ b/ui/src/stores/interface.ts @@ -32,6 +32,7 @@ const interfaceSetting = create<InterfaceType>((set) => ({ language: DEFAULT_LANG, time_zone: '', default_avatar: 'system', + gravatar_base_url: '', }, update: (params) => set(() => { diff --git a/ui/src/stores/siteInfo.ts b/ui/src/stores/siteInfo.ts index 5208b9b4..725546d6 100644 --- a/ui/src/stores/siteInfo.ts +++ b/ui/src/stores/siteInfo.ts @@ -39,8 +39,6 @@ const defaultUsersConf: AdminSettingsUsers = { allow_update_location: false, allow_update_username: false, allow_update_website: false, - default_avatar: 'system', - gravatar_base_url: 'https://www.gravatar.com/avatar/', }; const siteInfo = create<SiteInfoType>((set) => ({