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

Revert "Combine dtable"

This commit is contained in:
Daniel Pan
2019-06-14 16:30:33 +08:00
committed by GitHub
parent 4465e29628
commit 834f7bbef4
25 changed files with 783 additions and 8569 deletions

View File

@@ -220,9 +220,9 @@ module.exports = {
paths.appSrc + "/pages/sys-admin",
],
viewDataGrid: [
// require.resolve('./polyfills'),
require.resolve('./polyfills'),
require.resolve('react-dev-utils/webpackHotDevClient'),
paths.appSrc + "/view-file-dtable.js",
paths.appSrc + "/view-file-ctable.js",
],
viewCdoc: [
require.resolve('./polyfills'),

View File

@@ -92,7 +92,7 @@ module.exports = {
repoFolderTrash: [require.resolve('./polyfills'), paths.appSrc + "/repo-folder-trash.js"],
orgAdmin: [require.resolve('./polyfills'), paths.appSrc + "/pages/org-admin"],
sysAdmin: [require.resolve('./polyfills'), paths.appSrc + "/pages/sys-admin"],
viewDataGrid: [require.resolve('./polyfills'), paths.appSrc + "/view-file-dtable.js"],
viewDataGrid: [require.resolve('./polyfills'), paths.appSrc + "/view-file-ctable.js"],
viewCdoc: [require.resolve('./polyfills'), paths.appSrc + "/view-file-cdoc.js"],
search: [require.resolve('./polyfills'), paths.appSrc + "/pages/search"]
},

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,6 @@
"private": true,
"dependencies": {
"@reach/router": "^1.2.0",
"@seafile/dtable": "0.0.25",
"@seafile/react-data-grid": "^6.1.5",
"@seafile/react-data-grid-addons": "^6.1.5",
"@seafile/resumablejs": "^1.1.9",
@@ -30,15 +29,15 @@
"prismjs": "^1.15.0",
"prop-types": "^15.6.2",
"raf": "3.4.0",
"react": "^16.8.6",
"react": "^16.4.2",
"react-codemirror": "^1.0.0",
"react-cookies": "^0.1.0",
"react-dom": "^16.8.6",
"react-dom": "^16.5.2",
"react-image-lightbox": "^5.1.0",
"react-moment": "^0.7.9",
"react-responsive": "^6.1.1",
"react-select": "^2.4.1",
"reactstrap": "^8.0.0",
"reactstrap": "^6.4.0",
"seafile-js": "^0.2.91",
"socket.io-client": "^2.2.0",
"sw-precache-webpack-plugin": "0.11.4",

View File

@@ -1,3 +0,0 @@
.rc-calendar table tbody tr {
height: fit-content;
}

View File

@@ -0,0 +1,26 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Button } from 'reactstrap';
const propTypes = {
onSave: PropTypes.func.isRequired,
};
const { fileName } = window.app.pageOptions;
class AddHeader extends React.Component {
render() {
let {isContentChanged} = this.props;
return (
<div id="header">
<div className="sf-font">{fileName}</div>
<Button color="primary" onClick={this.props.onSave} disabled={!isContentChanged}>保存</Button>
</div>
);
}
}
AddHeader.propTypes = propTypes;
export default AddHeader;

View File

@@ -0,0 +1,187 @@
import React from 'react';
import isHotkey from 'is-hotkey';
import ReactDataGrid from '@seafile/react-data-grid/';
import { Menu } from '@seafile/react-data-grid-addons';
import { seafileAPI } from '../../utils/seafile-api';
import ModalPortal from '../../components/modal-portal';
import NewColumnDialog from './new-column-dialog';
import GridHeaderContextMenu from './grid-header-contextmenu';
import GridContentContextMenu from './grid-content-contextmenu';
import DTableStore from './store/dtable-store';
const { repoID, filePath } = window.app.pageOptions;
const DEFAULT_DATA = {
columns: [
{
key: 'name',
name: 'Name',
type: '',
width: 80,
editable: true,
resizable: true
}
],
rows: [{name: 'name_' + 0}]
};
class AppMain extends React.Component {
constructor(props, context) {
super(props, context);
this.state = {
value: null,
isNewColumnDialogShow: false,
};
this.dTableStore = new DTableStore();
}
componentDidMount() {
seafileAPI.getFileDownloadLink(repoID, filePath).then(res => {
let url = res.data;
seafileAPI.getFileContent(url).then(res => {
let data = res.data ? res.data : JSON.stringify(DEFAULT_DATA);
let value = this.dTableStore.deseralizeGridData(data);
this.setState({value});
});
});
document.addEventListener('keydown', this.onHotKey);
}
componentWillUnmount() {
document.removeEventListener('keydown', this.onHotKey);
}
getRowAt = (index) => {
if (index < 0 || index > this.getSize()) {
return undefined;
}
return this.state.value.rows[index];
};
getSize = () => {
return this.state.value.rows.length;
};
onInsertRow = () => {
let newRowIndex = this.getSize();
let value = this.dTableStore.insertRow(newRowIndex);
this.setState({value});
this.props.onContentChanged();
}
onInsertColumn = () => {
this.setState({isNewColumnDialogShow: true});
}
onNewColumn = (columnName, columnType) => {
let idx = this.state.value.columns.length;
let value = this.dTableStore.insertColumn(idx, columnName, columnType);
this.setState({value});
this.onNewColumnCancel();
this.props.onContentChanged();
}
onNewColumnCancel = () => {
this.setState({isNewColumnDialogShow: false});
this.props.onContentChanged();
}
onRowDelete = (e, data) => {
let { rowIdx } = data;
let value = this.dTableStore.deleteRow(rowIdx);
this.setState({value});
this.props.onContentChanged();
}
onColumnDelete = (e, data) => {
let column = data.column;
let idx = column.idx - 1;
let value = this.dTableStore.deleteColumn(idx);
this.setState({value});
this.props.onContentChanged();
}
onColumnResize = (index, width) => {
let idx = index - 1;
let value = this.dTableStore.resizeColumn(idx, width);
this.setState({value});
this.props.onContentChanged();
}
handleGridRowsUpdated = ({fromRow, updated}) => {
let rowIdx = fromRow;
let value = this.dTableStore.modifyCell(rowIdx, updated);
this.setState({value});
this.props.onContentChanged();
}
serializeGridData = () => {
return this.dTableStore.serializeGridData();
}
onHotKey = (event) => {
if (isHotkey('mod+s', event)) {
event.preventDefault();
this.props.onSave();
return true;
}
}
render() {
if (!this.state.value) {
return '';
}
return (
<div id="main">
<ReactDataGrid
ref={ node => this.grid = node }
enableCellSelect={true}
columns={this.state.value.columns}
rowGetter={this.getRowAt}
rowsCount={this.getSize()}
onGridRowsUpdated={this.handleGridRowsUpdated}
enableRowSelect={true}
rowHeight={50}
minHeight={500}
rowScrollTimeout={200}
enableInsertColumn={true}
enableInsertRow={true}
onInsertRow={this.onInsertRow}
onInsertColumn={this.onInsertColumn}
RowsContainer={Menu.ContextMenuTrigger}
headerContextMenu={<GridHeaderContextMenu id="grid-header-contxtmenu" onColumnDelete={this.onColumnDelete} />}
contextMenu={<GridContentContextMenu id="grid-content-contxtmenu" onRowDelete={this.onRowDelete} />}
onColumnResize={this.onColumnResize}
/>
{this.state.isNewColumnDialogShow && (
<ModalPortal>
<NewColumnDialog
onNewColumnCancel={this.onNewColumnCancel}
onNewColumn={this.onNewColumn}
/>
</ModalPortal>
)}
</div>
);
}
}
export default AppMain;

View File

@@ -0,0 +1,33 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Menu } from '@seafile/react-data-grid-addons';
const propTypes = {
id: PropTypes.string.isRequired,
idx: PropTypes.number,
rowIdx: PropTypes.number,
onRowDelete: PropTypes.func.isRequired,
};
class GridContentContextMenu extends React.Component {
onRowDelete = (e, data) => {
if (typeof(this.props.onRowDelete) === 'function') {
this.props.onRowDelete(e, data);
}
};
render() {
const { idx, id, rowIdx } = this.props;
return (
<Menu.ContextMenu id={id}>
<Menu.MenuItem data={{ rowIdx, idx }} onClick={this.onRowDelete}>Delete Row</Menu.MenuItem>
</Menu.ContextMenu>
);
}
}
GridContentContextMenu.propTypes = propTypes;
export default GridContentContextMenu;

