1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-14 14:21:23 +00:00

feat: side properties (#6485)

* feat: side properties

* feat: show metadata

* feat: delete record expand

* feat: optimzie code

* fix: bug

---------

Co-authored-by: 杨国璇 <ygx@Hello-word.local>
This commit is contained in:
杨国璇
2024-08-06 17:30:11 +08:00
committed by GitHub
parent 1c169b5329
commit b043cb8491
63 changed files with 408 additions and 2490 deletions

View File

@@ -1,22 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Col } from 'reactstrap';
function ColumnName(props) {
const { column } = props;
const { name } = column;
return (
<Col md={3} className="d-flex column-name">
<div className="w-100 text-truncate">
{name || ''}
</div>
</Col>
);
}
ColumnName.propTypes = {
column: PropTypes.object.isRequired,
};
export default ColumnName;

View File

@@ -1,7 +0,0 @@
.extra-attributes-dialog .column-name {
padding-top: 9px;
}
.extra-attributes-dialog .column-item {
min-height: 56px;
}

View File

@@ -1,37 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Col } from 'reactstrap';
import ColumnName from './column-name';
import CONFIG from '../editor';
import './index.css';
class Column extends Component {
render() {
const { column, row, columns } = this.props;
const Editor = CONFIG[column.type] || CONFIG['text'];
return (
<div className="pb-4 row column-item">
<ColumnName column={column} />
<Col md={9} className='d-flex align-items-center extra-attribute-item-info'>
<Editor
column={column}
row={row}
columns={columns}
onCommit={this.props.onCommit}
/>
</Col>
</div>
);
}
}
Column.propTypes = {
column: PropTypes.object,
row: PropTypes.object,
columns: PropTypes.array,
onCommit: PropTypes.func,
};
export default Column;

View File

@@ -1,22 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { getDateDisplayString } from '../../../../utils/extra-attributes';
class CtimeFormatter extends Component {
render() {
const { column, row } = this.props;
const { key } = column;
const value = getDateDisplayString(row[key], 'YYYY-MM-DD HH:mm:ss') || '';
return (
<div className="form-control" style={{ width: 320 }}>{value}</div>
);
}
}
CtimeFormatter.propTypes = {
column: PropTypes.object,
row: PropTypes.object,
};
export default CtimeFormatter;

View File

@@ -1,28 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { getDateDisplayString } from '../../../../utils/extra-attributes';
class DateEditor extends Component {
render() {
const { column, row } = this.props;
const { data, key } = column;
const value = getDateDisplayString(row[key], data ? data.format : '');
return (
<input
type="text"
className="form-control"
value={value}
disabled={true}
/>
);
}
}
DateEditor.propTypes = {
column: PropTypes.object,
row: PropTypes.object,
};
export default DateEditor;

View File

@@ -1,31 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FORMULA_RESULT_TYPE } from '../../../../constants';
import { getDateDisplayString } from '../../../../utils/extra-attributes';
function FormulaFormatter(props) {
const { column, row } = props;
const value = row[column.key];
const { data } = column;
const { result_type, format } = data || {};
if (result_type === FORMULA_RESULT_TYPE.DATE) {
return (
<div className="form-control disabled">{getDateDisplayString(value, format)}</div>
);
}
if (result_type === FORMULA_RESULT_TYPE.STRING) {
return value;
}
if (typeof value === 'object') {
return null;
}
return <></>;
}
FormulaFormatter.propTypes = {
column: PropTypes.object,
row: PropTypes.object,
};
export default FormulaFormatter;

View File

@@ -1,20 +0,0 @@
import SimpleText from './simple-text';
import FormulaFormatter from './formula-formatter';
import SingleSelect from './single-select';
import NumberEditor from './number-editor';
import DateEditor from './date-editor';
import CtimeFormatter from './ctime-formatter';
import { EXTRA_ATTRIBUTES_COLUMN_TYPE } from '../../../../constants';
const CONFIG = {
[EXTRA_ATTRIBUTES_COLUMN_TYPE.TEXT]: SimpleText,
[EXTRA_ATTRIBUTES_COLUMN_TYPE.FORMULA]: FormulaFormatter,
[EXTRA_ATTRIBUTES_COLUMN_TYPE.SINGLE_SELECT]: SingleSelect,
[EXTRA_ATTRIBUTES_COLUMN_TYPE.NUMBER]: NumberEditor,
[EXTRA_ATTRIBUTES_COLUMN_TYPE.DATE]: DateEditor,
[EXTRA_ATTRIBUTES_COLUMN_TYPE.CTIME]: CtimeFormatter,
[EXTRA_ATTRIBUTES_COLUMN_TYPE.MTIME]: CtimeFormatter,
};
export default CONFIG;

View File

@@ -1,90 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { getNumberDisplayString, replaceNumberNotAllowInput, formatStringToNumber, isMac } from '../../../../utils/extra-attributes';
import { KeyCodes, DEFAULT_NUMBER_FORMAT } from '../../../../constants';
class NumberEditor extends React.Component {
constructor(props) {
super(props);
const { row, column } = props;
const value = row[column.key];
this.state = {
value: getNumberDisplayString(value, column.data),
};
}
onChange = (event) => {
const { data } = this.props.column; // data maybe 'null'
const format = (data && data.format) ? data.format : DEFAULT_NUMBER_FORMAT;
let currency_symbol = null;
if (data && data.format === 'custom_currency') {
currency_symbol = data['currency_symbol'];
}
const initValue = event.target.value.trim();
// Prevent the repetition of periods bug in the Chinese input method of the Windows system
if (!isMac() && initValue.indexOf('.。') > -1) return;
let value = replaceNumberNotAllowInput(initValue, format, currency_symbol);
if (value === this.state.value) return;
this.setState({ value });
};
onKeyDown = (event) => {
let { selectionStart, selectionEnd, value } = event.currentTarget;
if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Esc) {
event.preventDefault();
this.input.blur();
} else if ((event.keyCode === KeyCodes.LeftArrow && selectionStart === 0) ||
(event.keyCode === KeyCodes.RightArrow && selectionEnd === value.length)
) {
event.stopPropagation();
}
};
onBlur = () => {
const { value } = this.state;
const { column } = this.props;
this.props.onCommit({ [column.key]: formatStringToNumber(value, column.data) }, column);
};
setInputRef = (input) => {
this.input = input;
return this.input;
};
onPaste = (e) => {
e.stopPropagation();
};
onCut = (e) => {
e.stopPropagation();
};
render() {
const { column } = this.props;
return (
<input
ref={this.setInputRef}
type="text"
className="form-control"
value={this.state.value}
onBlur={this.onBlur}
onPaste={this.onPaste}
onCut={this.onCut}
onKeyDown={this.onKeyDown}
onChange={this.onChange}
disabled={!column.editable}
/>
);
}
}
NumberEditor.propTypes = {
column: PropTypes.object,
row: PropTypes.object,
onCommit: PropTypes.func,
};
export default NumberEditor;

