1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-06 09:21:54 +00:00

Watch file changes (#5364)

* [my libraries] redesigned 'watch/unwatch file changes'

- added 'watch file changes' & 'unwatch file changes' to the operation
menu
- added icon & tooltip for monitored libraries
- added support for mobile

* [my libs] updated 'watch/unwatch file changes'

* [notifications] update for 'watch/unwatch file changes'
This commit is contained in:
llj
2023-02-03 09:51:18 +08:00
committed by GitHub
parent 2f958cdd6b
commit 4c62d5086f
11 changed files with 1524 additions and 138 deletions

View File

@@ -182,12 +182,12 @@ module.exports = function (webpackEnv) {
// this defaults to 'window', but by setting it to 'this' then // this defaults to 'window', but by setting it to 'this' then
// module chunks which are built will work in web workers as well. // module chunks which are built will work in web workers as well.
globalObject: 'this', globalObject: 'this',
} };
if (isEnvDevelopment) { if (isEnvDevelopment) {
// webpack uses `publicPath` to determine where the app is being served from. // webpack uses `publicPath` to determine where the app is being served from.
// It requires a trailing slash, or the file assets will get an incorrect path. // It requires a trailing slash, or the file assets will get an incorrect path.
// We inferred the "public path" (such as / or /my-project) from homepage. // We inferred the "public path" (such as / or /my-project) from homepage.
output = Object.assign({}, output, {publicPath: "http://127.0.0.1:3000/assets/bundles/"}); output = Object.assign({}, output, {publicPath: 'http://127.0.0.1:3000/assets/bundles/'});
} }
return output; return output;
}; };
@@ -396,7 +396,7 @@ module.exports = function (webpackEnv) {
}, },
], ],
], ],
plugins: [ plugins: [
[ [
require.resolve('babel-plugin-named-asset-import'), require.resolve('babel-plugin-named-asset-import'),
@@ -442,7 +442,7 @@ module.exports = function (webpackEnv) {
cacheDirectory: true, cacheDirectory: true,
// See #6846 for context on why cacheCompression is disabled // See #6846 for context on why cacheCompression is disabled
cacheCompression: false, cacheCompression: false,
// Babel sourcemaps are needed for debugging into node_modules // Babel sourcemaps are needed for debugging into node_modules
// code. Without the options below, debuggers like VSCode // code. Without the options below, debuggers like VSCode
// show incorrect code and set breakpoints on the wrong lines. // show incorrect code and set breakpoints on the wrong lines.
@@ -524,6 +524,24 @@ module.exports = function (webpackEnv) {
'sass-loader' 'sass-loader'
), ),
}, },
{
test: /\.svg$/,
use: [
{
loader: 'svg-sprite-loader', options: {}
},
{ loader: 'svgo-loader', options: {
plugins:[
'removeTitle',
'removeStyleElement',
'cleanupIDs',
'inlineStyles',
'removeXMLProcInst',
]
}
}
]
},
// "file" loader makes sure those assets get served by WebpackDevServer. // "file" loader makes sure those assets get served by WebpackDevServer.
// When you `import` an asset, you get its (virtual) filename. // When you `import` an asset, you get its (virtual) filename.
// In production, they would get copied to the `build` folder. // In production, they would get copied to the `build` folder.
@@ -715,11 +733,11 @@ module.exports = function (webpackEnv) {
contentBase: '../assets', contentBase: '../assets',
historyApiFallback: true, historyApiFallback: true,
headers: { headers: {
"Access-Control-Allow-Origin": "*", 'Access-Control-Allow-Origin': '*',
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS", 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
"Access-Control-Allow-Headers": "X-Requested-With, content-type, Authorization" 'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization'
} }
}, },
}; };
}; };

File diff suppressed because it is too large Load Diff

View File

