diff --git a/frontend/src/shared-dir-view.js b/frontend/src/shared-dir-view.js index a41d08f490..7152bdeb64 100644 --- a/frontend/src/shared-dir-view.js +++ b/frontend/src/shared-dir-view.js @@ -1,6 +1,7 @@ import React, { Fragment } from 'react'; +import MD5 from 'MD5'; import ReactDOM from 'react-dom'; -import { Button, Dropdown, DropdownToggle, DropdownItem } from 'reactstrap'; +import { Button, Modal, ModalHeader, ModalBody, ModalFooter, Dropdown, DropdownToggle, DropdownItem, UncontrolledTooltip} from 'reactstrap'; import moment from 'moment'; import Account from './components/common/account'; import { isPro, useGoFileserver, fileServerRoot, gettext, siteRoot, mediaUrl, logoPath, logoWidth, logoHeight, siteTitle, thumbnailSizeForOriginal } from './utils/constants'; @@ -15,8 +16,13 @@ import FileUploader from './components/shared-link-file-uploader/file-uploader'; import SaveSharedDirDialog from './components/dialog/save-shared-dir-dialog'; import CopyMoveDirentProgressDialog from './components/dialog/copy-move-dirent-progress-dialog'; +import RepoTag from './models/repo-tag'; +import FileTag from './models/file-tag'; + + import './css/shared-dir-view.css'; import './css/grid-view.css'; +import './css/repo-info-bar.css'; moment.locale(window.app.config.lang); @@ -49,6 +55,9 @@ class SharedDirView extends React.Component { isZipDialogOpen: false, zipFolderPath: '', + usedRepoTags: [], + isRepoInfoBarShow: false, + isSaveSharedDirDialogShow: false, itemsForSave: [], @@ -88,6 +97,8 @@ class SharedDirView extends React.Component { errorMsg: errorMsg }); }); + + this.getShareLinkRepoTags() } sortItems = (sortBy, sortOrder) => { @@ -400,6 +411,25 @@ class SharedDirView extends React.Component { seafileAPI.shareLinksUploadDone(token, Utils.joinPath(dirPath, name)); } + getShareLinkRepoTags = () => { + seafileAPI.getShareLinkRepoTags(token).then(res => { + let usedRepoTags = []; + res.data.repo_tags.forEach(item => { + let usedRepoTag = new RepoTag(item); + if (usedRepoTag.fileCount > 0) { + usedRepoTags.push(usedRepoTag); + } + }); + this.setState({usedRepoTags: usedRepoTags}); + if (usedRepoTags.length != 0 && relativePath == '/') { + this.setState({isRepoInfoBarShow: true}); + } + }).catch(error => { + let errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }); + } + render() { const isDesktop = Utils.isDesktop(); const modeBaseClass = 'btn btn-secondary btn-icon sf-view-mode-btn'; @@ -462,6 +492,16 @@ class SharedDirView extends React.Component { onFileUploadSuccess={this.onFileUploadSuccess} /> )} + + {this.state.isRepoInfoBarShow && ( + + )} + {gettext('Name')} {sortBy == 'name' && sortIcon} + {gettext('Size')} {sortBy == 'size' && sortIcon} {gettext('Last Update')} {sortBy == 'time' && sortIcon} @@ -674,6 +715,13 @@ class Item extends React.Component { const { item, isDesktop } = this.props; const { isIconShown } = this.state; + let toolTipID = ''; + let tagTitle = ''; + if (item.file_tags && item.file_tags.length > 0) { + toolTipID = MD5(item.file_name).slice(0, 7); + tagTitle = item.file_tags.map(item => item.tag_name).join(' '); + } + if (item.is_dir) { return isDesktop ? ( @@ -687,6 +735,7 @@ class Item extends React.Component { {item.folder_name} + {moment(item.last_modified).fromNow()} {showDownloadIcon && @@ -743,6 +792,23 @@ class Item extends React.Component { {item.file_name} + + {(item.file_tags && item.file_tags.length > 0) && ( + +
+ {item.file_tags.map((fileTag, index) => { + let length = item.file_tags.length; + return ( + + ); + })} +
+ + {tagTitle} + +
+ )} + {Utils.bytesToSize(item.size)} {moment(item.last_modified).fromNow()} @@ -862,6 +928,176 @@ class GridItem extends React.Component { } } + +class RepoInfoBar extends React.Component { + + constructor(props) { + super(props); + this.state = { + currentTag: null, + isListTaggedFileShow: false, + }; + } + + onListTaggedFiles = (currentTag) => { + this.setState({ + currentTag: currentTag, + isListTaggedFileShow: !this.state.isListTaggedFileShow, + }); + } + + onCloseDialog = () => { + this.setState({ + isListTaggedFileShow: false + }); + } + + render() { + let {repoID, currentPath, usedRepoTags} = this.props; + // let href = readmeMarkdown !== null ? siteRoot + 'lib/' + repoID + '/file' + Utils.joinPath(currentPath, readmeMarkdown.name) + '?mode=edit' : ''; + // let filePath = readmeMarkdown !== null ? currentPath + readmeMarkdown.name : ''; + return ( +
+ {usedRepoTags.length > 0 && ( +
    + {usedRepoTags.map((usedRepoTag) => { + return ( +
  • + + {usedRepoTag.name} + +
  • + ); + })} +
+ )} + {this.state.isListTaggedFileShow && ( + + + + )} +
+ ); + } +} + + +class ListTaggedFilesDialog extends React.Component { + + constructor(props) { + super(props); + this.state = { + taggedFileList: [], + }; + } + + componentDidMount() { + this.getTaggedFiles(); + } + + getTaggedFiles = () => { + let { repoID, currentTag } = this.props; + seafileAPI.getShareLinkTaggedFiles(token, currentTag.id).then(res => { + let taggedFileList = []; + res.data.tagged_files !== undefined && + res.data.tagged_files.forEach(file => { + let taggedFile = file; + taggedFileList.push(taggedFile); + }); + this.setState({ + taggedFileList: taggedFileList, + }); + }).catch(error => { + let errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }); + } + + render() { + let taggedFileList = this.state.taggedFileList; + return ( + + {gettext('Tagged Files')} + + + + + + + + + + + {taggedFileList.map((taggedFile, index) => { + return ( + + ); + })} + +
{gettext('Name')}{gettext('Size')}{gettext('Last Update')}
+
+ + + +
+ ); + } +} + +class TaggedFile extends React.Component { + + constructor(props) { + super(props); + this.state = ({ + active: false, + }); + } + + onMouseEnter = () => { + this.setState({ + active: true + }); + } + + onMouseLeave = () => { + this.setState({ + active: false + }); + } + + render() { + const taggedFile = this.props.taggedFile; + let className = this.state.active ? 'action-icon sf2-icon-x3' : 'action-icon vh sf2-icon-x3'; + let path = taggedFile.parent_path ? Utils.joinPath(taggedFile.parent_path, taggedFile.filename) : ''; + let href = siteRoot + 'd/' + token + '/files/?p=' + Utils.encodePath(path); + return ( taggedFile.file_deleted ? + + {taggedFile.filename}{' '} + {gettext('deleted')} + + + : + + {taggedFile.filename} + {Utils.bytesToSize(taggedFile.size)} + {moment.unix(taggedFile.mtime).fromNow()} + + ); + } +} + ReactDOM.render( , document.getElementById('wrapper') diff --git a/seahub/api2/endpoints/share_links.py b/seahub/api2/endpoints/share_links.py index 4dcde26a95..4a68d340b1 100644 --- a/seahub/api2/endpoints/share_links.py +++ b/seahub/api2/endpoints/share_links.py @@ -8,6 +8,7 @@ import posixpath from constance import config from dateutil.relativedelta import relativedelta import dateutil.parser +from collections import defaultdict from rest_framework.authentication import SessionAuthentication from rest_framework.permissions import IsAuthenticated @@ -35,6 +36,7 @@ from seahub.utils import gen_shared_link, is_org_context, normalize_file_path, \ check_filename_with_rename, gen_file_upload_url, get_password_strength_level from seahub.utils.file_op import if_locked_by_online_office from seahub.utils.file_types import IMAGE, VIDEO, XMIND +from seahub.utils.file_tags import get_tagged_files, get_files_tags_in_dir from seahub.utils.timeutils import datetime_to_isoformat_timestr, \ timestamp_to_isoformat_timestr from seahub.utils.repo import parse_repo_perm @@ -48,6 +50,7 @@ from seahub.wiki.models import Wiki from seahub.views.file import can_edit_file from seahub.views import check_folder_permission from seahub.signals import upload_file_successful +from seahub.repo_tags.models import RepoTags logger = logging.getLogger(__name__) @@ -789,6 +792,12 @@ class ShareLinkDirents(APIView): error_msg = 'Internal Server Error' return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + try: + files_tags_in_dir = get_files_tags_in_dir(repo_id, path) + except Exception as e: + logger.error(e) + files_tags_in_dir = {} + result = [] for dirent in dirent_list: @@ -818,6 +827,13 @@ class ShareLinkDirents(APIView): src = get_share_link_thumbnail_src(token, thumbnail_size, req_image_path) dirent_info['encoded_thumbnail_src'] = urlquote(src) + # get tag info + file_tags = files_tags_in_dir.get(dirent.obj_name, []) + if file_tags: + dirent_info['file_tags'] = [] + for file_tag in file_tags: + dirent_info['file_tags'].append(file_tag) + result.append(dirent_info) return Response({'dirent_list': result}) @@ -1217,3 +1233,93 @@ class ShareLinkSaveItemsToRepo(APIView): result['task_id'] = res.task_id return Response(result) + + +class ShareLinkRepoTags(APIView): + + throttle_classes = (UserRateThrottle,) + + def get(self, request, token): + """get all repo_tags by share link token. + """ + + try: + share_link = FileShare.objects.get(token=token) + except FileShare.DoesNotExist: + error_msg = 'Token %s not found.' % token + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + repo_id = share_link.repo_id + repo = seafile_api.get_repo(repo_id) + if not repo: + error_msg = 'Library %s not found.' % repo_id + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + # get all tags in repo + repo_tags = RepoTags.objects.filter(repo_id=repo_id) + + # get tagged files by tag id + tag_id_file_list_dict = defaultdict(list) + for repo_tag in repo_tags: + tagged_files = get_tagged_files(repo, repo_tag.pk)['tagged_files'] + tagged_files = [item for item in tagged_files if item.get('parent_path') and item.get('parent_path').startswith(share_link.path.rstrip('/'))] + tag_id_file_list_dict[repo_tag.pk] = tagged_files + + # generate response + result = { + "repo_tags": [] + } + + for repo_tag in repo_tags: + + repo_tag_info = repo_tag.to_dict() + repo_tag_id = repo_tag_info["repo_tag_id"] + repo_tag_info["files_count"] = len(tag_id_file_list_dict.get(repo_tag_id, [])) + + result['repo_tags'].append(repo_tag_info) + + return Response(result) + + +class ShareLinkRepoTagsTaggedFiles(APIView): + + throttle_classes = (UserRateThrottle,) + + def get(self, request, token, tag_id): + """get tagged files by share link token and tag id. + """ + + try: + share_link = FileShare.objects.get(token=token) + except FileShare.DoesNotExist: + error_msg = 'Token %s not found.' % token + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + repo_id = share_link.repo_id + repo = seafile_api.get_repo(repo_id) + if not repo: + error_msg = 'Library %s not found.' % repo_id + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + repo_tag = RepoTags.objects.get_repo_tag_by_id(tag_id) + if not repo_tag: + error_msg = 'repo_tag %s not found.' % tag_id + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + share_link_path = share_link.path.rstrip('/') if share_link.path != '/' else '/' + + filtered_tagged_files = [] + tagged_files = get_tagged_files(repo, tag_id) + for tagged_file in tagged_files.get('tagged_files', []): + + if tagged_file.get('file_deleted', False): + continue + + tagged_file_parent_path = tagged_file.get('parent_path', '') + if share_link_path == '/': + filtered_tagged_files.append(tagged_file) + elif tagged_file_parent_path.startswith(share_link_path): + tagged_file['parent_path'] = '/' + tagged_file_parent_path.lstrip(share_link_path) + filtered_tagged_files.append(tagged_file) + + return Response({'tagged_files': filtered_tagged_files}) diff --git a/seahub/urls.py b/seahub/urls.py index bdb42ae176..b38ccd360f 100644 --- a/seahub/urls.py +++ b/seahub/urls.py @@ -39,7 +39,8 @@ from seahub.api2.endpoints.group_members import GroupMembers, GroupSearchMember, from seahub.api2.endpoints.search_group import SearchGroup from seahub.api2.endpoints.share_links import ShareLinks, ShareLink, \ ShareLinkOnlineOfficeLock, ShareLinkDirents, ShareLinkSaveFileToRepo, \ - ShareLinkUpload, ShareLinkUploadDone, ShareLinkSaveItemsToRepo + ShareLinkUpload, ShareLinkUploadDone, ShareLinkSaveItemsToRepo, \ + ShareLinkRepoTags, ShareLinkRepoTagsTaggedFiles from seahub.api2.endpoints.shared_folders import SharedFolders from seahub.api2.endpoints.shared_repos import SharedRepos, SharedRepo from seahub.api2.endpoints.upload_links import UploadLinks, UploadLink, \ @@ -348,6 +349,9 @@ urlpatterns = [ url(r'^api/v2.1/share-links/(?P[a-f0-9]+)/upload/$', ShareLinkUpload.as_view(), name='api-v2.1-share-link-upload'), url(r'^api/v2.1/share-links/(?P[a-f0-9]+)/upload/done/$', ShareLinkUploadDone.as_view(), name='api-v2.1-share-link-upload-done'), + url(r'^api/v2.1/share-links/(?P[a-f0-9]+)/repo-tags/$', ShareLinkRepoTags.as_view(), name='api-v2.1-share-link-repo-tags'), + url(r'^api/v2.1/share-links/(?P[a-f0-9]+)/repo-tags/(?P\d+)/$', ShareLinkRepoTagsTaggedFiles.as_view(), name='api-v2.1-share-link-repo-tags-tagged-files'), + ## user::shared-upload-links url(r'^api/v2.1/upload-links/$', UploadLinks.as_view(), name='api-v2.1-upload-links'), url(r'^api/v2.1/upload-links/(?P[a-f0-9]+)/$', UploadLink.as_view(), name='api-v2.1-upload-link'),