diff --git a/src/applications/pholio/controller/PholioMockEditController.php b/src/applications/pholio/controller/PholioMockEditController.php index 60c4202444..f3dedc9a33 100644 --- a/src/applications/pholio/controller/PholioMockEditController.php +++ b/src/applications/pholio/controller/PholioMockEditController.php @@ -1,379 +1,383 @@ getViewer(); $id = $request->getURIData('id'); if ($id) { $mock = id(new PholioMockQuery()) ->setViewer($viewer) ->needImages(true) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->withIDs(array($id)) ->executeOne(); if (!$mock) { return new Aphront404Response(); } - $title = pht('Edit Mock'); + $title = pht('Edit Mock: %s', $mock->getName()); + $header_icon = 'fa-pencil'; $is_new = false; $mock_images = $mock->getImages(); $files = mpull($mock_images, 'getFile'); $mock_images = mpull($mock_images, null, 'getFilePHID'); } else { $mock = PholioMock::initializeNewMock($viewer); $title = pht('Create Mock'); + $header_icon = 'fa-plus-square'; $is_new = true; $files = array(); $mock_images = array(); } if ($is_new) { $v_projects = array(); } else { $v_projects = PhabricatorEdgeQuery::loadDestinationPHIDs( $mock->getPHID(), PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); $v_projects = array_reverse($v_projects); } $e_name = true; $e_images = count($mock_images) ? null : true; $errors = array(); $posted_mock_images = array(); $v_name = $mock->getName(); $v_desc = $mock->getDescription(); $v_view = $mock->getViewPolicy(); $v_edit = $mock->getEditPolicy(); $v_cc = PhabricatorSubscribersQuery::loadSubscribersForPHID( $mock->getPHID()); $v_space = $mock->getSpacePHID(); if ($request->isFormPost()) { $xactions = array(); $type_name = PholioTransaction::TYPE_NAME; $type_desc = PholioTransaction::TYPE_DESCRIPTION; $type_view = PhabricatorTransactions::TYPE_VIEW_POLICY; $type_edit = PhabricatorTransactions::TYPE_EDIT_POLICY; $type_cc = PhabricatorTransactions::TYPE_SUBSCRIBERS; $type_space = PhabricatorTransactions::TYPE_SPACE; $v_name = $request->getStr('name'); $v_desc = $request->getStr('description'); $v_view = $request->getStr('can_view'); $v_edit = $request->getStr('can_edit'); $v_cc = $request->getArr('cc'); $v_projects = $request->getArr('projects'); $v_space = $request->getStr('spacePHID'); $mock_xactions = array(); $mock_xactions[$type_name] = $v_name; $mock_xactions[$type_desc] = $v_desc; $mock_xactions[$type_view] = $v_view; $mock_xactions[$type_edit] = $v_edit; $mock_xactions[$type_cc] = array('=' => $v_cc); $mock_xactions[$type_space] = $v_space; if (!strlen($request->getStr('name'))) { $e_name = pht('Required'); $errors[] = pht('You must give the mock a name.'); } $file_phids = $request->getArr('file_phids'); if ($file_phids) { $files = id(new PhabricatorFileQuery()) ->setViewer($viewer) ->withPHIDs($file_phids) ->execute(); $files = mpull($files, null, 'getPHID'); $files = array_select_keys($files, $file_phids); } else { $files = array(); } if (!$files) { $e_images = pht('Required'); $errors[] = pht('You must add at least one image to the mock.'); } else { $mock->setCoverPHID(head($files)->getPHID()); } foreach ($mock_xactions as $type => $value) { $xactions[$type] = id(new PholioTransaction()) ->setTransactionType($type) ->setNewValue($value); } $order = $request->getStrList('imageOrder'); $sequence_map = array_flip($order); $replaces = $request->getArr('replaces'); $replaces_map = array_flip($replaces); /** * Foreach file posted, check to see whether we are replacing an image, * adding an image, or simply updating image metadata. Create * transactions for these cases as appropos. */ foreach ($files as $file_phid => $file) { $replaces_image_phid = null; if (isset($replaces_map[$file_phid])) { $old_file_phid = $replaces_map[$file_phid]; if ($old_file_phid != $file_phid) { $old_image = idx($mock_images, $old_file_phid); if ($old_image) { $replaces_image_phid = $old_image->getPHID(); } } } $existing_image = idx($mock_images, $file_phid); $title = (string)$request->getStr('title_'.$file_phid); $description = (string)$request->getStr('description_'.$file_phid); $sequence = $sequence_map[$file_phid]; if ($replaces_image_phid) { $replace_image = id(new PholioImage()) ->setReplacesImagePHID($replaces_image_phid) ->setFilePhid($file_phid) ->attachFile($file) ->setName(strlen($title) ? $title : $file->getName()) ->setDescription($description) ->setSequence($sequence); $xactions[] = id(new PholioTransaction()) ->setTransactionType( PholioTransaction::TYPE_IMAGE_REPLACE) ->setNewValue($replace_image); $posted_mock_images[] = $replace_image; } else if (!$existing_image) { // this is an add $add_image = id(new PholioImage()) ->setFilePhid($file_phid) ->attachFile($file) ->setName(strlen($title) ? $title : $file->getName()) ->setDescription($description) ->setSequence($sequence); $xactions[] = id(new PholioTransaction()) ->setTransactionType(PholioTransaction::TYPE_IMAGE_FILE) ->setNewValue( array('+' => array($add_image))); $posted_mock_images[] = $add_image; } else { $xactions[] = id(new PholioTransaction()) ->setTransactionType(PholioTransaction::TYPE_IMAGE_NAME) ->setNewValue( array($existing_image->getPHID() => $title)); $xactions[] = id(new PholioTransaction()) ->setTransactionType( PholioTransaction::TYPE_IMAGE_DESCRIPTION) ->setNewValue( array($existing_image->getPHID() => $description)); $xactions[] = id(new PholioTransaction()) ->setTransactionType( PholioTransaction::TYPE_IMAGE_SEQUENCE) ->setNewValue( array($existing_image->getPHID() => $sequence)); $posted_mock_images[] = $existing_image; } } foreach ($mock_images as $file_phid => $mock_image) { if (!isset($files[$file_phid]) && !isset($replaces[$file_phid])) { // this is an outright delete $xactions[] = id(new PholioTransaction()) ->setTransactionType(PholioTransaction::TYPE_IMAGE_FILE) ->setNewValue( array('-' => array($mock_image))); } } if (!$errors) { $proj_edge_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST; $xactions[] = id(new PholioTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) ->setMetadataValue('edge:type', $proj_edge_type) ->setNewValue(array('=' => array_fuse($v_projects))); $mock->openTransaction(); $editor = id(new PholioMockEditor()) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true) ->setActor($viewer); $xactions = $editor->applyTransactions($mock, $xactions); $mock->saveTransaction(); return id(new AphrontRedirectResponse()) ->setURI('/M'.$mock->getID()); } } if ($id) { $submit = id(new AphrontFormSubmitControl()) ->addCancelButton('/M'.$id) ->setValue(pht('Save')); } else { $submit = id(new AphrontFormSubmitControl()) ->addCancelButton($this->getApplicationURI()) ->setValue(pht('Create')); } $policies = id(new PhabricatorPolicyQuery()) ->setViewer($viewer) ->setObject($mock) ->execute(); // NOTE: Make this show up correctly on the rendered form. $mock->setViewPolicy($v_view); $mock->setEditPolicy($v_edit); $image_elements = array(); if ($posted_mock_images) { $display_mock_images = $posted_mock_images; } else { $display_mock_images = $mock_images; } foreach ($display_mock_images as $mock_image) { $image_elements[] = id(new PholioUploadedImageView()) ->setUser($viewer) ->setImage($mock_image) ->setReplacesPHID($mock_image->getFilePHID()); } $list_id = celerity_generate_unique_node_id(); $drop_id = celerity_generate_unique_node_id(); $order_id = celerity_generate_unique_node_id(); $list_control = phutil_tag( 'div', array( 'id' => $list_id, 'class' => 'pholio-edit-list', ), $image_elements); $drop_control = phutil_tag( 'div', array( 'id' => $drop_id, 'class' => 'pholio-edit-drop', ), pht('Drag and drop images here to add them to the mock.')); $order_control = phutil_tag( 'input', array( 'type' => 'hidden', 'name' => 'imageOrder', 'id' => $order_id, )); Javelin::initBehavior( 'pholio-mock-edit', array( 'listID' => $list_id, 'dropID' => $drop_id, 'orderID' => $order_id, 'uploadURI' => '/file/dropupload/', 'renderURI' => $this->getApplicationURI('image/upload/'), 'pht' => array( 'uploading' => pht('Uploading Image...'), 'uploaded' => pht('Upload Complete...'), 'undo' => pht('Undo'), 'removed' => pht('This image will be removed from the mock.'), ), )); require_celerity_resource('pholio-edit-css'); $form = id(new AphrontFormView()) ->setUser($viewer) ->appendChild($order_control) ->appendChild( id(new AphrontFormTextControl()) ->setName('name') ->setValue($v_name) ->setLabel(pht('Name')) ->setError($e_name)) ->appendChild( id(new PhabricatorRemarkupControl()) ->setName('description') ->setValue($v_desc) ->setLabel(pht('Description')) ->setUser($viewer)) ->appendControl( id(new AphrontFormTokenizerControl()) ->setLabel(pht('Projects')) ->setName('projects') ->setValue($v_projects) ->setDatasource(new PhabricatorProjectDatasource())) ->appendControl( id(new AphrontFormTokenizerControl()) ->setLabel(pht('Subscribers')) ->setName('cc') ->setValue($v_cc) ->setUser($viewer) ->setDatasource(new PhabricatorMetaMTAMailableDatasource())) ->appendChild( id(new AphrontFormPolicyControl()) ->setUser($viewer) ->setCapability(PhabricatorPolicyCapability::CAN_VIEW) ->setPolicyObject($mock) ->setPolicies($policies) ->setSpacePHID($v_space) ->setName('can_view')) ->appendChild( id(new AphrontFormPolicyControl()) ->setUser($viewer) ->setCapability(PhabricatorPolicyCapability::CAN_EDIT) ->setPolicyObject($mock) ->setPolicies($policies) ->setName('can_edit')) ->appendChild( id(new AphrontFormMarkupControl()) ->setValue($list_control)) ->appendChild( id(new AphrontFormMarkupControl()) ->setValue($drop_control) ->setError($e_images)) ->appendChild($submit); $form_box = id(new PHUIObjectBoxView()) - ->setHeaderText($title) + ->setHeaderText(pht('Mock')) ->setFormErrors($errors) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setForm($form); $crumbs = $this->buildApplicationCrumbs(); if (!$is_new) { $crumbs->addTextCrumb($mock->getMonogram(), '/'.$mock->getMonogram()); } $crumbs->addTextCrumb($title); + $crumbs->setBorder(true); - $content = array( - $crumbs, - $form_box, - ); + $header = id(new PHUIHeaderView()) + ->setHeader($title) + ->setHeaderIcon($header_icon); + + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setFooter($form_box); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) ->addQuicksandConfig( array('mockEditConfig' => true)) - ->appendChild( - array( - $form_box, - )); + ->appendChild($view); } } diff --git a/src/applications/pholio/controller/PholioMockViewController.php b/src/applications/pholio/controller/PholioMockViewController.php index 78c1d4bfe9..565d2d6cb4 100644 --- a/src/applications/pholio/controller/PholioMockViewController.php +++ b/src/applications/pholio/controller/PholioMockViewController.php @@ -1,225 +1,236 @@ maniphestTaskPHIDs = $maniphest_task_phids; return $this; } private function getManiphestTaskPHIDs() { return $this->maniphestTaskPHIDs; } public function shouldAllowPublic() { return true; } public function handleRequest(AphrontRequest $request) { $viewer = $request->getViewer(); $id = $request->getURIData('id'); $image_id = $request->getURIData('imageID'); $mock = id(new PholioMockQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->needImages(true) ->needInlineComments(true) ->executeOne(); if (!$mock) { return new Aphront404Response(); } $phids = PhabricatorEdgeQuery::loadDestinationPHIDs( $mock->getPHID(), PholioMockHasTaskEdgeType::EDGECONST); $this->setManiphestTaskPHIDs($phids); $engine = id(new PhabricatorMarkupEngine()) ->setViewer($viewer); $engine->addObject($mock, PholioMock::MARKUP_FIELD_DESCRIPTION); $title = $mock->getName(); if ($mock->isClosed()) { $header_icon = 'fa-ban'; $header_name = pht('Closed'); $header_color = 'dark'; } else { $header_icon = 'fa-square-o'; $header_name = pht('Open'); $header_color = 'bluegrey'; } $header = id(new PHUIHeaderView()) ->setHeader($title) ->setUser($viewer) ->setStatus($header_icon, $header_color, $header_name) - ->setPolicyObject($mock); + ->setPolicyObject($mock) + ->setHeaderIcon('fa-camera-retro'); $timeline = $this->buildTransactionTimeline( $mock, new PholioTransactionQuery(), $engine); $timeline->setMock($mock); - $actions = $this->buildActionView($mock); - $properties = $this->buildPropertyView($mock, $engine, $actions); + $curtain = $this->buildCurtainView($mock); + $details = $this->buildDescriptionView($mock, $engine); require_celerity_resource('pholio-css'); require_celerity_resource('pholio-inline-comments-css'); $comment_form_id = celerity_generate_unique_node_id(); $mock_view = id(new PholioMockImagesView()) ->setRequestURI($request->getRequestURI()) ->setCommentFormID($comment_form_id) ->setUser($viewer) ->setMock($mock) ->setImageID($image_id); $output = id(new PHUIObjectBoxView()) - ->setHeaderText(pht('Image')) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->appendChild($mock_view); $add_comment = $this->buildAddCommentView($mock, $comment_form_id); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb('M'.$mock->getID(), '/M'.$mock->getID()); - - $object_box = id(new PHUIObjectBoxView()) - ->setHeader($header) - ->addPropertyList($properties); + $crumbs->setBorder(true); $thumb_grid = id(new PholioMockThumbGridView()) ->setUser($viewer) ->setMock($mock); + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setCurtain($curtain) + ->setMainColumn(array( + $output, + $thumb_grid, + $details, + $timeline, + $add_comment, + )); + return $this->newPage() ->setTitle('M'.$mock->getID().' '.$title) ->setCrumbs($crumbs) ->setPageObjectPHIDs(array($mock->getPHID())) ->addQuicksandConfig( array('mockViewConfig' => $mock_view->getBehaviorConfig())) - ->appendChild( - array( - $object_box, - $output, - $thumb_grid, - $timeline, - $add_comment, - )); + ->appendChild($view); } - private function buildActionView(PholioMock $mock) { + private function buildCurtainView(PholioMock $mock) { $viewer = $this->getViewer(); - $actions = id(new PhabricatorActionListView()) - ->setUser($viewer) - ->setObject($mock); + $curtain = $this->newCurtainView($mock); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $mock, PhabricatorPolicyCapability::CAN_EDIT); - $actions->addAction( + $curtain->addAction( id(new PhabricatorActionView()) ->setIcon('fa-pencil') ->setName(pht('Edit Mock')) ->setHref($this->getApplicationURI('/edit/'.$mock->getID().'/')) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); if ($mock->isClosed()) { - $actions->addAction( + $curtain->addAction( id(new PhabricatorActionView()) ->setIcon('fa-check') ->setName(pht('Open Mock')) ->setHref($this->getApplicationURI('/archive/'.$mock->getID().'/')) ->setDisabled(!$can_edit) ->setWorkflow(true)); } else { - $actions->addAction( + $curtain->addAction( id(new PhabricatorActionView()) ->setIcon('fa-ban') ->setName(pht('Close Mock')) ->setHref($this->getApplicationURI('/archive/'.$mock->getID().'/')) ->setDisabled(!$can_edit) ->setWorkflow(true)); } - $actions->addAction( + $curtain->addAction( id(new PhabricatorActionView()) ->setIcon('fa-anchor') ->setName(pht('Edit Maniphest Tasks')) ->setHref("/search/attach/{$mock->getPHID()}/TASK/edge/") ->setDisabled(!$viewer->isLoggedIn()) ->setWorkflow(true)); - return $actions; + if ($this->getManiphestTaskPHIDs()) { + $curtain->newPanel() + ->setHeaderText(pht('Maniphest Tasks')) + ->appendChild( + $viewer->renderHandleList($this->getManiphestTaskPHIDs())); + } + + $curtain->newPanel() + ->setHeaderText(pht('Authored By')) + ->appendChild($this->buildAuthorPanel($mock)); + + return $curtain; } - private function buildPropertyView( - PholioMock $mock, - PhabricatorMarkupEngine $engine, - PhabricatorActionListView $actions) { + private function buildDescriptionView(PholioMock $mock) { $viewer = $this->getViewer(); - $properties = id(new PHUIPropertyListView()) - ->setUser($viewer) - ->setObject($mock) - ->setActionList($actions); - - $properties->addProperty( - pht('Author'), - $viewer->renderHandle($mock->getAuthorPHID())); - - $properties->addProperty( - pht('Created'), - phabricator_datetime($mock->getDateCreated(), $viewer)); - - if ($this->getManiphestTaskPHIDs()) { - $properties->addProperty( - pht('Maniphest Tasks'), - $viewer->renderHandleList($this->getManiphestTaskPHIDs())); + ->setUser($viewer); + $description = $mock->getDescription(); + + if (strlen($description)) { + $properties->addImageContent($description); + return id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Mock Description')) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->appendChild($properties); } - $properties->invokeWillRenderEvent(); - - $properties->addSectionHeader( - pht('Description'), - PHUIPropertyListView::ICON_SUMMARY); - - $properties->addImageContent( - $engine->getOutput($mock, PholioMock::MARKUP_FIELD_DESCRIPTION)); + return null; + } - return $properties; + private function buildAuthorPanel(PholioMock $mock) { + $viewer = $this->getViewer(); + $author_phid = $mock->getAuthorPHID(); + $handles = $viewer->loadHandles(array($author_phid)); + + $author_uri = $handles[$author_phid]->getImageURI(); + $author_href = $handles[$author_phid]->getURI(); + $author = $viewer->renderHandle($author_phid)->render(); + + $content = phutil_tag('strong', array(), $author); + $date = phabricator_date($mock->getDateCreated(), $viewer); + $content = pht('%s, %s', $content, $date); + $authored_by = id(new PHUIHeadThingView()) + ->setImage($author_uri) + ->setImageHref($author_href) + ->setContent($content); + + return $authored_by; } private function buildAddCommentView(PholioMock $mock, $comment_form_id) { $viewer = $this->getViewer(); $draft = PhabricatorDraft::newFromUserAndKey($viewer, $mock->getPHID()); $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); $title = $is_serious ? pht('Add Comment') : pht('History Beckons'); $form = id(new PhabricatorApplicationTransactionCommentView()) ->setUser($viewer) ->setObjectPHID($mock->getPHID()) ->setFormID($comment_form_id) ->setDraft($draft) ->setHeaderText($title) ->setSubmitButtonName(pht('Add Comment')) ->setAction($this->getApplicationURI('/comment/'.$mock->getID().'/')) ->setRequestURI($this->getRequest()->getRequestURI()); return $form; } } diff --git a/src/applications/pholio/storage/PholioMock.php b/src/applications/pholio/storage/PholioMock.php index 49cf2ace5d..00e8efd981 100644 --- a/src/applications/pholio/storage/PholioMock.php +++ b/src/applications/pholio/storage/PholioMock.php @@ -1,325 +1,323 @@ setViewer($actor) ->withClasses(array('PhabricatorPholioApplication')) ->executeOne(); $view_policy = $app->getPolicy(PholioDefaultViewCapability::CAPABILITY); $edit_policy = $app->getPolicy(PholioDefaultEditCapability::CAPABILITY); return id(new PholioMock()) ->setAuthorPHID($actor->getPHID()) ->attachImages(array()) ->setStatus(self::STATUS_OPEN) ->setViewPolicy($view_policy) ->setEditPolicy($edit_policy) ->setSpacePHID($actor->getDefaultSpacePHID()); } public function getMonogram() { return 'M'.$this->getID(); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text128', 'description' => 'text', 'originalName' => 'text128', 'mailKey' => 'bytes20', 'status' => 'text12', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), 'unique' => true, ), 'authorPHID' => array( 'columns' => array('authorPHID'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID('MOCK'); } public function save() { if (!$this->getMailKey()) { $this->setMailKey(Filesystem::readRandomCharacters(20)); } return parent::save(); } /** * These should be the images currently associated with the Mock. */ public function attachImages(array $images) { assert_instances_of($images, 'PholioImage'); $this->images = $images; return $this; } public function getImages() { $this->assertAttached($this->images); return $this->images; } /** * These should be *all* images associated with the Mock. This includes * images which have been removed and / or replaced from the Mock. */ public function attachAllImages(array $images) { assert_instances_of($images, 'PholioImage'); $this->allImages = $images; return $this; } public function getAllImages() { $this->assertAttached($this->images); return $this->allImages; } public function attachCoverFile(PhabricatorFile $file) { $this->coverFile = $file; return $this; } public function getCoverFile() { $this->assertAttached($this->coverFile); return $this->coverFile; } public function getTokenCount() { $this->assertAttached($this->tokenCount); return $this->tokenCount; } public function attachTokenCount($count) { $this->tokenCount = $count; return $this; } public function getImageHistorySet($image_id) { $images = $this->getAllImages(); $images = mpull($images, null, 'getID'); $selected_image = $images[$image_id]; $replace_map = mpull($images, null, 'getReplacesImagePHID'); $phid_map = mpull($images, null, 'getPHID'); // find the earliest image $image = $selected_image; while (isset($phid_map[$image->getReplacesImagePHID()])) { $image = $phid_map[$image->getReplacesImagePHID()]; } // now build history moving forward $history = array($image->getID() => $image); while (isset($replace_map[$image->getPHID()])) { $image = $replace_map[$image->getPHID()]; $history[$image->getID()] = $image; } return $history; } public function getStatuses() { $options = array(); $options[self::STATUS_OPEN] = pht('Open'); $options[self::STATUS_CLOSED] = pht('Closed'); return $options; } public function isClosed() { return ($this->getStatus() == 'closed'); } /* -( PhabricatorSubscribableInterface Implementation )-------------------- */ public function isAutomaticallySubscribed($phid) { return ($this->authorPHID == $phid); } /* -( PhabricatorPolicyInterface Implementation )-------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return $this->getEditPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return ($viewer->getPHID() == $this->getAuthorPHID()); } public function describeAutomaticCapability($capability) { return pht("A mock's owner can always view and edit it."); } /* -( PhabricatorMarkupInterface )----------------------------------------- */ public function getMarkupFieldKey($field) { $hash = PhabricatorHash::digest($this->getMarkupText($field)); return 'M:'.$hash; } public function newMarkupEngine($field) { return PhabricatorMarkupEngine::newMarkupEngine(array()); } public function getMarkupText($field) { if ($this->getDescription()) { - $description = $this->getDescription(); - } else { - $description = pht('No Description Given'); + return $this->getDescription(); } - return $description; + return null; } public function didMarkupText($field, $output, PhutilMarkupEngine $engine) { require_celerity_resource('phabricator-remarkup-css'); return phutil_tag( 'div', array( 'class' => 'phabricator-remarkup', ), $output); } public function shouldUseMarkupCache($field) { return (bool)$this->getID(); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PholioMockEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new PholioTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { PholioMockQuery::loadImages( $request->getUser(), array($this), $need_inline_comments = true); $timeline->setMock($this); return $timeline; } /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ public function getUsersToNotifyOfTokenGiven() { return array( $this->getAuthorPHID(), ); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $images = id(new PholioImage())->loadAllWhere( 'mockID = %d', $this->getID()); foreach ($images as $image) { $image->delete(); } $this->delete(); $this->saveTransaction(); } /* -( PhabricatorSpacesInterface )----------------------------------------- */ public function getSpacePHID() { return $this->spacePHID; } /* -( PhabricatorFulltextInterface )--------------------------------------- */ public function newFulltextEngine() { return new PholioMockFulltextEngine(); } } diff --git a/src/applications/pholio/storage/PholioTransaction.php b/src/applications/pholio/storage/PholioTransaction.php index 17b0730dcb..d98c6b451b 100644 --- a/src/applications/pholio/storage/PholioTransaction.php +++ b/src/applications/pholio/storage/PholioTransaction.php @@ -1,389 +1,411 @@ getObjectPHID(); $new = $this->getNewValue(); $old = $this->getOldValue(); switch ($this->getTransactionType()) { case self::TYPE_IMAGE_FILE: $phids = array_merge($phids, $new, $old); break; case self::TYPE_IMAGE_REPLACE: $phids[] = $new; $phids[] = $old; break; case self::TYPE_IMAGE_DESCRIPTION: case self::TYPE_IMAGE_NAME: case self::TYPE_IMAGE_SEQUENCE: $phids[] = key($new); break; } return $phids; } public function shouldHide() { $old = $this->getOldValue(); switch ($this->getTransactionType()) { case self::TYPE_DESCRIPTION: return ($old === null); case self::TYPE_IMAGE_NAME: case self::TYPE_IMAGE_DESCRIPTION: return ($old === array(null => null)); // this is boring / silly to surface; changing sequence is NBD case self::TYPE_IMAGE_SEQUENCE: return true; } return parent::shouldHide(); } public function getIcon() { + + $new = $this->getNewValue(); + $old = $this->getOldValue(); + switch ($this->getTransactionType()) { case self::TYPE_INLINE: return 'fa-comment'; case self::TYPE_NAME: case self::TYPE_DESCRIPTION: case self::TYPE_STATUS: + if ($new == PholioMock::STATUS_CLOSED) { + return 'fa-ban'; + } else { + return 'fa-check'; + } case self::TYPE_IMAGE_NAME: case self::TYPE_IMAGE_DESCRIPTION: case self::TYPE_IMAGE_SEQUENCE: return 'fa-pencil'; case self::TYPE_IMAGE_FILE: case self::TYPE_IMAGE_REPLACE: return 'fa-picture-o'; } return parent::getIcon(); } public function getMailTags() { $tags = array(); switch ($this->getTransactionType()) { case self::TYPE_INLINE: case PhabricatorTransactions::TYPE_COMMENT: $tags[] = self::MAILTAG_COMMENT; break; case self::TYPE_STATUS: $tags[] = self::MAILTAG_STATUS; break; case self::TYPE_NAME: case self::TYPE_DESCRIPTION: case self::TYPE_IMAGE_NAME: case self::TYPE_IMAGE_DESCRIPTION: case self::TYPE_IMAGE_SEQUENCE: case self::TYPE_IMAGE_FILE: case self::TYPE_IMAGE_REPLACE: $tags[] = self::MAILTAG_UPDATED; break; default: $tags[] = self::MAILTAG_OTHER; break; } return $tags; } public function getTitle() { $author_phid = $this->getAuthorPHID(); $old = $this->getOldValue(); $new = $this->getNewValue(); $type = $this->getTransactionType(); switch ($type) { case self::TYPE_NAME: if ($old === null) { return pht( '%s created "%s".', $this->renderHandleLink($author_phid), $new); } else { return pht( '%s renamed this mock from "%s" to "%s".', $this->renderHandleLink($author_phid), $old, $new); } break; case self::TYPE_DESCRIPTION: return pht( "%s updated the mock's description.", $this->renderHandleLink($author_phid)); break; case self::TYPE_STATUS: - return pht( - "%s updated the mock's status.", - $this->renderHandleLink($author_phid)); + if ($new == PholioMock::STATUS_CLOSED) { + return pht( + '%s closed this mock.', + $this->renderHandleLink($author_phid)); + } else { + return pht( + '%s opened this mock.', + $this->renderHandleLink($author_phid)); + } break; case self::TYPE_INLINE: $count = 1; foreach ($this->getTransactionGroup() as $xaction) { if ($xaction->getTransactionType() == $type) { $count++; } } return pht( '%s added %d inline comment(s).', $this->renderHandleLink($author_phid), $count); break; case self::TYPE_IMAGE_REPLACE: return pht( '%s replaced %s with %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($old), $this->renderHandleLink($new)); break; case self::TYPE_IMAGE_FILE: $add = array_diff($new, $old); $rem = array_diff($old, $new); if ($add && $rem) { return pht( '%s edited image(s), added %d: %s; removed %d: %s.', $this->renderHandleLink($author_phid), count($add), $this->renderHandleList($add), count($rem), $this->renderHandleList($rem)); } else if ($add) { return pht( '%s added %d image(s): %s.', $this->renderHandleLink($author_phid), count($add), $this->renderHandleList($add)); } else { return pht( '%s removed %d image(s): %s.', $this->renderHandleLink($author_phid), count($rem), $this->renderHandleList($rem)); } break; case self::TYPE_IMAGE_NAME: return pht( '%s renamed an image (%s) from "%s" to "%s".', $this->renderHandleLink($author_phid), $this->renderHandleLink(key($new)), reset($old), reset($new)); break; case self::TYPE_IMAGE_DESCRIPTION: return pht( '%s updated an image\'s (%s) description.', $this->renderHandleLink($author_phid), $this->renderHandleLink(key($new))); break; case self::TYPE_IMAGE_SEQUENCE: return pht( '%s updated an image\'s (%s) sequence.', $this->renderHandleLink($author_phid), $this->renderHandleLink(key($new))); break; } return parent::getTitle(); } public function getTitleForFeed() { $author_phid = $this->getAuthorPHID(); $object_phid = $this->getObjectPHID(); $old = $this->getOldValue(); $new = $this->getNewValue(); $type = $this->getTransactionType(); switch ($type) { case self::TYPE_NAME: if ($old === null) { return pht( '%s created %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); } else { return pht( '%s renamed %s from "%s" to "%s".', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid), $old, $new); } break; case self::TYPE_DESCRIPTION: return pht( '%s updated the description for %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); break; case self::TYPE_STATUS: - return pht( - '%s updated the status for %s.', - $this->renderHandleLink($author_phid), - $this->renderHandleLink($object_phid)); + if ($new == PholioMock::STATUS_CLOSED) { + return pht( + '%s closed a mock %s.', + $this->renderHandleLink($author_phid), + $this->renderHandleLink($object_phid)); + } else { + return pht( + '%s opened a mock %s.', + $this->renderHandleLink($author_phid), + $this->renderHandleLink($object_phid)); + } break; case self::TYPE_INLINE: return pht( '%s added an inline comment to %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); break; case self::TYPE_IMAGE_REPLACE: case self::TYPE_IMAGE_FILE: return pht( '%s updated images of %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); break; case self::TYPE_IMAGE_NAME: return pht( '%s updated the image names of %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); break; case self::TYPE_IMAGE_DESCRIPTION: return pht( '%s updated image descriptions of %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); break; case self::TYPE_IMAGE_SEQUENCE: return pht( '%s updated image sequence of %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); break; } return parent::getTitleForFeed(); } public function getRemarkupBodyForFeed(PhabricatorFeedStory $story) { $text = null; switch ($this->getTransactionType()) { case self::TYPE_NAME: if ($this->getOldValue() === null) { $mock = $story->getPrimaryObject(); $text = $mock->getDescription(); } break; case self::TYPE_INLINE: $text = $this->getComment()->getContent(); break; } return $text; } public function hasChangeDetails() { switch ($this->getTransactionType()) { case self::TYPE_DESCRIPTION: case self::TYPE_IMAGE_DESCRIPTION: return true; } return parent::hasChangeDetails(); } public function renderChangeDetails(PhabricatorUser $viewer) { $old = $this->getOldValue(); $new = $this->getNewValue(); if ($this->getTransactionType() == self::TYPE_IMAGE_DESCRIPTION) { $old = reset($old); $new = reset($new); } return $this->renderTextCorpusChangeDetails( $viewer, $old, $new); } public function getColor() { $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { + case self::TYPE_STATUS: + if ($new == PholioMock::STATUS_CLOSED) { + return PhabricatorTransactions::COLOR_INDIGO; + } else { + return PhabricatorTransactions::COLOR_GREEN; + } case self::TYPE_NAME: if ($old === null) { return PhabricatorTransactions::COLOR_GREEN; } - case self::TYPE_DESCRIPTION: - case self::TYPE_STATUS: - case self::TYPE_IMAGE_NAME: - case self::TYPE_IMAGE_DESCRIPTION: - case self::TYPE_IMAGE_SEQUENCE: - return PhabricatorTransactions::COLOR_BLUE; case self::TYPE_IMAGE_REPLACE: return PhabricatorTransactions::COLOR_YELLOW; case self::TYPE_IMAGE_FILE: $add = array_diff($new, $old); $rem = array_diff($old, $new); if ($add && $rem) { return PhabricatorTransactions::COLOR_YELLOW; } else if ($add) { return PhabricatorTransactions::COLOR_GREEN; } else { return PhabricatorTransactions::COLOR_RED; } } return parent::getColor(); } public function getNoEffectDescription() { switch ($this->getTransactionType()) { case self::TYPE_IMAGE_NAME: return pht('The image title was not updated.'); case self::TYPE_IMAGE_DESCRIPTION: return pht('The image description was not updated.'); case self::TYPE_IMAGE_SEQUENCE: return pht('The image sequence was not updated.'); } return parent::getNoEffectDescription(); } } diff --git a/src/applications/pholio/view/PholioMockThumbGridView.php b/src/applications/pholio/view/PholioMockThumbGridView.php index 6467106c14..457d700f1f 100644 --- a/src/applications/pholio/view/PholioMockThumbGridView.php +++ b/src/applications/pholio/view/PholioMockThumbGridView.php @@ -1,180 +1,181 @@ mock = $mock; return $this; } public function render() { $mock = $this->mock; $all_images = $mock->getAllImages(); $all_images = mpull($all_images, null, 'getPHID'); $history = mpull($all_images, 'getReplacesImagePHID', 'getPHID'); $replaced = array(); foreach ($history as $phid => $replaces_phid) { if ($replaces_phid) { $replaced[$replaces_phid] = true; } } // Figure out the columns. Start with all the active images. $images = mpull($mock->getImages(), null, 'getPHID'); // Now, find deleted images: obsolete images which were not replaced. foreach ($mock->getAllImages() as $image) { if (!$image->getIsObsolete()) { // Image is current. continue; } if (isset($replaced[$image->getPHID()])) { // Image was replaced. continue; } // This is an obsolete image which was not replaced, so it must be // a deleted image. $images[$image->getPHID()] = $image; } $cols = array(); $depth = 0; foreach ($images as $image) { $phid = $image->getPHID(); $col = array(); // If this is a deleted image, null out the final column. if ($image->getIsObsolete()) { $col[] = null; } $col[] = $phid; while ($phid && isset($history[$phid])) { $col[] = $history[$phid]; $phid = $history[$phid]; } $cols[] = $col; $depth = max($depth, count($col)); } $grid = array(); $jj = $depth; for ($ii = 0; $ii < $depth; $ii++) { $row = array(); if ($depth == $jj) { $row[] = phutil_tag( 'th', array( 'valign' => 'middle', 'class' => 'pholio-history-header', ), pht('Current Revision')); } else { $row[] = phutil_tag('th', array(), null); } foreach ($cols as $col) { if (empty($col[$ii])) { $row[] = phutil_tag('td', array(), null); } else { $thumb = $this->renderThumbnail($all_images[$col[$ii]]); $row[] = phutil_tag('td', array(), $thumb); } } $grid[] = phutil_tag('tr', array(), $row); $jj--; } $grid = phutil_tag( 'table', array( 'id' => 'pholio-mock-thumb-grid', 'class' => 'pholio-mock-thumb-grid', ), $grid); $grid = id(new PHUIBoxView()) ->addClass('pholio-mock-thumb-grid-container') ->appendChild($grid); return id(new PHUIObjectBoxView()) ->setHeaderText(pht('Mock History')) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->appendChild($grid); } private function renderThumbnail(PholioImage $image) { $thumbfile = $image->getFile(); $preview_key = PhabricatorFileThumbnailTransform::TRANSFORM_THUMBGRID; $xform = PhabricatorFileTransform::getTransformByKey($preview_key); Javelin::initBehavior('phabricator-tooltips'); $attributes = array( 'class' => 'pholio-mock-thumb-grid-image', 'src' => $thumbfile->getURIForTransform($xform), ); if ($image->getFile()->isViewableImage()) { $dimensions = $xform->getTransformedDimensions($thumbfile); if ($dimensions) { list($x, $y) = $dimensions; $attributes += array( 'width' => $x, 'height' => $y, 'style' => 'top: '.floor((100 - $y) / 2).'px', ); } } else { // If this is a PDF or a text file or something, we'll end up using a // generic thumbnail which is always sized correctly. $attributes += array( 'width' => 100, 'height' => 100, ); } $tag = phutil_tag('img', $attributes); $classes = array('pholio-mock-thumb-grid-item'); if ($image->getIsObsolete()) { $classes[] = 'pholio-mock-thumb-grid-item-obsolete'; } $inline_count = null; if ($image->getInlineComments()) { $inline_count[] = phutil_tag( 'span', array( 'class' => 'pholio-mock-thumb-grid-comment-count', ), pht('%s', phutil_count($image->getInlineComments()))); } return javelin_tag( 'a', array( 'sigil' => 'mock-thumbnail has-tooltip', 'class' => implode(' ', $classes), 'href' => '#', 'meta' => array( 'imageID' => $image->getID(), 'tip' => $image->getName(), 'align' => 'N', ), ), array( $tag, $inline_count, )); } }