diff --git a/frontend/src/components/dialog/create-table-dialog.js b/frontend/src/components/dialog/create-table-dialog.js new file mode 100644 index 0000000000..18facb8627 --- /dev/null +++ b/frontend/src/components/dialog/create-table-dialog.js @@ -0,0 +1,109 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Button, Modal, ModalHeader, Input, ModalBody, ModalFooter, Form, FormGroup, Label, Alert } from 'reactstrap'; +import { gettext } from '../../utils/constants'; + + +const propTypes = { + createTable: PropTypes.func.isRequired, + onAddTable: PropTypes.func.isRequired, +}; + +class CreateTableDialog extends React.Component { + constructor(props) { + super(props); + this.state = { + tableName: '', + errMessage: '', + isSubmitBtnActive: false, + }; + this.newInput = React.createRef(); + } + + componentDidMount() { + this.newInput.focus(); + this.newInput.setSelectionRange(0,0); + } + + handleChange = (e) => { + if (!e.target.value.trim()) { + this.setState({isSubmitBtnActive: false}); + } else { + this.setState({isSubmitBtnActive: true}); + } + + this.setState({ + tableName: e.target.value, + }) ; + } + + handleSubmit = () => { + if (!this.state.isSubmitBtnActive) { + return; + } + + let isValid = this.validateInputParams(); + if (isValid) { + let newName = this.state.tableName.trim(); + this.props.createTable(newName); + } + } + + handleKeyPress = (e) => { + if (e.key === 'Enter') { + this.handleSubmit(); + e.preventDefault(); + } + } + + toggle = () => { + this.props.onAddTable(); + } + + validateInputParams = () => { + let errMessage = ''; + let tableName = this.state.tableName.trim(); + if (!tableName.length) { + errMessage = gettext('Name is required'); + this.setState({errMessage: errMessage}); + return false; + } + if (tableName.indexOf('/') > -1) { + errMessage = gettext('Name should not include \'/\'.'); + this.setState({errMessage: errMessage}); + return false; + } + return true; + } + + render() { + return ( + + {gettext('New Table')} + +
+ + + {this.newInput = input;}} + value={this.state.tableName} + onChange={this.handleChange} + /> + +
+ {this.state.errMessage && {this.state.errMessage}} +
+ + + + +
+ ); + } +} + +CreateTableDialog.propTypes = propTypes; + +export default CreateTableDialog; diff --git a/frontend/src/components/dialog/create-workspace-dialog.js b/frontend/src/components/dialog/create-workspace-dialog.js index 4f0bb69f3e..27e41ea5bf 100644 --- a/frontend/src/components/dialog/create-workspace-dialog.js +++ b/frontend/src/components/dialog/create-workspace-dialog.js @@ -5,11 +5,11 @@ import { gettext } from '../../utils/constants'; const propTypes = { - createWorkSpace: PropTypes.func.isRequired, - onCreateToggle: PropTypes.func.isRequired, + createWorkspace: PropTypes.func.isRequired, + onAddWorkspace: PropTypes.func.isRequired, }; -class CreateWorkSpaceDialog extends React.Component { +class CreateWorkspaceDialog extends React.Component { constructor(props) { super(props); this.state = { @@ -34,7 +34,7 @@ class CreateWorkSpaceDialog extends React.Component { let isValid= this.validateInputParams(); if (isValid) { let workspaceName = this.state.workspaceName.trim(); - this.props.createWorkSpace(workspaceName); + this.props.createWorkspace(workspaceName); } } @@ -46,7 +46,7 @@ class CreateWorkSpaceDialog extends React.Component { } toggle = () => { - this.props.onCreateToggle(); + this.props.onAddWorkspace(); } componentDidMount() { @@ -72,7 +72,7 @@ class CreateWorkSpaceDialog extends React.Component { render() { return ( - {gettext('New Library')} + {gettext('New Workspace')}
@@ -97,6 +97,6 @@ class CreateWorkSpaceDialog extends React.Component { } } -CreateWorkSpaceDialog.propTypes = propTypes; +CreateWorkspaceDialog.propTypes = propTypes; -export default CreateWorkSpaceDialog; +export default CreateWorkspaceDialog; diff --git a/frontend/src/components/dialog/delete-table-dialog.js b/frontend/src/components/dialog/delete-table-dialog.js new file mode 100644 index 0000000000..09528df9da --- /dev/null +++ b/frontend/src/components/dialog/delete-table-dialog.js @@ -0,0 +1,39 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { gettext } from '../../utils/constants'; +import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap'; + +const propTypes = { + currentTable: PropTypes.object.isRequired, + deleteCancel: PropTypes.func.isRequired, + handleSubmit: PropTypes.func.isRequired, +}; + +class DeleteTableDialog extends React.Component { + + toggle = () => { + this.props.deleteCancel(); + } + + render() { + let currentTable = this.props.currentTable; + let name = currentTable.name; + + return ( + + {gettext('Delete Table')} + +

{gettext('Are you sure to delete')}{' '}{name} ?

+
+ + + + +
+ ); + } +} + +DeleteTableDialog.propTypes = propTypes; + +export default DeleteTableDialog; diff --git a/frontend/src/components/dialog/delete-workspace-dialog.js b/frontend/src/components/dialog/delete-workspace-dialog.js new file mode 100644 index 0000000000..39a38cf7b4 --- /dev/null +++ b/frontend/src/components/dialog/delete-workspace-dialog.js @@ -0,0 +1,39 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { gettext } from '../../utils/constants'; +import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap'; + +const propTypes = { + currentWorkspace: PropTypes.object.isRequired, + deleteCancel: PropTypes.func.isRequired, + handleSubmit: PropTypes.func.isRequired, +}; + +class DeleteWorkspaceDialog extends React.Component { + + toggle = () => { + this.props.deleteCancel(); + } + + render() { + let currentWorkspace = this.props.currentWorkspace; + let name = currentWorkspace.name; + + return ( + + {gettext('Delete Workspace')} + +

{gettext('Are you sure to delete')}{' '}{name} ?

+
+ + + + +
+ ); + } +} + +DeleteWorkspaceDialog.propTypes = propTypes; + +export default DeleteWorkspaceDialog; diff --git a/frontend/src/pages/dtable/dtable.js b/frontend/src/pages/dtable/dtable.js index 6deb987e4c..0241fe4969 100644 --- a/frontend/src/pages/dtable/dtable.js +++ b/frontend/src/pages/dtable/dtable.js @@ -1,40 +1,220 @@ import React, { Component, Fragment } from 'react'; import PropTypes from 'prop-types'; -import { Button, ModalHeader, ModalBody, ModalFooter, Alert, Dropdown, DropdownToggle, DropdownMenu, DropdownItem } from 'reactstrap'; +import { Button, Dropdown, DropdownToggle, DropdownMenu, DropdownItem } from 'reactstrap'; import moment from 'moment'; import { seafileAPI } from '../../utils/seafile-api'; import { Utils } from '../../utils/utils'; import { gettext, siteRoot } from '../../utils/constants'; import Loading from '../../components/loading'; -import ModalPortal from '../../components/modal-portal'; -import CreateWorkSpaceDialog from '../../components/dialog/create-workspace-dialog'; +import CreateWorkspaceDialog from '../../components/dialog/create-workspace-dialog'; +import DeleteWorkspaceDialog from '../../components/dialog/delete-workspace-dialog'; +import CreateTableDialog from '../../components/dialog/create-table-dialog'; +import DeleteTableDialog from '../../components/dialog/delete-table-dialog'; +import Rename from '../../components/rename'; moment.locale(window.app.config.lang); -const itemPropTypes = { - item: PropTypes.object.isRequired, - renameWorkSpace: PropTypes.func.isRequired, - deleteWorkSpace: PropTypes.func.isRequired, +const tablePropTypes = { + table: PropTypes.object.isRequired, + repoID: PropTypes.string.isRequired, + renameTable: PropTypes.func.isRequired, + deleteTable: PropTypes.func.isRequired, }; -class Item extends Component { +class Table extends Component { constructor(props) { super(props); this.state = { + isTableRenaming: false, + isTableDeleting: false, dropdownOpen: false, - newName: '', + active: false, }; } - onRenameWorkSpace(workspace) { - let name = this.state.newName; - this.props.renameWorkSpace(workspace, name); + onRenameTableCancel = () => { + this.setState({isTableRenaming: !this.state.isTableRenaming}); } - onDeleteWorkSpace(workspace) { - this.props.deleteWorkSpace(workspace); + onRenameTableConfirm = (newTableName) => { + let oldTableName = this.props.table.name; + this.props.renameTable(oldTableName, newTableName); + this.onRenameTableCancel(); + } + + onDeleteTableCancel = () => { + this.setState({isTableDeleting: !this.state.isTableDeleting}); + } + + onDeleteTableSubmit = () => { + let tableName = this.props.table.name; + console.log(tableName); + this.props.deleteTable(tableName); + this.onDeleteTableCancel(); + } + + dropdownToggle = () => { + this.setState({ dropdownOpen: !this.state.dropdownOpen }); + } + + onMouseEnter = () => { + this.setState({ + active: true + }); + } + + onMouseLeave = () => { + this.setState({ + active: false + }); + } + + render() { + + let table = this.props.table; + let tableHref = siteRoot + 'lib/' + this.props.repoID + '/file' + Utils.encodePath(Utils.joinPath('/', table.name)); + + return ( + + + + {this.state.isTableRenaming && + + } + {!this.state.isTableRenaming && + {table.name} + } + + {table.modifier} + {moment(table.mtime).fromNow()} + + {this.state.active && + + + + + {gettext('Rename')} + {gettext('Delete')} + + + } + {this.state.isTableDeleting && + + } + + + ); + } +} + +Table.propTypes = tablePropTypes; + + +const workspacePropTypes = { + workspace: PropTypes.object.isRequired, + renameWorkspace: PropTypes.func.isRequired, + deleteWorkspace: PropTypes.func.isRequired, +}; + +class Workspace extends Component { + + constructor(props) { + super(props); + this.state = { + tableList: this.props.workspace.table_list, + errorMsg: '', + isWorkspaceRenaming: false, + isWorkspaceDeleting: false, + isShowAddTableDialog: false, + dropdownOpen: false, + }; + } + + onAddTable = () => { + this.setState({ + isShowAddTableDialog: !this.state.isShowAddTableDialog, + }); + } + + createTable = (tableName) => { + let workspaceID = this.props.workspace.id; + seafileAPI.createTable(workspaceID, tableName).then((res) => { + this.state.tableList.push(res.data.table); + this.setState({tableList: this.state.tableList}); + }).catch((error) => { + if(error.response) { + this.setState({errorMsg: gettext('Error')}); + } + }); + this.onAddTable(); + } + + renameTable = (oldTableName, newTableName) => { + let workspaceID = this.props.workspace.id; + seafileAPI.renameTable(workspaceID, oldTableName, newTableName).then((res) => { + let tableList = this.state.tableList.map((table) => { + if (table.name === oldTableName) { + table = res.data.table; + } + return table; + }); + this.setState({tableList: tableList}); + }).catch((error) => { + if(error.response) { + this.setState({errorMsg: gettext('Error')}); + } + }); + } + + deleteTable = (tableName) => { + let workspaceID = this.props.workspace.id; + seafileAPI.deleteTable(workspaceID, tableName).then(() => { + let tableList = this.state.tableList.filter(table => { + return table.name !== tableName; + }); + this.setState({tableList: tableList}); + }).catch((error) => { + if(error.response) { + this.setState({errorMsg: gettext('Error')}); + } + }); + } + + onRenameWorkspaceCancel = () => { + this.setState({isWorkspaceRenaming: !this.state.isWorkspaceRenaming}); + } + + onRenameWorkspaceConfirm = (newName) => { + let workspace = this.props.workspace; + this.props.renameWorkspace(workspace, newName); + this.onRenameWorkspaceCancel(); + } + + onDeleteWorkspaceCancel = () => { + this.setState({isWorkspaceDeleting: !this.state.isWorkspaceDeleting}); + } + + onDeleteWorkspaceSubmit = () => { + let workspace = this.props.workspace; + this.props.deleteWorkspace(workspace); + this.onDeleteWorkspaceCancel(); } dropdownToggle = () => { @@ -42,63 +222,89 @@ class Item extends Component { } render() { - let item = this.props.item; + let workspace = this.props.workspace; return( - {item.name} - - - - - {gettext('Rename')} - {gettext('Delete')} - - + {this.state.isWorkspaceRenaming && + + } + {!this.state.isWorkspaceRenaming && +
+ {workspace.name} + + + + + {gettext('Rename')} + {gettext('Delete')} + + + {this.state.isWorkspaceDeleting && + + } +
+ } - {item.table_list.map((table, index) => { - let tableHref = siteRoot + 'lib/' + item.repo_id + '/file' + Utils.encodePath(Utils.joinPath('/', table.name)); + {this.state.tableList.map((table, index) => { return ( - - - {table.name} - {table.modifier} - {moment(table.mtime).fromNow()} - - + ); })} - - + + ); } } -Item.propTypes = itemPropTypes; +Workspace.propTypes = workspacePropTypes; const contentPropTypes = { - items: PropTypes.array.isRequired, - renameWorkSpace: PropTypes.func.isRequired, - deleteWorkSpace: PropTypes.func.isRequired, + workspaceList: PropTypes.array.isRequired, + renameWorkspace: PropTypes.func.isRequired, + deleteWorkspace: PropTypes.func.isRequired, }; class Content extends Component { render() { - let items = this.props.items; + let workspaceList = this.props.workspaceList; return (
{gettext('Add a table')} + + {this.state.isShowAddTableDialog && + + } + {gettext('Add a DTable')}
@@ -110,13 +316,13 @@ class Content extends Component { - {items.map((item, index) => { + {workspaceList.map((workspace, index) => { return ( - ); })} @@ -135,38 +341,39 @@ class DTable extends Component { this.state = { loading: true, errorMsg: '', - items: [], - isShowCreateDialog: false, + workspaceList: [], + isShowAddWorkspaceDialog: false, }; } - onCreateToggle = () => { + onAddWorkspace = () => { this.setState({ - isShowCreateDialog: !this.state.isShowCreateDialog, + isShowAddWorkspaceDialog: !this.state.isShowAddWorkspaceDialog, }); } - createWorkSpace = (name) => { - seafileAPI.addWorkSpace(name).then((res) => { - this.state.items.push(res.data.workspace); - this.setState({items: this.state.items}); + createWorkspace = (name) => { + seafileAPI.createWorkspace(name).then((res) => { + this.state.workspaceList.push(res.data.workspace); + this.setState({workspaceList: this.state.workspaceList}); }).catch((error) => { if(error.response) { this.setState({errorMsg: gettext('Error')}); } }); - this.onCreateToggle(); + this.onAddWorkspace(); } - renameWorkSpace = (workspace, name) => { - seafileAPI.renameWorkSpace(workspace.id, name).then((res) => { - let items = this.state.items.map((item) => { - if (item.id === workspace.id) { - item = res.data; + renameWorkspace = (workspace, name) => { + let workspaceID = workspace.id; + seafileAPI.renameWorkspace(workspaceID, name).then((res) => { + let workspaceList = this.state.workspaceList.map((workspace) => { + if (workspace.id === workspaceID) { + workspace = res.data.workspace; } - return item; + return workspace; }); - this.setState({items: items}); + this.setState({workspaceList: workspaceList}); }).catch((error) => { if(error.response) { this.setState({errorMsg: gettext('Error')}); @@ -174,12 +381,13 @@ class DTable extends Component { }); } - deleteWorkSpace = (workspace) => { - seafileAPI.deleteWorkSpace(workspace.id).then(() => { - let items = this.state.items.filter(item => { - return item.id !== workspace.id; + deleteWorkspace = (workspace) => { + let workspaceID = workspace.id; + seafileAPI.deleteWorkspace(workspaceID).then(() => { + let workspaceList = this.state.workspaceList.filter(workspace => { + return workspace.id !== workspaceID; }); - this.setState({items: items}); + this.setState({workspaceList: workspaceList}); }).catch((error) => { if(error.response) { this.setState({errorMsg: gettext('Error')}); @@ -188,10 +396,10 @@ class DTable extends Component { } componentDidMount() { - seafileAPI.listWorkSpaces().then((res) => { + seafileAPI.listWorkspaces().then((res) => { this.setState({ loading: false, - items: res.data.workspace_list, + workspaceList: res.data.workspace_list, }); }).catch((error) => { if (error.response) { @@ -223,21 +431,21 @@ class DTable extends Component { {!this.state.loading &&
- {this.state.isShowCreateDialog && - + {gettext('Add a Workspace')} + + {this.state.isShowAddWorkspaceDialog && + } -
} diff --git a/seahub/api2/endpoints/dtable.py b/seahub/api2/endpoints/dtable.py index 3f167fc82d..909c0c66b3 100644 --- a/seahub/api2/endpoints/dtable.py +++ b/seahub/api2/endpoints/dtable.py @@ -104,7 +104,11 @@ class WorkSpacesView(APIView): error_msg = 'Internal Server Error.' return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) - return Response({"workspace": workspace.to_dict()}, status=status.HTTP_201_CREATED) + res = workspace.to_dict() + res["table_list"] = [] + res["updated_at"] = workspace.updated_at + + return Response({"workspace": res}, status=status.HTTP_201_CREATED) class WorkSpaceView(APIView): @@ -162,7 +166,20 @@ class WorkSpaceView(APIView): error_msg = 'Internal Server Error.' return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) - return Response({"workspace": workspace.to_dict()}, status=status.HTTP_200_OK) + table_objs = seafile_api.list_dir_by_path(repo_id, '/') + table_list = list() + for table_obj in table_objs: + table = dict() + table["name"] = table_obj.obj_name + table["mtime"] = timestamp_to_isoformat_timestr(table_obj.mtime) + table["modifier"] = email2nickname(table_obj.modifier) if table_obj.modifier else email2nickname(owner) + table_list.append(table) + + res = workspace.to_dict() + res["table_list"] = table_list + res["updated_at"] = workspace.updated_at + + return Response({"workspace": res}, status=status.HTTP_200_OK) def delete(self, request, workspace_id): """delete a workspace diff --git a/seahub/urls.py b/seahub/urls.py index 17538c48b2..018cec930a 100644 --- a/seahub/urls.py +++ b/seahub/urls.py @@ -347,7 +347,7 @@ urlpatterns = [ # user: workspaces url(r'^api/v2.1/workspaces/$', WorkSpacesView.as_view(), name='api-v2.1-workspaces'), url(r'^api/v2.1/workspace/(?P\d+)/$', WorkSpaceView.as_view(), name='api-v2.1-workspace'), - url(r'^api/v2.1/workspace/(?P\d+)/table/$', DTableView.as_view(), name='api-v2.1-workspace-table'), + url(r'^api/v2.1/workspace/(?P\d+)/dtable/$', DTableView.as_view(), name='api-v2.1-workspace-table'), # Deprecated url(r'^api/v2.1/repos/(?P[-0-9a-f]{36})/tags/$', FileTagsView.as_view(), name="api-v2.1-filetags-view"),