View File

@@ -1,108 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
class SearchInput extends Component {
constructor(props) {
super(props);
this.state = {
searchValue: props.value,
};
this.isInputtingChinese = false;
this.timer = null;
this.inputRef = null;
}
componentDidMount() {
if (this.props.autoFocus && this.inputRef && this.inputRef !== document.activeElement) {
setTimeout(() => {
this.inputRef.focus();
}, 0);
}
}
UNSAFE_componentWillReceiveProps(nextProps) {
if (nextProps.value !== this.props.value) {
this.setState({ searchValue: nextProps.value });
}
}
componentWillUnmount() {
this.timer && clearTimeout(this.timer);
this.timer = null;
this.inputRef = null;
}
onCompositionStart = () => {
this.isInputtingChinese = true;
};
onChange = (e) => {
this.timer && clearTimeout(this.timer);
const { onChange, wait } = this.props;
let text = e.target.value;
this.setState({ searchValue: text || '' }, () => {
if (this.isInputtingChinese) return;
this.timer = setTimeout(() => {
onChange && onChange(this.state.searchValue.trim());
}, wait);
});
};
onCompositionEnd = (e) => {
this.isInputtingChinese = false;
this.onChange(e);
};
setFocus = (isSelectAllText) => {
if (this.inputRef === document.activeElement) return;
this.inputRef.focus();
if (isSelectAllText) {
const txtLength = this.state.searchValue.length;
this.inputRef.setSelectionRange(0, txtLength);
}
};
render() {
const { placeholder, autoFocus, className, onKeyDown, disabled, style } = this.props;
const { searchValue } = this.state;
return (
<input
type="text"
value={searchValue}
className={classnames('form-control', className)}
onChange={this.onChange}
autoFocus={autoFocus}
placeholder={placeholder}
onCompositionStart={this.onCompositionStart}
onCompositionEnd={this.onCompositionEnd}
onKeyDown={onKeyDown}
disabled={disabled}
style={style}
ref={ref => this.inputRef = ref}
/>
);
}
}
SearchInput.propTypes = {
placeholder: PropTypes.string,
autoFocus: PropTypes.bool,
className: PropTypes.string,
onChange: PropTypes.func.isRequired,
onKeyDown: PropTypes.func,
wait: PropTypes.number,
disabled: PropTypes.bool,
style: PropTypes.object,
value: PropTypes.string,
};
SearchInput.defaultProps = {
wait: 100,
disabled: false,
value: '',
};
export default SearchInput;

View File

@@ -1,92 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { KeyCodes } from '../../../../constants';
class SimpleText extends React.Component {
constructor(props) {
super(props);
this.state = {
value: props.row[props.column.key] || '',
};
this.inputRef = React.createRef();
}
UNSAFE_componentWillReceiveProps(nextProps) {
const nextValue = nextProps.row[nextProps.column.key];
if (nextValue !== this.state.value) {
this.setState({ value: nextValue });
}
}
blurInput = () => {
setTimeout(() => {
this.inputRef.current && this.inputRef.current.blur();
}, 1);
};
onBlur = () => {
let { column, onCommit } = this.props;
const updated = {};
updated[column.key] = this.state.value.trim();
onCommit(updated, column);
};
onChange = (e) => {
let value = e.target.value;
if (value === this.state.value) return;
this.setState({ value });
};
onCut = (e) => {
e.stopPropagation();
};
onPaste = (e) => {
e.stopPropagation();
};
onKeyDown = (e) => {
if (e.keyCode === KeyCodes.Esc) {
e.stopPropagation();
this.blurInput();
return;
}
let { selectionStart, selectionEnd, value } = e.currentTarget;
if (
(e.keyCode === KeyCodes.ChineseInputMethod) ||
(e.keyCode === KeyCodes.LeftArrow && selectionStart === 0) ||
(e.keyCode === KeyCodes.RightArrow && selectionEnd === value.length)
) {
e.stopPropagation();
}
};
render() {
const { column } = this.props;
const { value } = this.state;
return (
<input
type="text"
onBlur={this.onBlur}
onCut={this.onCut}
onPaste={this.onPaste}
onChange={this.onChange}
className="form-control"
value={value || ''}
onKeyDown={this.onKeyDown}
disabled={!column.editable}
ref={this.inputRef}
/>
);
}
}
SimpleText.propTypes = {
column: PropTypes.object,
row: PropTypes.object,
onCommit: PropTypes.func.isRequired,
};
export default SimpleText;

View File

