diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 78cd41f437..426c1e52c3 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -3464,6 +3464,7 @@ phutil_register_library_map(array( 'PhabricatorFileDataController' => 'applications/files/controller/PhabricatorFileDataController.php', 'PhabricatorFileDeleteController' => 'applications/files/controller/PhabricatorFileDeleteController.php', 'PhabricatorFileDeleteTransaction' => 'applications/files/xaction/PhabricatorFileDeleteTransaction.php', + 'PhabricatorFileDetachController' => 'applications/files/controller/PhabricatorFileDetachController.php', 'PhabricatorFileDocumentController' => 'applications/files/controller/PhabricatorFileDocumentController.php', 'PhabricatorFileDocumentRenderingEngine' => 'applications/files/document/render/PhabricatorFileDocumentRenderingEngine.php', 'PhabricatorFileDropUploadController' => 'applications/files/controller/PhabricatorFileDropUploadController.php', @@ -9919,6 +9920,7 @@ phutil_register_library_map(array( 'PhabricatorFileDataController' => 'PhabricatorFileController', 'PhabricatorFileDeleteController' => 'PhabricatorFileController', 'PhabricatorFileDeleteTransaction' => 'PhabricatorFileTransactionType', + 'PhabricatorFileDetachController' => 'PhabricatorFileController', 'PhabricatorFileDocumentController' => 'PhabricatorFileController', 'PhabricatorFileDocumentRenderingEngine' => 'PhabricatorDocumentRenderingEngine', 'PhabricatorFileDropUploadController' => 'PhabricatorFileController', diff --git a/src/applications/files/application/PhabricatorFilesApplication.php b/src/applications/files/application/PhabricatorFilesApplication.php index a9ebc29ea5..e246d4c90c 100644 --- a/src/applications/files/application/PhabricatorFilesApplication.php +++ b/src/applications/files/application/PhabricatorFilesApplication.php @@ -96,6 +96,8 @@ final class PhabricatorFilesApplication extends PhabricatorApplication { 'document/(?P[^/]+)/(?P[^/]+)/' => 'PhabricatorFileDocumentController', 'ui/' => array( + 'detach/(?P[^/]+)/(?P[^/]+)/' + => 'PhabricatorFileDetachController', 'curtain/' => array( 'list/(?P[^/]+)/' => 'PhabricatorFileUICurtainListController', diff --git a/src/applications/files/controller/PhabricatorFileDetachController.php b/src/applications/files/controller/PhabricatorFileDetachController.php new file mode 100644 index 0000000000..146eb42874 --- /dev/null +++ b/src/applications/files/controller/PhabricatorFileDetachController.php @@ -0,0 +1,120 @@ +getViewer(); + + $object_phid = $request->getURIData('objectPHID'); + $file_phid = $request->getURIData('filePHID'); + + $object = id(new PhabricatorObjectQuery()) + ->setViewer($viewer) + ->withPHIDs(array($object_phid)) + ->executeOne(); + if (!$object) { + return new Aphront404Response(); + } + + $handles = $viewer->loadHandles( + array( + $object_phid, + $file_phid, + )); + + $object_handle = $handles[$object_phid]; + $file_handle = $handles[$file_phid]; + $cancel_uri = $file_handle->getURI(); + + $dialog = $this->newDialog() + ->setViewer($viewer) + ->setTitle(pht('Detach File')) + ->addCancelButton($cancel_uri, pht('Close')); + + $file_link = phutil_tag('strong', array(), $file_handle->renderLink()); + $object_link = phutil_tag('strong', array(), $object_handle->renderLink()); + + $attachment = id(new PhabricatorFileAttachmentQuery()) + ->setViewer($viewer) + ->withObjectPHIDs(array($object->getPHID())) + ->withFilePHIDs(array($file_phid)) + ->needFiles(true) + ->withVisibleFiles(true) + ->executeOne(); + if (!$attachment) { + $body = pht( + 'The file %s is not attached to the object %s.', + $file_link, + $object_link); + + return $dialog->appendParagraph($body); + } + + $mode_reference = PhabricatorFileAttachment::MODE_REFERENCE; + if ($attachment->getAttachmentMode() === $mode_reference) { + $body = pht( + 'The file %s is referenced by the object %s, but not attached to '. + 'it, so it can not be detached.', + $file_link, + $object_link); + + return $dialog->appendParagraph($body); + } + + if (!$attachment->canDetach()) { + $body = pht( + 'The file %s can not be detached from the object %s.', + $file_link, + $object_link); + + return $dialog->appendParagraph($body); + } + + if (!$request->isDialogFormPost()) { + $dialog->appendParagraph( + pht( + 'Detach the file %s from the object %s?', + $file_link, + $object_link)); + + $dialog->addSubmitButton(pht('Detach File')); + + return $dialog; + } + + if (!($object instanceof PhabricatorApplicationTransactionInterface)) { + $dialog->appendParagraph( + pht( + 'This object (of class "%s") does not implement the required '. + 'interface ("%s"), so files can not be manually detached from it.', + get_class($object), + 'PhabricatorApplicationTransactionInterface')); + + return $dialog; + } + + $editor = $object->getApplicationTransactionEditor() + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true); + + $template = $object->getApplicationTransactionTemplate(); + + $xactions = array(); + + $xactions[] = id(clone $template) + ->setTransactionType(PhabricatorTransactions::TYPE_FILE) + ->setNewValue( + array( + $file_phid => PhabricatorFileAttachment::MODE_DETACH, + )); + + $editor->applyTransactions($object, $xactions); + + return $this->newRedirect() + ->setURI($cancel_uri); + } + +} diff --git a/src/applications/files/controller/PhabricatorFileUICurtainAttachController.php b/src/applications/files/controller/PhabricatorFileUICurtainAttachController.php index c34d1ba5a4..8dcfa5b840 100644 --- a/src/applications/files/controller/PhabricatorFileUICurtainAttachController.php +++ b/src/applications/files/controller/PhabricatorFileUICurtainAttachController.php @@ -28,9 +28,6 @@ final class PhabricatorFileUICurtainAttachController return new Aphront404Response(); } - $file = $attachment->getFile(); - $file_phid = $file->getPHID(); - $handles = $viewer->loadHandles( array( $object_phid, @@ -44,7 +41,7 @@ final class PhabricatorFileUICurtainAttachController $dialog = $this->newDialog() ->setViewer($viewer) ->setTitle(pht('Attach File')) - ->addCancelButton($object_handle->getURI(), pht('Close')); + ->addCancelButton($cancel_uri, pht('Close')); $file_link = phutil_tag('strong', array(), $file_handle->renderLink()); $object_link = phutil_tag('strong', array(), $object_handle->renderLink()); diff --git a/src/applications/files/controller/PhabricatorFileViewController.php b/src/applications/files/controller/PhabricatorFileViewController.php index 145f066492..389fc66247 100644 --- a/src/applications/files/controller/PhabricatorFileViewController.php +++ b/src/applications/files/controller/PhabricatorFileViewController.php @@ -320,20 +320,13 @@ final class PhabricatorFileViewController extends PhabricatorFileController { $finfo->addProperty(pht('Default Alt Text'), $default_alt); } - $phids = $file->getObjectPHIDs(); - if ($phids) { - $attached = new PHUIPropertyListView(); + $attachments_table = $this->newAttachmentsView($file); - $tab_group->addTab( - id(new PHUITabView()) - ->setName(pht('Attached')) - ->setKey('attached') - ->appendChild($attached)); - - $attached->addProperty( - pht('Attached To'), - $viewer->renderHandleList($phids)); - } + $tab_group->addTab( + id(new PHUITabView()) + ->setName(pht('Attached')) + ->setKey('attached') + ->appendChild($attachments_table)); $engine = $this->loadStorageEngine($file); if ($engine) { @@ -420,4 +413,81 @@ final class PhabricatorFileViewController extends PhabricatorFileController { return $engine->newDocumentView($ref); } + private function newAttachmentsView(PhabricatorFile $file) { + $viewer = $this->getViewer(); + + $attachments = id(new PhabricatorFileAttachmentQuery()) + ->setViewer($viewer) + ->withFilePHIDs(array($file->getPHID())) + ->execute(); + + $handles = $viewer->loadHandles(mpull($attachments, 'getObjectPHID')); + + $rows = array(); + + $mode_map = PhabricatorFileAttachment::getModeNameMap(); + $mode_attach = PhabricatorFileAttachment::MODE_ATTACH; + + foreach ($attachments as $attachment) { + $object_phid = $attachment->getObjectPHID(); + $handle = $handles[$object_phid]; + + $attachment_mode = $attachment->getAttachmentMode(); + + $mode_name = idx($mode_map, $attachment_mode); + if ($mode_name === null) { + $mode_name = pht('Unknown ("%s")', $attachment_mode); + } + + $detach_uri = urisprintf( + '/file/ui/detach/%s/%s/', + $object_phid, + $file->getPHID()); + + $is_disabled = !$attachment->canDetach(); + + $detach_button = id(new PHUIButtonView()) + ->setHref($detach_uri) + ->setTag('a') + ->setWorkflow(true) + ->setDisabled($is_disabled) + ->setColor(PHUIButtonView::GREY) + ->setSize(PHUIButtonView::SMALL) + ->setText(pht('Detach File')); + + javelin_tag( + 'a', + array( + 'href' => $detach_uri, + 'sigil' => 'workflow', + 'disabled' => true, + 'class' => 'small button button-grey disabled', + ), + pht('Detach File')); + + $rows[] = array( + $handle->renderLink(), + $mode_name, + $detach_button, + ); + } + + $table = id(new AphrontTableView($rows)) + ->setHeaders( + array( + pht('Attached To'), + pht('Mode'), + null, + )) + ->setColumnClasses( + array( + 'pri wide', + null, + null, + )); + + return $table; + } + + } diff --git a/src/applications/files/storage/PhabricatorFileAttachment.php b/src/applications/files/storage/PhabricatorFileAttachment.php index f2d2a13f4e..c6aa170c79 100644 --- a/src/applications/files/storage/PhabricatorFileAttachment.php +++ b/src/applications/files/storage/PhabricatorFileAttachment.php @@ -46,6 +46,13 @@ final class PhabricatorFileAttachment ); } + public static function getModeNameMap() { + return array( + self::MODE_ATTACH => pht('Attached'), + self::MODE_REFERENCE => pht('Referenced'), + ); + } + public function isPolicyAttachment() { switch ($this->getAttachmentMode()) { case self::MODE_ATTACH: @@ -73,6 +80,15 @@ final class PhabricatorFileAttachment return $this->assertAttached($this->file); } + public function canDetach() { + switch ($this->getAttachmentMode()) { + case self::MODE_ATTACH: + return true; + } + + return false; + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php b/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php index ff4a992521..7b9a4c27e1 100644 --- a/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php +++ b/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php @@ -1803,6 +1803,13 @@ final class PhabricatorUSEnglishTranslation ), ), + '%s removed %s attached file(s): %s.' => array( + array( + '%s removed an attached file: %3$s.', + '%s removed attached files: %3$s.', + ), + ), + ); }