feat: The editor has added support for uploading attachments, and the management background has added file upload configuration information.
This commit is contained in:
+21
-1
@@ -915,7 +915,7 @@ ui:
|
||||
msg:
|
||||
empty: File cannot be empty.
|
||||
only_image: Only image files are allowed.
|
||||
max_size: File size cannot exceed 4 MB.
|
||||
max_size: File size cannot exceed {{size}} MB.
|
||||
desc:
|
||||
label: Description
|
||||
tab_url: Image URL
|
||||
@@ -957,6 +957,10 @@ ui:
|
||||
text: Table
|
||||
heading: Heading
|
||||
cell: Cell
|
||||
file:
|
||||
text: Attach files
|
||||
not_supported: "Don’t support that file type. Try again with {{file_type}}."
|
||||
max_size: "Attach files size cannot exceed {{size}} MB."
|
||||
close_modal:
|
||||
title: I am closing this post as...
|
||||
btn_cancel: Cancel
|
||||
@@ -1557,6 +1561,7 @@ ui:
|
||||
newest: Newest
|
||||
active: Active
|
||||
hot: Hot
|
||||
frequent: Frequent
|
||||
recommend: Recommend
|
||||
score: Score
|
||||
unanswered: Unanswered
|
||||
@@ -2051,6 +2056,21 @@ ui:
|
||||
reserved_tags:
|
||||
label: Reserved tags
|
||||
text: "Reserved tags can only be used by moderator."
|
||||
image_size:
|
||||
label: Max image size (MB)
|
||||
text: "The maximum image upload size."
|
||||
attachment_size:
|
||||
label: Max attachment size (MB)
|
||||
text: "The maximum attachment files upload size."
|
||||
image_megapixels:
|
||||
label: Max image megapixels
|
||||
text: "Maximum number of megapixels allowed for an image."
|
||||
image_extensions:
|
||||
label: Authorized image extensions
|
||||
text: "A list of file extensions allowed for image display, separate with commas."
|
||||
attachment_extensions:
|
||||
label: Authorized attachment extensions
|
||||
text: "A list of file extensions allowed for upload, separate with commas. WARNING: Allowing uploads may cause security issues."
|
||||
seo:
|
||||
page_title: SEO
|
||||
permalink:
|
||||
|
||||
@@ -173,7 +173,7 @@ export interface UserInfoRes extends UserInfoBase {
|
||||
[prop: string]: any;
|
||||
}
|
||||
|
||||
export type UploadType = 'post' | 'avatar' | 'branding';
|
||||
export type UploadType = 'post' | 'avatar' | 'branding' | 'post_attachment';
|
||||
export interface UploadReq {
|
||||
file: FormData;
|
||||
}
|
||||
@@ -301,7 +301,8 @@ export type QuestionOrderBy =
|
||||
| 'active'
|
||||
| 'hot'
|
||||
| 'score'
|
||||
| 'unanswered';
|
||||
| 'unanswered'
|
||||
| 'frequent';
|
||||
|
||||
export interface QueryQuestionsReq extends Paging {
|
||||
order: QuestionOrderBy;
|
||||
@@ -439,6 +440,11 @@ export interface AdminSettingsWrite {
|
||||
recommend_tags?: Tag[];
|
||||
required_tag?: boolean;
|
||||
reserved_tags?: Tag[];
|
||||
max_image_size?: number;
|
||||
max_attachment_size?: number;
|
||||
max_image_megapixel?: number;
|
||||
authorized_image_extensions?: string[];
|
||||
authorized_attachment_extensions?: string[];
|
||||
}
|
||||
|
||||
export interface AdminSettingsSeo {
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* 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, memo, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Modal as AnswerModal } from '@/components';
|
||||
import ToolItem from '../toolItem';
|
||||
import { IEditorContext, Editor } from '../types';
|
||||
import { uploadImage } from '@/services';
|
||||
import { writeSettingStore } from '@/stores';
|
||||
|
||||
let context: IEditorContext;
|
||||
const Image = ({ editorInstance }) => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'editor' });
|
||||
const { max_attachment_size = 8, authorized_attachment_extensions = [] } =
|
||||
writeSettingStore((state) => state.write);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [editor, setEditor] = useState<Editor>(editorInstance);
|
||||
|
||||
const item = {
|
||||
label: 'paperclip',
|
||||
tip: `${t('file.text')}`,
|
||||
};
|
||||
|
||||
const addLink = (ctx) => {
|
||||
context = ctx;
|
||||
setEditor(context.editor);
|
||||
fileInputRef.current?.click?.();
|
||||
};
|
||||
|
||||
const verifyFileSize = (files: FileList) => {
|
||||
if (files.length === 0) {
|
||||
return false;
|
||||
}
|
||||
const unSupportFiles = Array.from(files).filter((file) => {
|
||||
const fileName = file.name.toLowerCase();
|
||||
return !authorized_attachment_extensions.find((v) =>
|
||||
fileName.endsWith(v),
|
||||
);
|
||||
});
|
||||
|
||||
if (unSupportFiles.length > 0) {
|
||||
AnswerModal.confirm({
|
||||
content: t('file.not_supported', {
|
||||
file_type: authorized_attachment_extensions.join(', '),
|
||||
}),
|
||||
showCancel: false,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
const attachmentOverSizeFiles = Array.from(files).filter(
|
||||
(file) => file.size / 1024 / 1024 > max_attachment_size,
|
||||
);
|
||||
if (attachmentOverSizeFiles.length > 0) {
|
||||
AnswerModal.confirm({
|
||||
content: t('file.max_size', { size: max_attachment_size }),
|
||||
showCancel: false,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const onUpload = async (e) => {
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
const files = e.target?.files || [];
|
||||
const bool = verifyFileSize(files);
|
||||
|
||||
if (!bool) {
|
||||
return;
|
||||
}
|
||||
const fileName = files[0].name;
|
||||
const loadingText = `![${t('image.uploading')} ${fileName}...]()`;
|
||||
const startPos = editor.getCursor();
|
||||
|
||||
const endPos = { ...startPos, ch: startPos.ch + loadingText.length };
|
||||
editor.replaceSelection(loadingText);
|
||||
editor.setReadOnly(true);
|
||||
|
||||
uploadImage({ file: e.target.files[0], type: 'post_attachment' })
|
||||
.then((url) => {
|
||||
const text = `[${fileName}](${url})`;
|
||||
editor.replaceRange('', startPos, endPos);
|
||||
editor.replaceSelection(text);
|
||||
})
|
||||
.finally(() => {
|
||||
editor.setReadOnly(false);
|
||||
editor.focus();
|
||||
});
|
||||
};
|
||||
|
||||
if (!authorized_attachment_extensions.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ToolItem {...item} onClick={addLink}>
|
||||
<input
|
||||
type="file"
|
||||
className="d-none"
|
||||
accept={authorized_attachment_extensions.join(',.').toLocaleLowerCase()}
|
||||
ref={fileInputRef}
|
||||
onChange={onUpload}
|
||||
/>
|
||||
</ToolItem>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Image);
|
||||
@@ -25,11 +25,18 @@ import { Modal as AnswerModal } from '@/components';
|
||||
import ToolItem from '../toolItem';
|
||||
import { IEditorContext, Editor } from '../types';
|
||||
import { uploadImage } from '@/services';
|
||||
import { writeSettingStore } from '@/stores';
|
||||
|
||||
let context: IEditorContext;
|
||||
const Image = ({ editorInstance }) => {
|
||||
const [editor, setEditor] = useState<Editor>(editorInstance);
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'editor' });
|
||||
const {
|
||||
max_image_size = 4,
|
||||
max_attachment_size = 8,
|
||||
authorized_image_extensions = [],
|
||||
authorized_attachment_extensions = [],
|
||||
} = writeSettingStore((state) => state.write);
|
||||
|
||||
const loadingText = `![${t('image.uploading')}...]()`;
|
||||
|
||||
@@ -52,41 +59,85 @@ const Image = ({ editorInstance }) => {
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
});
|
||||
|
||||
const verifyImageSize = (files: FileList) => {
|
||||
if (files.length === 0) {
|
||||
return false;
|
||||
}
|
||||
const filteredFiles = Array.from(files).filter(
|
||||
(file) => file.type.indexOf('image') === -1,
|
||||
);
|
||||
|
||||
if (filteredFiles.length > 0) {
|
||||
/**
|
||||
* When allowing attachments to be uploaded, verification logic for attachment information has been added. In order to avoid abnormal judgment caused by the order of drag and drop upload, the drag and drop upload verification of attachments and the drag and drop upload of images are put together.
|
||||
*
|
||||
*/
|
||||
const canUploadAttachment = authorized_attachment_extensions.length > 0;
|
||||
const allowedAllType = [
|
||||
...authorized_image_extensions,
|
||||
...authorized_attachment_extensions,
|
||||
];
|
||||
const unSupportFiles = Array.from(files).filter((file) => {
|
||||
const fileName = file.name.toLowerCase();
|
||||
return canUploadAttachment
|
||||
? !allowedAllType.find((v) => fileName.endsWith(v))
|
||||
: file.type.indexOf('image') === -1;
|
||||
});
|
||||
|
||||
if (unSupportFiles.length > 0) {
|
||||
AnswerModal.confirm({
|
||||
content: t('image.form_image.fields.file.msg.only_image'),
|
||||
content: canUploadAttachment
|
||||
? t('file.not_supported', { file_type: allowedAllType.join(', ') })
|
||||
: t('image.form_image.fields.file.msg.only_image'),
|
||||
showCancel: false,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
const filteredImages = Array.from(files).filter(
|
||||
(file) => file.size / 1024 / 1024 > 4,
|
||||
);
|
||||
|
||||
if (filteredImages.length > 0) {
|
||||
const otherFiles = Array.from(files).filter((file) => {
|
||||
return file.type.indexOf('image') === -1;
|
||||
});
|
||||
|
||||
if (canUploadAttachment && otherFiles.length > 0) {
|
||||
const attachmentOverSizeFiles = otherFiles.filter(
|
||||
(file) => file.size / 1024 / 1024 > max_attachment_size,
|
||||
);
|
||||
if (attachmentOverSizeFiles.length > 0) {
|
||||
AnswerModal.confirm({
|
||||
content: t('file.max_size', { size: max_attachment_size }),
|
||||
showCancel: false,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const imageFiles = Array.from(files).filter(
|
||||
(file) => file.type.indexOf('image') > -1,
|
||||
);
|
||||
const oversizedImages = imageFiles.filter(
|
||||
(file) => file.size / 1024 / 1024 > max_image_size,
|
||||
);
|
||||
if (oversizedImages.length > 0) {
|
||||
AnswerModal.confirm({
|
||||
content: t('image.form_image.fields.file.msg.max_size'),
|
||||
content: t('image.form_image.fields.file.msg.max_size', {
|
||||
size: max_image_size,
|
||||
}),
|
||||
showCancel: false,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const upload = (
|
||||
files: FileList,
|
||||
): Promise<{ url: string; name: string }[]> => {
|
||||
): Promise<{ url: string; name: string; type: string }[]> => {
|
||||
const promises = Array.from(files).map(async (file) => {
|
||||
const url = await uploadImage({ file, type: 'post' });
|
||||
const type = file.type.indexOf('image') > -1 ? 'post' : 'post_attachment';
|
||||
const url = await uploadImage({ file, type });
|
||||
|
||||
return {
|
||||
name: file.name,
|
||||
url,
|
||||
type,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -103,7 +154,6 @@ const Image = ({ editorInstance }) => {
|
||||
}
|
||||
const drop = async (e) => {
|
||||
const fileList = e.dataTransfer.files;
|
||||
|
||||
const bool = verifyImageSize(fileList);
|
||||
|
||||
if (!bool) {
|
||||
@@ -122,9 +172,9 @@ const Image = ({ editorInstance }) => {
|
||||
|
||||
const text: string[] = [];
|
||||
if (Array.isArray(urls)) {
|
||||
urls.forEach(({ name, url }) => {
|
||||
urls.forEach(({ name, url, type }) => {
|
||||
if (name && url) {
|
||||
text.push(``);
|
||||
text.push(`${type === 'post' ? '!' : ''}[${name}](${url})`);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -150,8 +200,8 @@ const Image = ({ editorInstance }) => {
|
||||
editor.replaceSelection(loadingText);
|
||||
editor.setReadOnly(true);
|
||||
const urls = await upload(clipboard.files);
|
||||
const text = urls.map(({ name, url }) => {
|
||||
return ``;
|
||||
const text = urls.map(({ name, url, type }) => {
|
||||
return `${type === 'post' ? '!' : ''}[${name}](${url})`;
|
||||
});
|
||||
|
||||
editor.replaceRange(text.join('\n'), startPos, endPos);
|
||||
@@ -252,6 +302,7 @@ const Image = ({ editorInstance }) => {
|
||||
|
||||
uploadImage({ file: e.target.files[0], type: 'post' }).then((url) => {
|
||||
setLink({ ...link, value: url });
|
||||
setImageName({ ...imageName, value: files[0].name });
|
||||
});
|
||||
};
|
||||
|
||||
@@ -283,6 +334,7 @@ const Image = ({ editorInstance }) => {
|
||||
type="file"
|
||||
onChange={onUpload}
|
||||
isInvalid={currentTab === 'localImage' && link.isInvalid}
|
||||
accept="image/*"
|
||||
/>
|
||||
|
||||
<Form.Control.Feedback type="invalid">
|
||||
|
||||
@@ -32,6 +32,7 @@ import BlockQuote from './blockquote';
|
||||
import Image from './image';
|
||||
import Help from './help';
|
||||
import Chart from './chart';
|
||||
import File from './file';
|
||||
|
||||
export {
|
||||
Table,
|
||||
@@ -49,4 +50,5 @@ export {
|
||||
Image,
|
||||
Help,
|
||||
Chart,
|
||||
File,
|
||||
};
|
||||
|
||||
@@ -45,6 +45,7 @@ import {
|
||||
Outdent,
|
||||
Table,
|
||||
UL,
|
||||
File,
|
||||
} from './ToolBars';
|
||||
import { htmlRender, useEditor } from './utils';
|
||||
import Viewer from './Viewer';
|
||||
@@ -130,6 +131,7 @@ const MDEditor: ForwardRefRenderFunction<EditorRef, Props> = (
|
||||
<LinkItem />
|
||||
<BlockQuote />
|
||||
<Image editorInstance={editor} />
|
||||
<File editorInstance={editor} />
|
||||
<Table />
|
||||
<div className="toolbar-divider" />
|
||||
<OL />
|
||||
|
||||
@@ -93,6 +93,7 @@ const ToolItem: FC<IProps> = (props) => {
|
||||
disabled={disable}
|
||||
tabIndex={-1}
|
||||
onClick={(e) => {
|
||||
console.log('onClick', e);
|
||||
e.preventDefault();
|
||||
onClick?.({
|
||||
editor,
|
||||
|
||||
@@ -35,8 +35,8 @@ interface Props {
|
||||
className?: string;
|
||||
pathname?: string;
|
||||
wrapClassName?: string;
|
||||
maxBtnCount?: number;
|
||||
}
|
||||
const MAX_BUTTON_COUNT = 3;
|
||||
const Index: FC<Props> = ({
|
||||
data = [],
|
||||
currentSort = '',
|
||||
@@ -45,6 +45,7 @@ const Index: FC<Props> = ({
|
||||
className = '',
|
||||
pathname = '',
|
||||
wrapClassName = '',
|
||||
maxBtnCount = 3,
|
||||
}) => {
|
||||
const [searchParams, setUrlSearchParams] = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
@@ -71,79 +72,94 @@ const Index: FC<Props> = ({
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const filteredData = data.filter((_, index) => index > MAX_BUTTON_COUNT - 2);
|
||||
const currentBtn = filteredData.find((btn) => {
|
||||
const moreBtnData = data.length > 4 ? data.slice(maxBtnCount) : [];
|
||||
const normalBtnData = data.length > 4 ? data.slice(0, maxBtnCount) : data;
|
||||
const currentBtn = moreBtnData.find((btn) => {
|
||||
return (typeof btn === 'string' ? btn : btn.name) === currentSort;
|
||||
});
|
||||
|
||||
return (
|
||||
<ButtonGroup size="sm" className={wrapClassName}>
|
||||
{data.map((btn, index) => {
|
||||
const key = typeof btn === 'string' ? btn : btn.sort;
|
||||
const name = typeof btn === 'string' ? btn : btn.name;
|
||||
return (
|
||||
<Button
|
||||
key={key}
|
||||
variant="outline-secondary"
|
||||
active={currentSort === name}
|
||||
className={classNames(
|
||||
'text-capitalize fit-content',
|
||||
data.length > MAX_BUTTON_COUNT &&
|
||||
index > MAX_BUTTON_COUNT - 2 &&
|
||||
'd-none d-md-block',
|
||||
className,
|
||||
)}
|
||||
style={
|
||||
data.length > MAX_BUTTON_COUNT && index === data.length - 1
|
||||
? {
|
||||
borderTopRightRadius: '0.25rem',
|
||||
borderBottomRightRadius: '0.25rem',
|
||||
<>
|
||||
<ButtonGroup
|
||||
size="sm"
|
||||
className={classNames('d-none d-sm-block', wrapClassName)}>
|
||||
{normalBtnData.map((btn) => {
|
||||
const key = typeof btn === 'string' ? btn : btn.sort;
|
||||
const name = typeof btn === 'string' ? btn : btn.name;
|
||||
return (
|
||||
<Button
|
||||
key={key}
|
||||
variant="outline-secondary"
|
||||
active={currentSort === name}
|
||||
className={classNames('text-capitalize fit-content', className)}
|
||||
href={
|
||||
pathname
|
||||
? `${REACT_BASE_PATH}${pathname}${handleParams(key)}`
|
||||
: handleParams(key)
|
||||
}
|
||||
onClick={(evt) => handleClick(evt, key)}>
|
||||
{t(name)}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
{moreBtnData.length > 0 && (
|
||||
<DropdownButton
|
||||
size="sm"
|
||||
variant={currentBtn ? 'secondary' : 'outline-secondary'}
|
||||
as={ButtonGroup}
|
||||
title={currentBtn ? t(currentSort) : t('more')}>
|
||||
{moreBtnData.map((btn) => {
|
||||
const key = typeof btn === 'string' ? btn : btn.sort;
|
||||
const name = typeof btn === 'string' ? btn : btn.name;
|
||||
return (
|
||||
<Dropdown.Item
|
||||
as="a"
|
||||
key={key}
|
||||
active={currentSort === name}
|
||||
className={classNames('text-capitalize', className)}
|
||||
href={
|
||||
pathname
|
||||
? `${REACT_BASE_PATH}${pathname}${handleParams(key)}`
|
||||
: handleParams(key)
|
||||
}
|
||||
: {}
|
||||
}
|
||||
href={
|
||||
pathname
|
||||
? `${REACT_BASE_PATH}${pathname}${handleParams(key)}`
|
||||
: handleParams(key)
|
||||
}
|
||||
onClick={(evt) => handleClick(evt, key)}>
|
||||
{t(name)}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
{data.length > MAX_BUTTON_COUNT && (
|
||||
<DropdownButton
|
||||
size="sm"
|
||||
variant={currentBtn ? 'secondary' : 'outline-secondary'}
|
||||
className="d-block d-md-none"
|
||||
as={ButtonGroup}
|
||||
title={currentBtn ? t(currentSort) : t('more')}>
|
||||
{filteredData.map((btn) => {
|
||||
const key = typeof btn === 'string' ? btn : btn.sort;
|
||||
const name = typeof btn === 'string' ? btn : btn.name;
|
||||
return (
|
||||
<Dropdown.Item
|
||||
as="a"
|
||||
key={key}
|
||||
active={currentSort === name}
|
||||
className={classNames(
|
||||
'text-capitalize',
|
||||
'd-block d-md-none',
|
||||
className,
|
||||
)}
|
||||
href={
|
||||
pathname
|
||||
? `${REACT_BASE_PATH}${pathname}${handleParams(key)}`
|
||||
: handleParams(key)
|
||||
}
|
||||
onClick={(evt) => handleClick(evt, key)}>
|
||||
{t(name)}
|
||||
</Dropdown.Item>
|
||||
);
|
||||
})}
|
||||
</DropdownButton>
|
||||
)}
|
||||
</ButtonGroup>
|
||||
onClick={(evt) => handleClick(evt, key)}>
|
||||
{t(name)}
|
||||
</Dropdown.Item>
|
||||
);
|
||||
})}
|
||||
</DropdownButton>
|
||||
)}
|
||||
</ButtonGroup>
|
||||
<DropdownButton
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
className="d-block d-sm-none"
|
||||
title={t(currentSort)}>
|
||||
{data.map((btn) => {
|
||||
const key = typeof btn === 'string' ? btn : btn.sort;
|
||||
const name = typeof btn === 'string' ? btn : btn.name;
|
||||
return (
|
||||
<Dropdown.Item
|
||||
as="a"
|
||||
key={key}
|
||||
active={currentSort === name}
|
||||
className={classNames(
|
||||
'text-capitalize',
|
||||
'd-block d-sm-none',
|
||||
className,
|
||||
)}
|
||||
href={
|
||||
pathname
|
||||
? `${REACT_BASE_PATH}${pathname}${handleParams(key)}`
|
||||
: handleParams(key)
|
||||
}
|
||||
onClick={(evt) => handleClick(evt, key)}>
|
||||
{t(name)}
|
||||
</Dropdown.Item>
|
||||
);
|
||||
})}
|
||||
</DropdownButton>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -40,10 +40,10 @@ import { useSkeletonControl } from '@/hooks';
|
||||
export const QUESTION_ORDER_KEYS: Type.QuestionOrderBy[] = [
|
||||
'newest',
|
||||
'active',
|
||||
'hot',
|
||||
'score',
|
||||
'unanswered',
|
||||
'recommend',
|
||||
'frequent',
|
||||
'score',
|
||||
];
|
||||
interface Props {
|
||||
source: 'questions' | 'tag' | 'linked';
|
||||
@@ -83,6 +83,7 @@ const QuestionList: FC<Props> = ({
|
||||
currentSort={curOrder}
|
||||
pathname={source === 'questions' ? '/questions' : ''}
|
||||
i18nKeyPrefix="question"
|
||||
maxBtnCount={source === 'tag' ? 3 : 4}
|
||||
/>
|
||||
</div>
|
||||
<ListGroup className="rounded-0">
|
||||
|
||||
@@ -52,6 +52,31 @@ const initFormData = {
|
||||
errorMsg: '',
|
||||
isInvalid: false,
|
||||
},
|
||||
max_image_size: {
|
||||
value: 4,
|
||||
errorMsg: '',
|
||||
isInvalid: false,
|
||||
},
|
||||
max_attachment_size: {
|
||||
value: 8,
|
||||
errorMsg: '',
|
||||
isInvalid: false,
|
||||
},
|
||||
max_image_megapixel: {
|
||||
value: 40,
|
||||
errorMsg: '',
|
||||
isInvalid: false,
|
||||
},
|
||||
authorized_image_extensions: {
|
||||
value: 'jpg, jpeg, png, gif, webp',
|
||||
errorMsg: '',
|
||||
isInvalid: false,
|
||||
},
|
||||
authorized_attachment_extensions: {
|
||||
value: '',
|
||||
errorMsg: '',
|
||||
isInvalid: false,
|
||||
},
|
||||
};
|
||||
|
||||
const Index: FC = () => {
|
||||
@@ -111,6 +136,18 @@ const Index: FC = () => {
|
||||
reserved_tags: formData.reserved_tags.value,
|
||||
required_tag: formData.required_tag.value,
|
||||
restrict_answer: formData.restrict_answer.value,
|
||||
max_image_size: Number(formData.max_image_size.value),
|
||||
max_attachment_size: Number(formData.max_attachment_size.value),
|
||||
max_image_megapixel: Number(formData.max_image_megapixel.value),
|
||||
authorized_image_extensions: formData.authorized_image_extensions.value
|
||||
.split(',')
|
||||
?.map((item) => item.trim().toLowerCase()),
|
||||
authorized_attachment_extensions:
|
||||
formData.authorized_attachment_extensions.value.length > 0
|
||||
? formData.authorized_attachment_extensions.value
|
||||
.split(',')
|
||||
?.map((item) => item.trim().toLowerCase())
|
||||
: [],
|
||||
};
|
||||
postRequireAndReservedTag(reqParams)
|
||||
.then(() => {
|
||||
@@ -120,7 +157,7 @@ const Index: FC = () => {
|
||||
});
|
||||
writeSettingStore
|
||||
.getState()
|
||||
.update({ restrict_answer: reqParams.restrict_answer });
|
||||
.update({ restrict_answer: reqParams.restrict_answer, ...reqParams });
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.isError) {
|
||||
@@ -142,6 +179,13 @@ const Index: FC = () => {
|
||||
if (Array.isArray(res.reserved_tags)) {
|
||||
formData.reserved_tags.value = res.reserved_tags;
|
||||
}
|
||||
formData.max_image_size.value = res.max_image_size;
|
||||
formData.max_attachment_size.value = res.max_attachment_size;
|
||||
formData.max_image_megapixel.value = res.max_image_megapixel;
|
||||
formData.authorized_image_extensions.value =
|
||||
res.authorized_image_extensions?.join(', ').toLowerCase();
|
||||
formData.authorized_attachment_extensions.value =
|
||||
res.authorized_attachment_extensions?.join(', ').toLowerCase();
|
||||
setFormData({ ...formData });
|
||||
});
|
||||
};
|
||||
@@ -243,6 +287,111 @@ const Index: FC = () => {
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group className="mb-3" controlId="image_size">
|
||||
<Form.Label>{t('image_size.label')}</Form.Label>
|
||||
<Form.Control
|
||||
type="number"
|
||||
value={formData.max_image_size.value}
|
||||
onChange={(evt) => {
|
||||
handleValueChange({
|
||||
max_image_size: {
|
||||
value: evt.target.value,
|
||||
errorMsg: '',
|
||||
isInvalid: false,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Form.Text>{t('image_size.text')}</Form.Text>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formData.max_image_size.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group className="mb-3" controlId="attachment_size">
|
||||
<Form.Label>{t('attachment_size.label')}</Form.Label>
|
||||
<Form.Control
|
||||
type="number"
|
||||
value={formData.max_attachment_size.value}
|
||||
onChange={(evt) => {
|
||||
handleValueChange({
|
||||
max_attachment_size: {
|
||||
value: evt.target.value,
|
||||
errorMsg: '',
|
||||
isInvalid: false,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Form.Text>{t('attachment_size.text')}</Form.Text>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formData.max_attachment_size.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group className="mb-3" controlId="image_megapixels">
|
||||
<Form.Label>{t('image_megapixels.label')}</Form.Label>
|
||||
<Form.Control
|
||||
type="number"
|
||||
value={formData.max_image_megapixel.value}
|
||||
onChange={(evt) => {
|
||||
handleValueChange({
|
||||
max_image_megapixel: {
|
||||
value: evt.target.value,
|
||||
errorMsg: '',
|
||||
isInvalid: false,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Form.Text>{t('image_megapixels.text')}</Form.Text>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formData.max_image_megapixel.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group className="mb-3" controlId="image_extensions">
|
||||
<Form.Label>{t('image_extensions.label')}</Form.Label>
|
||||
<Form.Control
|
||||
type="text"
|
||||
value={formData.authorized_image_extensions.value}
|
||||
onChange={(evt) => {
|
||||
handleValueChange({
|
||||
authorized_image_extensions: {
|
||||
value: evt.target.value.toLowerCase(),
|
||||
errorMsg: '',
|
||||
isInvalid: false,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Form.Text>{t('image_extensions.text')}</Form.Text>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formData.authorized_image_extensions.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group className="mb-3" controlId="attachment_extensions">
|
||||
<Form.Label>{t('attachment_extensions.label')}</Form.Label>
|
||||
<Form.Control
|
||||
type="text"
|
||||
value={formData.authorized_attachment_extensions.value}
|
||||
onChange={(evt) => {
|
||||
handleValueChange({
|
||||
authorized_attachment_extensions: {
|
||||
value: evt.target.value.toLowerCase(),
|
||||
errorMsg: '',
|
||||
isInvalid: false,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Form.Text>{t('attachment_extensions.text')}</Form.Text>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formData.authorized_attachment_extensions.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group className="mb-3">
|
||||
<Button type="submit">{t('save', { keyPrefix: 'btns' })}</Button>
|
||||
</Form.Group>
|
||||
|
||||
@@ -186,7 +186,7 @@ const Index: FC = () => {
|
||||
source="tag"
|
||||
data={listData}
|
||||
order={curOrder}
|
||||
orderList={QUESTION_ORDER_KEYS.slice(0, 5)}
|
||||
orderList={QUESTION_ORDER_KEYS.filter((k) => k !== 'recommend')}
|
||||
isLoading={listLoading}
|
||||
/>
|
||||
</Col>
|
||||
|
||||
@@ -32,6 +32,11 @@ const Index = create<IProps>((set) => ({
|
||||
recommend_tags: [],
|
||||
required_tag: false,
|
||||
reserved_tags: [],
|
||||
max_image_size: 4,
|
||||
max_attachment_size: 8,
|
||||
max_image_megapixel: 40,
|
||||
authorized_image_extensions: [],
|
||||
authorized_attachment_extensions: [],
|
||||
},
|
||||
update: (params) =>
|
||||
set((state) => {
|
||||
|
||||
@@ -408,9 +408,10 @@ export const initAppSettingsStore = async () => {
|
||||
customizeStore.getState().update(appSettings.custom_css_html);
|
||||
themeSettingStore.getState().update(appSettings.theme);
|
||||
seoSettingStore.getState().update(appSettings.site_seo);
|
||||
writeSettingStore
|
||||
.getState()
|
||||
.update({ restrict_answer: appSettings.site_write.restrict_answer });
|
||||
writeSettingStore.getState().update({
|
||||
restrict_answer: appSettings.site_write.restrict_answer,
|
||||
...appSettings.site_write,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user