View File

@@ -0,0 +1,33 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Menu } from '@seafile/react-data-grid-addons';
const propTypes = {
id: PropTypes.string.isRequired,
column: PropTypes.object,
onColumnRename: PropTypes.func,
};
class GridHeaderContextMenu extends React.Component {
onColumnDelete = (e, data) => {
if (typeof(this.props.onColumnDelete) === 'function') {
this.props.onColumnDelete(e, data);
}
}
render() {
let { id, column } = this.props;
return (
<Menu.ContextMenu id={id}>
<Menu.MenuItem data={{ column }} onClick={this.onColumnDelete}>Delete</Menu.MenuItem>
</Menu.ContextMenu>
);
}
}
GridHeaderContextMenu.propTypes = propTypes;
export default GridHeaderContextMenu;

View File

@@ -0,0 +1,13 @@
export default class GridColumn {
constructor(object) {
this.key = object.columnName || object.name;
this.name = object.columnName || object.name;
this.type = object.columnType || null;
this.editor = null;
this.editable = object.editable || true;
this.width = object.width || 200;
this.resizable = object.resizable || true;
}
}

View File

@@ -0,0 +1,7 @@
export default class GridRow {
constructor(object) {
this.name = object.name || 'name_' + object.newRowIdx;
}
}

View File

