1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-27 07:44:50 +00:00

refactor: ocr result display (#7936)

Co-authored-by: 杨国璇 <ygx@MacBookPro.lan>
This commit is contained in:
杨国璇
2025-06-19 18:32:46 +08:00
committed by GitHub
parent 99660d9bef
commit 9d8e35af7a
20 changed files with 406 additions and 181 deletions

View File

@@ -0,0 +1,36 @@
import React from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import { canUseDOM } from '../../utils/dom';
class BodyPortal extends React.Component {
componentWillUnmount() {
if (this.defaultNode) {
document.body.removeChild(this.defaultNode);
}
this.defaultNode = null;
}
render() {
if (!canUseDOM) {
return null;
}
if (!this.props.node && !this.defaultNode) {
this.defaultNode = document.createElement('div');
document.body.appendChild(this.defaultNode);
}
return ReactDOM.createPortal(
this.props.children,
this.props.node || this.defaultNode,
);
}
}
BodyPortal.propTypes = {
children: PropTypes.node.isRequired,
node: PropTypes.any,
};
export default BodyPortal;

View File

@@ -1,5 +1,6 @@
import { MENU_SHOW, MENU_HIDE } from './actions'; import { MENU_SHOW, MENU_HIDE } from './actions';
import { uniqueId, hasOwnProp, canUseDOM } from './helpers'; import { uniqueId, hasOwnProp } from './helpers';
import { canUseDOM } from '../../utils/dom';
class GlobalEventListener { class GlobalEventListener {

View File

@@ -11,7 +11,3 @@ export function uniqueId() {
} }
export const store = {}; export const store = {};
export const canUseDOM = Boolean(
typeof window !== 'undefined' && window.document && window.document.createElement
);

View File

@@ -52,7 +52,7 @@ const ImageDialog = ({ repoID, repoInfo, enableRotate: oldEnableRotate = true, i
const isSystemFolder = SYSTEM_FOLDERS.find(folderPath => mainImg.parentDir.startsWith(folderPath)); const isSystemFolder = SYSTEM_FOLDERS.find(folderPath => mainImg.parentDir.startsWith(folderPath));
let onOCR = null; let onOCR = null;
if (enableOCR && enableMetadata && canModify && !isSystemFolder) { if (enableOCR && enableMetadata && canModify && !isSystemFolder) {
onOCR = () => onOCRByImageDialog({ parentDir: mainImg.parentDir, fileName: mainImg.name }); onOCR = () => onOCRByImageDialog({ parentDir: mainImg.parentDir, fileName: mainImg.name }, document.activeElement);
} }
const renderSidePanel = () => { const renderSidePanel = () => {

View File

@@ -14,7 +14,8 @@ function DragMask({ draggedRange, getSelectedRangeDimensions, getSelectedDimensi
return ( return (
<CellMask <CellMask
{...dimensions} {...dimensions}
className='react-grid-cell-dragged-over-down' className="react-grid-cell-dragged-over-down"
id="sf-table-cell-dragged-over-down"
/> />
); );
} }

View File

@@ -7,6 +7,7 @@ function SelectionMask({ innerRef, selectedPosition, getSelectedDimensions, chil
return ( return (
<CellMask <CellMask
className="rdg-selected" className="rdg-selected"
id="sf-table-rdg-selected"
tabIndex="0" tabIndex="0"
innerRef={innerRef} innerRef={innerRef}
{...dimensions} {...dimensions}

View File

@@ -8,6 +8,7 @@ function SelectionRangeMask({ selectedRange, innerRef, getSelectedRangeDimension
<CellMask <CellMask
{...dimensions} {...dimensions}
className="rdg-selected-range" className="rdg-selected-range"
id="sf-table-rdg-selected-range"
innerRef={innerRef} innerRef={innerRef}
> >
{children} {children}

View File

@@ -16,6 +16,7 @@ import { openInNewTab, openParentFolder } from '../../metadata/utils/file';
const TableFilesToolbar = ({ repoID }) => { const TableFilesToolbar = ({ repoID }) => {
const [selectedRecordIds, setSelectedRecordIds] = useState([]); const [selectedRecordIds, setSelectedRecordIds] = useState([]);
const metadataRef = useRef([]); const metadataRef = useRef([]);
const menuRef = useRef(null);
const { enableOCR } = useMetadataStatus(); const { enableOCR } = useMetadataStatus();
const canModify = window.sfMetadataContext && window.sfMetadataContext.canModify(); const canModify = window.sfMetadataContext && window.sfMetadataContext.canModify();
@@ -139,7 +140,7 @@ const TableFilesToolbar = ({ repoID }) => {
break; break;
} }
case TextTranslation.OCR.key: { case TextTranslation.OCR.key: {
eventBus && eventBus.dispatch(EVENT_BUS_TYPE.OCR, records[0]); eventBus && eventBus.dispatch(EVENT_BUS_TYPE.OCR, records[0], menuRef.current.dropdownRef.current);
break; break;
} }
default: default:
@@ -180,8 +181,9 @@ const TableFilesToolbar = ({ repoID }) => {
} }
{length > 0 && ( {length > 0 && (
<ItemDropdownMenu <ItemDropdownMenu
ref={menuRef}
item={{}} item={{}}
toggleClass={'cur-view-path-btn sf3-font-more sf3-font'} toggleClass="cur-view-path-btn sf3-font-more sf3-font"
onMenuItemClick={onMenuItemClick} onMenuItemClick={onMenuItemClick}
getMenuList={getMenuList} getMenuList={getMenuList}
/> />

View File

@@ -4,7 +4,7 @@ import { Utils } from '../utils/utils';
import toaster from '../components/toast'; import toaster from '../components/toast';
import { PRIVATE_COLUMN_KEY, EVENT_BUS_TYPE } from '../metadata/constants'; import { PRIVATE_COLUMN_KEY, EVENT_BUS_TYPE } from '../metadata/constants';
import { gettext, lang } from '../utils/constants'; import { gettext, lang } from '../utils/constants';
import OCRResultDialog from '../metadata/components/dialog/ocr-result-dialog'; import { OCRResultPopover } from '../metadata/components/popover';
import FileTagsDialog from '../metadata/components/dialog/file-tags-dialog'; import FileTagsDialog from '../metadata/components/dialog/file-tags-dialog';
// This hook provides content related to metadata ai operation // This hook provides content related to metadata ai operation
@@ -23,6 +23,7 @@ export const MetadataAIOperationsProvider = ({
const [isFileTagsDialogShow, setFileTagsDialogShow] = useState(false); const [isFileTagsDialogShow, setFileTagsDialogShow] = useState(false);
const recordRef = useRef(null); const recordRef = useRef(null);
const targetRef = useRef(null);
const opCallBack = useRef(null); const opCallBack = useRef(null);
const permission = useMemo(() => repoInfo.permission !== 'admin' && repoInfo.permission !== 'rw' ? 'r' : 'rw', [repoInfo]); const permission = useMemo(() => repoInfo.permission !== 'admin' && repoInfo.permission !== 'rw' ? 'r' : 'rw', [repoInfo]);
@@ -36,21 +37,24 @@ export const MetadataAIOperationsProvider = ({
const closeOcrResultDialog = useCallback(() => { const closeOcrResultDialog = useCallback(() => {
recordRef.current = null; recordRef.current = null;
targetRef.current = null;
opCallBack.current = null; opCallBack.current = null;
setOcrResultDialogShow(false); setOcrResultDialogShow(false);
}, []); }, []);
const onOCR = useCallback((record, { success_callback }) => { const onOCR = useCallback((record, { success_callback }, target) => {
targetRef.current = target;
recordRef.current = record; recordRef.current = record;
opCallBack.current = success_callback; opCallBack.current = success_callback;
setOcrResultDialogShow(true); setOcrResultDialogShow(true);
}, []); }, []);
const onOCRByImageDialog = useCallback(({ parentDir, fileName } = {}) => { const onOCRByImageDialog = useCallback(({ parentDir, fileName } = {}, target) => {
recordRef.current = { recordRef.current = {
[PRIVATE_COLUMN_KEY.PARENT_DIR]: parentDir, [PRIVATE_COLUMN_KEY.PARENT_DIR]: parentDir,
[PRIVATE_COLUMN_KEY.FILE_NAME]: fileName, [PRIVATE_COLUMN_KEY.FILE_NAME]: fileName,
}; };
targetRef.current = target;
opCallBack.current = (description) => { opCallBack.current = (description) => {
const update = { [PRIVATE_COLUMN_KEY.FILE_DESCRIPTION]: description }; const update = { [PRIVATE_COLUMN_KEY.FILE_DESCRIPTION]: description };
@@ -149,7 +153,13 @@ export const MetadataAIOperationsProvider = ({
<FileTagsDialog record={recordRef.current} onToggle={closeFileTagsDialog} onSubmit={opCallBack.current} /> <FileTagsDialog record={recordRef.current} onToggle={closeFileTagsDialog} onSubmit={opCallBack.current} />
)} )}
{isOcrResultDialogShow && ( {isOcrResultDialogShow && (
<OCRResultDialog repoID={repoID} record={recordRef.current} onToggle={closeOcrResultDialog} saveToDescription={opCallBack.current} /> <OCRResultPopover
repoID={repoID}
target={targetRef.current}
record={recordRef.current}
onToggle={closeOcrResultDialog}
saveToDescription={opCallBack.current}
/>
)} )}
</MetadataAIOperationsContext.Provider> </MetadataAIOperationsContext.Provider>
); );

View File

@@ -33,11 +33,8 @@ const ContextMenu = ({
const dividerHeight = 16; const dividerHeight = 16;
const optionHeight = 32; const optionHeight = 32;
const menuDefaultHeight = options.reduce((total, option) => { const menuDefaultHeight = options.reduce((total, option) => {
if (option === 'Divider') { if (option === 'Divider') return total + dividerHeight;
return total + dividerHeight;
} else {
return total + optionHeight; return total + optionHeight;
}
}, menuMargin + indent); }, menuMargin + indent);
if (menuStyles.left + menuDefaultWidth + indent > window.innerWidth) { if (menuStyles.left + menuDefaultWidth + indent > window.innerWidth) {
menuStyles.left = window.innerWidth - menuDefaultWidth - indent; menuStyles.left = window.innerWidth - menuDefaultWidth - indent;

View File

@@ -35,7 +35,6 @@
align-items: center; align-items: center;
} }
.sf-file-exit-tag { .sf-file-exit-tag {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;

View File

@@ -1,8 +0,0 @@
.sf-metadata-ocr-file-dialog .longtext-header-divider {
border-bottom: 1px solid #e9ecef;
}
.sf-metadata-ocr-file-dialog .seafile-multicolor-icon-save {
height: 15px;
width: 15px;
}

View File

@@ -1,148 +0,0 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { I18nextProvider } from 'react-i18next';
import { SimpleEditor, getBrowserInfo, LongTextModal, BrowserTip, slateToMdString, MarkdownPreview } from '@seafile/seafile-editor';
import { gettext, lang } from '../../../../utils/constants';
import { getFileNameFromRecord, getParentDirFromRecord } from '../../../utils/cell';
import { Utils } from '../../../../utils/utils';
import i18n from '../../../../_i18n/i18n-seafile-editor';
import Icon from '../../../../components/icon';
import metadataAPI from '../../../api';
import { useMetadataAIOperations } from '../../../../hooks/metadata-ai-operation';
import './index.css';
const OCRResultDialog = ({ repoID, record, onToggle, saveToDescription }) => {
const [isLoading, setLoading] = useState(true);
const [errorMessage, setErrorMessage] = useState('');
const [isFullScreen, setIsFullScreen] = useState(false);
const [dialogStyle, setDialogStyle] = useState({});
const { canModify } = useMetadataAIOperations();
const ocrResult = useRef(null);
const parentDir = useMemo(() => getParentDirFromRecord(record), [record]);
const fileName = useMemo(() => getFileNameFromRecord(record), [record]);
const editorRef = useRef(null);
const { isValidBrowser, isWindowsWechat } = useMemo(() => {
return getBrowserInfo(true);
}, []);
const onFullScreenToggle = useCallback(() => {
let containerStyle = {};
if (!isFullScreen) {
containerStyle = {
width: '100%',
height: '100%',
top: 0,
border: 'none'
};
}
setIsFullScreen(!isFullScreen);
setDialogStyle(containerStyle);
}, [isFullScreen]);
const onContainerKeyDown = useCallback((event) => {
event.stopPropagation();
if (event.keyCode === 27) {
event.preventDefault();
onToggle();
}
}, [onToggle]);
const onSave = useCallback(() => {
const newContent = editorRef.current?.getSlateValue();
const value = slateToMdString(newContent);
saveToDescription(value);
onToggle();
}, [saveToDescription, onToggle]);
useEffect(() => {
const path = Utils.joinPath(parentDir, fileName);
metadataAPI.ocr(repoID, path).then(res => {
const result = res.data?.ocr_result || '';
ocrResult.current = result.replaceAll('\f', '\n').replaceAll('\n\n', '\n').replaceAll('\n', '\n\n');
setLoading(false);
}).catch(error => {
const errorMessage = gettext('Failed to extract text');
setErrorMessage(errorMessage);
setLoading(false);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
document.addEventListener('keydown', onContainerKeyDown);
return () => {
document.removeEventListener('keydown', onContainerKeyDown);
};
}, [onContainerKeyDown]);
return (
<I18nextProvider i18n={ i18n }>
<LongTextModal onModalClick={onToggle} containerClass="sf-metadata-ocr-file-dialog">
<div style={dialogStyle} className="longtext-dialog-container">
<div className={classnames('longtext-header-container', {
'longtext-header-container-border': isWindowsWechat,
'longtext-header-divider': isLoading || errorMessage
})}>
<div className="longtext-header">
<span className="longtext-header-name">{gettext('OCR result')}</span>
<div className="longtext-header-tool">
{!isLoading && !errorMessage && canModify && (
<span
className="longtext-header-tool-item d-flex align-items-center mr-1"
title={gettext('Save to description property')}
onClick={onSave}
>
<Icon symbol="save" />
</span>
)}
<span
className={classnames('longtext-header-tool-item mr-1 iconfont icon-full-screen', { 'long-text-full-screen': isFullScreen })}
onClick={onFullScreenToggle}
title={gettext('Full screen')}
>
</span>
<span
className="longtext-header-tool-item iconfont icon-x"
onClick={onToggle}
title={gettext('Close')}
>
</span>
</div>
</div>
{!isValidBrowser && <BrowserTip lang={lang} isWindowsWechat={isWindowsWechat} />}
</div>
<div
onKeyDown={onContainerKeyDown}
className={classnames('longtext-content-container', { 'longtext-container-scroll': isWindowsWechat })}
>
{errorMessage ? (
<div className="w-100 h-100 d-flex align-items-center justify-content-center error">{errorMessage}</div>
) : (
<>
{isWindowsWechat ? (
<MarkdownPreview isWindowsWechat={isWindowsWechat} value={ocrResult.current} isShowOutline={false} />
) : (
<SimpleEditor ref={editorRef} isFetching={isLoading} focusEnd={false} value={ocrResult.current} />
)}
</>
)}
</div>
</div>
</LongTextModal>
</I18nextProvider>
);
};
OCRResultDialog.propTypes = {
record: PropTypes.object,
onToggle: PropTypes.func,
saveToDescription: PropTypes.func,
};
export default OCRResultDialog;

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useMemo, useState } from 'react'; import React, { useCallback, useMemo, useRef, useState } from 'react';
import { Dropdown, DropdownToggle, DropdownMenu, DropdownItem } from 'reactstrap'; import { Dropdown, DropdownToggle, DropdownMenu, DropdownItem } from 'reactstrap';
import Icon from '../../../components/icon'; import Icon from '../../../components/icon';
import { useMetadataDetails } from '../../hooks'; import { useMetadataDetails } from '../../hooks';
@@ -19,6 +19,7 @@ const OPERATION = {
}; };
const AIIcon = () => { const AIIcon = () => {
const menuToggleRef = useRef(null);
const [isMenuShow, setMenuShow] = useState(false); const [isMenuShow, setMenuShow] = useState(false);
@@ -85,7 +86,7 @@ const AIIcon = () => {
case OPERATION.OCR: { case OPERATION.OCR: {
onOCR(record, { onOCR(record, {
success_callback: updateDescription success_callback: updateDescription
}); }, menuToggleRef.current);
break; break;
} }
case OPERATION.FILE_TAGS: { case OPERATION.FILE_TAGS: {
@@ -141,7 +142,7 @@ const AIIcon = () => {
aria-label='AI' aria-label='AI'
tabIndex={0} tabIndex={0}
> >
<div className="detail-control mr-2"> <div className="detail-control mr-2" ref={menuToggleRef}>
<Icon symbol="ai" className="detail-control-icon" /> <Icon symbol="ai" className="detail-control-icon" />
</div> </div>
</DropdownToggle> </DropdownToggle>

View File

@@ -5,6 +5,7 @@ import FilterPopover from './filter-popover';
import SortPopover from './sort-popover'; import SortPopover from './sort-popover';
import GroupbysPopover from './groupbys-popover'; import GroupbysPopover from './groupbys-popover';
import HideColumnPopover from './hidden-column-popover'; import HideColumnPopover from './hidden-column-popover';
import OCRResultPopover from './ocr-result-popover';
import './index.css'; import './index.css';
@@ -16,4 +17,5 @@ export {
SortPopover, SortPopover,
GroupbysPopover, GroupbysPopover,
HideColumnPopover, HideColumnPopover,
OCRResultPopover,
}; };

View File

@@ -0,0 +1,81 @@
.sf-metadata-ocr-result-popover .popover {
max-width: 500px;
width: 500px;
height: 100%;
overflow: hidden;
}
.sf-metadata-ocr-result-popover .popover-inner {
width: 100%;
height: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
}
.sf-metadata-ocr-result-popover .sf-metadata-ocr-result-popover-header {
height: 48px;
border-bottom: 1px solid rgba(0, 40, 100, .12);
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px;
}
.sf-metadata-ocr-result-popover .sf-metadata-ocr-result-popover-header h5 {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-bottom: 0;
}
.sf-metadata-ocr-result-popover .sf-metadata-ocr-result-popover-body {
flex: 1 1 auto;
padding: 0;
overflow: hidden;
}
.sf-metadata-ocr-result-popover .sf-metadata-ocr-file-loading-container {
display: inline-flex;
align-items: center;
justify-content: flex-start;
padding: 16px;
}
.sf-metadata-ocr-result-popover .sf-metadata-ocr-file-loading-container .loading-tip {
margin: 0;
height: 16px;
width: 16px;
display: block;
flex-shrink: 0;
}
.sf-metadata-ocr-result-popover .sf-metadata-ocr-file-loading-container .sf-metadata-ocr-file-loading-tip-text {
color: #666;
margin-left: 8px;
}
.sf-metadata-ocr-result-popover .sf-metadata-ocr-result-display-container {
width: 100%;
height: 100%;
max-height: inherit;
padding: 16px;
overflow: auto;
white-space: pre-line;
}
.sf-metadata-ocr-result-popover .sf-metadata-ocr-result-display-container.empty-tip {
color: #666;
margin: 0;
display: block;
}
.sf-metadata-ocr-result-popover .sf-metadata-ocr-result-popover-footer {
display: flex;
align-items: center;
flex-shrink: 0;
flex-wrap: wrap;
justify-content: flex-end;
padding: 12px;
border-top: 1px solid rgba(0, 40, 100, .12);
}

View File

@@ -0,0 +1,185 @@
import React, { useCallback, useEffect, useMemo, useRef, useState, useLayoutEffect } from 'react';
import PropTypes from 'prop-types';
import { Button } from 'reactstrap';
import { deserializeHtml, slateToMdString } from '@seafile/seafile-editor';
import isHotkey from 'is-hotkey';
import classnames from 'classnames';
import { gettext } from '../../../../utils/constants';
import { getFileNameFromRecord, getParentDirFromRecord } from '../../../utils/cell';
import { Utils } from '../../../../utils/utils';
import metadataAPI from '../../../api';
import { useMetadataAIOperations } from '../../../../hooks/metadata-ai-operation';
import Loading from '../../../../components/loading';
import { getTarget } from '../../../../utils/dom';
import BodyPortal from '../../../../components/body-portal';
import ClickOutside from '../../../../components/click-outside';
import './index.css';
const OCRResultPopover = ({ repoID, target, record, onToggle, saveToDescription }) => {
const [isLoading, setLoading] = useState(true);
const [errorMessage, setErrorMessage] = useState('');
const [value, setValue] = useState('');
const [isMount, setMount] = useState(false);
const [style, setStyle] = useState({});
const popoverRef = useRef(null);
const bodyRef = useRef(null);
const { canModify } = useMetadataAIOperations();
const parentDir = useMemo(() => getParentDirFromRecord(record), [record]);
const fileName = useMemo(() => getFileNameFromRecord(record), [record]);
const element = useMemo(() => {
let _element = document.createElement('div');
_element.setAttribute('tabindex', '-1');
return _element;
}, []);
const onSave = useCallback(() => {
const newContent = slateToMdString(deserializeHtml(value));
// eslint-disable-next-line no-irregular-whitespace
// '': Special line breaks
// https://symbl.cc/en/200B/
if (newContent.replaceAll('\n', '').replaceAll('\r', '').replaceAll('', '').trim()) {
saveToDescription(newContent);
}
onToggle();
}, [value, saveToDescription, onToggle]);
const onHotKey = useCallback((event) => {
if (isHotkey('esc', event)) {
event.stopPropagation();
event.preventDefault();
onToggle();
return false;
}
}, [onToggle]);
const updateStyle = useCallback(() => {
const { bottom, top, left } = getTarget(target).getBoundingClientRect();
const innerHeight = window.innerHeight;
const innerWidth = window.innerWidth;
const gap = 5; // gap between target and popover
const marginTB = 28; // margin top/bottom between screen and popover
const totalGap = gap + marginTB;
const popoverWidth = 500;
// 119: popover header height(48) + popover footer height(71)
const minPopoverBodyHeight = 160;
const x = left + popoverWidth > innerWidth ? 16 : innerWidth - left - popoverWidth;
let y = bottom + gap;
let popoverHeight = 0;
if (isLoading || errorMessage || !value) {
popoverHeight = minPopoverBodyHeight + 119;
} else {
const childNode = bodyRef.current.firstChild;
popoverHeight = Math.max(childNode.scrollHeight, minPopoverBodyHeight) + 119;
}
if (top > innerHeight - bottom) {
if (popoverHeight + totalGap > top) {
y = 28;
popoverHeight = top - totalGap;
} else {
y = top - popoverHeight - gap;
}
} else {
y = bottom + gap;
if (popoverHeight + totalGap > innerHeight - bottom) {
popoverHeight = innerHeight - bottom - totalGap;
}
}
setStyle({
transform: `translate3d(-${x}px, ${y}px, 0px)`,
height: popoverHeight,
inset: '0 0 auto auto'
});
}, [isLoading, errorMessage, value, target]);
useEffect(() => {
document.body.appendChild(element);
setMount(true);
return () => {
document.body.removeChild(element);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
document.addEventListener('keydown', onHotKey, true);
window.addEventListener('resize', updateStyle);
return () => {
document.removeEventListener('keydown', onHotKey);
window.removeEventListener('resize', updateStyle);
};
}, [onHotKey, updateStyle]);
useLayoutEffect(() => {
updateStyle();
}, [updateStyle]);
useEffect(() => {
if (!isMount) return;
updateStyle();
const path = Utils.joinPath(parentDir, fileName);
metadataAPI.ocr(repoID, path).then(res => {
const result = (res.data?.ocr_result || '').replaceAll('\f', '\n');
const value = result.replaceAll('\n', '').trim() ? result : '';
setValue(value);
setLoading(false);
}).catch(error => {
const errorMessage = gettext('Failed to extract text');
setErrorMessage(errorMessage);
setLoading(false);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isMount]);
return (
<BodyPortal node={element}>
<div className="sf-metadata-ocr-result-popover show">
<ClickOutside onClickOutside={onToggle}>
<div className="popover show bs-popover-auto position-absolute" style={style}>
<div className="popover-inner" ref={popoverRef}>
<div className="sf-metadata-ocr-result-popover-header">
<h5>{gettext('OCR result')}</h5>
</div>
<div className="sf-metadata-ocr-result-popover-body" ref={bodyRef}>
{isLoading && (
<div className="sf-metadata-ocr-file-loading-container">
<Loading />
<span className="sf-metadata-ocr-file-loading-tip-text">{gettext('Extracting text, please wait...')}</span>
</div>
)}
{!isLoading && errorMessage && (
<div className="h-100 w-100 d-flex align-items-center justify-content-center p-4 error">{errorMessage}</div>
)}
{!isLoading && !errorMessage && (
<div className={classnames('sf-metadata-ocr-result-display-container', { 'empty-tip': !value })}>
{value || gettext('No text extracted')}
</div>
)}
</div>
<div className="sf-metadata-ocr-result-popover-footer">
<Button color="secondary" className="mr-2" onClick={onToggle}>{gettext('Cancel')}</Button>
<Button color="primary" disabled={!canModify || isLoading || Boolean(errorMessage) || !value} onClick={onSave}>{gettext('Save to description field')}</Button>
</div>
</div>
</div>
</ClickOutside>
</div>
</BodyPortal>
);
};
OCRResultPopover.propTypes = {
repoID: PropTypes.string,
record: PropTypes.object,
target: PropTypes.oneOfType([PropTypes.string, PropTypes.node, PropTypes.object]),
onToggle: PropTypes.func,
saveToDescription: PropTypes.func,
};
export default OCRResultPopover;

View File

@@ -381,7 +381,7 @@ export const MetadataViewProvider = ({
}); });
}, [modifyRecords, generateDescription]); }, [modifyRecords, generateDescription]);
const onOCR = useCallback((record) => { const onOCR = useCallback((record, target) => {
const parentDir = getParentDirFromRecord(record); const parentDir = getParentDirFromRecord(record);
const fileName = getFileNameFromRecord(record); const fileName = getFileNameFromRecord(record);
if (!fileName || !parentDir) return; if (!fileName || !parentDir) return;
@@ -399,7 +399,7 @@ export const MetadataViewProvider = ({
idOriginalRecordUpdates[updateRecordId] = { [descriptionColumnKey]: description || null }; idOriginalRecordUpdates[updateRecordId] = { [descriptionColumnKey]: description || null };
modifyRecords(recordIds, idRecordUpdates, idOriginalRecordUpdates, idOldRecordData, idOriginalOldRecordData); modifyRecords(recordIds, idRecordUpdates, idOriginalRecordUpdates, idOldRecordData, idOriginalOldRecordData);
} }
}); }, target);
}, [modifyRecords, OCRAPI]); }, [modifyRecords, OCRAPI]);
const generateFileTags = useCallback((record) => { const generateFileTags = useCallback((record) => {

View File

@@ -283,7 +283,7 @@ const ContextMenu = ({
case OPERATION.OCR: { case OPERATION.OCR: {
const { record } = option; const { record } = option;
if (!record) break; if (!record) break;
onOCR(record); onOCR(record, 'sf-table-rdg-selected');
break; break;
} }
case OPERATION.DELETE_RECORD: { case OPERATION.DELETE_RECORD: {

View File

@@ -17,3 +17,71 @@ export const removeClassName = (originClassName, targetClassName) => {
originClassNames.splice(targetClassNameIndex, 1); originClassNames.splice(targetClassNameIndex, 1);
return originClassNames.join(' '); return originClassNames.join(' ');
}; };
const isReactRefObj = (target) => {
if (target && typeof target === 'object') return 'current' in target;
return false;
};
const isObject = (value) => {
const type = typeof value;
return value != null && (type === 'object' || type === 'function');
};
const getTag = (value) => {
if (value == null) return value === undefined ? '[object Undefined]' : '[object Null]';
return Object.prototype.toString.call(value);
};
const isFunction = (value) => {
if (!isObject(value)) return false;
const tag = getTag(value);
return (
tag === '[object Function]' ||
tag === '[object AsyncFunction]' ||
tag === '[object GeneratorFunction]' ||
tag === '[object Proxy]'
);
};
export const canUseDOM = !!(
typeof window !== 'undefined' &&
window.document &&
window.document.createElement
);
export const findDOMElements = (target) => {
if (isReactRefObj(target)) return target.current;
if (isFunction(target)) return target();
if (typeof target === 'string' && canUseDOM) {
let selection = document.querySelectorAll(target);
if (!selection.length) {
selection = document.querySelectorAll(`#${target}`);
}
if (!selection.length) {
throw new Error(
`The target '${target}' could not be identified in the dom, tip: check spelling`,
);
}
return selection;
}
return target;
};
const isArrayOrNodeList = (els) => {
if (els === null) return false;
return Array.isArray(els) || (canUseDOM && typeof els.length === 'number');
};
export const getTarget = (target, allElements) => {
const els = findDOMElements(target);
if (allElements) {
if (isArrayOrNodeList(els)) return els;
if (els === null) return [];
return [els];
}
if (isArrayOrNodeList(els)) return els[0];
return els;
};