Merge branch 'feat/ui-0.5.0' into test

This commit is contained in:
haitao(lj) 2022-11-30 10:13:35 +08:00
commit 5451625725
15 changed files with 179 additions and 95 deletions

View File

@ -551,7 +551,7 @@ ui:
footer:
build_on: >-
Built on <1> Answer </1>- the open-source software that power Q&A
communities<br />Made with love © 2022 Answer
communities.<br />Made with love © {{cc}}.
upload_img:
name: Change
loading: loading...

View File

@ -1,3 +1,4 @@
export const DEFAULT_SITE_NAME = 'Answer';
export const DEFAULT_LANG = 'en_US';
export const CURRENT_LANG_STORAGE_KEY = '_a_lang_';
export const LANG_RESOURCE_STORAGE_KEY = '_a_lang_r_';
@ -9,29 +10,29 @@ export const CAPTCHA_CODE_STORAGE_KEY = '_a_captcha_';
export const ADMIN_LIST_STATUS = {
// normal;
1: {
variant: 'success',
variant: 'text-bg-success',
name: 'normal',
},
// closed;
2: {
variant: 'warning',
variant: 'text-bg-warning',
name: 'closed',
},
// deleted
10: {
variant: 'danger',
variant: 'text-bg-danger',
name: 'deleted',
},
normal: {
variant: 'success',
variant: 'text-bg-success',
name: 'normal',
},
closed: {
variant: 'warning',
variant: 'text-bg-warning',
name: 'closed',
},
deleted: {
variant: 'danger',
variant: 'text-bg-danger',
name: 'deleted',
},
};

View File

@ -287,9 +287,9 @@ export interface AdminSettingsSmtp {
from_name: string;
smtp_authentication: boolean;
smtp_host: string;
smtp_password: string;
smtp_password?: string;
smtp_port: number;
smtp_username: string;
smtp_username?: string;
test_email_recipient?: string;
}

View File

