diff --git a/resources/sql/patches/pholio.sql b/resources/sql/patches/pholio.sql new file mode 100644 index 0000000000..9dc443dd77 --- /dev/null +++ b/resources/sql/patches/pholio.sql @@ -0,0 +1,74 @@ +CREATE TABLE {$NAMESPACE}_pholio.pholio_mock ( + id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, + phid VARCHAR(64) NOT NULL COLLATE utf8_bin, + name VARCHAR(128) NOT NULL COLLATE utf8_general_ci, + originalName VARCHAR(128) NOT NULL COLLATE utf8_general_ci, + description LONGTEXT NOT NULL COLLATE utf8_general_ci, + authorPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, + viewPolicy VARCHAR(64) NOT NULL COLLATE utf8_bin, + coverPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, + mailKey VARCHAR(20) NOT NULL COLLATE utf8_bin, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY (phid), + KEY (authorPHID) +) ENGINE=InnoDB, COLLATE utf8_general_ci; + +CREATE TABLE {$NAMESPACE}_pholio.edge ( + src VARCHAR(64) NOT NULL COLLATE utf8_bin, + type VARCHAR(64) NOT NULL COLLATE utf8_bin, + dst VARCHAR(64) NOT NULL COLLATE utf8_bin, + dateCreated INT UNSIGNED NOT NULL, + seq INT UNSIGNED NOT NULL, + dataID INT UNSIGNED, + PRIMARY KEY (src, type, dst), + KEY (src, type, dateCreated, seq) +) ENGINE=InnoDB, COLLATE utf8_general_ci; + +CREATE TABLE {$NAMESPACE}_pholio.edgedata ( + id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, + data LONGTEXT NOT NULL COLLATE utf8_bin +) ENGINE=InnoDB, COLLATE utf8_general_ci; + +CREATE TABLE {$NAMESPACE}_pholio.pholio_transaction ( + id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, + authorPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, + mockID INT UNSIGNED NOT NULL, + transactionType VARCHAR(32) NOT NULL COLLATE utf8_bin, + oldValue LONGTEXT NOT NULL COLLATE utf8_bin, + newValue LONGTEXT NOT NULL COLLATE utf8_bin, + comment LONGTEXT NOT NULL COLLATE utf8_general_ci, + metadata LONGTEXT NOT NULL COLLATE utf8_bin, + contentSource LONGTEXT NOT NULL COLLATE utf8_bin, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL +) ENGINE=InnoDB, COLLATE utf8_general_ci; + +CREATE TABLE {$NAMESPACE}_pholio.pholio_image ( + id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, + mockID INT UNSIGNED NOT NULL, + filePHID VARCHAR(64) NOT NULL COLLATE utf8_bin, + name VARCHAR(128) NOT NULL COLLATE utf8_general_ci, + description LONGTEXT NOT NULL COLLATE utf8_general_ci, + sequence INT UNSIGNED NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + KEY (mockID, sequence) +) ENGINE=InnoDB, COLLATE utf8_general_ci; + +CREATE TABLE {$NAMESPACE}_pholio.pholio_pixelcomment ( + id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, + mockID INT UNSIGNED NOT NULL, + imageID INT UNSIGNED NOT NULL, + transactionID INT UNSIGNED, + authorPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, + x INT UNSIGNED NOT NULL, + y INT UNSIGNED NOT NULL, + width INT UNSIGNED NOT NULL, + height INT UNSIGNED NOT NULL, + comment LONGTEXT NOT NULL COLLATE utf8_general_ci, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + KEY (mockID), + KEY (authorPHID, transactionID) +) ENGINE=InnoDB, COLLATE utf8_general_ci; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 0fef1c616f..c03a983b6e 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -582,6 +582,7 @@ phutil_register_library_map(array( 'PhabricatorApplicationPaste' => 'applications/paste/application/PhabricatorApplicationPaste.php', 'PhabricatorApplicationPeople' => 'applications/people/application/PhabricatorApplicationPeople.php', 'PhabricatorApplicationPhame' => 'applications/phame/application/PhabricatorApplicationPhame.php', + 'PhabricatorApplicationPholio' => 'applications/pholio/application/PhabricatorApplicationPholio.php', 'PhabricatorApplicationPhriction' => 'applications/phriction/application/PhabricatorApplicationPhriction.php', 'PhabricatorApplicationPonder' => 'applications/ponder/application/PhabricatorApplicationPonder.php', 'PhabricatorApplicationProject' => 'applications/project/application/PhabricatorApplicationProject.php', @@ -1199,6 +1200,17 @@ phutil_register_library_map(array( 'PhamePostViewController' => 'applications/phame/controller/post/PhamePostViewController.php', 'PhameResourceController' => 'applications/phame/controller/PhameResourceController.php', 'PhameSkinSpecification' => 'applications/phame/skins/PhameSkinSpecification.php', + 'PholioController' => 'applications/pholio/controller/PholioController.php', + 'PholioDAO' => 'applications/pholio/storage/PholioDAO.php', + 'PholioImage' => 'applications/pholio/storage/PholioImage.php', + 'PholioIndexer' => 'applications/pholio/indexer/PholioIndexer.php', + 'PholioMock' => 'applications/pholio/storage/PholioMock.php', + 'PholioMockEditor' => 'applications/pholio/editor/PholioMockEditor.php', + 'PholioMockListController' => 'applications/pholio/controller/PholioMockListController.php', + 'PholioMockQuery' => 'applications/pholio/query/PholioMockQuery.php', + 'PholioMockViewController' => 'applications/pholio/controller/PholioMockViewController.php', + 'PholioPixelComment' => 'applications/pholio/storage/PholioPixelComment.php', + 'PholioTransaction' => 'applications/pholio/storage/PholioTransaction.php', 'PhortuneMonthYearExpiryControl' => 'applications/phortune/control/PhortuneMonthYearExpiryControl.php', 'PhortuneStripeBaseController' => 'applications/phortune/stripe/controller/PhortuneStripeBaseController.php', 'PhortuneStripePaymentFormView' => 'applications/phortune/stripe/view/PhortuneStripePaymentFormView.php', @@ -1799,6 +1811,7 @@ phutil_register_library_map(array( 'PhabricatorApplicationPaste' => 'PhabricatorApplication', 'PhabricatorApplicationPeople' => 'PhabricatorApplication', 'PhabricatorApplicationPhame' => 'PhabricatorApplication', + 'PhabricatorApplicationPholio' => 'PhabricatorApplication', 'PhabricatorApplicationPhriction' => 'PhabricatorApplication', 'PhabricatorApplicationPonder' => 'PhabricatorApplication', 'PhabricatorApplicationProject' => 'PhabricatorApplication', @@ -2377,6 +2390,35 @@ phutil_register_library_map(array( 'PhamePostView' => 'AphrontView', 'PhamePostViewController' => 'PhameController', 'PhameResourceController' => 'CelerityResourceController', + 'PholioController' => 'PhabricatorController', + 'PholioDAO' => 'PhabricatorLiskDAO', + 'PholioImage' => + array( + 0 => 'PholioDAO', + 1 => 'PhabricatorMarkupInterface', + ), + 'PholioIndexer' => 'PhabricatorSearchDocumentIndexer', + 'PholioMock' => + array( + 0 => 'PholioDAO', + 1 => 'PhabricatorMarkupInterface', + 2 => 'PhabricatorPolicyInterface', + 3 => 'PhabricatorSubscribableInterface', + ), + 'PholioMockEditor' => 'PhabricatorEditor', + 'PholioMockListController' => 'PholioController', + 'PholioMockQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PholioMockViewController' => 'PholioController', + 'PholioPixelComment' => + array( + 0 => 'PholioDAO', + 1 => 'PhabricatorMarkupInterface', + ), + 'PholioTransaction' => + array( + 0 => 'PholioDAO', + 1 => 'PhabricatorMarkupInterface', + ), 'PhortuneMonthYearExpiryControl' => 'AphrontFormControl', 'PhortuneStripeBaseController' => 'PhabricatorController', 'PhortuneStripePaymentFormView' => 'AphrontView', diff --git a/src/applications/phid/PhabricatorPHIDConstants.php b/src/applications/phid/PhabricatorPHIDConstants.php index e1926fca49..c89896381d 100644 --- a/src/applications/phid/PhabricatorPHIDConstants.php +++ b/src/applications/phid/PhabricatorPHIDConstants.php @@ -28,4 +28,6 @@ final class PhabricatorPHIDConstants { const PHID_TYPE_BLOG = 'BLOG'; const PHID_TYPE_QUES = 'QUES'; const PHID_TYPE_ANSW = 'ANSW'; + const PHID_TYPE_MOCK = 'MOCK'; + } diff --git a/src/applications/phid/handle/PhabricatorObjectHandleData.php b/src/applications/phid/handle/PhabricatorObjectHandleData.php index aac83ecf40..eb0db7707b 100644 --- a/src/applications/phid/handle/PhabricatorObjectHandleData.php +++ b/src/applications/phid/handle/PhabricatorObjectHandleData.php @@ -88,12 +88,22 @@ final class PhabricatorObjectHandleData { break; case PhabricatorPHIDConstants::PHID_TYPE_QUES: $questions = id(new PonderQuestionQuery()) + ->setViewer($this->viewer) ->withPHIDs($phids) ->execute(); foreach ($questions as $question) { $objects[$question->getPHID()] = $question; } break; + case PhabricatorPHIDConstants::PHID_TYPE_MOCK: + $mocks = id(new PholioMockQuery()) + ->setViewer($this->viewer) + ->withPHIDs($phids) + ->execute(); + foreach ($mocks as $mock) { + $objects[$mock->getPHID()] = $mock; + } + break; } } @@ -546,6 +556,29 @@ final class PhabricatorObjectHandleData { $handles[$phid] = $handle; } break; + case PhabricatorPHIDConstants::PHID_TYPE_MOCK: + $mocks = id(new PholioMockQuery()) + ->withPHIDs($phids) + ->setViewer($this->viewer) + ->execute(); + $mocks = mpull($mocks, null, 'getPHID'); + + foreach ($phids as $phid) { + $handle = new PhabricatorObjectHandle(); + $handle->setPHID($phid); + $handle->setType($type); + if (empty($mocks[$phid])) { + $handle->setName('Unknown Mock'); + } else { + $mock = $mocks[$phid]; + $handle->setName($mock->getName()); + $handle->setFullName($mock->getName()); + $handle->setURI('/M'.$mock->getID()); + $handle->setComplete(true); + } + $handles[$phid] = $handle; + } + break; default: $loader = null; if (isset($external_loaders[$type])) { diff --git a/src/applications/pholio/application/PhabricatorApplicationPholio.php b/src/applications/pholio/application/PhabricatorApplicationPholio.php new file mode 100644 index 0000000000..02e5461c90 --- /dev/null +++ b/src/applications/pholio/application/PhabricatorApplicationPholio.php @@ -0,0 +1,63 @@ +[1-9]\d*)' => 'PholioMockViewController', + '/pholio/' => array( + '' => 'PholioMockListController', + ), + ); + } + +} diff --git a/src/applications/pholio/controller/PholioController.php b/src/applications/pholio/controller/PholioController.php new file mode 100644 index 0000000000..491d5ee3f2 --- /dev/null +++ b/src/applications/pholio/controller/PholioController.php @@ -0,0 +1,25 @@ +getRequest(); + $user = $request->getUser(); + + $query = id(new PholioMockQuery()) + ->setViewer($user); + + $title = 'All Mocks'; + + $pager = new AphrontCursorPagerView(); + $pager->readFromRequest($request); + + $mocks = $query->executeWithCursorPager($pager); + + $board = new PhabricatorPinboardView(); + foreach ($mocks as $mock) { + $board->addItem( + id(new PhabricatorPinboardItemView()) + ->setHeader($mock->getName()) + ->setURI('/M'.$mock->getID())); + } + + $header = id(new PhabricatorHeaderView()) + ->setHeader($title); + + $content = array( + $header, + $board, + $pager, + ); + + return $this->buildApplicationPage( + $content, + array( + 'title' => $title, + )); + } + +} diff --git a/src/applications/pholio/controller/PholioMockViewController.php b/src/applications/pholio/controller/PholioMockViewController.php new file mode 100644 index 0000000000..5372ccc706 --- /dev/null +++ b/src/applications/pholio/controller/PholioMockViewController.php @@ -0,0 +1,67 @@ +id = $data['id']; + } + + public function processRequest() { + $request = $this->getRequest(); + $user = $request->getUser(); + + $mock = id(new PholioMockQuery()) + ->setViewer($user) + ->withIDs(array($this->id)) + ->executeOne(); + + if (!$mock) { + return new Aphront404Response(); + } + + $title = 'M'.$mock->getID().' '.$mock->getName(); + + $header = id(new PhabricatorHeaderView()) + ->setHeader($title); + + $actions = id(new PhabricatorActionListView()) + ->setUser($user) + ->setObject($mock); + + $properties = new PhabricatorPropertyListView(); + + $content = array( + $header, + $actions, + $properties + ); + + return $this->buildApplicationPage( + $content, + array( + 'title' => $title, + )); + } + +} diff --git a/src/applications/pholio/editor/PholioMockEditor.php b/src/applications/pholio/editor/PholioMockEditor.php new file mode 100644 index 0000000000..7d037a2a03 --- /dev/null +++ b/src/applications/pholio/editor/PholioMockEditor.php @@ -0,0 +1,26 @@ +setPHID($mock->getPHID()); + $doc->setDocumentType(phid_get_type($mock->getPHID())); + $doc->setDocumentTitle($mock->getName()); + $doc->setDocumentCreated($mock->getDateCreated()); + $doc->setDocumentModified($mock->getDateModified()); + + $doc->addField( + PhabricatorSearchField::FIELD_BODY, + $mock->getDescription()); + + $doc->addRelationship( + PhabricatorSearchRelationship::RELATIONSHIP_AUTHOR, + $mock->getAuthorPHID(), + PhabricatorPHIDConstants::PHID_TYPE_USER, + $mock->getDateCreated()); + + self::reindexAbstractDocument($doc); + } +} diff --git a/src/applications/pholio/query/PholioMockQuery.php b/src/applications/pholio/query/PholioMockQuery.php new file mode 100644 index 0000000000..1964a1959a --- /dev/null +++ b/src/applications/pholio/query/PholioMockQuery.php @@ -0,0 +1,88 @@ +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 loadPage() { + $table = new PholioMock(); + $conn_r = $table->establishConnection('r'); + + $data = queryfx_all( + $conn_r, + 'SELECT * FROM %T %Q %Q %Q', + $table->getTableName(), + $this->buildWhereClause($conn_r), + $this->buildOrderClause($conn_r), + $this->buildLimitClause($conn_r)); + + return $table->loadAllFromArray($data); + } + + private function buildWhereClause(AphrontDatabaseConnection $conn_r) { + $where = array(); + + $where[] = $this->buildPagingClause($conn_r); + + if ($this->ids) { + $where[] = qsprintf( + $conn_r, + 'id IN (%Ld)', + $this->ids); + } + + if ($this->phids) { + $where[] = qsprintf( + $conn_r, + 'phid IN (%Ls)', + $this->phids); + } + + if ($this->authorPHIDs) { + $where[] = qsprintf( + $conn_r, + 'authorPHID in (%Ls)', + $this->authorPHIDs); + } + + return $this->formatWhereClause($where); + } + +} diff --git a/src/applications/pholio/storage/PholioDAO.php b/src/applications/pholio/storage/PholioDAO.php new file mode 100644 index 0000000000..ba49bc7479 --- /dev/null +++ b/src/applications/pholio/storage/PholioDAO.php @@ -0,0 +1,28 @@ +getMarkupText($field)); + return 'M:'.$hash; + } + + public function newMarkupEngine($field) { + return PhabricatorMarkupEngine::newMarkupEngine(array()); + } + + public function getMarkupText($field) { + return $this->getDescription(); + } + + public function didMarkupText($field, $output, PhutilMarkupEngine $engine) { + return $output; + } + + public function shouldUseMarkupCache($field) { + return (bool)$this->getID(); + } + +} diff --git a/src/applications/pholio/storage/PholioMock.php b/src/applications/pholio/storage/PholioMock.php new file mode 100644 index 0000000000..3e8274a5d4 --- /dev/null +++ b/src/applications/pholio/storage/PholioMock.php @@ -0,0 +1,113 @@ + true, + ) + parent::getConfiguration(); + } + + public function generatePHID() { + return PhabricatorPHID::generateNewPHID('MOCK'); + } + + public function save() { + if (!$this->getMailKey()) { + $this->setMailKey(Filesystem::readRandomCharacters(20)); + } + return parent::save(); + } + + +/* -( 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 PhabricatorPolicies::POLICY_NOONE; + } + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + return ($viewer->getPHID() == $this->getAuthorPHID()); + } + + +/* -( 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) { + return $this->getDescription(); + } + + public function didMarkupText($field, $output, PhutilMarkupEngine $engine) { + return $output; + } + + public function shouldUseMarkupCache($field) { + return (bool)$this->getID(); + } + +} diff --git a/src/applications/pholio/storage/PholioPixelComment.php b/src/applications/pholio/storage/PholioPixelComment.php new file mode 100644 index 0000000000..3a2b235640 --- /dev/null +++ b/src/applications/pholio/storage/PholioPixelComment.php @@ -0,0 +1,62 @@ +getID(); + } + + public function newMarkupEngine($field) { + return PhabricatorMarkupEngine::newMarkupEngine(array()); + } + + public function getMarkupText($field) { + return $this->getComment(); + } + + public function didMarkupText($field, $output, PhutilMarkupEngine $engine) { + return $output; + } + + public function shouldUseMarkupCache($field) { + return ($this->getID() && $this->getTransactionID()); + } + +} diff --git a/src/applications/pholio/storage/PholioTransaction.php b/src/applications/pholio/storage/PholioTransaction.php new file mode 100644 index 0000000000..01937c2ed2 --- /dev/null +++ b/src/applications/pholio/storage/PholioTransaction.php @@ -0,0 +1,111 @@ + array( + 'oldValue' => self::SERIALIZATION_JSON, + 'newValue' => self::SERIALIZATION_JSON, + 'metadata' => self::SERIALIZATION_JSON, + ), + ) + parent::getConfiguration(); + } + + public function setContentSource(PhabricatorContentSource $content_source) { + $this->contentSource = $content_source->serialize(); + return $this; + } + + public function getContentSource() { + return PhabricatorContentSource::newFromSerialized($this->contentSource); + } + + +/* -( 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 PhabricatorPolicies::POLICY_NOONE; + } + } + + public function hasAutomaticCapbility($capability, PhabricatorUser $viewer) { + return ($viewer->getPHID() == $this->getAuthorPHID()); + } + + +/* -( PhabricatorMarkupInterface )----------------------------------------- */ + + + public function getMarkupFieldKey($field) { + return 'MX:'.$this->getID(); + } + + public function newMarkupEngine($field) { + return PhabricatorMarkupEngine::newMarkupEngine(array()); + } + + public function getMarkupText($field) { + return $this->getComment(); + } + + public function didMarkupText($field, $output, PhutilMarkupEngine $engine) { + return $output; + } + + public function shouldUseMarkupCache($field) { + return (bool)$this->getID(); + } + +} diff --git a/src/infrastructure/edges/constants/PhabricatorEdgeConfig.php b/src/infrastructure/edges/constants/PhabricatorEdgeConfig.php index 1aff2cfdb7..558349fae7 100644 --- a/src/infrastructure/edges/constants/PhabricatorEdgeConfig.php +++ b/src/infrastructure/edges/constants/PhabricatorEdgeConfig.php @@ -107,6 +107,8 @@ final class PhabricatorEdgeConfig extends PhabricatorEdgeConstants { PhabricatorPHIDConstants::PHID_TYPE_POST => 'PhamePost', PhabricatorPHIDConstants::PHID_TYPE_QUES => 'PonderQuestion', PhabricatorPHIDConstants::PHID_TYPE_ANSW => 'PonderAnswer', + PhabricatorPHIDConstants::PHID_TYPE_MOCK => 'PholioMock', + ); $class = idx($class_map, $phid_type); diff --git a/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php b/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php index 78f3fd8ac1..6490f76b59 100644 --- a/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php +++ b/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php @@ -155,6 +155,10 @@ final class PhabricatorBuiltinPatchList extends PhabricatorSQLPatchList { 'type' => 'db', 'name' => 'xhprof', ), + 'db.pholio' => array( + 'type' => 'db', + 'name' => 'pholio', + ), '0000.legacy.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('0000.legacy.sql'), @@ -1032,6 +1036,10 @@ final class PhabricatorBuiltinPatchList extends PhabricatorSQLPatchList { 'type' => 'sql', 'name' => $this->getPatchPath('liskcounters-task.sql'), ), + 'pholio.sql' => array( + 'type' => 'sql', + 'name' => $this->getPatchPath('pholio.sql'), + ), ); } diff --git a/src/view/layout/PhabricatorPinboardView.php b/src/view/layout/PhabricatorPinboardView.php index 201e775c4d..62bed4eb32 100644 --- a/src/view/layout/PhabricatorPinboardView.php +++ b/src/view/layout/PhabricatorPinboardView.php @@ -4,7 +4,7 @@ final class PhabricatorPinboardView extends AphrontView { private $items = array(); - public function addItem(PhabricatorPinBoardItemView $item) { + public function addItem(PhabricatorPinboardItemView $item) { $this->items[] = $item; return $this; }