diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 0beb5c17b42..b53a19d1e2a 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -17,7 +17,7 @@ "@seafile/sdoc-editor": "1.0.8", "@seafile/seafile-calendar": "0.0.12", "@seafile/seafile-editor": "1.0.99", - "@seafile/sf-metadata-ui-component": "0.0.9", + "@seafile/sf-metadata-ui-component": "0.0.10", "@uiw/codemirror-extensions-langs": "^4.19.4", "@uiw/react-codemirror": "^4.19.4", "chart.js": "2.9.4", @@ -4953,9 +4953,9 @@ } }, "node_modules/@seafile/sf-metadata-ui-component": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/@seafile/sf-metadata-ui-component/-/sf-metadata-ui-component-0.0.9.tgz", - "integrity": "sha512-J0D3DK1TI16QPlhAeBp64ilcKO7pCX9w03Q94D1Ni7phLquqZwCD3PFFyRgv6oUkWtGojTL++SiLVTTXubI68g==", + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@seafile/sf-metadata-ui-component/-/sf-metadata-ui-component-0.0.10.tgz", + "integrity": "sha512-WP1SH6NbP4tH3ZQ1dgzFKPpYfFzhDD/uzPktxJFpBX66xnTWlBirqYl5IHkyJ5nx6vuXG9CcRc9LhNyl+r3jeg==", "dependencies": { "@seafile/seafile-calendar": "0.0.24", "classnames": "2.3.2", @@ -32160,9 +32160,9 @@ } }, "@seafile/sf-metadata-ui-component": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/@seafile/sf-metadata-ui-component/-/sf-metadata-ui-component-0.0.9.tgz", - "integrity": "sha512-J0D3DK1TI16QPlhAeBp64ilcKO7pCX9w03Q94D1Ni7phLquqZwCD3PFFyRgv6oUkWtGojTL++SiLVTTXubI68g==", + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@seafile/sf-metadata-ui-component/-/sf-metadata-ui-component-0.0.10.tgz", + "integrity": "sha512-WP1SH6NbP4tH3ZQ1dgzFKPpYfFzhDD/uzPktxJFpBX66xnTWlBirqYl5IHkyJ5nx6vuXG9CcRc9LhNyl+r3jeg==", "requires": { "@seafile/seafile-calendar": "0.0.24", "classnames": "2.3.2", diff --git a/frontend/package.json b/frontend/package.json index e60c4aa086f..59674dea924 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,7 +12,7 @@ "@seafile/sdoc-editor": "1.0.8", "@seafile/seafile-calendar": "0.0.12", "@seafile/seafile-editor": "1.0.99", - "@seafile/sf-metadata-ui-component": "0.0.9", + "@seafile/sf-metadata-ui-component": "0.0.10", "@uiw/codemirror-extensions-langs": "^4.19.4", "@uiw/react-codemirror": "^4.19.4", "chart.js": "2.9.4", diff --git a/frontend/src/metadata/metadata-view/components/cell-editor/editor-container.js b/frontend/src/metadata/metadata-view/components/cell-editor/editor-container.js new file mode 100644 index 00000000000..9434ab4a3ac --- /dev/null +++ b/frontend/src/metadata/metadata-view/components/cell-editor/editor-container.js @@ -0,0 +1,31 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Editor from './editor'; + +const EditorContainer = (props) => { + if (!props.column) return null; + return (); +}; + + +EditorContainer.propTypes = { + table: PropTypes.object, + columns: PropTypes.array, + isGroupView: PropTypes.bool, + scrollTop: PropTypes.number, + scrollLeft: PropTypes.number, + firstEditorKeyDown: PropTypes.object, + openEditorMode: PropTypes.string, + portalTarget: PropTypes.any, + editorPosition: PropTypes.object, + record: PropTypes.object, + column: PropTypes.object, + width: PropTypes.number.isRequired, + height: PropTypes.number.isRequired, + left: PropTypes.number.isRequired, + top: PropTypes.number.isRequired, + onCommit: PropTypes.func, + onCommitCancel: PropTypes.func, +}; + +export default EditorContainer; diff --git a/frontend/src/metadata/metadata-view/components/cell-editor/editor-portal.jsx b/frontend/src/metadata/metadata-view/components/cell-editor/editor-portal.jsx new file mode 100644 index 00000000000..5d4cb260b2c --- /dev/null +++ b/frontend/src/metadata/metadata-view/components/cell-editor/editor-portal.jsx @@ -0,0 +1,42 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import PropTypes from 'prop-types'; + +class EditorPortal extends React.Component { + static propTypes = { + children: PropTypes.node.isRequired, + target: PropTypes.instanceOf(Element).isRequired + }; + + // Keep track of when the modal element is added to the DOM + state = { + isMounted: false + }; + + el = document.createElement('div'); + + componentDidMount() { + this.props.target.appendChild(this.el); + // eslint-disable-next-line react/no-did-mount-set-state + this.setState({ isMounted: true }); + } + + componentWillUnmount() { + this.props.target.removeChild(this.el); + } + + render() { + // Don't render the portal until the component has mounted, + // So the portal can safely access the DOM. + if (!this.state.isMounted) { + return null; + } + + return ReactDOM.createPortal( + this.props.children, + this.el, + ); + } +} + +export default EditorPortal; diff --git a/frontend/src/metadata/metadata-view/components/cell-editor/editor.js b/frontend/src/metadata/metadata-view/components/cell-editor/editor.js new file mode 100644 index 00000000000..c2f81ca71a7 --- /dev/null +++ b/frontend/src/metadata/metadata-view/components/cell-editor/editor.js @@ -0,0 +1,22 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { CellType } from '../../_basic'; +import FileNameEditor from './file-name-editor'; + +const Editor = (props) => { + + switch (props.column.type) { + case CellType.FILE_NAME: { + return (); + } + default: { + return null; + } + } +}; + +Editor.propTypes = { + column: PropTypes.object.isRequired, +}; + +export default Editor; diff --git a/frontend/src/metadata/metadata-view/components/cell-editor/file-name-editor.js b/frontend/src/metadata/metadata-view/components/cell-editor/file-name-editor.js new file mode 100644 index 00000000000..f0fe53185e5 --- /dev/null +++ b/frontend/src/metadata/metadata-view/components/cell-editor/file-name-editor.js @@ -0,0 +1,97 @@ +import React, { useEffect, useMemo } from 'react'; +import PropTypes from 'prop-types'; +import { ModalPortal } from '@seafile/sf-metadata-ui-component'; +import { PRIVATE_COLUMN_KEY } from '../../_basic'; +import { Utils } from '../../../../utils/utils'; +import ImageDialog from '../../../../components/dialog/image-dialog'; +import { serviceURL, siteRoot, thumbnailSizeForOriginal } from '../../../../utils/constants'; + +const FileNameEditor = ({ column, record, onCommitCancel }) => { + const _isDir = useMemo(() => { + const isDirValue = record[PRIVATE_COLUMN_KEY.IS_DIR]; + if (typeof isDirValue === 'string') return isDirValue.toUpperCase() === 'TRUE'; + return isDirValue; + }, [record]); + + const fileName = useMemo(() => { + const { key } = column; + return record[key]; + }, [column, record]); + + const fileType = useMemo(() => { + if (_isDir) return 'folder'; + if (!fileName) return ''; + const index = fileName.lastIndexOf('.'); + if (index === -1) return 'file'; + const suffix = fileName.slice(index).toLowerCase(); + if (Utils.imageCheck(fileName)) return 'image'; + if (suffix === '.sdoc') return 'sdoc'; + return 'file'; + }, [_isDir, fileName]); + + const parentDir = useMemo(() => { + const value = record[PRIVATE_COLUMN_KEY.PARENT_DIR]; + if (value === '/') return ''; + return value; + }, [record]); + + const repoID = useMemo(() => { + return window.sfMetadataContext.getSetting('repoID'); + }, []); + + const path = useMemo(() => { + return Utils.encodePath(Utils.joinPath(parentDir, fileName)); + }, [parentDir, fileName]); + + const url = useMemo(() => { + return `${siteRoot}lib/${repoID}/file${path}`; + }, [path, repoID]); + + useEffect(() => { + if (fileType === 'image') return; + onCommitCancel && onCommitCancel(); + }, [fileType, onCommitCancel]); + + if (!fileName) return null; + + if (fileType === 'image') { + const fileExt = fileName.substr(fileName.lastIndexOf('.') + 1).toLowerCase(); + const isGIF = fileExt === 'gif'; + const useThumbnail = window.sfMetadataContext.getSetting('currentRepoInfo')?.encrypted; + let src = ''; + if (useThumbnail && !isGIF) { + src = `${siteRoot}thumbnail/${repoID}/${thumbnailSizeForOriginal}${path}`; + } else { + src = `${siteRoot}repo/${repoID}/raw${path}`; + } + const images = [ + { 'name': fileName, 'url': url, 'src': src }, + ]; + return ( + + {}} + moveToNextImage={() => {}} + /> + + ); + } + + if (fileType === 'sdoc') { + window.open(serviceURL + url); + } else { + window.open(window.location.href + Utils.encodePath(Utils.joinPath(parentDir, fileName))); + } + return null; +}; + +FileNameEditor.propTypes = { + column: PropTypes.object, + record: PropTypes.object, + onCommitCancel: PropTypes.func, +}; + +export default FileNameEditor; diff --git a/frontend/src/metadata/metadata-view/components/cell-editor/index.js b/frontend/src/metadata/metadata-view/components/cell-editor/index.js new file mode 100644 index 00000000000..8d3dbed8940 --- /dev/null +++ b/frontend/src/metadata/metadata-view/components/cell-editor/index.js @@ -0,0 +1,7 @@ +import EditorPortal from './editor-portal'; +import EditorContainer from './editor-container'; + +export { + EditorPortal, + EditorContainer, +}; diff --git a/frontend/src/metadata/metadata-view/components/table/table-main/records/record-cell.js b/frontend/src/metadata/metadata-view/components/table/table-main/records/record-cell.js index ed731ef64b4..91f4e513dd9 100644 --- a/frontend/src/metadata/metadata-view/components/table/table-main/records/record-cell.js +++ b/frontend/src/metadata/metadata-view/components/table/table-main/records/record-cell.js @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; import toaster from '../../../../../../components/toast'; -import { isFunction } from '../../../../_basic'; +import { PRIVATE_COLUMN_KEY, isFunction } from '../../../../_basic'; import { isNameColumn } from '../../../../utils/column-utils'; import { TABLE_SUPPORT_EDIT_TYPE_MAP } from '../../../../constants'; import { isCellValueChanged } from '../../../../utils/cell-comparer'; @@ -75,9 +75,7 @@ class RecordCell extends React.Component { }; onCellMouseDown = (e) => { - if (e.button === 2) { - return; - } + if (e.button === 2) return; const { column, groupRecordIndex, recordIndex, cellMetaData } = this.props; const cell = { idx: column.idx, groupRecordIndex, rowIdx: recordIndex }; @@ -133,9 +131,16 @@ class RecordCell extends React.Component { toaster.warning(message); }; + isDir = () => { + const { record } = this.props; + const isDirValue = record[PRIVATE_COLUMN_KEY.IS_DIR]; + if (typeof isDirValue === 'string') return isDirValue.toUpperCase() === 'TRUE'; + return isDirValue; + }; + render = () => { const { frozen, record, column, needBindEvents, height, bgColor } = this.props; - const { key, name, left, width } = column; + const { key, left, width } = column; const readonly = true; const commentCount = isNameColumn(column) && this.getCommentCount(); const hasComment = !!commentCount; @@ -151,7 +156,7 @@ class RecordCell extends React.Component { cellStyle['backgroundColor'] = bgColor; } - let cellValue = record[name] || record[key]; + let cellValue = record[key]; const cellEvents = needBindEvents && this.getEvents(); const props = { className, @@ -159,7 +164,7 @@ class RecordCell extends React.Component { ...cellEvents, }; const cellContent = ( - + ); return ( diff --git a/frontend/src/metadata/metadata-view/components/table/table-masks/interaction-masks/index.js b/frontend/src/metadata/metadata-view/components/table/table-masks/interaction-masks/index.js index 194e6c58ef8..39f277e8cd3 100644 --- a/frontend/src/metadata/metadata-view/components/table/table-masks/interaction-masks/index.js +++ b/frontend/src/metadata/metadata-view/components/table/table-masks/interaction-masks/index.js @@ -25,6 +25,7 @@ import { getGroupRecordByIndex } from '../../../../utils/group-metrics'; import DragMask from '../drag-mask'; import DragHandler from '../drag-handler'; import { gettext } from '../../../../../../utils/constants'; +import { EditorPortal, EditorContainer } from '../../../cell-editor'; import './index.css'; @@ -32,53 +33,6 @@ const READONLY_PREVIEW_COLUMNS = [ ]; -const propTypes = { - table: PropTypes.object, - columns: PropTypes.array, - canAddRow: PropTypes.bool, - isGroupView: PropTypes.bool, - recordsCount: PropTypes.number, - recordMetrics: PropTypes.object, - groups: PropTypes.array, - groupMetrics: PropTypes.object, - rowHeight: PropTypes.number, - groupOffsetLeft: PropTypes.number, - frozenColumnsWidth: PropTypes.number, - enableCellSelect: PropTypes.bool, - getRowTop: PropTypes.func, - scrollTop: PropTypes.number, - getScrollLeft: PropTypes.func, - getTableContentRect: PropTypes.func, - getMobileFloatIconStyle: PropTypes.func, - onToggleMobileMoreOperations: PropTypes.func, - onToggleInsertRecordDialog: PropTypes.func, - onCellRangeSelectionStarted: PropTypes.func, - onCellRangeSelectionUpdated: PropTypes.func, - onCellRangeSelectionCompleted: PropTypes.func, - selectNone: PropTypes.func, - onCheckCellIsEditable: PropTypes.func, - editorPortalTarget: PropTypes.instanceOf(Element).isRequired, - modifyRecord: PropTypes.func.isRequired, - recordGetterByIndex: PropTypes.func, - recordGetterById: PropTypes.func, - updateRecords: PropTypes.func, - deleteRecordsLinks: PropTypes.func, - paste: PropTypes.func, - editMobileCell: PropTypes.func, - getVisibleIndex: PropTypes.func, - onHitBottomBoundary: PropTypes.func, - onHitTopBoundary: PropTypes.func, - onCellClick: PropTypes.func, - scrollToColumn: PropTypes.func, - setRecordsScrollLeft: PropTypes.func, - getGroupCanvasScrollTop: PropTypes.func, - setGroupCanvasScrollTop: PropTypes.func, - appPage: PropTypes.object, - onFillingDragRows: PropTypes.func, - onCellsDragged: PropTypes.func, - gridUtils: PropTypes.object, - getCopiedRecordsAndColumnsFromRange: PropTypes.func, -}; class InteractionMasks extends React.Component { @@ -478,7 +432,7 @@ class InteractionMasks extends React.Component { } }; - modifyRecord = (updated, closeEditor = true) => { + onCommit = (updated, closeEditor = true) => { this.props.modifyRecord(updated); if (closeEditor) { this.closeEditor(); @@ -1057,7 +1011,6 @@ class InteractionMasks extends React.Component { } }; - renderSingleCellSelectView = () => { const { columns } = this.props; const { @@ -1157,7 +1110,8 @@ class InteractionMasks extends React.Component { }; render() { - const { selectedRange, draggedRange } = this.state; + const { selectedRange, isEditorEnabled, draggedRange, selectedPosition, firstEditorKeyDown, openEditorMode, editorPosition } = this.state; + const { table, columns, isGroupView, recordGetterByIndex, scrollTop, getScrollLeft, editorPortalTarget } = this.props; const isSelectedSingleCell = selectedRangeIsSingleCell(selectedRange); return (
+ + + )}
); } } -InteractionMasks.propTypes = propTypes; +InteractionMasks.propTypes = { + table: PropTypes.object, + columns: PropTypes.array, + canAddRow: PropTypes.bool, + isGroupView: PropTypes.bool, + recordsCount: PropTypes.number, + recordMetrics: PropTypes.object, + groups: PropTypes.array, + groupMetrics: PropTypes.object, + rowHeight: PropTypes.number, + groupOffsetLeft: PropTypes.number, + frozenColumnsWidth: PropTypes.number, + enableCellSelect: PropTypes.bool, + getRowTop: PropTypes.func, + scrollTop: PropTypes.number, + getScrollLeft: PropTypes.func, + getTableContentRect: PropTypes.func, + getMobileFloatIconStyle: PropTypes.func, + onToggleMobileMoreOperations: PropTypes.func, + onToggleInsertRecordDialog: PropTypes.func, + onCellRangeSelectionStarted: PropTypes.func, + onCellRangeSelectionUpdated: PropTypes.func, + onCellRangeSelectionCompleted: PropTypes.func, + selectNone: PropTypes.func, + onCheckCellIsEditable: PropTypes.func, + editorPortalTarget: PropTypes.instanceOf(Element).isRequired, + modifyRecord: PropTypes.func.isRequired, + recordGetterByIndex: PropTypes.func, + recordGetterById: PropTypes.func, + updateRecords: PropTypes.func, + deleteRecordsLinks: PropTypes.func, + paste: PropTypes.func, + editMobileCell: PropTypes.func, + getVisibleIndex: PropTypes.func, + onHitBottomBoundary: PropTypes.func, + onHitTopBoundary: PropTypes.func, + onCellClick: PropTypes.func, + scrollToColumn: PropTypes.func, + setRecordsScrollLeft: PropTypes.func, + getGroupCanvasScrollTop: PropTypes.func, + setGroupCanvasScrollTop: PropTypes.func, + appPage: PropTypes.object, + onFillingDragRows: PropTypes.func, + onCellsDragged: PropTypes.func, + gridUtils: PropTypes.object, + getCopiedRecordsAndColumnsFromRange: PropTypes.func, + onCommit: PropTypes.func, +}; export default InteractionMasks; diff --git a/frontend/src/metadata/metadata-view/context.js b/frontend/src/metadata/metadata-view/context.js index 13f289fd991..d6a2d72ebb9 100644 --- a/frontend/src/metadata/metadata-view/context.js +++ b/frontend/src/metadata/metadata-view/context.js @@ -76,7 +76,7 @@ class Context { }; canModifyRow = (row) => { - return false; + return true; }; getPermission = () => { diff --git a/frontend/src/metadata/metadata-view/utils/column-utils.js b/frontend/src/metadata/metadata-view/utils/column-utils.js index 8835dd8f8fd..01e33c8819b 100644 --- a/frontend/src/metadata/metadata-view/utils/column-utils.js +++ b/frontend/src/metadata/metadata-view/utils/column-utils.js @@ -120,15 +120,13 @@ export const setColumnOffsets = (columns) => { export function isColumnSupportEdit(cell, columns) { const column = columns[cell.idx]; - if (column?.type === CellType.LINK_FORMULA && [CellType.IMAGE, CellType.FILE].includes(column?.data?.array_type)) { - return true; - } + if (column.type === CellType.FILE_NAME) return true; return false; } export function isColumnSupportDirectEdit(cell, columns) { const column = columns[cell.idx]; - return [].includes(column?.type); + return [CellType.CHECKBOX].includes(column?.type); } const _getCustomColumnsWidth = () => { @@ -172,21 +170,17 @@ export const recalculate = (columns, allColumns) => { export const getColumnName = (key, name) => { switch (key) { case PRIVATE_COLUMN_KEY.CTIME: + case PRIVATE_COLUMN_KEY.FILE_CTIME: return gettext('Created time'); case PRIVATE_COLUMN_KEY.MTIME: + case PRIVATE_COLUMN_KEY.FILE_MTIME: return gettext('Last modified time'); case PRIVATE_COLUMN_KEY.CREATOR: + case PRIVATE_COLUMN_KEY.FILE_CREATOR: return gettext('Creator'); case PRIVATE_COLUMN_KEY.LAST_MODIFIER: - return gettext('Last modifier'); - case PRIVATE_COLUMN_KEY.FILE_CREATOR: - return gettext('File creator'); case PRIVATE_COLUMN_KEY.FILE_MODIFIER: - return gettext('File modifier'); - case PRIVATE_COLUMN_KEY.FILE_CTIME: - return gettext('File created time'); - case PRIVATE_COLUMN_KEY.FILE_MTIME: - return gettext('File last modified time'); + return gettext('Last modifier'); case PRIVATE_COLUMN_KEY.IS_DIR: return gettext('Is folder'); case PRIVATE_COLUMN_KEY.PARENT_DIR: