diff --git a/src/applications/phriction/controller/PhrictionMoveController.php b/src/applications/phriction/controller/PhrictionMoveController.php index c278c4133c..00f8549df3 100644 --- a/src/applications/phriction/controller/PhrictionMoveController.php +++ b/src/applications/phriction/controller/PhrictionMoveController.php @@ -1,161 +1,159 @@ id = idx($data, 'id'); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); if ($this->id) { $document = id(new PhrictionDocumentQuery()) ->setViewer($user) ->withIDs(array($this->id)) + ->needContent(true) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); } else { $slug = PhabricatorSlug::normalize( $request->getStr('slug')); if (!$slug) { return new Aphront404Response(); } $document = id(new PhrictionDocumentQuery()) ->setViewer($user) ->withSlugs(array($slug)) + ->needContent(true) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); } if (!$document) { return new Aphront404Response(); } if (!isset($slug)) { $slug = $document->getSlug(); } $target_slug = PhabricatorSlug::normalize( $request->getStr('new-slug', $slug)); $submit_uri = $request->getRequestURI()->getPath(); $cancel_uri = PhrictionDocument::getSlugURI($slug); $errors = array(); $error_view = null; $e_url = null; $disallowed_statuses = array( PhrictionDocumentStatus::STATUS_DELETED => true, // Silly PhrictionDocumentStatus::STATUS_MOVED => true, // Plain silly PhrictionDocumentStatus::STATUS_STUB => true, // Utterly silly ); if (isset($disallowed_statuses[$document->getStatus()])) { $error_dialog = id(new AphrontDialogView()) ->setUser($user) ->setTitle('Can not move page!') ->appendChild(pht('An already moved or deleted document '. 'can not be moved again.')) ->addCancelButton($cancel_uri); return id(new AphrontDialogResponse())->setDialog($error_dialog); } - $content = id(new PhrictionContent())->load($document->getContentID()); + $content = $document->getContent(); if ($request->isFormPost() && !count($errors)) { - if (!count($errors)) { // First check if the target document exists - // NOTE: We use the ominpotent user because we can't let users overwrite - // documents even if they can't see them. - $target_document = id(new PhrictionDocumentQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->withSlugs(array($target_slug)) - ->executeOne(); - - // Considering to overwrite existing docs? Nuke this! - if ($target_document && $target_document->getStatus() == - PhrictionDocumentStatus::STATUS_EXISTS) { + // NOTE: We use the ominpotent user because we can't let users overwrite + // documents even if they can't see them. + $target_document = id(new PhrictionDocumentQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withSlugs(array($target_slug)) + ->needContent(true) + ->executeOne(); - $errors[] = pht('Can not overwrite existing target document.'); - $e_url = pht('Already exists.'); - } + // Considering to overwrite existing docs? Nuke this! + $exists = PhrictionDocumentStatus::STATUS_EXISTS; + if ($target_document && $target_document->getStatus() == $exists) { + $errors[] = pht('Can not overwrite existing target document.'); + $e_url = pht('Already exists.'); } - if (!count($errors)) { // I like to move it, move it! - $from_editor = id(PhrictionDocumentEditor::newForSlug($slug)) - ->setActor($user) - ->setTitle($content->getTitle()) - ->setContent($content->getContent()) - ->setDescription($content->getDescription()); + if (!count($errors)) { - $target_editor = id(PhrictionDocumentEditor::newForSlug( - $target_slug)) + $editor = id(new PhrictionTransactionEditor()) ->setActor($user) - ->setTitle($content->getTitle()) - ->setContent($content->getContent()) - ->setDescription($content->getDescription()); - - // Move it! - $target_editor->moveHere($document->getID(), $document->getPHID()); - - // Retrieve the target doc directly from the editor - // No need to load it per Sql again - $target_document = $target_editor->getDocument(); - $from_editor->moveAway($target_document->getID()); + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true) + ->setDescription($request->getStr('description')); + + $xactions = array(); + $xactions[] = id(new PhrictionTransaction()) + ->setTransactionType(PhrictionTransaction::TYPE_MOVE_TO) + ->setNewValue($document); + if (!$target_document) { + $target_document = PhrictionDocument::initializeNewDocument( + $user, + $target_slug); + } + $editor->applyTransactions($target_document, $xactions); - $redir_uri = PhrictionDocument::getSlugURI($target_document->getSlug()); + $redir_uri = PhrictionDocument::getSlugURI( + $target_document->getSlug()); return id(new AphrontRedirectResponse())->setURI($redir_uri); } } if ($errors) { $error_view = id(new AphrontErrorView()) ->setErrors($errors); } $form = id(new PHUIFormLayoutView()) ->setUser($user) ->appendChild( id(new AphrontFormStaticControl()) - ->setLabel(pht('Title')) - ->setValue($content->getTitle())) + ->setLabel(pht('Title')) + ->setValue($content->getTitle())) ->appendChild( id(new AphrontFormTextControl()) - ->setLabel(pht('New URI')) - ->setValue($target_slug) - ->setError($e_url) - ->setName('new-slug') - ->setCaption(pht('The new location of the document.'))) + ->setLabel(pht('New URI')) + ->setValue($target_slug) + ->setError($e_url) + ->setName('new-slug') + ->setCaption(pht('The new location of the document.'))) ->appendChild( id(new AphrontFormTextControl()) - ->setLabel(pht('Edit Notes')) - ->setValue($content->getDescription()) - ->setError(null) - ->setName('description')); + ->setLabel(pht('Edit Notes')) + ->setValue($content->getDescription()) + ->setError(null) + ->setName('description')); $dialog = id(new AphrontDialogView()) ->setUser($user) ->setTitle(pht('Move Document')) ->appendChild($form) ->setSubmitURI($submit_uri) ->addSubmitButton(pht('Move Document')) ->addCancelButton($cancel_uri); return id(new AphrontDialogResponse())->setDialog($dialog); } } diff --git a/src/applications/phriction/editor/PhrictionTransactionEditor.php b/src/applications/phriction/editor/PhrictionTransactionEditor.php index 4fef27449c..6e2de641dc 100644 --- a/src/applications/phriction/editor/PhrictionTransactionEditor.php +++ b/src/applications/phriction/editor/PhrictionTransactionEditor.php @@ -1,309 +1,372 @@ description = $description; return $this; } private function getDescription() { return $this->description; } private function setOldContent(PhrictionContent $content) { $this->oldContent = $content; return $this; } private function getOldContent() { return $this->oldContent; } private function setNewContent(PhrictionContent $content) { $this->newContent = $content; return $this; } private function getNewContent() { return $this->newContent; } public function getEditorApplicationClass() { return 'PhabricatorPhrictionApplication'; } public function getEditorObjectsDescription() { return pht('Phriction Documents'); } public function getTransactionTypes() { $types = parent::getTransactionTypes(); $types[] = PhabricatorTransactions::TYPE_COMMENT; $types[] = PhrictionTransaction::TYPE_TITLE; $types[] = PhrictionTransaction::TYPE_CONTENT; $types[] = PhrictionTransaction::TYPE_DELETE; + $types[] = PhrictionTransaction::TYPE_MOVE_TO; + $types[] = PhrictionTransaction::TYPE_MOVE_AWAY; /* TODO $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY; $types[] = PhabricatorTransactions::TYPE_EDIT_POLICY; */ return $types; } protected function getCustomTransactionOldValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhrictionTransaction::TYPE_TITLE: if ($this->getIsNewObject()) { return null; } return $this->getOldContent()->getTitle(); case PhrictionTransaction::TYPE_CONTENT: if ($this->getIsNewObject()) { return null; } return $this->getOldContent()->getContent(); case PhrictionTransaction::TYPE_DELETE: + case PhrictionTransaction::TYPE_MOVE_TO: + case PhrictionTransaction::TYPE_MOVE_AWAY: return null; } } protected function getCustomTransactionNewValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhrictionTransaction::TYPE_TITLE: case PhrictionTransaction::TYPE_CONTENT: case PhrictionTransaction::TYPE_DELETE: return $xaction->getNewValue(); + case PhrictionTransaction::TYPE_MOVE_TO: + $document = $xaction->getNewValue(); + // grab the real object now for the sub-editor to come + $this->moveAwayDocument = $document; + $dict = array( + 'id' => $document->getID(), + 'phid' => $document->getPHID(), + 'content' => $document->getContent()->getContent(),); + return $dict; + case PhrictionTransaction::TYPE_MOVE_AWAY: + $document = $xaction->getNewValue(); + $dict = array( + 'id' => $document->getID(), + 'phid' => $document->getPHID(), + 'content' => $document->getContent()->getContent(),); + return $dict; } } protected function shouldApplyInitialEffects( PhabricatorLiskDAO $object, array $xactions) { foreach ($xactions as $xaction) { switch ($xaction->getTransactionType()) { case PhrictionTransaction::TYPE_TITLE: case PhrictionTransaction::TYPE_CONTENT: case PhrictionTransaction::TYPE_DELETE: + case PhrictionTransaction::TYPE_MOVE_TO: + case PhrictionTransaction::TYPE_MOVE_AWAY: return true; } } return parent::shouldApplyInitialEffects($object, $xactions); } protected function applyInitialEffects( PhabricatorLiskDAO $object, array $xactions) { $this->setOldContent($object->getContent()); $this->setNewContent($this->buildNewContentTemplate($object)); } protected function applyCustomInternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhrictionTransaction::TYPE_TITLE: case PhrictionTransaction::TYPE_CONTENT: + case PhrictionTransaction::TYPE_MOVE_TO: $object->setStatus(PhrictionDocumentStatus::STATUS_EXISTS); return; + case PhrictionTransaction::TYPE_MOVE_AWAY: + $object->setStatus(PhrictionDocumentStatus::STATUS_MOVED); + return; } } protected function applyCustomExternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhrictionTransaction::TYPE_TITLE: $this->getNewContent()->setTitle($xaction->getNewValue()); break; case PhrictionTransaction::TYPE_CONTENT: $this->getNewContent()->setContent($xaction->getNewValue()); break; case PhrictionTransaction::TYPE_DELETE: $this->getNewContent()->setContent(''); $this->getNewContent()->setChangeType( PhrictionChangeType::CHANGE_DELETE); break; + case PhrictionTransaction::TYPE_MOVE_TO: + $dict = $xaction->getNewValue(); + $this->getNewContent()->setContent($dict['content']); + $this->getNewContent()->setChangeType( + PhrictionChangeType::CHANGE_MOVE_HERE); + $this->getNewContent()->setChangeRef($dict['id']); + break; + case PhrictionTransaction::TYPE_MOVE_AWAY: + $dict = $xaction->getNewValue(); + $this->getNewContent()->setContent(''); + $this->getNewContent()->setChangeType( + PhrictionChangeType::CHANGE_MOVE_AWAY); + $this->getNewContent()->setChangeRef($dict['id']); + break; default: break; } } protected function applyFinalEffects( PhabricatorLiskDAO $object, array $xactions) { $save_content = false; foreach ($xactions as $xaction) { switch ($xaction->getTransactionType()) { case PhrictionTransaction::TYPE_TITLE: case PhrictionTransaction::TYPE_CONTENT: case PhrictionTransaction::TYPE_DELETE: + case PhrictionTransaction::TYPE_MOVE_AWAY: + case PhrictionTransaction::TYPE_MOVE_TO: $save_content = true; break; default: break; } } if ($save_content) { $content = $this->getNewContent(); $content->setDocumentID($object->getID()); $content->save(); $object->setContentID($content->getID()); $object->save(); $object->attachContent($content); } if ($this->getIsNewObject()) { // Stub out empty parent documents if they don't exist $ancestral_slugs = PhabricatorSlug::getAncestry($object->getSlug()); if ($ancestral_slugs) { $ancestors = id(new PhrictionDocument())->loadAllWhere( 'slug IN (%Ls)', $ancestral_slugs); $ancestors = mpull($ancestors, null, 'getSlug'); foreach ($ancestral_slugs as $slug) { // We check for change type to prevent near-infinite recursion if (!isset($ancestors[$slug]) && $content->getChangeType() != PhrictionChangeType::CHANGE_STUB) { id(PhrictionDocumentEditor::newForSlug($slug)) ->setActor($this->getActor()) ->setTitle(PhabricatorSlug::getDefaultTitle($slug)) ->setContent('') ->setDescription(pht('Empty Parent Document')) ->stub(); } } } } + + if ($this->moveAwayDocument !== null) { + $move_away_xactions = array(); + $move_away_xactions[] = id(new PhrictionTransaction()) + ->setTransactionType(PhrictionTransaction::TYPE_MOVE_AWAY) + ->setNewValue($object); + $sub_editor = id(new PhrictionTransactionEditor()) + ->setActor($this->getActor()) + ->setContentSource($this->getContentSource()) + ->setContinueOnNoEffect($this->getContinueOnNoEffect()) + ->setDescription($this->getDescription()) + ->applyTransactions($this->moveAwayDocument, $move_away_xactions); + } + return $xactions; } protected function shouldSendMail( PhabricatorLiskDAO $object, array $xactions) { - - $xactions = mfilter($xactions, 'shouldHide', true); - return $xactions; + return true; } protected function getMailSubjectPrefix() { return '[Phriction]'; } protected function getMailTo(PhabricatorLiskDAO $object) { return array( $object->getContent()->getAuthorPHID(), $this->getActingAsPHID(), ); } public function getMailTagsMap() { return array( PhrictionTransaction::MAILTAG_TITLE => pht("A document's title changes."), PhrictionTransaction::MAILTAG_CONTENT => pht("A document's content changes."), PhrictionTransaction::MAILTAG_DELETE => pht('A document is deleted.'), ); } protected function buildReplyHandler(PhabricatorLiskDAO $object) { return id(new PhrictionReplyHandler()) ->setMailReceiver($object); } protected function buildMailTemplate(PhabricatorLiskDAO $object) { $id = $object->getID(); $title = $object->getContent()->getTitle(); return id(new PhabricatorMetaMTAMail()) ->setSubject($title) ->addHeader('Thread-Topic', $object->getPHID()); } protected function buildMailBody( PhabricatorLiskDAO $object, array $xactions) { $body = parent::buildMailBody($object, $xactions); if ($this->getIsNewObject()) { $body->addTextSection( pht('DOCUMENT CONTENT'), $object->getContent()->getContent()); } $body->addLinkSection( pht('DOCUMENT DETAIL'), PhabricatorEnv::getProductionURI( PhrictionDocument::getSlugURI($object->getSlug()))); return $body; } protected function shouldPublishFeedStory( PhabricatorLiskDAO $object, array $xactions) { return $this->shouldSendMail($object, $xactions); } protected function getFeedRelatedPHIDs( PhabricatorLiskDAO $object, array $xactions) { $phids = parent::getFeedRelatedPHIDs($object, $xactions); - // TODO - once the editor supports moves, we'll need to surface the - // "from document phid" to related phids. + + foreach ($xactions as $xaction) { + switch ($xaction->getTransactionType()) { + case PhrictionTransaction::TYPE_MOVE_TO: + $dict = $xaction->getNewValue(); + $phids[] = $dict['phid']; + break; + } + } + return $phids; } protected function supportsSearch() { return true; } protected function shouldApplyHeraldRules( PhabricatorLiskDAO $object, array $xactions) { return false; } private function buildNewContentTemplate( PhrictionDocument $document) { $new_content = id(new PhrictionContent()) ->setSlug($document->getSlug()) ->setAuthorPHID($this->getActor()->getPHID()) ->setChangeType(PhrictionChangeType::CHANGE_EDIT) ->setTitle($this->getOldContent()->getTitle()) ->setContent($this->getOldContent()->getContent()); if (strlen($this->getDescription())) { $new_content->setDescription($this->getDescription()); } $new_content->setVersion($this->getOldContent()->getVersion() + 1); return $new_content; } } diff --git a/src/applications/phriction/storage/PhrictionTransaction.php b/src/applications/phriction/storage/PhrictionTransaction.php index 1d0fc12700..90fa86f369 100644 --- a/src/applications/phriction/storage/PhrictionTransaction.php +++ b/src/applications/phriction/storage/PhrictionTransaction.php @@ -1,210 +1,267 @@ getNewValue(); + switch ($this->getTransactionType()) { + case self::TYPE_MOVE_TO: + case self::TYPE_MOVE_AWAY: + $phids[] = $new['phid']; + break; + } + + + return $phids; + } + public function getRemarkupBlocks() { $blocks = parent::getRemarkupBlocks(); switch ($this->getTransactionType()) { case self::TYPE_CONTENT: $blocks[] = $this->getNewValue(); break; } return $blocks; } public function shouldHide() { switch ($this->getTransactionType()) { case self::TYPE_CONTENT: if ($this->getOldValue() === null) { return true; } else { return false; } break; } return parent::shouldHide(); } + public function shouldHideForMail(array $xactions) { + switch ($this->getTransactionType()) { + case self::TYPE_MOVE_TO: + case self::TYPE_MOVE_AWAY: + return true; + } + return parent::shouldHideForMail($xactions); + } + + public function shouldHideForFeed() { + switch ($this->getTransactionType()) { + case self::TYPE_MOVE_TO: + case self::TYPE_MOVE_AWAY: + return true; + } + return parent::shouldHideForFeed(); + } + public function getActionStrength() { switch ($this->getTransactionType()) { case self::TYPE_TITLE: return 1.4; case self::TYPE_CONTENT: return 1.3; case self::TYPE_DELETE: return 1.5; + case self::TYPE_MOVE_TO: + case self::TYPE_MOVE_AWAY: + return 1.0; } return parent::getActionStrength(); } public function getActionName() { $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case self::TYPE_TITLE: if ($old === null) { return pht('Created'); } return pht('Retitled'); case self::TYPE_CONTENT: return pht('Edited'); case self::TYPE_DELETE: return pht('Deleted'); + case self::TYPE_MOVE_TO: + return pht('Moved'); + + case self::TYPE_MOVE_AWAY: + return pht('Moved Away'); + } return parent::getActionName(); } public function getIcon() { $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case self::TYPE_TITLE: case self::TYPE_CONTENT: return 'fa-pencil'; case self::TYPE_DELETE: return 'fa-times'; + case self::TYPE_MOVE_TO: + case self::TYPE_MOVE_AWAY: + return 'fa-arrows'; } return parent::getIcon(); } public function getTitle() { $author_phid = $this->getAuthorPHID(); $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case self::TYPE_TITLE: if ($old === null) { return pht( '%s created this document.', $this->renderHandleLink($author_phid)); } return pht( '%s changed the title from "%s" to "%s".', $this->renderHandleLink($author_phid), $old, $new); case self::TYPE_CONTENT: return pht( '%s edited the document content.', $this->renderHandleLink($author_phid)); case self::TYPE_DELETE: return pht( '%s deleted this document.', $this->renderHandleLink($author_phid)); + case self::TYPE_MOVE_TO: + return pht( + '%s moved this document from %s', + $this->renderHandleLink($author_phid), + $this->renderHandleLink($new['phid'])); + + case self::TYPE_MOVE_AWAY: + return pht( + '%s moved this document to %s', + $this->renderHandleLink($author_phid), + $this->renderHandleLink($new['phid'])); } return parent::getTitle(); } public function getTitleForFeed(PhabricatorFeedStory $story) { $author_phid = $this->getAuthorPHID(); $object_phid = $this->getObjectPHID(); $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case self::TYPE_TITLE: if ($old === null) { return pht( '%s created %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); } return pht( '%s renamed %s from "%s" to "%s".', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid), $old, $new); case self::TYPE_CONTENT: return pht( '%s edited the content of %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); case self::TYPE_DELETE: return pht( '%s deleted %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); } return parent::getTitleForFeed($story); } public function hasChangeDetails() { switch ($this->getTransactionType()) { case self::TYPE_CONTENT: return true; } return parent::hasChangeDetails(); } public function renderChangeDetails(PhabricatorUser $viewer) { return $this->renderTextCorpusChangeDetails( $viewer, $this->getOldValue(), $this->getNewValue()); } public function getMailTags() { $tags = array(); switch ($this->getTransactionType()) { case self::TYPE_TITLE: $tags[] = self::MAILTAG_TITLE; break; case self::TYPE_CONTENT: $tags[] = self::MAILTAG_CONTENT; break; case self::TYPE_DELETE: $tags[] = self::MAILTAG_DELETE; break; } return $tags; } }