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) => ({

Reply via email to