diff --git a/frontend/src/components/cur-dir-path/dir-tool.js b/frontend/src/components/cur-dir-path/dir-tool.js
index 9c32a916772..df7240f0d55 100644
--- a/frontend/src/components/cur-dir-path/dir-tool.js
+++ b/frontend/src/components/cur-dir-path/dir-tool.js
@@ -9,6 +9,7 @@ import ListTagPopover from '../popover/list-tag-popover';
import ViewModes from '../../components/view-modes';
import SortMenu from '../../components/sort-menu';
import MetadataViewToolBar from '../../metadata/components/view-toolbar';
+import TagsTableSearcher from '../../tag/views/all-tags/tags-table/tags-searcher';
import { PRIVATE_FILE_TYPE } from '../../constants';
const propTypes = {
@@ -116,6 +117,7 @@ class DirTool extends React.Component {
if (isTagView) {
return (
+
);
}
diff --git a/frontend/src/components/sf-table/index.css b/frontend/src/components/sf-table/index.css
index 2e442725e7b..83981c91b14 100644
--- a/frontend/src/components/sf-table/index.css
+++ b/frontend/src/components/sf-table/index.css
@@ -1,3 +1,97 @@
+.sf-table-searcher-container {
+ margin-left: 15px;
+}
+
+.sf-table-searcher-btn {
+ display: inline-flex;
+ align-items: center;
+ border-radius: 3px;
+ height: 22px;
+ padding: 0 .5rem;
+ transition: all .1s ease-in;
+}
+
+.sf-table-searcher-btn:hover {
+ background-color: #efefef;
+ cursor: pointer;
+}
+
+.sf-table-searcher-btn .sf3-font-search {
+ display: inline-block;
+ color: #666;
+}
+
+.sf-table-searcher-input-wrapper {
+ position: relative;
+}
+
+.sf-table-searcher-input {
+ padding-left: 30px;
+ padding-right: 60px;
+ height: 30px;
+ width: 260px;
+}
+
+.sf-table-searcher-input-wrapper .btn-close-searcher-wrapper {
+ pointer-events: all;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 20px;
+ min-width: 20px;
+ height: 20px;
+ right: 4px;
+ top: 50%;
+ transform: translateY(-50%);
+}
+
+.sf-table-searcher-input-wrapper .btn-close-searcher {
+ font-size: 14px;
+}
+
+.sf-table-searcher-input-wrapper .btn-close-searcher-wrapper:hover {
+ background-color: #efefef;
+ cursor: pointer;
+}
+
+.sf-table-searcher-input-wrapper .input-icon-addon.search-poll-button {
+ display: flex;
+ font-size: 12px;
+ height: 30px;
+ left: auto;
+ line-height: 30px;
+ min-width: 35px;
+ pointer-events: all;
+ right: 28px;
+ text-align: center;
+}
+
+.sf-table-searcher-input-wrapper .search-upward,
+.sf-table-searcher-input-wrapper .search-backward {
+ color: #666;
+ font-size: 12px;
+ background-color: #efefef;
+ display: inline-block;
+ height: 20px;
+ line-height: 20px;
+ width: 20px;
+}
+
+.sf-table-searcher-input-wrapper .search-upward:hover,
+.sf-table-searcher-input-wrapper .search-backward:hover {
+ cursor: pointer;
+ background-color: #DBDBDB;
+}
+
+.sf-table-searcher-input-wrapper .search-upward {
+ margin-left: 8px;
+ border-radius: 2px 0 0 2px;
+}
+
+.sf-table-searcher-input-wrapper .search-backward {
+ border-radius: 0 2px 2px 0;
+}
+
.sf-table-wrapper {
height: 100%;
width: 100%;
diff --git a/frontend/src/components/sf-table/searcher/index.js b/frontend/src/components/sf-table/searcher/index.js
new file mode 100644
index 00000000000..0b3443e0757
--- /dev/null
+++ b/frontend/src/components/sf-table/searcher/index.js
@@ -0,0 +1,100 @@
+import React, { useMemo, useState } from 'react';
+import { gettext } from '../../../utils/constants';
+import { KeyCodes } from '../../../constants';
+import { isModG, isModShiftG } from '../../../metadata/utils/hotkey';
+import SFTableSearcherInput from './searcher-input';
+import { checkHasSearchResult } from '../utils/search';
+
+const SFTableSearcher = ({ recordsCount, columnsCount, searchResult, searchCells, closeSearcher, focusNextMatchedCell, focusPreviousMatchedCell }) => {
+ const [isSearchActive, setIsSearchActive] = useState(false);
+ const [hasSearchValue, setHasSearchValue] = useState(false);
+
+ const hasSearchResult = useMemo(() => {
+ return checkHasSearchResult(searchResult);
+ }, [searchResult]);
+
+ const onToggleSearch = () => {
+ setIsSearchActive(!isSearchActive);
+ };
+
+ const handleCloseSearcher = () => {
+ setIsSearchActive(false);
+ closeSearcher && closeSearcher();
+ };
+
+ const onKeyDown = (e) => {
+ const isEmptySearchResult = !hasSearchResult;
+ if (e.keyCode === KeyCodes.Escape) {
+ e.preventDefault();
+ handleCloseSearcher();
+ } else if (isModG(e)) {
+ e.preventDefault();
+ if (isEmptySearchResult) return;
+ focusNextMatchedCell && focusNextMatchedCell();
+ } else if (isModShiftG(e)) {
+ e.preventDefault();
+ if (isEmptySearchResult) return;
+ focusPreviousMatchedCell && focusPreviousMatchedCell();
+ }
+ };
+
+ const renderSearchPollButton = () => {
+ return (
+
+ {hasSearchValue &&
+
+ {hasSearchResult ?
+ (searchResult.currentSelectIndex + 1 + ' of ' + searchResult.matchedCells.length) : '0 of 0'
+ }
+
+ }
+ {hasSearchResult &&
+ <>
+ {}}>
+
+ {}}>
+
+ >
+ }
+
+ );
+ };
+
+ return (
+
+ {!isSearchActive && (
+
+
+
+ )}
+ {isSearchActive && (
+
+
+
+ {renderSearchPollButton()}
+
+
+
+
+ )}
+
+ );
+};
+
+export default SFTableSearcher;
diff --git a/frontend/src/components/sf-table/searcher/searcher-input.js b/frontend/src/components/sf-table/searcher/searcher-input.js
new file mode 100644
index 00000000000..f48bcf2e1a3
--- /dev/null
+++ b/frontend/src/components/sf-table/searcher/searcher-input.js
@@ -0,0 +1,59 @@
+import React, { useRef, useState } from 'react';
+import { gettext } from '../../../utils/constants';
+
+const SFTableSearcherInput = ({ recordsCount, columnsCount, setHasSearchValue, searchCells, onKeyDown }) => {
+ const [searchValue, setSearchValue] = useState('');
+
+ const isInputtingChinese = useRef(false);
+ const inputTimer = useRef(null);
+
+ const getSearchDelayTime = () => {
+ const viewCellsCount = (recordsCount || 0) * (columnsCount || 0);
+ let delayTime = viewCellsCount * 0.1;
+ delayTime = delayTime > 500 ? 500 : Math.floor(delayTime);
+ if (delayTime < 100) {
+ delayTime = 100;
+ }
+ return delayTime;
+ };
+
+ const onChangeSearchValue = (e) => {
+ inputTimer.current && clearTimeout(inputTimer.current);
+ const text = e.target.value;
+ const wait = getSearchDelayTime();
+ const currSearchValue = text || '';
+ const trimmedSearchValue = currSearchValue.trim();
+ setSearchValue(currSearchValue);
+ setHasSearchValue(!!trimmedSearchValue);
+ if (!isInputtingChinese.current) {
+ inputTimer.current = setTimeout(() => {
+ searchCells && searchCells(trimmedSearchValue);
+ }, wait);
+ }
+ };
+
+ const onCompositionStart = () => {
+ isInputtingChinese.current = true;
+ };
+
+ const onCompositionEnd = (e) => {
+ isInputtingChinese.current = false;
+ onChangeSearchValue(e);
+ };
+
+ return (
+
+ );
+};
+
+export default SFTableSearcherInput;
diff --git a/frontend/src/components/sf-table/table-main/records/record/index.js b/frontend/src/components/sf-table/table-main/records/record/index.js
index 9cc8eea26a8..626aa9c3bf4 100644
--- a/frontend/src/components/sf-table/table-main/records/record/index.js
+++ b/frontend/src/components/sf-table/table-main/records/record/index.js
@@ -82,15 +82,15 @@ class Record extends React.Component {
getFrozenCells = () => {
const {
columns, sequenceColumnWidth, lastFrozenColumnKey, groupRecordIndex, index: recordIndex, record,
- cellMetaData, isGroupView, height, columnColor
+ cellMetaData, isGroupView, height, columnColor, treeNodeKey,
} = this.props;
const frozenColumns = getFrozenColumns(columns);
if (frozenColumns.length === 0) return null;
const recordId = record._id;
return frozenColumns.map((column, index) => {
const { key } = column;
- const isCellHighlight = this.checkIsCellHighlight(key, recordId);
- const isCurrentCellHighlight = this.checkIsCurrentCellHighlight(key, recordId);
+ const isCellHighlight = this.checkIsCellHighlight(key, recordId, treeNodeKey);
+ const isCurrentCellHighlight = this.checkIsCurrentCellHighlight(key, recordId, treeNodeKey);
const highlightClassName = isCurrentCellHighlight ? 'cell-current-highlight' : isCellHighlight ? 'cell-highlight' : null;
const isCellSelected = this.checkIsCellSelected(index);
const isLastCell = this.checkIsLastCell(columns, key);
@@ -126,10 +126,10 @@ class Record extends React.Component {
});
};
- checkIsCellHighlight = (columnKey, rowId) => {
+ checkIsCellHighlight = (columnKey, rowId, treeNodeKey) => {
const { searchResult } = this.props;
if (searchResult) {
- const matchedColumns = searchResult.matchedRows[rowId];
+ const matchedColumns = this.props.showRecordAsTree ? searchResult.matchedRows[treeNodeKey] : searchResult.matchedRows[rowId];
if (matchedColumns && matchedColumns.includes(columnKey)) {
return true;
}
@@ -137,14 +137,15 @@ class Record extends React.Component {
return false;
};
- checkIsCurrentCellHighlight = (columnKey, rowId) => {
+ checkIsCurrentCellHighlight = (columnKey, rowId, treeNodeKey) => {
const { searchResult } = this.props;
if (searchResult) {
const { currentSelectIndex } = searchResult;
if (typeof(currentSelectIndex) !== 'number') return false;
const currentSelectCell = searchResult.matchedCells[currentSelectIndex];
if (!currentSelectCell) return false;
- if (currentSelectCell.row === rowId && currentSelectCell.column === columnKey) return true;
+ const isCurrentRow = this.props.showRecordAsTree ? currentSelectCell.nodeKey === treeNodeKey : currentSelectCell.row === rowId;
+ return isCurrentRow && currentSelectCell.column === columnKey;
}
return false;
};
@@ -152,7 +153,7 @@ class Record extends React.Component {
getColumnCells = () => {
const {
columns, sequenceColumnWidth, colOverScanStartIdx, colOverScanEndIdx, groupRecordIndex, index: recordIndex,
- record, cellMetaData, isGroupView, height, columnColor,
+ record, cellMetaData, isGroupView, height, columnColor, treeNodeKey,
} = this.props;
const recordId = record._id;
const rendererColumns = columns.slice(colOverScanStartIdx, colOverScanEndIdx);
@@ -160,8 +161,8 @@ class Record extends React.Component {
const { key, frozen } = column;
const needBindEvents = !frozen;
const isCellSelected = this.checkIsCellSelected(columns.findIndex(col => col.key === column.key));
- const isCellHighlight = this.checkIsCellHighlight(key, recordId);
- const isCurrentCellHighlight = this.checkIsCurrentCellHighlight(key, recordId);
+ const isCellHighlight = this.checkIsCellHighlight(key, recordId, treeNodeKey);
+ const isCurrentCellHighlight = this.checkIsCurrentCellHighlight(key, recordId, treeNodeKey);
const highlightClassName = isCurrentCellHighlight ? 'cell-current-highlight' : isCellHighlight ? 'cell-highlight' : null;
const isLastCell = this.checkIsLastCell(columns, key);
const bgColor = columnColor && columnColor[key];
diff --git a/frontend/src/components/sf-table/table-main/records/tree-body.js b/frontend/src/components/sf-table/table-main/records/tree-body.js
index 19e0e7be749..f6d809e238c 100644
--- a/frontend/src/components/sf-table/table-main/records/tree-body.js
+++ b/frontend/src/components/sf-table/table-main/records/tree-body.js
@@ -13,6 +13,7 @@ import { checkEditableViaClickCell, checkIsColumnSupportDirectEdit, getColumnByI
import { checkIsCellSupportOpenEditor } from '../../utils/selected-cell-utils';
import { LOCAL_KEY_TREE_NODE_FOLDED } from '../../constants/tree';
import { TreeMetrics } from '../../utils/tree-metrics';
+import { checkHasSearchResult } from '../../utils/search';
const ROW_HEIGHT = 33;
const RENDER_MORE_NUMBER = 10;
@@ -36,6 +37,7 @@ class TreeBody extends Component {
startRenderIndex: 0,
endRenderIndex: this.getInitEndIndex(nodes),
keyNodeFoldedMap: validKeyTreeNodeFoldedMap,
+ keyNodeFoldedMapForSearch: {},
selectedPosition: null,
isScrollingRightScrollbar: false,
};
@@ -58,12 +60,19 @@ class TreeBody extends Component {
}
UNSAFE_componentWillReceiveProps(nextProps) {
- const { recordsCount, recordIds, treeNodesCount, recordsTree } = nextProps;
+ const { recordsCount, recordIds, treeNodesCount, recordsTree, searchResult } = nextProps;
+ const searchResultChanged = searchResult !== this.props.searchResult;
if (
recordsCount !== this.props.recordsCount || recordIds !== this.props.recordIds ||
- treeNodesCount !== this.props.treeNodesCount || recordsTree !== this.props.recordsTree
+ treeNodesCount !== this.props.treeNodesCount || recordsTree !== this.props.recordsTree ||
+ searchResultChanged
) {
- this.recalculateRenderIndex(recordsTree);
+ const hasSearchResult = checkHasSearchResult(searchResult);
+ const keyNodeFoldedMap = hasSearchResult ? {} : this.state.keyNodeFoldedMap;
+ this.recalculateRenderIndex(recordsTree, keyNodeFoldedMap);
+ if (searchResultChanged) {
+ this.setState({ keyNodeFoldedMapForSearch: {} });
+ }
}
}
@@ -119,8 +128,8 @@ class TreeBody extends Component {
return Math.min(Math.ceil((contentScrollTop + height) / ROW_HEIGHT) + RENDER_MORE_NUMBER, nodes.length);
};
- recalculateRenderIndex = (recordsTree) => {
- const { startRenderIndex, endRenderIndex, keyNodeFoldedMap } = this.state;
+ recalculateRenderIndex = (recordsTree, keyNodeFoldedMap) => {
+ const { startRenderIndex, endRenderIndex } = this.state;
const nodes = this.getShownNodes(recordsTree, keyNodeFoldedMap);
const contentScrollTop = this.resultContentRef.scrollTop;
const start = Math.max(0, Math.floor(contentScrollTop / ROW_HEIGHT) - RENDER_MORE_NUMBER);
@@ -339,6 +348,14 @@ class TreeBody extends Component {
this.columnVisibleEnd = columnVisibleEnd;
};
+ jumpToRow = (scrollToRowIndex) => {
+ const { treeNodesCount } = this.props;
+ const rowHeight = this.getRowHeight();
+ const height = this.resultContentRef.offsetHeight;
+ const scrollTop = Math.min(scrollToRowIndex * rowHeight, treeNodesCount * rowHeight - height);
+ this.setScrollTop(scrollTop);
+ };
+
scrollToColumn = (idx) => {
const { columns, getTableContentRect } = this.props;
const { width: tableContentWidth } = getTableContentRect();
@@ -496,13 +513,25 @@ class TreeBody extends Component {
};
toggleExpandNode = (nodeKey) => {
- const { recordsTree } = this.props;
- const { keyNodeFoldedMap, endRenderIndex } = this.state;
+ const { recordsTree, searchResult } = this.props;
+ const { keyNodeFoldedMap, keyNodeFoldedMapForSearch, endRenderIndex } = this.state;
+ const hasSearchResult = checkHasSearchResult(searchResult);
let updatedKeyNodeFoldedMap = { ...keyNodeFoldedMap };
- if (updatedKeyNodeFoldedMap[nodeKey]) {
- delete updatedKeyNodeFoldedMap[nodeKey];
+ let updatedKeyNodeFoldedMapForSearch = { ...keyNodeFoldedMapForSearch };
+ if (hasSearchResult) {
+ if (updatedKeyNodeFoldedMapForSearch[nodeKey]) {
+ delete updatedKeyNodeFoldedMapForSearch[nodeKey];
+ delete updatedKeyNodeFoldedMap[nodeKey];
+ } else {
+ updatedKeyNodeFoldedMapForSearch[nodeKey] = true;
+ updatedKeyNodeFoldedMap[nodeKey] = true;
+ }
} else {
- updatedKeyNodeFoldedMap[nodeKey] = true;
+ if (updatedKeyNodeFoldedMap[nodeKey]) {
+ delete updatedKeyNodeFoldedMap[nodeKey];
+ } else {
+ updatedKeyNodeFoldedMap[nodeKey] = true;
+ }
}
if (this.props.storeFoldedTreeNodes) {
@@ -510,8 +539,15 @@ class TreeBody extends Component {
this.props.storeFoldedTreeNodes(LOCAL_KEY_TREE_NODE_FOLDED, updatedKeyNodeFoldedMap);
}
- const updatedNodes = this.getShownNodes(recordsTree, updatedKeyNodeFoldedMap);
- let updates = { nodes: updatedNodes, keyNodeFoldedMap: updatedKeyNodeFoldedMap };
+ const updatedNodes = this.getShownNodes(recordsTree, hasSearchResult ? updatedKeyNodeFoldedMapForSearch : updatedKeyNodeFoldedMap);
+ let updates = {
+ nodes: updatedNodes,
+ keyNodeFoldedMap: updatedKeyNodeFoldedMap,
+ };
+ if (hasSearchResult) {
+ updates.keyNodeFoldedMapForSearch = updatedKeyNodeFoldedMapForSearch;
+ }
+
const end = this.recalculateRenderEndIndex(updatedNodes);
if (end !== endRenderIndex) {
updates.endRenderIndex = end;
@@ -525,8 +561,8 @@ class TreeBody extends Component {
};
renderRecords = () => {
- const { treeMetrics, showCellColoring, columnColors } = this.props;
- const { nodes, keyNodeFoldedMap, startRenderIndex, endRenderIndex, selectedPosition } = this.state;
+ const { treeMetrics, showCellColoring, columnColors, searchResult } = this.props;
+ const { nodes, keyNodeFoldedMap, keyNodeFoldedMapForSearch, startRenderIndex, endRenderIndex, selectedPosition } = this.state;
this.initFrozenNodesRef();
const visibleNodes = this.getVisibleNodesInRange();
const nodesCount = nodes.length;
@@ -534,6 +570,7 @@ class TreeBody extends Component {
const scrollLeft = this.props.getScrollLeft();
const rowHeight = this.getRowHeight();
const cellMetaData = this.getCellMetaData();
+ const hasSearchResult = checkHasSearchResult(searchResult);
let shownNodes = visibleNodes.map((node, index) => {
const { _id: recordId, node_key, node_depth, node_index } = node;
const hasChildNodes = checkTreeNodeHasChildNodes(node);
@@ -543,7 +580,7 @@ class TreeBody extends Component {
const isLastRecord = lastRecordIndex === recordIndex;
const hasSelectedCell = this.props.hasSelectedCell({ recordIndex }, selectedPosition);
const columnColor = showCellColoring ? columnColors[recordId] : {};
- const isFoldedNode = !!keyNodeFoldedMap[node_key];
+ const isFoldedNode = hasSearchResult ? !!keyNodeFoldedMapForSearch[node_key] : !!keyNodeFoldedMap[node_key];
return (
{
+ if (typeof value !== 'string') return '';
+ return value.replace(/[.\\[\]{}()|^$?*+]/g, '\\$&');
+};
+
+export const getSearchRule = (value) => {
+ if (typeof value !== 'string') {
+ return false;
+ }
+ let searchRule = value;
+ searchRule = searchRule.trim();
+ if (searchRule.length === 0) {
+ return false;
+ }
+ // i: search value uppercase and lowercase are not sensitive
+ return new RegExp(escapeRegExp(searchRule), 'i');
+};
+
+export const checkHasSearchResult = (searchResult) => {
+ const { matchedCells } = searchResult || {};
+ return Array.isArray(matchedCells) ? matchedCells.length > 0 : false;
+};
diff --git a/frontend/src/metadata/constants/event-bus-type.js b/frontend/src/metadata/constants/event-bus-type.js
index 43d479dea46..3916e7fa678 100644
--- a/frontend/src/metadata/constants/event-bus-type.js
+++ b/frontend/src/metadata/constants/event-bus-type.js
@@ -41,6 +41,7 @@ export const EVENT_BUS_TYPE = {
// metadata
RELOAD_DATA: 'reload_data',
+ UPDATE_SEARCH_RESULT: 'update_search_result',
// view
MODIFY_FILTERS: 'modify_filters',
diff --git a/frontend/src/tag/constants/column/private.js b/frontend/src/tag/constants/column/private.js
index 2b1190739a7..ad5245557e2 100644
--- a/frontend/src/tag/constants/column/private.js
+++ b/frontend/src/tag/constants/column/private.js
@@ -31,3 +31,7 @@ export const EDITABLE_PRIVATE_COLUMN_KEYS = [
PRIVATE_COLUMN_KEY.TAG_COLOR,
PRIVATE_COLUMN_KEY.TAG_FILE_LINKS,
];
+
+export const VISIBLE_COLUMNS_KEYS = [
+ PRIVATE_COLUMN_KEY.TAG_NAME, PRIVATE_COLUMN_KEY.PARENT_LINKS, PRIVATE_COLUMN_KEY.SUB_LINKS, PRIVATE_COLUMN_KEY.TAG_FILE_LINKS,
+];
diff --git a/frontend/src/tag/views/all-tags/tags-table/index.css b/frontend/src/tag/views/all-tags/tags-table/index.css
index 5af683cd09f..e1647ae5a84 100644
--- a/frontend/src/tag/views/all-tags/tags-table/index.css
+++ b/frontend/src/tag/views/all-tags/tags-table/index.css
@@ -30,6 +30,11 @@
}
+.sf-metadata-all-tags-wrapper .sf-table-row .name-cell.cell-selected {
+ cursor: default;
+}
+
.sf-table-cell.cell-selected .sf-table-tag-name-formatter .sf-table-tag-name:hover {
text-decoration: underline;
+ cursor: pointer;
}
diff --git a/frontend/src/tag/views/all-tags/tags-table/index.js b/frontend/src/tag/views/all-tags/tags-table/index.js
index 0ff5116a0c6..455ac836220 100644
--- a/frontend/src/tag/views/all-tags/tags-table/index.js
+++ b/frontend/src/tag/views/all-tags/tags-table/index.js
@@ -1,4 +1,4 @@
-import React, { useCallback, useMemo, useRef, useState } from 'react';
+import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import PropTypes from 'prop-types';
import SFTable from '../../../../components/sf-table';
import EditTagDialog from '../../../components/dialog/edit-tag-dialog';
@@ -6,11 +6,13 @@ import MergeTagsSelector from '../../../components/merge-tags-selector';
import { createTableColumns } from './columns-factory';
import { createContextMenuOptions } from './context-menu-options';
import { gettext } from '../../../../utils/constants';
-import { PRIVATE_COLUMN_KEY } from '../../../constants';
+import { PRIVATE_COLUMN_KEY, VISIBLE_COLUMNS_KEYS } from '../../../constants';
import { useTags } from '../../../hooks';
import EventBus from '../../../../components/common/event-bus';
-import { EVENT_BUS_TYPE } from '../../../../components/sf-table/constants/event-bus-type';
+import { EVENT_BUS_TYPE } from '../../../../metadata/constants';
+import { EVENT_BUS_TYPE as TABLE_EVENT_BUS_TYPE } from '../../../../components/sf-table/constants/event-bus-type';
import { LOCAL_KEY_TREE_NODE_FOLDED } from '../../../../components/sf-table/constants/tree';
+import { isNumber } from '../../../../utils/number';
import './index.css';
@@ -24,10 +26,6 @@ const DEFAULT_TABLE_DATA = {
const KEY_STORE_SCROLL = 'table_scroll';
-const VISIBLE_COLUMNS_KEYS = [
- PRIVATE_COLUMN_KEY.TAG_NAME, PRIVATE_COLUMN_KEY.PARENT_LINKS, PRIVATE_COLUMN_KEY.SUB_LINKS, PRIVATE_COLUMN_KEY.TAG_FILE_LINKS,
-];
-
const TagsTable = ({
context,
isLoadingMoreRecords,
@@ -40,6 +38,7 @@ const TagsTable = ({
const [isShowNewSubTagDialog, setIsShowNewSubTagDialog] = useState(false);
const [isShowMergeTagsSelector, setIsShowMergeTagsSelector] = useState(false);
+ const [searchResult, setSearchResult] = useState(null);
const parentTagIdRef = useRef(null);
const mergeTagsSelectorProps = useRef({});
@@ -108,7 +107,7 @@ const TagsTable = ({
deleteTags(tagsIds);
const eventBus = EventBus.getInstance();
- eventBus.dispatch(EVENT_BUS_TYPE.SELECT_NONE);
+ eventBus.dispatch(TABLE_EVENT_BUS_TYPE.SELECT_NONE);
}, [deleteTags]);
const onNewSubTag = useCallback((parentTagId) => {
@@ -179,6 +178,44 @@ const TagsTable = ({
return false;
}, []);
+ const scrollToCurrentSelectedCell = useCallback((searchResult, currentSelectIndex) => {
+ if (!window.sfTableBody) {
+ return;
+ }
+ const cell = searchResult.matchedCells[currentSelectIndex];
+ if (!cell) {
+ return;
+ }
+ const { column: cellColumn, rowIndex: focusRowIndex } = cell;
+ const { rowVisibleStart, rowVisibleEnd, columnVisibleStart, columnVisibleEnd } = window.sfTableBody;
+ if (focusRowIndex < rowVisibleStart) {
+ window.sfTableBody.jumpToRow(focusRowIndex - 1);
+ } else if (focusRowIndex >= rowVisibleEnd) {
+ window.sfTableBody.jumpToRow(focusRowIndex);
+ }
+
+ const focusColumnIndex = visibleColumns.findIndex((column) => column.key === cellColumn);
+ if (columnVisibleStart >= focusColumnIndex || focusColumnIndex > columnVisibleEnd) {
+ window.sfTableBody.scrollToColumn(focusColumnIndex);
+ }
+ }, [visibleColumns]);
+
+ const updateSearchResult = useCallback((searchResult) => {
+ setSearchResult(searchResult);
+ const { currentSelectIndex } = searchResult || {};
+ if (searchResult && isNumber(currentSelectIndex)) {
+ scrollToCurrentSelectedCell(searchResult, currentSelectIndex);
+ }
+ }, [scrollToCurrentSelectedCell]);
+
+ useEffect(() => {
+ const eventBus = EventBus.getInstance();
+ const unsubscribeUpdateSearchResult = eventBus.subscribe(EVENT_BUS_TYPE.UPDATE_SEARCH_RESULT, updateSearchResult);
+ return () => {
+ unsubscribeUpdateSearchResult();
+ };
+ }, [updateSearchResult]);
+
return (
<>
{
+ const { tagsData } = useTags();
+ const [searchResult, setSearchResult] = useState(null);
+
+ const recordsTree = useMemo(() => {
+ return tagsData.rows_tree || [];
+ }, [tagsData]);
+
+ const recordsCount = useMemo(() => {
+ return recordsTree.length;
+ }, [recordsTree]);
+
+ const getTagStrContentByNode = useCallback((tagNode) => {
+ const tagId = getTreeNodeId(tagNode);
+ const tag = getRowById(tagsData, tagId);
+ if (!tag) return null;
+ let strContent = {};
+ VISIBLE_COLUMNS_KEYS.forEach((columnKey) => {
+ switch (columnKey) {
+ case PRIVATE_COLUMN_KEY.TAG_NAME: {
+ strContent[columnKey] = tag[columnKey] || '';
+ break;
+ }
+ default: {
+ break;
+ }
+ }
+ });
+ return strContent;
+ }, [tagsData]);
+
+ const checkIsCellValueMatchedRegVal = useCallback((tagContent, columnKey, regVal) => {
+ const cellValueDisplayString = (tagContent && tagContent[columnKey]) || '';
+ const isMatched = regVal.test(cellValueDisplayString);
+ return isMatched;
+ }, []);
+
+ const handleUpdateSearchResult = useCallback((searchResult) => {
+ setSearchResult(searchResult);
+
+ const eventBus = EventBus.getInstance();
+ eventBus.dispatch(EVENT_BUS_TYPE.UPDATE_SEARCH_RESULT, searchResult);
+ }, []);
+
+ const searchCells = useCallback((searchRegRule) => {
+ let searchResult = {};
+ let matchedRows = {};
+ searchResult.matchedCells = [];
+ searchResult.matchedRows = matchedRows;
+ recordsTree.forEach((tagNode, nodeIndex) => {
+ const nodeKey = getTreeNodeKey(tagNode);
+ const tagStrContent = getTagStrContentByNode(tagNode);
+ SUPPORT_SEARCH_COLUMNS_KEYS.forEach((columnKey) => {
+ const isMatched = checkIsCellValueMatchedRegVal(tagStrContent, columnKey, searchRegRule);
+ if (isMatched) {
+ if (matchedRows[nodeKey]) {
+ matchedRows[nodeKey].push(columnKey);
+ } else {
+ matchedRows[nodeKey] = [columnKey];
+ }
+ searchResult.matchedCells.push({ nodeKey, tagStrContent, column: columnKey, rowIndex: nodeIndex });
+ }
+ });
+ });
+ searchResult.currentSelectIndex = 0;
+ return searchResult;
+ }, [recordsTree, getTagStrContentByNode, checkIsCellValueMatchedRegVal]);
+
+
+ const handleSearchTags = useCallback((searchVal) => {
+ if (searchVal.length === 0) {
+ handleUpdateSearchResult(null);
+ } else {
+ const searchRegRule = getSearchRule(searchVal);
+ const searchResult = searchRegRule ? searchCells(searchRegRule) : null;
+ handleUpdateSearchResult(searchResult);
+ }
+ }, [searchCells, handleUpdateSearchResult]);
+
+ const focusNextMatchedCell = useCallback(() => {
+ if (!searchResult) return;
+ const matchedCellsLength = searchResult.matchedCells.length;
+ let currentSelectIndex = searchResult.currentSelectIndex;
+ currentSelectIndex++;
+ if (currentSelectIndex === matchedCellsLength) {
+ currentSelectIndex = 0;
+ }
+ const nextSearchResult = Object.assign({}, searchResult, { currentSelectIndex: currentSelectIndex });
+ handleUpdateSearchResult(nextSearchResult);
+ }, [searchResult, handleUpdateSearchResult]);
+
+ const focusPreviousMatchedCell = useCallback(() => {
+ if (!searchResult) return;
+ const matchedCellsLength = searchResult.matchedCells.length;
+ let currentSelectIndex = searchResult.currentSelectIndex;
+ currentSelectIndex--;
+ if (currentSelectIndex === -1) {
+ currentSelectIndex = matchedCellsLength - 1;
+ }
+ const nextSearchResult = Object.assign({}, searchResult, { currentSelectIndex: currentSelectIndex });
+ handleUpdateSearchResult(nextSearchResult);
+ }, [searchResult, handleUpdateSearchResult]);
+
+ const closeSearcher = useCallback(() => {
+ handleUpdateSearchResult(null);
+ }, [handleUpdateSearchResult]);
+
+ return (
+
+ );
+};
+
+export default TagsTableSearcher;