@@ -1,102 +0,0 @@
.extra-attributes-dialog .selected-single-select-container {
height: 38px;
width: 100%;
padding: 0 10px;
border-radius: 3px;
user-select: none;
border: 1px solid rgba(0, 40, 100, .12);
appearance: none;
background: #fff;
}
.extra-attributes-dialog .selected-single-select-container.disable {
background-color: #f8f9fa;
}
.extra-attributes-dialog .selected-single-select-container.focus {
border-color: #1991eb!important;
box-shadow: 0 0 0 2px rgba(70, 127, 207, .25);
}
.extra-attributes-dialog .selected-single-select-container:not(.disable):hover {
cursor: pointer;
}
.extra-attributes-dialog .selected-single-select-container .single-select-option {
text-align: center;
width: min-content;
max-width: 250px;
line-height: 20px;
border-radius: 10px;
padding: 0 10px;
font-size: 13px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* editor */
.single-select-editor-popover .popover,
.single-select-editor-popover .popover-inner {
width: fit-content;
max-width: fit-content;
}
.single-select-editor-container {
min-height: 160px;
width: 320px;
overflow: hidden;
background-color: #fff;
}
.single-select-editor-container .search-single-selects {
padding: 10px 10px 0;
}
.single-select-editor-container .search-single-selects input {
max-height: 30px;
font-size: 14px;
}
.single-select-editor-container .single-select-editor-content {
max-height: 200px;
min-height: 100px;
padding: 10px;
overflow-x: hidden;
overflow-y: scroll;
}
.single-select-editor-container .single-select-editor-content .single-select-option-container {
width: 100%;
height: 30px;
border-radius: 2px;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 13px;
color: #212529;
padding-left: 12px;
}
.single-select-editor-container .single-select-editor-content .single-select-option-container:hover {
background-color: #f5f5f5;
cursor: pointer;
}
.single-select-editor-container .single-select-editor-content .single-select-option {
padding: 0 10px;
height: 20px;
line-height: 20px;
text-align: center;
border-radius: 10px;
margin-right: 10px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.single-select-editor-container .single-select-editor-content .single-select-option-selected {
width: 20px;
text-align: center;
}

View File

@@ -1,84 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { DELETED_OPTION_BACKGROUND_COLOR, DELETED_OPTION_TIPS } from '../../../../../constants';
import { gettext } from '../../../../../utils/constants';
import SingleSelectEditor from './single-select-editor';
import { getSelectColumnOptions } from '../../../../../utils/extra-attributes';
import './index.css';
class SingleSelect extends Component {
constructor(props) {
super(props);
const { column } = props;
this.options = getSelectColumnOptions(column);
this.state = {
isShowSingleSelect: false,
};
this.editorKey = `single-select-editor-${column.key}`;
}
updateState = () => {
this.setState({ isShowSingleSelect: !this.state.isShowSingleSelect });
};
onCommit = (value, column) => {
this.props.onCommit(value, column);
};
render() {
const { isShowSingleSelect } = this.state;
const { column, row } = this.props;
const currentOptionID = row[column.key];
const option = this.options.find(option => option.id === currentOptionID);
const optionStyle = option ?
{ backgroundColor: option.color, color: option.textColor || null } :
{ backgroundColor: DELETED_OPTION_BACKGROUND_COLOR };
const optionName = option ? option.name : gettext(DELETED_OPTION_TIPS);
return (
<>
<div
id={this.editorKey}
className={classnames('selected-single-select-container', { 'disable': !column.editable, 'focus': isShowSingleSelect })}
>
<div className="single-select-inner w-100 h-100 d-flex align-items-center justify-content-between">
<div>
{currentOptionID && (
<div
className="single-select-option"
style={optionStyle}
title={optionName}
>{optionName}
</div>
)}
</div>
{column.editable && (
<i className="sf3-font sf3-font-down"></i>
)}
</div>
</div>
{column.editable && (
<SingleSelectEditor
column={column}
row={this.props.row}
columns={this.props.columns}
onCommit={this.onCommit}
onUpdateState={this.updateState}
/>
)}
</>
);
}
}
SingleSelect.propTypes = {
column: PropTypes.object,
row: PropTypes.object,
columns: PropTypes.array,
onCommit: PropTypes.func,
};
export default SingleSelect;

View File

@@ -1,141 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { UncontrolledPopover } from 'reactstrap';
import { gettext } from '../../../../../utils/constants';
import SearchInput from '../search-input';
import { getSelectColumnOptions } from '../../../../../utils/extra-attributes';
class SingleSelectEditor extends Component {
constructor(props) {
super(props);
const options = this.getSelectColumnOptions(props);
this.state = {
value: props.row[props.column.key],
searchVal: '',
highlightIndex: -1,
maxItemNum: 0,
itemHeight: 0,
filteredOptions: options,
};
this.options = options;
this.timer = null;
this.editorKey = `single-select-editor-${props.column.key}`;
}
UNSAFE_componentWillReceiveProps(nextProps) {
const currentCascadeColumnValue = this.getCascadeColumnValue(this.props);
const nextCascadeColumnValue = this.getCascadeColumnValue(nextProps);
if (currentCascadeColumnValue !== nextCascadeColumnValue) {
this.options = this.getSelectColumnOptions(nextProps);
this.setState({ filteredOptions: this.options });
}
}
getCascadeColumnValue = (props) => {
const { column, row, columns } = props;
const { data } = column;
const { cascade_column_key } = data || {};
if (!cascade_column_key) return '';
const cascadeColumn = columns.find(item => item.key === cascade_column_key);
if (!cascadeColumn) return '';
return row[cascade_column_key];
};
getSelectColumnOptions = (props) => {
const { column, row, columns } = props;
let options = getSelectColumnOptions(column);
const { data } = column;
const { cascade_column_key, cascade_settings } = data || {};
if (cascade_column_key) {
const cascadeColumn = columns.find(item => item.key === cascade_column_key);
if (cascadeColumn) {
const cascadeColumnValue = row[cascade_column_key];
if (!cascadeColumnValue) return [];
const cascadeSetting = cascade_settings[cascadeColumnValue];
if (!cascadeSetting || !Array.isArray(cascadeSetting) || cascadeSetting.length === 0) return [];
return options.filter(option => cascadeSetting.includes(option.id));
}
}
return options;
};
toggle = () => {
this.ref.toggle();
this.props.onUpdateState();
};
onChangeSearch = (searchVal) => {
const { searchVal: oldSearchVal } = this.state;
if (oldSearchVal === searchVal) return;
const val = searchVal.toLowerCase();
const filteredOptions = val ?
this.options.filter((item) => item.name && item.name.toLowerCase().indexOf(val) > -1) : this.options;
this.setState({ searchVal, filteredOptions });
};
onSelectOption = (optionID) => {
const { column } = this.props;
this.setState({ value: optionID }, () => {
this.props.onCommit({ [column.key]: optionID }, column);
this.toggle();
});
};
render() {
const { value, filteredOptions } = this.state;
const { column } = this.props;
return (
<UncontrolledPopover
target={this.editorKey}
className="single-select-editor-popover"
trigger="legacy"
placement="bottom-start"
hideArrow={true}
toggle={this.toggle}
ref={ref => this.ref = ref}
>
<div className="single-select-editor-container">
<div className="search-single-selects">
<SearchInput
placeholder={gettext('Find an option')}
onKeyDown={this.onKeyDown}
onChange={this.onChangeSearch}
autoFocus={true}
/>
</div>
<div className="single-select-editor-content">
{filteredOptions.map(option => {
const isSelected = value === option.id;
const style = {
backgroundColor: option.color,
color: option.textColor || null,
maxWidth: Math.max(200 - 62, column.width ? column.width - 62 : 0)
};
return (
<div className="single-select-option-container" key={option.id} onClick={this.onSelectOption.bind(this, isSelected ? null : option.id)}>
<div className="single-select-option" style={style}>{option.name}</div>
<div className="single-select-option-selected">
{isSelected && (<i ></i>)}
</div>
</div>
);
})}
</div>
</div>
</UncontrolledPopover>
);
}
}
SingleSelectEditor.propTypes = {
value: PropTypes.string,
row: PropTypes.object,
column: PropTypes.object,
columns: PropTypes.array,
onUpdateState: PropTypes.func,
onCommit: PropTypes.func,
};
export default SingleSelectEditor;

View File

@@ -1,17 +0,0 @@
.extra-attributes-dialog {
margin: 28px 0 0 0;
}
.extra-attributes-dialog .extra-attributes-content-container {
height: 100%;
overflow: hidden;
}
.extra-attributes-dialog .modal-body {
overflow-y: scroll;
padding: 30px;
}
.extra-attributes-dialog .modal-body .form-control.disabled {
background-color: #f8f9fa;
}

View File

@@ -1,235 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Modal, ModalHeader, ModalBody } from 'reactstrap';
import isHotkey from 'is-hotkey';
import { zIndexes, DIALOG_MAX_HEIGHT } from '../../../constants';
import { gettext } from '../../../utils/constants';
import { Utils } from '../../../utils/utils';
import { getValidColumns } from '../../../utils/extra-attributes';
import Column from './column';
import Loading from '../../loading';
import toaster from '../../toast';
import metadataAPI from '../../../metadata/api';
import './index.css';
class ExtraMetadataAttributesDialog extends Component {
constructor(props) {
super(props);
const { direntDetail, direntType } = props;
this.state = {
animationEnd: false,
isLoading: true,
update: {},
row: {},
columns: [],
errorMsg: '',
};
if (direntType === 'dir') {
this.isEmptyFile = false;
} else {
const direntDetailId = direntDetail?.id || '';
this.isEmptyFile = direntDetailId === '0'.repeat(direntDetailId.length);
}
this.isExist = false;
this.modalRef = React.createRef();
}
componentDidMount() {
this.startAnimation(this.getData);
window.addEventListener('keydown', this.onHotKey);
}
componentWillUnmount() {
window.removeEventListener('keydown', this.onHotKey);
}
startAnimation = (callback) => {
if (this.state.animationEnd === true) {
callback && callback();
}
// use setTimeout to make sure real dom rendered
setTimeout(() => {
let dom = this.modalRef.current.firstChild;
const { width, maxWidth, marginLeft, height } = this.getDialogStyle();
dom.style.width = `${width}px`;
dom.style.maxWidth = `${maxWidth}px`;
dom.style.marginLeft = `${marginLeft}px`;
dom.style.height = `${height}px`;
dom.style.marginRight = 'unset';
dom.style.marginTop = '28px';
// after animation, change style and run callback
setTimeout(() => {
this.setState({ animationEnd: true }, () => {
dom.style.transition = 'none';
callback && callback();
});
}, 280);
}, 1);
};
getData = () => {
const { repoID, filePath, direntType } = this.props;
let dirName = Utils.getDirName(filePath);
let fileName = Utils.getFileName(filePath);
let parentDir = direntType === 'file' ? dirName : dirName.slice(0, dirName.length - fileName.length - 1);
if (!parentDir.startsWith('/')) {
parentDir = '/' + parentDir;
}
metadataAPI.getMetadataRecordInfo(repoID, parentDir, fileName).then(res => {
const { row, metadata, editable_columns } = res.data;
this.isExist = Boolean(row._id);
this.setState({ row: row, columns: getValidColumns(metadata, editable_columns, this.isEmptyFile), isLoading: false, errorMsg: '' });
}).catch(error => {
const errorMsg = Utils.getErrorMsg(error);
this.setState({ isLoading: false, errorMsg });
});
};
updateData = (update, column) => {
const newRow = { ...this.state.row, ...update };
this.setState({ row: newRow }, () => {
const { repoID } = this.props;
let newValue = update[column.key];
let recordID = this.state.row._id;
if (this.isExist) {
metadataAPI.modifyRecord(repoID, recordID, { [column.name]: newValue }).then(res => {
this.setState({ update: {}, row: res.data.row });
}).catch(error => {
const errorMsg = Utils.getErrorMsg(error);
toaster.danger(gettext(errorMsg));
});
} else {
// this.createData(data);
}
});
};
onHotKey = (event) => {
if (isHotkey('esc', event)) {
this.onToggle();
return;
}
};
onToggle = () => {
this.props.onToggle();
};
getDialogStyle = () => {
const width = 800;
return {
width,
maxWidth: width,
marginLeft: (window.innerWidth - width) / 2,
height: DIALOG_MAX_HEIGHT,
};
};
getInitStyle = () => {
const transition = 'all .3s';
const defaultMargin = 80; // sequence cell width
const defaultHeight = 100;
const marginTop = '30%';
const width = window.innerWidth;
return {
width: `${width - defaultMargin}px`,
maxWidth: `${width - defaultMargin}px`,
marginLeft: `${defaultMargin}px`,
height: `${defaultHeight}px`,
marginRight: `${defaultMargin}px`,
marginTop,
transition,
};
};
renderColumns = () => {
const { isLoading, errorMsg, columns, row, update } = this.state;
if (isLoading) {
return (
<div className="w-100 h-100 d-flex align-items-center justify-content-center">
<Loading />
</div>
);
}
if (errorMsg) {
return (
<div className="w-100 h-100 d-flex align-items-center justify-content-center error-message">
{gettext(errorMsg)}
</div>
);
}
const newRow = { ...row, ...update };
return (
<>
{columns.map(column => {
return (
<Column
key={column.key}
column={column}
row={newRow}
columns={columns}
onCommit={this.updateData}
/>
);
})}
</>
);
};
renderContent = () => {
if (!this.state.animationEnd) return null;
return (
<>
<ModalHeader toggle={this.onToggle}>{gettext('Edit extra properties')}</ModalHeader>
<ModalBody>
{this.renderColumns()}
</ModalBody>
</>
);
};
render() {
const { animationEnd } = this.state;
return (
<Modal
isOpen={true}
className="extra-attributes-dialog"
style={animationEnd ? this.getDialogStyle() : this.getInitStyle()}
zIndex={zIndexes.EXTRA_ATTRIBUTES_DIALOG_MODAL}
contentClassName="extra-attributes-content-container"
modalClassName="extra-attributes-modal"
wrapClassName="extra-attributes"
fade={false}
innerRef={this.modalRef}
toggle={this.onToggle}
>
{this.renderContent()}
</Modal>
);
}
}
ExtraMetadataAttributesDialog.propTypes = {
repoID: PropTypes.string,
filePath: PropTypes.string,
direntType: PropTypes.string,
direntDetail: PropTypes.object,
onToggle: PropTypes.func,
};
export default ExtraMetadataAttributesDialog;

View File

@@ -49,3 +49,9 @@
.dirent-detail-item-value .creator-formatter {
height: 20px;
}
.dirent-detail-item-value .sf-metadata-record-cell-empty::before {
content: attr(placeholder);
color: #666;
font-size: 14px;
}

View File

@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import { Formatter, Icon } from '@seafile/sf-metadata-ui-component';
import classnames from 'classnames';
import { CellType, COLUMNS_ICON_CONFIG } from '../../../metadata/metadata-view/_basic';
import { gettext } from '../../../utils/constants';
import './index.css';
@@ -19,15 +20,19 @@ const DetailItem = ({ field, value, valueId, valueClick, children, ...params })
<span className="dirent-detail-item-name">{field.name}</span>
</div>
<div className={classnames('dirent-detail-item-value', { 'editable': valueClick })} id={valueId} onClick={valueClick}>
{children ? children : (<Formatter { ...params } field={field} value={value}/>)}
{children ? children : (<Formatter { ...params } field={field} value={value} />)}
</div>
</div>
);
};
DetailItem.defaultProps = {
emptyTip: gettext('Empty')
};
DetailItem.propTypes = {
field: PropTypes.object.isRequired,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.array, PropTypes.object]),
value: PropTypes.any,
children: PropTypes.any,
valueId: PropTypes.string,
};