@@ -0,0 +1,76 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Modal, ModalHeader, ModalBody, ModalFooter, Form, FormGroup, Label, Input, Alert, Button } from 'reactstrap';
import { gettext } from '../../utils/constants';
const propTypes = {
onNewColumnCancel: PropTypes.func,
onNewColumn: PropTypes.func,
};
class NewCoumnDialog extends React.Component {
constructor(props) {
super(props);
this.state = {
columnType: 'text',
columnName: '',
errMessage: '',
};
}
handleChange = (event) => {
let value = event.target.value.trim();
this.setState({columnName: value});
}
onSelectChange = (event) => {
let type = event.target.value;
this.setState({columnType: type});
}
toggle = () => {
this.props.onNewColumnCancel();
}
handleSubmit = () => {
let { columnName, columnType } = this.state;
if (!columnName) {
this.setState({errMessage: gettext('Name is required.')});
return;
}
this.props.onNewColumn(columnName, columnType);
}
render() {
return (
<Modal isOpen={true} toggle={this.toggle}>
<ModalHeader>New Column</ModalHeader>
<ModalBody>
<Form>
<FormGroup>
<Label for="columnName">Name</Label>
<Input id="columnName" value={this.state.columnName} innerRef={input => {this.newInput = input;}} onChange={this.handleChange} />
</FormGroup>
<FormGroup>
<Label for="typeSelect">Type</Label>
<Input id="typeSelect" type='select' name="select" onChange={this.onSelectChange}>
<option vlaue="text">text</option>
<option value="number">number</option>
</Input>
</FormGroup>
</Form>
{this.state.errMessage && <Alert color="danger" className="mt-2">{this.state.errMessage}</Alert>}
</ModalBody>
<ModalFooter>
<Button color="secondary" onClick={this.toggle}>{gettext('Cancel')}</Button>
<Button color="primary" onClick={this.handleSubmit}>{gettext('Submit')}</Button>
</ModalFooter>
</Modal>
);
}
}
NewCoumnDialog.propTypes = propTypes;
export default NewCoumnDialog;

View File

@@ -0,0 +1,61 @@
import OperationTypes from './operation-types';
import GridColumn from '../model/grid-column';
import GridRow from '../model/grid-row';
function apply(value, op) {
let { type } = op;
let next = value.slice(0); // clone a copy
switch(type) {
case OperationTypes.DELETE_ROW : {
let { rowIdx } = op;
next.splice(rowIdx, 1);
return next;
}
case OperationTypes.INSERT_ROW : {
let { newRowIdx } = op;
let row = new GridRow({newRowIdx});
next.push(row);
return next;
}
case OperationTypes.DELETE_COLUMN : {
let { idx } = op;
next.splice(idx, 1);
return next;
}
case OperationTypes.INSERT_COLUMN : {
let { idx, columnName, columnType } = op;
let column = new GridColumn({idx, columnName, columnType});
next.push(column);
return next;
}
case OperationTypes.MODIFY_CELL : {
let { rowIdx, updated } = op;
let updateRow = next[rowIdx];
updateRow = Object.assign({}, {...updateRow}, updated); // todo
next[rowIdx] = updateRow;
return next;
}
case OperationTypes.MODIFY_COLUMN : {
let { idx, newColumnName } = op;
next[idx]['key'] = newColumnName;
next[idx]['name'] = newColumnName;
return next;
}
case OperationTypes.RESIZE_COLUMN : {
let { idx, width } = op;
next[idx].width = width;
return next;
}
}
}
export default apply;

