mirror of https://gitee.com/answerdev/answer.git
Merge branch 'feat/ui-0.5.0' into test
This commit is contained in:
commit
5451625725
|
@ -551,7 +551,7 @@ ui:
|
||||||
footer:
|
footer:
|
||||||
build_on: >-
|
build_on: >-
|
||||||
Built on <1> Answer </1>- the open-source software that power Q&A
|
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:
|
upload_img:
|
||||||
name: Change
|
name: Change
|
||||||
loading: loading...
|
loading: loading...
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
export const DEFAULT_SITE_NAME = 'Answer';
|
||||||
export const DEFAULT_LANG = 'en_US';
|
export const DEFAULT_LANG = 'en_US';
|
||||||
export const CURRENT_LANG_STORAGE_KEY = '_a_lang_';
|
export const CURRENT_LANG_STORAGE_KEY = '_a_lang_';
|
||||||
export const LANG_RESOURCE_STORAGE_KEY = '_a_lang_r_';
|
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 = {
|
export const ADMIN_LIST_STATUS = {
|
||||||
// normal;
|
// normal;
|
||||||
1: {
|
1: {
|
||||||
variant: 'success',
|
variant: 'text-bg-success',
|
||||||
name: 'normal',
|
name: 'normal',
|
||||||
},
|
},
|
||||||
// closed;
|
// closed;
|
||||||
2: {
|
2: {
|
||||||
variant: 'warning',
|
variant: 'text-bg-warning',
|
||||||
name: 'closed',
|
name: 'closed',
|
||||||
},
|
},
|
||||||
// deleted
|
// deleted
|
||||||
10: {
|
10: {
|
||||||
variant: 'danger',
|
variant: 'text-bg-danger',
|
||||||
name: 'deleted',
|
name: 'deleted',
|
||||||
},
|
},
|
||||||
normal: {
|
normal: {
|
||||||
variant: 'success',
|
variant: 'text-bg-success',
|
||||||
name: 'normal',
|
name: 'normal',
|
||||||
},
|
},
|
||||||
closed: {
|
closed: {
|
||||||
variant: 'warning',
|
variant: 'text-bg-warning',
|
||||||
name: 'closed',
|
name: 'closed',
|
||||||
},
|
},
|
||||||
deleted: {
|
deleted: {
|
||||||
variant: 'danger',
|
variant: 'text-bg-danger',
|
||||||
name: 'deleted',
|
name: 'deleted',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -287,9 +287,9 @@ export interface AdminSettingsSmtp {
|
||||||
from_name: string;
|
from_name: string;
|
||||||
smtp_authentication: boolean;
|
smtp_authentication: boolean;
|
||||||
smtp_host: string;
|
smtp_host: string;
|
||||||
smtp_password: string;
|
smtp_password?: string;
|
||||||
smtp_port: number;
|
smtp_port: number;
|
||||||
smtp_username: string;
|
smtp_username?: string;
|
||||||
test_email_recipient?: string;
|
test_email_recipient?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { FC } from 'react';
|
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 { useTranslation } from 'react-i18next';
|
||||||
import { useNavigate, useMatch } from 'react-router-dom';
|
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}
|
{!isLeaf ? <Icon name="chevron-right" className="me-1" /> : null}
|
||||||
{t(menu.name)}
|
{t(menu.name)}
|
||||||
{menu.badgeContent ? (
|
{menu.badgeContent ? (
|
||||||
<Badge bg="dark" className="ms-auto top-0">
|
<span className="badge text-bg-dark ms-auto top-0">
|
||||||
{menu.badgeContent}
|
{menu.badgeContent}
|
||||||
</Badge>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -2,12 +2,20 @@ import React from 'react';
|
||||||
import { Container } from 'react-bootstrap';
|
import { Container } from 'react-bootstrap';
|
||||||
import { Trans } from 'react-i18next';
|
import { Trans } from 'react-i18next';
|
||||||
|
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
import { siteInfoStore } from '@/stores';
|
||||||
|
import { DEFAULT_SITE_NAME } from '@/common/constants';
|
||||||
|
|
||||||
const Index = () => {
|
const Index = () => {
|
||||||
|
const fullYear = dayjs().format('YYYY');
|
||||||
|
const siteName = siteInfoStore.getState().siteInfo.name || DEFAULT_SITE_NAME;
|
||||||
|
const cc = `${fullYear} ${siteName}`;
|
||||||
return (
|
return (
|
||||||
<footer className="bg-light py-3">
|
<footer className="bg-light py-3">
|
||||||
<Container>
|
<Container>
|
||||||
<p className="text-center mb-0 fs-14 text-secondary">
|
<p className="text-center mb-0 fs-14 text-secondary">
|
||||||
<Trans i18nKey="footer.build_on">
|
<Trans i18nKey="footer.build_on" values={{ cc }}>
|
||||||
Built on
|
Built on
|
||||||
{/* eslint-disable-next-line react/jsx-no-target-blank */}
|
{/* eslint-disable-next-line react/jsx-no-target-blank */}
|
||||||
<a href="https://answer.dev/" target="_blank">
|
<a href="https://answer.dev/" target="_blank">
|
||||||
|
@ -15,7 +23,7 @@ const Index = () => {
|
||||||
</a>
|
</a>
|
||||||
- the open-source software that powers Q&A communities.
|
- the open-source software that powers Q&A communities.
|
||||||
<br />
|
<br />
|
||||||
Made with love. © 2022 Answer .
|
Made with love. © 2022 Answer.
|
||||||
</Trans>
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
</Container>
|
</Container>
|
||||||
|
|
|
@ -20,6 +20,7 @@ import {
|
||||||
import { loggedUserInfoStore, siteInfoStore, brandingStore } from '@/stores';
|
import { loggedUserInfoStore, siteInfoStore, brandingStore } from '@/stores';
|
||||||
import { logout, useQueryNotificationStatus } from '@/services';
|
import { logout, useQueryNotificationStatus } from '@/services';
|
||||||
import { RouteAlias } from '@/router/alias';
|
import { RouteAlias } from '@/router/alias';
|
||||||
|
import { DEFAULT_SITE_NAME } from '@/common/constants';
|
||||||
|
|
||||||
import NavItems from './components/NavItems';
|
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>
|
</Navbar.Brand>
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,8 @@ import { FC } from 'react';
|
||||||
import { Form, Button, Stack } from 'react-bootstrap';
|
import { Form, Button, Stack } from 'react-bootstrap';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import classnames from 'classnames';
|
||||||
|
|
||||||
import BrandUpload from '../BrandUpload';
|
import BrandUpload from '../BrandUpload';
|
||||||
import TimeZonePicker from '../TimeZonePicker';
|
import TimeZonePicker from '../TimeZonePicker';
|
||||||
import type * as Type from '@/common/interface';
|
import type * as Type from '@/common/interface';
|
||||||
|
@ -52,7 +54,10 @@ export interface UISchema {
|
||||||
| 'url'
|
| 'url'
|
||||||
| 'week';
|
| 'week';
|
||||||
empty?: string;
|
empty?: string;
|
||||||
validator?: (value) => Promise<string | true | void> | true | string;
|
validator?: (
|
||||||
|
value,
|
||||||
|
formData?,
|
||||||
|
) => Promise<string | true | void> | true | string;
|
||||||
textRender?: () => React.ReactElement;
|
textRender?: () => React.ReactElement;
|
||||||
imageType?: Type.UploadType;
|
imageType?: Type.UploadType;
|
||||||
acceptType?: string;
|
acceptType?: string;
|
||||||
|
@ -153,7 +158,7 @@ const SchemaForm: FC<IProps> = ({
|
||||||
const value = formData[key]?.value;
|
const value = formData[key]?.value;
|
||||||
promises.push({
|
promises.push({
|
||||||
key,
|
key,
|
||||||
promise: validator(value),
|
promise: validator(value, formData),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -263,7 +268,10 @@ const SchemaForm: FC<IProps> = ({
|
||||||
uiSchema[key] || {};
|
uiSchema[key] || {};
|
||||||
if (widget === 'select') {
|
if (widget === 'select') {
|
||||||
return (
|
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.Label>{title}</Form.Label>
|
||||||
<Form.Select
|
<Form.Select
|
||||||
aria-label={description}
|
aria-label={description}
|
||||||
|
@ -282,13 +290,18 @@ const SchemaForm: FC<IProps> = ({
|
||||||
<Form.Control.Feedback type="invalid">
|
<Form.Control.Feedback type="invalid">
|
||||||
{formData[key]?.errorMsg}
|
{formData[key]?.errorMsg}
|
||||||
</Form.Control.Feedback>
|
</Form.Control.Feedback>
|
||||||
<Form.Text>{description}</Form.Text>
|
{description && (
|
||||||
|
<Form.Text className="text-muted">{description}</Form.Text>
|
||||||
|
)}
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (widget === 'checkbox' || widget === 'radio') {
|
if (widget === 'checkbox' || widget === 'radio') {
|
||||||
return (
|
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.Label>{title}</Form.Label>
|
||||||
<Stack direction="horizontal">
|
<Stack direction="horizontal">
|
||||||
{properties[key].enum?.map((item, index) => {
|
{properties[key].enum?.map((item, index) => {
|
||||||
|
@ -313,14 +326,19 @@ const SchemaForm: FC<IProps> = ({
|
||||||
<Form.Control.Feedback type="invalid">
|
<Form.Control.Feedback type="invalid">
|
||||||
{formData[key]?.errorMsg}
|
{formData[key]?.errorMsg}
|
||||||
</Form.Control.Feedback>
|
</Form.Control.Feedback>
|
||||||
<Form.Text>{description}</Form.Text>
|
{description && (
|
||||||
|
<Form.Text className="text-muted">{description}</Form.Text>
|
||||||
|
)}
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (widget === 'switch') {
|
if (widget === 'switch') {
|
||||||
return (
|
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.Label>{title}</Form.Label>
|
||||||
<Form.Check
|
<Form.Check
|
||||||
required
|
required
|
||||||
|
@ -337,13 +355,18 @@ const SchemaForm: FC<IProps> = ({
|
||||||
<Form.Control.Feedback type="invalid">
|
<Form.Control.Feedback type="invalid">
|
||||||
{formData[key]?.errorMsg}
|
{formData[key]?.errorMsg}
|
||||||
</Form.Control.Feedback>
|
</Form.Control.Feedback>
|
||||||
<Form.Text>{description}</Form.Text>
|
{description && (
|
||||||
|
<Form.Text className="text-muted">{description}</Form.Text>
|
||||||
|
)}
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (widget === 'timezone') {
|
if (widget === 'timezone') {
|
||||||
return (
|
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.Label>{title}</Form.Label>
|
||||||
<TimeZonePicker
|
<TimeZonePicker
|
||||||
value={formData[key]?.value}
|
value={formData[key]?.value}
|
||||||
|
@ -358,14 +381,19 @@ const SchemaForm: FC<IProps> = ({
|
||||||
<Form.Control.Feedback type="invalid">
|
<Form.Control.Feedback type="invalid">
|
||||||
{formData[key]?.errorMsg}
|
{formData[key]?.errorMsg}
|
||||||
</Form.Control.Feedback>
|
</Form.Control.Feedback>
|
||||||
<Form.Text>{description}</Form.Text>
|
{description && (
|
||||||
|
<Form.Text className="text-muted">{description}</Form.Text>
|
||||||
|
)}
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (widget === 'upload') {
|
if (widget === 'upload') {
|
||||||
return (
|
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.Label>{title}</Form.Label>
|
||||||
<BrandUpload
|
<BrandUpload
|
||||||
type={options.imageType || 'avatar'}
|
type={options.imageType || 'avatar'}
|
||||||
|
@ -381,14 +409,19 @@ const SchemaForm: FC<IProps> = ({
|
||||||
<Form.Control.Feedback type="invalid">
|
<Form.Control.Feedback type="invalid">
|
||||||
{formData[key]?.errorMsg}
|
{formData[key]?.errorMsg}
|
||||||
</Form.Control.Feedback>
|
</Form.Control.Feedback>
|
||||||
<Form.Text>{description}</Form.Text>
|
{description && (
|
||||||
|
<Form.Text className="text-muted">{description}</Form.Text>
|
||||||
|
)}
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (widget === 'textarea') {
|
if (widget === 'textarea') {
|
||||||
return (
|
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.Label>{title}</Form.Label>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
as="textarea"
|
as="textarea"
|
||||||
|
@ -404,12 +437,17 @@ const SchemaForm: FC<IProps> = ({
|
||||||
{formData[key]?.errorMsg}
|
{formData[key]?.errorMsg}
|
||||||
</Form.Control.Feedback>
|
</Form.Control.Feedback>
|
||||||
|
|
||||||
<Form.Text>{description}</Form.Text>
|
{description && (
|
||||||
|
<Form.Text className="text-muted">{description}</Form.Text>
|
||||||
|
)}
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
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.Label>{title}</Form.Label>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
name={key}
|
name={key}
|
||||||
|
@ -424,7 +462,9 @@ const SchemaForm: FC<IProps> = ({
|
||||||
{formData[key]?.errorMsg}
|
{formData[key]?.errorMsg}
|
||||||
</Form.Control.Feedback>
|
</Form.Control.Feedback>
|
||||||
|
|
||||||
<Form.Text>{description}</Form.Text>
|
{description && (
|
||||||
|
<Form.Text className="text-muted">{description}</Form.Text>
|
||||||
|
)}
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import { FC } from 'react';
|
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 { useSearchParams } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
FormatTime,
|
FormatTime,
|
||||||
Icon,
|
Icon,
|
||||||
|
@ -159,9 +161,13 @@ const Answers: FC = () => {
|
||||||
</Stack>
|
</Stack>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<Badge bg={ADMIN_LIST_STATUS[curFilter]?.variant}>
|
<span
|
||||||
|
className={classNames(
|
||||||
|
'badge',
|
||||||
|
ADMIN_LIST_STATUS[curFilter]?.variant,
|
||||||
|
)}>
|
||||||
{t(ADMIN_LIST_STATUS[curFilter]?.name)}
|
{t(ADMIN_LIST_STATUS[curFilter]?.name)}
|
||||||
</Badge>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
{curFilter !== 'deleted' && (
|
{curFilter !== 'deleted' && (
|
||||||
<td>
|
<td>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { FC } from 'react';
|
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 { useTranslation } from 'react-i18next';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
@ -29,38 +29,31 @@ const HealthStatus: FC<IProps> = ({ data }) => {
|
||||||
<span className="text-secondary me-1">{t('version')}</span>
|
<span className="text-secondary me-1">{t('version')}</span>
|
||||||
<strong>{version}</strong>
|
<strong>{version}</strong>
|
||||||
{isLatest && (
|
{isLatest && (
|
||||||
<Badge
|
<a
|
||||||
pill
|
className="ms-1 badge rounded-pill text-bg-success"
|
||||||
bg="success"
|
|
||||||
className="ms-1"
|
|
||||||
as="a"
|
|
||||||
target="_blank"
|
target="_blank"
|
||||||
href="https://github.com/answerdev/answer/releases">
|
href="https://github.com/answerdev/answer/releases"
|
||||||
|
rel="noreferrer">
|
||||||
{t('latest')}
|
{t('latest')}
|
||||||
</Badge>
|
</a>
|
||||||
)}
|
)}
|
||||||
{!isLatest && hasNewerVersion && (
|
{!isLatest && hasNewerVersion && (
|
||||||
<Badge
|
<a
|
||||||
pill
|
className="ms-1 badge rounded-pill text-bg-warning"
|
||||||
bg="warning"
|
|
||||||
text="dark"
|
|
||||||
className="ms-1"
|
|
||||||
as="a"
|
|
||||||
target="_blank"
|
target="_blank"
|
||||||
href="https://github.com/answerdev/answer/releases">
|
href="https://github.com/answerdev/answer/releases"
|
||||||
|
rel="noreferrer">
|
||||||
{t('update_to')} {remote_version}
|
{t('update_to')} {remote_version}
|
||||||
</Badge>
|
</a>
|
||||||
)}
|
)}
|
||||||
{!isLatest && !remote_version && (
|
{!isLatest && !remote_version && (
|
||||||
<Badge
|
<a
|
||||||
pill
|
className="ms-1 badge rounded-pill text-bg-danger"
|
||||||
bg="danger"
|
|
||||||
className="ms-1"
|
|
||||||
as="a"
|
|
||||||
target="_blank"
|
target="_blank"
|
||||||
href="https://github.com/answerdev/answer/releases">
|
href="https://github.com/answerdev/answer/releases"
|
||||||
|
rel="noreferrer">
|
||||||
{t('check_failed')}
|
{t('check_failed')}
|
||||||
</Badge>
|
</a>
|
||||||
)}
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={6} className="mb-1">
|
<Col xs={6} className="mb-1">
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import { FC } from 'react';
|
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 { Link, useSearchParams } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
FormatTime,
|
FormatTime,
|
||||||
Icon,
|
Icon,
|
||||||
|
@ -167,9 +169,13 @@ const Questions: FC = () => {
|
||||||
</Stack>
|
</Stack>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<Badge bg={ADMIN_LIST_STATUS[curFilter]?.variant}>
|
<span
|
||||||
|
className={classNames(
|
||||||
|
'badge',
|
||||||
|
ADMIN_LIST_STATUS[curFilter]?.variant,
|
||||||
|
)}>
|
||||||
{t(ADMIN_LIST_STATUS[curFilter]?.name)}
|
{t(ADMIN_LIST_STATUS[curFilter]?.name)}
|
||||||
</Badge>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
{curFilter !== 'deleted' && (
|
{curFilter !== 'deleted' && (
|
||||||
<td>
|
<td>
|
||||||
|
|
|
@ -37,8 +37,8 @@ const Smtp: FC = () => {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
title: t('encryption.label'),
|
title: t('encryption.label'),
|
||||||
description: t('encryption.text'),
|
description: t('encryption.text'),
|
||||||
enum: ['SSL', ''],
|
enum: ['SSL', 'None'],
|
||||||
enumNames: ['SSL', ''],
|
enumNames: ['SSL', 'None'],
|
||||||
},
|
},
|
||||||
smtp_port: {
|
smtp_port: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
@ -54,12 +54,10 @@ const Smtp: FC = () => {
|
||||||
smtp_username: {
|
smtp_username: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
title: t('smtp_username.label'),
|
title: t('smtp_username.label'),
|
||||||
description: t('smtp_username.text'),
|
|
||||||
},
|
},
|
||||||
smtp_password: {
|
smtp_password: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
title: t('smtp_password.label'),
|
title: t('smtp_password.label'),
|
||||||
description: t('smtp_password.text'),
|
|
||||||
},
|
},
|
||||||
test_email_recipient: {
|
test_email_recipient: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
@ -70,15 +68,35 @@ const Smtp: FC = () => {
|
||||||
};
|
};
|
||||||
const uiSchema: UISchema = {
|
const uiSchema: UISchema = {
|
||||||
encryption: {
|
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: {
|
smtp_password: {
|
||||||
'ui:options': {
|
'ui:options': {
|
||||||
type: 'password',
|
type: 'password',
|
||||||
|
validator: (value: string, formData) => {
|
||||||
|
if (formData.smtp_authentication.value) {
|
||||||
|
if (!value) {
|
||||||
|
return t('smtp_password.msg');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
smtp_authentication: {
|
smtp_authentication: {
|
||||||
'ui:widget': 'radio',
|
'ui:widget': 'switch',
|
||||||
},
|
},
|
||||||
smtp_port: {
|
smtp_port: {
|
||||||
'ui:options': {
|
'ui:options': {
|
||||||
|
@ -116,8 +134,12 @@ const Smtp: FC = () => {
|
||||||
encryption: formData.encryption.value,
|
encryption: formData.encryption.value,
|
||||||
smtp_port: Number(formData.smtp_port.value),
|
smtp_port: Number(formData.smtp_port.value),
|
||||||
smtp_authentication: formData.smtp_authentication.value,
|
smtp_authentication: formData.smtp_authentication.value,
|
||||||
smtp_username: formData.smtp_username.value,
|
...(formData.smtp_authentication.value
|
||||||
smtp_password: formData.smtp_password.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,
|
test_email_recipient: formData.test_email_recipient.value,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -151,6 +173,22 @@ const Smtp: FC = () => {
|
||||||
setFormData(formState);
|
setFormData(formState);
|
||||||
}, [setting]);
|
}, [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) => {
|
const handleOnChange = (data) => {
|
||||||
setFormData(data);
|
setFormData(data);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import { FC } from 'react';
|
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 { useSearchParams } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Pagination,
|
Pagination,
|
||||||
FormatTime,
|
FormatTime,
|
||||||
|
@ -24,10 +26,10 @@ const UserFilterKeys: Type.UserFilterBy[] = [
|
||||||
];
|
];
|
||||||
|
|
||||||
const bgMap = {
|
const bgMap = {
|
||||||
normal: 'success',
|
normal: 'text-bg-success',
|
||||||
suspended: 'danger',
|
suspended: 'text-bg-danger',
|
||||||
deleted: 'danger',
|
deleted: 'text-bg-danger',
|
||||||
inactive: 'secondary',
|
inactive: 'text-bg-secondary',
|
||||||
};
|
};
|
||||||
|
|
||||||
const PAGE_SIZE = 10;
|
const PAGE_SIZE = 10;
|
||||||
|
@ -132,7 +134,9 @@ const Users: FC = () => {
|
||||||
</td>
|
</td>
|
||||||
)}
|
)}
|
||||||
<td>
|
<td>
|
||||||
<Badge bg={bgMap[user.status]}>{t(user.status)}</Badge>
|
<span className={classNames('badge', bgMap[user.status])}>
|
||||||
|
{t(user.status)}
|
||||||
|
</span>
|
||||||
</td>
|
</td>
|
||||||
{curFilter !== 'deleted' ? (
|
{curFilter !== 'deleted' ? (
|
||||||
<td>
|
<td>
|
||||||
|
|
|
@ -1,13 +1,5 @@
|
||||||
import { FC, useEffect, useState } from 'react';
|
import { FC, useEffect, useState } from 'react';
|
||||||
import {
|
import { Container, Row, Col, Alert, Stack, Button } from 'react-bootstrap';
|
||||||
Container,
|
|
||||||
Row,
|
|
||||||
Col,
|
|
||||||
Alert,
|
|
||||||
Badge,
|
|
||||||
Stack,
|
|
||||||
Button,
|
|
||||||
} from 'react-bootstrap';
|
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
@ -120,9 +112,7 @@ const Index: FC = () => {
|
||||||
<Col lg={{ span: 7, offset: 1 }}>
|
<Col lg={{ span: 7, offset: 1 }}>
|
||||||
<Alert variant="secondary">
|
<Alert variant="secondary">
|
||||||
<Stack className="align-items-start">
|
<Stack className="align-items-start">
|
||||||
<Badge bg="secondary" className="mb-2">
|
<span className="badge text-bg-secondary">{editBadge}</span>
|
||||||
{editBadge}
|
|
||||||
</Badge>
|
|
||||||
<Link to={itemLink} target="_blank">
|
<Link to={itemLink} target="_blank">
|
||||||
{itemTitle}
|
{itemTitle}
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { memo, FC } from 'react';
|
import { memo, FC } from 'react';
|
||||||
import { ListGroupItem, Badge } from 'react-bootstrap';
|
import { ListGroupItem } from 'react-bootstrap';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { Icon, Tag, FormatTime, BaseUserCard } from '@/components';
|
import { Icon, Tag, FormatTime, BaseUserCard } from '@/components';
|
||||||
|
@ -21,12 +21,11 @@ const Index: FC<Props> = ({ data }) => {
|
||||||
return (
|
return (
|
||||||
<ListGroupItem className="py-3 px-0">
|
<ListGroupItem className="py-3 px-0">
|
||||||
<div className="mb-2 clearfix">
|
<div className="mb-2 clearfix">
|
||||||
<Badge
|
<span
|
||||||
bg="dark"
|
className="float-start me-2 badge text-bg-dark"
|
||||||
className="me-2 float-start"
|
|
||||||
style={{ marginTop: '2px' }}>
|
style={{ marginTop: '2px' }}>
|
||||||
{data.object_type === 'question' ? 'Q' : 'A'}
|
{data.object_type === 'question' ? 'Q' : 'A'}
|
||||||
</Badge>
|
</span>
|
||||||
<a className="h5 mb-0 link-dark text-break" href={itemUrl}>
|
<a className="h5 mb-0 link-dark text-break" href={itemUrl}>
|
||||||
{data.object.title}
|
{data.object.title}
|
||||||
{data.object.status === 'closed'
|
{data.object.status === 'closed'
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { FC, memo } from 'react';
|
import { FC, memo } from 'react';
|
||||||
import { Badge, OverlayTrigger, Tooltip } from 'react-bootstrap';
|
import { OverlayTrigger, Tooltip } from 'react-bootstrap';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
@ -42,9 +42,7 @@ const Index: FC<Props> = ({ data }) => {
|
||||||
<OverlayTrigger
|
<OverlayTrigger
|
||||||
placement="top"
|
placement="top"
|
||||||
overlay={<Tooltip>{t('mod_long')}</Tooltip>}>
|
overlay={<Tooltip>{t('mod_long')}</Tooltip>}>
|
||||||
<Badge bg="light" className="text-body">
|
<span className="badge text-bg-light">{t('mod_short')}</span>
|
||||||
{t('mod_short')}
|
|
||||||
</Badge>
|
|
||||||
</OverlayTrigger>
|
</OverlayTrigger>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
Loading…
Reference in New Issue