View File

@@ -32,7 +32,7 @@ class DetailListView extends React.Component {
this.tagListTitleID = `detail-list-view-tags-${uuidv4()}`;
}
getDirentPosition = () => {
getFileParent = () => {
let { repoInfo } = this.props;
let direntPath = this.getDirentPath();
let position = repoInfo.repo_name;
@@ -69,7 +69,7 @@ class DetailListView extends React.Component {
renderTags = () => {
const { direntType, direntDetail } = this.props;
const position = this.getDirentPosition();
const position = this.getFileParent();
if (direntType === 'dir') {
return (
<table className="table-thead-hidden">

View File

@@ -1,18 +1,18 @@
import React, { useMemo } from 'react';
import PropTypes from 'prop-types';
import { getDirentPath, getDirentPosition } from './utils';
import { getDirentPath, getFileParent } from './utils';
import DetailItem from '../detail-item';
import { CellType } from '../../../metadata/metadata-view/_basic';
import { gettext } from '../../../utils/constants';
import EditMetadata from './edit-metadata';
import { MetadataDetails } from '../../../metadata';
const DirDetails = ({ repoID, repoInfo, dirent, direntType, path, direntDetail }) => {
const position = useMemo(() => getDirentPosition(repoInfo, dirent, path), [repoInfo, dirent, path]);
const DirDetails = ({ repoID, repoInfo, dirent, path, direntDetail, ...params }) => {
const parent = useMemo(() => getFileParent(repoInfo, dirent, path), [repoInfo, dirent, path]);
const direntPath = useMemo(() => getDirentPath(dirent, path), [dirent, path]);
return (
<>
<DetailItem field={{ type: CellType.TEXT, name: gettext('File location') }} value={position} />
<DetailItem field={{ type: CellType.TEXT, name: gettext('Parent') }} value={parent} />
<DetailItem field={{ type: 'size', name: gettext('Size') }} value={repoInfo.size} />
<DetailItem field={{ type: CellType.CREATOR, name: gettext('Creator') }} value={repoInfo.owner_email} collaborators={[{
name: repoInfo.owner_name,
@@ -21,8 +21,8 @@ const DirDetails = ({ repoID, repoInfo, dirent, direntType, path, direntDetail }
avatar_url: repoInfo.owner_avatar,
}]} />
<DetailItem field={{ type: CellType.MTIME, name: gettext('Last modified time') }} value={direntDetail.mtime} />
{direntDetail.permission === 'rw' && window.app.pageOptions.enableMetadataManagement && (
<EditMetadata repoID={repoID} direntPath={direntPath} direntType={direntType} direntDetail={direntDetail} />
{window.app.pageOptions.enableMetadataManagement && (
<MetadataDetails repoID={repoID} filePath={direntPath} direntType="dir" { ...params } />
)}
</>
);
@@ -32,7 +32,6 @@ DirDetails.propTypes = {
repoID: PropTypes.string,
repoInfo: PropTypes.object,
dirent: PropTypes.object,
direntType: PropTypes.string,
path: PropTypes.string,
direntDetail: PropTypes.object,
};

View File

@@ -1,31 +0,0 @@
.detail-edit-metadata-btn {
height: 34px;
width: fit-content;
max-width: 100%;
padding: 0 6px;
display: flex;
align-items: center;
overflow: hidden;
}
.detail-edit-metadata-btn .seafile-multicolor-icon {
margin-right: 6px;
flex-shrink: 0;
font-size: 14px;
fill: #999;
}
.detail-edit-metadata-btn:hover {
background-color: #F5F5F5;
border-radius: 3px;
cursor: pointer;
}
.detail-edit-metadata-btn .detail-edit-metadata-btn-title {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: #666;
font-size: 14px;
}

View File

@@ -1,41 +0,0 @@
import React, { useCallback, useState } from 'react';
import PropTypes from 'prop-types';
import ExtraMetadataAttributesDialog from '../../../dialog/extra-metadata-attributes-dialog';
import { gettext } from '../../../../utils/constants';
import Icon from '../../../icon';
import './index.css';
const EditMetadata = ({ repoID, direntPath, direntType, direntDetail }) => {
const [isShowDialog, setShowDialog] = useState(false);
const onToggle = useCallback(() => {
setShowDialog(!isShowDialog);
}, [isShowDialog]);
return (
<>
<div className="detail-edit-metadata-btn" onClick={onToggle}>
<Icon symbol="add-table" />
<span className="detail-edit-metadata-btn-title">{gettext('Edit metadata properties')}</span>
</div>
{isShowDialog && (
<ExtraMetadataAttributesDialog
repoID={repoID}
filePath={direntPath}
direntType={direntType}
direntDetail={direntDetail}
onToggle={onToggle}
/>
)}
</>
);
};
EditMetadata.propTypes = {
repoID: PropTypes.string,
direntPath: PropTypes.string,
direntType: PropTypes.string,
direntDetail: PropTypes.object,
};
export default EditMetadata;

View File

@@ -1,19 +1,20 @@
import React, { useCallback, useMemo, useState } from 'react';
import PropTypes from 'prop-types';
import { v4 as uuidV4 } from 'uuid';
import { getDirentPath, getDirentPosition } from './utils';
import { getDirentPath, getFileParent } from './utils';
import DetailItem from '../detail-item';
import { CellType } from '../../../metadata/metadata-view/_basic';
import { gettext } from '../../../utils/constants';
import EditMetadata from './edit-metadata';
import EditFileTagPopover from '../../popover/edit-filetag-popover';
import FileTagList from '../../file-tag-list';
import { Utils } from '../../../utils/utils';
import { MetadataDetails } from '../../../metadata';
import ObjectUtils from '../../../metadata/metadata-view/utils/object-utils';
const FileDetails = ({ repoID, repoInfo, dirent, direntType, path, direntDetail, onFileTagChanged, repoTags, fileTagList }) => {
const FileDetails = React.memo(({ repoID, repoInfo, dirent, path, direntDetail, onFileTagChanged, repoTags, fileTagList, ...params }) => {
const [isEditFileTagShow, setEditFileTagShow] = useState(false);
const position = useMemo(() => getDirentPosition(repoInfo, dirent, path), [repoInfo, dirent, path]);
const parent = useMemo(() => getFileParent(repoInfo, dirent, path), [repoInfo, dirent, path]);
const direntPath = useMemo(() => getDirentPath(dirent, path), [dirent, path]);
const tagListTitleID = useMemo(() => `detail-list-view-tags-${uuidV4()}`, []);
@@ -27,24 +28,26 @@ const FileDetails = ({ repoID, repoInfo, dirent, direntType, path, direntDetail,
return (
<>
<DetailItem field={{ type: CellType.TEXT, name: gettext('File location') }} value={position} />
<DetailItem field={{ type: CellType.TEXT, name: gettext('Parent') }} value={parent} />
<DetailItem field={{ type: 'size', name: gettext('Size') }} value={Utils.bytesToSize(direntDetail.size)} />
<DetailItem field={{ type: CellType.CREATOR, name: gettext('Creator') }} value={direntDetail.last_modifier_email} collaborators={[{
<DetailItem field={{ type: CellType.LAST_MODIFIER, name: gettext('Last modifier') }} value={direntDetail.last_modifier_email} collaborators={[{
name: direntDetail.last_modifier_name,
contact_email: direntDetail.last_modifier_contact_email,
email: direntDetail.last_modifier_email,
avatar_url: direntDetail.last_modifier_avatar,
}]} />
<DetailItem field={{ type: CellType.MTIME, name: gettext('Last modified time') }} value={direntDetail.last_modified} />
<DetailItem field={{ type: CellType.SINGLE_SELECT, name: gettext('Tags') }} valueId={tagListTitleID} valueClick={onEditFileTagToggle} >
{Array.isArray(fileTagList) && fileTagList.length > 0 ? (
<FileTagList fileTagList={fileTagList} />
) : (
<span className="empty-tip-text">{gettext('Empty')}</span>
)}
</DetailItem>
{direntDetail.permission === 'rw' && window.app.pageOptions.enableMetadataManagement && (
<EditMetadata repoID={repoID} direntPath={direntPath} direntType={direntType} direntDetail={direntDetail} />
{!window.app.pageOptions.enableMetadataManagement && (
<DetailItem field={{ type: CellType.SINGLE_SELECT, name: gettext('Tags') }} valueId={tagListTitleID} valueClick={onEditFileTagToggle} >
{Array.isArray(fileTagList) && fileTagList.length > 0 ? (
<FileTagList fileTagList={fileTagList} />
) : (
<span className="empty-tip-text">{gettext('Empty')}</span>
)}
</DetailItem>
)}
{window.app.pageOptions.enableMetadataManagement && (
<MetadataDetails repoID={repoID} filePath={direntPath} direntType="file" { ...params } />
)}
{isEditFileTagShow &&
<EditFileTagPopover
@@ -59,13 +62,22 @@ const FileDetails = ({ repoID, repoInfo, dirent, direntType, path, direntDetail,
}
</>
);
};
}, (props, nextProps) => {
const { repoID, repoInfo, dirent, path, direntDetail } = props;
const isChanged = (
repoID !== nextProps.repoID ||
path !== nextProps.path ||
!ObjectUtils.isSameObject(repoInfo, nextProps.repoInfo) ||
!ObjectUtils.isSameObject(dirent, nextProps.dirent) ||
!ObjectUtils.isSameObject(direntDetail, nextProps.direntDetail)
);
return !isChanged;
});
FileDetails.propTypes = {
repoID: PropTypes.string,
repoInfo: PropTypes.object,
dirent: PropTypes.object,
direntType: PropTypes.string,
path: PropTypes.string,
direntDetail: PropTypes.object,
onFileTagChanged: PropTypes.func,

View File

@@ -25,5 +25,5 @@
}
.detail-container .empty-tip-text {
color: #666
color: #666;
}

View File

@@ -1,6 +1,6 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import { siteRoot } from '../../../utils/constants';
import { siteRoot, mediaUrl } from '../../../utils/constants';
import { seafileAPI } from '../../../utils/seafile-api';
import { Utils } from '../../../utils/utils';
import toaster from '../../toast';
@@ -8,112 +8,157 @@ import Dirent from '../../../models/dirent';
import Header from '../header';
import DirDetails from './dir-details';
import FileDetails from './file-details';
import ObjectUtils from '../../../metadata/metadata-view/utils/object-utils';
import metadataAPI from '../../../metadata/api';
import { User } from '../../../metadata/metadata-view/model';
import { UserService } from '../../../metadata/metadata-view/_basic';
import './index.css';
const DirentDetails = ({ dirent, path, repoID, currentRepoInfo, repoTags, fileTags, onItemDetailsClose, onFileTagChanged }) => {
const [direntType, setDirentType] = useState('');
const [direntDetail, setDirentDetail] = useState('');
const [folderDirent, setFolderDirent] = useState(null);
const direntRef = useRef(null);
class DirentDetails extends React.Component {
const updateDetailView = useCallback((repoID, dirent, direntPath) => {
constructor(props) {
super(props);
this.state = {
direntDetail: '',
dirent: null,
collaborators: [],
collaboratorsCache: {},
};
this.userService = new UserService({ mediaUrl, api: metadataAPI.listUserInfo });
}
updateCollaboratorsCache = (user) => {
const newCollaboratorsCache = { ...this.state.collaboratorsCache, [user.email]: user };
this.setState({ collaboratorsCache: newCollaboratorsCache });
};
loadCollaborators = () => {
metadataAPI.getCollaborators(this.props.repoID).then(res => {
const collaborators = Array.isArray(res?.data?.user_list) ? res.data.user_list.map(user => new User(user)) : [];
this.setState({ collaborators });
}).catch(error => {
this.setState({ collaborators: [] });
});
};
updateDetail = (repoID, dirent, direntPath) => {
const apiName = dirent.type === 'file' ? 'getFileInfo' : 'getDirInfo';
seafileAPI[apiName](repoID, direntPath).then(res => {
setDirentType(dirent.type === 'file' ? 'file' : 'dir');
setDirentDetail(res.data);
this.setState(({ direntDetail: res.data, dirent }));
}).catch(error => {
const errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
}, []);
};
useEffect(() => {
if (direntRef.current && dirent === direntRef.current) return;
direntRef.current = dirent;
loadDetail = (repoID, dirent, path) => {
if (dirent) {
const direntPath = Utils.joinPath(path, dirent.name);
updateDetailView(repoID, dirent, direntPath);
this.updateDetail(repoID, dirent, direntPath);
return;
}
const dirPath = Utils.getDirName(path);
seafileAPI.listDir(repoID, dirPath).then(res => {
const direntList = res.data.dirent_list;
let folderDirent = null;
for (let i = 0; i < direntList.length; i++) {
let dirent = direntList[i];
if (dirent.parent_dir + dirent.name === path) {
folderDirent = new Dirent(dirent);
break;
}
let folderDirent = direntList.find(item => item.parent_dir + item.name === path) || null;
if (folderDirent) {
folderDirent = new Dirent(folderDirent);
}
setFolderDirent(folderDirent);
updateDetailView(repoID, folderDirent, path);
this.updateDetail(repoID, folderDirent, path);
}).catch(error => {
const errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dirent, path, repoID]);
};
if (!dirent && !folderDirent) return '';
const direntName = dirent ? dirent.name : folderDirent.name;
const smallIconUrl = dirent ? Utils.getDirentIcon(dirent) : Utils.getDirentIcon(folderDirent);
// let bigIconUrl = dirent ? Utils.getDirentIcon(dirent, true) : Utils.getDirentIcon(folderDirent, true);
let bigIconUrl = '';
const isImg = dirent ? Utils.imageCheck(dirent.name) : Utils.imageCheck(folderDirent.name);
// const isVideo = dirent ? Utils.videoCheck(dirent.name) : Utils.videoCheck(folderDirent.name);
if (isImg) {
bigIconUrl = `${siteRoot}thumbnail/${repoID}/1024` + Utils.encodePath(`${path === '/' ? '' : path}/${dirent.name}`);
componentDidMount() {
this.loadCollaborators();
this.loadDetail(this.props.repoID, this.props.dirent, this.props.path);
}
return (
<div className="detail-container">
<Header title={direntName} icon={smallIconUrl} onClose={onItemDetailsClose} />
<div className="detail-body dirent-info">
{isImg && (
<div className="detail-image-thumbnail">
<img src={bigIconUrl} alt="" className="thumbnail" />
</div>
)}
{direntDetail && (
<div className="detail-content">
{direntType === 'dir' ? (
<DirDetails
repoID={repoID}
repoInfo={currentRepoInfo}
dirent={dirent || folderDirent}
direntType={direntType}
direntDetail={direntDetail}
path={path}
/>
) : (
<FileDetails
repoID={repoID}
repoInfo={currentRepoInfo}
dirent={dirent || folderDirent}
direntType={direntType}
path={path}
direntDetail={direntDetail}
repoTags={repoTags}
fileTagList={dirent ? dirent.file_tags : fileTags}
onFileTagChanged={onFileTagChanged}
/>
)}
</div>
)}
UNSAFE_componentWillReceiveProps(nextProps) {
const { dirent, path, repoID, currentRepoInfo, repoTags, fileTags } = this.props;
if (!ObjectUtils.isSameObject(currentRepoInfo, nextProps.currentRepoInfo) ||
!ObjectUtils.isSameObject(dirent, nextProps.dirent) ||
JSON.stringify(repoTags || []) !== JSON.stringify(nextProps.repoTags || []) ||
JSON.stringify(fileTags || []) !== JSON.stringify(nextProps.fileTags || []) ||
path !== nextProps.path ||
repoID !== nextProps.repoID) {
this.setState({ dirent: null }, () => {
this.loadDetail(nextProps.repoID, nextProps.dirent, nextProps.path);
});
}
}
render() {
const { dirent, direntDetail, collaborators, collaboratorsCache } = this.state;
if (!dirent) return null;
const { repoID, path, fileTags } = this.props;
const direntName = dirent.name;
const smallIconUrl = Utils.getDirentIcon(dirent);
// let bigIconUrl = Utils.getDirentIcon(dirent, true);
let bigIconUrl = '';
const isImg = Utils.imageCheck(dirent.name);
// const isVideo = Utils.videoCheck(dirent.name);
if (isImg) {
bigIconUrl = `${siteRoot}thumbnail/${repoID}/1024` + Utils.encodePath(`${path === '/' ? '' : path}/${dirent.name}`);
}
return (
<div className="detail-container">
<Header title={direntName} icon={smallIconUrl} onClose={this.props.onClose} />
<div className="detail-body dirent-info">
{isImg && (
<div className="detail-image-thumbnail">
<img src={bigIconUrl} alt="" className="thumbnail" />
</div>
)}
{direntDetail && (
<div className="detail-content">
{dirent.type !== 'file' ? (
<DirDetails
repoID={repoID}
repoInfo={this.props.currentRepoInfo}
dirent={dirent}
direntDetail={direntDetail}
path={path}
collaborators={collaborators}
collaboratorsCache={collaboratorsCache}
updateCollaboratorsCache={this.updateCollaboratorsCache}
queryUserAPI={this.userService?.queryUser}
/>
) : (
<FileDetails
repoID={repoID}
repoInfo={this.props.currentRepoInfo}
dirent={dirent}
path={path}
direntDetail={direntDetail}
repoTags={this.props.repoTags}
fileTagList={dirent ? dirent.file_tags : fileTags}
onFileTagChanged={this.props.onFileTagChanged}
collaborators={collaborators}
collaboratorsCache={collaboratorsCache}
updateCollaboratorsCache={this.updateCollaboratorsCache}
queryUserAPI={this.userService?.queryUser}
/>
)}
</div>
)}
</div>
</div>
</div>
);
};
);
}
}
DirentDetails.propTypes = {
repoID: PropTypes.string.isRequired,
dirent: PropTypes.object,
path: PropTypes.string.isRequired,
currentRepoInfo: PropTypes.object.isRequired,
onItemDetailsClose: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
onFileTagChanged: PropTypes.func.isRequired,
direntDetailPanelTab: PropTypes.string,
repoTags: PropTypes.array,
fileTags: PropTypes.array,
};

View File

@@ -2,10 +2,11 @@ import { Utils } from '../../../utils/utils';
export const getDirentPath = (dirent, path) => {
if (Utils.isMarkdownFile(path)) return path; // column mode: view file
if (dirent.type === 'dir') return path;
return Utils.joinPath(path, dirent.name);
};
export const getDirentPosition = (repoInfo, dirent, path) => {
export const getFileParent = (repoInfo, dirent, path) => {
const direntPath = getDirentPath(dirent, path);
const position = repoInfo.repo_name;
if (direntPath === '/') return position;

View File

@@ -4,7 +4,6 @@
align-items: center;
justify-content: space-between;
line-height: 2.5rem;
background-color: #f9f9f9;
border-bottom: 1px solid #e8e8e8;
height: 48px;
padding: 8px 16px;

View File

@@ -1,9 +1,49 @@
import React from 'react';
import PropTypes from 'prop-types';
import LibDetail from './lib-details';
import DirentDetail from './dirent-details';
import './index.css';
import ObjectUtils from '../../metadata/metadata-view/utils/object-utils';
export {
LibDetail,
DirentDetail,
const Index = React.memo(({ repoID, path, dirent, currentRepoInfo, repoTags, fileTags, onClose, onFileTagChanged }) => {
if (path === '/' && !dirent) {
return (
<LibDetail currentRepoInfo={currentRepoInfo} onClose={onClose} />
);
}
return (
<DirentDetail
repoID={repoID}
path={path}
dirent={dirent}
currentRepoInfo={currentRepoInfo}
repoTags={repoTags}
fileTags={fileTags}
onFileTagChanged={onFileTagChanged}
onClose={onClose}
/>
);
}, (props, nextProps) => {
const isChanged = props.repoID !== nextProps.repoID ||
props.path !== nextProps.path ||
!ObjectUtils.isSameObject(props.dirent, nextProps.dirent) ||
!ObjectUtils.isSameObject(props.currentRepoInfo, nextProps.currentRepoInfo) ||
JSON.stringify(props.repoTags || []) !== JSON.stringify(nextProps.repoTags || []) ||
JSON.stringify(props.fileTags || []) !== JSON.stringify(nextProps.fileTags || []);
return !isChanged;
});
Index.propTypes = {
repoID: PropTypes.string,
path: PropTypes.string,
dirent: PropTypes.object,
currentRepoInfo: PropTypes.object,
repoTags: PropTypes.array,
fileTags: PropTypes.array,
onClose: PropTypes.func,
onFileTagChanged: PropTypes.func,
};
export default Index;

View File

@@ -10,14 +10,14 @@ import Loading from '../../loading';
import DetailItem from '../detail-item';
import { CellType } from '../../../metadata/metadata-view/_basic';
const LibDetail = React.memo(({ currentRepo, closeDetails }) => {
const LibDetail = React.memo(({ currentRepoInfo, onClose }) => {
const [isLoading, setLoading] = useState(true);
const [repo, setRepo] = useState({});
const smallIconUrl = useMemo(() => Utils.getLibIconUrl(currentRepo), [currentRepo]);
const smallIconUrl = useMemo(() => Utils.getLibIconUrl(currentRepoInfo), [currentRepoInfo]);
useEffect(() => {
setLoading(true);
seafileAPI.getRepoInfo(currentRepo.repo_id).then(res => {
seafileAPI.getRepoInfo(currentRepoInfo.repo_id).then(res => {
const repo = new Repo(res.data);
setRepo(repo);
setLoading(false);
@@ -25,11 +25,11 @@ const LibDetail = React.memo(({ currentRepo, closeDetails }) => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
}, [currentRepo.repo_id]);
}, [currentRepoInfo.repo_id]);
return (
<div className="detail-container">
<Header title={currentRepo.repo_name} icon={smallIconUrl} onClose={closeDetails} />
<Header title={currentRepoInfo.repo_name} icon={smallIconUrl} onClose={onClose} />
<div className="detail-body dirent-info">
{isLoading ? (
<div className="w-100 h-100 d-flex algin-items-center justify-content-center"><Loading /></div>
@@ -55,8 +55,8 @@ const LibDetail = React.memo(({ currentRepo, closeDetails }) => {
});
LibDetail.propTypes = {
currentRepo: PropTypes.object.isRequired,
closeDetails: PropTypes.func.isRequired,
currentRepoInfo: PropTypes.object.isRequired,
onClose: PropTypes.func.isRequired,
};
export default LibDetail;

View File

@@ -9,7 +9,7 @@ import Icon from '../icon';
import { gettext, siteRoot, username } from '../../utils/constants';
import SearchResultItem from './search-result-item';
import SearchResultLibrary from './search-result-library';
import { isMac } from '../../utils/extra-attributes';
import { Utils } from '../../utils/utils';
import Loading from '../loading';
const INDEX_STATE = {
@@ -19,7 +19,7 @@ const INDEX_STATE = {
};
const PER_PAGE = 10;
const controlKey = isMac() ? '⌘' : 'Ctrl';
const controlKey = Utils.isMac() ? '⌘' : 'Ctrl';
export default class AISearch extends Component {

View File

@@ -9,7 +9,6 @@ import { gettext, siteRoot } from '../../utils/constants';
import SearchResultItem from './search-result-item';
import SearchResultLibrary from './search-result-library';
import { Utils } from '../../utils/utils';
import { isMac } from '../../utils/extra-attributes';
import toaster from '../toast';
import Loading from '../loading';
@@ -23,7 +22,7 @@ const propTypes = {
};
const PER_PAGE = 10;
const controlKey = isMac() ? '⌘' : 'Ctrl';
const controlKey = Utils.isMac() ? '⌘' : 'Ctrl';
const isEnter = isHotkey('enter');
const isUp = isHotkey('up');