View File

@@ -0,0 +1,134 @@
import Operation from './operation';
import OperationTypes from './operation-types';
import editorFactory from '../utils/editor-factory';
// todo Immutable
// Implement the current version with an array
export default class DTableStore {
constructor() {
this.value = {};
this.value.columns = [];
this.value.rows = [];
this.operations = [];
}
serializeGridData() {
let value = this.value;
let columns = value.columns.map(column => {
delete column.editor; // delete editor attr;
return column;
});
value.columns = columns;
return JSON.stringify(value);
}
deseralizeGridData(gridData) {
gridData = JSON.parse(gridData);
let columns = gridData.columns;
let rows = gridData.rows;
columns = columns.map(column => {
if (column.type) {
let editor = editorFactory.createEditor(column.type);
column.editor = editor;
}
return column;
});
this.value.columns = columns;
this.value.rows = rows;
return this.value;
}
createOperation(op) {
return new Operation(op);
}
deleteRow(rowIdx) {
let type = OperationTypes.DELETE_ROW;
let operation = this.createOperation({type, rowIdx});
let next = operation.apply(this.value.rows);
this.operations.push(operation);
this.value.rows = next;
return this.value;
}
insertRow(newRowIdx) {
let type = OperationTypes.INSERT_ROW;
let operation = this.createOperation({type, newRowIdx});
let next = operation.apply(this.value.rows);
this.operations.push(operation);
this.value.rows = next;
return this.value;
}
deleteColumn(idx) {
let type = OperationTypes.DELETE_COLUMN;
let operation = this.createOperation({type, idx});
let next = operation.apply(this.value.columns);
this.operations.push(operation);
this.value.columns = next;
return this.value;
}
insertColumn(idx, columnName, columnType) {
let type = OperationTypes.INSERT_COLUMN;
let operation = this.createOperation({type, idx, columnName, columnType});
let next = operation.apply(this.value.columns);
this.operations.push(operation);
this.value.columns = next;
let value = this.serializeGridData();
this.deseralizeGridData(value);
return this.value;
}
modifyColumn(idx, oldColumnName, newColumnName) {
let type = OperationTypes.MODIFY_COLUMN;
let operation = this.createOperation({type, idx, oldColumnName, newColumnName});
let next = operation.apply(this.value.columns);
this.operations.push(operation);
this.value.columns = next;
return this.value;
}
modifyCell(rowIdx, updated) {
let type = OperationTypes.MODIFY_CELL;
let operation = this.createOperation({type, rowIdx, updated});
let next = operation.apply(this.value.rows);
this.operations.push(operation);
this.value.rows = next;
return this.value;
}
resizeColumn(idx, width) {
let type = OperationTypes.RESIZE_COLUMN;
let operation = this.createOperation({type, idx, width});
let next = operation.apply(this.value.columns);
this.operations.push(operation);
this.value.columns = next;
return this.value;
}
}

View File

@@ -0,0 +1,35 @@
import Operation from './operation';
import OperationTypes from './operation-types';
function invert(operation) {
let op = new Operation(operation);
let { type } = operation;
switch(type) {
case OperationTypes.DELETE_COLUMN : {
op.type = OperationTypes.INSERT_COLUMN;
return op;
}
case OperationTypes.INSERT_COLUMN : {
op.type = OperationTypes.DELETE_COLUMN;
return op;
}
case OperationTypes.DELETE_ROW : {
op.type = OperationTypes.INSERT_ROW;
return op;
}
case OperationTypes.INSERT_ROW : {
op.type = OperationTypes.DELETE_ROW;
return op;
}
default :
break;
}
}
export default invert;

View File

@@ -0,0 +1,13 @@
const OperationTypes = {
DELETE_ROW: 'DELETE_NODE',
INSERT_ROW: 'INSERT_ROW',
DELETE_COLUMN: 'DELETE_COLUMN',
INSERT_COLUMN: 'INSERT_COLUMN',
MODIFY_CELL: 'MODIFY_CELL',
MODIFY_COLUMN: 'MODIFY_COLUMN',
RESIZE_COLUMN: 'RESIZE_COLUMN',
};
export default OperationTypes;

View File

