diff --git a/README.md b/README.md index f7a05e902..be4a28c20 100644 --- a/README.md +++ b/README.md @@ -556,7 +556,6 @@ The following summarizes the various [apps](#adding-a-new-platform-ui-applicatio - [Gamification Admin](#gamification-admin) - [Learn](#learn) - [Self Service](#self-service) -- [Review](#review) ## Platform App @@ -600,10 +599,3 @@ Application that allows customers to submit/start challenges self-service. [Work README TBD](./src/apps/self-service/README.md) [Work Routes](./src/apps/self-service/src/self-service.routes.tsx) - -## Review - -The application that allows managing the review submissions. - -[Review README TBD](./src/apps/review/README.md) -[Review Routes](./src/apps/review/src/review-app.routes.tsx) diff --git a/package.json b/package.json index 6881377ef..93a0323e3 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,6 @@ "@storybook/react": "7.6.10", "@stripe/react-stripe-js": "1.13.0", "@stripe/stripe-js": "1.41.0", - "@types/codemirror": "^5.60.15", "apexcharts": "^3.36.0", "axios": "^1.7.9", "browser-cookies": "^1.2.0", @@ -44,7 +43,6 @@ "draft-js-export-html": "^1.2.0", "draft-js-markdown-shortcuts-plugin": "^0.3.0", "draft-js-plugins-editor": "^2.0.3", - "easymde": "^2.20.0", "express": "^4.21.2", "express-fileupload": "^1.4.0", "express-interceptor": "^1.2.0", @@ -94,12 +92,9 @@ "redux-promise": "^0.6.0", "redux-promise-middleware": "^6.1.3", "redux-thunk": "^2.4.1", - "rehype-raw": "^7.0.0", - "rehype-stringify": "^10.0.1", "remark-breaks": "^3.0.2", "remark-frontmatter": "^4.0.1", "remark-gfm": "^3.0.1", - "remark-parse": "^11.0.0", "remove": "^0.1.5", "sanitize-html": "^2.12.1", "sass": "^1.79.0", diff --git a/src/apps/admin/src/lib/components/DialogEditUserRoles/DialogEditUserRoles.tsx b/src/apps/admin/src/lib/components/DialogEditUserRoles/DialogEditUserRoles.tsx index 17453acc6..a3e8e23e0 100644 --- a/src/apps/admin/src/lib/components/DialogEditUserRoles/DialogEditUserRoles.tsx +++ b/src/apps/admin/src/lib/components/DialogEditUserRoles/DialogEditUserRoles.tsx @@ -100,7 +100,7 @@ export const DialogEditUserRoles: FC = (props: Props) => { variant='danger' label='Remove' onClick={function onClick() { - doRemoveRole(data.id) + doRemoveRole(String(data.id)) }} disabled={isRemoving[data.id]} /> diff --git a/src/apps/admin/src/lib/components/ReviewSummaryList/MobileListView/MobileListView.tsx b/src/apps/admin/src/lib/components/ReviewSummaryList/MobileListView/MobileListView.tsx index dee9332af..a828fc033 100644 --- a/src/apps/admin/src/lib/components/ReviewSummaryList/MobileListView/MobileListView.tsx +++ b/src/apps/admin/src/lib/components/ReviewSummaryList/MobileListView/MobileListView.tsx @@ -67,22 +67,22 @@ const MobileListView: FC> = props => {
- {/* Title */ propertyElements[1]} - {/* Status */ propertyElements[4]} + {/* Title */ propertyElements[0]} + {/* Status */ propertyElements[2]}
- {/* Legacy ID */ propertyElements[2]} + {/* Legacy ID */ propertyElements[1]}
- {propertyElementLabels[5]} - {/* Open Review Opp' */ propertyElements[5]} + {/* propertyElementLabels[5] */} + {/* Open Review Opp' */ propertyElements[3]}
- {propertyElementLabels[6]} - {/* Review Applications */ propertyElements[6]} + {propertyElementLabels[4]} + {/* Review Applications */ propertyElements[4]}
- {/* Action */ propertyElements[7]} + {/* Action */ propertyElements[5]}
diff --git a/src/apps/admin/src/lib/components/ReviewSummaryList/ReviewSummaryList.tsx b/src/apps/admin/src/lib/components/ReviewSummaryList/ReviewSummaryList.tsx index ae2103f54..cf520cec4 100644 --- a/src/apps/admin/src/lib/components/ReviewSummaryList/ReviewSummaryList.tsx +++ b/src/apps/admin/src/lib/components/ReviewSummaryList/ReviewSummaryList.tsx @@ -58,11 +58,12 @@ const ChallengeTitle: FC<{ const ReviewSummaryList: FC = props => { const columns = useMemo[]>( () => [ - { - label: 'Challenge type', - propertyName: '', - type: 'text', - }, + // Hide the columns temporary, we do not have these data now + // { + // label: 'Challenge type', + // propertyName: '', + // type: 'text', + // }, { label: 'Challenge Title', propertyName: 'challengeName', @@ -76,11 +77,11 @@ const ReviewSummaryList: FC = props => { propertyName: 'legacyChallengeId', type: 'text', }, - { - label: 'Current phase', - propertyName: '', - type: 'text', - }, + // { + // label: 'Current phase', + // propertyName: '', + // type: 'text', + // }, { label: 'Status', propertyName: 'challengeStatus', type: 'text' }, // I think this column is important, and it exits in `admin-app` // but resp does not have it, so I just comment it here diff --git a/src/apps/admin/src/lib/components/ReviewerList/ReviewerList.tsx b/src/apps/admin/src/lib/components/ReviewerList/ReviewerList.tsx index a3e8cca39..4fb40c7c0 100644 --- a/src/apps/admin/src/lib/components/ReviewerList/ReviewerList.tsx +++ b/src/apps/admin/src/lib/components/ReviewerList/ReviewerList.tsx @@ -1,7 +1,7 @@ import { FC, useMemo } from 'react' import { format } from 'date-fns' -import { CheckIcon } from '@heroicons/react/solid' +import { CheckIcon, XIcon } from '@heroicons/react/solid' import { useWindowSize, WindowSize } from '~/libs/shared' import { Button, @@ -27,6 +27,7 @@ export interface ReviewerListProps { approvingReviewerId: number onPageChange: (page: number) => void onApproveApplication: (reviewer: Reviewer) => void + onUnapproveApplication: (reviewer: Reviewer) => void onToggleSort: (sort: Sort) => void } @@ -35,15 +36,22 @@ const ApproveButton: FC<{ openReviews: number approvingReviewerId: number onApproveApplication: ReviewerListProps['onApproveApplication'] + onUnapproveApplication: ReviewerListProps['onUnapproveApplication'] }> = props => { const handleApprove = useEventCallback((): void => { props.onApproveApplication(props.reviewer) }) + const handleRemove = useEventCallback((): void => { + props.onUnapproveApplication(props.reviewer) + }) + const isApproving = props.approvingReviewerId === props.reviewer.userId const isOtherApproving = props.approvingReviewerId > 0 const hideApproveButton = props.openReviews < 1 || props.reviewer.applicationStatus !== 'Pending' + const showRemoveButton + = props.reviewer.applicationStatus === 'Approved' return ( <> @@ -53,7 +61,19 @@ const ApproveButton: FC<{ className={styles.approvingLoadingSpinner} /> ) : ( - !hideApproveButton && ( + hideApproveButton ? ( + showRemoveButton && ( + + ) + ) : ( - - -) - -export default ChallengeLinks diff --git a/src/apps/review/src/lib/components/ChallengeLinks/index.ts b/src/apps/review/src/lib/components/ChallengeLinks/index.ts deleted file mode 100644 index 05d4b8e7c..000000000 --- a/src/apps/review/src/lib/components/ChallengeLinks/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as ChallengeLinks } from './ChallengeLinks' diff --git a/src/apps/review/src/lib/components/ChallengePhaseInfo/ChallengePhaseInfo.module.scss b/src/apps/review/src/lib/components/ChallengePhaseInfo/ChallengePhaseInfo.module.scss deleted file mode 100644 index 39265ee81..000000000 --- a/src/apps/review/src/lib/components/ChallengePhaseInfo/ChallengePhaseInfo.module.scss +++ /dev/null @@ -1,31 +0,0 @@ -@import "@libs/ui/styles/includes"; - -.container { - display: flex; - gap: 48px; - margin-top: 24px; - flex-wrap: wrap; - - @include ltelg { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 15px 30px; - } - - @include ltesm { - grid-template-columns: 1fr; - } -} - -.blockItem { - display: flex; - font-size: 14px; - line-height: 19px; - /* identical to box height, or 136% */ - letter-spacing: -0.02em; - gap: 5px; - - strong { - @include font-weight-semibold; - } -} diff --git a/src/apps/review/src/lib/components/ChallengePhaseInfo/ChallengePhaseInfo.tsx b/src/apps/review/src/lib/components/ChallengePhaseInfo/ChallengePhaseInfo.tsx deleted file mode 100644 index 2283ba44a..000000000 --- a/src/apps/review/src/lib/components/ChallengePhaseInfo/ChallengePhaseInfo.tsx +++ /dev/null @@ -1,58 +0,0 @@ -/** - * Challenge Phase Info. - */ -import { FC, useMemo } from 'react' -import classNames from 'classnames' - -import { ChallengeInfo } from '../../models' - -import styles from './ChallengePhaseInfo.module.scss' - -interface Props { - className?: string - challengeInfo: ChallengeInfo -} - -export const ChallengePhaseInfo: FC = (props: Props) => { - const uiItems = useMemo(() => { - const data = props.challengeInfo - return [ - { - title: 'Phase', - value: data.currentPhase, - }, - { - title: 'Phase End Date', - value: data.currentPhaseEndDateString, - }, - { - style: { - color: data.timeLeftColor, - }, - title: 'Time Left', - value: data.timeLeft, - }, - { - title: 'Review Progress', - value: data.reviewProgress - ? `${data.reviewProgress}% Completed` - : '-', - }, - ] - }, [props.challengeInfo]) - return ( -
- {uiItems.map(item => ( -
- - {item.title} - : - - {item.value} -
- ))} -
- ) -} - -export default ChallengePhaseInfo diff --git a/src/apps/review/src/lib/components/ChallengePhaseInfo/index.ts b/src/apps/review/src/lib/components/ChallengePhaseInfo/index.ts deleted file mode 100644 index 7e930cfc3..000000000 --- a/src/apps/review/src/lib/components/ChallengePhaseInfo/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as ChallengePhaseInfo } from './ChallengePhaseInfo' diff --git a/src/apps/review/src/lib/components/ConfirmModal/ConfirmModal.module.scss b/src/apps/review/src/lib/components/ConfirmModal/ConfirmModal.module.scss deleted file mode 100644 index 3c295c38e..000000000 --- a/src/apps/review/src/lib/components/ConfirmModal/ConfirmModal.module.scss +++ /dev/null @@ -1,4 +0,0 @@ -.bodyClassName { - margin: 0; - padding: 0; -} diff --git a/src/apps/review/src/lib/components/ConfirmModal/ConfirmModal.tsx b/src/apps/review/src/lib/components/ConfirmModal/ConfirmModal.tsx deleted file mode 100644 index fbd73cebd..000000000 --- a/src/apps/review/src/lib/components/ConfirmModal/ConfirmModal.tsx +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Confirm Modal. - */ -import { FC } from 'react' - -import { ConfirmModalProps } from '~/libs/ui/lib/components/modals/confirm/ConfirmModal' -import { ConfirmModal as ConfirmModalOriginal } from '~/libs/ui' - -import styles from './ConfirmModal.module.scss' - -export const ConfirmModal: FC = ( - props: ConfirmModalProps, -) => ( - -) - -export default ConfirmModal diff --git a/src/apps/review/src/lib/components/ConfirmModal/index.ts b/src/apps/review/src/lib/components/ConfirmModal/index.ts deleted file mode 100644 index b00ce52b7..000000000 --- a/src/apps/review/src/lib/components/ConfirmModal/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as ConfirmModal } from './ConfirmModal' diff --git a/src/apps/review/src/lib/components/FieldMarkdownEditor/FieldMarkdownEditor.module.scss b/src/apps/review/src/lib/components/FieldMarkdownEditor/FieldMarkdownEditor.module.scss deleted file mode 100644 index 5e5092611..000000000 --- a/src/apps/review/src/lib/components/FieldMarkdownEditor/FieldMarkdownEditor.module.scss +++ /dev/null @@ -1,161 +0,0 @@ -@import '@libs/ui/styles/typography'; -@import '@libs/ui/styles/includes'; - -$error-line-height: 14px; - -.container { - display: flex; - flex-direction: column; - height: 366px; - gap: 9px; - - &.showBorder { - :global { - .CodeMirror.CodeMirror-wrap { - border-right: 1px solid var(--GrayBorder); - border-left: 1px solid var(--GrayBorder); - border-bottom: 1px solid var(--GrayBorder); - } - .editor-toolbar { - border-top: 1px solid var(--GrayBorder); - border-left: 1px solid var(--GrayBorder); - border-right: 1px solid var(--GrayBorder); - } - } - } - - &.isError { - :global { - .CodeMirror.CodeMirror-wrap { - border-right: 1px solid var(--RedError); - border-left: 1px solid var(--RedError); - border-bottom: 1px solid var(--RedError); - } - .editor-toolbar { - border-top: 1px solid var(--RedError); - border-left: 1px solid var(--RedError); - border-right: 1px solid var(--RedError); - } - } - } - - :global { - .EasyMDEContainer { - height: 100px; - flex: 1; - display: flex; - flex-direction: column; - } - - .CodeMirror.CodeMirror-wrap { - min-height: 0; - flex: 1; - box-sizing: border-box; - height: auto; - border-right: 1px solid white; - border-left: 1px solid white; - border-bottom: 1px solid white; - border-top: 1px solid var(--GrayBorder); - border-bottom-left-radius: 8px; - border-bottom-right-radius: 8px; - color: var(--Primary); - } - - .editor-toolbar { - opacity: 1; - border-top-left-radius: 8px; - border-top-right-radius: 8px; - border-top: 1px solid white; - border-left: 1px solid white; - border-right: 1px solid white; - display: flex; - flex-wrap: wrap; - gap: 8px; - padding: 0 8px; - background-color: white; - - &::after, - &::before { - content: none; - } - - button.table { - width: auto; - } - - button { - margin: 8px 0; - width: 32px; - height: 32px; - display: flex; - align-items: center; - justify-content: center; - padding: 0; - margin: 9px 0; - - &::after { - content: none; - } - } - - i.separator { - border-left: 1px solid var(--GrayBorder); - } - } - - .editor-statusbar { - font-size: 14px; - line-height: 19px; - padding: 9px 0 0 0; - overflow: hidden; - display: flex; - - span { - min-width: 0; - } - - .upload-image { - margin-left: 0; - margin-right: auto; - display: flex; - color: var(--Secondary); - font-size: 14px; - line-height: 19px; - } - - .countOfRemainingChars { - margin-left: 0; - min-width: 0; - display: flex; - } - - .message { - display: none; - } - } - - .cm-s-easymde { - .cm-link, - .cm-url { - color: var(--Link); - } - } - } -} - -.error { - display: flex; - align-items: center; - color: $red-100; - // extend body ultra small and override it - font-size: 14px; - line-height: 19px; - line-height: $error-line-height; - margin-top: $sp-1; - - svg { - @include icon-md; - fill: $red-100; - margin-right: $sp-1; - } -} diff --git a/src/apps/review/src/lib/components/FieldMarkdownEditor/FieldMarkdownEditor.tsx b/src/apps/review/src/lib/components/FieldMarkdownEditor/FieldMarkdownEditor.tsx deleted file mode 100644 index c1aa2a24e..000000000 --- a/src/apps/review/src/lib/components/FieldMarkdownEditor/FieldMarkdownEditor.tsx +++ /dev/null @@ -1,805 +0,0 @@ -/** - * Field Markdown Editor. - */ -import { FC, useCallback, useEffect, useRef } from 'react' -import _ from 'lodash' -import CodeMirror from 'codemirror' -import EasyMDE from 'easymde' -import classNames from 'classnames' -import 'easymde/dist/easymde.min.css' - -import { useOnComponentDidMount } from '~/apps/admin/src/lib/hooks' -import { IconSolid } from '~/libs/ui' - -import { - IconBold, - IconCode, - IconHeading1, - IconHeading2, - IconHeading3, - IconImage, - IconItalic, - IconLink, - IconMentions, - IconOrderedList, - IconQuote, - IconStrikethrough, - IconTable, - IconUnorderedList, - IconUploadFile, -} from '../../assets/icons' -import { MockUploadUrl } from '../../../mock-datas' -import { humanFileSize } from '../../utils' - -import styles from './FieldMarkdownEditor.module.scss' - -interface Props { - className?: string - placeholder?: string - initialValue?: string - onChange?: (value: string) => void - onBlur?: () => void - error?: string - showBorder?: boolean -} -const errorMessages = { - fileTooLarge: - 'Uploading #image_name# was failed. The file is too big (#image_size#).\n' - + 'Maximum file size is #image_max_size#.', - importError: - 'Uploading #image_name# was failed. Something went wrong when uploading the file.', - noFileGiven: 'Select a file.', - typeNotAllowed: - 'Uploading #image_name# was failed. The file type (#image_type#) is not supported.', -} -const maxUploadSize = 40 * 1024 * 1024 -const imageExtensions = ['gif', 'png', 'jpeg', 'jpg', 'bmp', 'svg'] -const allowedImageExtensions = [ - ...imageExtensions, - ...imageExtensions.map(key => `image/${key}`), -] -const allowedOtherExtensions = [ - 'application/zip', - 'zip', - 'application/octet-stream', - 'application/x-zip-compressed', - 'multipart/x-zip', - 'text/plain', - 'txt', - 'mov', - 'video/mpeg', - 'mp4', - 'video/mp4', - 'webm', - 'video/webm', - 'doc', - 'docx', - 'pdf', - 'application/pdf', - 'csv', - 'text/csv', - 'htm', - 'html', - 'text/html', - 'js', - 'json', - 'application/json', - 'application/msword', - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - 'application/vnd.ms-excel', - 'xls', - 'xlsx', - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - 'ppt', - 'application/vnd.ms-powerpoint', - 'pptx', - 'application/vnd.openxmlformats-officedocument.presentationml.presentation', -] - -export const FieldMarkdownEditor: FC = (props: Props) => { - const elementRef = useRef(null) - const easyMDE = useRef(null) - - /** - * The state of CodeMirror at the given position. - */ - const getState = useCallback( - // eslint-disable-next-line complexity - (cm: CodeMirror.Editor, posInput?: CodeMirror.Position | undefined) => { - const pos = posInput || cm.getCursor('start') - const stat = cm.getTokenAt(pos) - if (!stat.type) return {} - - const types = stat.type.split(' ') - - const ret: any = {} - - let data - let text - for (let i = 0; i < types.length; i++) { - data = types[i] - if (data === 'strong') { - ret.bold = true - } else if (data === 'variable-2') { - text = cm.getLine(pos.line) - if (/^\s*\d+\.\s/.test(text)) { - ret['ordered-list'] = true - } else { - ret['unordered-list'] = true - } - } else if (data === 'atom') { - ret.quote = true - } else if (data === 'em') { - ret.italic = true - } else if (data === 'quote') { - ret.quote = true - } else if (data === 'strikethrough') { - ret.strikethrough = true - } else if (data === 'comment') { - ret.code = true - } else if (data === 'link') { - ret.link = true - } else if (data === 'tag') { - ret.image = true - } else if (data.match(/^header(-[1-6])?$/)) { - ret[data.replace('header', 'heading')] = true - } - } - - return ret - }, - [], - ) - - /** - * Handle toggle block - */ - const toggleBlock = useCallback( - // eslint-disable-next-line complexity - (editor: any, type: string, startChars: any, endCharsInput?: any) => { - if ( - /editor-preview-active/.test( - editor.codemirror.getWrapperElement().lastChild.className, - ) - ) { - return - } - - const endChars = typeof endCharsInput === 'undefined' ? startChars : endCharsInput - const cm = editor.codemirror - const stat = getState(cm) - - let text = '' - let start = startChars - let end = endChars - - const startPoint = cm.getCursor('start') - const endPoint = cm.getCursor('end') - - if (stat[type]) { - text = cm.getLine(startPoint.line) - start = text.slice(0, startPoint.ch) - end = text.slice(startPoint.ch) - if (type === 'bold') { - start = start.replace(/(\*\*|__)(?![\s\S]*(\*\*|__))/, '') - end = end.replace(/(\*\*|__)/, '') - } else if (type === 'italic') { - start = start.replace(/(\*|_)(?![\s\S]*(\*|_))/, '') - end = end.replace(/(\*|_)/, '') - } else if (type === 'strikethrough') { - start = start.replace(/(\*\*|~~)(?![\s\S]*(\*\*|~~))/, '') - end = end.replace(/(\*\*|~~)/, '') - } - - cm.replaceRange( - start + end, - { - ch: 0, - line: startPoint.line, - }, - { - ch: 99999999999999, - line: startPoint.line, - }, - ) - - if (type === 'bold' || type === 'strikethrough') { - startPoint.ch -= 2 - if (startPoint !== endPoint) { - endPoint.ch -= 2 - } - } else if (type === 'italic') { - startPoint.ch -= 1 - if (startPoint !== endPoint) { - endPoint.ch -= 1 - } - } - } else { - text = cm.getSelection() - let trimText = text.trim() - let lastSpaces = '' - for (let i = trimText.length; i < text.length; i++) { - lastSpaces += text[i] - } - - if (type === 'bold') { - trimText = trimText.split('**') - .join('') - } else if (type === 'italic') { - trimText = trimText.split('*') - .join('') - } else if (type === 'strikethrough') { - trimText = trimText.split('~~') - .join('') - } - - cm.replaceSelection(start + trimText + end + lastSpaces) - - startPoint.ch += startChars.length - endPoint.ch = startPoint.ch + text.length - } - - cm.setSelection(startPoint, endPoint) - cm.focus() - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [], - ) - - /** - * Show hint after '@' - */ - const completeAfter = useCallback((cm: CodeMirror.Editor) => { - if (!cm.state.completionActive) { - if (cm.getCursor().ch === 0) { - cm.replaceSelection('@') - } else { - const from = { - ch: 0, - line: cm.getCursor().line, - } - const to = cm.getCursor() - const line = cm.getRange(from, to) - const lastIndexOf = line.lastIndexOf(' ') - const tokenIndex = lastIndexOf > -1 ? lastIndexOf + 1 : 0 - cm.replaceRange('@', { - ch: tokenIndex, - line: cm.getCursor().line, - }) - } - } - - return CodeMirror.Pass - }, []) - - /** - * Update file tag - */ - const updateFileTag = useCallback( - (cm: CodeMirror.Editor, position: any, startEnd: any, data: any) => { - if ( - /editor-preview-active/.test( - (cm.getWrapperElement()?.lastChild as any)?.className, - ) - ) { - return - } - - let start = startEnd[0] - let end = startEnd[1] - const startPoint: any = {} - const endPoint: any = {} - if (data && (data.url || data.name)) { - start = start.replace('#name#', data.name) // url is in start for upload-image - start = start.replace('#url#', data.url) // url is in start for upload-image - end = end.replace('#name#', data.name) - end = end.replace('#url#', data.url) - } - - Object.assign(startPoint, { - ch: position.start.ch, - line: position.start.line, - }) - Object.assign(endPoint, { - ch: position.end.ch, - line: position.end.line, - }) - cm.replaceRange(start + end, startPoint, endPoint) - - const selectionPosition = { - ch: start.length + end.length, - line: position.start.line, - } - cm.setSelection(selectionPosition, selectionPosition) - cm.focus() - }, - [], - ) - - /** - * After file uploaded - */ - const afterFileUploaded = useCallback((jsonData: any, position: any) => { - const editor = easyMDE.current - const cm = editor.codemirror - const options = editor.options - const imageName = jsonData.name - const ext = imageName.substring(imageName.lastIndexOf('.') + 1) - - // Check if file type is an image - if (allowedImageExtensions.includes(ext)) { - updateFileTag( - cm, - position, - options.insertTexts.uploadedImage, - jsonData, - ) - } else { - updateFileTag( - cm, - position, - options.insertTexts.uploadedFile, - jsonData, - ) - } - - // show uploaded image filename for 1000ms - editor.updateStatusBar( - 'upload-image', - editor.options.imageTexts.sbOnUploaded.replace( - '#image_name#', - imageName, - ), - ) - setTimeout(() => { - editor.updateStatusBar( - 'upload-image', - editor.options.imageTexts.sbInit, - ) - }, 1000) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) - - /** - * Reset file input - */ - const resetFileInput = useCallback(() => { - const imageInput - = easyMDE.current.gui.toolbar.getElementsByClassName('imageInput')[0] - imageInput.value = '' - }, []) - - /** - * Replace selection - */ - const replaceSelection = useCallback( - ( - cm: CodeMirror.Editor, - active: boolean, - startEnd: string[], - data: any, - onPosition: any, - ) => { - if ( - /editor-preview-active/.test( - (cm.getWrapperElement()?.lastChild as any)?.className, - ) - ) { - return - } - - let text - let start = startEnd[0] - let end = startEnd[1] - const startPoint: any = {} - const endPoint: any = {} - const currentPosition = cm.getCursor() - - // Start uploading from a new line - if (currentPosition.ch !== 0) { - cm.replaceSelection('\n') - } - - Object.assign(startPoint, cm.getCursor('start')) - Object.assign(endPoint, cm.getCursor('end')) - if (data && data.name) { - start = start.replace('#name#', data.name) - end = end.replace('#name#', data.name) - } - - const initStartPosition = { - ch: startPoint.ch, - line: startPoint.line, - } - - if (active) { - text = cm.getLine(startPoint.line) - start = text.slice(0, startPoint.ch) - end = text.slice(startPoint.ch) - cm.replaceRange(start + end, { - ch: 0, - line: startPoint.line, - }) - } else { - text = cm.getSelection() - cm.replaceSelection(start + text + end) - startPoint.ch += start.length - if (startPoint !== endPoint) { - endPoint.ch += start.length - } - } - - onPosition(initStartPosition, endPoint) - - const line = cm.getLine(cm.getCursor().line) - const appendedTextLength = start.length + text.length + end.length - if (line.length > appendedTextLength) { - cm.replaceSelection('\n') - cm.setSelection( - { - ch: line.length - appendedTextLength, - line: startPoint.line + 1, - }, - { - ch: line.length - appendedTextLength, - line: startPoint.line + 1, - }, - ) - } else { - // Set a cursor at the end of line - cm.setSelection(startPoint, endPoint) - } - - cm.focus() - }, - [], - ) - - /** - * Before uploading file - */ - const beforeUploadingFile = useCallback((file: File, onPosition: any) => { - const editor = easyMDE.current - const cm = editor.codemirror - const stat = getState(cm) - const options = editor.options - const fileName = file.name - const ext = fileName.substring(fileName.lastIndexOf('.') + 1) - // Check if file type is an image - if (allowedImageExtensions.includes(ext)) { - replaceSelection( - cm, - stat.image, - options.insertTexts.uploadingImage, - { name: fileName }, - onPosition, - ) - } else { - replaceSelection( - cm, - stat.link, - options.insertTexts.uploadingFile, - { name: fileName }, - onPosition, - ) - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) - - /** - * Upload image - */ - const customUploadImage = useCallback((file: File) => { - const position: any = {} - - const onSuccess: (jsonData: any) => void = (jsonData: any) => { - afterFileUploaded(jsonData, position) - resetFileInput() - } - - const onError: () => void = () => { - if (position && position.start && position.end) { - easyMDE.current.codemirror.replaceRange( - '', - position.start, - position.end, - ) - } - - resetFileInput() - } - - const onErrorSup: (errorMessage: string) => void = (errorMessage: string) => { - // show reset status bar - easyMDE.current.updateStatusBar( - 'upload-image', - easyMDE.current.options.imageTexts.sbInit, - ) - // run custom error handler - if (onError && typeof onError === 'function') { - onError() - } - - // run error handler from options - easyMDE.current.options.errorCallback(errorMessage) - } - - // Sometimes a browser couldn't define mime/types, use file extension - const getFileType: () => string = () => (file.type - ? file.type - : file.name.substring(file.name.lastIndexOf('.') + 1)) - - // Parse a message - const fillErrorMessage: (errorMessage: string) => string = (errorMessage: string) => { - const units - = easyMDE.current.options.imageTexts.sizeUnits.split(',') - - const error = errorMessage - .replace('#image_type#', getFileType()) - .replace('#image_name#', file.name) - .replace('#image_size#', humanFileSize(file.size, units)) - .replace( - '#image_max_size#', - humanFileSize(easyMDE.current.options.imageMaxSize, units), - ) - - return ( - `
  • ${error}
` - ) - } - - // Save a position of image/file tag - const onPosition: (start: any, end: any) => void = (start: any, end: any) => { - position.start = start - position.end = end - } - - // Check mime types - if (!easyMDE.current.options.imageAccept.includes(getFileType())) { - onErrorSup( - fillErrorMessage( - easyMDE.current.options.errorMessages.typeNotAllowed, - ), - ) - return - } - - // Check max file size before uploading - if (file.size > easyMDE.current.options.imageMaxSize) { - onErrorSup( - fillErrorMessage( - easyMDE.current.options.errorMessages.fileTooLarge, - ), - ) - return - } - - beforeUploadingFile(file, onPosition) - - onSuccess({ - name: file.name, - url: MockUploadUrl, - }) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) - - useOnComponentDidMount(() => { - easyMDE.current = new EasyMDE({ - autofocus: false, - element: elementRef.current as HTMLElement, - errorCallback: _.noop, // A callback function used to define how to display an error message. - errorMessages, - forceSync: true, // true, force text changes made in EasyMDE to be immediately stored in original text area. - hideIcons: ['guide', 'heading', 'preview', 'side-by-side'], - imageAccept: [ - ...allowedImageExtensions, - ...allowedOtherExtensions, - ].join(', '), // A comma-separated list of mime-types and extensions - imageMaxSize: maxUploadSize, // Maximum image size in bytes - imageTexts: { - sbInit: 'Attach files by dragging & dropping, selecting or pasting them.', - sbOnDragEnter: 'Drop file to upload it.', - sbOnDrop: 'Uploading file #images_names#...', - sbOnUploaded: 'Uploaded #image_name#', - sbProgress: 'Uploading #file_name#: #progress#%', - sizeUnits: ' B, KB, MB', - }, - imageUploadFunction: file => { - setTimeout(() => { - customUploadImage(file) - }) - }, - initialValue: props.initialValue, - insertTexts: { - file: ['[](', '#url#)'], - horizontalRule: ['', '\n\n-----\n\n'], - image: ['![](', '#url#)'], - link: ['[', '](#url#)'], - table: [ - '', - // eslint-disable-next-line max-len - '\n\n| Column 1 | Column 2 | Column 3 |\n| -------- | -------- | -------- |\n| Text | Text | Text |\n\n', - ], - uploadedFile: ['[#name#](#url#)', ''], - uploadedImage: ['![#name#](#url#)', ''], - uploadingFile: ['[Uploading #name#]()', ''], - uploadingImage: ['![Uploading #name#]()', ''], - } as any, - placeholder: '', - shortcuts: { - toggleHeading1: '', - toggleHeading2: '', - toggleHeading3: '', - }, - status: [ - { - className: 'message', - defaultValue: el => { - el.innerHTML = '' - }, - onUpdate: el => { - el.innerHTML = '' - }, - }, - 'upload-image', - ], - toolbar: [ - { - action: (editor: any) => { - toggleBlock( - editor, - 'bold', - editor.options.blockStyles.bold, - ) - }, - className: 'fa fa-bold', - icon: IconBold, - name: 'toggleBold', - title: 'Bold', - }, - { - action: (editor: any) => { - toggleBlock( - editor, - 'italic', - editor.options.blockStyles.italic, - ) - }, - className: 'fa fa-italic', - icon: IconItalic, - name: 'toggleItalic', - title: 'Italic', - }, - { - action: EasyMDE.toggleStrikethrough, - className: 'fa fa-bold', - icon: IconStrikethrough, - name: 'strikethrough', - title: 'Strikethrough', - }, - '|', - { - action: EasyMDE.toggleHeading1, - className: 'fa fa-bold', - icon: IconHeading1, - name: 'heading-1', - title: 'Big Heading', - }, - { - action: EasyMDE.toggleHeading2, - className: 'fa fa-bold', - icon: IconHeading2, - name: 'heading-2', - title: 'Medium Heading', - }, - { - action: EasyMDE.toggleHeading3, - className: 'fa fa-bold', - icon: IconHeading3, - name: 'heading-3', - title: 'Small Heading', - }, - '|', - { - action: EasyMDE.toggleOrderedList, - className: 'fa fa-bold', - icon: IconOrderedList, - name: 'ordered-list', - title: 'Numbered List', - }, - { - action: EasyMDE.toggleUnorderedList, - className: 'fa fa-bold', - icon: IconUnorderedList, - name: 'unordered-list', - title: 'Generic List', - }, - '|', - { - action: EasyMDE.drawLink, - className: 'fa fa-bold', - icon: IconLink, - name: 'link', - title: 'Create Link', - }, - { - action: EasyMDE.drawUploadedImage, - className: 'fa fa-upload', - icon: IconUploadFile, - name: 'upload-image', - title: 'Upload a file', - }, - { - action: EasyMDE.drawImage, - className: 'fa fa-bold', - icon: IconImage, - name: 'image', - title: 'Insert Image', - }, - { - action: EasyMDE.toggleCodeBlock, - className: 'fa fa-bold', - icon: IconCode, - name: 'code', - title: 'Code', - }, - { - action: EasyMDE.drawTable, - className: 'fa fa-bold', - icon: IconTable, - name: 'table', - title: 'Insert Table', - }, - { - action: function mentions(editor: EasyMDE) { - completeAfter(editor.codemirror) - }, - className: 'fa fa-at', - icon: IconMentions, - name: 'mentions', - title: 'Mention a Topcoder User', - }, - { - action: EasyMDE.toggleBlockquote, - className: 'fa fa-bold', - icon: IconQuote, - name: 'quote', - title: 'Quote', - }, - ], - uploadImage: true, - }) - - easyMDE.current.codemirror.on('change', (cm: CodeMirror.Editor) => { - props.onChange?.(cm.getValue()) - }) - - easyMDE.current.codemirror.on('blur', () => { - props.onBlur?.() - }) - }) - - useEffect(() => { - easyMDE.current?.value(props.initialValue) - }, [props.initialValue]) - - return ( -
-