mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-20 19:08:21 +00:00
add file ledger apis (#5507)
* add file ledger apis * remove apis about export ledgers * opt code struct * feat: update api * feat: update code * rename init-ledger script -> init-extended-props * remove useless code * POST/PUT extended-props return row * return default some fields when extended-row not exists * feat: update seafile-js version --------- Co-authored-by: er-pai-r <18335219360@163.com>
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
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;
|
@@ -0,0 +1,7 @@
|
||||
.extra-attributes-dialog .column-name {
|
||||
padding-top: 9px;
|
||||
}
|
||||
|
||||
.extra-attributes-dialog .column-item {
|
||||
min-height: 56px;
|
||||
}
|
@@ -0,0 +1,37 @@
|
||||
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;
|
@@ -0,0 +1,22 @@
|
||||
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;
|
@@ -0,0 +1,28 @@
|
||||
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;
|
@@ -0,0 +1,31 @@
|
||||
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;
|
@@ -0,0 +1,20 @@
|
||||
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;
|
@@ -0,0 +1,90 @@
|
||||
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;
|
@@ -0,0 +1,108 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
@@ -0,0 +1,92 @@
|
||||
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;
|
@@ -0,0 +1,101 @@
|
||||
.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 .fa-caret-down {
|
||||
font-size: 16px;
|
||||
color: #949494;
|
||||
}
|
||||
|
||||
.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-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;
|
||||
}
|
||||
|
@@ -0,0 +1,83 @@
|
||||
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="fas fa-caret-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;
|
@@ -0,0 +1,126 @@
|
||||
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();
|
||||
this.state = {
|
||||
value: props.row[props.column.key],
|
||||
searchVal: '',
|
||||
highlightIndex: -1,
|
||||
maxItemNum: 0,
|
||||
itemHeight: 0
|
||||
};
|
||||
this.options = options;
|
||||
this.filteredOptions = options;
|
||||
this.timer = null;
|
||||
this.editorKey = `single-select-editor-${props.column.key}`;
|
||||
}
|
||||
|
||||
getSelectColumnOptions = () => {
|
||||
const { column, row, columns } = this.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;
|
||||
}
|
||||
|
||||
setRef = (ref) => {
|
||||
this.ref = ref;
|
||||
if (!this.ref) return;
|
||||
const { toggle } = this.ref;
|
||||
this.ref.toggle = () => {
|
||||
toggle && toggle();
|
||||
this.props.onUpdateState();
|
||||
};
|
||||
}
|
||||
|
||||
onChangeSearch = (searchVal) => {
|
||||
const { searchVal: oldSearchVal } = this.state;
|
||||
if (oldSearchVal === searchVal) return;
|
||||
const val = searchVal.toLowerCase();
|
||||
this.filteredOptions = val ?
|
||||
this.options.filter((item) => item.name && item.name.toLowerCase().indexOf(val) > -1) : this.options;
|
||||
this.setState({ searchVal });
|
||||
}
|
||||
|
||||
onSelectOption = (optionID) => {
|
||||
const { column } = this.props;
|
||||
this.setState({ value: optionID }, () => {
|
||||
this.props.onCommit({ [column.key]: optionID }, column);
|
||||
this.ref.toggle();
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { value } = this.state;
|
||||
const { column } = this.props;
|
||||
|
||||
return (
|
||||
<UncontrolledPopover
|
||||
target={this.editorKey}
|
||||
className="single-select-editor-popover"
|
||||
trigger="legacy"
|
||||
placement="bottom-start"
|
||||
hideArrow={true}
|
||||
ref={this.setRef}
|
||||
>
|
||||
<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">
|
||||
{this.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;
|
@@ -0,0 +1,17 @@
|
||||
.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;
|
||||
}
|
250
frontend/src/components/dialog/extra-attributes-dialog/index.js
Normal file
250
frontend/src/components/dialog/extra-attributes-dialog/index.js
Normal file
@@ -0,0 +1,250 @@
|
||||
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, EXTRA_ATTRIBUTES_COLUMN_TYPE } from '../../../constants';
|
||||
import { gettext } from '../../../utils/constants';
|
||||
import { seafileAPI } from '../../../utils/seafile-api';
|
||||
import { Utils } from '../../../utils/utils';
|
||||
import { getSelectColumnOptions, getValidColumns } from '../../../utils/extra-attributes';
|
||||
import Column from './column';
|
||||
import Loading from '../../loading';
|
||||
import toaster from '../../toast';
|
||||
|
||||
import './index.css';
|
||||
|
||||
class ExtraAttributesDialog extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const { direntDetail } = props;
|
||||
this.state = {
|
||||
animationEnd: false,
|
||||
isLoading: true,
|
||||
update: {},
|
||||
row: {},
|
||||
columns: [],
|
||||
errorMsg: '',
|
||||
};
|
||||
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);
|
||||
}
|
||||
|
||||
getFormatUpdateData = (update = {}) => {
|
||||
const { columns } = this.state;
|
||||
const updateData = {};
|
||||
for (let key in update) {
|
||||
const column = columns.find(column => column.key === key);
|
||||
if (column && column.editable) {
|
||||
const { type, name } = column;
|
||||
const value = update[key];
|
||||
if (type === EXTRA_ATTRIBUTES_COLUMN_TYPE.SINGLE_SELECT) {
|
||||
const options = getSelectColumnOptions(column);
|
||||
const option = options.find(item => item.id === value);
|
||||
updateData[name] = option ? option.name : '';
|
||||
} else {
|
||||
updateData[column.name] = update[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
return updateData;
|
||||
}
|
||||
|
||||
getData = () => {
|
||||
const { repoID, filePath } = this.props;
|
||||
seafileAPI.getFileExtendedProperties(repoID, filePath).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 });
|
||||
});
|
||||
}
|
||||
|
||||
createData = (data) => {
|
||||
const { repoID, filePath } = this.props;
|
||||
seafileAPI.newFileExtendedProperties(repoID, filePath, data).then(res => {
|
||||
this.isExist = true;
|
||||
const { row } = res.data;
|
||||
this.setState({ row: row, isLoading: false, errorMsg: '' });
|
||||
}).catch(error => {
|
||||
const errorMsg =Utils.getErrorMsg(error);
|
||||
toaster.danger(gettext(errorMsg));
|
||||
});
|
||||
};
|
||||
|
||||
updateData = (update, column) => {
|
||||
const newRow = { ...this.state.row, ...update };
|
||||
this.setState({ row: newRow }, () => {
|
||||
const data = this.getFormatUpdateData(update);
|
||||
const { repoID, filePath } = this.props;
|
||||
if (this.isExist) {
|
||||
seafileAPI.updateFileExtendedProperties(repoID, filePath, data).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 attributes')}</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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ExtraAttributesDialog.propTypes = {
|
||||
repoID: PropTypes.string,
|
||||
filePath: PropTypes.string,
|
||||
direntDetail: PropTypes.object,
|
||||
onToggle: PropTypes.func,
|
||||
};
|
||||
|
||||
export default ExtraAttributesDialog;
|
Reference in New Issue
Block a user