Merge branch 'ui-v0.3' of git.backyard.segmentfault.com:opensource/answer into ui-v0.3

This commit is contained in:
haitao(lj) 2022-11-04 10:47:23 +08:00
commit b6026dae16
17 changed files with 409 additions and 71 deletions

View File

@ -22,6 +22,11 @@ module.exports = {
changeOrigin: true,
secure: false,
},
'/installation': {
target: 'http://10.0.10.98:2060',
changeOrigin: true,
secure: false,
},
};
return config;
};

View File

@ -258,6 +258,8 @@ export interface AdminSettingsGeneral {
name: string;
short_description: string;
description: string;
site_url: string;
contact_email: string;
}
export interface AdminSettingsInterface {

View File

@ -1,6 +1,6 @@
import { FC, memo } from 'react';
import { ButtonGroup, Button, DropdownButton, Dropdown } from 'react-bootstrap';
import { useSearchParams } from 'react-router-dom';
import { useSearchParams, useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import classNames from 'classnames';
@ -11,6 +11,7 @@ interface Props {
currentSort: string;
sortKey?: string;
className?: string;
pathname?: string;
}
const MAX_BUTTON_COUNT = 3;
const Index: FC<Props> = ({
@ -19,8 +20,10 @@ const Index: FC<Props> = ({
sortKey = 'order',
i18nKeyPrefix = '',
className = '',
pathname = '',
}) => {
const [searchParams, setUrlSearchParams] = useSearchParams();
const navigate = useNavigate();
const { t } = useTranslation('translation', {
keyPrefix: i18nKeyPrefix,
@ -36,7 +39,11 @@ const Index: FC<Props> = ({
const handleClick = (e, type) => {
e.preventDefault();
const str = handleParams(type);
setUrlSearchParams(str);
if (pathname) {
navigate(`${pathname}${str}`);
} else {
setUrlSearchParams(str);
}
};
const filteredData = data.filter((_, index) => index > MAX_BUTTON_COUNT - 2);
@ -69,7 +76,9 @@ const Index: FC<Props> = ({
}
: {}
}
href={handleParams(key)}
href={
pathname ? `${pathname}${handleParams(key)}` : handleParams(key)
}
onClick={(evt) => handleClick(evt, key)}>
{t(name)}
</Button>
@ -95,7 +104,11 @@ const Index: FC<Props> = ({
'd-block d-md-none',
className,
)}
href={handleParams(key)}
href={
pathname
? `${pathname}${handleParams(key)}`
: handleParams(key)
}
onClick={(evt) => handleClick(evt, key)}>
{t(name)}
</Dropdown.Item>

View File

@ -115,6 +115,7 @@ const QuestionList: FC<Props> = ({ source }) => {
<QueryGroup
data={QuestionOrderKeys}
currentSort={curOrder}
pathname={source === 'questions' ? '/questions' : ''}
i18nKeyPrefix="question"
/>
</Col>

View File

@ -744,6 +744,7 @@
"title": "Answer",
"next": "Next",
"done": "Done",
"config_yaml_error": "Cant create the config.yaml file.",
"lang": {
"label": "Please choose a language"
},
@ -784,22 +785,42 @@
"site_information": "Site Information",
"admin_account": "Admin Account",
"site_name": {
"label": "Site Name"
"label": "Site Name",
"msg": "Site Name cannot be empty."
},
"site_url": {
"label": "Site URL",
"text": "The address of your site.",
"msg": {
"empty": "Site URL cannot be empty.",
"incorrect": "Site URL incorrect format."
}
},
"contact_email": {
"label": "Contact Email",
"text": "Email address of key contact responsible for this site."
"text": "Email address of key contact responsible for this site.",
"msg": {
"empty": "Contact Email cannot be empty.",
"incorrect": "Contact Email incorrect format."
}
},
"admin_name": {
"label": "Name"
"label": "Name",
"msg": "Name cannot be empty."
},
"admin_password": {
"label": "Password",
"text": "You will need this password to log in. Please store it in a secure location."
"text": "You will need this password to log in. Please store it in a secure location.",
"msg": "Password cannot be empty."
},
"admin_email": {
"label": "Email",
"text": "You will need this email to log in."
"text": "You will need this email to log in.",
"msg": {
"empty": "Email cannot be empty.",
"incorrect": "Email incorrect format."
}
},
"ready_title": "Your Answer is Ready!",
"ready_description": "If you ever feel like changing more settings, visit <1>admin section</1>; find it in the site menu.",
@ -976,6 +997,11 @@
"msg": "Site name cannot be empty.",
"text": "The name of this site, as used in the title tag."
},
"site_url": {
"label": "Site URL",
"msg": "Site url cannot be empty.",
"text": "The address of your site."
},
"short_description": {
"label": "Short Site Description (optional)",
"msg": "Short site description cannot be empty.",
@ -985,6 +1011,11 @@
"label": "Site Description (optional)",
"msg": "Site description cannot be empty.",
"text": "Describe this site in one sentence, as used in the meta description tag."
},
"contact_email": {
"label": "Contact Email",
"msg": "Contact email cannot be empty.",
"text": "Email address of key contact responsible for this site."
}
},
"interface": {
@ -1004,7 +1035,7 @@
"msg": "Interface language cannot be empty.",
"text": "User interface language. It will change when you refresh the page."
},
"timezone": {
"time_zone": {
"label": "Timezone",
"msg": "Timezone cannot be empty.",
"text": "Choose a UTC (Coordinated Universal Time) time offset."

View File

@ -1,6 +1,7 @@
import { FC } from 'react';
import { Card, Row, Col } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import type * as Type from '@/common/interface';
@ -38,9 +39,9 @@ const Statistics: FC<IProps> = ({ data }) => {
<Col xs={6}>
<span className="text-secondary me-1">{t('flags')}</span>
<strong>{data.report_count}</strong>
<a href="###" className="ms-2">
<Link to="/admin/flags" className="ms-2">
{t('review')}
</a>
</Link>
</Col>
</Row>
</Card.Body>

View File

@ -23,6 +23,11 @@ const General: FC = () => {
isInvalid: false,
errorMsg: '',
},
site_url: {
value: '',
isInvalid: false,
errorMsg: '',
},
short_description: {
value: '',
isInvalid: false,
@ -33,10 +38,15 @@ const General: FC = () => {
isInvalid: false,
errorMsg: '',
},
contact_email: {
value: '',
isInvalid: false,
errorMsg: '',
},
});
const checkValidated = (): boolean => {
let ret = true;
const { name } = formData;
const { name, site_url, contact_email } = formData;
if (!name.value) {
ret = false;
formData.name = {
@ -45,6 +55,22 @@ const General: FC = () => {
errorMsg: t('name.msg'),
};
}
if (!site_url.value) {
ret = false;
formData.site_url = {
value: '',
isInvalid: true,
errorMsg: t('site_url.msg'),
};
}
if (!contact_email.value) {
ret = false;
formData.contact_email = {
value: '',
isInvalid: true,
errorMsg: t('contact_email.msg'),
};
}
setFormData({
...formData,
});
@ -61,6 +87,8 @@ const General: FC = () => {
name: formData.name.value,
description: formData.description.value,
short_description: formData.short_description.value,
site_url: formData.site_url.value,
contact_email: formData.contact_email.value,
};
updateGeneralSetting(reqParams)
@ -100,7 +128,7 @@ const General: FC = () => {
Object.keys(setting).forEach((k) => {
formMeta[k] = { ...formData[k], value: setting[k] };
});
setFormData(formMeta);
setFormData({ ...formData, ...formMeta });
}, [setting]);
return (
<>
@ -120,6 +148,20 @@ const General: FC = () => {
{formData.name.errorMsg}
</Form.Control.Feedback>
</Form.Group>
<Form.Group controlId="siteUrl" className="mb-3">
<Form.Label>{t('site_url.label')}</Form.Label>
<Form.Control
required
type="text"
value={formData.site_url.value}
isInvalid={formData.site_url.isInvalid}
onChange={(evt) => onFieldChange('site_url', evt.target.value)}
/>
<Form.Text as="div">{t('site_url.text')}</Form.Text>
<Form.Control.Feedback type="invalid">
{formData.site_url.errorMsg}
</Form.Control.Feedback>
</Form.Group>
<Form.Group controlId="shortDescription" className="mb-3">
<Form.Label>{t('short_description.label')}</Form.Label>
<Form.Control
@ -150,6 +192,20 @@ const General: FC = () => {
{formData.description.errorMsg}
</Form.Control.Feedback>
</Form.Group>
<Form.Group controlId="contact_email" className="mb-3">
<Form.Label>{t('contact_email.label')}</Form.Label>
<Form.Control
required
type="text"
value={formData.contact_email.value}
isInvalid={formData.contact_email.isInvalid}
onChange={(evt) => onFieldChange('contact_email', evt.target.value)}
/>
<Form.Text as="div">{t('contact_email.text')}</Form.Text>
<Form.Control.Feedback type="invalid">
{formData.contact_email.errorMsg}
</Form.Control.Feedback>
</Form.Group>
<Button variant="primary" type="submit">
{t('save', { keyPrefix: 'btns' })}

View File

@ -6,8 +6,9 @@ import Progress from '../Progress';
interface Props {
visible: boolean;
siteUrl: string;
}
const Index: FC<Props> = ({ visible }) => {
const Index: FC<Props> = ({ visible, siteUrl = '' }) => {
const { t } = useTranslation('translation', { keyPrefix: 'install' });
if (!visible) return null;
@ -17,14 +18,15 @@ const Index: FC<Props> = ({ visible }) => {
<p>
<Trans i18nKey="install.ready_description">
If you ever feel like changing more settings, visit
<a href="/">admin section</a>; find it in the site menu.
<a href={`${siteUrl}/users/login`}>admin section</a>; find it in the
site menu.
</Trans>
</p>
<p>{t('good_luck')}</p>
<div className="d-flex align-items-center justify-content-between">
<Progress step={5} />
<Button>{t('done')}</Button>
<Button href={siteUrl}>{t('done')}</Button>
</div>
</div>
);

View File

@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
import type { LangsType, FormValue, FormDataType } from '@/common/interface';
import Progress from '../Progress';
import { getLanguageOptions } from '@/services';
import { getInstallLangOptions } from '@/services';
interface Props {
data: FormValue;
@ -18,8 +18,15 @@ const Index: FC<Props> = ({ visible, data, changeCallback, nextCallback }) => {
const [langs, setLangs] = useState<LangsType[]>();
const getLangs = async () => {
const res: LangsType[] = await getLanguageOptions();
const res: LangsType[] = await getInstallLangOptions();
setLangs(res);
changeCallback({
lang: {
value: res[0].value,
isInvalid: false,
errorMsg: '',
},
});
};
const handleSubmit = () => {

View File

@ -18,6 +18,7 @@ const Index: FC<Props> = ({ visible, data, changeCallback, nextCallback }) => {
let bol = true;
const {
site_name,
site_url,
contact_email,
admin_name,
admin_password,
@ -33,12 +34,40 @@ const Index: FC<Props> = ({ visible, data, changeCallback, nextCallback }) => {
};
}
if (!site_url.value) {
bol = false;
data.site_url = {
value: '',
isInvalid: true,
errorMsg: t('site_name.msg.empty'),
};
}
const reg = /^(http|https):\/\//g;
if (site_url.value && !site_url.value.match(reg)) {
bol = false;
data.site_url = {
value: site_url.value,
isInvalid: true,
errorMsg: t('site_url.msg.incorrect'),
};
}
if (!contact_email.value) {
bol = false;
data.contact_email = {
value: '',
isInvalid: true,
errorMsg: t('contact_email.msg'),
errorMsg: t('contact_email.msg.empty'),
};
}
const mailReg = /^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/;
if (contact_email.value && !contact_email.value.match(mailReg)) {
bol = false;
data.contact_email = {
value: contact_email.value,
isInvalid: true,
errorMsg: t('contact_email.msg.incorrect'),
};
}
@ -65,7 +94,16 @@ const Index: FC<Props> = ({ visible, data, changeCallback, nextCallback }) => {
data.admin_email = {
value: '',
isInvalid: true,
errorMsg: t('admin_email.msg'),
errorMsg: t('admin_email.msg.empty'),
};
}
if (admin_email.value && !admin_email.value.match(mailReg)) {
bol = false;
data.admin_email = {
value: '',
isInvalid: true,
errorMsg: t('admin_email.msg.incorrect'),
};
}
@ -108,6 +146,27 @@ const Index: FC<Props> = ({ visible, data, changeCallback, nextCallback }) => {
{data.site_name.errorMsg}
</Form.Control.Feedback>
</Form.Group>
<Form.Group controlId="site_url" className="mb-3">
<Form.Label>{t('site_url.label')}</Form.Label>
<Form.Control
required
value={data.site_url.value}
isInvalid={data.site_url.isInvalid}
onChange={(e) => {
changeCallback({
site_url: {
value: e.target.value,
isInvalid: false,
errorMsg: '',
},
});
}}
/>
<Form.Text>{t('site_url.text')}</Form.Text>
<Form.Control.Feedback type="invalid">
{data.site_url.errorMsg}
</Form.Control.Feedback>
</Form.Group>
<Form.Group controlId="contact_email" className="mb-3">
<Form.Label>{t('contact_email.label')}</Form.Label>
<Form.Control

View File

@ -6,29 +6,43 @@ import Progress from '../Progress';
interface Props {
visible: boolean;
errorMsg;
nextCallback: () => void;
}
const Index: FC<Props> = ({ visible, nextCallback }) => {
const Index: FC<Props> = ({ visible, errorMsg, nextCallback }) => {
const { t } = useTranslation('translation', { keyPrefix: 'install' });
if (!visible) return null;
return (
<div>
<h5>{t('config_yaml.title')}</h5>
<div className="mb-3">{t('config_yaml.label')}</div>
<div className="fmt">
<p>
<Trans
i18nKey="install.config_yaml.description"
components={{ 1: <code /> }}
/>
</p>
</div>
<FormGroup className="mb-3">
<Form.Control type="text" as="textarea" rows={5} className="fs-14" />
</FormGroup>
<div className="mb-3">{t('config_yaml.info')}</div>
{errorMsg?.msg?.length > 0 ? (
<>
<div className="fmt">
<p>
<Trans
i18nKey="install.config_yaml.description"
components={{ 1: <code /> }}
/>
</p>
</div>
<FormGroup className="mb-3">
<Form.Control
type="text"
as="textarea"
rows={8}
className="fs-14"
value={errorMsg?.default_config}
/>
</FormGroup>
<div className="mb-3">{t('config_yaml.info')}</div>
</>
) : (
<div className="mb-3">{t('config_yaml.label')}</div>
)}
<div className="d-flex align-items-center justify-content-between">
<Progress step={3} />
<Button onClick={nextCallback}>{t('next')}</Button>

View File

@ -3,8 +3,13 @@ import { Container, Row, Col, Card, Alert } from 'react-bootstrap';
import { useTranslation, Trans } from 'react-i18next';
import type { FormDataType } from '@/common/interface';
import { Storage } from '@/utils';
import { PageTitle } from '@/components';
import {
dbCheck,
installInit,
installBaseInfo,
checkConfigFileExists,
} from '@/services';
import {
FirstStep,
@ -17,7 +22,11 @@ import {
const Index: FC = () => {
const { t } = useTranslation('translation', { keyPrefix: 'install' });
const [step, setStep] = useState(1);
const [showError] = useState(false);
const [loading, setLoading] = useState(true);
const [errorData, setErrorData] = useState<{ [propName: string]: any }>({
msg: '',
});
const [tableExist, setTableExist] = useState(false);
const [formData, setFormData] = useState<FormDataType>({
lang: {
@ -26,7 +35,7 @@ const Index: FC = () => {
errorMsg: '',
},
db_type: {
value: '',
value: 'mysql',
isInvalid: false,
errorMsg: '',
},
@ -55,12 +64,16 @@ const Index: FC = () => {
isInvalid: false,
errorMsg: '',
},
site_name: {
value: '',
isInvalid: false,
errorMsg: '',
},
site_url: {
value: '',
isInvalid: false,
errorMsg: '',
},
contact_email: {
value: '',
isInvalid: false,
@ -88,33 +101,107 @@ const Index: FC = () => {
setFormData({ ...formData, ...params });
};
const handleStep = () => {
const handleErr = (data) => {
window.scrollTo(0, 0);
setErrorData(data);
};
const handleNext = async () => {
setErrorData({
msg: '',
});
setStep((pre) => pre + 1);
};
// const handleSubmit = () => {
// const params = {
// lang: formData.lang.value,
// db_type: formData.db_type.value,
// db_username: formData.db_username.value,
// db_password: formData.db_password.value,
// db_host: formData.db_host.value,
// db_name: formData.db_name.value,
// db_file: formData.db_file.value,
// site_name: formData.site_name.value,
// contact_email: formData.contact_email.value,
// admin_name: formData.admin_name.value,
// admin_password: formData.admin_password.value,
// admin_email: formData.admin_email.value,
// };
const submitDatabaseForm = () => {
const params = {
lang: formData.lang.value,
db_type: formData.db_type.value,
db_username: formData.db_username.value,
db_password: formData.db_password.value,
db_host: formData.db_host.value,
db_name: formData.db_name.value,
db_file: formData.db_file.value,
};
dbCheck(params)
.then(() => {
handleNext();
})
.catch((err) => {
console.log(err);
handleErr(err);
});
};
// console.log(params);
// };
const checkInstall = () => {
installInit()
.then(() => {
handleNext();
})
.catch((err) => {
handleErr(err);
});
};
const submitSiteConfig = () => {
const params = {
site_name: formData.site_name.value,
contact_email: formData.contact_email.value,
admin_name: formData.admin_name.value,
admin_password: formData.admin_password.value,
admin_email: formData.admin_email.value,
};
installBaseInfo(params)
.then(() => {
handleNext();
})
.catch((err) => {
handleErr(err);
});
};
const handleStep = () => {
if (step === 2) {
submitDatabaseForm();
} else if (step === 3) {
checkInstall();
} else if (step === 4) {
submitSiteConfig();
} else {
handleNext();
}
};
const handleInstallNow = (e) => {
e.preventDefault();
if (tableExist) {
setStep(7);
} else {
setStep(4);
}
};
const configYmlCheck = () => {
checkConfigFileExists()
.then((res) => {
setTableExist(res?.db_table_exist);
if (res && res.config_file_exist) {
setStep(5);
}
})
.finally(() => {
setLoading(false);
});
};
useEffect(() => {
console.log('step===', Storage.get('INSTALL_STEP'));
configYmlCheck();
}, []);
if (loading) {
return <div />;
}
return (
<div className="page-wrap2">
<PageTitle title={t('install', { keyPrefix: 'page_title' })} />
@ -124,7 +211,9 @@ const Index: FC = () => {
<h2 className="mb-4 text-center">{t('title')}</h2>
<Card>
<Card.Body>
{showError && <Alert variant="danger"> show error msg </Alert>}
{errorData?.msg && (
<Alert variant="danger">{errorData?.msg}</Alert>
)}
<FirstStep
visible={step === 1}
@ -140,7 +229,11 @@ const Index: FC = () => {
nextCallback={handleStep}
/>
<ThirdStep visible={step === 3} nextCallback={handleStep} />
<ThirdStep
visible={step === 3}
nextCallback={handleStep}
errorMsg={errorData}
/>
<FourthStep
visible={step === 4}
@ -149,7 +242,7 @@ const Index: FC = () => {
nextCallback={handleStep}
/>
<Fifth visible={step === 5} />
<Fifth visible={step === 5} siteUrl={formData.site_url.value} />
{step === 6 && (
<div>
<h5>{t('warning')}</h5>
@ -158,7 +251,10 @@ const Index: FC = () => {
The file <code>config.yaml</code> already exists. If you
need to reset any of the configuration items in this
file, please delete it first. You may try{' '}
<a href="/">installing now</a>.
<a href="###" onClick={(e) => handleInstallNow(e)}>
installing now
</a>
.
</Trans>
</p>
</div>

View File

@ -1,17 +1,20 @@
import { useState } from 'react';
import { Container, Row, Col, Card, Button } from 'react-bootstrap';
import { Container, Row, Col, Card, Button, Spinner } from 'react-bootstrap';
import { useTranslation, Trans } from 'react-i18next';
import { PageTitle } from '@/components';
import { upgradSystem } from '@/services';
const Index = () => {
const { t } = useTranslation('translation', {
keyPrefix: 'upgrade',
});
const [step, setStep] = useState(1);
const [step] = useState(1);
const [loading, setLoading] = useState(false);
const handleUpdate = () => {
setStep(2);
const handleUpdate = async () => {
await upgradSystem();
setLoading(true);
};
return (
<div className="page-wrap2">
@ -29,9 +32,22 @@ const Index = () => {
i18nKey="upgrade.update_description"
components={{ 1: <p /> }}
/>
<Button className="float-end" onClick={handleUpdate}>
{t('update_btn')}
</Button>
{loading ? (
<Button variant="primary" disabled className="float-end">
<Spinner
as="span"
animation="border"
size="sm"
role="status"
aria-hidden="true"
/>
<span> {t('update_btn')}</span>
</Button>
) : (
<Button className="float-end" onClick={handleUpdate}>
{t('update_btn')}
</Button>
)}
</>
)}
@ -39,7 +55,9 @@ const Index = () => {
<>
<h5>{t('done_title')}</h5>
<p>{t('done_desscription')}</p>
<Button className="float-end">{t('done_btn')}</Button>
<Button className="float-end" href="/">
{t('done_btn')}
</Button>
</>
)}
</Card.Body>

View File

@ -248,3 +248,7 @@ export const changeEmailVerify = (params: { code: string }) => {
export const getAppSettings = () => {
return request.get<Type.SiteSettings>('/answer/api/v1/siteinfo');
};
export const upgradSystem = () => {
return request.post('/answer/api/v1/upgradation');
};

View File

@ -1,3 +1,4 @@
export * from './admin';
export * from './common';
export * from './client';
export * from './install';

View File

@ -0,0 +1,21 @@
import request from '@/utils/request';
export const checkConfigFileExists = () => {
return request.post('/installation/config-file/check');
};
export const dbCheck = (params) => {
return request.post('/installation/db/check', params);
};
export const installInit = () => {
return request.post('/installation/init');
};
export const installBaseInfo = (params) => {
return request.post('/installation/base-info', params);
};
export const getInstallLangOptions = () => {
return request.get('/installation/language/options');
};

View File

@ -73,7 +73,14 @@ class Request {
});
}
if (data.type === 'modal') {
if (data.err_type === 'alert') {
return Promise.reject({
msg,
...data,
});
}
if (data.err_type === 'modal') {
// modal error message
Modal.confirm({
content: msg,