@@ -39,6 +39,8 @@
"reactstrap": "8.9.0", "reactstrap": "8.9.0",
"seafile-js": "0.2.191", "seafile-js": "0.2.191",
"socket.io-client": "^2.2.0", "socket.io-client": "^2.2.0",
"svg-sprite-loader": "^6.0.11",
"svgo-loader": "^3.0.1",
"unified": "^7.0.0", "unified": "^7.0.0",
"url-parse": "^1.4.3", "url-parse": "^1.4.3",
"video.js": "^7.4.1", "video.js": "^7.4.1",

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 32 32" style="enable-background:new 0 0 32 32;" xml:space="preserve">
<style type="text/css">
.st0{fill:#999999;}
</style>
<title>monitor</title>
<g id="monitor">
<g id="形状" transform="translate(1.000000, 1.000000)">
<path class="st0" fill="currentColor" d="M25.1,20.8c-0.5-0.2-0.8-0.6-0.9-1.1v-4.7c0-4.4-4.2-7.3-6-8.3c-0.1-0.5-0.4-1.4-1-2
c-0.6-0.6-1.4-0.9-2.3-0.8h-0.1c-0.8,0-1.6,0.3-2.3,0.8c-0.7,0.6-0.9,1.5-1,2c-1.8,0.9-6,3.8-6,8.3v4.6c0,0.5-0.4,0.9-0.9,1.1
c-1.6,0.7-1.8,2.1-1.4,3.1c0.2,0.5,0.6,1,3.6,1h2.5c0.2,2.6,2.5,4.6,5.4,4.6s5.3-2.1,5.4-4.6h2.6c3,0,3.4-0.5,3.6-1
C26.9,22.9,26.7,21.5,25.1,20.8L25.1,20.8z M14.9,27.8c-1.9,0-3.4-1.3-3.6-2.9h7.1C18.3,26.5,16.8,27.8,14.9,27.8L14.9,27.8z
M22.9,23.1H7c-0.6,0-1.3,0-1.9-0.1c0-0.3,0.1-0.4,0.5-0.6c1.1-0.5,1.9-1.5,1.9-2.7v-4.7c0-3.9,4.5-6.5,5.3-6.9L13.4,8V7.4
c0-0.2,0.1-1.7,1.5-1.7H15c1.4,0,1.5,1.5,1.5,1.7V8L17,8.2c0.8,0.3,5.3,2.9,5.3,6.9v4.7c0,1.2,0.8,2.2,1.9,2.7
c0.4,0.2,0.5,0.3,0.5,0.6C24.2,23.1,23.5,23.1,22.9,23.1L22.9,23.1z"/>
<path class="st0" d="M23.9,0.2c0.4,0.3,0.6,0.9,0.3,1.3l-2.1,3.2c-0.3,0.4-0.9,0.6-1.3,0.3s-0.6-0.9-0.3-1.3l2.1-3.2
C22.8,0,23.4-0.1,23.9,0.2z M29.9,7.1c0.2,0.5,0,1.1-0.5,1.3L25.9,10c-0.5,0.2-1,0-1.3-0.5c-0.2-0.5,0-1,0.5-1.3l3.5-1.6
C29.1,6.4,29.7,6.6,29.9,7.1L29.9,7.1z M0.1,7.1c0.1-0.2,0.3-0.4,0.5-0.5c0.2-0.1,0.5-0.1,0.7,0l3.5,1.6c0.3,0.1,0.5,0.5,0.5,0.8
c0,0.3-0.1,0.7-0.4,0.9c-0.3,0.2-0.6,0.2-1,0.1L0.6,8.4C0.1,8.2-0.1,7.6,0.1,7.1L0.1,7.1z M6.1,0.2C6.3,0,6.6,0,6.8,0
s0.5,0.2,0.6,0.4l2.1,3.2c0.2,0.3,0.2,0.7,0.1,1S9.1,5.2,8.8,5.2C8.4,5.2,8.1,5,7.9,4.7L5.8,1.5C5.7,1.3,5.6,1,5.7,0.8
C5.8,0.5,5.9,0.3,6.1,0.2L6.1,0.2L6.1,0.2z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -165,7 +165,7 @@ class NoticeItem extends React.Component {
// 2. handle xss(cross-site scripting) // 2. handle xss(cross-site scripting)
notice = notice.replace('{upload_file_link}', `${fileName}`); notice = notice.replace('{upload_file_link}', `${fileName}`);
notice = Utils.HTMLescape(notice); notice = Utils.HTMLescape(notice);
notice = notice.replace('{uploaded_link}', `<strong>Deleted Library</strong>`); notice = notice.replace('{uploaded_link}', '<strong>Deleted Library</strong>');
} }
return {avatar_url, notice}; return {avatar_url, notice};
} }
@@ -226,69 +226,85 @@ class NoticeItem extends React.Component {
} }
if (noticeType === MSG_TYPE_REPO_MONITOR) { if (noticeType === MSG_TYPE_REPO_MONITOR) {
let avatar_url = detail.op_user_avatar_url; const {
let repoLink = siteRoot + 'library/' + detail.repo_id + '/' + detail.repo_name + '/'; op_user_avatar_url: avatar_url,
let notice = ''; op_user_email,
op_user_name,
op_type,
repo_id, repo_name,
obj_type,
obj_path_list,
old_obj_path_list
} = detail;
let op = ''; const userProfileURL = `${siteRoot}profile/${encodeURIComponent(op_user_email)}`;
if (detail.obj_type == 'file') { const userLink = `<a href=${userProfileURL} target="_blank">${Utils.HTMLescape(op_user_name)}</a>`;
switch (detail.op_type) {
const repoURL = `${siteRoot}library/${repo_id}/${encodeURIComponent(repo_name)}/`;
const repoLink = `<a href=${repoURL} target="_blank">${Utils.HTMLescape(repo_name)}</a>`;
let notice = '';
if (obj_type == 'file') {
const fileName = Utils.getFileName(obj_path_list[0]);
const fileURL = `${siteRoot}lib/${repo_id}/file${Utils.encodePath(obj_path_list[0])}`;
const fileLink = `<a href=${fileURL} target="_blank">${Utils.HTMLescape(fileName)}</a>`;
switch (op_type) {
case 'create': case 'create':
op = gettext('created file'); notice = obj_path_list.length == 1 ? gettext('{user} created file {fileName} in library {libraryName}.') : gettext('{user} created file {fileName} and {fileCount} other file(s) in library {libraryName}.');
break; break;
case 'delete': case 'delete':
op = gettext('deleted file'); notice = obj_path_list.length == 1 ? gettext('{user} deleted file {fileName} in library {libraryName}.') : gettext('{user} deleted file {fileName} and {fileCount} other file(s) in library {libraryName}.');
notice = notice.replace('{fileName}', fileName);
break; break;
case 'recover': case 'recover':
op = gettext('restored file'); notice = gettext('{user} restored file {fileName} in library {libraryName}.');
break; break;
case 'rename': case 'rename':
op = gettext('renamed file'); notice = gettext('{user} renamed file {oldFileName} {fileName} in library {libraryName}.');
notice = notice.replace('{oldFileName}', Utils.getFileName(old_obj_path_list[0]));
break; break;
case 'move': case 'move':
op = gettext('moved file'); notice = obj_path_list.length == 1 ? gettext('{user} moved file {fileName} in library {libraryName}.') : gettext('{user} moved file {fileName} and {fileCount} other file(s) in library {libraryName}.');
break; break;
case 'edit': case 'edit':
op = gettext('updated file'); notice = gettext('{user} updated file {fileName} in library {libraryName}.');
break; break;
// no default
} }
notice = notice.replace('{fileName}', fileLink);
notice = notice.replace('{fileCount}', obj_path_list.length - 1);
} else { // dir } else { // dir
const folderName = Utils.getFolderName(obj_path_list[0]);
const folderURL = `${siteRoot}library/${repo_id}/${encodeURIComponent(repo_name)}${Utils.encodePath(obj_path_list[0])}`;
const folderLink = `<a href=${folderURL} target="_blank">${Utils.HTMLescape(folderName)}</a>`;
switch (detail.op_type) { switch (detail.op_type) {
case 'create': case 'create':
op = gettext('created folder'); notice = obj_path_list.length == 1 ? gettext('{user} created folder {folderName} in library {libraryName}.') : gettext('{user} created folder {folderName} and {folderCount} other folder(s) in library {libraryName}.');
break; break;
case 'delete': case 'delete':
op = gettext('deleted folder'); notice = obj_path_list.length == 1 ? gettext('{user} deleted folder {folderName} in library {libraryName}.') : gettext('{user} deleted folder {folderName} and {folderCount} other folder(s) in library {libraryName}.');
notice = notice.replace('{folderName}', folderName);
break; break;
case 'recover': case 'recover':
op = gettext('restored folder'); notice = gettext('{user} restored folder {folderName} in library {libraryName}.');
break; break;
case 'rename': case 'rename':
op = gettext('renamed folder'); notice = gettext('{user} renamed folder {oldFolderName} {folderName} in library {libraryName}.');
notice = notice.replace('{oldFolderName}', Utils.getFolderName(old_obj_path_list[0]));
break; break;
case 'move': case 'move':
op = gettext('moved folder'); notice = obj_path_list.length == 1 ? gettext('{user} moved folder {folderName} in library {libraryName}.') : gettext('{user} moved folder {folderName} and {folderCount} other folder(s) in library {libraryName}.');
break; break;
// no default
} }
notice = notice.replace('{folderName}', folderLink);
notice = notice.replace('{folderCount}', obj_path_list.length - 1);
} }
// 1. handle translate notice = notice.replace('{user}', userLink);
notice = gettext('{op_user} {op_type} {obj_name} in {repo_link}.'); notice = notice.replace('{libraryName}', repoLink);
let obj_name = Utils.getFileName(detail.obj_path_list[0]); return { avatar_url, notice };
// 2. handle xss(cross-site scripting)
notice = notice.replace('{op_user}', `${detail.op_user_name}`);
notice = notice.replace('{op_type}', `${op}`);
notice = notice.replace('{obj_name}', `${obj_name}`);
notice = notice.replace('{repo_link}', `{tagA}${detail.repo_name}{/tagA}`);
notice = Utils.HTMLescape(notice);
// 3. add jump link
notice = notice.replace('{tagA}', `<a href=${Utils.encodePath(repoLink)}>`);
notice = notice.replace('{/tagA}', '</a>');
return {avatar_url, notice};
} }
// if (noticeType === MSG_TYPE_GUEST_INVITATION_ACCEPTED) { // if (noticeType === MSG_TYPE_GUEST_INVITATION_ACCEPTED) {

