diff --git a/frontend/src/components/constants.js b/frontend/src/components/constants.js
index 375033cb22..86a553837b 100644
--- a/frontend/src/components/constants.js
+++ b/frontend/src/components/constants.js
@@ -10,6 +10,7 @@ export const logoWidth = window.app.config.logoWidth;
export const logoHeight = window.app.config.logoHeight;
export const isPro = window.app.config.isPro === "True";
export const lang = window.app.config.lang;
+export const fileServerRoot = window.app.config.fileServerRoot;
// wiki
export const slug = window.wiki ? window.wiki.config.slug : '';
diff --git a/frontend/src/components/dialog/zip-download-dialog.js b/frontend/src/components/dialog/zip-download-dialog.js
new file mode 100644
index 0000000000..d64abdac6b
--- /dev/null
+++ b/frontend/src/components/dialog/zip-download-dialog.js
@@ -0,0 +1,22 @@
+import React from 'react'
+import { Modal, ModalHeader, ModalBody } from 'reactstrap';
+
+class ZipDownloadDialog extends React.Component {
+
+ toggle = () => {
+ this.props.onCancelDownload();
+ }
+
+ render() {
+ return (
+
+
+
+ {this.props.progress}
+
+
+ )
+ }
+}
+
+export default ZipDownloadDialog;
diff --git a/frontend/src/components/dirent-operation/operation-group.js b/frontend/src/components/dirent-operation/operation-group.js
new file mode 100644
index 0000000000..5b8fbb71bf
--- /dev/null
+++ b/frontend/src/components/dirent-operation/operation-group.js
@@ -0,0 +1,101 @@
+import React from 'react';
+import { gettext } from '../constants';
+import OperationMenu from './operation-menu';
+
+class OperationGroup extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ isItemMenuShow: false,
+ menuPosition: {top: 0, left: 0 },
+ }
+ }
+
+ componentDidMount() {
+ document.addEventListener('click', this.onItemMenuHide);
+ }
+
+ componentWillUnmount() {
+ document.removeEventListener('click', this.onItemMenuHide);
+ }
+
+ onDownload = (e) => {
+ e.nativeEvent.stopImmediatePropagation();
+ this.props.onDownload();
+ }
+
+ onShare = (e) => {
+ //todos::
+ }
+
+ onDelete = (e) => {
+ e.nativeEvent.stopImmediatePropagation(); //for document event
+ this.props.onDelete();
+ }
+
+ onItemMenuShow = (e) => {
+ if (!this.state.isItemMenuShow) {
+ e.stopPropagation();
+ e.nativeEvent.stopImmediatePropagation();
+
+ let left = e.clientX - 8*16;
+ let top = e.clientY + 15;
+ let position = Object.assign({},this.state.menuPosition, {left: left, top: top});
+ this.setState({
+ menuPosition: position,
+ isItemMenuShow: true,
+ });
+ this.props.onItemMenuShow();
+ } else {
+ this.onItemMenuHide();
+ }
+ }
+
+ onItemMenuHide = () => {
+ this.setState({
+ isItemMenuShow: false,
+ });
+ this.props.onItemMenuHide();
+ }
+
+ onRename = () => {
+ //todos:
+ }
+
+ onCopy = () => {
+ //todos
+ }
+
+ render() {
+ return (
+
-  |
- {fileName} |
- {draft.owner} |
- {localTime} |
-
+ |  |
+ {fileName} |
+ {draft.owner} |
+ {localTime} |
+
-
-
-
- );
- } else if (permission) {
- trashUrl = siteRoot + 'dir/recycle/' + repoID + '/?dir_path=' + encodeURIComponent(initialFilePath);
- return (
-
- );
+class PathToolbar extends React.Component {
+
+ isMarkdownFile(filePath) {
+ let lastIndex = filePath.lastIndexOf('/');
+ let name = filePath.slice(lastIndex + 1);
+ return name.indexOf('.md') > -1 ? true : false;
+ }
+
+ render() {
+ let isFile = this.isMarkdownFile(this.props.filePath);
+ let index = this.props.filePath.lastIndexOf('/');
+ let name = this.props.filePath.slice(index + 1);
+ let trashUrl = siteRoot + 'repo/recycle/' + repoID + '/?referer=' + encodeURIComponent(location.href);
+ let historyUrl = siteRoot + 'repo/history/' + repoID + '/?referer=' + encodeURIComponent(location.href);
+ if ( (name === slug || name === '') && !isFile && permission) {
+ return (
+
+ );
+ } else if ( !isFile && permission) {
+ return (
+
+ );
+ } else if (permission) {
+ historyUrl = siteRoot + 'repo/file_revisions/' + repoID + '/?p=' + encodePath(this.props.filePath) + '&referer=' + encodeURIComponent(location.href);
+ return (
+
+ )
+ }
+ return '';
}
- return '';
-
}
+PathToolbar.propTypes = propTypes;
+
export default PathToolbar;
diff --git a/frontend/src/components/tree-dir-view/tree-dir-list.js b/frontend/src/components/tree-dir-view/tree-dir-list.js
index 00a31051f2..72c237f897 100644
--- a/frontend/src/components/tree-dir-view/tree-dir-list.js
+++ b/frontend/src/components/tree-dir-view/tree-dir-list.js
@@ -1,41 +1,93 @@
import React, { Component } from 'react';
import { serviceUrl } from '../constants';
+import OperationGroup from '../dirent-operation/operation-group';
+
class TreeDirList extends React.Component {
constructor(props) {
super(props);
this.state = {
- isMourseEnter: false,
- highlight: '',
- }
+ highlight: false,
+ isOperationShow: false,
+ };
}
onMouseEnter = () => {
- this.setState({
- highlight: 'tr-highlight'
- });
+ if (!this.props.isItemFreezed) {
+ this.setState({
+ highlight: true,
+ isOperationShow: true,
+ });
+ }
+ }
+
+ onMouseOver = () => {
+ if (!this.props.isItemFreezed) {
+ this.setState({
+ highlight: true,
+ isOperationShow: true,
+ });
+ }
}
onMouseLeave = () => {
+ if (!this.props.isItemFreezed) {
+ this.setState({
+ highlight: false,
+ isOperationShow: false
+ });
+ }
+ }
+
+ onItemMenuShow = () => {
+ this.props.onItemMenuShow();
+ }
+
+ onItemMenuHide = () => {
this.setState({
- highlight: '',
+ isOperationShow: false,
+ highlight: ''
});
+ this.props.onItemMenuHide();
}
onMainNodeClick = () => {
this.props.onMainNodeClick(this.props.node);
}
+ onDownload = () => {
+ this.props.onDownload(this.props.node);
+ }
+
+ onDelete = () => {
+ this.props.onDelete(this.props.node);
+ }
+
render() {
let node = this.props.node;
return (
-
-
+ |
+
|
- {node.name} |
- {node.size} |
- {node.last_update_time} |
+ {node.name} |
+ {
+ this.props.needOperationGroup &&
+
+ {
+ this.state.isOperationShow &&
+
+ }
+ |
+ }
+ {node.size} |
+ {node.last_update_time} |
)
}
diff --git a/frontend/src/components/tree-dir-view/tree-dir-view.js b/frontend/src/components/tree-dir-view/tree-dir-view.js
index b25a031aa6..62262f8e78 100644
--- a/frontend/src/components/tree-dir-view/tree-dir-view.js
+++ b/frontend/src/components/tree-dir-view/tree-dir-view.js
@@ -1,9 +1,81 @@
-import React, { Component } from "react";
+import React from "react";
+import { gettext, repoID } from '../constants';
+import editorUtilities from '../../utils/editor-utilties';
+import URLDecorator from '../../utils/url-decorator';
+import ZipDownloadDialog from '../dialog/zip-download-dialog';
import TreeDirList from './tree-dir-list'
import "../../css/common.css";
-const gettext = window.gettext;
class TreeDirView extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ isProgressDialogShow: false,
+ progress: '0%',
+ isItemFreezed: false
+ };
+ this.zip_token = null;
+ this.interval = null;
+ }
+
+ onDownload = (item) => {
+ if (item.isDir()) {
+ this.setState({isProgressDialogShow: true, progress: '0%'});
+ editorUtilities.zipDownload(item.parent_path, item.name).then(res => {
+ this.zip_token = res.data['zip_token'];
+ this.addDownloadAnimation();
+ this.interval = setInterval(this.addDownloadAnimation, 1000);
+ });
+ } else {
+ let url = URLDecorator.getUrl({type:'download_file_url', repoID: repoID, filePath: item.path});
+ location.href = url;
+ }
+ }
+
+ addDownloadAnimation = () => {
+ let _this = this;
+ let token = this.zip_token;
+ editorUtilities.queryZipProgress(token).then(res => {
+ let data = res.data;
+ let progress = data.total === 0 ? '100%' : (data.zipped / data.total * 100).toFixed(0) + '%';
+ this.setState({progress: progress});
+
+ if (data['total'] === data['zipped']) {
+ this.setState({
+ progress: '100%'
+ });
+ clearInterval(this.interval);
+ location.href = URLDecorator.getUrl({type: 'download_dir_zip_url', token: token});
+ setTimeout(function() {
+ _this.setState({isProgressDialogShow: false});
+ }, 500);
+ }
+
+ });
+ }
+
+ onCancelDownload = () => {
+ let zip_token = this.zip_token;
+ editorUtilities.cancelZipTask(zip_token).then(res => {
+ this.setState({
+ isProgressDialogShow: false,
+ });
+ })
+ }
+
+ onItemMenuShow = () => {
+ this.setState({
+ isItemFreezed: true,
+ })
+ }
+
+ onItemMenuHide = () => {
+ this.setState({
+ isItemFreezed: false,
+ });
+ }
+
render() {
let node = this.props.node;
let children = node.hasChildren() ? node.children : null;
@@ -12,21 +84,46 @@ class TreeDirView extends React.Component {
-
- |
- {gettext('Name')} |
- {gettext('Size')} |
- {gettext('Last Update')} |
-
+ {
+ this.props.needOperationGroup ?
+
+ |
+ {gettext('Name')} |
+ |
+ {gettext('Size')} |
+ {gettext('Last Update')} |
+
+ :
+
+ |
+ {gettext('Name')} |
+ {gettext('Size')} |
+ {gettext('Last Update')} |
+
+ }
{children && children.map((node, index) => {
return (
-
+
)
})}
+ {
+ this.state.isProgressDialogShow &&
+
+ }
)
}
diff --git a/frontend/src/components/tree-view/node.js b/frontend/src/components/tree-view/node.js
index ebf1289217..8871445989 100644
--- a/frontend/src/components/tree-view/node.js
+++ b/frontend/src/components/tree-view/node.js
@@ -1,13 +1,15 @@
class Node {
static deserializefromJson(object) {
- const {name, type, size, last_update_time, isExpanded = true, children = []} = object;
+ const {name, type, size, last_update_time, permission, parent_path, isExpanded = true, children = []} = object;
const node = new Node({
name,
type,
size,
last_update_time,
+ permission,
+ parent_path,
isExpanded,
children: children.map(item => Node.deserializefromJson(item)),
});
@@ -15,11 +17,13 @@ class Node {
return node;
}
- constructor({name, type, size, last_update_time, isExpanded, children}) {
+ constructor({name, type, size, last_update_time, permission, parent_path, isExpanded, children}) {
this.name = name;
this.type = type;
this.size = size;
this.last_update_time = last_update_time;
+ this.permission = permission;
+ this.parent_path = parent_path;
this.isExpanded = isExpanded !== undefined ? isExpanded : true;
this.children = children ? children : [];
this.parent = null;
@@ -31,6 +35,8 @@ class Node {
type: this.type,
size: this.size,
last_update_time: this.last_update_time,
+ permission: this.permission,
+ parent_path: this.parent_path,
isExpanded: this.isExpanded
});
n.children = this.children.map(child => {
@@ -101,6 +107,8 @@ class Node {
type: this.type,
size: this.size,
last_update_time: this.last_update_time,
+ permission: this.permission,
+ parent_path: this.parent_path,
isExpanded: this.isExpanded,
children: children
}
diff --git a/frontend/src/components/tree-view/tree.js b/frontend/src/components/tree-view/tree.js
index 4a870621e3..62d22f61c9 100644
--- a/frontend/src/components/tree-view/tree.js
+++ b/frontend/src/components/tree-view/tree.js
@@ -186,6 +186,8 @@ class Tree {
type: model.type,
size: bytesToSize(model.size),
last_update_time: moment.unix(model.last_update_time).fromNow(),
+ permission: model.permission,
+ parent_path: model.parent_path,
isExpanded: false
});
if (model.children instanceof Array) {
@@ -214,6 +216,8 @@ class Tree {
type: nodeObj.type,
size: bytesToSize(nodeObj.size),
last_update_time: moment.unix(nodeObj.last_update_time).fromNow(),
+ permission: nodeObj.permission,
+ parent_path: nodeObj.parent_path,
isExpanded: false
});
node.parent_path = nodeObj.parent_path;
@@ -240,6 +244,8 @@ class Tree {
type: node.type,
size: bytesToSize(node.size),
last_update_time: moment.unix(node.last_update_time).fromNow(),
+ permission: node.permission,
+ parent_path: node.parent_path,
isExpanded: false
});
if (node.children instanceof Array) {
diff --git a/frontend/src/components/utils.js b/frontend/src/components/utils.js
index a9b6b58aaf..b5f8d81626 100644
--- a/frontend/src/components/utils.js
+++ b/frontend/src/components/utils.js
@@ -18,3 +18,12 @@ export function bytesToSize(bytes) {
if (i === 0) return bytes + ' ' + sizes[i];
return (bytes / (1000 ** i)).toFixed(1) + ' ' + sizes[i];
}
+
+export function encodePath(path) {
+ let path_arr = path.split('/');
+ let path_arr_ = [];
+ for (let i = 0, len = path_arr.length; i < len; i++) {
+ path_arr_.push(encodeURIComponent(path_arr[i]));
+ }
+ return path_arr_.join('/');
+}
\ No newline at end of file
diff --git a/frontend/src/css/common.css b/frontend/src/css/common.css
index 9d39abbe07..7868570705 100644
--- a/frontend/src/css/common.css
+++ b/frontend/src/css/common.css
@@ -99,17 +99,12 @@
}
.table-container table .icon {
- position: relative;
+ text-align: center;
}
.table-container table .icon img {
- position: absolute;
- display: block;
- width: 24px;
- height: 24px;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
+ width: 1.5rem;
+ height: 1.5rem;
}
/* specific handler */
.table-container table .menu-toggle {
@@ -131,6 +126,12 @@
.dropdown-item {
cursor: pointer;
}
+
+.dropdown-item.menu-inner-divider {
+ margin: 0.25rem 0;
+ border-bottom: 1px solid #ddd;
+}
+
/* end dropdown-menu style */
/* begin tip */
@@ -172,3 +173,39 @@
text-decoration: underline;
}
/* end more component */
+
+/* begin operation menu */
+.operation {
+ display: flex;
+}
+
+.operation .operation-group {
+ list-style: none;
+}
+
+.operation-group .operation-group-item {
+ display: inline-block;
+ color: #f89a68;
+ margin-right: 0.5rem;
+}
+
+.operation-group-item i {
+ font-style: normal;
+ font-size: 1.25rem;
+ line-height: 1;
+ cursor: pointer;
+ vertical-align: middle;
+}
+.operation-group-item i:hover {
+ text-decoration: underline;
+}
+
+.operation-group-item .sf-dropdown-toggle {
+ font-size: 0.85rem;
+ color: #888;
+}
+
+.operation-group-item .sf-dropdown-toggle:hover {
+ text-decoration: none;
+}
+/* end operaton menu */
diff --git a/frontend/src/globals.js b/frontend/src/globals.js
deleted file mode 100644
index 17caff3c15..0000000000
--- a/frontend/src/globals.js
+++ /dev/null
@@ -1,15 +0,0 @@
-export const gettext = window.gettext;
-export const siteRoot = window.app.config.siteRoot;
-export const lang = window.app.config.lang;
-
-export const getUrl = (options) => {
- switch (options.name) {
- case 'user_profile': return siteRoot + 'profile/' + options.username + '/';
- case 'common_lib': return siteRoot + '#common/lib/' + options.repoID + options.path;
- case 'view_lib_file': return `${siteRoot}lib/${options.repoID}/file${options.filePath}`;
- case 'download_historic_file': return `${siteRoot}repo/${options.repoID}/${options.objID}/download/?p=${options.filePath}`;
- case 'view_historic_file': return `${siteRoot}repo/${options.repoID}/history/files/?obj_id=${options.objID}&commit_id=${options.commitID}&p=${options.filePath}`;
- case 'diff_historic_file': return `${siteRoot}repo/text_diff/${options.repoID}/?commit=${options.commitID}&p=${options.filePath}`;
-
- }
-}
diff --git a/frontend/src/pages/repo-wiki-mode/main-panel.js b/frontend/src/pages/repo-wiki-mode/main-panel.js
index fe23f32fe8..76f3f50c25 100644
--- a/frontend/src/pages/repo-wiki-mode/main-panel.js
+++ b/frontend/src/pages/repo-wiki-mode/main-panel.js
@@ -10,8 +10,9 @@ class MainPanel extends Component {
constructor(props) {
super(props);
this.state = {
- isWikiMode: true
- }
+ isWikiMode: true,
+ needOperationGroup: true,
+ };
}
onMenuClick = () => {
@@ -90,7 +91,7 @@ class MainPanel extends Component {
{slug}
{pathElem}
-
+
{ this.props.isViewFileState &&
@@ -106,6 +107,9 @@ class MainPanel extends Component {
}
diff --git a/frontend/src/pages/wiki/main-panel.js b/frontend/src/pages/wiki/main-panel.js
index e65c30bc19..248cb5bbcc 100644
--- a/frontend/src/pages/wiki/main-panel.js
+++ b/frontend/src/pages/wiki/main-panel.js
@@ -6,6 +6,13 @@ import TreeDirView from '../../components/tree-dir-view/tree-dir-view';
class MainPanel extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ needOperationGroupo: false
+ };
+ }
+
onMenuClick = () => {
this.props.onMenuClick();
}
@@ -82,7 +89,10 @@ class MainPanel extends Component {
{ !this.props.isViewFileState &&
}
diff --git a/frontend/src/repo-wiki-mode.js b/frontend/src/repo-wiki-mode.js
index b8e6dfbc9f..ac64fbf6b3 100644
--- a/frontend/src/repo-wiki-mode.js
+++ b/frontend/src/repo-wiki-mode.js
@@ -352,13 +352,12 @@ class Wiki extends Component {
onDeleteNode = (node) => {
let filePath = node.path;
- if (node.isMarkdown()) {
- editorUtilities.deleteFile(filePath);
- } else if (node.isDir()) {
+ if (node.isDir()) {
editorUtilities.deleteDir(filePath);
} else {
- return false;
+ editorUtilities.deleteFile(filePath);
}
+
let isCurrentFile = false;
if (node.isDir()) {
@@ -533,9 +532,11 @@ class Wiki extends Component {
onMainNavBarClick={this.onMainNavBarClick}
onMainNodeClick={this.onMainNodeClick}
switchViewMode={this.switchViewMode}
+ onDeleteNode={this.onDeleteNode}
+ onRenameNode={this.onRenameNode}
/>
- )
+ );
}
}
diff --git a/frontend/src/utils/editor-utilties.js b/frontend/src/utils/editor-utilties.js
index 1a416e179a..254bcb76fc 100644
--- a/frontend/src/utils/editor-utilties.js
+++ b/frontend/src/utils/editor-utilties.js
@@ -12,6 +12,7 @@ class EditorUtilities {
isExpanded: item.type === 'dir' ? true : false,
parent_path: item.parent_dir,
last_update_time: item.last_update_time,
+ permission: item.permission,
size: item.size
};
});
@@ -28,6 +29,7 @@ class EditorUtilities {
isExpanded: item.type === 'dir' ? true : false,
parent_path: item.parent_dir,
last_update_time: item.mtime,
+ permission: item.permission,
size: item.size
};
});
@@ -104,6 +106,19 @@ class EditorUtilities {
publishDraft(id) {
return seafileAPI.publishDraft(id);
}
+
+ zipDownload(parent_dir, dirents) {
+ return seafileAPI.zipDownload(repoID, parent_dir, dirents);
+ }
+
+ queryZipProgress(zip_token) {
+ return seafileAPI.queryZipProgress(zip_token);
+ }
+
+ cancelZipTask(zip_token) {
+ return seafileAPI.cancelZipTask(zip_token)
+ }
+
}
const editorUtilities = new EditorUtilities();
diff --git a/frontend/src/utils/url-decorator.js b/frontend/src/utils/url-decorator.js
index 9d9cf48dd7..88492fffa5 100644
--- a/frontend/src/utils/url-decorator.js
+++ b/frontend/src/utils/url-decorator.js
@@ -1,32 +1,20 @@
-const siteRoot = window.app.config.siteRoot;
-const repoID = window.fileHistory.pageOptions.repoID;
-
+import {siteRoot, historyRepoID, fileServerRoot } from '../components/constants';
+import { encodePath } from '../components/utils';
class URLDecorator {
static getUrl(options) {
let url = '';
let params = '';
switch (options.type) {
- case 'user_profile':
- url = siteRoot + 'profile/' + options.username + '/';
- break;
- case 'common_lib':
- url = siteRoot + '#common/lib/' + repoID + options.path;
- break;
- case 'view_lib_file':
- url = siteRoot + 'lib/' + repoID + '/file' + options.filePath;
- break;
case 'download_historic_file':
params = 'p=' + options.filePath;
- url = siteRoot + 'repo/' + repoID + '/' + options.objID + '/download?' + params;
+ url = siteRoot + 'repo/' + historyRepoID + '/' + options.objID + '/download?' + params;
break;
- case 'view_historic_file':
- params = 'obj_id=' + options.objID + '&commit_id=' + options.commitID + '&p=' + options.filePath;
- url = siteRoot + 'repo/' + options.repoID + 'history/files/?' + params;
+ case 'download_dir_zip_url':
+ url = fileServerRoot + 'zip/' + options.token;
break;
- case 'diff_historic_file':
- params = 'commit_id=' + options.commitID + '&p=' + options.filePath;
- url = siteRoot + 'repo/text_diff/' + repoID + '/?' + params;
+ case 'download_file_url':
+ url = siteRoot + 'lib/' + options.repoID + "/file" + encodePath(options.filePath) + "?dl=1";
break;
default:
url = '';
diff --git a/frontend/src/wiki.js b/frontend/src/wiki.js
index 536d31df47..669b7c0faf 100644
--- a/frontend/src/wiki.js
+++ b/frontend/src/wiki.js
@@ -314,14 +314,13 @@ class Wiki extends Component {
onDeleteNode = (node) => {
let filePath = node.path;
- if (node.isMarkdown()) {
- editorUtilities.deleteFile(filePath);
- } else if (node.isDir()) {
+ if (node.isDir()) {
editorUtilities.deleteDir(filePath);
} else {
- return false;
+ editorUtilities.deleteFile(filePath);
}
+
let isCurrentFile = false;
if (node.isDir()) {
isCurrentFile = this.isModifyContainsCurrentFile(node);
@@ -494,6 +493,8 @@ class Wiki extends Component {
onSearchedClick={this.onSearchedClick}
onMainNavBarClick={this.onMainNavBarClick}
onMainNodeClick={this.onMainNodeClick}
+ onDeleteNode={this.onDeleteNode}
+ onRenameNode={this.onRenameNode}
/>
);
diff --git a/media/css/seahub_react.css b/media/css/seahub_react.css
index 7e1a6e5e83..0719b0ccd7 100644
--- a/media/css/seahub_react.css
+++ b/media/css/seahub_react.css
@@ -65,6 +65,9 @@
.sf2-icon-edit:before { content:"\e018"; }
.sf2-icon-history:before { content:"\e014"; }
.sf2-icon-trash:before { content:"\e016"; }
+.sf2-icon-download:before { content:"\e008"; }
+.sf2-icon-delete:before { content:"\e006"; }
+.sf2-icon-caret-down:before { content:"\e01a"; }
/* common class and element style*/
a { color:#eb8205; }
diff --git a/seahub/templates/base_for_react.html b/seahub/templates/base_for_react.html
index c5d9c19010..43da0c53af 100644
--- a/seahub/templates/base_for_react.html
+++ b/seahub/templates/base_for_react.html
@@ -30,7 +30,8 @@
siteTitle: '{{ site_title }}',
siteRoot: '{{ SITE_ROOT }}',
isPro: '{{ is_pro }}',
- lang: '{{ LANGUAGE_CODE }}'
+ lang: '{{ LANGUAGE_CODE }}',
+ fileServerRoot: '{{ FILE_SERVER_ROOT }}'
}
};
|