diff --git a/src/applications/pholio/controller/PholioMockCommentController.php b/src/applications/pholio/controller/PholioMockCommentController.php index d54b4677e5..a4f9daf886 100644 --- a/src/applications/pholio/controller/PholioMockCommentController.php +++ b/src/applications/pholio/controller/PholioMockCommentController.php @@ -1,81 +1,81 @@ getViewer(); $id = $request->getURIData('id'); if (!$request->isFormPost()) { return new Aphront400Response(); } $mock = id(new PholioMockQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->needImages(true) ->executeOne(); if (!$mock) { return new Aphront404Response(); } $is_preview = $request->isPreviewRequest(); $draft = PhabricatorDraft::buildFromRequest($request); - $mock_uri = '/M'.$mock->getID(); + $mock_uri = $mock->getURI(); $comment = $request->getStr('comment'); $xactions = array(); $inline_comments = id(new PholioTransactionComment())->loadAllWhere( 'authorphid = %s AND transactionphid IS NULL AND imageid IN (%Ld)', $viewer->getPHID(), - mpull($mock->getImages(), 'getID')); + mpull($mock->getActiveImages(), 'getID')); if (!$inline_comments || strlen($comment)) { $xactions[] = id(new PholioTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_COMMENT) ->attachComment( id(new PholioTransactionComment()) ->setContent($comment)); } foreach ($inline_comments as $inline_comment) { $xactions[] = id(new PholioTransaction()) ->setTransactionType(PholioMockInlineTransaction::TRANSACTIONTYPE) ->attachComment($inline_comment); } $editor = id(new PholioMockEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect($request->isContinueRequest()) ->setIsPreview($is_preview); try { $xactions = $editor->applyTransactions($mock, $xactions); } catch (PhabricatorApplicationTransactionNoEffectException $ex) { return id(new PhabricatorApplicationTransactionNoEffectResponse()) ->setCancelURI($mock_uri) ->setException($ex); } if ($draft) { $draft->replaceOrDelete(); } if ($request->isAjax() && $is_preview) { return id(new PhabricatorApplicationTransactionResponse()) ->setObject($mock) ->setViewer($viewer) ->setTransactions($xactions) ->setIsPreview($is_preview); } else { return id(new AphrontRedirectResponse())->setURI($mock_uri); } } } diff --git a/src/applications/pholio/controller/PholioMockEditController.php b/src/applications/pholio/controller/PholioMockEditController.php index fd1df77a8e..6c0ea58d13 100644 --- a/src/applications/pholio/controller/PholioMockEditController.php +++ b/src/applications/pholio/controller/PholioMockEditController.php @@ -1,373 +1,373 @@ 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: %s', $mock->getName()); $is_new = false; - $mock_images = $mock->getImages(); + $mock_images = $mock->getActiveImages(); $files = mpull($mock_images, 'getFile'); $mock_images = mpull($mock_images, null, 'getFilePHID'); } else { $mock = PholioMock::initializeNewMock($viewer); $title = pht('Create Mock'); $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 = PholioMockNameTransaction::TRANSACTIONTYPE; $type_desc = PholioMockDescriptionTransaction::TRANSACTIONTYPE; $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; $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 = PholioImage::initializeNewImage() ->setAuthorPHID($viewer->getPHID()) ->setReplacesImagePHID($replaces_image_phid) ->setFilePhid($file_phid) ->attachFile($file) ->setName(strlen($title) ? $title : $file->getName()) ->setDescription($description) ->setSequence($sequence); $xactions[] = id(new PholioTransaction()) ->setTransactionType( PholioImageReplaceTransaction::TRANSACTIONTYPE) ->setNewValue($replace_image); $posted_mock_images[] = $replace_image; } else if (!$existing_image) { // this is an add $add_image = PholioImage::initializeNewImage() ->setAuthorPHID($viewer->getPHID()) ->setFilePhid($file_phid) ->attachFile($file) ->setName(strlen($title) ? $title : $file->getName()) ->setDescription($description) ->setSequence($sequence); $xactions[] = id(new PholioTransaction()) ->setTransactionType(PholioImageFileTransaction::TRANSACTIONTYPE) ->setNewValue( array('+' => array($add_image))); $posted_mock_images[] = $add_image; } else { $xactions[] = id(new PholioTransaction()) ->setTransactionType(PholioImageNameTransaction::TRANSACTIONTYPE) ->setNewValue( array($existing_image->getPHID() => $title)); $xactions[] = id(new PholioTransaction()) ->setTransactionType( PholioImageDescriptionTransaction::TRANSACTIONTYPE) ->setNewValue( array($existing_image->getPHID() => $description)); $xactions[] = id(new PholioTransaction()) ->setTransactionType( PholioImageSequenceTransaction::TRANSACTIONTYPE) ->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(PholioImageFileTransaction::TRANSACTIONTYPE) ->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( 'a', array( 'id' => $drop_id, 'class' => 'pholio-edit-drop', ), pht('Click here, or drag and drop images 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('Tags')) ->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) ->setFormErrors($errors) ->setBackground(PHUIObjectBoxView::WHITE_CONFIG) ->setForm($form); $crumbs = $this->buildApplicationCrumbs(); if (!$is_new) { $crumbs->addTextCrumb($mock->getMonogram(), '/'.$mock->getMonogram()); } $crumbs->addTextCrumb($title); $crumbs->setBorder(true); $view = id(new PHUITwoColumnView()) ->setFooter($form_box); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) ->addQuicksandConfig( array('mockEditConfig' => true)) ->appendChild($view); } } diff --git a/src/applications/pholio/query/PholioMockQuery.php b/src/applications/pholio/query/PholioMockQuery.php index 9e3154ec5c..465f23d34b 100644 --- a/src/applications/pholio/query/PholioMockQuery.php +++ b/src/applications/pholio/query/PholioMockQuery.php @@ -1,176 +1,174 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withAuthorPHIDs(array $author_phids) { $this->authorPHIDs = $author_phids; return $this; } public function withStatuses(array $statuses) { $this->statuses = $statuses; return $this; } public function needCoverFiles($need_cover_files) { $this->needCoverFiles = $need_cover_files; return $this; } public function needImages($need_images) { $this->needImages = $need_images; return $this; } public function needInlineComments($need_inline_comments) { $this->needInlineComments = $need_inline_comments; return $this; } public function needTokenCounts($need) { $this->needTokenCounts = $need; return $this; } public function newResultObject() { return new PholioMock(); } protected function loadPage() { - $mocks = $this->loadStandardPage(new PholioMock()); + $mocks = $this->loadStandardPage($this->newResultObject()); if ($mocks && $this->needImages) { self::loadImages($this->getViewer(), $mocks, $this->needInlineComments); } if ($mocks && $this->needCoverFiles) { $this->loadCoverFiles($mocks); } if ($mocks && $this->needTokenCounts) { $this->loadTokenCounts($mocks); } return $mocks; } protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { $where = parent::buildWhereClauseParts($conn); if ($this->ids !== null) { $where[] = qsprintf( $conn, 'mock.id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( $conn, 'mock.phid IN (%Ls)', $this->phids); } if ($this->authorPHIDs !== null) { $where[] = qsprintf( $conn, 'mock.authorPHID in (%Ls)', $this->authorPHIDs); } if ($this->statuses !== null) { $where[] = qsprintf( $conn, 'mock.status IN (%Ls)', $this->statuses); } return $where; } public static function loadImages( PhabricatorUser $viewer, array $mocks, $need_inline_comments) { assert_instances_of($mocks, 'PholioMock'); $mock_map = mpull($mocks, null, 'getPHID'); $all_images = id(new PholioImageQuery()) ->setViewer($viewer) ->setMockCache($mock_map) ->withMockPHIDs(array_keys($mock_map)) ->needInlineComments($need_inline_comments) ->execute(); $image_groups = mgroup($all_images, 'getMockPHID'); foreach ($mocks as $mock) { $mock_images = idx($image_groups, $mock->getPHID(), array()); - $mock->attachAllImages($mock_images); - $active_images = mfilter($mock_images, 'getIsObsolete', true); - $mock->attachImages(msort($active_images, 'getSequence')); + $mock->attachImages($mock_images); } } private function loadCoverFiles(array $mocks) { assert_instances_of($mocks, 'PholioMock'); $cover_file_phids = mpull($mocks, 'getCoverPHID'); $cover_files = id(new PhabricatorFileQuery()) ->setViewer($this->getViewer()) ->withPHIDs($cover_file_phids) ->execute(); $cover_files = mpull($cover_files, null, 'getPHID'); foreach ($mocks as $mock) { $file = idx($cover_files, $mock->getCoverPHID()); if (!$file) { $file = PhabricatorFile::loadBuiltin($this->getViewer(), 'missing.png'); } $mock->attachCoverFile($file); } } private function loadTokenCounts(array $mocks) { assert_instances_of($mocks, 'PholioMock'); $phids = mpull($mocks, 'getPHID'); $counts = id(new PhabricatorTokenCountQuery()) ->withObjectPHIDs($phids) ->execute(); foreach ($mocks as $mock) { $mock->attachTokenCount(idx($counts, $mock->getPHID(), 0)); } } public function getQueryApplicationClass() { return 'PhabricatorPholioApplication'; } protected function getPrimaryTableAlias() { return 'mock'; } } diff --git a/src/applications/pholio/query/PholioMockSearchEngine.php b/src/applications/pholio/query/PholioMockSearchEngine.php index 2433484d69..cbb175b2de 100644 --- a/src/applications/pholio/query/PholioMockSearchEngine.php +++ b/src/applications/pholio/query/PholioMockSearchEngine.php @@ -1,153 +1,153 @@ needCoverFiles(true) ->needImages(true) ->needTokenCounts(true); } protected function buildCustomSearchFields() { return array( id(new PhabricatorUsersSearchField()) ->setKey('authorPHIDs') ->setAliases(array('authors')) ->setLabel(pht('Authors')), id(new PhabricatorSearchCheckboxesField()) ->setKey('statuses') ->setLabel(pht('Status')) ->setOptions( id(new PholioMock()) ->getStatuses()), ); } protected function buildQueryFromParameters(array $map) { $query = $this->newQuery(); if ($map['authorPHIDs']) { $query->withAuthorPHIDs($map['authorPHIDs']); } if ($map['statuses']) { $query->withStatuses($map['statuses']); } return $query; } protected function getURI($path) { return '/pholio/'.$path; } protected function getBuiltinQueryNames() { $names = array( 'open' => pht('Open Mocks'), 'all' => pht('All Mocks'), ); if ($this->requireViewer()->isLoggedIn()) { $names['authored'] = pht('Authored'); } return $names; } public function buildSavedQueryFromBuiltin($query_key) { $query = $this->newSavedQuery(); $query->setQueryKey($query_key); switch ($query_key) { case 'open': return $query->setParameter( 'statuses', array('open')); case 'all': return $query; case 'authored': return $query->setParameter( 'authorPHIDs', array($this->requireViewer()->getPHID())); } return parent::buildSavedQueryFromBuiltin($query_key); } protected function renderResultList( array $mocks, PhabricatorSavedQuery $query, array $handles) { assert_instances_of($mocks, 'PholioMock'); $viewer = $this->requireViewer(); $handles = $viewer->loadHandles(mpull($mocks, 'getAuthorPHID')); $xform = PhabricatorFileTransform::getTransformByKey( PhabricatorFileThumbnailTransform::TRANSFORM_PINBOARD); $board = new PHUIPinboardView(); foreach ($mocks as $mock) { $image = $mock->getCoverFile(); $image_uri = $image->getURIForTransform($xform); list($x, $y) = $xform->getTransformedDimensions($image); $header = 'M'.$mock->getID().' '.$mock->getName(); $item = id(new PHUIPinboardItemView()) ->setUser($viewer) ->setHeader($header) ->setObject($mock) ->setURI('/M'.$mock->getID()) ->setImageURI($image_uri) ->setImageSize($x, $y) ->setDisabled($mock->isClosed()) - ->addIconCount('fa-picture-o', count($mock->getImages())) + ->addIconCount('fa-picture-o', count($mock->getActiveImages())) ->addIconCount('fa-trophy', $mock->getTokenCount()); if ($mock->getAuthorPHID()) { $author_handle = $handles[$mock->getAuthorPHID()]; $datetime = phabricator_date($mock->getDateCreated(), $viewer); $item->appendChild( pht('By %s on %s', $author_handle->renderLink(), $datetime)); } $board->addItem($item); } $result = new PhabricatorApplicationSearchResultView(); $result->setContent($board); return $result; } protected function getNewUserBody() { $create_button = id(new PHUIButtonView()) ->setTag('a') ->setText(pht('Create a Mock')) ->setHref('/pholio/create/') ->setColor(PHUIButtonView::GREEN); $icon = $this->getApplication()->getIcon(); $app_name = $this->getApplication()->getName(); $view = id(new PHUIBigInfoView()) ->setIcon($icon) ->setTitle(pht('Welcome to %s', $app_name)) ->setDescription( pht('Upload sets of images for review with revision history and '. 'inline comments.')) ->addAction($create_button); return $view; } } diff --git a/src/applications/pholio/storage/PholioMock.php b/src/applications/pholio/storage/PholioMock.php index a7e9a9b05d..0e61ffc2d1 100644 --- a/src/applications/pholio/storage/PholioMock.php +++ b/src/applications/pholio/storage/PholioMock.php @@ -1,291 +1,285 @@ 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(); } public function getURI() { return '/'.$this->getMonogram(); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text128', 'description' => 'text', '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'); + $images = mpull($images, null, 'getPHID'); + $images = msort($images, 'getSequence'); $this->images = $images; return $this; } public function getImages() { - $this->assertAttached($this->images); - return $this->images; + return $this->assertAttached($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 getActiveImages() { + $images = $this->getImages(); + + foreach ($images as $phid => $image) { + if ($image->getIsObsolete()) { + unset($images[$phid]); + } + } - public function getAllImages() { - $this->assertAttached($this->images); - return $this->allImages; + return $images; } 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 = $this->getImages(); $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."); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PholioMockEditor(); } public function getApplicationTransactionTemplate() { return new PholioTransaction(); } /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ public function getUsersToNotifyOfTokenGiven() { return array( $this->getAuthorPHID(), ); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $images = id(new PholioImageQuery()) ->setViewer($engine->getViewer()) ->withMockIDs(array($this->getPHID())) ->execute(); foreach ($images as $image) { $image->delete(); } $this->delete(); $this->saveTransaction(); } /* -( PhabricatorSpacesInterface )----------------------------------------- */ public function getSpacePHID() { return $this->spacePHID; } /* -( PhabricatorFulltextInterface )--------------------------------------- */ public function newFulltextEngine() { return new PholioMockFulltextEngine(); } /* -( PhabricatorFerretInterface )----------------------------------------- */ public function newFerretEngine() { return new PholioMockFerretEngine(); } /* -( PhabricatorTimelineInterace )---------------------------------------- */ public function newTimelineEngine() { return new PholioMockTimelineEngine(); } } diff --git a/src/applications/pholio/view/PholioMockEmbedView.php b/src/applications/pholio/view/PholioMockEmbedView.php index 88f2f2ac55..38b375b68b 100644 --- a/src/applications/pholio/view/PholioMockEmbedView.php +++ b/src/applications/pholio/view/PholioMockEmbedView.php @@ -1,63 +1,63 @@ mock = $mock; return $this; } public function setImages(array $images) { $this->images = $images; return $this; } public function render() { if (!$this->mock) { throw new PhutilInvalidStateException('setMock'); } $mock = $this->mock; $images_to_show = array(); $thumbnail = null; if (!empty($this->images)) { $images_to_show = array_intersect_key( - $this->mock->getImages(), array_flip($this->images)); + $this->mock->getActiveImages(), array_flip($this->images)); } $xform = PhabricatorFileTransform::getTransformByKey( PhabricatorFileThumbnailTransform::TRANSFORM_PINBOARD); if ($images_to_show) { $image = head($images_to_show); $thumbfile = $image->getFile(); $header = 'M'.$mock->getID().' '.$mock->getName(). ' (#'.$image->getID().')'; $uri = '/M'.$this->mock->getID().'/'.$image->getID().'/'; } else { $thumbfile = $mock->getCoverFile(); $header = 'M'.$mock->getID().' '.$mock->getName(); $uri = '/M'.$this->mock->getID(); } $thumbnail = $thumbfile->getURIForTransform($xform); list($x, $y) = $xform->getTransformedDimensions($thumbfile); $item = id(new PHUIPinboardItemView()) ->setUser($this->getUser()) ->setObject($mock) ->setHeader($header) ->setURI($uri) ->setImageURI($thumbnail) ->setImageSize($x, $y) ->setDisabled($mock->isClosed()) - ->addIconCount('fa-picture-o', count($mock->getImages())) + ->addIconCount('fa-picture-o', count($mock->getActiveImages())) ->addIconCount('fa-trophy', $mock->getTokenCount()); return $item; } } diff --git a/src/applications/pholio/view/PholioMockImagesView.php b/src/applications/pholio/view/PholioMockImagesView.php index d5ac41cd03..70f5fe8bb3 100644 --- a/src/applications/pholio/view/PholioMockImagesView.php +++ b/src/applications/pholio/view/PholioMockImagesView.php @@ -1,223 +1,223 @@ commentFormID = $comment_form_id; return $this; } public function getCommentFormID() { return $this->commentFormID; } public function setRequestURI(PhutilURI $request_uri) { $this->requestURI = $request_uri; return $this; } public function getRequestURI() { return $this->requestURI; } public function setImageID($image_id) { $this->imageID = $image_id; return $this; } public function getImageID() { return $this->imageID; } public function setMock(PholioMock $mock) { $this->mock = $mock; return $this; } public function getMock() { return $this->mock; } public function __construct() { $this->panelID = celerity_generate_unique_node_id(); $this->viewportID = celerity_generate_unique_node_id(); } public function getBehaviorConfig() { if (!$this->getMock()) { throw new PhutilInvalidStateException('setMock'); } if ($this->behaviorConfig === null) { $this->behaviorConfig = $this->calculateBehaviorConfig(); } return $this->behaviorConfig; } private function calculateBehaviorConfig() { $mock = $this->getMock(); // TODO: We could maybe do a better job with tailoring this, which is the // image shown on the review stage. $viewer = $this->getUser(); $default = PhabricatorFile::loadBuiltin($viewer, 'image-100x100.png'); $images = array(); $current_set = 0; - foreach ($mock->getAllImages() as $image) { + foreach ($mock->getImages() as $image) { $file = $image->getFile(); $metadata = $file->getMetadata(); $x = idx($metadata, PhabricatorFile::METADATA_IMAGE_WIDTH); $y = idx($metadata, PhabricatorFile::METADATA_IMAGE_HEIGHT); $is_obs = (bool)$image->getIsObsolete(); if (!$is_obs) { $current_set++; } $description = $image->getDescription(); if (strlen($description)) { $description = new PHUIRemarkupView($viewer, $description); } $history_uri = '/pholio/image/history/'.$image->getID().'/'; $images[] = array( 'id' => $image->getID(), 'fullURI' => $file->getBestURI(), 'stageURI' => ($file->isViewableImage() ? $file->getBestURI() : $default->getBestURI()), 'pageURI' => $this->getImagePageURI($image, $mock), 'downloadURI' => $file->getDownloadURI(), 'historyURI' => $history_uri, 'width' => $x, 'height' => $y, 'title' => $image->getName(), 'descriptionMarkup' => $description, 'isObsolete' => (bool)$image->getIsObsolete(), 'isImage' => $file->isViewableImage(), 'isViewable' => $file->isViewableInBrowser(), ); } - $ids = mpull($mock->getImages(), 'getID'); + $ids = mpull($mock->getActiveImages(), 'getID'); if ($this->imageID && isset($ids[$this->imageID])) { $selected_id = $this->imageID; } else { $selected_id = head_key($ids); } $navsequence = array(); - foreach ($mock->getImages() as $image) { + foreach ($mock->getActiveImages() as $image) { $navsequence[] = $image->getID(); } $full_icon = array( javelin_tag('span', array('aural' => true), pht('View Raw File')), id(new PHUIIconView())->setIcon('fa-file-image-o'), ); $download_icon = array( javelin_tag('span', array('aural' => true), pht('Download File')), id(new PHUIIconView())->setIcon('fa-download'), ); $login_uri = id(new PhutilURI('/login/')) ->setQueryParam('next', (string)$this->getRequestURI()); $config = array( 'mockID' => $mock->getID(), 'panelID' => $this->panelID, 'viewportID' => $this->viewportID, 'commentFormID' => $this->getCommentFormID(), 'images' => $images, 'selectedID' => $selected_id, 'loggedIn' => $this->getUser()->isLoggedIn(), 'logInLink' => (string)$login_uri, 'navsequence' => $navsequence, 'fullIcon' => hsprintf('%s', $full_icon), 'downloadIcon' => hsprintf('%s', $download_icon), 'currentSetSize' => $current_set, ); return $config; } public function render() { if (!$this->getMock()) { throw new PhutilInvalidStateException('setMock'); } $mock = $this->getMock(); require_celerity_resource('javelin-behavior-pholio-mock-view'); $panel_id = $this->panelID; $viewport_id = $this->viewportID; $config = $this->getBehaviorConfig(); Javelin::initBehavior( 'pholio-mock-view', $this->getBehaviorConfig()); $mock_wrapper = javelin_tag( 'div', array( 'id' => $this->viewportID, 'sigil' => 'mock-viewport', 'class' => 'pholio-mock-image-viewport', ), ''); $image_header = javelin_tag( 'div', array( 'id' => 'mock-image-header', 'class' => 'pholio-mock-image-header', ), ''); $mock_wrapper = javelin_tag( 'div', array( 'id' => $this->panelID, 'sigil' => 'mock-panel touchable', 'class' => 'pholio-mock-image-panel', ), array( $image_header, $mock_wrapper, )); $inline_comments_holder = javelin_tag( 'div', array( 'id' => 'mock-image-description', 'sigil' => 'mock-image-description', 'class' => 'mock-image-description', ), ''); return phutil_tag( 'div', array( 'class' => 'pholio-mock-image-container', 'id' => 'pholio-mock-image-container', ), array($mock_wrapper, $inline_comments_holder)); } private function getImagePageURI(PholioImage $image, PholioMock $mock) { $uri = '/M'.$mock->getID().'/'.$image->getID().'/'; return $uri; } } diff --git a/src/applications/pholio/view/PholioMockThumbGridView.php b/src/applications/pholio/view/PholioMockThumbGridView.php index 457d700f1f..b859eaaa9a 100644 --- a/src/applications/pholio/view/PholioMockThumbGridView.php +++ b/src/applications/pholio/view/PholioMockThumbGridView.php @@ -1,181 +1,181 @@ mock = $mock; return $this; } public function render() { $mock = $this->mock; - $all_images = $mock->getAllImages(); + $all_images = $mock->getImages(); $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'); + $images = mpull($mock->getActiveImages(), null, 'getPHID'); // Now, find deleted images: obsolete images which were not replaced. - foreach ($mock->getAllImages() as $image) { + foreach ($mock->getImages() 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, )); } } diff --git a/src/applications/pholio/view/PholioTransactionView.php b/src/applications/pholio/view/PholioTransactionView.php index 69613c5428..1126db9c85 100644 --- a/src/applications/pholio/view/PholioTransactionView.php +++ b/src/applications/pholio/view/PholioTransactionView.php @@ -1,141 +1,141 @@ mock = $mock; return $this; } public function getMock() { return $this->mock; } protected function shouldGroupTransactions( PhabricatorApplicationTransaction $u, PhabricatorApplicationTransaction $v) { if ($u->getAuthorPHID() != $v->getAuthorPHID()) { // Don't group transactions by different authors. return false; } if (($v->getDateCreated() - $u->getDateCreated()) > 60) { // Don't group if transactions happened more than 60s apart. return false; } switch ($u->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: case PholioMockInlineTransaction::TRANSACTIONTYPE: break; default: return false; } switch ($v->getTransactionType()) { case PholioMockInlineTransaction::TRANSACTIONTYPE: return true; } return parent::shouldGroupTransactions($u, $v); } protected function renderTransactionContent( PhabricatorApplicationTransaction $xaction) { $out = array(); $group = $xaction->getTransactionGroup(); $type = $xaction->getTransactionType(); if ($type == PholioMockInlineTransaction::TRANSACTIONTYPE) { array_unshift($group, $xaction); } else { $out[] = parent::renderTransactionContent($xaction); } if (!$group) { return $out; } $inlines = array(); foreach ($group as $xaction) { switch ($xaction->getTransactionType()) { case PholioMockInlineTransaction::TRANSACTIONTYPE: $inlines[] = $xaction; break; default: throw new Exception(pht('Unknown grouped transaction type!')); } } if ($inlines) { $icon = id(new PHUIIconView()) ->setIcon('fa-comment bluegrey msr'); $header = phutil_tag( 'div', array( 'class' => 'phabricator-transaction-subheader', ), array($icon, pht('Inline Comments'))); $out[] = $header; foreach ($inlines as $inline) { if (!$inline->getComment()) { continue; } $out[] = $this->renderInlineContent($inline); } } return $out; } private function renderInlineContent(PholioTransaction $inline) { $comment = $inline->getComment(); $mock = $this->getMock(); - $images = $mock->getAllImages(); + $images = $mock->getImages(); $images = mpull($images, null, 'getID'); $image = idx($images, $comment->getImageID()); if (!$image) { throw new Exception(pht('No image attached!')); } $file = $image->getFile(); if (!$file->isViewableImage()) { throw new Exception(pht('File is not viewable.')); } $image_uri = $file->getBestURI(); $thumb = id(new PHUIImageMaskView()) ->addClass('mrl') ->setImage($image_uri) ->setDisplayHeight(100) ->setDisplayWidth(200) ->withMask(true) ->centerViewOnPoint( $comment->getX(), $comment->getY(), $comment->getHeight(), $comment->getWidth()); $link = phutil_tag( 'a', array( 'href' => '#', 'class' => 'pholio-transaction-inline-image-anchor', ), $thumb); $inline_comment = parent::renderTransactionContent($inline); return phutil_tag( 'div', array('class' => 'pholio-transaction-inline-comment'), array($link, $inline_comment)); } } diff --git a/src/applications/pholio/xaction/PholioImageFileTransaction.php b/src/applications/pholio/xaction/PholioImageFileTransaction.php index 5f68dad9f1..6d257c313e 100644 --- a/src/applications/pholio/xaction/PholioImageFileTransaction.php +++ b/src/applications/pholio/xaction/PholioImageFileTransaction.php @@ -1,120 +1,120 @@ getImages(); + $images = $object->getActiveImages(); return array_values(mpull($images, 'getPHID')); } public function generateNewValue($object, $value) { $new_value = array(); foreach ($value as $key => $images) { $new_value[$key] = mpull($images, 'getPHID'); } $old = array_fuse($this->getOldValue()); return $this->getEditor()->getPHIDList($old, $new_value); } public function applyInternalEffects($object, $value) { $old_map = array_fuse($this->getOldValue()); $new_map = array_fuse($this->getNewValue()); $obsolete_map = array_diff_key($old_map, $new_map); - $images = $object->getImages(); + $images = $object->getActiveImages(); foreach ($images as $seq => $image) { if (isset($obsolete_map[$image->getPHID()])) { $image->setIsObsolete(1); $image->save(); unset($images[$seq]); } } $object->attachImages($images); } public function getTitle() { $old = $this->getOldValue(); $new = $this->getNewValue(); $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->renderAuthor(), count($add), $this->renderHandleList($add), count($rem), $this->renderHandleList($rem)); } else if ($add) { return pht( '%s added %d image(s): %s.', $this->renderAuthor(), count($add), $this->renderHandleList($add)); } else { return pht( '%s removed %d image(s): %s.', $this->renderAuthor(), count($rem), $this->renderHandleList($rem)); } } public function getTitleForFeed() { $old = $this->getOldValue(); $new = $this->getNewValue(); return pht( '%s updated images of %s.', $this->renderAuthor(), $this->renderObject()); } public function getIcon() { return 'fa-picture-o'; } public function getColor() { $old = $this->getOldValue(); $new = $this->getNewValue(); $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; } } public function extractFilePHIDs($object, $value) { $images = $this->getEditor()->getNewImages(); $images = mpull($images, null, 'getPHID'); $file_phids = array(); foreach ($value as $image_phid) { $image = idx($images, $image_phid); if (!$image) { continue; } $file_phids[] = $image->getFilePHID(); } return $file_phids; } public function mergeTransactions( $object, PhabricatorApplicationTransaction $u, PhabricatorApplicationTransaction $v) { return $this->getEditor()->mergePHIDOrEdgeTransactions($u, $v); } }