diff --git a/fabfile/locale.py b/fabfile/locale.py
index c4dcc49a10..1f31206ff5 100644
--- a/fabfile/locale.py
+++ b/fabfile/locale.py
@@ -37,6 +37,11 @@ def make(default=True, lang='en'):
local('django-admin.py makemessages -l %s -d djangojs -i "thirdpart" -i "node_modules" -i "media" -i "static/scripts/dist" -i "static/scripts/lib" -i "tests" -i "tools" -i "tagging" -i "static/scripts/i18n" --verbosity 2' % lang)
+@task
+def makejs(default=True, lang='en'):
+
+ local('django-admin.py makemessages -l %s -d djangojs -i "thirdpart" -i "node_modules" -i "media" -i "static/scripts/dist" -i "static/scripts/lib" -i "tests" -i "tools" -i "tagging" -i "static/scripts/i18n" -i "frontend/build" -i "frontend/config" -i "frontend/scripts" --verbosity 2' % lang)
+
@task
def push():
"""Push source file to Transifex.
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index f9d63795b3..3b53dd11d2 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -35,7 +35,7 @@
"promise": "8.0.1",
"prop-types": "15.6.2",
"raf": "3.4.0",
- "react": "16.2.0",
+ "react": "16.4.2",
"react-dev-utils": "5.0.0",
"react-dom": "16.2.0",
"react-s-alert": "1.4.1",
@@ -9517,9 +9517,9 @@
}
},
"react": {
- "version": "16.2.0",
- "resolved": "https://registry.npmjs.org/react/-/react-16.2.0.tgz",
- "integrity": "sha512-ZmIomM7EE1DvPEnSFAHZn9Vs9zJl5A9H7el0EGTE6ZbW9FKe/14IYAlPbC8iH25YarEQxZL+E8VW7Mi7kfQrDQ==",
+ "version": "16.4.2",
+ "resolved": "https://registry.npmjs.org/react/-/react-16.4.2.tgz",
+ "integrity": "sha512-dMv7YrbxO4y2aqnvA7f/ik9ibeLSHQJTI6TrYAenPSaQ6OXfb+Oti+oJiy8WBxgRzlKatYqtCjphTgDSCEiWFg==",
"requires": {
"fbjs": "0.8.16",
"loose-envify": "1.3.1",
@@ -10296,9 +10296,9 @@
}
},
"seafile-ui": {
- "version": "0.1.4",
- "resolved": "https://registry.npmjs.org/seafile-ui/-/seafile-ui-0.1.4.tgz",
- "integrity": "sha512-pV7jq4tuBGBCECNEyJxGeKyehFb2nqyvnmLZZY71LCFX4sFNI8j40KSWoKkLHrBUTW4SoP6keabzfuPBxPLQVg==",
+ "version": "0.1.8",
+ "resolved": "https://registry.npmjs.org/seafile-ui/-/seafile-ui-0.1.8.tgz",
+ "integrity": "sha512-k5HL8WMgHnptCBDjSDSJZzU3ZG+X60NDLXty9CQn68w5Ar0mHyDK6Ev1rvWIx3FdR0G+PIE23iTgjWXMMw31mQ==",
"requires": {
"bootstrap": "4.1.3"
}
diff --git a/frontend/package.json b/frontend/package.json
index 3c8446f11a..60d476884e 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -28,11 +28,11 @@
"prismjs": "^1.15.0",
"promise": "8.0.1",
"raf": "3.4.0",
- "react": "^16.2.0",
+ "react": "^16.4.2",
"react-cookies": "^0.1.0",
"react-dom": "^16.2.0",
"react-s-alert": "^1.4.1",
- "reactstrap": "^5.0.0-beta.3",
+ "reactstrap": "^6.4.0",
"rehype-format": "^2.2.0",
"rehype-raw": "^2.0.0",
"rehype-stringify": "^3.0.0",
@@ -42,7 +42,7 @@
"remark-rehype": "^3.0.0",
"remark-slug": "^5.0.0",
"seafile-js": "^0.2.7",
- "seafile-ui": "^0.1.4",
+ "seafile-ui": "^0.1.8",
"sw-precache-webpack-plugin": "0.11.4",
"unified": "^6.1.6",
"url-loader": "0.6.2",
diff --git a/frontend/src/components/MainPanel.js b/frontend/src/components/MainPanel.js
index 5f31ce845b..cc18f31e5c 100644
--- a/frontend/src/components/MainPanel.js
+++ b/frontend/src/components/MainPanel.js
@@ -2,7 +2,7 @@ import React, { Component } from 'react';
import Search from './search';
import MarkdownViewer from './markdown-viewer';
import Account from './account';
-import { repoID, serviceUrl, slug, siteRoot } from './constance';
+import { gettext, repoID, serviceUrl, slug, siteRoot } from './constance';
class MainPanel extends Component {
@@ -33,7 +33,7 @@ class MainPanel extends Component {
@@ -43,7 +43,7 @@ class MainPanel extends Component {
-
Wikis
+
{gettext("Wikis")}
/
{slug}
{pathElem}
diff --git a/frontend/src/components/SidePanel.js b/frontend/src/components/SidePanel.js
index e7e022e4a8..7813daca40 100644
--- a/frontend/src/components/SidePanel.js
+++ b/frontend/src/components/SidePanel.js
@@ -1,16 +1,219 @@
import React, { Component } from 'react';
import TreeView from './tree-view/tree-view';
import { siteRoot, logoPath, mediaUrl, siteTitle, logoWidth, logoHeight } from './constance';
+import Tree from './tree-view/tree';
+import Node from './tree-view/node'
+import NodeMenu from './menu-component/node-menu';
+import MenuControl from './menu-component/node-menu-control';
+const gettext = window.gettext;
class SidePanel extends Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ tree_data: new Tree(),
+ currentNode: null,
+ isNodeItemFrezee: false,
+ isShowMenu: false,
+ menuPosition: {
+ left: 0,
+ top: 0
+ },
+ isLoadFailed: false,
+ isMenuIconShow: false
+ }
+ }
+
closeSide = () => {
this.props.onCloseSide();
}
- onFileClick = (e, node) => {
+ onMouseEnter = () => {
+ this.setState({
+ isMenuIconShow: true
+ })
+ }
+
+ onMouseLeave = () => {
+ this.setState({
+ isMenuIconShow: false
+ })
+ }
+
+ onNodeClick = (e, node) => {
+ this.setState({
+ currentNode: node
+ })
this.props.onFileClick(e, node)
}
+ onShowContextMenu = (e, node) => {
+ let left = e.clientX - 8*16;
+ let top = e.clientY + 10;
+ let position = Object.assign({},this.state.menuPosition, {left: left, top: top});
+ this.setState({
+ isShowMenu: !this.state.isShowMenu,
+ currentNode: node,
+ menuPosition: position,
+ isNodeItemFrezee: true
+ })
+ }
+
+ onHeadingMenuClick = (e) => {
+ e.nativeEvent.stopImmediatePropagation();
+ let node = this.state.tree_data.root;
+ let left = e.clientX - 8*16;
+ let top = e.clientY + 10;
+ let position = Object.assign({},this.state.menuPosition, {left: left, top: top});
+ this.setState({
+ isShowMenu: !this.state.isShowMenu,
+ currentNode: node,
+ menuPosition: position
+ })
+ }
+
+ onHideContextMenu = () => {
+ this.setState({
+ isShowMenu: false,
+ isNodeItemFrezee: false
+ })
+ }
+
+ onAddFolderNode = (dirPath) => {
+ this.props.editorUtilities.createDir(dirPath).then(res => {
+ this.initializeTreeData()
+ })
+ }
+
+ onAddFileNode = (filePath) => {
+ this.props.editorUtilities.createFile(filePath).then(res => {
+ this.initializeTreeData()
+ })
+ }
+
+ onRenameNode = (newName) => {
+ var node = this.state.currentNode;
+ let type = node.type;
+ let filePath = node.path;
+ if (type === 'file') {
+ this.props.editorUtilities.renameFile(filePath, newName).then(res => {
+ this.initializeTreeData()
+ if (this.isModifyCurrentFile()) {
+ node.name = newName;
+ this.props.onFileClick(null, node);
+ }
+ })
+ }
+
+ if (type === 'dir') {
+ this.props.editorUtilities.renameDir(filePath, newName).then(res => {
+ this.initializeTreeData();
+ if (this.isModifyContainsCurrentFile()) {
+ let currentNode = this.state.currentNode;
+ let nodePath = encodeURI(currentNode.path);
+ let pathname = window.location.pathname;
+ let start = pathname.indexOf(nodePath);
+ let node = currentNode.getNodeByPath(decodeURI(pathname.slice(start)));
+ if(node){
+ currentNode.name = newName;
+ this.props.onFileClick(null, node);
+ }
+ }
+ })
+ }
+ }
+
+ onDeleteNode = () => {
+ var currentNode = this.state.currentNode;
+ let filePath = currentNode.path;
+ let type = currentNode.type;
+ if (type === 'file') {
+ this.props.editorUtilities.deleteFile(filePath).then(res => {
+ this.initializeTreeData();
+ })
+ }
+
+ if (type === 'dir') {
+ this.props.editorUtilities.deleteDir(filePath).then(res => {
+ this.initializeTreeData();
+ })
+ }
+
+ let isCurrentFile = false;
+ if (this.state.currentNode.type === "dir") {
+ isCurrentFile = this.isModifyContainsCurrentFile();
+ } else {
+ isCurrentFile = this.isModifyCurrentFile();
+ }
+
+ if (isCurrentFile) {
+ let homeNode = this.getHomeNode();
+ this.props.onFileClick(null, homeNode);
+ }
+ }
+
+ isModifyCurrentFile() {
+ let name = this.state.currentNode.name;
+ let pathname = window.location.pathname;
+ let currentName = pathname.slice(pathname.lastIndexOf("/") + 1);
+ return name === currentName;
+ }
+
+ isModifyContainsCurrentFile() {
+ let pathname = window.location.pathname;
+ let nodePath = this.state.currentNode.path;
+ if (pathname.indexOf(nodePath)) {
+ return true;
+ }
+ return false;
+ }
+
+ initializeTreeData() {
+ this.props.editorUtilities.getFiles().then((files) => {
+ // construct the tree object
+ var rootObj = {
+ name: '/',
+ type: 'dir',
+ isExpanded: true
+ }
+ var treeData = new Tree();
+ treeData.parseFromList(rootObj, files);
+ let homeNode = this.getHomeNode(treeData);
+ this.setState({
+ tree_data: treeData,
+ currentNode: homeNode
+ })
+ }, () => {
+ console.log("failed to load files");
+ this.setState({
+ isLoadFailed: true
+ })
+ })
+ }
+
+ getHomeNode(treeData) {
+ let root = null;
+ if (treeData) {
+ root = treeData.root;
+ } else {
+ root = this.state.tree_data.root;
+ }
+ let homeNode = root.getNodeByPath(decodeURI("/home.md"));
+ return homeNode;
+ }
+
+ componentDidMount() {
+ //init treeview data
+ this.initializeTreeData();
+
+ document.addEventListener('click', this.onHideContextMenu);
+ }
+
+ componentWillUnmount() {
+ document.removeEventListener('click', this.onHideContextMenu);
+ }
+
render() {
return (
@@ -21,11 +224,39 @@ class SidePanel extends Component {
-
Pages
+
+ {gettext("Pages")}
+
+
+
+
+ {this.state.tree_data &&
+ }
+
diff --git a/frontend/src/components/account.js b/frontend/src/components/account.js
index 5975885096..bca8548dfc 100644
--- a/frontend/src/components/account.js
+++ b/frontend/src/components/account.js
@@ -2,7 +2,7 @@ import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import cookie from 'react-cookies';
import { keyCodes, bytesToSize } from './utils';
-import { siteRoot, avatarInfo } from './constance';
+import { siteRoot, avatarInfo, gettext } from './constance';
class Account extends Component {
@@ -88,7 +88,7 @@ class Account extends Component {
renderMenu = () => {
if(this.state.isStaff){
return (
-
System Admin
+
{gettext("System Admin")}
)
}
}
@@ -124,15 +124,15 @@ class Account extends Component {
-
Used: {this.state.quotaUsage} / {this.state.quotaTotal}
+
{gettext("Used")}: {this.state.quotaUsage} / {this.state.quotaTotal}
-
Settings
+
{gettext("Settings")}
{this.renderMenu()}
-
Log out
+
{gettext("Log out")}
diff --git a/frontend/src/components/constance.js b/frontend/src/components/constance.js
index 829393a3ce..096a223c6d 100644
--- a/frontend/src/components/constance.js
+++ b/frontend/src/components/constance.js
@@ -1,4 +1,5 @@
export const dirPath = '/';
+export const gettext = window.gettext;
export const siteRoot = window.app.config.siteRoot;
export const avatarInfo = window.app.config.avatarInfo;
diff --git a/frontend/src/components/menu-component/menu-dialog/create-fileforder-dialog.js b/frontend/src/components/menu-component/menu-dialog/create-fileforder-dialog.js
new file mode 100644
index 0000000000..1a754cb5c4
--- /dev/null
+++ b/frontend/src/components/menu-component/menu-dialog/create-fileforder-dialog.js
@@ -0,0 +1,100 @@
+import React from 'react';
+import { Button, Modal, ModalHeader, Input, ModalBody, ModalFooter, Form, FormGroup, Label, Col, FormText } from 'reactstrap';
+const gettext = window.gettext;
+
+class CreateFileForder extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ parentPath: '',
+ childName: ''
+ };
+ this.newInput = React.createRef()
+ }
+
+ handleChange = (e) => {
+ this.setState({
+ childName: e.target.value,
+ })
+ }
+
+ handleSubmit = () => {
+ let path = this.state.parentPath + this.state.childName
+ if (this.props.isFile) {
+ this.props.onAddFile(path);
+ } else {
+ this.props.onAddFolder(path);
+ }
+ }
+
+ toggle = () => {
+ if (this.props.isFile) {
+ this.props.addFileCancel();
+ } else {
+ this.props.addFolderCancel();
+ }
+ }
+
+ componentWillMount() {
+ this.changeState(this.props.isFile);
+ }
+
+ componentDidMount() {
+ if (this.props.currentNode.path === "/") {
+ this.setState({parentPath: this.props.currentNode.path});
+ } else {
+ this.setState({parentPath: this.props.currentNode.path + "/"});
+ }
+ this.newInput.focus();
+ this.newInput.setSelectionRange(0,0);
+ }
+
+ changeState(isFile) {
+ if (isFile) {
+ this.setState({childName: '.md'});
+ } else{
+ this.setState({childName: ""});
+ }
+ }
+
+ componentWillReceiveProps(nextProps) {
+ this.changeState(nextProps.isFile);
+ }
+
+ changeState(isFile) {
+ if (isFile) {
+ this.setState({childName: '.md'});
+ } else{
+ this.setState({childName: ""});
+ }
+ }
+
+
+ render() {
+ return (
+
+ {this.props.isFile ? gettext("New File") : gettext("New Folder")}
+
+
+
+
+
+
+
+
+ )
+ }
+}
+
+export default CreateFileForder;
diff --git a/frontend/src/components/menu-component/menu-dialog/delete-dialog.js b/frontend/src/components/menu-component/menu-dialog/delete-dialog.js
new file mode 100644
index 0000000000..d2c3fe872f
--- /dev/null
+++ b/frontend/src/components/menu-component/menu-dialog/delete-dialog.js
@@ -0,0 +1,29 @@
+import React from 'react';
+import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap';
+
+const gettext = window.gettext;
+
+class Delete extends React.Component {
+
+ toggle = () => {
+ this.props.toggleCancel();
+ }
+
+ render() {
+ let name = this.props.currentNode.name;
+ return (
+
+ {gettext("Delete")}
+
+ {gettext("Are you sure to delete")}{' '}{name} ?
+
+
+
+
+
+
+ )
+ }
+}
+
+export default Delete;
diff --git a/frontend/src/components/menu-component/menu-dialog/rename-dialog.js b/frontend/src/components/menu-component/menu-dialog/rename-dialog.js
new file mode 100644
index 0000000000..06b01b258d
--- /dev/null
+++ b/frontend/src/components/menu-component/menu-dialog/rename-dialog.js
@@ -0,0 +1,73 @@
+import React from 'react';
+import { Button, Modal, ModalHeader, Input, ModalBody, ModalFooter } from 'reactstrap';
+const gettext = window.gettext;
+
+class Rename extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ newName: '',
+ };
+ this.newInput = React.createRef();
+ }
+
+ handleChange = (e) => {
+ this.setState({
+ newName: e.target.value,
+ });
+ }
+
+ handleSubmit = () => {
+ this.props.onRename(this.state.newName);
+ }
+
+ toggle = () => {
+ this.props.toggleCancel();
+ }
+
+ componentWillMount() {
+ this.setState({
+ newName: this.props.currentNode.name
+ })
+ }
+
+ componentDidMount() {
+ this.changeState(this.props.currentNode);
+ this.newInput.focus();
+ let type = this.props.currentNode.type;
+ if (type === "file") {
+ var endIndex = this.props.currentNode.name.lastIndexOf(".md");
+ this.newInput.setSelectionRange(0, endIndex, "forward");
+ } else {
+ this.newInput.setSelectionRange(0, -1);
+ }
+ }
+
+ componentWillReceiveProps(nextProps) {
+ this.changeState(nextProps.currentNode);
+ }
+
+ changeState(currentNode) {
+ this.setState({newName: currentNode.name});
+ }
+
+ render() {
+ let type = this.props.currentNode.type;
+ let preName = this.props.currentNode.name;
+ return (
+
+ {type === 'file' ? gettext("Rename File") : gettext("Rename Folder") }
+
+ {type === 'file' ? gettext("Enter the new file name:"): gettext("Enter the new folder name:")}
+ {this.newInput = input}} placeholder="newName" value={this.state.newName} onChange={this.handleChange} />
+
+
+
+
+
+
+ )
+ }
+}
+
+export default Rename;
diff --git a/frontend/src/components/menu-component/node-menu-control.js b/frontend/src/components/menu-component/node-menu-control.js
new file mode 100644
index 0000000000..97442f91e8
--- /dev/null
+++ b/frontend/src/components/menu-component/node-menu-control.js
@@ -0,0 +1,21 @@
+import React from 'react';
+
+class NodeMenuControl extends React.Component {
+
+ onClick = (e) => {
+ let node = this.props.currentNode;
+ this.props.onClick(e, node);
+ }
+
+ render() {
+ return (
+
+
+ )
+ }
+}
+
+export default NodeMenuControl;
\ No newline at end of file
diff --git a/frontend/src/components/menu-component/node-menu.js b/frontend/src/components/menu-component/node-menu.js
new file mode 100644
index 0000000000..9df628bba1
--- /dev/null
+++ b/frontend/src/components/menu-component/node-menu.js
@@ -0,0 +1,166 @@
+import React from 'react'
+import Delete from './menu-dialog/delete-dialog';
+import CreateFlieFolder from './menu-dialog/create-fileforder-dialog';
+import Rename from './menu-dialog/rename-dialog';
+
+const gettext = window.gettext;
+
+class NodeMenu extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ showDelete: false,
+ showAddFileFolder: false,
+ showRename: false,
+ isFile: false
+ };
+ }
+
+ toggleDelete = () => {
+ this.setState({showDelete: !this.state.showDelete});
+ this.props.onHideContextMenu();
+ }
+
+ toggleAddFileFolder = (ev, flag) => {
+ if(flag){
+ this.setState({
+ showAddFileFolder: !this.state.showAddFileFolder,
+ isFile: true
+ });
+ } else {
+ this.setState({
+ showAddFileFolder: !this.state.showAddFileFolder,
+ isFile: false
+ })
+ }
+ this.props.onHideContextMenu();
+ }
+
+ toggleRename = () => {
+ this.setState({showRename: !this.state.showRename});
+ this.props.onHideContextMenu();
+ }
+
+ onDelete = () => {
+ this.setState({showDelete: !this.state.showDelete});
+ this.props.onDeleteNode();
+ }
+
+ deleteCancel = () => {
+ this.setState({showDelete: !this.state.showDelete});
+ }
+
+ onAddFile = (filePath) => {
+ this.setState({
+ showAddFileFolder: !this.state.showAddFileFolder,
+ isFile: false
+ });
+ this.props.onAddFileNode(filePath);
+ }
+
+ addFileCancel = () => {
+ this.setState({
+ showAddFileFolder: !this.state.showAddFileFolder,
+ isFile: false
+ });
+ }
+
+ onAddFolder = (dirPath) => {
+ this.setState({
+ showAddFileFolder: !this.state.showAddFileFolder,
+ isFile: false
+ });
+ this.props.onAddFolderNode(dirPath);
+ }
+
+ addFolderCancel = () => {
+ this.setState({
+ showAddFileFolder: !this.state.showAddFileFolder,
+ isFile: false
+ });
+ }
+
+ onRename = (newName) => {
+ this.setState({showRename: !this.state.showRename});
+ this.props.onRenameNode(newName);
+ }
+
+ renameCancel = () => {
+ this.setState({showRename: !this.state.showRename});
+ }
+
+ renderNodeMenu() {
+ let style = null;
+ let position = this.props.menuPosition;
+ if (this.props.isShowMenu) {
+ style = {position: "fixed",left: position.left, top: position.top, display: 'block'};
+ }
+
+ if (this.props.currentNode.type === "dir") {
+
+ if (this.props.currentNode.name === "/") {
+ return (
+
+ - {gettext("New Folder")}
+ - this.toggleAddFileFolder(ev,true)}>{gettext("New File")}
+
+ )
+ }
+
+ return (
+
+ - {gettext("New Folder")}
+ - this.toggleAddFileFolder(ev,true)}>{gettext("New File")}
+ - {gettext("Rename")}
+ - {gettext("Delete")}
+
+ )
+ }
+
+ return (
+
+ - {gettext("Rename")}
+ - {gettext("Delete")}
+
+ )
+
+ }
+
+ render() {
+ if (!this.props.currentNode) {
+ return (
)
+ }
+ return (
+
+ {this.renderNodeMenu()}
+ {this.state.showDelete &&
+
+ }
+ {this.state.showAddFileFolder &&
+
+ }
+ {this.state.showRename &&
+
+ }
+
+ )
+ }
+}
+
+export default NodeMenu;
diff --git a/frontend/src/components/search.js b/frontend/src/components/search.js
index 38d74cbe1b..b8251a3152 100644
--- a/frontend/src/components/search.js
+++ b/frontend/src/components/search.js
@@ -1,5 +1,5 @@
import React, { Component } from 'react';
-import { repoID } from './constance';
+import { gettext, repoID } from './constance';
import SearchResultItem from './SearchResultItem';
class Search extends Component {
@@ -195,7 +195,7 @@ class Search extends Component {
type="text"
className="search-input"
name="query"
- placeholder="Search files in this wiki"
+ placeholder={gettext("Search files in this wiki")}
style={style}
value={this.state.value}
onFocus={this.onFocusHandler}
diff --git a/frontend/src/components/tree-view/node.js b/frontend/src/components/tree-view/node.js
index 1456e3376f..155c2e479c 100644
--- a/frontend/src/components/tree-view/node.js
+++ b/frontend/src/components/tree-view/node.js
@@ -100,6 +100,33 @@ class Node {
return this.type == "dir";
}
+ getleafPaths() {
+ let paths = new Map();
+ function getleafPath(node){
+ if (node.hasChildren()) {
+ let children = node.children;
+ children.forEach(child => {
+ if (child.hasChildren()) {
+ getleafPath(child);
+ } else {
+ let path = child.path;
+ paths.set(path,child);
+ }
+ });
+ }
+ }
+ getleafPath(this);
+ return paths;
+ }
+
+ getNodeByPath(path) {
+ let paths = this.getleafPaths();
+ if (paths.has(path)) {
+ return paths.get(path);
+ }
+ return null;
+ }
+
/**
* Return a JSON representation of the node.
*
diff --git a/frontend/src/components/tree-view/tree-node-view.js b/frontend/src/components/tree-view/tree-node-view.js
index ef8e81ccb0..e4eda3bc2f 100644
--- a/frontend/src/components/tree-view/tree-node-view.js
+++ b/frontend/src/components/tree-view/tree-node-view.js
@@ -1,4 +1,5 @@
import React from 'react';
+import MenuControl from '../menu-component/node-menu-control'
function sortByType(a, b) {
if (a.type == "dir" && b.type != "dir") {
@@ -12,6 +13,69 @@ function sortByType(a, b) {
class TreeNodeView extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ isMenuIconShow: false
+ }
+ }
+
+ onClick = (e) => {
+ // e.nativeEvent.stopImmediatePropagation();
+ let { node } = this.props;
+ this.props.treeView.onNodeClick(e, node);
+ }
+
+ onMouseEnter = () => {
+ if (!this.props.isNodeItemFrezee) {
+ this.setState({
+ isMenuIconShow: true
+ })
+ }
+ }
+
+ onMouseLeave = () => {
+ if (!this.props.isNodeItemFrezee) {
+ this.setState({
+ isMenuIconShow: false
+ })
+ }
+ }
+
+ handleCollapse = (e) => {
+ e.stopPropagation();
+ const { node } = this.props;
+ if (this.props.treeView.toggleCollapse) {
+ this.props.treeView.toggleCollapse(node);
+ }
+ }
+
+ onDragStart = (e) => {
+ const { node } = this.props;
+ this.props.treeView.onDragStart(e, node);
+ }
+
+ onMenuControlClick = (e) => {
+ e.stopPropagation();
+ e.nativeEvent.stopImmediatePropagation();
+ const { node } = this.props;
+ this.props.treeView.onShowContextMenu(e, node);
+ }
+
+ hideMenuIcon = () => {
+ this.setState({
+ isMenuIconShow: false
+ });
+ }
+
+ componentDidMount() {
+ document.addEventListener('click', this.hideMenuIcon);
+ }
+
+ componentWillUnmount() {
+ document.removeEventListener('click', this.hideMenuIcon);
+ }
+
renderCollapse = () => {
const { node } = this.props;
@@ -50,6 +114,8 @@ class TreeNodeView extends React.Component {
key={child.path}
paddingLeft={this.props.paddingLeft}
treeView={this.props.treeView}
+ isNodeItemFrezee={this.props.isNodeItemFrezee}
+ permission={this.props.permission}
/>
);
})}
@@ -60,16 +126,30 @@ class TreeNodeView extends React.Component {
return null;
}
- render() {
- const { node } = this.props;
- const styles = {};
- var icon, type;
- if (node.type === "dir") {
+ renderMenuController() {
+ if (this.props.permission === "rw") {
+ return (
+
+
+
+ )
+ }
+ return;
+ }
+
+ getNodeTypeAndIcon() {
+ const node = this.props.node;
+ let icon = '';
+ let type = '';
+ if (node.type === 'dir') {
icon =
;
type = 'dir';
- } else {
+ } else {
let index = node.name.lastIndexOf(".");
- if (index === -1) {
+ if (index === -1) {
icon =
;
type = 'file';
} else {
@@ -84,52 +164,37 @@ class TreeNodeView extends React.Component {
}
}
+ return { type, icon };
+
+
+ }
+
+ render() {
+ const styles = {};
+ let node = this.props.node;
+ let { type, icon } = this.getNodeTypeAndIcon();
+
return (
-
-
+
- {this.renderCollapse()}
-
- {icon}
-
-
{node.name}
+ type={type}
+ className={`tree-node-inner text-nowrap ${node.name === '/'? 'hide': ''}`}
+ >
+
{node.name}
+
+ {this.renderCollapse()}
+ {icon}
+
+ {this.renderMenuController()}
{node.isExpanded ? this.renderChildren() : null}
);
}
- onClick = e => {
- let { node } = this.props;
- this.props.treeView.onClick(e, node);
- }
-
- onMouseEnter = e => {
- let { node } = this.props;
- this.props.treeView.showImagePreview(e, node);
- }
-
- onMouseLeave = e => {
- this.props.treeView.hideImagePreview(e);
- }
-
- handleCollapse = e => {
- e.stopPropagation();
- const { node } = this.props;
- if (this.props.treeView.toggleCollapse) {
- this.props.treeView.toggleCollapse(node);
- }
- }
-
- onDragStart = e => {
- const { node } = this.props;
- this.props.treeView.onDragStart(e, node);
- }
-
}
export default TreeNodeView;
diff --git a/frontend/src/components/tree-view/tree-view.js b/frontend/src/components/tree-view/tree-view.js
index 1d9b5c796c..22e8aa13fc 100644
--- a/frontend/src/components/tree-view/tree-view.js
+++ b/frontend/src/components/tree-view/tree-view.js
@@ -1,110 +1,8 @@
import React from 'react';
import TreeNodeView from './tree-node-view';
-import Tree from './tree';
class TreeView extends React.PureComponent {
- static defaultProps = {
- paddingLeft: 20
- };
-
- imagePreviewTimeout = null
-
- state = {
- tree: new Tree(),
- loadingFailed: false,
- imagePreviewPosition: {
- left: 10+'px',
- top: 10+'px'
- },
- isShowImagePreview: false,
- imagePreviewLoading: false,
- imageSrc: '',
- }
-
- showImagePreview = (e, node) => {
- e.persist();
-
- let type = e.target.getAttribute('type');
- if (type === 'image') {
- this.imagePreviewTimeout = setTimeout(() => {
- let X = e.clientX + 20;
- let Y = e.clientY - 55;
- if (e.view.innerHeight < e.clientY + 150) {
- Y = e.clientY - 219;
- }
- this.setState({
- isShowImagePreview: true,
- imagePreviewLoading: true,
- imageSrc: this.props.editorUtilities.getFileURL(node),
- imagePreviewPosition: {
- left: X + 'px',
- top: Y + 'px'
- }
- });
- }, 1000)
- }
- }
-
- hideImagePreview = (e) => {
- clearTimeout(this.imagePreviewTimeout);
- this.setState({
- isShowImagePreview: false,
- imagePreviewLoading: false,
- });
- }
-
- imageLoaded = () => {
- this.setState({
- imagePreviewLoading: false,
- });
- }
-
- componentDidMount() {
- this.props.editorUtilities.getFiles().then((files) => {
- // construct the tree object
- var rootObj = {
- name: '/',
- type: 'dir',
- isExpanded: true
- }
- var treeData = new Tree();
- treeData.parseFromList(rootObj, files);
- this.setState({
- tree: treeData
- })
- }, () => {
- console.log("failed to load files");
- this.setState({
- loadingFailed: true
- })
- })
- }
-
-
- render() {
- const tree = this.state.tree;
- if (!tree.root) {
- return
Loading...
- }
-
- return (
-
-
- { this.state.isShowImagePreview &&
-
- { this.state.imagePreviewLoading &&
}
-

-
- }
-
- );
- }
-
change = (tree) => {
/*
this._updated = true;
@@ -113,7 +11,7 @@ class TreeView extends React.PureComponent {
}
toggleCollapse = (node) => {
- const tree = this.state.tree;
+ const tree = this.props.treeData;
node.isExpanded = !node.isExpanded;
// copy the tree to make PureComponent work
@@ -130,12 +28,35 @@ class TreeView extends React.PureComponent {
e.dataTransfer.setData("text/plain", url);
}
- onClick = (e, node) => {
+ onNodeClick = (e, node) => {
if (node.isDir()) {
this.toggleCollapse(node);
return;
}
- this.props.onClick(e, node);
+ this.props.onNodeClick(e, node);
+ }
+
+ onShowContextMenu = (e, node) => {
+ this.props.onShowContextMenu(e, node);
+ }
+
+ render() {
+ if (!this.props.treeData.root) {
+ return
Loading...
+ }
+
+ return (
+
+
+
+ );
}
}
diff --git a/frontend/src/css/side-panel.css b/frontend/src/css/side-panel.css
index a9422beed0..67d2801310 100644
--- a/frontend/src/css/side-panel.css
+++ b/frontend/src/css/side-panel.css
@@ -1,38 +1,8 @@
-/*tree view */
-.tree-node:not([type = 'dir']):hover {
- background-color: rgb(255,239,178);
-}
-
-.tree-node {
- min-width: -moz-max-content;
- min-width: -webkit-max-content;
- min-width: max-content;
-}
-
-.tree-node-inner {
- position: relative;
- padding-left: 12px;
-}
-
/*
the main reason to icon can not be align is that .folder has a real width it take the place
of .tree-node-inner causing tree-node-icon not aligned , use absolute can make sure .tree-node-icon
is always at the far left of .tree-node-inner
*/
-.folder-toggle-icon {
- position: absolute;
- left: 0;
- line-height: 1.5;
-}
-
-.tree-node-icon {
- margin-right: 0.4rem;
- margin-left: 0.1rem;
- display: inline-block;
- width: 1rem;
- text-align: center;
-}
-
.side-panel {
user-select: none;
height:100%;
@@ -119,7 +89,89 @@
.outline-h3:hover {
color: #eb8205;
}
+
+/*tree view */
+.tree-node:not([type = 'dir']):hover {
+ background-color: rgb(255,239,178);
+}
+
.tree-view {
padding-left: 10px;
line-height: 1.5;
}
+
+.tree-node-inner {
+ position: relative;
+ padding-left: 12px;
+ height: 24px;
+}
+
+.tree-node-inner .tree-node-text {
+ padding-left: 1.2rem;
+ width: calc(100% - 1.5rem);
+ font-size: 15px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.tree-node-inner .left-icon {
+ position: absolute;
+ display:flex;
+ justify-content:center;
+ align-items:center;
+ top: 0;
+ left: 0;
+ padding-left: 0.7rem;
+}
+
+.folder-toggle-icon {
+ position: absolute;
+ left: 0;
+ line-height: 1.5;
+}
+
+.tree-node-icon {
+ margin-right: 0.4rem;
+ margin-left: 0.1rem;
+ display: inline-block;
+ width: 1rem;
+ text-align: center;
+}
+
+.tree-node-inner .right-icon {
+ position: absolute;
+ top: 0;
+ right: 0;
+ width: 1.5rem;
+ color: #888;
+ z-index: 2;
+ font-size: 0.8125rem;
+ text-align: center;
+}
+
+.tree-node-inner .right-icon i {
+ width: 100%;
+ height: 100%;
+ vertical-align: middle;
+ font-size: 0.8125rem;
+ line-height: 1.625 !important;
+}
+
+.dropdown-menu {
+ min-width: 8rem;
+}
+
+.parent-path {
+ position: relative;
+}
+
+.parent-path .form-text {
+ position: absolute;
+ top: 50%;
+ transform: translateY(-50%);
+ margin-top: 0;
+ margin-left: 0.25rem;
+ font-size: 0.9375rem;
+}
+
diff --git a/frontend/src/css/wiki.css b/frontend/src/css/wiki.css
index a8ce87f572..f28e60c172 100644
--- a/frontend/src/css/wiki.css
+++ b/frontend/src/css/wiki.css
@@ -8,6 +8,7 @@
}
.wiki-pages-heading {
+ position: relative;
font-size: 1rem;
font-weight: normal;
text-align:center;
@@ -16,6 +17,13 @@
line-height: 1.5;
height:40px;
}
+.heading-icon {
+ position: absolute;
+ right: 1rem;
+ top: 25%;
+ color: #888;
+ font-size: 0.8125rem;
+}
.wiki-pages-container {
overflow: hidden;
padding-bottom: 10px;
diff --git a/frontend/src/wiki.js b/frontend/src/wiki.js
index dca102938a..2c208b7f80 100644
--- a/frontend/src/wiki.js
+++ b/frontend/src/wiki.js
@@ -33,6 +33,31 @@ class EditorUtilities {
return files;
})
}
+
+ createFile(filePath) {
+ return seafileAPI.createFile(repoID, filePath)
+ }
+
+ deleteFile(filePath) {
+ return seafileAPI.deleteFile(repoID, filePath)
+ }
+
+ renameFile(filePath, newFileName) {
+ return seafileAPI.renameFile(repoID, filePath, newFileName)
+ }
+
+ createDir(dirPath) {
+ return seafileAPI.createDir(repoID, dirPath)
+ }
+
+ deleteDir(dirPath) {
+ return seafileAPI.deleteDir(repoID, dirPath)
+ }
+
+ renameDir(dirPath, newDirName) {
+ return seafileAPI.renameDir(repoID, dirPath, newDirName)
+ }
+
}
const editorUtilities = new EditorUtilities();
@@ -115,7 +140,7 @@ class Wiki extends Component {
})
})
- let fileUrl = siteRoot + 'wikis/' + slug + filePath;
+ let fileUrl = serviceUrl + '/wikis/' + slug + filePath;
window.history.pushState({urlPath: fileUrl, filePath: filePath}, filePath, fileUrl);
}
@@ -145,6 +170,7 @@ class Wiki extends Component {
closeSideBar={this.state.closeSideBar}
onCloseSide ={this.onCloseSide}
editorUtilities={editorUtilities}
+ permission={this.state.permission}
/>
\n"
"Language-Team: LANGUAGE \n"
@@ -17,13 +17,120 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
+#: frontend/src/components/MainPanel.js:36
+msgid "Edit Page"
+msgstr ""
+
+#: frontend/src/components/MainPanel.js:46
+msgid "Wikis"
+msgstr ""
+
+#: frontend/src/components/SidePanel.js:232
+msgid "Pages"
+msgstr ""
+
+#: frontend/src/components/account.js:91
+msgid "System Admin"
+msgstr ""
+
+#: frontend/src/components/account.js:127
+msgid "Used"
+msgstr ""
+
+#: frontend/src/components/account.js:133
+msgid "Settings"
+msgstr ""
+
+#: frontend/src/components/account.js:135
+msgid "Log out"
+msgstr ""
+
+#: frontend/src/components/menu-component/menu-dialog/create-fileforder-dialog.js:76
+#: frontend/src/components/menu-component/node-menu.js:106
+#: frontend/src/components/menu-component/node-menu.js:114
+#: static/scripts/app/views/dir.js:770
+msgid "New File"
+msgstr ""
+
+#: frontend/src/components/menu-component/menu-dialog/create-fileforder-dialog.js:76
+#: frontend/src/components/menu-component/node-menu.js:105
+#: frontend/src/components/menu-component/node-menu.js:113
+msgid "New Folder"
+msgstr ""
+
+#: frontend/src/components/menu-component/menu-dialog/create-fileforder-dialog.js:84
+msgid "Name"
+msgstr ""
+
+#: frontend/src/components/menu-component/menu-dialog/create-fileforder-dialog.js:86
+msgid "newName"
+msgstr ""
+
+#: frontend/src/components/menu-component/menu-dialog/create-fileforder-dialog.js:92
+#: frontend/src/components/menu-component/menu-dialog/rename-dialog.js:65
+msgid "Submit"
+msgstr ""
+
+#: frontend/src/components/menu-component/menu-dialog/create-fileforder-dialog.js:93
+#: frontend/src/components/menu-component/menu-dialog/rename-dialog.js:66
+#: static/scripts/app/views/fileupload.js:21
+msgid "Cancel"
+msgstr ""
+
+#: frontend/src/components/menu-component/menu-dialog/delete-dialog.js:16
+#: frontend/src/components/menu-component/node-menu.js:116
+#: frontend/src/components/menu-component/node-menu.js:124
+#: static/scripts/app/views/fileupload.js:22
+#: static/scripts/sysadmin-app/views/device-trusted-ipaddress.js:27
+msgid "Delete"
+msgstr ""
+
+#: frontend/src/components/menu-component/menu-dialog/delete-dialog.js:18
+msgid "Are you sure to delete"
+msgstr ""
+
+#: frontend/src/components/menu-component/menu-dialog/delete-dialog.js:21
+msgid "YES"
+msgstr ""
+
+#: frontend/src/components/menu-component/menu-dialog/delete-dialog.js:22
+msgid "NO"
+msgstr ""
+
+#: frontend/src/components/menu-component/menu-dialog/rename-dialog.js:59
+#: static/scripts/app/views/dialogs/dirent-rename.js:39
+msgid "Rename File"
+msgstr ""
+
+#: frontend/src/components/menu-component/menu-dialog/rename-dialog.js:59
+#: static/scripts/app/views/dialogs/dirent-rename.js:39
+msgid "Rename Folder"
+msgstr ""
+
+#: frontend/src/components/menu-component/menu-dialog/rename-dialog.js:61
+msgid "Enter the new file name:"
+msgstr ""
+
+#: frontend/src/components/menu-component/menu-dialog/rename-dialog.js:61
+msgid "Enter the new folder name:"
+msgstr ""
+
+#: frontend/src/components/menu-component/node-menu.js:115
+#: frontend/src/components/menu-component/node-menu.js:123
+msgid "Rename"
+msgstr ""
+
+#: frontend/src/components/search.js:198
+msgid "Search files in this wiki"
+msgstr ""
+
#: static/scripts/app/models/deleted-repo.js:19
-#: static/scripts/app/models/repo.js:59 static/scripts/common.js:333
+#: static/scripts/app/models/repo.js:59 static/scripts/common.js:343
msgid "Encrypted library"
msgstr ""
#: static/scripts/app/models/deleted-repo.js:21
-#: static/scripts/app/models/repo.js:61 static/scripts/common.js:337
+#: static/scripts/app/models/repo.js:61 static/scripts/common.js:347
#: static/scripts/sysadmin-app/models/trash-repo.js:14
msgid "Read-Write library"
msgstr ""
@@ -34,7 +141,7 @@ msgstr ""
#: static/scripts/app/views/fileupload.js:406
#: static/scripts/app/views/fileupload.js:418
#: static/scripts/app/views/fileupload.js:430
-#: static/scripts/app/views/group.js:63 static/scripts/common.js:611
+#: static/scripts/app/views/group.js:65 static/scripts/common.js:640
msgid "Just now"
msgstr ""
@@ -45,29 +152,29 @@ msgstr ""
#: static/scripts/app/models/repo.js:37
#: static/scripts/app/views/dialogs/dirent-rename.js:65
#: static/scripts/app/views/dir.js:666 static/scripts/app/views/dir.js:726
-#: static/scripts/app/views/dirent.js:501
-#: static/scripts/app/views/group-repo.js:133
-#: static/scripts/app/views/repo.js:181
+#: static/scripts/app/views/dirent.js:511
+#: static/scripts/app/views/group-repo.js:131
+#: static/scripts/app/views/repo.js:187
msgid "Name should not include '/'."
msgstr ""
-#: static/scripts/app/models/repo.js:41 static/scripts/app/views/share.js:303
+#: static/scripts/app/models/repo.js:41 static/scripts/app/views/share.js:294
msgid "Please enter password"
msgstr ""
-#: static/scripts/app/models/repo.js:42 static/scripts/app/views/share.js:311
+#: static/scripts/app/models/repo.js:42 static/scripts/app/views/share.js:302
msgid "Please enter the password again"
msgstr ""
-#: static/scripts/app/models/repo.js:44 static/scripts/app/views/share.js:307
+#: static/scripts/app/models/repo.js:44 static/scripts/app/views/share.js:298
msgid "Password is too short"
msgstr ""
-#: static/scripts/app/models/repo.js:46 static/scripts/app/views/share.js:315
+#: static/scripts/app/models/repo.js:46 static/scripts/app/views/share.js:306
msgid "Passwords don't match"
msgstr ""
-#: static/scripts/app/models/repo.js:63 static/scripts/common.js:335
+#: static/scripts/app/models/repo.js:63 static/scripts/common.js:345
msgid "Read-Only library"
msgstr ""
@@ -81,46 +188,6 @@ msgstr ""
msgid "Read-Only"
msgstr ""
-#: static/scripts/app/views/account.js:58
-#: static/scripts/app/views/dialogs/repo-folder-perm-admin.js:135
-#: static/scripts/app/views/dialogs/repo-share-link-admin.js:97
-#: static/scripts/app/views/dir.js:476 static/scripts/app/views/dir.js:510
-#: static/scripts/app/views/dirent.js:301
-#: static/scripts/app/views/file-comments.js:120
-#: static/scripts/app/views/group-discussions.js:119
-#: static/scripts/app/views/group-discussions.js:199
-#: static/scripts/app/views/group-discussions.js:242
-#: static/scripts/app/views/group-manage-members.js:121
-#: static/scripts/app/views/group-manage-members.js:150
-#: static/scripts/app/views/group-members.js:80
-#: static/scripts/app/views/group.js:190 static/scripts/app/views/group.js:219
-#: static/scripts/app/views/groups.js:95
-#: static/scripts/app/views/groups.js:149
-#: static/scripts/app/views/invitations.js:126
-#: static/scripts/app/views/my-deleted-repos.js:86
-#: static/scripts/app/views/myhome-repos.js:94
-#: static/scripts/app/views/myhome-shared-repos.js:79
-#: static/scripts/app/views/notifications.js:63
-#: static/scripts/app/views/notifications.js:163
-#: static/scripts/app/views/organization.js:135
-#: static/scripts/app/views/repo-shared-link.js:90
-#: static/scripts/app/views/share-admin-folders.js:96
-#: static/scripts/app/views/share-admin-repos.js:96
-#: static/scripts/app/views/share-admin-share-links.js:147
-#: static/scripts/app/views/share-admin-upload-links.js:61
-#: static/scripts/app/views/share.js:225 static/scripts/app/views/share.js:531
-#: static/scripts/app/views/share.js:680 static/scripts/app/views/share.js:847
-#: static/scripts/sysadmin-app/views/address-book-group.js:143
-#: static/scripts/sysadmin-app/views/address-book-group.js:215
-#: static/scripts/sysadmin-app/views/address-book-group.js:269
-#: static/scripts/sysadmin-app/views/address-book.js:89
-#: static/scripts/sysadmin-app/views/device-trusted-ipaddresses.js:62
-#: static/scripts/sysadmin-app/views/group-members.js:90
-#: static/scripts/sysadmin-app/views/groups.js:100
-#: static/scripts/sysadmin-app/views/repos.js:90
-msgid "Please check the network."
-msgstr ""
-
#: static/scripts/app/views/activity-item.js:25
msgid "Removed all items from trash."
msgstr ""
@@ -157,78 +224,6 @@ msgstr ""
msgid "Deleted directories"
msgstr ""
-#: static/scripts/app/views/details.js:94
-#: static/scripts/app/views/dialogs/dirent-mvcp.js:144
-#: static/scripts/app/views/dialogs/dirent-mvcp.js:173
-#: static/scripts/app/views/dialogs/dirent-rename.js:96
-#: static/scripts/app/views/dialogs/repo-change-password.js:94
-#: static/scripts/app/views/dialogs/repo-folder-perm-admin.js:303
-#: static/scripts/app/views/dialogs/repo-history-settings.js:83
-#: static/scripts/app/views/dialogs/repo-history-settings.js:142
-#: static/scripts/app/views/dir.js:1332 static/scripts/app/views/dir.js:1366
-#: static/scripts/app/views/dir.js:1435
-#: static/scripts/app/views/dirent-details.js:160
-#: static/scripts/app/views/dirent-grid.js:354
-#: static/scripts/app/views/dirent-grid.js:395
-#: static/scripts/app/views/dirent.js:519
-#: static/scripts/app/views/dirent.js:649
-#: static/scripts/app/views/dirent.js:690
-#: static/scripts/app/views/file-comment.js:52
-#: static/scripts/app/views/file-comments.js:163
-#: static/scripts/app/views/folder-perm.js:266
-#: static/scripts/app/views/folder-share-item.js:127
-#: static/scripts/app/views/folder-share-item.js:188
-#: static/scripts/app/views/group-discussion.js:94
-#: static/scripts/app/views/group-manage-member.js:79
-#: static/scripts/app/views/group-manage-member.js:105
-#: static/scripts/app/views/group-manage-members.js:184
-#: static/scripts/app/views/group-repo.js:166
-#: static/scripts/app/views/group-repo.js:299
-#: static/scripts/app/views/group-settings.js:134
-#: static/scripts/app/views/group-settings.js:191
-#: static/scripts/app/views/group-settings.js:235
-#: static/scripts/app/views/group-settings.js:292
-#: static/scripts/app/views/group-settings.js:332
-#: static/scripts/app/views/group-settings.js:367
-#: static/scripts/app/views/invitations.js:194
-#: static/scripts/app/views/repo-folder-perm-item.js:96
-#: static/scripts/app/views/repo-folder-perm-item.js:147
-#: static/scripts/app/views/repo.js:206 static/scripts/app/views/repo.js:281
-#: static/scripts/app/views/repo.js:398 static/scripts/app/views/repo.js:440
-#: static/scripts/app/views/share.js:448 static/scripts/app/views/share.js:944
-#: static/scripts/app/views/share.js:1037 static/scripts/common.js:432
-#: static/scripts/common.js:521
-#: static/scripts/sysadmin-app/views/address-book-group-item.js:104
-#: static/scripts/sysadmin-app/views/address-book-group.js:314
-#: static/scripts/sysadmin-app/views/address-book-group.js:339
-#: static/scripts/sysadmin-app/views/address-book.js:129
-#: static/scripts/sysadmin-app/views/admin-login-logs.js:129
-#: static/scripts/sysadmin-app/views/admin-operation-logs.js:129
-#: static/scripts/sysadmin-app/views/dashboard.js:84
-#: static/scripts/sysadmin-app/views/desktop-devices.js:105
-#: static/scripts/sysadmin-app/views/device-errors.js:92
-#: static/scripts/sysadmin-app/views/device-trusted-ipaddresses.js:115
-#: static/scripts/sysadmin-app/views/dir.js:195
-#: static/scripts/sysadmin-app/views/folder-share-item.js:90
-#: static/scripts/sysadmin-app/views/folder-share-item.js:139
-#: static/scripts/sysadmin-app/views/group-member.js:71
-#: static/scripts/sysadmin-app/views/group-members.js:151
-#: static/scripts/sysadmin-app/views/group-repos.js:75
-#: static/scripts/sysadmin-app/views/group.js:108
-#: static/scripts/sysadmin-app/views/groups.js:168
-#: static/scripts/sysadmin-app/views/mobile-devices.js:106
-#: static/scripts/sysadmin-app/views/repo.js:142
-#: static/scripts/sysadmin-app/views/repos.js:163
-#: static/scripts/sysadmin-app/views/search-groups.js:76
-#: static/scripts/sysadmin-app/views/search-repos.js:77
-#: static/scripts/sysadmin-app/views/search-trash-repos.js:113
-#: static/scripts/sysadmin-app/views/share.js:195
-#: static/scripts/sysadmin-app/views/share.js:269
-#: static/scripts/sysadmin-app/views/system-repo.js:59
-#: static/scripts/sysadmin-app/views/trash-repos.js:142
-msgid "Failed. Please check the network."
-msgstr ""
-
#: static/scripts/app/views/device.js:42
#: static/scripts/sysadmin-app/views/device.js:42
msgid "Unlink device"
@@ -275,43 +270,35 @@ msgid "Failed."
msgstr ""
#: static/scripts/app/views/dialogs/dirent-mvcp.js:133
-#: static/scripts/app/views/dialogs/dirent-mvcp.js:164
-#: static/scripts/app/views/dir.js:1426
+#: static/scripts/app/views/dialogs/dirent-mvcp.js:159
+#: static/scripts/app/views/dir.js:1416
msgid "Canceled."
msgstr ""
-#: static/scripts/app/views/dialogs/dirent-mvcp.js:196
+#: static/scripts/app/views/dialogs/dirent-mvcp.js:186
#: static/scripts/app/views/dir.js:1171
msgid "Invalid destination path"
msgstr ""
-#: static/scripts/app/views/dialogs/dirent-mvcp.js:215
-#: static/scripts/app/views/dirent.js:292
+#: static/scripts/app/views/dialogs/dirent-mvcp.js:205
+#: static/scripts/app/views/dirent.js:295
msgid "Successfully moved %(name)s"
msgstr ""
-#: static/scripts/app/views/dialogs/dirent-mvcp.js:218
+#: static/scripts/app/views/dialogs/dirent-mvcp.js:208
msgid "Successfully copied %(name)s"
msgstr ""
-#: static/scripts/app/views/dialogs/dirent-rename.js:39
-msgid "Rename Folder"
-msgstr ""
-
-#: static/scripts/app/views/dialogs/dirent-rename.js:39
-msgid "Rename File"
-msgstr ""
-
#: static/scripts/app/views/dialogs/dirent-rename.js:59
#: static/scripts/app/views/dir.js:661 static/scripts/app/views/dir.js:721
-#: static/scripts/app/views/dirent.js:495
-#: static/scripts/app/views/group-repo.js:127
-#: static/scripts/app/views/groups.js:127
+#: static/scripts/app/views/dirent.js:505
+#: static/scripts/app/views/group-repo.js:125
+#: static/scripts/app/views/groups.js:118
#: static/scripts/app/views/invitations.js:65
-#: static/scripts/app/views/repo.js:175 static/scripts/app/views/repo.js:411
-#: static/scripts/sysadmin-app/views/address-book-group-item.js:74
-#: static/scripts/sysadmin-app/views/address-book-group.js:174
-#: static/scripts/sysadmin-app/views/address-book-group.js:239
+#: static/scripts/app/views/repo.js:181 static/scripts/app/views/repo.js:406
+#: static/scripts/sysadmin-app/views/address-book-group-item.js:85
+#: static/scripts/sysadmin-app/views/address-book-group.js:181
+#: static/scripts/sysadmin-app/views/address-book-group.js:252
#: static/scripts/sysadmin-app/views/device-trusted-ipaddresses.js:42
#: static/scripts/sysadmin-app/views/dir.js:82
#: static/scripts/sysadmin-app/views/group-members.js:51
@@ -350,13 +337,13 @@ msgstr ""
msgid "Successfully changed library password."
msgstr ""
-#: static/scripts/app/views/dialogs/repo-folder-perm-admin.js:83
+#: static/scripts/app/views/dialogs/repo-folder-perm-admin.js:91
msgid "{placeholder} Folder Permission"
msgstr ""
-#: static/scripts/app/views/dialogs/repo-folder-perm-admin.js:102
-#: static/scripts/app/views/group-settings.js:155
-#: static/scripts/app/views/repo.js:245
+#: static/scripts/app/views/dialogs/repo-folder-perm-admin.js:110
+#: static/scripts/app/views/group-settings.js:150
+#: static/scripts/app/views/repo.js:247
#: static/scripts/sysadmin-app/views/group.js:74
#: static/scripts/sysadmin-app/views/groups.js:68
#: static/scripts/sysadmin-app/views/repo.js:107
@@ -364,56 +351,15 @@ msgstr ""
msgid "Search user or enter email and press Enter"
msgstr ""
-#: static/scripts/app/views/dialogs/repo-folder-perm-admin.js:111
+#: static/scripts/app/views/dialogs/repo-folder-perm-admin.js:141
msgid "Select a group"
msgstr ""
-#: static/scripts/app/views/dialogs/repo-folder-perm-admin.js:113
+#: static/scripts/app/views/dialogs/repo-folder-perm-admin.js:143
msgid "You can only select 1 item"
msgstr ""
-#: static/scripts/app/views/dialogs/repo-folder-perm-admin.js:130
-#: static/scripts/app/views/dialogs/repo-share-link-admin.js:92
-#: static/scripts/app/views/file-comments.js:115
-#: static/scripts/app/views/group-discussions.js:114
-#: static/scripts/app/views/group-discussions.js:194
-#: static/scripts/app/views/group-manage-members.js:116
-#: static/scripts/app/views/group-members.js:75
-#: static/scripts/app/views/group.js:214 static/scripts/app/views/groups.js:90
-#: static/scripts/app/views/invitations.js:189
-#: static/scripts/app/views/my-deleted-repos.js:81
-#: static/scripts/app/views/myhome-repos.js:89
-#: static/scripts/app/views/myhome-shared-repos.js:74
-#: static/scripts/app/views/organization.js:130
-#: static/scripts/app/views/share-admin-folders.js:91
-#: static/scripts/app/views/share-admin-repos.js:91
-#: static/scripts/app/views/share-admin-share-links.js:142
-#: static/scripts/app/views/share-admin-upload-links.js:56
-#: static/scripts/app/views/share.js:220 static/scripts/app/views/share.js:526
-#: static/scripts/app/views/share.js:675 static/scripts/app/views/share.js:842
-#: static/scripts/sysadmin-app/views/address-book-group.js:309
-#: static/scripts/sysadmin-app/views/address-book-group.js:334
-#: static/scripts/sysadmin-app/views/address-book.js:124
-#: static/scripts/sysadmin-app/views/admin-login-logs.js:124
-#: static/scripts/sysadmin-app/views/admin-operation-logs.js:124
-#: static/scripts/sysadmin-app/views/dashboard.js:79
-#: static/scripts/sysadmin-app/views/desktop-devices.js:100
-#: static/scripts/sysadmin-app/views/device-errors.js:87
-#: static/scripts/sysadmin-app/views/dir.js:190
-#: static/scripts/sysadmin-app/views/group-members.js:146
-#: static/scripts/sysadmin-app/views/group-repos.js:70
-#: static/scripts/sysadmin-app/views/groups.js:163
-#: static/scripts/sysadmin-app/views/mobile-devices.js:101
-#: static/scripts/sysadmin-app/views/repos.js:158
-#: static/scripts/sysadmin-app/views/search-groups.js:71
-#: static/scripts/sysadmin-app/views/search-repos.js:72
-#: static/scripts/sysadmin-app/views/search-trash-repos.js:108
-#: static/scripts/sysadmin-app/views/system-repo.js:54
-#: static/scripts/sysadmin-app/views/trash-repos.js:137
-msgid "Permission error"
-msgstr ""
-
-#: static/scripts/app/views/dialogs/repo-folder-perm-admin.js:193
+#: static/scripts/app/views/dialogs/repo-folder-perm-admin.js:222
msgid "Please click and choose a directory."
msgstr ""
@@ -421,7 +367,7 @@ msgstr ""
msgid "{placeholder} History Setting"
msgstr ""
-#: static/scripts/app/views/dialogs/repo-history-settings.js:135
+#: static/scripts/app/views/dialogs/repo-history-settings.js:130
msgid "Successfully set library history."
msgstr ""
@@ -468,12 +414,13 @@ msgstr ""
msgid "Password is required."
msgstr ""
-#: static/scripts/app/views/dir.js:732
-msgid "Only an extension there, please input a name."
+#: static/scripts/app/views/dir.js:479
+#: static/scripts/app/views/notifications.js:63 static/scripts/common.js:464
+msgid "Please check the network."
msgstr ""
-#: static/scripts/app/views/dir.js:770
-msgid "New File"
+#: static/scripts/app/views/dir.js:732
+msgid "Only an extension there, please input a name."
msgstr ""
#: static/scripts/app/views/dir.js:778
@@ -536,27 +483,27 @@ msgstr ""
msgid "Copy selected item(s) to:"
msgstr ""
-#: static/scripts/app/views/dir.js:1220 static/scripts/app/views/dir.js:1387
+#: static/scripts/app/views/dir.js:1220 static/scripts/app/views/dir.js:1377
msgid "Successfully moved %(name)s."
msgstr ""
-#: static/scripts/app/views/dir.js:1222 static/scripts/app/views/dir.js:1389
+#: static/scripts/app/views/dir.js:1222 static/scripts/app/views/dir.js:1379
msgid "Successfully moved %(name)s and 1 other item."
msgstr ""
-#: static/scripts/app/views/dir.js:1224 static/scripts/app/views/dir.js:1391
+#: static/scripts/app/views/dir.js:1224 static/scripts/app/views/dir.js:1381
msgid "Successfully moved %(name)s and %(amount)s other items."
msgstr ""
-#: static/scripts/app/views/dir.js:1229 static/scripts/app/views/dir.js:1395
+#: static/scripts/app/views/dir.js:1229 static/scripts/app/views/dir.js:1385
msgid "Successfully copied %(name)s."
msgstr ""
-#: static/scripts/app/views/dir.js:1231 static/scripts/app/views/dir.js:1397
+#: static/scripts/app/views/dir.js:1231 static/scripts/app/views/dir.js:1387
msgid "Successfully copied %(name)s and 1 other item."
msgstr ""
-#: static/scripts/app/views/dir.js:1233 static/scripts/app/views/dir.js:1399
+#: static/scripts/app/views/dir.js:1233 static/scripts/app/views/dir.js:1389
msgid "Successfully copied %(name)s and %(amount)s other items."
msgstr ""
@@ -592,8 +539,8 @@ msgstr ""
msgid "Failed to copy %(name)s"
msgstr ""
-#: static/scripts/app/views/dirent-details.js:95 static/scripts/common.js:662
-#: static/scripts/common.js:733
+#: static/scripts/app/views/dirent-details.js:95 static/scripts/common.js:691
+#: static/scripts/common.js:762
msgid "No matches"
msgstr ""
@@ -607,7 +554,7 @@ msgid "locked by {placeholder}"
msgstr ""
#: static/scripts/app/views/dirent-grid.js:186
-#: static/scripts/app/views/dirent.js:403
+#: static/scripts/app/views/dirent.js:412
msgid "Successfully deleted %(name)s"
msgstr ""
@@ -638,25 +585,10 @@ msgstr ""
#: static/scripts/app/views/fileupload.js:17
#: static/scripts/app/views/fileupload.js:225
#: static/scripts/app/views/fileupload.js:265
-#: static/scripts/app/views/group-discussions.js:116
-#: static/scripts/app/views/group-discussions.js:196
-#: static/scripts/app/views/group-manage-members.js:118
-#: static/scripts/app/views/group-manage-members.js:182
-#: static/scripts/app/views/group-members.js:77
-#: static/scripts/app/views/group.js:216 static/scripts/app/views/groups.js:92
-#: static/scripts/app/views/my-deleted-repos.js:83
-#: static/scripts/app/views/myhome-repos.js:91
-#: static/scripts/app/views/myhome-shared-repos.js:76
-#: static/scripts/app/views/organization.js:132
#: static/scripts/app/views/repo-details.js:37
-#: static/scripts/app/views/share-admin-folders.js:93
-#: static/scripts/app/views/share-admin-repos.js:93
-#: static/scripts/app/views/share-admin-share-links.js:144
-#: static/scripts/app/views/share-admin-upload-links.js:58
-#: static/scripts/app/views/share.js:222 static/scripts/app/views/share.js:528
-#: static/scripts/app/views/share.js:677 static/scripts/app/views/share.js:844
-#: static/scripts/sysadmin-app/views/address-book-group-item.js:128
-#: static/scripts/sysadmin-app/views/address-book-group-item.js:135
+#: static/scripts/sysadmin-app/views/address-book-group-item.js:145
+#: static/scripts/sysadmin-app/views/address-book-group-item.js:152
+#: static/scripts/sysadmin-app/views/system-repo.js:52
msgid "Error"
msgstr ""
@@ -672,15 +604,6 @@ msgstr ""
msgid "Start"
msgstr ""
-#: static/scripts/app/views/fileupload.js:21
-msgid "Cancel"
-msgstr ""
-
-#: static/scripts/app/views/fileupload.js:22
-#: static/scripts/sysadmin-app/views/device-trusted-ipaddress.js:27
-msgid "Delete"
-msgstr ""
-
#: static/scripts/app/views/fileupload.js:46
msgid "File Uploading..."
msgstr ""
@@ -705,12 +628,16 @@ msgstr ""
msgid "File is locked"
msgstr ""
-#: static/scripts/app/views/folder-perm.js:62
+#: static/scripts/app/views/fileupload.js:440
+msgid "Network error"
+msgstr ""
+
+#: static/scripts/app/views/folder-perm.js:60
msgid "Set {placeholder}'s permission"
msgstr ""
-#: static/scripts/app/views/folder-perm.js:134
-#: static/scripts/app/views/share.js:827
+#: static/scripts/app/views/folder-perm.js:126
+#: static/scripts/app/views/share.js:763
msgid "Select groups"
msgstr ""
@@ -718,6 +645,13 @@ msgstr ""
msgid "Edit failed"
msgstr ""
+#: static/scripts/app/views/folder-share-item.js:127
+#: static/scripts/app/views/folder-share-item.js:188
+#: static/scripts/common.js:444
+#: static/scripts/sysadmin-app/views/dashboard.js:84
+msgid "Failed. Please check the network."
+msgstr ""
+
#: static/scripts/app/views/folder-share-item.js:186
msgid "Delete failed"
msgstr ""
@@ -726,18 +660,18 @@ msgstr ""
msgid "{placeholder} Members"
msgstr ""
-#: static/scripts/app/views/group-repo.js:60
-#: static/scripts/sysadmin-app/views/address-book-group-library.js:44
+#: static/scripts/app/views/group-repo.js:59
+#: static/scripts/sysadmin-app/views/address-book-group-library.js:55
msgid "Successfully deleted library {placeholder}"
msgstr ""
-#: static/scripts/app/views/group-repo.js:291
+#: static/scripts/app/views/group-repo.js:287
#: static/scripts/app/views/organization-repo.js:50
msgid "Successfully unshared 1 item."
msgstr ""
-#: static/scripts/app/views/group-settings.js:156
-#: static/scripts/app/views/repo.js:246
+#: static/scripts/app/views/group-settings.js:151
+#: static/scripts/app/views/repo.js:248
#: static/scripts/sysadmin-app/views/group.js:75
#: static/scripts/sysadmin-app/views/groups.js:69
#: static/scripts/sysadmin-app/views/repo.js:108
@@ -745,37 +679,37 @@ msgstr ""
msgid "You cannot select any more choices"
msgstr ""
-#: static/scripts/app/views/group-settings.js:183
+#: static/scripts/app/views/group-settings.js:178
msgid ""
"Successfully transferred the group. You are now a normal member of the group."
msgstr ""
-#: static/scripts/app/views/group-settings.js:253
+#: static/scripts/app/views/group-settings.js:238
msgid "Please choose a CSV file"
msgstr ""
-#: static/scripts/app/views/group-settings.js:284
+#: static/scripts/app/views/group-settings.js:269
msgid "Successfully imported."
msgstr ""
-#: static/scripts/app/views/group-settings.js:312
+#: static/scripts/app/views/group-settings.js:292
msgid "Dismiss Group"
msgstr ""
-#: static/scripts/app/views/group-settings.js:313
+#: static/scripts/app/views/group-settings.js:293
msgid "Really want to dismiss this group?"
msgstr ""
-#: static/scripts/app/views/group-settings.js:346
+#: static/scripts/app/views/group-settings.js:321
msgid "Quit Group"
msgstr ""
-#: static/scripts/app/views/group-settings.js:347
+#: static/scripts/app/views/group-settings.js:322
msgid "Are you sure you want to quit this group?"
msgstr ""
#: static/scripts/app/views/invitation.js:37
-#: static/scripts/sysadmin-app/views/address-book-group-item.js:46
+#: static/scripts/sysadmin-app/views/address-book-group-item.js:57
#: static/scripts/sysadmin-app/views/device-trusted-ipaddress.js:39
#: static/scripts/sysadmin-app/views/group.js:44
msgid "Successfully deleted 1 item."
@@ -801,7 +735,7 @@ msgstr ""
msgid "Refresh"
msgstr ""
-#: static/scripts/app/views/repo.js:91
+#: static/scripts/app/views/repo.js:95
#: static/scripts/sysadmin-app/views/address-book-group-library.js:30
#: static/scripts/sysadmin-app/views/admin-operation-log.js:51
#: static/scripts/sysadmin-app/views/repo.js:65
@@ -809,33 +743,33 @@ msgstr ""
msgid "Delete Library"
msgstr ""
-#: static/scripts/app/views/repo.js:92
-#: static/scripts/sysadmin-app/views/address-book-group-item.js:33
+#: static/scripts/app/views/repo.js:96
+#: static/scripts/sysadmin-app/views/address-book-group-item.js:47
#: static/scripts/sysadmin-app/views/address-book-group-library.js:31
#: static/scripts/sysadmin-app/views/device-trusted-ipaddress.js:28
-#: static/scripts/sysadmin-app/views/group-member.js:82
+#: static/scripts/sysadmin-app/views/group-member.js:91
#: static/scripts/sysadmin-app/views/group.js:31
#: static/scripts/sysadmin-app/views/repo.js:66
#, javascript-format
msgid "Are you sure you want to delete %s ?"
msgstr ""
-#: static/scripts/app/views/repo.js:102
+#: static/scripts/app/views/repo.js:106
#: static/scripts/sysadmin-app/views/repo.js:79
msgid "Successfully deleted."
msgstr ""
-#: static/scripts/app/views/repo.js:235
+#: static/scripts/app/views/repo.js:237
#: static/scripts/sysadmin-app/views/repo.js:97
msgid "Transfer Library {library_name} To"
msgstr ""
-#: static/scripts/app/views/repo.js:274
+#: static/scripts/app/views/repo.js:276
#: static/scripts/sysadmin-app/views/repo.js:135
msgid "Successfully transferred the library."
msgstr ""
-#: static/scripts/app/views/repo.js:429
+#: static/scripts/app/views/repo.js:424
msgid "Successfully added label(s) for library {placeholder}"
msgstr ""
@@ -860,32 +794,32 @@ msgstr ""
msgid "Expired"
msgstr ""
-#: static/scripts/app/views/share.js:248 static/scripts/app/views/share.js:249
-#: static/scripts/app/views/share.js:270 static/scripts/app/views/share.js:271
+#: static/scripts/app/views/share.js:239 static/scripts/app/views/share.js:240
+#: static/scripts/app/views/share.js:261 static/scripts/app/views/share.js:262
msgid "Hide"
msgstr ""
-#: static/scripts/app/views/share.js:266 static/scripts/app/views/share.js:267
+#: static/scripts/app/views/share.js:257 static/scripts/app/views/share.js:258
msgid "Show"
msgstr ""
-#: static/scripts/app/views/share.js:325
+#: static/scripts/app/views/share.js:316
msgid "Please enter days."
msgstr ""
-#: static/scripts/app/views/share.js:329
+#: static/scripts/app/views/share.js:320
msgid "Please enter valid days"
msgstr ""
-#: static/scripts/app/views/share.js:415
+#: static/scripts/app/views/share.js:406
msgid "Please input at least an email."
msgstr ""
-#: static/scripts/app/views/share.js:432
+#: static/scripts/app/views/share.js:423
msgid "Successfully sent to {placeholder}"
msgstr ""
-#: static/scripts/app/views/share.js:436
+#: static/scripts/app/views/share.js:427
msgid "Failed to send to {placeholder}"
msgstr ""
@@ -902,45 +836,50 @@ msgstr ""
msgid "Successfully unstared {placeholder}"
msgstr ""
-#: static/scripts/common.js:653
-#: static/scripts/sysadmin-app/views/address-book-group.js:165
+#: static/scripts/common.js:458
+#: static/scripts/sysadmin-app/views/dashboard.js:79
+msgid "Permission error"
+msgstr ""
+
+#: static/scripts/common.js:682
+#: static/scripts/sysadmin-app/views/address-book-group.js:172
#: static/scripts/sysadmin-app/views/group-members.js:41
msgid "Search users or enter emails and press Enter"
msgstr ""
-#: static/scripts/common.js:661 static/scripts/common.js:732
+#: static/scripts/common.js:690 static/scripts/common.js:761
msgid "Please enter 1 or more character"
msgstr ""
-#: static/scripts/common.js:663 static/scripts/common.js:734
+#: static/scripts/common.js:692 static/scripts/common.js:763
msgid "Searching..."
msgstr ""
-#: static/scripts/common.js:664 static/scripts/common.js:735
+#: static/scripts/common.js:693 static/scripts/common.js:764
msgid "Loading failed"
msgstr ""
-#: static/scripts/common.js:724
+#: static/scripts/common.js:753
msgid "Search groups"
msgstr ""
-#: static/scripts/common.js:1059
+#: static/scripts/common.js:1088
msgid "Packaging..."
msgstr ""
-#: static/scripts/sysadmin-app/views/address-book-group-item.js:32
+#: static/scripts/sysadmin-app/views/address-book-group-item.js:46
msgid "Delete Department"
msgstr ""
-#: static/scripts/sysadmin-app/views/address-book-group-item.js:80
+#: static/scripts/sysadmin-app/views/address-book-group-item.js:91
msgid "Invalid quota."
msgstr ""
-#: static/scripts/sysadmin-app/views/address-book-group.js:99
+#: static/scripts/sysadmin-app/views/address-book-group.js:101
msgid "New Sub-department"
msgstr ""
-#: static/scripts/sysadmin-app/views/address-book-group.js:111
+#: static/scripts/sysadmin-app/views/address-book-group.js:114
#: static/scripts/sysadmin-app/views/address-book.js:64
#: static/scripts/sysadmin-app/views/groups.js:79
#: static/scripts/sysadmin-app/views/repos.js:69
@@ -1016,11 +955,11 @@ msgstr ""
msgid "Successfully clean all errors."
msgstr ""
-#: static/scripts/sysadmin-app/views/group-member.js:81
+#: static/scripts/sysadmin-app/views/group-member.js:90
msgid "Delete Member"
msgstr ""
-#: static/scripts/sysadmin-app/views/group-member.js:95
+#: static/scripts/sysadmin-app/views/group-member.js:100
msgid "Successfully deleted member {placeholder}"
msgstr ""
diff --git a/seahub/templates/base_for_react.html b/seahub/templates/base_for_react.html
index 8fd4183daa..73edfb3566 100644
--- a/seahub/templates/base_for_react.html
+++ b/seahub/templates/base_for_react.html
@@ -32,6 +32,7 @@
}
};
+
{% block extra_script %}{% endblock %}