diff --git a/frontend/src/components/dialog/share-to-group.js b/frontend/src/components/dialog/share-to-group.js index 2775ac25b8d..0478280f829 100644 --- a/frontend/src/components/dialog/share-to-group.js +++ b/frontend/src/components/dialog/share-to-group.js @@ -113,6 +113,7 @@ const propTypes = { itemPath: PropTypes.string.isRequired, itemType: PropTypes.string.isRequired, repoID: PropTypes.string.isRequired, + repoType: PropTypes.string.isRequired, isRepoOwner: PropTypes.bool.isRequired, onAddCustomPermissionToggle: PropTypes.func, }; @@ -126,7 +127,8 @@ class ShareToGroup extends React.Component { selectedOption: null, errorMsg: [], permission: 'rw', - sharedItems: [] + sharedItems: [], + isWiki: this.props.repoType === 'wiki' }; this.permissions = []; let { itemType, isRepoOwner } = props; @@ -141,6 +143,9 @@ class ShareToGroup extends React.Component { if (this.props.isGroupOwnedRepo) { this.permissions = ['rw', 'r', 'cloud-edit', 'preview']; } + if (this.state.isWiki) { + this.permissions = ['rw', 'r']; + } } handleSelectChange = (option) => { @@ -344,6 +349,7 @@ class ShareToGroup extends React.Component { permissions={this.permissions} onPermissionChanged={this.setPermission} enableAddCustomPermission={isPro} + isWiki={this.state.isWiki} onAddCustomPermissionToggle={this.props.onAddCustomPermissionToggle} /> diff --git a/frontend/src/components/dialog/share-to-user.js b/frontend/src/components/dialog/share-to-user.js index 6983b83013f..9d75c9ea7f3 100644 --- a/frontend/src/components/dialog/share-to-user.js +++ b/frontend/src/components/dialog/share-to-user.js @@ -153,6 +153,7 @@ const propTypes = { itemType: PropTypes.string.isRequired, repoID: PropTypes.string.isRequired, isRepoOwner: PropTypes.bool.isRequired, + repoType: PropTypes.string.isRequired, onAddCustomPermissionToggle: PropTypes.func, }; @@ -164,7 +165,8 @@ class ShareToUser extends React.Component { selectedOption: null, errorMsg: [], permission: 'rw', - sharedItems: [] + sharedItems: [], + isWiki: this.props.repoType === 'wiki' }; this.options = []; this.permissions = []; @@ -180,6 +182,9 @@ class ShareToUser extends React.Component { if (this.props.isGroupOwnedRepo) { this.permissions = ['rw', 'r', 'cloud-edit', 'preview']; } + if (this.state.isWiki) { + this.permissions = ['rw', 'r']; + } } handleSelectChange = (option) => { @@ -369,6 +374,7 @@ class ShareToUser extends React.Component { permissions={this.permissions} onPermissionChanged={this.setPermission} enableAddCustomPermission={isPro} + isWiki={this.state.isWiki} onAddCustomPermissionToggle={this.props.onAddCustomPermissionToggle} /> diff --git a/frontend/src/components/dialog/share-wiki-dialog.js b/frontend/src/components/dialog/share-wiki-dialog.js new file mode 100644 index 00000000000..c459457cb9e --- /dev/null +++ b/frontend/src/components/dialog/share-wiki-dialog.js @@ -0,0 +1,176 @@ +import React, { Fragment } from 'react'; +import PropTypes from 'prop-types'; +import { Modal, ModalHeader, ModalBody, TabContent, TabPane, Nav, NavItem, NavLink } from 'reactstrap'; +import { gettext, username, additionalShareDialogNote, canShareRepo } from '../../utils/constants'; +import ShareToUser from './share-to-user'; +import ShareToGroup from './share-to-group'; +import { seafileAPI } from '../../utils/seafile-api'; +import { Utils } from '../../utils/utils'; +import Loading from '../loading'; +import toaster from '../toast'; + +import '../../css/share-link-dialog.css'; + +const propTypes = { + isGroupOwnedRepo: PropTypes.bool, + itemType: PropTypes.string.isRequired, // there will be three choose: ['library', 'dir', 'file'] + itemName: PropTypes.string.isRequired, + itemPath: PropTypes.string.isRequired, + toggleDialog: PropTypes.func.isRequired, + repoID: PropTypes.string.isRequired, + repoEncrypted: PropTypes.bool, + enableDirPrivateShare: PropTypes.bool, +}; + +class ShareWikiDialog extends React.Component { + constructor(props) { + super(props); + this.state = { + activeTab: 'shareToUser', + isRepoJudgemented: false, + isRepoOwner: false, + isGroupOwnedRepo: false, + }; + } + + componentDidMount() { + let repoID = this.props.repoID; + seafileAPI.getRepoInfo(repoID).then(res => { + const isGroupOwnedRepo = res.data.owner_email.indexOf('@seafile_group') > -1; + let isRepoOwner = res.data.owner_email === username; + this.setState({ + isRepoJudgemented: true, + isRepoOwner: isRepoOwner, + repoType: res.data.repo_type, + isGroupOwnedRepo: isGroupOwnedRepo, + }); + }).catch(error => { + let errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }); + } + + toggle = (tab) => { + if (this.state.activeTab !== tab) { + this.setState({ activeTab: tab }); + } + }; + + onAddCustomPermissionToggle = () => { + this.toggle('customSharePermission'); + }; + + renderDirContent = () => { + + if (!this.state.isRepoJudgemented) { + return ; + } + + let activeTab = this.state.activeTab; + let { repoEncrypted, enableDirPrivateShare, itemType } = this.props; + // for encrypted repo, 'dir private share' is only enabled for the repo itself, + // not for the folders in it. + if (repoEncrypted) { + enableDirPrivateShare = itemType == 'library'; + } + return ( + +
+ +
+
+ + {enableDirPrivateShare && + + {(activeTab === 'shareToUser' && canShareRepo) && + + + + } + {(activeTab === 'shareToGroup' && canShareRepo) && + + + + } + + } + +
+
+ ); + }; + + onTabKeyDown = (e) => { + if (e.key == 'Enter' || e.key == 'Space') { + e.target.click(); + } + }; + + renderExternalShareMessage = () => { + if (additionalShareDialogNote && (typeof additionalShareDialogNote) === 'object') { + return ( +
+
{additionalShareDialogNote.title}
+

{additionalShareDialogNote.content}

+
+ ); + } + return null; + }; + + render() { + const { itemType, itemName } = this.props; + return ( +
+ + +
{gettext('Share')} {itemName}
+ {this.renderExternalShareMessage()} +
+ + {(itemType === 'library') && this.renderDirContent()} + +
+
+ ); + } +} + +ShareWikiDialog.propTypes = propTypes; + +export default ShareWikiDialog; diff --git a/frontend/src/components/select-editor/select-editor.js b/frontend/src/components/select-editor/select-editor.js index abcae3eff02..70c99e149f2 100644 --- a/frontend/src/components/select-editor/select-editor.js +++ b/frontend/src/components/select-editor/select-editor.js @@ -9,6 +9,7 @@ const propTypes = { isTextMode: PropTypes.bool.isRequired, // there will be two mode. first: text and select. second: just select isEditing: PropTypes.bool, isEditIconShow: PropTypes.bool.isRequired, + isWiki: PropTypes.bool, autoFocus: PropTypes.bool, options: PropTypes.array.isRequired, currentOption: PropTypes.string.isRequired, @@ -56,7 +57,7 @@ class SelectEditor extends React.Component { } const { enableAddCustomPermission } = this.props; - if (enableAddCustomPermission) { + if (enableAddCustomPermission && !this.props.isWiki) { const option = { value: gettext('Add custom permission'), isDisabled: true, diff --git a/frontend/src/components/select-editor/share-permission-editor.js b/frontend/src/components/select-editor/share-permission-editor.js index 1da3eb4a60a..324d938f02b 100644 --- a/frontend/src/components/select-editor/share-permission-editor.js +++ b/frontend/src/components/select-editor/share-permission-editor.js @@ -11,6 +11,7 @@ const propTypes = { repoID: PropTypes.string, isTextMode: PropTypes.bool.isRequired, isEditing: PropTypes.bool, + isWiki: PropTypes.bool, autoFocus: PropTypes.bool, isEditIconShow: PropTypes.bool.isRequired, permissions: PropTypes.array.isRequired, @@ -136,6 +137,7 @@ class SharePermissionEditor extends React.Component { translateOption={this.translatePermission} translateExplanation={this.translateExplanation} enableAddCustomPermission={this.props.enableAddCustomPermission} + isWiki={this.props.isWiki} onAddCustomPermissionToggle={this.props.onAddCustomPermissionToggle} /> ); diff --git a/frontend/src/components/wiki-card-view/wiki-card-group.js b/frontend/src/components/wiki-card-view/wiki-card-group.js index ec414d2ea14..df60a8146f8 100644 --- a/frontend/src/components/wiki-card-view/wiki-card-group.js +++ b/frontend/src/components/wiki-card-view/wiki-card-group.js @@ -7,7 +7,9 @@ import { SIDE_PANEL_FOLDED_WIDTH } from '../../constants'; const propTypes = { wikis: PropTypes.array.isRequired, + group: PropTypes.object, deleteWiki: PropTypes.func.isRequired, + unshareGroupWiki: PropTypes.func.isRequired, title: PropTypes.string.isRequired, isDepartment: PropTypes.bool.isRequired, isShowAvatar: PropTypes.bool.isRequired, @@ -48,10 +50,14 @@ class WikiCardGroup extends Component { }; render() { - const { wikis, title, isDepartment, toggelAddWikiDialog } = this.props; + const { wikis, title, isDepartment, toggelAddWikiDialog, group } = this.props; const containerWidth = this.getContainerWidth(); const numberOfWiki = Math.floor(containerWidth / 180); const grids = (Math.floor((containerWidth - (numberOfWiki + 1) * 16) / numberOfWiki) + 'px ').repeat(numberOfWiki); + let isGroup = false; + if (group){ + isGroup = true; + } return (

@@ -60,11 +66,21 @@ class WikiCardGroup extends Component {

{wikis.map((wiki, index) => { - return ( + return (isGroup ? : { + this.setState({ + isShowShareDialog: !this.state.isShowShareDialog, + }); + }; + onDeleteToggle = (e) => { e.preventDefault(); this.setState({ @@ -52,6 +62,14 @@ class WikiCardItem extends Component { }); }; + onItemUnshare = () => { + let wiki = this.props.wiki; + this.props.unshareGroupWiki(wiki, this.props.group.group_id); + this.setState({ + isShowDeleteDialog: !this.state.isShowDeleteDialog, + }); + }; + renameWiki = (newName) => { if (this.props.wiki.name !== newName) { this.props.renameWiki(this.props.wiki, newName); @@ -94,7 +112,17 @@ class WikiCardItem extends Component { render() { const { wiki, isDepartment, isShowAvatar } = this.props; + let isAdmin = false; + if (wiki.admins){ + isAdmin = wiki.admins.includes(username); + } + let isGroupOwner = false; + if (this.props.group){ + isGroupOwner = wiki.owner.split('@')[0] === this.props.group.group_id.toString(); + } + let isWikiOwner = username === wiki.owner; let isOldVersion = wiki.version !== 'v2'; + let enableShare = username === wiki.owner || isAdmin; let publishedUrl = `${siteRoot}published/${encodeURIComponent(wiki.slug)}/`; let editUrl = `${siteRoot}wikis/${wiki.id}/`; let wikiName = isOldVersion ? `${wiki.name} (old version)` : wiki.name; @@ -120,11 +148,23 @@ class WikiCardItem extends Component { style={{ 'minWidth': '0' }} /> - {gettext('Rename')} + {(isWikiOwner || isAdmin) && + {gettext('Rename')} + } + {enableShare && + {gettext('Share')} + } {isOldVersion ? {gettext('Unpublish')} - : - {gettext('Delete')} + : ((isDepartment && isGroupOwner) ? + {gettext('Delete')} : + (isDepartment ? + {gettext('Leave')} : + (isWikiOwner ? + {gettext('Delete')} : + {gettext('Leave')} + )) + ) } @@ -145,14 +185,34 @@ class WikiCardItem extends Component { content={

{gettext('Are you sure you want to unpublish Wiki')}{' '}{wiki.name} ?

} footer={gettext('Unpublish')} /> - : - {gettext('Are you sure you want to delete Wiki')}{' '}{wiki.name} ?

} - footer={gettext('Delete')} - /> + : ((isDepartment && isGroupOwner) ? + {gettext('Are you sure you want to delete Wiki')}{' '}{wiki.name} ?

} + footer={gettext('Delete')} + /> : isDepartment ? {gettext('Are you sure you want to leave share Wiki')}{' '}{wiki.name} ?

} + footer={gettext('Leave')} + /> : (isWikiOwner ? {gettext('Are you sure you want to delete Wiki')}{' '}{wiki.name} ?

} + footer={gettext('Delete')} + /> : {gettext('Are you sure you want to leave share Wiki')}{' '}{wiki.name} ?

} + footer={gettext('Leave')} + /> + ) + ) } } @@ -165,6 +225,19 @@ class WikiCardItem extends Component { /> } + {this.state.isShowShareDialog && + + + + } ); } diff --git a/frontend/src/components/wiki-card-view/wiki-card-view.js b/frontend/src/components/wiki-card-view/wiki-card-view.js index e0a86523916..44622b545ed 100644 --- a/frontend/src/components/wiki-card-view/wiki-card-view.js +++ b/frontend/src/components/wiki-card-view/wiki-card-view.js @@ -12,6 +12,8 @@ const propTypes = { data: PropTypes.object.isRequired, deleteWiki: PropTypes.func.isRequired, renameWiki: PropTypes.func.isRequired, + leaveSharedWiki: PropTypes.func.isRequired, + unshareGroupWiki: PropTypes.func.isRequired, toggelAddWikiDialog: PropTypes.func, sidePanelRate: PropTypes.number, isSidePanelFolded: PropTypes.bool, @@ -30,7 +32,7 @@ class WikiCardView extends Component { if (!canPublishRepo || !isPro) return; let departmentMap = {}; wikiAPI.listWikiDepartments().then(res => { - res.data.forEach(item => departmentMap[item.email] = true); + res.data.forEach(item => departmentMap[item.id] = true); this.setState({ departmentMap }); }).catch(error => { let errMessage = Utils.getErrorMsg(error); @@ -41,12 +43,15 @@ class WikiCardView extends Component { classifyWikis = (wikis) => { let v1Wikis = []; let myWikis = []; + let sharedWikis = []; let department2WikisMap = {}; for (let i = 0; i < wikis.length; i++) { if (wikis[i].version === 'v1') { v1Wikis.push(wikis[i]); } else if (wikis[i].owner === username) { myWikis.push(wikis[i]); + } else if (wikis[i].type === 'shared') { + sharedWikis.push(wikis[i]); } else { if (!department2WikisMap[wikis[i].owner]) { department2WikisMap[wikis[i].owner] = []; @@ -54,11 +59,12 @@ class WikiCardView extends Component { department2WikisMap[wikis[i].owner].push(wikis[i]); } } - return { department2WikisMap, myWikis, v1Wikis }; + return { department2WikisMap, myWikis, v1Wikis, sharedWikis }; }; + render() { - let { loading, errorMsg, wikis } = this.props.data; + let { loading, errorMsg, wikis, groupWikis } = this.props.data; const { toggelAddWikiDialog, sidePanelRate, isSidePanelFolded } = this.props; if (loading) { @@ -67,13 +73,14 @@ class WikiCardView extends Component { if (errorMsg) { return

{errorMsg}

; } - const { v1Wikis, myWikis, department2WikisMap } = this.classifyWikis(wikis); + const { v1Wikis, myWikis, sharedWikis } = this.classifyWikis(wikis); let wikiCardGroups = []; wikiCardGroups.push( ); - for (let deptID in department2WikisMap) { + wikiCardGroups.push( + + ); + for (let deptID in groupWikis) { + groupWikis[deptID].wiki_info.length !== 0 && wikiCardGroups.push( ); } @@ -104,6 +127,7 @@ class WikiCardView extends Component { key='old-Wikis' deleteWiki={this.props.deleteWiki} renameWiki={this.props.renameWiki} + unshareGroupWiki={this.props.unshareGroupWiki} isSidePanelFolded={isSidePanelFolded} sidePanelRate={sidePanelRate} wikis={v1Wikis} diff --git a/frontend/src/pages/wikis/wikis.js b/frontend/src/pages/wikis/wikis.js index 3617f1a8822..5913695ddb8 100644 --- a/frontend/src/pages/wikis/wikis.js +++ b/frontend/src/pages/wikis/wikis.js @@ -9,6 +9,8 @@ import EmptyTip from '../../components/empty-tip'; import AddWikiDialog from '../../components/dialog/add-wiki-dialog'; import wikiAPI from '../../utils/wiki-api'; import WikiCardView from '../../components/wiki-card-view/wiki-card-view'; +import { seafileAPI } from '../../utils/seafile-api'; + const propTypes = { sidePanelRate: PropTypes.number, @@ -23,6 +25,7 @@ class Wikis extends Component { errorMsg: '', currentDeptID: '', wikis: [], + groupWikis: [], isShowAddWikiMenu: false, isShowAddDialog: false, isDropdownMenuShown: false, @@ -35,6 +38,7 @@ class Wikis extends Component { getWikis = () => { let wikis = []; + let groupWikis = []; wikiAPI.listWikis().then(res => { wikis = wikis.concat(res.data.data); wikis.map(wiki => { @@ -42,12 +46,20 @@ class Wikis extends Component { }); wikiAPI.listWikis2().then(res => { let wikis2 = res.data.wikis; + groupWikis = res.data.group_wikis; + groupWikis.forEach(group => { + group.wiki_info.forEach(wiki => { + wiki.version = 'v2'; + wiki.admins = group.group_admins; + }); + }); wikis2.map(wiki => { return wiki['version'] = 'v2'; }); this.setState({ loading: false, - wikis: wikis.concat(wikis2) + wikis: wikis.concat(wikis2), + groupWikis: groupWikis }); }).catch((error) => { this.setState({ @@ -89,11 +101,22 @@ class Wikis extends Component { addWiki = (wikiName, currentDeptID) => { wikiAPI.addWiki2(wikiName, currentDeptID).then((res) => { let wikis = this.state.wikis.slice(0); + let groupWikis = this.state.groupWikis; let new_wiki = res.data; new_wiki['version'] = 'v2'; - wikis.push(new_wiki); + if (currentDeptID){ + groupWikis.filter(group => { + if (group.group_id === currentDeptID){ + group.wiki_info.push(new_wiki); + } + return group; + }); + } else { + wikis.push(new_wiki); + } this.setState({ wikis, + groupWikis, currentDeptID: '', }); }).catch((error) => { @@ -110,7 +133,14 @@ class Wikis extends Component { let wikis = this.state.wikis.filter(item => { return item.name !== wiki.name; }); - this.setState({ wikis: wikis }); + let groupWikis = this.state.groupWikis.filter(group => { + group.wiki_info = group.wiki_info.filter(item => item.name !== wiki.name); + return group; + }); + this.setState({ + wikis: wikis, + groupWikis: groupWikis, + }); }).catch((error) => { if (error.response) { let errorMsg = error.response.data.error_msg; @@ -122,7 +152,14 @@ class Wikis extends Component { let wikis = this.state.wikis.filter(item => { return item.name !== wiki.name; }); - this.setState({ wikis: wikis }); + let groupWikis = this.state.groupWikis.filter(group => { + group.wiki_info = group.wiki_info.filter(item => item.name !== wiki.name); + return group; + }); + this.setState({ + wikis: wikis, + groupWikis: groupWikis, + }); }).catch((error) => { if (error.response) { let errorMsg = error.response.data.error_msg; @@ -132,6 +169,57 @@ class Wikis extends Component { } }; + leaveSharedWiki = (wiki) => { + if (!wiki.owner.includes('@seafile_group')) { + let options = { + 'share_type': 'personal', + 'from': wiki.owner + }; + seafileAPI.leaveShareRepo(wiki.repo_id, options).then(res => { + let wikis = this.state.wikis.filter(item => { + return item.name !== wiki.name; + }); + this.setState({ + wikis: wikis, + }); + }).catch((error) => { + let errorMsg = Utils.getErrorMsg(error, true); + toaster.danger(errorMsg); + }); + } else { + seafileAPI.leaveShareGroupOwnedRepo(wiki.repo_id).then(res => { + let wikis = this.state.wikis.filter(item => { + return item.name !== wiki.name; + }); + this.setState({ + wikis: wikis, + }); + }).catch((error) => { + let errorMsg = Utils.getErrorMsg(error, true); + toaster.danger(errorMsg); + }); + } + + }; + + unshareGroupWiki = (wiki, groupId) => { + seafileAPI.unshareRepoToGroup(wiki.repo_id, groupId).then(() => { + let groupWikis = this.state.groupWikis.map(group => { + if (group.group_id === groupId) { + return { + ...group, + wiki_info: group.wiki_info.filter(item => item.name !== wiki.name) + }; + } + return group; + }); + this.setState({ groupWikis: groupWikis }); + }).catch(error => { + let errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }); + }; + renameWiki = (wiki, newName) => { if (wiki.version === 'v1') { wikiAPI.renameWiki(wiki.id, newName).then(() => { @@ -213,6 +301,8 @@ class Wikis extends Component { = 3 else '', + } + wiki_info.update(repo_info) + if w.id in filter_repo_type_ids_map['shared']: + repo_info = { + 'type': 'shared', + "last_modified": timestamp_to_isoformat_timestr(w.last_modify), + "size": w.size, + "encrypted": w.encrypted, + "permission": w.permission, + "status": normalize_repo_status_code(w.status), + "salt": w.salt if w.enc_version >= 3 else '', + } + wiki_info.update(repo_info) + if is_group_wiki(wiki): group_id = int(wiki.owner.split('@')[0]) wiki_info['owner_nickname'] = group_id_to_name(group_id) else: wiki_info['owner_nickname'] = email2nickname(wiki.owner) wiki_list.append(wiki_info) - + wiki_list = sorted(wiki_list, key=lambda x: x.get('updated_at'), reverse=True) - return Response({'wikis': wiki_list}) + return Response({'wikis': wiki_list, 'group_wikis': group_wiki_list}) def post(self, request, format=None): """Add a new wiki.