@@ -0,0 +1,20 @@
import apply from './apply';
import invert from './invert';
export default class Operation {
constructor(operation) {
this.operation = operation;
}
apply(value) {
let next = apply(value, this.operation);
return next;
}
invert() {
let inverted = invert(this);
return inverted;
}
}

View File

@@ -0,0 +1,28 @@
import React from 'react';
import { Editors } from '@seafile/react-data-grid-addons';
const EDITOR_NUMBER = 'number';
const EDITOR_TEXT = 'text';
class EditorFactory {
createEditor(editorType) {
switch(editorType) {
case EDITOR_NUMBER: {
return <Editors.NumberEditor />;
}
case EDITOR_TEXT: {
return '';
}
default: {
return '';
}
}
}
}
let editorFactory = new EditorFactory();
export default editorFactory;

View File

@@ -0,0 +1,58 @@
import React, { Fragment } from 'react';
import ReactDOM from 'react-dom';
import AppHeader from './pages/data-grid/app-header';
import AppMain from './pages/data-grid/app-main';
import { seafileAPI } from './utils/seafile-api';
import { Utils } from './utils/utils';
import { gettext } from './utils/constants';
import toaster from './components/toast';
import './css/layout.css';
import './css/file-view-data-grid.css';
import './css/react-context-menu.css';
const { repoID, fileName, filePath } = window.app.pageOptions;
class ViewFileSDB extends React.Component {
constructor(props) {
super(props);
this.state = {
isContentChanged: false,
};
}
onContentChanged = () => {
this.setState({isContentChanged: true});
}
onSave = () => {
this.setState({isContentChanged: false});
let data = this.refs.data_grid.serializeGridData();
let dirPath = Utils.getDirName(filePath);
seafileAPI.getUpdateLink(repoID, dirPath).then(res => {
let updateLink = res.data;
seafileAPI.updateFile(updateLink, filePath, fileName, JSON.stringify(data)).then(res => {
toaster.success(gettext('File saved.'));
}).catch(() => {
toaster.success(gettext('File save failed.'));
});
});
}
render() {
return (
<Fragment>
<AppHeader onSave={this.onSave} isContentChanged={this.state.isContentChanged} />
<AppMain ref="data_grid" onContentChanged={this.onContentChanged} onSave={this.onSave} />
</Fragment>
);
}
}
ReactDOM.render(
<ViewFileSDB />,
document.getElementById('wrapper')
);

View File

@@ -1,20 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { seafileAPI } from './utils/seafile-api';
import SeafileTable from '@seafile/dtable/lib';
import "./css/dtable.css";
class ViewFileSDB extends React.Component {
render() {
return (
<SeafileTable pageOptions={window.app.pageOptions} seafileAPI={seafileAPI}/>
);
}
}
ReactDOM.render(
<ViewFileSDB />,
document.getElementById('wrapper')
);

5672
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -136,7 +136,7 @@ PREVIEW_FILEEXT = {
AUDIO: ('mp3', 'oga', 'ogg'),
#'3D': ('stl', 'obj'),
XMIND: ('xmind',),
DTABLE: ('dtable',),
CTABLE: ('ctable',),
CDOC: ('cdoc',),
}

View File

@@ -10,5 +10,5 @@ AUDIO = 'Audio'
SPREADSHEET = 'SpreadSheet'
DRAW = 'Draw'
XMIND = 'XMind'
DTABLE = 'dtable'
CTABLE = 'ctable'
CDOC = 'cdoc'

View File

@@ -63,7 +63,7 @@ from seahub.utils import render_error, is_org_context, \
from seahub.utils.ip import get_remote_ip
from seahub.utils.timeutils import utc_to_local
from seahub.utils.file_types import (IMAGE, PDF, SVG,
DOCUMENT, SPREADSHEET, AUDIO, MARKDOWN, TEXT, VIDEO, DRAW, XMIND, DTABLE, CDOC)
DOCUMENT, SPREADSHEET, AUDIO, MARKDOWN, TEXT, VIDEO, DRAW, XMIND, CTABLE, CDOC)
from seahub.utils.star import is_file_starred
from seahub.utils.http import json_response, \
BadRequestException, RequestForbbiddenException
@@ -734,7 +734,7 @@ def view_lib_file(request, repo_id, path):
return render(request, template, return_dict)
elif filetype == DTABLE:
elif filetype == CTABLE:
return render(request, template, return_dict)
elif filetype == CDOC:
return render(request, template, return_dict)