@ -1,5 +1,5 @@
import React, { FC } from 'react';
import { Accordion, Badge, Button, Stack } from 'react-bootstrap';
import { Accordion, Button, Stack } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import { useNavigate, useMatch } from 'react-router-dom';
@ -33,9 +33,9 @@ function MenuNode({ menu, callback, activeKey, isLeaf = false }) {
{!isLeaf ? <Icon name="chevron-right" className="me-1" /> : null}
{t(menu.name)}
{menu.badgeContent ? (
<Badge bg="dark" className="ms-auto top-0">
<span className="badge text-bg-dark ms-auto top-0">
{menu.badgeContent}
</Badge>
</span>
) : null}
</Stack>
</Button>

View File

@ -2,12 +2,20 @@ import React from 'react';
import { Container } from 'react-bootstrap';
import { Trans } from 'react-i18next';
import dayjs from 'dayjs';
import { siteInfoStore } from '@/stores';
import { DEFAULT_SITE_NAME } from '@/common/constants';
const Index = () => {
const fullYear = dayjs().format('YYYY');
const siteName = siteInfoStore.getState().siteInfo.name || DEFAULT_SITE_NAME;
const cc = `${fullYear} ${siteName}`;
return (
<footer className="bg-light py-3">
<Container>
<p className="text-center mb-0 fs-14 text-secondary">
<Trans i18nKey="footer.build_on">
<Trans i18nKey="footer.build_on" values={{ cc }}>
Built on
{/* eslint-disable-next-line react/jsx-no-target-blank */}
<a href="https://answer.dev/" target="_blank">
@ -15,7 +23,7 @@ const Index = () => {
</a>
- the open-source software that powers Q&A communities.
<br />
Made with love. © 2022 Answer .
Made with love. © 2022 Answer.
</Trans>
</p>
</Container>

View File

@ -20,6 +20,7 @@ import {
import { loggedUserInfoStore, siteInfoStore, brandingStore } from '@/stores';
import { logout, useQueryNotificationStatus } from '@/services';
import { RouteAlias } from '@/router/alias';
import { DEFAULT_SITE_NAME } from '@/common/constants';
import NavItems from './components/NavItems';
@ -88,7 +89,7 @@ const Header: FC = () => {
/>
</>
) : (
<span>{siteInfo.name || 'Answer'}</span>
<span>{siteInfo.name || DEFAULT_SITE_NAME}</span>
)}
</Navbar.Brand>

View File

@ -2,6 +2,8 @@ import { FC } from 'react';
import { Form, Button, Stack } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import classnames from 'classnames';
import BrandUpload from '../BrandUpload';
import TimeZonePicker from '../TimeZonePicker';
import type * as Type from '@/common/interface';
@ -52,7 +54,10 @@ export interface UISchema {
| 'url'
| 'week';
empty?: string;
validator?: (value) => Promise<string | true | void> | true | string;
validator?: (
value,
formData?,
) => Promise<string | true | void> | true | string;
textRender?: () => React.ReactElement;
imageType?: Type.UploadType;
acceptType?: string;
@ -153,7 +158,7 @@ const SchemaForm: FC<IProps> = ({
const value = formData[key]?.value;
promises.push({
key,
promise: validator(value),
promise: validator(value, formData),
});
}
});
@ -263,7 +268,10 @@ const SchemaForm: FC<IProps> = ({
uiSchema[key] || {};
if (widget === 'select') {
return (
<Form.Group key={title} controlId={key} className="mb-3">
<Form.Group
key={title}
controlId={key}
className={classnames('mb-3', formData[key].hidden && 'd-none')}>
<Form.Label>{title}</Form.Label>
<Form.Select
aria-label={description}
@ -282,13 +290,18 @@ const SchemaForm: FC<IProps> = ({
<Form.Control.Feedback type="invalid">
{formData[key]?.errorMsg}
</Form.Control.Feedback>
<Form.Text>{description}</Form.Text>
{description && (
<Form.Text className="text-muted">{description}</Form.Text>
)}
</Form.Group>
);
}
if (widget === 'checkbox' || widget === 'radio') {
return (
<Form.Group key={title} className="mb-3" controlId={key}>
<Form.Group
key={title}
className={classnames('mb-3', formData[key].hidden && 'd-none')}
controlId={key}>
<Form.Label>{title}</Form.Label>
<Stack direction="horizontal">
{properties[key].enum?.map((item, index) => {
@ -313,14 +326,19 @@ const SchemaForm: FC<IProps> = ({
<Form.Control.Feedback type="invalid">
{formData[key]?.errorMsg}
</Form.Control.Feedback>
<Form.Text>{description}</Form.Text>
{description && (
<Form.Text className="text-muted">{description}</Form.Text>
)}
</Form.Group>
);
}
if (widget === 'switch') {
return (
<Form.Group key={title} className="mb-3" controlId={key}>
<Form.Group
key={title}
className={classnames('mb-3', formData[key].hidden && 'd-none')}
controlId={key}>
<Form.Label>{title}</Form.Label>
<Form.Check
required
@ -337,13 +355,18 @@ const SchemaForm: FC<IProps> = ({
<Form.Control.Feedback type="invalid">
{formData[key]?.errorMsg}
</Form.Control.Feedback>
<Form.Text>{description}</Form.Text>
{description && (
<Form.Text className="text-muted">{description}</Form.Text>
)}
</Form.Group>
);
}
if (widget === 'timezone') {
return (
<Form.Group key={title} className="mb-3" controlId={key}>
<Form.Group
key={title}
className={classnames('mb-3', formData[key].hidden && 'd-none')}
controlId={key}>
<Form.Label>{title}</Form.Label>
<TimeZonePicker
value={formData[key]?.value}
@ -358,14 +381,19 @@ const SchemaForm: FC<IProps> = ({
<Form.Control.Feedback type="invalid">
{formData[key]?.errorMsg}
</Form.Control.Feedback>
<Form.Text>{description}</Form.Text>
{description && (
<Form.Text className="text-muted">{description}</Form.Text>
)}
</Form.Group>
);
}
if (widget === 'upload') {
return (
<Form.Group key={title} className="mb-3" controlId={key}>
<Form.Group
key={title}
className={classnames('mb-3', formData[key].hidden && 'd-none')}
controlId={key}>
<Form.Label>{title}</Form.Label>
<BrandUpload
type={options.imageType || 'avatar'}
@ -381,14 +409,19 @@ const SchemaForm: FC<IProps> = ({
<Form.Control.Feedback type="invalid">
{formData[key]?.errorMsg}
</Form.Control.Feedback>
<Form.Text>{description}</Form.Text>
{description && (
<Form.Text className="text-muted">{description}</Form.Text>
)}
</Form.Group>
);
}
if (widget === 'textarea') {
return (
<Form.Group controlId={key} key={key} className="mb-3">
<Form.Group
controlId={key}
key={key}
className={classnames('mb-3', formData[key].hidden && 'd-none')}>
<Form.Label>{title}</Form.Label>
<Form.Control
as="textarea"
@ -404,12 +437,17 @@ const SchemaForm: FC<IProps> = ({
{formData[key]?.errorMsg}
</Form.Control.Feedback>
<Form.Text>{description}</Form.Text>
{description && (
<Form.Text className="text-muted">{description}</Form.Text>
)}
</Form.Group>
);
}
return (
<Form.Group controlId={key} key={key} className="mb-3">
<Form.Group
controlId={key}
key={key}
className={classnames('mb-3', formData[key].hidden && 'd-none')}>
<Form.Label>{title}</Form.Label>
<Form.Control
name={key}
@ -424,7 +462,9 @@ const SchemaForm: FC<IProps> = ({
{formData[key]?.errorMsg}
</Form.Control.Feedback>
<Form.Text>{description}</Form.Text>
{description && (
<Form.Text className="text-muted">{description}</Form.Text>
)}
</Form.Group>
);
})}

View File

@ -1,8 +1,10 @@
import { FC } from 'react';
import { Button, Form, Table, Stack, Badge } from 'react-bootstrap';
import { Button, Form, Table, Stack } from 'react-bootstrap';
import { useSearchParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import classNames from 'classnames';
import {
FormatTime,
Icon,
@ -159,9 +161,13 @@ const Answers: FC = () => {
</Stack>
</td>
<td>
<Badge bg={ADMIN_LIST_STATUS[curFilter]?.variant}>
<span
className={classNames(
'badge',
ADMIN_LIST_STATUS[curFilter]?.variant,
)}>
{t(ADMIN_LIST_STATUS[curFilter]?.name)}
</Badge>
</span>
</td>
{curFilter !== 'deleted' && (
<td>

View File

@ -1,5 +1,5 @@
import { FC } from 'react';
import { Card, Row, Col, Badge } from 'react-bootstrap';
import { Card, Row, Col } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
@ -29,38 +29,31 @@ const HealthStatus: FC<IProps> = ({ data }) => {
<span className="text-secondary me-1">{t('version')}</span>
<strong>{version}</strong>
{isLatest && (
<Badge
pill
bg="success"
className="ms-1"
as="a"
<a
className="ms-1 badge rounded-pill text-bg-success"
target="_blank"
href="https://github.com/answerdev/answer/releases">
href="https://github.com/answerdev/answer/releases"
rel="noreferrer">
{t('latest')}
</Badge>
</a>
)}
{!isLatest && hasNewerVersion && (
<Badge
pill
bg="warning"
text="dark"
className="ms-1"
as="a"
<a
className="ms-1 badge rounded-pill text-bg-warning"
target="_blank"
href="https://github.com/answerdev/answer/releases">
href="https://github.com/answerdev/answer/releases"
rel="noreferrer">
{t('update_to')} {remote_version}
</Badge>
</a>
)}
{!isLatest && !remote_version && (
<Badge
pill
bg="danger"
className="ms-1"
as="a"
<a
className="ms-1 badge rounded-pill text-bg-danger"
target="_blank"
href="https://github.com/answerdev/answer/releases">
href="https://github.com/answerdev/answer/releases"
rel="noreferrer">
{t('check_failed')}
</Badge>
</a>
)}
</Col>
<Col xs={6} className="mb-1">

View File

@ -1,8 +1,10 @@
import { FC } from 'react';
import { Button, Form, Table, Stack, Badge } from 'react-bootstrap';
import { Button, Form, Table, Stack } from 'react-bootstrap';
import { Link, useSearchParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import classNames from 'classnames';
import {
FormatTime,
Icon,
@ -167,9 +169,13 @@ const Questions: FC = () => {
</Stack>
</td>
<td>
<Badge bg={ADMIN_LIST_STATUS[curFilter]?.variant}>
<span
className={classNames(
'badge',
ADMIN_LIST_STATUS[curFilter]?.variant,
)}>
{t(ADMIN_LIST_STATUS[curFilter]?.name)}
</Badge>
</span>
</td>
{curFilter !== 'deleted' && (
<td>

View File

@ -37,8 +37,8 @@ const Smtp: FC = () => {
type: 'boolean',
title: t('encryption.label'),
description: t('encryption.text'),
enum: ['SSL', ''],
enumNames: ['SSL', ''],
enum: ['SSL', 'None'],
enumNames: ['SSL', 'None'],
},
smtp_port: {
type: 'string',
@ -54,12 +54,10 @@ const Smtp: FC = () => {
smtp_username: {
type: 'string',
title: t('smtp_username.label'),
description: t('smtp_username.text'),
},
smtp_password: {
type: 'string',
title: t('smtp_password.label'),
description: t('smtp_password.text'),
},
test_email_recipient: {
type: 'string',
@ -70,15 +68,35 @@ const Smtp: FC = () => {
};
const uiSchema: UISchema = {
encryption: {
'ui:widget': 'radio',
'ui:widget': 'select',
},
smtp_username: {
'ui:options': {
validator: (value: string, formData) => {
if (formData.smtp_authentication.value) {
if (!value) {
return t('smtp_username.msg');
}
}
return true;
},
},
},
smtp_password: {
'ui:options': {
type: 'password',
validator: (value: string, formData) => {
if (formData.smtp_authentication.value) {
if (!value) {
return t('smtp_password.msg');
}
}
return true;
},
},
},
smtp_authentication: {
'ui:widget': 'radio',
'ui:widget': 'switch',
},
smtp_port: {
'ui:options': {
@ -116,8 +134,12 @@ const Smtp: FC = () => {
encryption: formData.encryption.value,
smtp_port: Number(formData.smtp_port.value),
smtp_authentication: formData.smtp_authentication.value,
smtp_username: formData.smtp_username.value,
smtp_password: formData.smtp_password.value,
...(formData.smtp_authentication.value
? { smtp_username: formData.smtp_username.value }
: {}),
...(formData.smtp_authentication.value
? { smtp_password: formData.smtp_password.value }
: {}),
test_email_recipient: formData.test_email_recipient.value,
};
@ -151,6 +173,22 @@ const Smtp: FC = () => {
setFormData(formState);
}, [setting]);
useEffect(() => {
if (formData.smtp_authentication.value) {
setFormData({
...formData,
smtp_username: { ...formData.smtp_username, hidden: false },
smtp_password: { ...formData.smtp_password, hidden: false },
});
} else {
setFormData({
...formData,
smtp_username: { ...formData.smtp_username, hidden: true },
smtp_password: { ...formData.smtp_password, hidden: true },
});
}
}, [formData.smtp_authentication]);
const handleOnChange = (data) => {
setFormData(data);
};

View File

@ -1,8 +1,10 @@
import { FC } from 'react';
import { Button, Form, Table, Badge } from 'react-bootstrap';
import { Button, Form, Table } from 'react-bootstrap';
import { useSearchParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import classNames from 'classnames';
import {
Pagination,
FormatTime,
@ -24,10 +26,10 @@ const UserFilterKeys: Type.UserFilterBy[] = [
];
const bgMap = {
normal: 'success',
suspended: 'danger',
deleted: 'danger',
inactive: 'secondary',
normal: 'text-bg-success',
suspended: 'text-bg-danger',
deleted: 'text-bg-danger',
inactive: 'text-bg-secondary',
};
const PAGE_SIZE = 10;
@ -132,7 +134,9 @@ const Users: FC = () => {
</td>
)}
<td>
<Badge bg={bgMap[user.status]}>{t(user.status)}</Badge>
<span className={classNames('badge', bgMap[user.status])}>
{t(user.status)}
</span>
</td>
{curFilter !== 'deleted' ? (
<td>

View File

@ -1,13 +1,5 @@
import { FC, useEffect, useState } from 'react';
import {
Container,
Row,
Col,
Alert,
Badge,
Stack,
Button,
} from 'react-bootstrap';
import { Container, Row, Col, Alert, Stack, Button } from 'react-bootstrap';
import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
@ -120,9 +112,7 @@ const Index: FC = () => {
<Col lg={{ span: 7, offset: 1 }}>
<Alert variant="secondary">
<Stack className="align-items-start">
<Badge bg="secondary" className="mb-2">
{editBadge}
</Badge>
<span className="badge text-bg-secondary">{editBadge}</span>
<Link to={itemLink} target="_blank">
{itemTitle}
</Link>

View File

@ -1,5 +1,5 @@
import { memo, FC } from 'react';
import { ListGroupItem, Badge } from 'react-bootstrap';
import { ListGroupItem } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import { Icon, Tag, FormatTime, BaseUserCard } from '@/components';
@ -21,12 +21,11 @@ const Index: FC<Props> = ({ data }) => {
return (
<ListGroupItem className="py-3 px-0">
<div className="mb-2 clearfix">
<Badge
bg="dark"
className="me-2 float-start"
<span
className="float-start me-2 badge text-bg-dark"
style={{ marginTop: '2px' }}>
{data.object_type === 'question' ? 'Q' : 'A'}
</Badge>
</span>
<a className="h5 mb-0 link-dark text-break" href={itemUrl}>
{data.object.title}
{data.object.status === 'closed'

View File

@ -1,5 +1,5 @@
import { FC, memo } from 'react';
import { Badge, OverlayTrigger, Tooltip } from 'react-bootstrap';
import { OverlayTrigger, Tooltip } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
@ -42,9 +42,7 @@ const Index: FC<Props> = ({ data }) => {
<OverlayTrigger
placement="top"
overlay={<Tooltip>{t('mod_long')}</Tooltip>}>
<Badge bg="light" className="text-body">
{t('mod_short')}
</Badge>
<span className="badge text-bg-light">{t('mod_short')}</span>
</OverlayTrigger>
</div>
)}