View File

@@ -0,0 +1,30 @@
import React from 'react';
import PropTypes from 'prop-types';
import '../css/icon.css';
const importAll = (requireContext) => {
requireContext.keys().forEach(requireContext);
};
try {
importAll(require.context('../assets/icons', true, /\.svg$/));
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
const Icon = (props) => {
const { className, symbol } = props;
const iconClass = `seafile-multicolor-icon seafile-multicolor-icon-${symbol} ${className || ''}`;
return (
<svg className={iconClass}>
<use xlinkHref={`#${symbol}`} />
</svg>
);
};
Icon.propTypes = {
symbol: PropTypes.string.isRequired,
className: PropTypes.string,
};
export default Icon;

12
frontend/src/css/icon.css Normal file
View File

@@ -0,0 +1,12 @@
.seafile-multicolor-icon {
width: 1em;
height: 1em;
fill: currentColor;
overflow: hidden;
}
.seafile-multicolor-icon-monitor {
font-size: 14px;
fill: #444;
color: #bdbdbd;
}

View File

@@ -112,6 +112,16 @@ class MyLibraries extends Component {
this.setState({repoList: repoList}); this.setState({repoList: repoList});
} }
onMonitorRepo = (repo, monitored) => {
let repoList = this.state.repoList.map(item => {
if (item.repo_id === repo.repo_id) {
item.monitored = monitored;
}
return item;
});
this.setState({repoList: repoList});
}
onDeleteRepo = (repo) => { onDeleteRepo = (repo) => {
let repoList = this.state.repoList.filter(item => { let repoList = this.state.repoList.filter(item => {
return item.repo_id !== repo.repo_id; return item.repo_id !== repo.repo_id;
@@ -168,6 +178,7 @@ class MyLibraries extends Component {
onRenameRepo={this.onRenameRepo} onRenameRepo={this.onRenameRepo}
onDeleteRepo={this.onDeleteRepo} onDeleteRepo={this.onDeleteRepo}
onTransferRepo={this.onTransferRepo} onTransferRepo={this.onTransferRepo}
onMonitorRepo={this.onMonitorRepo}
onRepoClick={this.onRepoClick} onRepoClick={this.onRepoClick}
sortRepoList={this.sortRepoList} sortRepoList={this.sortRepoList}
/> />

View File

@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import MediaQuery from 'react-responsive'; import MediaQuery from 'react-responsive';
import moment from 'moment'; import moment from 'moment';
import { Link, navigate } from '@gatsbyjs/reach-router'; import { Link, navigate } from '@gatsbyjs/reach-router';
import { UncontrolledTooltip } from 'reactstrap';
import { Utils } from '../../utils/utils'; import { Utils } from '../../utils/utils';
import { seafileAPI } from '../../utils/seafile-api'; import { seafileAPI } from '../../utils/seafile-api';
import { gettext, siteRoot, storages } from '../../utils/constants'; import { gettext, siteRoot, storages } from '../../utils/constants';
@@ -21,6 +22,7 @@ import MylibRepoMenu from './mylib-repo-menu';
import RepoAPITokenDialog from '../../components/dialog/repo-api-token-dialog'; import RepoAPITokenDialog from '../../components/dialog/repo-api-token-dialog';
import RepoShareUploadLinksDialog from '../../components/dialog/repo-share-upload-links-dialog'; import RepoShareUploadLinksDialog from '../../components/dialog/repo-share-upload-links-dialog';
import LibOldFilesAutoDelDialog from '../../components/dialog/lib-old-files-auto-del-dialog'; import LibOldFilesAutoDelDialog from '../../components/dialog/lib-old-files-auto-del-dialog';
import Icon from '../../components/icon';
const propTypes = { const propTypes = {
repo: PropTypes.object.isRequired, repo: PropTypes.object.isRequired,
@@ -40,7 +42,6 @@ class MylibRepoListItem extends React.Component {
this.state = { this.state = {
isOpIconShow: false, isOpIconShow: false,
isStarred: this.props.repo.starred, isStarred: this.props.repo.starred,
isMonitored: this.props.repo.monitored,
isRenaming: false, isRenaming: false,
isShareDialogShow: false, isShareDialogShow: false,
isDeleteDialogShow: false, isDeleteDialogShow: false,
@@ -110,6 +111,12 @@ class MylibRepoListItem extends React.Component {
case 'Reset Password': case 'Reset Password':
this.onResetPasswordToggle(); this.onResetPasswordToggle();
break; break;
case 'Watch File Changes':
this.watchFileChanges();
break;
case 'Unwatch File Changes':
this.unwatchFileChanges();
break;
case 'Folder Permission': case 'Folder Permission':
this.onFolderPermissionToggle(); this.onFolderPermissionToggle();
break; break;
@@ -166,30 +173,24 @@ class MylibRepoListItem extends React.Component {
} }
} }
onToggleMonitorRepo = (e) => { watchFileChanges = () => {
e.preventDefault(); const { repo } = this.props;
const repoName = this.props.repo.repo_name; seafileAPI.monitorRepo(repo.repo_id).then(() => {
if (this.state.isMonitored) { this.props.onMonitorRepo(repo, true);
seafileAPI.unMonitorRepo(this.props.repo.repo_id).then(() => { }).catch(error => {
this.setState({isMonitored: !this.state.isMonitored}); let errMessage = Utils.getErrorMsg(error);
const msg = gettext('Successfully unmonitored {library_name_placeholder}.') toaster.danger(errMessage);
.replace('{library_name_placeholder}', repoName); });
toaster.success(msg); }
}).catch(error => {
let errMessage = Utils.getErrorMsg(error); unwatchFileChanges = () => {
toaster.danger(errMessage); const { repo } = this.props;
}); seafileAPI.unMonitorRepo(repo.repo_id).then(() => {
} else { this.props.onMonitorRepo(repo, false);
seafileAPI.monitorRepo(this.props.repo.repo_id).then(() => { }).catch(error => {
this.setState({isMonitored: !this.state.isMonitored}); let errMessage = Utils.getErrorMsg(error);
const msg = gettext('Successfully monitored {library_name_placeholder}.') toaster.danger(errMessage);
.replace('{library_name_placeholder}', repoName); });
toaster.success(msg);
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
}
} }
onShareToggle = (e) => { onShareToggle = (e) => {
@@ -324,13 +325,6 @@ class MylibRepoListItem extends React.Component {
<i className={`fa-star ${this.state.isStarred ? 'fas' : 'far star-empty'}`}></i> <i className={`fa-star ${this.state.isStarred ? 'fas' : 'far star-empty'}`}></i>
</a> </a>
</td> </td>
<td className="text-center">
<a href="#" role="button" aria-label={this.state.isMonitored ? gettext('unMonitor') : gettext('Monitor')} onClick={this.onToggleMonitorRepo}>
<i className={`fa-star ${this.state.isMonitored ? 'fas' : 'far star-empty'}`}></i>
</a>
</td>
<td><img src={iconUrl} title={iconTitle} alt={iconTitle} width="24" /></td> <td><img src={iconUrl} title={iconTitle} alt={iconTitle} width="24" /></td>
<td> <td>
{this.state.isRenaming && ( {this.state.isRenaming && (
@@ -341,7 +335,22 @@ class MylibRepoListItem extends React.Component {
/> />
)} )}
{!this.state.isRenaming && repo.repo_name && ( {!this.state.isRenaming && repo.repo_name && (
<Link to={repoURL}>{repo.repo_name}</Link> <Fragment>
<Link to={repoURL}>{repo.repo_name}</Link>
{repo.monitored && (
<Fragment>
<span id={`watching-${repo.repo_id}`} className="ml-1">
<Icon symbol='monitor' />
</span>
<UncontrolledTooltip
placement="bottom"
target={`#watching-${repo.repo_id}`}
>
{gettext('You are watching file changes of this library.')}
</UncontrolledTooltip>
</Fragment>
)}
</Fragment>
)} )}
{!this.state.isRenaming && !repo.repo_name && {!this.state.isRenaming && !repo.repo_name &&
(gettext('Broken (please contact your administrator to fix this library)')) (gettext('Broken (please contact your administrator to fix this library)'))
@@ -387,7 +396,22 @@ class MylibRepoListItem extends React.Component {
/> />
)} )}
{!this.state.isRenaming && repo.repo_name && ( {!this.state.isRenaming && repo.repo_name && (
<div><Link to={repoURL}>{repo.repo_name}</Link></div> <div>
<Link to={repoURL}>{repo.repo_name}</Link>
{repo.monitored && (
<Fragment>
<span id={`watching-${repo.repo_id}`} className="ml-1">
<Icon symbol='monitor' />
</span>
<UncontrolledTooltip
placement="bottom"
target={`#watching-${repo.repo_id}`}
>
{gettext('You are watching file changes of this library.')}
</UncontrolledTooltip>
</Fragment>
)}
</div>
)} )}
{!this.state.isRenaming && !repo.repo_name && {!this.state.isRenaming && !repo.repo_name &&
<div>(gettext('Broken (please contact your administrator to fix this library)'))</div> <div>(gettext('Broken (please contact your administrator to fix this library)'))</div>

View File

@@ -68,6 +68,7 @@ class MylibRepoListView extends React.Component {
onRenameRepo={this.props.onRenameRepo} onRenameRepo={this.props.onRenameRepo}
onDeleteRepo={this.props.onDeleteRepo} onDeleteRepo={this.props.onDeleteRepo}
onTransferRepo={this.props.onTransferRepo} onTransferRepo={this.props.onTransferRepo}
onMonitorRepo={this.props.onMonitorRepo}
onRepoClick={this.props.onRepoClick} onRepoClick={this.props.onRepoClick}
/> />
); );
@@ -85,9 +86,8 @@ class MylibRepoListView extends React.Component {
<tr> <tr>
<th width="4%"></th> <th width="4%"></th>
<th width="4%"><span className="sr-only">{gettext('Library Type')}</span></th> <th width="4%"><span className="sr-only">{gettext('Library Type')}</span></th>
<th width="4%"><span className="sr-only">{gettext('Library Type')}</span></th>
<th width={showStorageBackend ? '33%' : '38%'}><a className="d-block table-sort-op" href="#" onClick={this.sortByName}>{gettext('Name')} {this.props.sortBy === 'name' && sortIcon}</a></th> <th width={showStorageBackend ? '33%' : '38%'}><a className="d-block table-sort-op" href="#" onClick={this.sortByName}>{gettext('Name')} {this.props.sortBy === 'name' && sortIcon}</a></th>
<th width="10%"><span className="sr-only">{gettext('Actions')}</span></th> <th width="14%"><span className="sr-only">{gettext('Actions')}</span></th>
<th width={showStorageBackend ? '15%' : '20%'}><a className="d-block table-sort-op" href="#" onClick={this.sortBySize}>{gettext('Size')} {this.props.sortBy === 'size' && sortIcon}</a></th> <th width={showStorageBackend ? '15%' : '20%'}><a className="d-block table-sort-op" href="#" onClick={this.sortBySize}>{gettext('Size')} {this.props.sortBy === 'size' && sortIcon}</a></th>
{showStorageBackend ? <th width="15%">{gettext('Storage Backend')}</th> : null} {showStorageBackend ? <th width="15%">{gettext('Storage Backend')}</th> : null}
<th width={showStorageBackend ? '15%' : '20%'}><a className="d-block table-sort-op" href="#" onClick={this.sortByTime}>{gettext('Last Update')} {this.props.sortBy === 'time' && sortIcon}</a></th> <th width={showStorageBackend ? '15%' : '20%'}><a className="d-block table-sort-op" href="#" onClick={this.sortByTime}>{gettext('Last Update')} {this.props.sortBy === 'time' && sortIcon}</a></th>

View File

@@ -70,12 +70,19 @@ class MylibRepoMenu extends React.Component {
operations.push('Folder Permission'); operations.push('Folder Permission');
} }
operations.push('Share Links Admin', 'Divider'); operations.push('Share Links Admin', 'Divider');
if (repo.encrypted) { if (repo.encrypted) {
operations.push('Change Password'); operations.push('Change Password');
} }
if (showResetPasswordMenuItem) { if (showResetPasswordMenuItem) {
operations.push('Reset Password'); operations.push('Reset Password');
} }
if (isPro) {
const monitorOp = repo.monitored ? 'Unwatch File Changes' : 'Watch File Changes';
operations.push(monitorOp);
}
operations.push('History Setting', 'API Token'); operations.push('History Setting', 'API Token');
if (this.props.isPC && enableRepoSnapshotLabel) { if (this.props.isPC && enableRepoSnapshotLabel) {
operations.push('Label Current State'); operations.push('Label Current State');
@@ -116,6 +123,12 @@ class MylibRepoMenu extends React.Component {
case 'Reset Password': case 'Reset Password':
translateResult = gettext('Reset Password'); translateResult = gettext('Reset Password');
break; break;
case 'Watch File Changes':
translateResult = gettext('Watch File Changes');
break;
case 'Unwatch File Changes':
translateResult = gettext('Unwatch File Changes');
break;
case 'Folder Permission': case 'Folder Permission':
translateResult = gettext('Folder Permission'); translateResult = gettext('Folder Permission');
break; break;