diff --git a/actions/deckedit/deckDeletion.js b/actions/deckedit/deckDeletion.js new file mode 100644 index 000000000..ba55173f4 --- /dev/null +++ b/actions/deckedit/deckDeletion.js @@ -0,0 +1,25 @@ +import log from '../log/clog'; + +import hideTransferOwnershipModal from './hideTransferOwnershipModal'; + +import UserProfileStore from '../../stores/UserProfileStore'; + +export default function deckDeletion(context, payload, done) { + log.info(context); + + context.executeAction(hideTransferOwnershipModal, {} , () => { + context.dispatch('START_DELETE_DECK'); + + payload.jwt = context.getStore(UserProfileStore).jwt; + + context.service.delete('deck.delete', payload, { timeout: 20 * 1000 }, (err, res) => { + if (err) { + context.dispatch('DELETE_DECK_ERROR', err); + } + else { + context.dispatch('DELETE_DECK_SUCCESS', res); + } + done(); + }); + }); +} diff --git a/actions/deckedit/hideTransferOwnershipModal.js b/actions/deckedit/hideTransferOwnershipModal.js new file mode 100644 index 000000000..e8d6df6c1 --- /dev/null +++ b/actions/deckedit/hideTransferOwnershipModal.js @@ -0,0 +1,7 @@ +const log = require('../log/clog'); + +export default function hideTransferOwnershipModal(context, payload, done) { + log.info(context); + context.dispatch('HIDE_TRANSFER_OWNERSHIP_MODAL', {}); + done(); +} diff --git a/actions/deckedit/loadEditors.js b/actions/deckedit/loadEditors.js new file mode 100644 index 000000000..ef2e06ee9 --- /dev/null +++ b/actions/deckedit/loadEditors.js @@ -0,0 +1,41 @@ +import log from '../log/clog'; +import UserProfileStore from '../../stores/UserProfileStore'; + +export default function loadEditors(context, payload, done) { + log.info(context); + context.dispatch('START_TRANSFER_OWNERSHIP'); + + let userid = context.getStore(UserProfileStore).userid; + const groupids = payload.groups.map((group) => parseInt(group.id, 10)); + + if (groupids.length < 1) { + context.dispatch('LOAD_EDITORS_LIST_SUCCESS', payload.users); + return done(); + } + + context.service.read('usergroup.getList', {groupids}, { timeout: 20 * 1000 }, (err, res) => { // payload needs groupids + if (err) { + context.dispatch('LOAD_EDITORS_LIST_ERROR', err); + } + else { + let users = payload.users; + users = users.concat(res.reduce((ret, group) => { + ret.push(group.creator); + return ret.concat(group.members); + }, [])); + + // purge duplicates and current user + users = users.reduce((ret, user) => { + let found = ret.find((u) => ((u.id || u.userid) === (user.id || user.userid))); + if (!found && (user.id || user.userid) !== userid) { + ret.push(user); + } + return ret; + }, []); + + // console.log('got all editors', users); + context.dispatch('LOAD_EDITORS_LIST_SUCCESS', users); + } + done(); + }); +} diff --git a/actions/deckedit/transferOwnership.js b/actions/deckedit/transferOwnership.js new file mode 100644 index 000000000..3a718cbcd --- /dev/null +++ b/actions/deckedit/transferOwnership.js @@ -0,0 +1,20 @@ +import log from '../log/clog'; + +import UserProfileStore from '../../stores/UserProfileStore'; + +export default function transferOwnership(context, payload, done) { + log.info(context); + context.dispatch('TRY_TRANSFER_OWNERSHIP'); + + payload.jwt = context.getStore(UserProfileStore).jwt; + + context.service.update('deck.transferOwnership', payload, { timeout: 20 * 1000 }, (err, res) => { + if (err) { + context.dispatch('TRANSFER_OWNERSHIP_ERROR', err); + } + else { + context.dispatch('TRANSFER_OWNERSHIP_SUCCESS', res); + } + done(); + }); +} diff --git a/actions/decktree/deleteTreeNode.js b/actions/decktree/deleteTreeNode.js index 1e394f155..b9e42f39c 100644 --- a/actions/decktree/deleteTreeNode.js +++ b/actions/decktree/deleteTreeNode.js @@ -8,6 +8,7 @@ import serviceUnavailable from '../error/serviceUnavailable'; export default function deleteTreeNode(context, payload, done) { log.info(context); let userid = context.getStore(UserProfileStore).userid; + if (userid != null && userid !== '') { //enrich with jwt payload.jwt = context.getStore(UserProfileStore).jwt; @@ -51,7 +52,7 @@ export default function deleteTreeNode(context, payload, done) { if (!isEmpty(contentRootId)) { activity.content_root_id = contentRootId; } - + context.executeAction(addActivity, {activity: activity}); } done(null, res); diff --git a/actions/decktree/deleteTreeNodeAndNavigate.js b/actions/decktree/deleteTreeNodeAndNavigate.js index 855cae747..7175c3ae4 100644 --- a/actions/decktree/deleteTreeNodeAndNavigate.js +++ b/actions/decktree/deleteTreeNodeAndNavigate.js @@ -1,7 +1,7 @@ import async from 'async'; import DeckTreeStore from '../../stores/DeckTreeStore'; import deleteTreeNode from './deleteTreeNode'; -const log = require('../log/clog'); +import log from '../log/clog'; import {navigateAction} from 'fluxible-router'; import Util from '../../components/common/Util'; import serviceUnavailable from '../error/serviceUnavailable'; @@ -9,45 +9,57 @@ import serviceUnavailable from '../error/serviceUnavailable'; export default function deleteTreeNodeAndNavigate(context, payload, done) { log.info(context); + let callback = (accepted) => { + //load all required actions in parallel + async.parallel([ + (callback) => { + context.executeAction(deleteTreeNode, payload, callback); + } + ], + // final callback + (err, results) => { + if (!err) { + //the logic for retrieving the parent node is handles in the stores + //therefore, we need to get access to the selector from the store + let currentState = context.getStore(DeckTreeStore).getState(); + let selector = { + id: currentState.selector.get('id'), + stype: currentState.selector.get('stype'), + sid: currentState.selector.get('sid'), + spath: currentState.selector.get('spath') + }; + context.executeAction(navigateAction, { + url: Util.makeNodeURL(selector, 'deck', 'view', undefined, undefined, true) + }); + } else { + log.error(context, { + filepath: __filename + }); + //context.executeAction(serviceUnavailable, payload, done); + } + done(); + }); + }; + + // skip swal as a previous modal already got approval + console.log(payload); + if (payload.confirmed) { + return callback(true); + } - let elementTitle = payload.stype; - if(elementTitle === 'deck') - elementTitle = 'sub' + elementTitle; + let elementTitle = payload.stype, html; + if (elementTitle === 'deck') { + elementTitle = 'subdeck'; + html = 'This subdeck will become a proper deck after removing it. It will be available in its creator\'s "My Decks" page.'; + } swal({ - title: 'Delete '+elementTitle+'. Are you sure?', + title: 'Remove ' + elementTitle + '. Are you sure?', + html, type: 'warning', showCancelButton: true, confirmButtonColor: '#3085d6', cancelButtonColor: '#d33', - confirmButtonText: 'Yes, delete it!' + confirmButtonText: 'Yes, remove it!' - }).then((accepted) => { - //load all required actions in parallel - async.parallel([ - (callback) => { - context.executeAction(deleteTreeNode, payload, callback); - } - ], - // final callback - (err, results) => { - if (!err) { - //the logic for retrieving the parent node is handles in the stores - //therefore, we need to get access to the selector from the store - let currentState = context.getStore(DeckTreeStore).getState(); - let selector = { - id: currentState.selector.get('id'), - stype: currentState.selector.get('stype'), - sid: currentState.selector.get('sid'), - spath: currentState.selector.get('spath') - }; - context.executeAction(navigateAction, { - url: Util.makeNodeURL(selector, 'deck', 'view', undefined, undefined, true) - }); - } else { - log.error(context, {filepath: __filename}); - //context.executeAction(serviceUnavailable, payload, done); - } - done(); - }); - }, (reason) => {/*do nothing*/}).catch(swal.noop); + }).then(callback, (reason) => { /*do nothing*/ }).catch(swal.noop); } diff --git a/components/Deck/ContentPanel/ContentActions/ContentActionsHeader.js b/components/Deck/ContentPanel/ContentActions/ContentActionsHeader.js index 65d4592d6..9726525f2 100644 --- a/components/Deck/ContentPanel/ContentActions/ContentActionsHeader.js +++ b/components/Deck/ContentPanel/ContentActions/ContentActionsHeader.js @@ -25,6 +25,7 @@ import DeckTranslationsModal from '../Translation/DeckTranslationsModal'; import SlideTranslationsModal from '../Translation/SlideTranslationsModal'; import addDeckTranslation from '../../../../actions/translation/addDeckTranslation'; import addSlideTranslation from '../../../../actions/translation/addSlideTranslation'; +import DeckViewStore from '../../../../stores/DeckViewStore'; class ContentActionsHeader extends React.Component { constructor(props){ @@ -66,6 +67,10 @@ class ContentActionsHeader extends React.Component { id: 'ContentActionsHeader.deleteAriaText', defaultMessage:'Delete slide' }, + deleteDeckAriaText:{ + id: 'ContentActionsHeader.deleteDeckAriaText', + defaultMessage:'Delete deck' + }, language:{ id: 'ContentActionsHeader.language', defaultMessage:'Language' @@ -123,6 +128,58 @@ class ContentActionsHeader extends React.Component { this.context.executeAction(deleteTreeNodeAndNavigate, selector); } + // TODO Remove this unused code once it is decided we don't ever need to provide this option + handleDeleteNodeWithCheck(selector) { + // plain remove for slides + if (selector.stype !== 'deck') { + return this.context.executeAction(deleteTreeNodeAndNavigate, selector); + } + + // plain remove for decks with subdecks + if (this.props.DeckViewStore.deckData.contentItems.find((i) => i.kind === 'deck')) { + return this.context.executeAction(deleteTreeNodeAndNavigate, selector); + } + + // plain remove for shared subdecks + if (this.props.DeckViewStore.deckData.usage.length > 0) { + // TODO make this test more strict: check for actual ids in usage matching current deck parent + const otherParents = this.props.DeckViewStore.deckData.usage; + if (otherParents.length > 1) { + return this.context.executeAction(deleteTreeNodeAndNavigate, selector); + } + } + + swal({ + title: 'Remove subdeck', + html: 'You have the option to simply remove this subdeck and keep it in "My Decks" or delete this subdeck completely.', + type: 'question', + showCancelButton: true, + confirmButtonText: 'Remove', + confirmButtonClass: 'ui button', + cancelButtonText: 'Delete', + cancelButtonClass: 'negative ui button', + allowEscapeKey: true, + allowOutsideClick: true, + buttonsStyling: false + }) + .then((result) => { + // confirm btn + // remove deck as node from the parent deck + selector.confirmed = true; + selector.purge = false; + this.context.executeAction(deleteTreeNodeAndNavigate, selector); + }, (action) => { + if (action === 'cancel') { + selector.confirmed = true; + selector.purge = true; + this.context.executeAction(deleteTreeNodeAndNavigate, selector); + } + }) + .catch((e) => { + console.log(e); + }); + } + handleSaveButtonClick(){ this.context.executeAction(saveClick, {}); } @@ -431,7 +488,7 @@ class ContentActionsHeader extends React.Component { + : ''; + let buttons = (
+ {deleteButton} @@ -567,6 +732,7 @@ class DeckPropertiesEditor extends React.Component {
+
) : ''} @@ -599,6 +765,7 @@ class DeckPropertiesEditor extends React.Component { DeckPropertiesEditor.contextTypes = { executeAction: PropTypes.func.isRequired, + getUser: PropTypes.func, intl: PropTypes.object.isRequired }; diff --git a/components/Deck/ContentPanel/DeckModes/DeckEditPanel/TransferOwnership.js b/components/Deck/ContentPanel/DeckModes/DeckEditPanel/TransferOwnership.js new file mode 100644 index 000000000..66d671dbf --- /dev/null +++ b/components/Deck/ContentPanel/DeckModes/DeckEditPanel/TransferOwnership.js @@ -0,0 +1,171 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import FocusTrap from 'focus-trap-react'; +import {Button, Icon, Modal, Header, TextArea, Segment, Container, Menu} from 'semantic-ui-react'; +import { FormattedMessage, defineMessages } from 'react-intl'; +import UserPicture from '../../../../common/UserPicture'; +import hideTransferOwnershipModal from '../../../../../actions/deckedit/hideTransferOwnershipModal'; +import transferOwnership from '../../../../../actions/deckedit/transferOwnership'; +import deckDeletion from '../../../../../actions/deckedit/deckDeletion'; + +class TransferOwnership extends React.Component { + constructor(props) { + super(props); + this.handleClose = this.handleClose.bind(this); + this.unmountTrap = this.unmountTrap.bind(this); + + this.messages = defineMessages({ + modalHeading: { + id: 'TransferOwnership.modalHeading', + defaultMessage: 'Transfer Ownership' + }, + close: { + id: 'TransferOwnership.cancel', + defaultMessage: 'Cancel' + }, + unknownCountry: { + id: 'TransferOwnership.unknownCountry', + defaultMessage: 'unknown country' + }, + unknownOrganization: { + id: 'TransferOwnership.unknownOrganization', + defaultMessage: 'Unknown organization' + }, + linkHint: { + id: 'TransferOwnership.linkHint', + defaultMessage: 'The username is a link which will open a new browser tab. Close it when you want to go back to this page.' + }, + continue: { + id: 'TransferOwnership.continue', + defaultMessage: 'Transfer deck' + }, + delete: { + id: 'TransferOwnership.delete', + defaultMessage: 'Delete deck' + }, + }); + + this.state = { + selectedUser: '' + }; + } + + handleClose() { + $('#app').attr('aria-hidden','false'); + this.context.executeAction(hideTransferOwnershipModal, {}); + } + + unmountTrap() { + // TODO commented this out as it appeared to cause trouble with flux and cascading updates, + // and it was impossible to get the transfer success dialog up + // this.handleClose(); + $('#app').attr('aria-hidden','false'); + } + + handleContinue() { + $('#app').attr('aria-hidden','false'); + this.context.executeAction(transferOwnership, {deckid: this.props.deckid, userid: this.state.selectedUser}); + } + + handleDelete() { + $('#app').attr('aria-hidden','false'); + this.context.executeAction(deckDeletion, { id: this.props.deckid }); + } + + render() { + let editors = []; + + if (this.props.users !== undefined && this.props.users.length > 0) { + this.props.users.forEach((user) => { + const optionalText = (user.organization || user.country) ? + (user.organization || this.context.intl.formatMessage(this.messages.unknownOrganization)) + ', ' + (user.country || this.context.intl.formatMessage(this.messages.unknownCountry)) : + ''; + editors.push( + ( + this.setState({selectedUser: user.id || user.userid})} + onKeyPress={(e) => e.key === 'Enter' && this.setState({selectedUser: user.id || user.userid})} > +
+
+ +
+
+