You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@answer.apache.org by sh...@apache.org on 2025/06/26 08:45:18 UTC
(answer) 02/02: feat: Blocked users can choose the duration of the block #1361
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 17e1b301752cd136e98cf83e81bae89cd03a3007
Author: shuai <li...@sifou.com>
AuthorDate: Thu Jun 26 16:44:42 2025 +0800
feat: Blocked users can choose the duration of the block #1361
---
i18n/en_US.yaml | 12 +++
i18n/zh_CN.yaml | 12 +++
ui/src/common/constants.ts | 30 +++++---
ui/src/components/Header/index.tsx | 1 -
.../pages/Admin/Users/components/Action/index.tsx | 16 ++--
.../Users/components/SuspenseUserModal/index.tsx | 90 ++++++++++++++++++++++
ui/src/pages/Admin/Users/index.tsx | 13 +++-
7 files changed, 150 insertions(+), 24 deletions(-)
diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml
index cb05e5e1..3889a12a 100644
--- a/i18n/en_US.yaml
+++ b/i18n/en_US.yaml
@@ -1070,6 +1070,9 @@ ui:
day: day
hours: hours
days: days
+ month: month
+ months: months
+ year: year
reaction:
heart: heart
smile: smile
@@ -1930,6 +1933,7 @@ ui:
created_at: Created time
delete_at: Deleted time
suspend_at: Suspended time
+ suspend_until: Suspend until
status: Status
role: Role
action: Action
@@ -1964,6 +1968,8 @@ ui:
suspend_user:
title: Suspend this user
content: A suspended user can't log in.
+ label: How long will the user be suspended for?
+ forever: Forever
questions:
page_title: Questions
unlisted: Unlisted
@@ -2024,6 +2030,12 @@ ui:
label: Timezone
msg: Timezone cannot be empty.
text: Choose a city in the same timezone as you.
+ avatar:
+ label: Default avatar
+ text: For users without a custom avatar of their own.
+ gravatar_base_url:
+ label: Gravatar base URL
+ text: URL of the Gravatar provider's API base. Ignored when empty.
smtp:
page_title: SMTP
from_email:
diff --git a/i18n/zh_CN.yaml b/i18n/zh_CN.yaml
index 9852ba60..ce5fbb07 100644
--- a/i18n/zh_CN.yaml
+++ b/i18n/zh_CN.yaml
@@ -1056,6 +1056,9 @@ ui:
day: 天
hours: 小时
days: 日
+ month: 月
+ months: 月
+ year: 年
reaction:
heart: 爱心
smile: 微笑
@@ -1890,6 +1893,7 @@ ui:
created_at: 创建时间
delete_at: 删除时间
suspend_at: 封禁时间
+ suspend_until: 封禁到期
status: 状态
role: 角色
action: 操作
@@ -1924,6 +1928,8 @@ ui:
suspend_user:
title: 挂起此用户
content: 被封禁的用户将无法登录。
+ label: 用户将被封禁多长时间?
+ forever: 永久
questions:
page_title: 问题
unlisted: 已隐藏
@@ -1984,6 +1990,12 @@ ui:
label: 时区
msg: 时区不能为空。
text: 选择一个与您相同时区的城市。
+ avatar:
+ label: 默认头像
+ text: 没有自定义头像的用户。
+ gravatar_base_url:
+ label: Gravatar 根路径 URL
+ text: Gravatar 提供商的 API 基础的 URL。当为空时忽略。
smtp:
page_title: SMTP
from_email:
diff --git a/ui/src/common/constants.ts b/ui/src/common/constants.ts
index 7ca57400..18f25114 100644
--- a/ui/src/common/constants.ts
+++ b/ui/src/common/constants.ts
@@ -663,42 +663,52 @@ export const DEFAULT_THEME_COLOR = '#0033ff';
export const SUSPENSE_USER_TIME = [
{
label: 'hours',
- value: '24',
+ time: '24',
+ value: '24h',
},
{
label: 'hours',
- value: '48',
+ time: '48',
+ value: '48h',
},
{
label: 'hours',
- value: '72',
+ time: '72',
+ value: '72h',
},
{
label: 'days',
- value: '7',
+ time: '7',
+ value: '7d',
},
{
label: 'days',
- value: '14',
+ time: '14',
+ value: '14d',
},
{
label: 'months',
- value: '1',
+ time: '1',
+ value: '1m',
},
{
label: 'months',
- value: '2',
+ time: '2',
+ value: '2m',
},
{
label: 'months',
- value: '3',
+ time: '3',
+ value: '3m',
},
{
label: 'months',
- value: '6',
+ time: '6',
+ value: '6m',
},
{
label: 'year',
- value: '1',
+ time: '1',
+ value: '1y',
},
];
diff --git a/ui/src/components/Header/index.tsx b/ui/src/components/Header/index.tsx
index 0b015ba6..dbc7565d 100644
--- a/ui/src/components/Header/index.tsx
+++ b/ui/src/components/Header/index.tsx
@@ -87,7 +87,6 @@ const Header: FC = () => {
if (theme_config?.[theme]?.navbar_style) {
// const color = theme_config[theme].navbar_style.startsWith('#')
themeMode = isLight(theme_config[theme].navbar_style) ? 'light' : 'dark';
- console.log('isLightTheme', themeMode);
navbarStyle = `theme-${themeMode}`;
}
diff --git a/ui/src/pages/Admin/Users/components/Action/index.tsx b/ui/src/pages/Admin/Users/components/Action/index.tsx
index 9f0847a2..b55971f4 100644
--- a/ui/src/pages/Admin/Users/components/Action/index.tsx
+++ b/ui/src/pages/Admin/Users/components/Action/index.tsx
@@ -42,6 +42,7 @@ interface Props {
currentUser;
refreshUsers: () => void;
showDeleteModal: (val) => void;
+ showSuspenseModal: (val) => void;
userData;
}
@@ -53,6 +54,7 @@ const UserOperation = ({
refreshUsers,
showDeleteModal,
userData,
+ showSuspenseModal,
}: Props) => {
const { t } = useTranslation('translation', { keyPrefix: 'admin.users' });
const Toast = useToast();
@@ -170,17 +172,9 @@ const UserOperation = ({
if (type === 'suspend') {
// cons
- Modal.confirm({
- title: t('suspend_user.title'),
- content: t('suspend_user.content'),
- cancelBtnVariant: 'link',
- cancelText: t('cancel', { keyPrefix: 'btns' }),
- confirmBtnVariant: 'danger',
- confirmText: t('suspend', { keyPrefix: 'btns' }),
- onConfirm: () => {
- // active -> suspended
- postUserStatus('suspended');
- },
+ showSuspenseModal({
+ show: true,
+ userId: userData.user_id,
});
}
diff --git a/ui/src/pages/Admin/Users/components/SuspenseUserModal/index.tsx b/ui/src/pages/Admin/Users/components/SuspenseUserModal/index.tsx
new file mode 100644
index 00000000..dc59ff34
--- /dev/null
+++ b/ui/src/pages/Admin/Users/components/SuspenseUserModal/index.tsx
@@ -0,0 +1,90 @@
+/*
+ * 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 { useState } from 'react';
+import { Modal, Button, Form } from 'react-bootstrap';
+import { useTranslation } from 'react-i18next';
+
+import { changeUserStatus } from '@/services';
+import { SUSPENSE_USER_TIME } from '@/common/constants';
+import { toastStore } from '@/stores';
+
+const SuspenseUserModal = ({ show, userId, onClose, refreshUsers }) => {
+ const { t } = useTranslation('translation', { keyPrefix: 'admin.users' });
+ const [checkVal, setCheckVal] = useState('forever');
+
+ const handleClose = () => {
+ onClose();
+ setCheckVal('forever');
+ };
+
+ const handleSubmit = (e) => {
+ e.preventDefault();
+ changeUserStatus({
+ user_id: userId,
+ status: 'suspended',
+ suspend_duration: checkVal,
+ }).then(() => {
+ toastStore.getState().show({
+ msg: t('user_suspended', { keyPrefix: 'messages' }),
+ variant: 'success',
+ });
+ refreshUsers?.();
+ handleClose();
+ });
+ };
+
+ return (
+ <Modal show={show} onHide={handleClose}>
+ <Modal.Header closeButton>
+ <Modal.Title>{t('suspend_user.title')}</Modal.Title>
+ </Modal.Header>
+ <Modal.Body>
+ <p>{t('suspend_user.content')}</p>
+ <Form>
+ <Form.Group controlId="delete_user" className="mb-3">
+ <Form.Label>{t('suspend_user.label')}</Form.Label>
+ <Form.Select
+ value={checkVal}
+ onChange={(e) => setCheckVal(e.target.value)}>
+ <option value="forever">{t('suspend_user.forever')}</option>
+ {SUSPENSE_USER_TIME.map((item) => {
+ return (
+ <option key={item.value} value={item.value}>
+ {item.time} {t(item.label, { keyPrefix: 'dates' })}
+ </option>
+ );
+ })}
+ </Form.Select>
+ </Form.Group>
+ </Form>
+ </Modal.Body>
+ <Modal.Footer>
+ <Button variant="link" onClick={handleClose}>
+ {t('cancel', { keyPrefix: 'btns' })}
+ </Button>
+ <Button variant="danger" onClick={handleSubmit}>
+ {t('suspend', { keyPrefix: 'btns' })}
+ </Button>
+ </Modal.Footer>
+ </Modal>
+ );
+};
+
+export default SuspenseUserModal;
diff --git a/ui/src/pages/Admin/Users/index.tsx b/ui/src/pages/Admin/Users/index.tsx
index 295cb075..7e8e4fd6 100644
--- a/ui/src/pages/Admin/Users/index.tsx
+++ b/ui/src/pages/Admin/Users/index.tsx
@@ -23,6 +23,7 @@ import { useSearchParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import classNames from 'classnames';
+import dayjs from 'dayjs';
import {
Pagination,
@@ -297,7 +298,14 @@ const Users: FC = () => {
<FormatTime time={user.suspended_at} />
</td>
<td className="text-nowrap">
- <FormatTime time={user.suspended_at} />
+ {user.suspended_until <= 0 ||
+ Number(
+ dayjs(user.suspended_until * 1000).format('YYYY'),
+ ) > 2099
+ ? t('suspend_user.forever')
+ : dayjs(user.suspended_until * 1000).format(
+ t('long_date_with_time', { keyPrefix: 'dates' }),
+ )}
</td>
</>
)}
@@ -357,13 +365,14 @@ const Users: FC = () => {
/>
<SuspenseUserModal
show={suspenseUserModalState.show}
+ userId={suspenseUserModalState.userId}
onClose={() => {
handleSuspenseUserModalState({
show: false,
userId: '',
});
}}
- onDelete={(val) => handleDelete(val)}
+ refreshUsers={refreshUsers}
/>
</>
);