mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-13 05:39:59 +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:
@@ -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;
|
@@ -1,7 +0,0 @@
|
||||
.extra-attributes-dialog .column-name {
|
||||
padding-top: 9px;
|
||||
}
|
||||
|
||||
.extra-attributes-dialog .column-item {
|
||||
min-height: 56px;
|
||||
}
|
@@ -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;
|
@@ -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;
|
@@ -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;
|
@@ -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;
|
@@ -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;
|
@@ -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;
|
@@ -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;
|
@@ -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;
|
@@ -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;
|
||||
}
|
||||
|
@@ -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;
|
@@ -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;
|
@@ -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;
|
||||
}
|
@@ -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;
|
@@ -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;
|
||||
}
|
||||
|
@@ -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,
|
||||
};
|
||||
|
@@ -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">
|
||||
|
@@ -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,
|
||||
};
|
||||
|
@@ -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;
|
||||
}
|
@@ -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;
|
@@ -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,
|
||||
|
@@ -25,5 +25,5 @@
|
||||
}
|
||||
|
||||
.detail-container .empty-tip-text {
|
||||
color: #666
|
||||
color: #666;
|
||||
}
|
||||
|
@@ -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,
|
||||
};
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
|
@@ -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 {
|
||||
|
||||
|
@@ -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');
|
||||
|
@@ -3,109 +3,6 @@ import KeyCodes from './keyCodes';
|
||||
|
||||
export const DIALOG_MAX_HEIGHT = window.innerHeight - 56; // Dialog margin is 3.5rem (56px)
|
||||
|
||||
export const EXTRA_ATTRIBUTES_COLUMN_TYPE = {
|
||||
TEXT: 'text',
|
||||
NUMBER: 'number',
|
||||
DATE: 'date',
|
||||
FORMULA: 'formula',
|
||||
SINGLE_SELECT: 'single-select',
|
||||
CTIME: 'ctime',
|
||||
MTIME: 'mtime'
|
||||
};
|
||||
|
||||
export const EXTRA_ATTRIBUTES_NOT_DISPLAY_COLUMN_KEY = [
|
||||
'_id',
|
||||
'_locked',
|
||||
'_locked_by',
|
||||
'_archived',
|
||||
'_creator',
|
||||
'_last_modifier',
|
||||
'_ctime',
|
||||
'_mtime',
|
||||
];
|
||||
|
||||
export const EXTRA_ATTRIBUTES_NOT_DISPLAY_COLUMN_NAME = [
|
||||
'Repo ID',
|
||||
'UUID',
|
||||
];
|
||||
|
||||
export const FORMULA_RESULT_TYPE = {
|
||||
NUMBER: 'number',
|
||||
STRING: 'string',
|
||||
DATE: 'date',
|
||||
BOOL: 'bool',
|
||||
ARRAY: 'array',
|
||||
};
|
||||
|
||||
export const DELETED_OPTION_BACKGROUND_COLOR = '#eaeaea';
|
||||
|
||||
export const DELETED_OPTION_TIPS = 'Deleted option';
|
||||
|
||||
export const DEFAULT_NUMBER_FORMAT = 'number';
|
||||
|
||||
export const ERROR = 'ERROR';
|
||||
export const ERROR_DIV_ZERO = 'DIV/0';
|
||||
export const ERROR_NAME = 'NAME';
|
||||
export const ERROR_NOT_AVAILABLE = 'N/A';
|
||||
export const ERROR_NULL = 'NULL';
|
||||
export const ERROR_NUM = 'NUM';
|
||||
export const ERROR_REF = 'REF';
|
||||
export const ERROR_VALUE = 'VALUE';
|
||||
export const GETTING_DATA = 'GETTING_DATA';
|
||||
|
||||
const errors = {
|
||||
[ERROR]: '#ERROR!',
|
||||
[ERROR_DIV_ZERO]: '#DIV/0!',
|
||||
[ERROR_NAME]: '#NAME?',
|
||||
[ERROR_NOT_AVAILABLE]: '#N/A',
|
||||
[ERROR_NULL]: '#NULL!',
|
||||
[ERROR_NUM]: '#NUM!',
|
||||
[ERROR_REF]: '#REF!',
|
||||
[ERROR_VALUE]: '#VALUE!',
|
||||
[GETTING_DATA]: '#GETTING_DATA',
|
||||
};
|
||||
|
||||
export const DISPLAY_INTERNAL_ERRORS = [
|
||||
errors[ERROR],
|
||||
errors[ERROR_DIV_ZERO],
|
||||
errors[ERROR_NAME],
|
||||
errors[ERROR_NOT_AVAILABLE],
|
||||
errors[ERROR_NULL],
|
||||
errors[ERROR_NUM],
|
||||
errors[ERROR_REF],
|
||||
errors[ERROR_VALUE],
|
||||
errors[GETTING_DATA],
|
||||
];
|
||||
|
||||
export const DURATION_FORMATS_MAP = {
|
||||
H_MM: 'h:mm',
|
||||
H_MM_SS: 'h:mm:ss',
|
||||
H_MM_SS_S: 'h:mm:ss.s',
|
||||
H_MM_SS_SS: 'h:mm:ss.ss',
|
||||
H_MM_SS_SSS: 'h:mm:ss.sss'
|
||||
};
|
||||
|
||||
export const DURATION_FORMATS = [
|
||||
{ name: DURATION_FORMATS_MAP.H_MM, type: DURATION_FORMATS_MAP.H_MM },
|
||||
{ name: DURATION_FORMATS_MAP.H_MM_SS, type: DURATION_FORMATS_MAP.H_MM_SS }
|
||||
];
|
||||
|
||||
export const DURATION_ZERO_DISPLAY = {
|
||||
[DURATION_FORMATS_MAP.H_MM]: '0:00',
|
||||
[DURATION_FORMATS_MAP.H_MM_SS]: '0:00',
|
||||
[DURATION_FORMATS_MAP.H_MM_SS_S]: '0:00.0',
|
||||
[DURATION_FORMATS_MAP.H_MM_SS_SS]: '0:00.00',
|
||||
[DURATION_FORMATS_MAP.H_MM_SS_SSS]: '0:00.000',
|
||||
};
|
||||
|
||||
export const DURATION_DECIMAL_DIGITS = {
|
||||
[DURATION_FORMATS_MAP.H_MM]: 0,
|
||||
[DURATION_FORMATS_MAP.H_MM_SS]: 0,
|
||||
[DURATION_FORMATS_MAP.H_MM_SS_S]: 1,
|
||||
[DURATION_FORMATS_MAP.H_MM_SS_SS]: 2,
|
||||
[DURATION_FORMATS_MAP.H_MM_SS_SSS]: 3,
|
||||
};
|
||||
|
||||
export const PRIVATE_FILE_TYPE = {
|
||||
FILE_EXTENDED_PROPERTIES: '__file_extended_properties'
|
||||
};
|
||||
|
@@ -1,9 +1,11 @@
|
||||
import SeafileMetadata from './metadata-view';
|
||||
import MetadataStatusManagementDialog from './metadata-status-manage-dialog';
|
||||
import MetadataTreeView from './metadata-tree-view';
|
||||
import MetadataDetails from './metadata-details';
|
||||
|
||||
export {
|
||||
SeafileMetadata,
|
||||
MetadataStatusManagementDialog,
|
||||
MetadataTreeView,
|
||||
MetadataDetails,
|
||||
};
|
||||
|
22
frontend/src/metadata/metadata-details/constants.js
Normal file
22
frontend/src/metadata/metadata-details/constants.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import { PRIVATE_COLUMN_KEY } from '../metadata-view/_basic';
|
||||
|
||||
export const NOT_DISPLAY_COLUMN_KEYS = [
|
||||
PRIVATE_COLUMN_KEY.ID,
|
||||
PRIVATE_COLUMN_KEY.CTIME,
|
||||
PRIVATE_COLUMN_KEY.MTIME,
|
||||
PRIVATE_COLUMN_KEY.CREATOR,
|
||||
PRIVATE_COLUMN_KEY.LAST_MODIFIER,
|
||||
PRIVATE_COLUMN_KEY.FILE_CREATOR,
|
||||
PRIVATE_COLUMN_KEY.FILE_CTIME,
|
||||
PRIVATE_COLUMN_KEY.FILE_MODIFIER,
|
||||
PRIVATE_COLUMN_KEY.FILE_MTIME,
|
||||
PRIVATE_COLUMN_KEY.PARENT_DIR,
|
||||
PRIVATE_COLUMN_KEY.FILE_NAME,
|
||||
PRIVATE_COLUMN_KEY.IS_DIR,
|
||||
PRIVATE_COLUMN_KEY.FILE_TYPE,
|
||||
PRIVATE_COLUMN_KEY.LOCATION,
|
||||
];
|
||||
|
||||
export {
|
||||
PRIVATE_COLUMN_KEY,
|
||||
};
|
41
frontend/src/metadata/metadata-details/index.css
Normal file
41
frontend/src/metadata/metadata-details/index.css
Normal file
@@ -0,0 +1,41 @@
|
||||
.dirent-detail-item .sf-metadata-checkbox-formatter {
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 3px;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dirent-detail-item .sf-metadata-checkbox-formatter .sf-metadata-icon-check-mark {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.dirent-detail-item .sf-metadata-single-select-formatter {
|
||||
height: 20px;
|
||||
line-height: 1 !important;
|
||||
}
|
||||
|
||||
.dirent-detail-item .sf-metadata-ui.collaborator-item .collaborator-avatar {
|
||||
margin-left: 1px;
|
||||
}
|
||||
|
||||
.dirent-detail-item .sf-metadata-collaborator-formatter .sf-metadata-ui.collaborator-item {
|
||||
margin-bottom: 2px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.dirent-detail-item .sf-metadata-record-checkbox-cell-empty {
|
||||
line-height: 1;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.dirent-detail-item .sf-metadata-record-checkbox-cell-empty::before {
|
||||
content: '';
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 3px;
|
||||
line-height: 1;
|
||||
}
|
53
frontend/src/metadata/metadata-details/index.js
Normal file
53
frontend/src/metadata/metadata-details/index.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Utils } from '../../utils/utils';
|
||||
import metadataAPI from '../api';
|
||||
import Column from '../metadata-view/model/metadata/column';
|
||||
import { normalizeFields, getCellValueByColumn } from './utils';
|
||||
import DetailItem from '../../components/dirent-detail/detail-item';
|
||||
import toaster from '../../components/toast';
|
||||
|
||||
import './index.css';
|
||||
|
||||
const MetadataDetails = ({ repoID, filePath, direntType, emptyTip, ...params }) => {
|
||||
const [isLoading, setLoading] = useState(true);
|
||||
const [metadata, setMetadata] = useState({ record: {}, fields: [] });
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
const dirName = Utils.getDirName(filePath);
|
||||
const 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 { results, metadata } = res.data;
|
||||
const record = Array.isArray(results) && results.length > 0 ? results[0] : {};
|
||||
const fields = normalizeFields(metadata).map(field => new Column(field));
|
||||
setMetadata({ record, fields });
|
||||
setLoading(false);
|
||||
}).catch(error => {
|
||||
const errMessage = Utils.getErrorMsg(error);
|
||||
toaster.danger(errMessage);
|
||||
setLoading(false);
|
||||
});
|
||||
}, [repoID, filePath, direntType]);
|
||||
|
||||
if (isLoading) return null;
|
||||
const { fields, record } = metadata;
|
||||
if (!record._id) return null;
|
||||
return fields.map(field => {
|
||||
const value = getCellValueByColumn(record, field);
|
||||
return (<DetailItem key={field.key} field={field} value={value} emptyTip={emptyTip} { ...params } />);
|
||||
});
|
||||
};
|
||||
|
||||
MetadataDetails.propTypes = {
|
||||
repoID: PropTypes.string,
|
||||
filePath: PropTypes.string,
|
||||
direntType: PropTypes.string,
|
||||
direntDetail: PropTypes.object,
|
||||
};
|
||||
|
||||
export default MetadataDetails;
|
25
frontend/src/metadata/metadata-details/utils.js
Normal file
25
frontend/src/metadata/metadata-details/utils.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import { getColumnType } from '../metadata-view/utils/column-utils';
|
||||
import { getCellValueByColumn } from '../metadata-view/_basic';
|
||||
import { NOT_DISPLAY_COLUMN_KEYS } from './constants';
|
||||
|
||||
export const normalizeFields = (fields) => {
|
||||
if (!Array.isArray(fields) || fields.length === 0) return [];
|
||||
const validFields = fields.map((field) => {
|
||||
const { type, key, ...params } = field;
|
||||
return {
|
||||
...params,
|
||||
key,
|
||||
type: getColumnType(key, type),
|
||||
width: 200,
|
||||
};
|
||||
}).filter(field => !NOT_DISPLAY_COLUMN_KEYS.includes(field.key));
|
||||
let displayFields = [];
|
||||
validFields.forEach(field => {
|
||||
displayFields.push(field);
|
||||
});
|
||||
return displayFields;
|
||||
};
|
||||
|
||||
export {
|
||||
getCellValueByColumn,
|
||||
};
|
@@ -1,9 +1,7 @@
|
||||
import DeleteConfirmDialog from './delete-confirm-dialog';
|
||||
import RecordDetailsDialog from './record-details-dialog';
|
||||
import Table from './table';
|
||||
|
||||
export {
|
||||
DeleteConfirmDialog,
|
||||
RecordDetailsDialog,
|
||||
Table,
|
||||
};
|
||||
|
@@ -1,22 +0,0 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import RecordDetails from './record-details';
|
||||
import { useMetadata, useRecordDetails } from '../../hooks';
|
||||
|
||||
const RecordDetailsDialog = () => {
|
||||
const { isShowRecordDetails, recordDetails, closeRecordDetails } = useRecordDetails();
|
||||
const { metadata } = useMetadata();
|
||||
const fields = useMemo(() => {
|
||||
const { columns, hidden_columns } = metadata.view;
|
||||
return columns.filter(column => !hidden_columns.includes(column.key));
|
||||
}, [metadata]);
|
||||
if (!isShowRecordDetails) return null;
|
||||
|
||||
const props = {
|
||||
record: recordDetails,
|
||||
fields: fields,
|
||||
onToggle: closeRecordDetails,
|
||||
};
|
||||
return (<RecordDetails { ...props } />);
|
||||
};
|
||||
|
||||
export default RecordDetailsDialog;
|
@@ -1,31 +0,0 @@
|
||||
.sf-metadata-record-details-item .sf-metadata-record-details-item-label {
|
||||
padding-top: 9px;
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.sf-metadata-record-details-item .sf-metadata-record-details-item-label .header-icon {
|
||||
display: inline-block;
|
||||
margin-left: -.3125rem;
|
||||
padding: 0 .3125rem;
|
||||
}
|
||||
|
||||
.sf-metadata-record-details-item .sf-metadata-record-details-item-label .field-description-section .header-icon .sf-metadata-icon {
|
||||
color: #212529a6;
|
||||
cursor: default;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.sf-metadata-record-details-item .sf-metadata-record-details-item-label .field-description-section .field-description-section-field-name {
|
||||
color: #212529b3;
|
||||
}
|
||||
|
||||
.sf-metadata-record-details-item .sf-metadata-record-details-item-label .field-description-section .field-uneditable-tip {
|
||||
color: #bdbdbd;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sf-metadata-record-details-item .sf-metadata-record-details-item-label .field-description-section .field-uneditable-tip:hover {
|
||||
color: #666;
|
||||
}
|
@@ -1,42 +0,0 @@
|
||||
import React, { useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { UncontrolledTooltip, Col } from 'reactstrap';
|
||||
import { IconBtn, Icon } from '@seafile/sf-metadata-ui-component';
|
||||
import { COLUMNS_ICON_CONFIG } from '../../../../_basic';
|
||||
|
||||
import './index.css';
|
||||
|
||||
const FieldLabel = ({ field }) => {
|
||||
const { type, name, description, key } = field;
|
||||
const iconRef = useRef(null);
|
||||
return (
|
||||
<Col md={3} className="d-flex sf-metadata-record-details-item-label">
|
||||
<div className="d-flex justify-content-between">
|
||||
<div className="field-description-section">
|
||||
<span className="header-icon" id={`header-icon-${key}`}>
|
||||
<Icon iconName={COLUMNS_ICON_CONFIG[type]} />
|
||||
</span>
|
||||
<span className="field-description-section-field-name">{name || ''}</span>
|
||||
{description &&
|
||||
<>
|
||||
<IconBtn ref={iconRef} iconName="description" className="field-uneditable-tip ml-2" />
|
||||
{iconRef.current && (
|
||||
<UncontrolledTooltip target={iconRef.current} fade={false} placement="right" modifiers={{ preventOverflow: { boundariesElement: document.body } }}>
|
||||
{description}
|
||||
</UncontrolledTooltip>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
FieldLabel.propTypes = {
|
||||
field: PropTypes.object,
|
||||
fieldIconConfig: PropTypes.object,
|
||||
};
|
||||
|
||||
export default FieldLabel;
|
@@ -1,318 +0,0 @@
|
||||
.sf-metadata-record-details-dialog {
|
||||
margin: 28px 0 0 0;
|
||||
}
|
||||
|
||||
.sf-metadata-record-details-dialog .modal-header {
|
||||
padding: 6px 14px 6px 20px;
|
||||
}
|
||||
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-content {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.sf-metadata-record-details-dialog .modal-content {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sf-metadata-record-details-dialog .header-close-list {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.sf-metadata-record-details-dialog .header-close-list .sf-metadata-icon-btn .sf-metadata-icon {
|
||||
color: #000;
|
||||
opacity: 0.5;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.sf-metadata-record-details-dialog .header-close-list .sf-metadata-icon-btn:hover .sf-metadata-icon {
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-container {
|
||||
width: 100%;
|
||||
height: calc(100% - 37px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: scroll;
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-container .sf-metadata-record-details-item {
|
||||
min-height: 56px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-container .sf-metadata-record-details-item:first-child {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-container .sf-metadata-ui.collaborator-item .collaborator-avatar {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .sf-metadata-record-cell-empty,
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .sf-metadata-text-formatter,
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .sf-metadata-single-select-formatter,
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .sf-metadata-multiple-select-formatter,
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .multiple-select-formatter,
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .sf-metadata-collaborator-formatter,
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .sf-metadata-date-formatter,
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .sf-metadata-url-formatter,
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .sf-metadata-email-formatter,
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .sf-metadata-ctime-formatter,
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .sf-metadata-mtime-formatter,
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .sf-metadata-number-formatter,
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .sf-metadata-formula-formatter,
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .sf-metadata-duration-formatter,
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .formula-formatter.multiple,
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .sf-metadata-link-formula-formatter,
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .sf-metadata-rate-formatter,
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .sf-metadata-link-formatter {
|
||||
background-color: #f8f9fa;
|
||||
cursor: default;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 2.375rem;
|
||||
padding: .375rem .75rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
color: #212529;
|
||||
background-clip: padding-box;
|
||||
border: 1px solid rgba(0, 40, 100, .12);
|
||||
border-radius: 3px;
|
||||
transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out;
|
||||
}
|
||||
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .sf-metadata-text-formatter,
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .sf-metadata-url-formatter,
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .sf-metadata-formula-formatter,
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .sf-metadata-email-formatter,
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .sf-metadata-rate-formatter {
|
||||
line-height: 24px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .sf-metadata-single-select-formatter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .sf-metadata-collaborator-formatter,
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .sf-metadata-multiple-select-formatter,
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .multiple-select-formatter,
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .formula-formatter.multiple,
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .sf-metadata-link-formatter {
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
height: fit-content;
|
||||
min-height: 38px;
|
||||
}
|
||||
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .sf-metadata-multiple-select-formatter .select-item,
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .multiple-select-formatter .select-item,
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .formula-formatter.multiple .formula-formatter-content-item {
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .sf-metadata-collaborator-formatter .collaborator-item,
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .sf-metadata-link-formatter .sf-metadata-link-item {
|
||||
margin: 5px 10px 5px 0;
|
||||
}
|
||||
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .sf-metadata-link-formatter .sf-metadata-link-item {
|
||||
height: 20px;
|
||||
max-width: 100%;
|
||||
padding: 0 8px;
|
||||
background: #eceff4;
|
||||
border-radius: 3px;
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .sf-metadata-date-formatter,
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .sf-metadata-ctime-formatter,
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .sf-metadata-mtime-formatter,
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .sf-metadata-number-formatter,
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .sf-metadata-formula-number-formatter,
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .sf-metadata-duration-formatter,
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .sf-metadata-formula-date-formatter,
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .sf-metadata-rate-formatter {
|
||||
width: 320px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .sf-metadata-rate-formatter .sf-metadata-icon {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .sf-metadata-checkbox-formatter {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .sf-metadata-record-cell-empty.sf-metadata-record-file-cell-empty,
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .sf-metadata-record-cell-empty.sf-metadata-record-image-cell-empty,
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .sf-metadata-record-creator-cell-empty,
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .sf-metadata-record-link-cell-empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* long text */
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .sf-metadata-long-text-formatter {
|
||||
background-color: #f8f9fa;
|
||||
cursor: default;
|
||||
min-height: 38px;
|
||||
border: 1px solid rgba(0, 40, 100, 0.12);
|
||||
border-radius: 3px;
|
||||
padding: 0 12px;
|
||||
width: 100%;
|
||||
overflow-x: hidden;
|
||||
text-overflow: unset;
|
||||
white-space: unset;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.sf-metadata-record-details-dialog .sf-metadata-long-text-formatter ol,
|
||||
.sf-metadata-record-details-dialog .sf-metadata-long-text-formatter ul {
|
||||
padding-inline-start: 40px;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.sf-metadata-record-details-dialog .sf-metadata-long-text-formatter ol li a,
|
||||
.sf-metadata-record-details-dialog .sf-metadata-long-text-formatter ul li a {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.sf-metadata-record-details-dialog .sf-metadata-long-text-formatter ul.contains-task-list,
|
||||
.sf-metadata-record-details-dialog .sf-metadata-long-text-formatter ol.contains-task-list {
|
||||
padding-inline-start: 20px;
|
||||
}
|
||||
|
||||
.sf-metadata-record-details-dialog .sf-metadata-long-text-formatter ul li.task-list-item,
|
||||
.sf-metadata-record-details-dialog .sf-metadata-long-text-formatter ol li.task-list-item {
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
.sf-metadata-record-details-dialog .sf-metadata-long-text-formatter li.task-list-item input[type=checkbox] {
|
||||
position: absolute;
|
||||
left: -1.4em;
|
||||
top: .4em;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.sf-metadata-record-details-dialog .sf-metadata-long-text-formatter thead tr {
|
||||
min-height: 42px;
|
||||
}
|
||||
|
||||
.sf-metadata-record-details-dialog .sf-metadata-long-text-formatter tbody tr {
|
||||
font-weight: normal;
|
||||
min-height: 42px;
|
||||
}
|
||||
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .sf-metadata-geolocation-formatter {
|
||||
min-width: 80px;
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
height: 28px;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
line-height: 24px;
|
||||
border: 2px solid transparent;
|
||||
padding: 0 10px;
|
||||
background-color: #f0f0f0;
|
||||
border-radius: 3px;
|
||||
font-size: 14px;
|
||||
color: #212529;
|
||||
}
|
||||
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .sf-metadata-formula-number-formatter {
|
||||
text-align: left !important;
|
||||
}
|
||||
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .sf-metadata-digital-sign-formatter {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-left: -5px;
|
||||
margin-right: -5px;
|
||||
}
|
||||
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .sf-metadata-creator-formatter,
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .sf-metadata-last-modifier-formatter,
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .sf-metadata-checkbox-formatter,
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .sf-metadata-image-formatter,
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .sf-metadata-file-formatter,
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .sf-metadata-geolocation-formatter,
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .sf-metadata-auto-number-formatter
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .sf-metadata-digital-sign-formatter {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .sf-metadata-image-formatter,
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .sf-metadata-file-formatter {
|
||||
flex-wrap: wrap;
|
||||
margin-left: -5px;
|
||||
margin-right: -5px;
|
||||
width: unset;
|
||||
}
|
||||
|
||||
/* image */
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .sf-metadata-record-image-item {
|
||||
height: 100px;
|
||||
width: 100px;
|
||||
border: 2px solid #ededed;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 5px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .sf-metadata-record-image-item:hover {
|
||||
border: 2px solid #c9c9c9;
|
||||
}
|
||||
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .sf-metadata-record-image-item .image-item,
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .sf-metadata-record-image-item .file-item-icon {
|
||||
display: block;
|
||||
width: 96px;
|
||||
height: unset;
|
||||
border: none;
|
||||
margin-right: 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .sf-metadata-record-image-item .image-item:hover {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .sf-metadata-digital-sign-formatter .sf-metadata-record-image-item {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.sf-metadata-record-details-dialog .sf-metadata-record-details-item .sf-metadata-record-checkbox-cell-empty {
|
||||
background-color: #fff;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 3px;
|
||||
padding: 0;
|
||||
margin-top: 8px;
|
||||
}
|
@@ -1,133 +0,0 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Modal, ModalHeader, ModalBody, Col } from 'reactstrap';
|
||||
import { IconBtn, CenteredLoading } from '@seafile/sf-metadata-ui-component';
|
||||
import { gettext } from '../../../utils';
|
||||
import FieldLabel from './field-label';
|
||||
import CellFormatter from '../../cell-formatter';
|
||||
import { getCellValueByColumn } from '../../../_basic';
|
||||
|
||||
import './index.css';
|
||||
|
||||
const RecordDetails = ({ fields, record, onToggle, ...params }) => {
|
||||
const [isAnimation, setAnimation] = useState(true);
|
||||
const [isLoading, setLoading] = useState(true);
|
||||
const modalRef = useRef(null);
|
||||
|
||||
const initStyle = useMemo(() => {
|
||||
const defaultMargin = 80; // sequence cell width
|
||||
const defaultHeight = 100;
|
||||
return {
|
||||
width: `${window.innerWidth - defaultMargin}px`,
|
||||
maxWidth: `${window.innerWidth - defaultMargin}px`,
|
||||
marginLeft: `${defaultMargin}px`,
|
||||
height: `${defaultHeight}px`,
|
||||
marginRight: `${defaultMargin}px`,
|
||||
marginTop: '30%',
|
||||
transition: 'all .3s',
|
||||
};
|
||||
}, []);
|
||||
|
||||
const style = useMemo(() => {
|
||||
const width = 800;
|
||||
return {
|
||||
width,
|
||||
maxWidth: width,
|
||||
marginLeft: (window.innerWidth - width) / 2,
|
||||
height: 'calc(100% - 56px)', // Dialog margin is 3.5rem (56px)
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// use setTimeout to make sure real dom rendered
|
||||
setTimeout(() => {
|
||||
let dom = modalRef.current.firstChild;
|
||||
const { width, maxWidth, marginLeft, height } = style;
|
||||
dom.style.width = `${width}px`;
|
||||
dom.style.maxWidth = `${maxWidth}px`;
|
||||
dom.style.marginLeft = `${marginLeft}px`;
|
||||
dom.style.height = height;
|
||||
dom.style.marginRight = 'unset';
|
||||
dom.style.marginTop = '28px';
|
||||
// after animation, change style and run callback
|
||||
setTimeout(() => {
|
||||
setAnimation(false);
|
||||
dom.style.transition = 'none';
|
||||
setLoading(false);
|
||||
}, 280);
|
||||
}, 1);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const toggle = useCallback(() => {
|
||||
onToggle();
|
||||
}, [onToggle]);
|
||||
|
||||
const renderFieldValue = useCallback((field) => {
|
||||
const cellValue = getCellValueByColumn(record, field);
|
||||
return (<CellFormatter field={field} value={cellValue} { ...params }/>);
|
||||
}, [record, params]);
|
||||
|
||||
const renderRowContent = useCallback(() => {
|
||||
if (isLoading) return (<CenteredLoading />);
|
||||
if (!Array.isArray(fields) || fields.length === 0) return null;
|
||||
return (
|
||||
<>
|
||||
{fields.map((field) => {
|
||||
return (
|
||||
<div className={`sf-metadata-record-details-item sf-metadata-record-details-item-field-${field.type}`} key={field.key}>
|
||||
<div className="pb-4 row">
|
||||
<FieldLabel field={field} />
|
||||
<Col md={9} className='d-flex align-items-center sf-metadata-record-details-item-col'>
|
||||
{renderFieldValue(field)}
|
||||
</Col>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}, [isLoading, fields, renderFieldValue]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={true}
|
||||
toggle={toggle}
|
||||
className="sf-metadata-record-details-dialog"
|
||||
style={isAnimation ? initStyle : style}
|
||||
zIndex={1048}
|
||||
contentClassName="sf-metadata-record-details-content"
|
||||
modalClassName="sf-metadata-record-details-modal"
|
||||
fade={false}
|
||||
innerRef={modalRef}
|
||||
keyboard={false}
|
||||
>
|
||||
{!isAnimation && (
|
||||
<div className="sf-metadata-record-details">
|
||||
<ModalHeader close={(
|
||||
<div className="header-close-list">
|
||||
<IconBtn iconName="close" size={24} onClick={toggle} />
|
||||
</div>
|
||||
)}>
|
||||
<div className="sf-metadata-record-details-left-btns">
|
||||
<div className="sf-metadata-record-details-title text-truncate">{gettext('Details')}</div>
|
||||
</div>
|
||||
</ModalHeader>
|
||||
<ModalBody className="sf-metadata-record-details-container">
|
||||
{renderRowContent()}
|
||||
</ModalBody>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
RecordDetails.propTypes = {
|
||||
record: PropTypes.object,
|
||||
fields: PropTypes.array,
|
||||
columns: PropTypes.array,
|
||||
onToggle: PropTypes.func,
|
||||
};
|
||||
|
||||
export default RecordDetails;
|
@@ -5,7 +5,6 @@ import { CommonlyUsedHotkey, getValidGroupbys } from '../../_basic';
|
||||
import { gettext } from '../../utils';
|
||||
import { useMetadata } from '../../hooks';
|
||||
import TableMain from './table-main';
|
||||
import RecordDetailsDialog from '../record-details-dialog';
|
||||
import { Utils } from '../../../../utils/utils';
|
||||
|
||||
import './index.css';
|
||||
@@ -202,7 +201,6 @@ const Container = () => {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<RecordDetailsDialog />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@@ -129,7 +129,6 @@
|
||||
}
|
||||
|
||||
.sf-metadata-result-table-content .sf-metadata-result-table-row.sf-metadata-last-table-row {
|
||||
height: 32px !important;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
|
@@ -4,7 +4,6 @@ import classnames from 'classnames';
|
||||
import Records from './records';
|
||||
import { GROUP_VIEW_OFFSET } from '../../../constants';
|
||||
import GridUtils from '../../../utils/grid-utils';
|
||||
import { useRecordDetails } from '../../../hooks';
|
||||
|
||||
import './index.css';
|
||||
|
||||
@@ -14,8 +13,6 @@ const TableMain = ({ metadata, modifyRecord, modifyRecords, loadMore, loadAll, s
|
||||
return new GridUtils(metadata, { modifyRecord, modifyRecords, recordGetterByIndex, recordGetterById });
|
||||
}, [metadata, modifyRecord, modifyRecords, recordGetterByIndex, recordGetterById]);
|
||||
|
||||
const { openRecordDetails } = useRecordDetails();
|
||||
|
||||
const groupbysCount = useMemo(() => {
|
||||
const groupbys = metadata?.view?.groupbys || [];
|
||||
return groupbys.length;
|
||||
@@ -63,7 +60,6 @@ const TableMain = ({ metadata, modifyRecord, modifyRecords, loadMore, loadAll, s
|
||||
groupOffsetLeft={groupOffset}
|
||||
modifyRecord={updateRecord}
|
||||
updateRecords={updateRecords}
|
||||
onRowExpand={openRecordDetails}
|
||||
getCopiedRecordsAndColumnsFromRange={getCopiedRecordsAndColumnsFromRange}
|
||||
recordGetterById={recordGetterById}
|
||||
recordGetterByIndex={recordGetterByIndex}
|
||||
|
@@ -232,10 +232,6 @@ class RecordsBody extends Component {
|
||||
}, 300);
|
||||
};
|
||||
|
||||
onRowExpand = (row) => {
|
||||
this.props.onRowExpand && this.props.onRowExpand(row);
|
||||
};
|
||||
|
||||
onScrollbarScroll = (scrollTop) => {
|
||||
// solve canvas&rightScrollbar circle scroll problem
|
||||
if (this.oldScrollTop === scrollTop) {
|
||||
@@ -482,7 +478,6 @@ class RecordsBody extends Component {
|
||||
selectedPosition={this.state.selectedPosition}
|
||||
selectNoneCells={this.selectNoneCells}
|
||||
onSelectRecord={this.props.onSelectRecord}
|
||||
onRowExpand={this.onRowExpand}
|
||||
modifyRecord={this.props.modifyRecord}
|
||||
searchResult={this.props.searchResult}
|
||||
columnColor={columnColor}
|
||||
@@ -610,7 +605,6 @@ RecordsBody.propTypes = {
|
||||
getCopiedRecordsAndColumnsFromRange: PropTypes.func,
|
||||
openDownloadFilesDialog: PropTypes.func,
|
||||
cacheDownloadFilesProps: PropTypes.func,
|
||||
onRowExpand: PropTypes.func,
|
||||
};
|
||||
|
||||
export default RecordsBody;
|
||||
|
@@ -15,7 +15,7 @@ import { getColumnScrollPosition, getColVisibleEndIdx, getColVisibleStartIdx } f
|
||||
import { GROUP_HEADER_HEIGHT, GROUP_ROW_TYPE, GROUP_VIEW_OFFSET, SEQUENCE_COLUMN_WIDTH, EVENT_BUS_TYPE } from '../../../../../constants';
|
||||
import { addClassName, removeClassName } from '../../../../../utils';
|
||||
|
||||
const ROW_HEIGHT = 32;
|
||||
const ROW_HEIGHT = 33;
|
||||
const GROUP_OVER_SCAN_ROWS = 10;
|
||||
const MAX_ANIMATION_ROWS = 50;
|
||||
const LOCAL_FOLDED_GROUP_KEY = 'path_folded_group';
|
||||
@@ -384,10 +384,6 @@ class GroupBody extends Component {
|
||||
}, 300);
|
||||
};
|
||||
|
||||
onRowExpand = (record) => {
|
||||
this.props.onRowExpand && this.props.onRowExpand(record);
|
||||
};
|
||||
|
||||
setRightScrollbarScrollTop = (scrollTop) => {
|
||||
this.rightScrollbar && this.rightScrollbar.setScrollTop(scrollTop);
|
||||
};
|
||||
@@ -826,7 +822,6 @@ class GroupBody extends Component {
|
||||
selectedPosition={this.state.selectedPosition}
|
||||
selectNoneCells={this.selectNoneCells}
|
||||
onSelectRecord={this.props.onSelectRecord}
|
||||
onRowExpand={this.onRowExpand}
|
||||
modifyRecord={this.props.modifyRecord}
|
||||
lockRecordViaButton={this.props.lockRecordViaButton}
|
||||
modifyRecordViaButton={this.props.modifyRecordViaButton}
|
||||
|
@@ -2,7 +2,6 @@ import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import { Tooltip } from 'reactstrap';
|
||||
import { Icon } from '@seafile/sf-metadata-ui-component';
|
||||
import { SEQUENCE_COLUMN_WIDTH } from '../../../../../../constants';
|
||||
import { isMobile, gettext } from '../../../../../../utils';
|
||||
|
||||
@@ -86,9 +85,6 @@ class ActionsCell extends Component {
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<span className='rdg-row-expand-icon' onClick={this.props.onRowExpand}>
|
||||
<Icon iconName="open-record" />
|
||||
</span>
|
||||
{/* {this.getLockedRowTooltip()} */}
|
||||
</div>
|
||||
);
|
||||
@@ -103,7 +99,6 @@ ActionsCell.propTypes = {
|
||||
index: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
onSelectRecord: PropTypes.func,
|
||||
onRowExpand: PropTypes.func,
|
||||
};
|
||||
|
||||
export default ActionsCell;
|
||||
|
@@ -57,11 +57,6 @@ class Record extends React.Component {
|
||||
this.props.onSelectRecord({ groupRecordIndex, recordIndex: index }, e);
|
||||
};
|
||||
|
||||
onRowExpand = () => {
|
||||
const { record } = this.props;
|
||||
this.props.onRowExpand(record);
|
||||
};
|
||||
|
||||
isCellSelected = (columnIdx) => {
|
||||
const { hasSelectedCell, selectedPosition } = this.props;
|
||||
if (!selectedPosition) return false;
|
||||
@@ -180,14 +175,15 @@ class Record extends React.Component {
|
||||
};
|
||||
|
||||
getRecordStyle = () => {
|
||||
const { isGroupView, height } = this.props;
|
||||
let style = {
|
||||
height: height + 'px',
|
||||
};
|
||||
const { isGroupView, height, isLastRecord } = this.props;
|
||||
let style = { height: isLastRecord ? height - 1 : height };
|
||||
if (isGroupView) {
|
||||
const { top, left } = this.props;
|
||||
style.top = top + 'px';
|
||||
style.left = left + 'px';
|
||||
style.top = top;
|
||||
style.left = left;
|
||||
if (isLastRecord) {
|
||||
style.height = height + 1;
|
||||
}
|
||||
}
|
||||
return style;
|
||||
};
|
||||
@@ -261,7 +257,6 @@ class Record extends React.Component {
|
||||
recordId={record._id}
|
||||
index={index}
|
||||
onSelectRecord={this.onSelectRecord}
|
||||
onRowExpand={this.onRowExpand}
|
||||
isLastFrozenCell={!lastFrozenColumnKey}
|
||||
height={cellHeight}
|
||||
/>
|
||||
@@ -294,7 +289,6 @@ Record.propTypes = {
|
||||
height: PropTypes.number,
|
||||
selectNoneCells: PropTypes.func,
|
||||
onSelectRecord: PropTypes.func,
|
||||
onRowExpand: PropTypes.func,
|
||||
modifyRecord: PropTypes.func,
|
||||
lockRecordViaButton: PropTypes.func,
|
||||
modifyRecordViaButton: PropTypes.func,
|
||||
|
@@ -28,7 +28,6 @@ class ActionsCell extends Component {
|
||||
selectNoneRecords={this.props.selectNoneRecords}
|
||||
selectAllRecords={this.props.selectAllRecords}
|
||||
/>
|
||||
<div className='header-action-cell-placeholder'></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable react/prop-types */
|
||||
import React, { useContext, useState, useRef, useCallback, useEffect } from 'react';
|
||||
import React, { useContext, useState, useCallback, useEffect } from 'react';
|
||||
import { useMetadata } from './metadata';
|
||||
import { mediaUrl } from '../../../utils/constants';
|
||||
import { isValidEmail } from '../_basic';
|
||||
@@ -9,7 +9,6 @@ const CollaboratorsContext = React.createContext(null);
|
||||
export const CollaboratorsProvider = ({
|
||||
children,
|
||||
}) => {
|
||||
const collaboratorsCacheRef = useRef({});
|
||||
const [collaboratorsCache, setCollaboratorsCache] = useState({});
|
||||
const [collaborators, setCollaborators] = useState([]);
|
||||
|
||||
@@ -26,10 +25,9 @@ export const CollaboratorsProvider = ({
|
||||
}, [collaborators, collaboratorsCache]);
|
||||
|
||||
const updateCollaboratorsCache = useCallback((user) => {
|
||||
const newCollaboratorsCache = { ...collaboratorsCacheRef.current, [user.email]: user };
|
||||
collaboratorsCacheRef.current = newCollaboratorsCache;
|
||||
const newCollaboratorsCache = { ...collaboratorsCache, [user.email]: user };
|
||||
setCollaboratorsCache(newCollaboratorsCache);
|
||||
}, []);
|
||||
}, [collaboratorsCache]);
|
||||
|
||||
const getCollaborator = useCallback((email) => {
|
||||
let collaborator = collaborators && collaborators.find(c => c.email === email);
|
||||
|
@@ -1,9 +1,7 @@
|
||||
import { CollaboratorsProvider, useCollaborators } from './collaborators';
|
||||
import { MetadataProvider, useMetadata } from './metadata';
|
||||
import { RecordDetailsProvider, useRecordDetails } from './record-details';
|
||||
|
||||
export {
|
||||
CollaboratorsProvider, useCollaborators,
|
||||
MetadataProvider, useMetadata,
|
||||
RecordDetailsProvider, useRecordDetails,
|
||||
};
|
||||
|
@@ -1,34 +0,0 @@
|
||||
/* eslint-disable react/prop-types */
|
||||
import React, { useContext, useState, useCallback } from 'react';
|
||||
|
||||
const RecordDetailsContext = React.createContext(null);
|
||||
|
||||
export const RecordDetailsProvider = ({ children }) => {
|
||||
const [isShowRecordDetails, setIsShowRecordDetails] = useState(false);
|
||||
const [recordDetails, setRecordDetails] = useState({});
|
||||
|
||||
const openRecordDetails = useCallback((recordDetails) => {
|
||||
setRecordDetails(recordDetails);
|
||||
setIsShowRecordDetails(true);
|
||||
}, []);
|
||||
|
||||
const closeRecordDetails = useCallback(() => {
|
||||
setRecordDetails({});
|
||||
setIsShowRecordDetails(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<RecordDetailsContext.Provider value={{ isShowRecordDetails, recordDetails, openRecordDetails, closeRecordDetails }}>
|
||||
{children}
|
||||
</RecordDetailsContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useRecordDetails = () => {
|
||||
const context = useContext(RecordDetailsContext);
|
||||
if (!context) {
|
||||
throw new Error('\'RecordDetailsContext\' is null');
|
||||
}
|
||||
const { isShowRecordDetails, recordDetails, openRecordDetails, closeRecordDetails } = context;
|
||||
return { isShowRecordDetails, recordDetails, openRecordDetails, closeRecordDetails };
|
||||
};
|
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { MetadataProvider, CollaboratorsProvider, RecordDetailsProvider } from './hooks/index';
|
||||
import { MetadataProvider, CollaboratorsProvider } from './hooks/index';
|
||||
import { Table } from './components/index';
|
||||
|
||||
const SeafileMetadata = ({ ...params }) => {
|
||||
@@ -8,9 +8,7 @@ const SeafileMetadata = ({ ...params }) => {
|
||||
return (
|
||||
<MetadataProvider { ...params }>
|
||||
<CollaboratorsProvider >
|
||||
<RecordDetailsProvider>
|
||||
<Table />
|
||||
</RecordDetailsProvider>
|
||||
<Table />
|
||||
</CollaboratorsProvider>
|
||||
</MetadataProvider>
|
||||
);
|
||||
|
@@ -200,7 +200,7 @@ export const getColumnName = (key, name) => {
|
||||
}
|
||||
};
|
||||
|
||||
const getColumnType = (key, type) => {
|
||||
export const getColumnType = (key, type) => {
|
||||
switch (key) {
|
||||
case PRIVATE_COLUMN_KEY.CTIME:
|
||||
case PRIVATE_COLUMN_KEY.FILE_CTIME:
|
||||
|
@@ -81,7 +81,7 @@ export const getGroupsRows = (
|
||||
const lastRowIndex = rowsLength - 1;
|
||||
const isRowVisible = isParentGroupVisible && isExpanded;
|
||||
const isBtnInsertRowVisible = isRowVisible && includeInsertRow;
|
||||
const rowsHeight = isRowVisible ? rowsLength * rowHeight : 0;
|
||||
const rowsHeight = isRowVisible ? rowsLength * rowHeight + 1 : 0;
|
||||
const btnInsertRowHeight = isBtnInsertRowVisible ? INSERT_ROW_HEIGHT : 0;
|
||||
let rows = row_ids.map((rowId, index) => {
|
||||
return {
|
||||
@@ -90,7 +90,7 @@ export const getGroupsRows = (
|
||||
rowIdx: index,
|
||||
isLastRow: index === lastRowIndex,
|
||||
visible: isRowVisible,
|
||||
height: rowHeight,
|
||||
height: index === lastRowIndex ? rowHeight + 1 : rowHeight,
|
||||
level: currentLevel,
|
||||
rowsLength,
|
||||
left,
|
||||
|
@@ -34,6 +34,7 @@ class ObjectUtils {
|
||||
}
|
||||
|
||||
static isSameObject(source, comparison) {
|
||||
if (!source && !comparison) return true;
|
||||
if (!source || !comparison) return false;
|
||||
return !this.isObjectChanged(source, comparison);
|
||||
}
|
||||
|
@@ -2,7 +2,7 @@ import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import CurDirPath from '../../components/cur-dir-path';
|
||||
import { LibDetail, DirentDetail } from '../../components/dirent-detail';
|
||||
import Detail from '../../components/dirent-detail';
|
||||
import DirColumnView from '../../components/dir-view-mode/dir-column-view';
|
||||
import ToolbarForSelectedDirents from '../../components/toolbar/selected-dirents-toolbar';
|
||||
|
||||
@@ -319,23 +319,16 @@ class LibContentContainer extends React.Component {
|
||||
)}
|
||||
{this.props.isDirentDetailShow && (
|
||||
<div className="cur-view-detail">
|
||||
{(this.props.path === '/' && !this.state.currentDirent) ?
|
||||
<LibDetail
|
||||
currentRepo={this.props.currentRepoInfo}
|
||||
closeDetails={this.props.closeDirentDetail}
|
||||
/> :
|
||||
<DirentDetail
|
||||
repoID={repoID}
|
||||
path={this.props.path}
|
||||
dirent={this.state.currentDirent}
|
||||
currentRepoInfo={this.props.currentRepoInfo}
|
||||
repoTags={this.props.repoTags}
|
||||
fileTags={this.props.isViewFile ? this.props.fileTags : []}
|
||||
onFileTagChanged={this.props.onFileTagChanged}
|
||||
onItemDetailsClose={this.props.closeDirentDetail}
|
||||
direntDetailPanelTab={this.props.direntDetailPanelTab}
|
||||
/>
|
||||
}
|
||||
<Detail
|
||||
path={path}
|
||||
repoID={repoID}
|
||||
currentRepoInfo={this.props.currentRepoInfo}
|
||||
dirent={this.state.currentDirent}
|
||||
repoTags={this.props.repoTags}
|
||||
fileTags={this.props.isViewFile ? this.props.fileTags : []}
|
||||
onFileTagChanged={this.props.onFileTagChanged}
|
||||
onClose={this.props.closeDirentDetail}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
@@ -416,7 +416,7 @@ class LibContentView extends React.Component {
|
||||
let repoID = this.props.repoID;
|
||||
|
||||
if (!this.state.isSessionExpired) {
|
||||
// update stste
|
||||
// update state
|
||||
this.setState({
|
||||
isDirentListLoading: true,
|
||||
isViewFile: false,
|
||||
@@ -494,7 +494,7 @@ class LibContentView extends React.Component {
|
||||
|
||||
showFileMetadata = (filePath, viewId) => {
|
||||
const repoID = this.props.repoID;
|
||||
this.setState({ path: filePath, isViewFile: true, isFileLoading: false, isFileLoadedErr: false, content: '__sf-metadata', metadataViewId: viewId });
|
||||
this.setState({ path: filePath, isViewFile: true, isFileLoading: false, isFileLoadedErr: false, content: '__sf-metadata', metadataViewId: viewId, isDirentDetailShow: false });
|
||||
const repoInfo = this.state.currentRepoInfo;
|
||||
const url = siteRoot + 'library/' + repoID + '/' + encodeURIComponent(repoInfo.repo_name);
|
||||
window.history.pushState({ url: url, path: '' }, '', url);
|
||||
|
@@ -1,351 +0,0 @@
|
||||
import moment from 'moment';
|
||||
import { EXTRA_ATTRIBUTES_NOT_DISPLAY_COLUMN_KEY, DEFAULT_NUMBER_FORMAT, DISPLAY_INTERNAL_ERRORS, DURATION_FORMATS_MAP,
|
||||
DURATION_FORMATS, DURATION_ZERO_DISPLAY, DURATION_DECIMAL_DIGITS, EXTRA_ATTRIBUTES_NOT_DISPLAY_COLUMN_NAME } from '../constants';
|
||||
import NP from './number-precision';
|
||||
|
||||
NP.enableBoundaryChecking(false);
|
||||
|
||||
export const getValidColumns = (columns, editableColumns = [], isEmptyFile = false) => {
|
||||
if (!Array.isArray(columns) || columns.length === 0) return [];
|
||||
return columns
|
||||
.map(column => {
|
||||
let validColumn = column;
|
||||
const canEdit = isEmptyFile ? false : editableColumns.includes(column.name);
|
||||
if (column.type === 'single-select') {
|
||||
if (!(column.data && column.data.options)) {
|
||||
validColumn.data = { options: [] };
|
||||
}
|
||||
}
|
||||
validColumn.editable = canEdit;
|
||||
return validColumn;
|
||||
})
|
||||
.filter(column => !EXTRA_ATTRIBUTES_NOT_DISPLAY_COLUMN_KEY.includes(column.key))
|
||||
.filter(column => !EXTRA_ATTRIBUTES_NOT_DISPLAY_COLUMN_NAME.includes(column.name));
|
||||
};
|
||||
|
||||
export const getDateDisplayString = (value, format) => {
|
||||
if (value === '' || !value || typeof value !== 'string') {
|
||||
return '';
|
||||
}
|
||||
// Compatible with older versions: if format is null, use defaultFormat
|
||||
const validValue = value.replace(/-/g, '/').replace('T', ' ').replace('Z', '');
|
||||
const date = moment(validValue);
|
||||
|
||||
if (!date.isValid()) return value;
|
||||
switch (format) {
|
||||
case 'D/M/YYYY':
|
||||
case 'DD/MM/YYYY': {
|
||||
const formatValue = date.format('YYYY-MM-DD');
|
||||
const formatValueList = formatValue.split('-');
|
||||
return `${formatValueList[2]}/${formatValueList[1]}/${formatValueList[0]}`;
|
||||
}
|
||||
case 'D/M/YYYY HH:mm':
|
||||
case 'DD/MM/YYYY HH:mm': {
|
||||
const formatValues = date.format('YYYY-MM-DD HH:mm');
|
||||
const formatValuesList = formatValues.split(' ');
|
||||
const formatDateList = formatValuesList[0].split('-');
|
||||
return `${formatDateList[2]}/${formatDateList[1]}/${formatDateList[0]} ${formatValuesList[1]}`;
|
||||
}
|
||||
case 'M/D/YYYY':
|
||||
return date.format('M/D/YYYY');
|
||||
case 'M/D/YYYY HH:mm':
|
||||
return date.format('M/D/YYYY HH:mm');
|
||||
case 'YYYY-MM-DD':
|
||||
return date.format('YYYY-MM-DD');
|
||||
case 'YYYY-MM-DD HH:mm':
|
||||
return date.format('YYYY-MM-DD HH:mm');
|
||||
case 'YYYY-MM-DD HH:mm:ss': {
|
||||
return date.format('YYYY-MM-DD HH:mm:ss');
|
||||
}
|
||||
case 'DD.MM.YYYY':
|
||||
return date.format('DD.MM.YYYY');
|
||||
case 'DD.MM.YYYY HH:mm':
|
||||
return date.format('DD.MM.YYYY HH:mm');
|
||||
default:
|
||||
return date.format('YYYY-MM-DD');
|
||||
}
|
||||
};
|
||||
|
||||
export const getSelectColumnOptions = (column) => {
|
||||
if (!column || !column.data || !Array.isArray(column.data.options)) {
|
||||
return [];
|
||||
}
|
||||
return column.data.options;
|
||||
};
|
||||
|
||||
const _getMathRoundedDuration = (num, duration_format) => {
|
||||
const decimalDigits = DURATION_DECIMAL_DIGITS[duration_format];
|
||||
if (decimalDigits < 1) {
|
||||
return num;
|
||||
}
|
||||
const ratio = Math.pow(10, decimalDigits);
|
||||
return Math.round(num * ratio) / ratio;
|
||||
};
|
||||
|
||||
const _getDurationDecimalSuffix = (duration_format, decimal) => {
|
||||
if (duration_format === DURATION_FORMATS_MAP.H_MM_SS_S) {
|
||||
return decimal === 0 ? '.0' : '';
|
||||
} else if (duration_format === DURATION_FORMATS_MAP.H_MM_SS_SS) {
|
||||
if (decimal === 0) {
|
||||
return '.00';
|
||||
} else if (decimal < 10) {
|
||||
return '0';
|
||||
}
|
||||
} else if (duration_format === DURATION_FORMATS_MAP.H_MM_SS_SSS) {
|
||||
if (decimal === 0) {
|
||||
return '.000';
|
||||
} else if (decimal < 10) {
|
||||
return '00';
|
||||
} else if (decimal < 100) {
|
||||
return '0';
|
||||
}
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
export const getDurationDisplayString = (value, data) => {
|
||||
if (!value && value !== 0) return '';
|
||||
let { duration_format } = data || {};
|
||||
duration_format = duration_format || DURATION_FORMATS_MAP.H_MM;
|
||||
if (DURATION_FORMATS.findIndex((format) => format.type === duration_format) < 0) {
|
||||
return '';
|
||||
}
|
||||
if (value === 0) {
|
||||
return DURATION_ZERO_DISPLAY[duration_format];
|
||||
}
|
||||
const includeDecimal = duration_format.indexOf('.') > -1;
|
||||
let positiveValue = Math.abs(value);
|
||||
if (!includeDecimal) {
|
||||
positiveValue = Math.round(positiveValue);
|
||||
}
|
||||
|
||||
positiveValue = _getMathRoundedDuration(positiveValue, duration_format);
|
||||
const decimalParts = (positiveValue + '').split('.');
|
||||
const decimalPartsLen = decimalParts.length;
|
||||
let decimal = 0;
|
||||
if (decimalPartsLen > 1) {
|
||||
decimal = decimalParts[decimalPartsLen - 1];
|
||||
decimal = decimal ? decimal - 0 : 0;
|
||||
}
|
||||
const decimalDigits = DURATION_DECIMAL_DIGITS[duration_format];
|
||||
const decimalSuffix = _getDurationDecimalSuffix(duration_format, decimal);
|
||||
let displayString = value < 0 ? '-' : '';
|
||||
let hours = parseInt(positiveValue / 3600);
|
||||
let minutes = parseInt((positiveValue - hours * 3600) / 60);
|
||||
if (duration_format === DURATION_FORMATS_MAP.H_MM) {
|
||||
displayString += `${hours}:${minutes > 9 ? minutes : '0' + minutes}`;
|
||||
return displayString;
|
||||
}
|
||||
let seconds = Number.parseFloat((positiveValue - hours * 3600 - minutes * 60).toFixed(decimalDigits));
|
||||
minutes = minutes > 9 ? minutes : `0${minutes}`;
|
||||
seconds = seconds > 9 ? seconds : `0${seconds}`;
|
||||
displayString += `${hours}:${minutes}:${seconds}${decimalSuffix}`;
|
||||
return displayString;
|
||||
};
|
||||
|
||||
const _separatorMap = {
|
||||
'comma': ',',
|
||||
'dot': '.',
|
||||
'no': '',
|
||||
'space': ' ',
|
||||
};
|
||||
|
||||
const _toThousands = (num, isCurrency, formatData) => {
|
||||
let { decimal = 'dot', thousands = 'no', precision = 2, enable_precision = false } = formatData || {};
|
||||
const decimalString = _separatorMap[decimal];
|
||||
const thousandsString = _separatorMap[thousands];
|
||||
if ((num + '').indexOf('e') > -1) {
|
||||
if (num < 1 && num > -1) {
|
||||
// 1.convert to non-scientific number
|
||||
let numericString = num.toFixed(enable_precision ? precision : 8);
|
||||
|
||||
// 2.remove 0 from end of the number which not set precision. e.g. 0.100000
|
||||
if (!enable_precision) {
|
||||
numericString = removeZerosFromEnd(numericString);
|
||||
}
|
||||
|
||||
// 3.remove minus from number which equal to 0. e.g. '-0.00'
|
||||
if (parseFloat(numericString) === 0) {
|
||||
return numericString.startsWith('-') ? numericString.substring(1) : numericString;
|
||||
}
|
||||
return numericString;
|
||||
}
|
||||
return num;
|
||||
}
|
||||
const decimalDigits = enable_precision ? precision : _getDecimalDigits(num);
|
||||
let value = parseFloat(num.toFixed(decimalDigits));
|
||||
const isMinus = value < 0;
|
||||
let integer = Math.trunc(value);
|
||||
// format decimal value
|
||||
let decimalValue = String(Math.abs(NP.minus(value, integer)).toFixed(decimalDigits)).slice(1);
|
||||
if (!enable_precision) {
|
||||
decimalValue = removeZerosFromEnd(decimalValue);
|
||||
}
|
||||
if (isCurrency) {
|
||||
if (!enable_precision) {
|
||||
if (decimalValue.length === 2) {
|
||||
decimalValue = decimalValue.padEnd(3, '0');
|
||||
} else {
|
||||
decimalValue = (decimalValue.substring(0, 3) || '.').padEnd(3, '0');
|
||||
}
|
||||
}
|
||||
}
|
||||
decimalValue = decimalValue.replace(/./, decimalString);
|
||||
// format integer value
|
||||
let result = []; let counter = 0;
|
||||
integer = Math.abs(integer).toString();
|
||||
for (var i = integer.length - 1; i >= 0; i--) {
|
||||
counter++;
|
||||
result.unshift(integer[i]);
|
||||
if (!(counter % 3) && i !== 0) {
|
||||
result.unshift(thousandsString);
|
||||
}
|
||||
}
|
||||
return (isMinus ? '-' : '') + result.join('') + decimalValue;
|
||||
};
|
||||
|
||||
const _getDecimalDigits = (num) => {
|
||||
if (Number.isInteger(num)) {
|
||||
return 0;
|
||||
}
|
||||
let valueArr = (num + '').split('.');
|
||||
let digitsLength = valueArr[1] ? valueArr[1].length : 8;
|
||||
return digitsLength > 8 ? 8 : digitsLength;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} value
|
||||
* e.g. removeZerosFromEnd('0.0100') // '0.01'
|
||||
*/
|
||||
const removeZerosFromEnd = (value) => {
|
||||
if (value.endsWith('0')) {
|
||||
return value.replace(/(?:\.0*|(\.\d+?)0+)$/, '$1');
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
export const getPrecisionNumber = (num, formatData) => {
|
||||
let { precision = 2, enable_precision = false } = formatData || {};
|
||||
let type = Object.prototype.toString.call(num);
|
||||
if (type !== '[object Number]') {
|
||||
if (type === '[object String]' && DISPLAY_INTERNAL_ERRORS.includes(num)) {
|
||||
return num;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
let decimalDigits = enable_precision ? precision : _getDecimalDigits(num);
|
||||
return num.toFixed(decimalDigits);
|
||||
};
|
||||
|
||||
export const getNumberDisplayString = (value, formatData) => {
|
||||
// formatData: old version maybe 'null'
|
||||
const type = Object.prototype.toString.call(value);
|
||||
if (type !== '[object Number]') {
|
||||
// return formula internal errors directly.
|
||||
if (type === '[object String]' && value.startsWith('#')) {
|
||||
return value;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
if (isNaN(value) || value === Infinity || value === -Infinity) return value + '';
|
||||
const { format = DEFAULT_NUMBER_FORMAT } = formatData || {};
|
||||
switch (format) {
|
||||
case 'number': {
|
||||
return _toThousands(value, false, formatData);
|
||||
}
|
||||
case 'percent': {
|
||||
return `${_toThousands(Number.parseFloat((value * 100).toFixed(8)), false, formatData)}%`;
|
||||
}
|
||||
case 'yuan': {
|
||||
return `¥${_toThousands(value, true, formatData)}`;
|
||||
}
|
||||
case 'dollar': {
|
||||
return `$${_toThousands(value, true, formatData)}`;
|
||||
}
|
||||
case 'euro': {
|
||||
return `€${_toThousands(value, true, formatData)}`;
|
||||
}
|
||||
case 'duration': {
|
||||
return getDurationDisplayString(value, formatData);
|
||||
}
|
||||
case 'custom_currency': {
|
||||
if (formatData.currency_symbol_position === 'after') {
|
||||
return `${_toThousands(value, true, formatData)}${formatData.currency_symbol || ''}`;
|
||||
} else {
|
||||
return `${formatData.currency_symbol || ''}${_toThousands(value, true, formatData)}`;
|
||||
}
|
||||
}
|
||||
default:
|
||||
return '' + value;
|
||||
}
|
||||
};
|
||||
|
||||
export const replaceNumberNotAllowInput = (value, format = DEFAULT_NUMBER_FORMAT, currency_symbol = null) => {
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
value = value.replace(/。/g, '.');
|
||||
switch (format) {
|
||||
case 'number': {
|
||||
return value.replace(/[^.-\d,]/g, '');
|
||||
}
|
||||
case 'percent': {
|
||||
return value.replace(/[^.-\d,%]/g, '');
|
||||
}
|
||||
case 'yuan': {
|
||||
return value.replace(/[^.-\d¥¥,]/g, '');
|
||||
}
|
||||
case 'dollar': {
|
||||
return value.replace(/[^.-\d$,]/g, '');
|
||||
}
|
||||
case 'euro': {
|
||||
return value.replace(/[^.-\d€,]/g, '');
|
||||
}
|
||||
case 'custom_currency': {
|
||||
// eslint-disable-next-line
|
||||
const reg = new RegExp('[^.-\d' + currency_symbol + ',]', 'g');
|
||||
return value.replace(reg, '');
|
||||
}
|
||||
default:
|
||||
return value.replace(/[^.-\d,]/g, '');
|
||||
}
|
||||
};
|
||||
|
||||
export const getFloatNumber = (data, format) => {
|
||||
if (!data && data !== 0) {
|
||||
return null;
|
||||
}
|
||||
let newData = parseFloat(data.replace(/[^.-\d]/g, ''));
|
||||
if (format === 'percent' && !isNaN(newData)) {
|
||||
return NP.divide(newData, 100);
|
||||
}
|
||||
return isNaN(newData) ? null : newData;
|
||||
};
|
||||
|
||||
export const formatStringToNumber = (numberString, formatData) => {
|
||||
let { format, decimal, thousands, enable_precision, precision } = formatData || {};
|
||||
let value = numberString;
|
||||
if (decimal && thousands && decimal === 'comma') {
|
||||
if (thousands === 'dot') {
|
||||
value = value.replace(/,/, '@');
|
||||
value = value.replace(/\./g, ',');
|
||||
value = value.replace(/@/, '.');
|
||||
} else {
|
||||
value = value.replace(/\./g, '');
|
||||
value = value.replace(/,/, '.');
|
||||
}
|
||||
}
|
||||
value = getFloatNumber(value, format);
|
||||
if (enable_precision && value) {
|
||||
if (format === 'percent') {
|
||||
precision += 2;
|
||||
}
|
||||
value = Number(parseFloat(value).toFixed(precision));
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
export const isMac = () => {
|
||||
const platform = navigator.platform;
|
||||
return (platform == 'Mac68K') || (platform == 'MacPPC') || (platform == 'Macintosh') || (platform == 'MacIntel');
|
||||
};
|
@@ -1,122 +0,0 @@
|
||||
/**
|
||||
* @desc Solve the problem of floating calculation, avoid multiple digits after the decimal point and loss of calculation accuracy.
|
||||
* example: 3 + 2.4 = 4.699999999999999,1.0 - 0.9 = 0.09999999999999998
|
||||
*/
|
||||
|
||||
/**
|
||||
* Correct wrong data
|
||||
* strip(0.09999999999999998)=0.1
|
||||
*/
|
||||
function strip(num, precision = 12) {
|
||||
return +parseFloat(num.toPrecision(precision));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return digits length of a number
|
||||
* @param {*number} num Input number
|
||||
*/
|
||||
function digitLength(num) {
|
||||
// Get digit length of e
|
||||
const eSplit = num.toString().split(/[eE]/);
|
||||
const len = (eSplit[0].split('.')[1] || '').length - (+(eSplit[1] || 0));
|
||||
return len > 0 ? len : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert decimals to integers and support scientific notation. If it is a decimal, it is enlarged to an integer
|
||||
* @param {*number} num Number of inputs
|
||||
*/
|
||||
function float2Fixed(num) {
|
||||
if (num.toString().indexOf('e') === -1) {
|
||||
return Number(num.toString().replace('.', ''));
|
||||
}
|
||||
const dLen = digitLength(num);
|
||||
return dLen > 0 ? strip(num * Math.pow(10, dLen)) : num;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the number is out of range, and give a prompt if it is out of range
|
||||
* @param {*number} num Number of inputs
|
||||
*/
|
||||
function checkBoundary(num) {
|
||||
if (_boundaryCheckingState) {
|
||||
if (num > Number.MAX_SAFE_INTEGER || num < Number.MIN_SAFE_INTEGER) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(`${num} is beyond boundary when transfer to integer, the results may not be accurate`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exact multiplication
|
||||
*/
|
||||
function times(num1, num2, ...others) {
|
||||
if (others.length > 0) {
|
||||
return times(times(num1, num2), others[0], ...others.slice(1));
|
||||
}
|
||||
const num1Changed = float2Fixed(num1);
|
||||
const num2Changed = float2Fixed(num2);
|
||||
const baseNum = digitLength(num1) + digitLength(num2);
|
||||
const leftValue = num1Changed * num2Changed;
|
||||
|
||||
checkBoundary(leftValue);
|
||||
|
||||
return leftValue / Math.pow(10, baseNum);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exact addition
|
||||
*/
|
||||
function plus(num1, num2, ...others) {
|
||||
if (others.length > 0) {
|
||||
return plus(plus(num1, num2), others[0], ...others.slice(1));
|
||||
}
|
||||
const baseNum = Math.pow(10, Math.max(digitLength(num1), digitLength(num2)));
|
||||
return (times(num1, baseNum) + times(num2, baseNum)) / baseNum;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exact subtraction
|
||||
*/
|
||||
function minus(num1, num2, ...others) {
|
||||
if (others.length > 0) {
|
||||
return minus(minus(num1, num2), others[0], ...others.slice(1));
|
||||
}
|
||||
const baseNum = Math.pow(10, Math.max(digitLength(num1), digitLength(num2)));
|
||||
return (times(num1, baseNum) - times(num2, baseNum)) / baseNum;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exact division
|
||||
*/
|
||||
function divide(num1, num2, ...others) {
|
||||
if (others.length > 0) {
|
||||
return divide(divide(num1, num2), others[0], ...others.slice(1));
|
||||
}
|
||||
const num1Changed = float2Fixed(num1);
|
||||
const num2Changed = float2Fixed(num2);
|
||||
checkBoundary(num1Changed);
|
||||
checkBoundary(num2Changed);
|
||||
// fix: Similar to 10 ** -4 is 0.00009999999999999999, strip correction
|
||||
return times((num1Changed / num2Changed), strip(Math.pow(10, digitLength(num2) - digitLength(num1))));
|
||||
}
|
||||
|
||||
/**
|
||||
* rounding
|
||||
*/
|
||||
function round(num, ratio) {
|
||||
const base = Math.pow(10, ratio);
|
||||
return divide(Math.round(times(num, base)), base);
|
||||
}
|
||||
|
||||
let _boundaryCheckingState = true;
|
||||
/**
|
||||
* Whether to perform boundary check, default true
|
||||
* @param flag Mark switch, true is on, false is off, default is true
|
||||
*/
|
||||
function enableBoundaryChecking(flag = true) {
|
||||
_boundaryCheckingState = flag;
|
||||
}
|
||||
export { strip, plus, minus, times, divide, round, digitLength, float2Fixed, enableBoundaryChecking };
|
||||
// eslint-disable-next-line
|
||||
export default { strip, plus, minus, times, divide, round, digitLength, float2Fixed, enableBoundaryChecking };
|
@@ -1650,7 +1650,12 @@ export const Utils = {
|
||||
isRelativePath(url) {
|
||||
let RgExp = new RegExp('^(?:[a-z]+:)?//', 'i');
|
||||
return !RgExp.test(url);
|
||||
}
|
||||
},
|
||||
|
||||
isMac() {
|
||||
const platform = navigator.platform;
|
||||
return (platform == 'Mac68K') || (platform == 'MacPPC') || (platform == 'Macintosh') || (platform == 'MacIntel');
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
|
@@ -307,8 +307,8 @@ class MetadataRecordInfo(APIView):
|
||||
error_msg = 'name invalid'
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||
|
||||
record = RepoMetadata.objects.filter(repo_id=repo_id).first()
|
||||
if not record or not record.enabled:
|
||||
metadata = RepoMetadata.objects.filter(repo_id=repo_id).first()
|
||||
if not metadata or not metadata.enabled:
|
||||
error_msg = f'The metadata module is disabled for repo {repo_id}.'
|
||||
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||
|
||||
@@ -337,40 +337,12 @@ class MetadataRecordInfo(APIView):
|
||||
error_msg = 'Internal Server Error'
|
||||
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||
|
||||
sys_columns = [
|
||||
METADATA_TABLE.columns.id.key,
|
||||
METADATA_TABLE.columns.file_creator.key,
|
||||
METADATA_TABLE.columns.file_ctime.key,
|
||||
METADATA_TABLE.columns.file_modifier.key,
|
||||
METADATA_TABLE.columns.file_mtime.key,
|
||||
METADATA_TABLE.columns.parent_dir.key,
|
||||
METADATA_TABLE.columns.file_name.key,
|
||||
METADATA_TABLE.columns.is_dir.key,
|
||||
]
|
||||
|
||||
rows = query_result.get('results')
|
||||
|
||||
if not rows:
|
||||
error_msg = 'Record not found'
|
||||
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||
|
||||
metadata = query_result.get('metadata')
|
||||
editable_columns = []
|
||||
name_to_key = {}
|
||||
for col in metadata:
|
||||
col_key = col.get('key')
|
||||
col_name = col.get('name')
|
||||
name_to_key[col_name] = col_key
|
||||
if col_key in sys_columns:
|
||||
continue
|
||||
editable_columns.append(col.get('name'))
|
||||
|
||||
row = {name_to_key[name]: value for name, value in rows[0].items()}
|
||||
query_result['row'] = row
|
||||
query_result['editable_columns'] = editable_columns
|
||||
|
||||
query_result.pop('results', None)
|
||||
|
||||
return Response(query_result)
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user