Skip to content

Commit

Permalink
Merge branch 'feat/1.4.2/ui' into test
Browse files Browse the repository at this point in the history
  • Loading branch information
shuashuai committed Nov 22, 2024
2 parents 5866197 + ab36105 commit 3a795b2
Show file tree
Hide file tree
Showing 18 changed files with 548 additions and 101 deletions.
35 changes: 34 additions & 1 deletion i18n/en_US.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1557,6 +1561,7 @@ ui:
newest: Newest
active: Active
hot: Hot
frequent: Frequent
recommend: Recommend
score: Score
unanswered: Unanswered
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -2252,6 +2272,7 @@ ui:
discard_confirm: Are you sure you want to discard your draft?
messages:
post_deleted: This post has been deleted.
post_cancel_deleted: This post has been undeleted.
post_pin: This post has been pinned.
post_unpin: This post has been unpinned.
post_hide_list: This post has been hidden from list.
Expand All @@ -2260,3 +2281,15 @@ ui:
post_list: This post has been listed.
post_unlist: This post has been unlisted.
post_pending: Your post is awaiting review. This is a preview, it will be visible after it has been approved.
post_closed: This post has been closed.
answer_deleted: This answer has been deleted.
answer_cancel_deleted: This answer has been undeleted.
change_user_role: This user's role has been changed.
user_inactive: This user is already inactive.
user_normal: This user is already normal.
user_suspended: This user has been suspended.
user_deleted: This user has been deleted.
badge_activated: This badge has been activated.
badge_inactivated: This badge has been inactivated.


10 changes: 8 additions & 2 deletions ui/src/common/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -301,7 +301,8 @@ export type QuestionOrderBy =
| 'active'
| 'hot'
| 'score'
| 'unanswered';
| 'unanswered'
| 'frequent';

export interface QueryQuestionsReq extends Paging {
order: QuestionOrderBy;
Expand Down Expand Up @@ -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 {
Expand Down
130 changes: 130 additions & 0 deletions ui/src/components/Editor/ToolBars/file.tsx
Original file line number Diff line number Diff line change
@@ -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);
86 changes: 69 additions & 17 deletions ui/src/components/Editor/ToolBars/image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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')}...]()`;

Expand All @@ -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,
};
});

Expand All @@ -103,7 +154,6 @@ const Image = ({ editorInstance }) => {
}
const drop = async (e) => {
const fileList = e.dataTransfer.files;

const bool = verifyImageSize(fileList);

if (!bool) {
Expand All @@ -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(`![${name}](${url})`);
text.push(`${type === 'post' ? '!' : ''}[${name}](${url})`);
}
});
}
Expand All @@ -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 `![${name}](${url})`;
const text = urls.map(({ name, url, type }) => {
return `${type === 'post' ? '!' : ''}[${name}](${url})`;
});

editor.replaceRange(text.join('\n'), startPos, endPos);
Expand Down Expand Up @@ -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 });
});
};

Expand Down Expand Up @@ -283,6 +334,7 @@ const Image = ({ editorInstance }) => {
type="file"
onChange={onUpload}
isInvalid={currentTab === 'localImage' && link.isInvalid}
accept="image/*"
/>

<Form.Control.Feedback type="invalid">
Expand Down
Loading

0 comments on commit 3a795b2

Please sign in to comment.