1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-08-31 14:42:10 +00:00

feat: filename support open a file (#6304)

* feat: filename support open a file

* feat: add currentRepoInfo

* feat: op code

---------

Co-authored-by: 杨国璇 <ygx@Hello-word.local>
This commit is contained in:
杨国璇
2024-07-05 17:41:34 +08:00
committed by GitHub
parent 024290a12c
commit ae5b90e6b2
13 changed files with 302 additions and 80 deletions

View File

@@ -19,7 +19,7 @@
"@seafile/sdoc-editor": "1.0.11",
"@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",
@@ -4970,9 +4970,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",
@@ -32193,9 +32193,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",

View File

@@ -14,7 +14,7 @@
"@seafile/sdoc-editor": "1.0.11",
"@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",

View File

@@ -16,6 +16,7 @@ const propTypes = {
lastModified: PropTypes.string,
latestContributor: PropTypes.string,
onLinkClick: PropTypes.func.isRequired,
currentRepoInfo: PropTypes.object,
};
class DirColumnFile extends React.Component {
@@ -60,7 +61,7 @@ class DirColumnFile extends React.Component {
return (
<div className="w-100 h-100 o-hidden d-flex" style={{ paddingRight: 10, flexDirection: 'column', alignItems: 'center' }}>
<div className="" style={{ width: '100%', height: 10, zIndex: 7, transform: 'translateZ(1000px)', position: 'relative', background: '#fff' }}></div>
<SeafileMetadata repoID={this.props.repoID} />
<SeafileMetadata repoID={this.props.repoID} currentRepoInfo={this.props.currentRepoInfo} />
</div>
);
}

View File

@@ -190,6 +190,7 @@ class DirColumnView extends React.Component {
isFileLoadedErr={this.props.isFileLoadedErr}
filePermission={this.props.filePermission}
content={this.props.content}
currentRepoInfo={this.props.currentRepoInfo}
lastModified={this.props.lastModified}
latestContributor={this.props.latestContributor}
onLinkClick={this.props.onLinkClick}

View File

@@ -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 (<Editor { ...props } />);
};
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;

View File

@@ -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;

View File

@@ -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 (<FileNameEditor {...props} />);
}
default: {
return null;
}
}
};
Editor.propTypes = {
column: PropTypes.object.isRequired,
};
export default Editor;

View File

@@ -0,0 +1,95 @@
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 '';
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 (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 (
<ModalPortal>
<ImageDialog
imageItems={images}
imageIndex={0}
closeImagePopup={onCommitCancel}
moveToPrevImage={() => {}}
moveToNextImage={() => {}}
/>
</ModalPortal>
);
}
if (!fileType || 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;

View File

@@ -0,0 +1,7 @@
import EditorPortal from './editor-portal';
import EditorContainer from './editor-container';
export {
EditorPortal,
EditorContainer,
};

View File

@@ -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 = (
<CellFormatter readonly={readonly} value={cellValue} field={column} isDir={record['_is_dir'] === 'True'} />
<CellFormatter readonly={readonly} value={cellValue} field={column} isDir={this.isDir()} />
);
return (

View File

@@ -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 (
<div
@@ -1178,11 +1132,81 @@ class InteractionMasks extends React.Component {
)}
{isSelectedSingleCell && this.renderSingleCellSelectView()}
{!isSelectedSingleCell && this.renderCellRangeSelectView()}
{isEditorEnabled && (
<EditorPortal target={editorPortalTarget}>
<EditorContainer
table={table}
columns={columns}
isGroupView={isGroupView}
scrollTop={scrollTop}
firstEditorKeyDown={firstEditorKeyDown}
openEditorMode={openEditorMode}
portalTarget={editorPortalTarget}
scrollLeft={getScrollLeft()}
record={getSelectedRow({ selectedPosition, isGroupView, recordGetterByIndex })}
column={getSelectedColumn({ selectedPosition, columns })}
onCommit={this.onCommit}
onCommitCancel={this.onCommitCancel}
editorPosition={editorPosition}
{...{
...this.getSelectedDimensions(selectedPosition),
...this.state.editorPosition
}}
/>
</EditorPortal>
)}
</div>
);
}
}
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;

View File

@@ -76,7 +76,7 @@ class Context {
};
canModifyRow = (row) => {
return false;
return true;
};
getPermission = () => {

View File

@@ -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: