From 90df4b2bd15327fcf6ec79e460e66478ab6e1286 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 29 Mar 2019 09:46:17 -0700 Subject: [PATCH] Add skeleton for Portals, a collection of dashboards and other resources Summary: Ref T13275. Today, you can build a custom page on the home page, on project pages, and in your favorites menu. PHI374 would approximately like to build a completely standalone custom page, and this generally seems like a reasonable capability which we should support, and which should be easy to support if the "custom menu" stuff is built right. In the near future, I'm planning to shore up some of the outstanding issues with profile menus and then build charts (which will have a big dashboard/panel component), so adding Portals now should let me double up on a lot of the testing and maybe make some of it a bit easier. Test Plan: Viewed the list of portals, created a new portal. Everything is currently a pure skeleton with no unique behavior. Here's a glorious portal page: {F6321846} Reviewers: amckinley Reviewed By: amckinley Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T13275 Differential Revision: https://secure.phabricator.com/D20348 --- .../20190329.portals.01.create.sql | 11 ++ .../20190329.portals.02.xaction.sql | 19 ++++ src/__phutil_library_map__.php | 37 +++++++ .../PhabricatorDashboardApplication.php | 8 ++ ...torDashboardPortalEditConduitAPIMethod.php | 19 ++++ ...rDashboardPortalSearchConduitAPIMethod.php | 18 +++ .../PhabricatorDashboardPortalStatus.php | 9 ++ .../PhabricatorDashboardPortalController.php | 14 +++ ...abricatorDashboardPortalEditController.php | 12 ++ ...abricatorDashboardPortalListController.php | 26 +++++ ...abricatorDashboardPortalViewController.php | 34 ++++++ .../PhabricatorDashboardPortalEditEngine.php | 83 ++++++++++++++ .../PhabricatorDashboardPortalEditor.php | 31 ++++++ .../PhabricatorDashboardPortalPHIDType.php | 42 +++++++ .../query/PhabricatorDashboardPortalQuery.php | 64 +++++++++++ ...PhabricatorDashboardPortalSearchEngine.php | 78 +++++++++++++ .../storage/PhabricatorDashboardPortal.php | 104 ++++++++++++++++++ .../PhabricatorDashboardPortalTransaction.php | 18 +++ ...bricatorDashboardPortalNameTransaction.php | 70 ++++++++++++ ...bricatorDashboardPortalTransactionType.php | 4 + 20 files changed, 701 insertions(+) create mode 100644 resources/sql/autopatches/20190329.portals.01.create.sql create mode 100644 resources/sql/autopatches/20190329.portals.02.xaction.sql create mode 100644 src/applications/dashboard/conduit/PhabricatorDashboardPortalEditConduitAPIMethod.php create mode 100644 src/applications/dashboard/conduit/PhabricatorDashboardPortalSearchConduitAPIMethod.php create mode 100644 src/applications/dashboard/constants/PhabricatorDashboardPortalStatus.php create mode 100644 src/applications/dashboard/controller/portal/PhabricatorDashboardPortalController.php create mode 100644 src/applications/dashboard/controller/portal/PhabricatorDashboardPortalEditController.php create mode 100644 src/applications/dashboard/controller/portal/PhabricatorDashboardPortalListController.php create mode 100644 src/applications/dashboard/controller/portal/PhabricatorDashboardPortalViewController.php create mode 100644 src/applications/dashboard/editor/PhabricatorDashboardPortalEditEngine.php create mode 100644 src/applications/dashboard/editor/PhabricatorDashboardPortalEditor.php create mode 100644 src/applications/dashboard/phid/PhabricatorDashboardPortalPHIDType.php create mode 100644 src/applications/dashboard/query/PhabricatorDashboardPortalQuery.php create mode 100644 src/applications/dashboard/query/PhabricatorDashboardPortalSearchEngine.php create mode 100644 src/applications/dashboard/storage/PhabricatorDashboardPortal.php create mode 100644 src/applications/dashboard/storage/PhabricatorDashboardPortalTransaction.php create mode 100644 src/applications/dashboard/xaction/portal/PhabricatorDashboardPortalNameTransaction.php create mode 100644 src/applications/dashboard/xaction/portal/PhabricatorDashboardPortalTransactionType.php diff --git a/resources/sql/autopatches/20190329.portals.01.create.sql b/resources/sql/autopatches/20190329.portals.01.create.sql new file mode 100644 index 0000000000..d7d1e6138f --- /dev/null +++ b/resources/sql/autopatches/20190329.portals.01.create.sql @@ -0,0 +1,11 @@ +CREATE TABLE {$NAMESPACE}_dashboard.dashboard_portal ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARBINARY(64) NOT NULL, + name VARCHAR(255) NOT NULL COLLATE {$COLLATE_TEXT}, + status VARCHAR(32) NOT NULL COLLATE {$COLLATE_TEXT}, + viewPolicy VARBINARY(64) NOT NULL, + editPolicy VARBINARY(64) NOT NULL, + properties LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT}, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20190329.portals.02.xaction.sql b/resources/sql/autopatches/20190329.portals.02.xaction.sql new file mode 100644 index 0000000000..057df69e2d --- /dev/null +++ b/resources/sql/autopatches/20190329.portals.02.xaction.sql @@ -0,0 +1,19 @@ +CREATE TABLE {$NAMESPACE}_dashboard.dashboard_portaltransaction ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARBINARY(64) NOT NULL, + authorPHID VARBINARY(64) NOT NULL, + objectPHID VARBINARY(64) NOT NULL, + viewPolicy VARBINARY(64) NOT NULL, + editPolicy VARBINARY(64) NOT NULL, + commentPHID VARBINARY(64) DEFAULT NULL, + commentVersion INT UNSIGNED NOT NULL, + transactionType VARCHAR(32) NOT NULL, + oldValue LONGTEXT NOT NULL, + newValue LONGTEXT NOT NULL, + contentSource LONGTEXT NOT NULL, + metadata LONGTEXT NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE {$COLLATE_TEXT}; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 33f2cf4d33..f6b439ec6e 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2946,6 +2946,22 @@ phutil_register_library_map(array( 'PhabricatorDashboardPanelTransactionQuery' => 'applications/dashboard/query/PhabricatorDashboardPanelTransactionQuery.php', 'PhabricatorDashboardPanelType' => 'applications/dashboard/paneltype/PhabricatorDashboardPanelType.php', 'PhabricatorDashboardPanelViewController' => 'applications/dashboard/controller/PhabricatorDashboardPanelViewController.php', + 'PhabricatorDashboardPortal' => 'applications/dashboard/storage/PhabricatorDashboardPortal.php', + 'PhabricatorDashboardPortalController' => 'applications/dashboard/controller/portal/PhabricatorDashboardPortalController.php', + 'PhabricatorDashboardPortalEditConduitAPIMethod' => 'applications/dashboard/conduit/PhabricatorDashboardPortalEditConduitAPIMethod.php', + 'PhabricatorDashboardPortalEditController' => 'applications/dashboard/controller/portal/PhabricatorDashboardPortalEditController.php', + 'PhabricatorDashboardPortalEditEngine' => 'applications/dashboard/editor/PhabricatorDashboardPortalEditEngine.php', + 'PhabricatorDashboardPortalEditor' => 'applications/dashboard/editor/PhabricatorDashboardPortalEditor.php', + 'PhabricatorDashboardPortalListController' => 'applications/dashboard/controller/portal/PhabricatorDashboardPortalListController.php', + 'PhabricatorDashboardPortalNameTransaction' => 'applications/dashboard/xaction/portal/PhabricatorDashboardPortalNameTransaction.php', + 'PhabricatorDashboardPortalPHIDType' => 'applications/dashboard/phid/PhabricatorDashboardPortalPHIDType.php', + 'PhabricatorDashboardPortalQuery' => 'applications/dashboard/query/PhabricatorDashboardPortalQuery.php', + 'PhabricatorDashboardPortalSearchConduitAPIMethod' => 'applications/dashboard/conduit/PhabricatorDashboardPortalSearchConduitAPIMethod.php', + 'PhabricatorDashboardPortalSearchEngine' => 'applications/dashboard/query/PhabricatorDashboardPortalSearchEngine.php', + 'PhabricatorDashboardPortalStatus' => 'applications/dashboard/constants/PhabricatorDashboardPortalStatus.php', + 'PhabricatorDashboardPortalTransaction' => 'applications/dashboard/storage/PhabricatorDashboardPortalTransaction.php', + 'PhabricatorDashboardPortalTransactionType' => 'applications/dashboard/xaction/portal/PhabricatorDashboardPortalTransactionType.php', + 'PhabricatorDashboardPortalViewController' => 'applications/dashboard/controller/portal/PhabricatorDashboardPortalViewController.php', 'PhabricatorDashboardProfileController' => 'applications/dashboard/controller/PhabricatorDashboardProfileController.php', 'PhabricatorDashboardProfileMenuItem' => 'applications/search/menuitem/PhabricatorDashboardProfileMenuItem.php', 'PhabricatorDashboardQuery' => 'applications/dashboard/query/PhabricatorDashboardQuery.php', @@ -8896,6 +8912,27 @@ phutil_register_library_map(array( 'PhabricatorDashboardPanelTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorDashboardPanelType' => 'Phobject', 'PhabricatorDashboardPanelViewController' => 'PhabricatorDashboardController', + 'PhabricatorDashboardPortal' => array( + 'PhabricatorDashboardDAO', + 'PhabricatorApplicationTransactionInterface', + 'PhabricatorPolicyInterface', + 'PhabricatorDestructibleInterface', + ), + 'PhabricatorDashboardPortalController' => 'PhabricatorDashboardController', + 'PhabricatorDashboardPortalEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod', + 'PhabricatorDashboardPortalEditController' => 'PhabricatorDashboardPortalController', + 'PhabricatorDashboardPortalEditEngine' => 'PhabricatorEditEngine', + 'PhabricatorDashboardPortalEditor' => 'PhabricatorApplicationTransactionEditor', + 'PhabricatorDashboardPortalListController' => 'PhabricatorDashboardPortalController', + 'PhabricatorDashboardPortalNameTransaction' => 'PhabricatorDashboardPortalTransactionType', + 'PhabricatorDashboardPortalPHIDType' => 'PhabricatorPHIDType', + 'PhabricatorDashboardPortalQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhabricatorDashboardPortalSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', + 'PhabricatorDashboardPortalSearchEngine' => 'PhabricatorApplicationSearchEngine', + 'PhabricatorDashboardPortalStatus' => 'Phobject', + 'PhabricatorDashboardPortalTransaction' => 'PhabricatorModularTransaction', + 'PhabricatorDashboardPortalTransactionType' => 'PhabricatorModularTransactionType', + 'PhabricatorDashboardPortalViewController' => 'PhabricatorDashboardPortalController', 'PhabricatorDashboardProfileController' => 'PhabricatorController', 'PhabricatorDashboardProfileMenuItem' => 'PhabricatorProfileMenuItem', 'PhabricatorDashboardQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', diff --git a/src/applications/dashboard/application/PhabricatorDashboardApplication.php b/src/applications/dashboard/application/PhabricatorDashboardApplication.php index d8e2701727..9de38a6d8f 100644 --- a/src/applications/dashboard/application/PhabricatorDashboardApplication.php +++ b/src/applications/dashboard/application/PhabricatorDashboardApplication.php @@ -57,6 +57,14 @@ final class PhabricatorDashboardApplication extends PhabricatorApplication { => 'PhabricatorDashboardPanelArchiveController', ), ), + '/portal/' => array( + $this->getQueryRoutePattern() => + 'PhabricatorDashboardPortalListController', + $this->getEditRoutePattern('edit/') => + 'PhabricatorDashboardPortalEditController', + 'view/(?P\d)/' => + 'PhabricatorDashboardPortalViewController', + ), ); } diff --git a/src/applications/dashboard/conduit/PhabricatorDashboardPortalEditConduitAPIMethod.php b/src/applications/dashboard/conduit/PhabricatorDashboardPortalEditConduitAPIMethod.php new file mode 100644 index 0000000000..489bb21cab --- /dev/null +++ b/src/applications/dashboard/conduit/PhabricatorDashboardPortalEditConduitAPIMethod.php @@ -0,0 +1,19 @@ +addTextCrumb(pht('Portals'), '/portal/'); + + return $crumbs; + } + +} diff --git a/src/applications/dashboard/controller/portal/PhabricatorDashboardPortalEditController.php b/src/applications/dashboard/controller/portal/PhabricatorDashboardPortalEditController.php new file mode 100644 index 0000000000..327d969f34 --- /dev/null +++ b/src/applications/dashboard/controller/portal/PhabricatorDashboardPortalEditController.php @@ -0,0 +1,12 @@ +setController($this) + ->buildResponse(); + } + +} diff --git a/src/applications/dashboard/controller/portal/PhabricatorDashboardPortalListController.php b/src/applications/dashboard/controller/portal/PhabricatorDashboardPortalListController.php new file mode 100644 index 0000000000..3eba0179b3 --- /dev/null +++ b/src/applications/dashboard/controller/portal/PhabricatorDashboardPortalListController.php @@ -0,0 +1,26 @@ +setController($this) + ->buildResponse(); + } + + protected function buildApplicationCrumbs() { + $crumbs = parent::buildApplicationCrumbs(); + + id(new PhabricatorDashboardPortalEditEngine()) + ->setViewer($this->getViewer()) + ->addActionToCrumbs($crumbs); + + return $crumbs; + } + +} diff --git a/src/applications/dashboard/controller/portal/PhabricatorDashboardPortalViewController.php b/src/applications/dashboard/controller/portal/PhabricatorDashboardPortalViewController.php new file mode 100644 index 0000000000..733754565e --- /dev/null +++ b/src/applications/dashboard/controller/portal/PhabricatorDashboardPortalViewController.php @@ -0,0 +1,34 @@ +getViewer(); + $id = $request->getURIData('id'); + + $portal = id(new PhabricatorDashboardPortalQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->executeOne(); + if (!$portal) { + return new Aphront404Response(); + } + + $content = $portal->getObjectName(); + + return $this->newPage() + ->setTitle( + array( + pht('Portal'), + $portal->getName(), + )) + ->setPageObjectPHIDs(array($portal->getPHID())) + ->appendChild($content); + } + +} diff --git a/src/applications/dashboard/editor/PhabricatorDashboardPortalEditEngine.php b/src/applications/dashboard/editor/PhabricatorDashboardPortalEditEngine.php new file mode 100644 index 0000000000..04741c59a2 --- /dev/null +++ b/src/applications/dashboard/editor/PhabricatorDashboardPortalEditEngine.php @@ -0,0 +1,83 @@ +getName()); + } + + protected function getObjectEditShortText($object) { + return pht('Edit Portal'); + } + + protected function getObjectCreateShortText() { + return pht('Create Portal'); + } + + protected function getObjectName() { + return pht('Portal'); + } + + protected function getObjectViewURI($object) { + return $object->getURI(); + } + + protected function getEditorURI() { + return '/portal/edit/'; + } + + protected function buildCustomEditFields($object) { + return array( + id(new PhabricatorTextEditField()) + ->setKey('name') + ->setLabel(pht('Name')) + ->setDescription(pht('Name of the portal.')) + ->setConduitDescription(pht('Rename the portal.')) + ->setConduitTypeDescription(pht('New portal name.')) + ->setTransactionType( + PhabricatorDashboardPortalNameTransaction::TRANSACTIONTYPE) + ->setIsRequired(true) + ->setValue($object->getName()), + ); + } + +} diff --git a/src/applications/dashboard/editor/PhabricatorDashboardPortalEditor.php b/src/applications/dashboard/editor/PhabricatorDashboardPortalEditor.php new file mode 100644 index 0000000000..9989d1e7d5 --- /dev/null +++ b/src/applications/dashboard/editor/PhabricatorDashboardPortalEditor.php @@ -0,0 +1,31 @@ +withPHIDs($phids); + } + + public function loadHandles( + PhabricatorHandleQuery $query, + array $handles, + array $objects) { + + foreach ($handles as $phid => $handle) { + $portal = $objects[$phid]; + + $handle + ->setName($portal->getName()) + ->setURI($portal->getURI()); + } + } + +} diff --git a/src/applications/dashboard/query/PhabricatorDashboardPortalQuery.php b/src/applications/dashboard/query/PhabricatorDashboardPortalQuery.php new file mode 100644 index 0000000000..d352b99c8f --- /dev/null +++ b/src/applications/dashboard/query/PhabricatorDashboardPortalQuery.php @@ -0,0 +1,64 @@ +ids = $ids; + return $this; + } + + public function withPHIDs(array $phids) { + $this->phids = $phids; + return $this; + } + + public function withStatuses(array $statuses) { + $this->statuses = $statuses; + return $this; + } + + public function newResultObject() { + return new PhabricatorDashboardPortal(); + } + + protected function loadPage() { + return $this->loadStandardPage($this->newResultObject()); + } + + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); + + if ($this->ids !== null) { + $where[] = qsprintf( + $conn, + 'id IN (%Ld)', + $this->ids); + } + + if ($this->phids !== null) { + $where[] = qsprintf( + $conn, + 'phid IN (%Ls)', + $this->phids); + } + + if ($this->statuses !== null) { + $where[] = qsprintf( + $conn, + 'status IN (%Ls)', + $this->statuses); + } + + return $where; + } + + public function getQueryApplicationClass() { + return 'PhabricatorDashboardApplication'; + } + +} diff --git a/src/applications/dashboard/query/PhabricatorDashboardPortalSearchEngine.php b/src/applications/dashboard/query/PhabricatorDashboardPortalSearchEngine.php new file mode 100644 index 0000000000..c1633f2e97 --- /dev/null +++ b/src/applications/dashboard/query/PhabricatorDashboardPortalSearchEngine.php @@ -0,0 +1,78 @@ +newQuery(); + return $query; + } + + protected function buildCustomSearchFields() { + return array(); + } + + protected function getURI($path) { + return '/portal/'.$path; + } + + protected function getBuiltinQueryNames() { + $names = array(); + + $names['all'] = pht('All Portals'); + + return $names; + } + + public function buildSavedQueryFromBuiltin($query_key) { + $query = $this->newSavedQuery(); + $query->setQueryKey($query_key); + $viewer = $this->requireViewer(); + + switch ($query_key) { + case 'all': + return $query; + } + + return parent::buildSavedQueryFromBuiltin($query_key); + } + + protected function renderResultList( + array $portals, + PhabricatorSavedQuery $query, + array $handles) { + + assert_instances_of($portals, 'PhabricatorDashboardPortal'); + + $viewer = $this->requireViewer(); + + $list = new PHUIObjectItemListView(); + $list->setUser($viewer); + foreach ($portals as $portal) { + $item = id(new PHUIObjectItemView()) + ->setObjectName($portal->getObjectName()) + ->setHeader($portal->getName()) + ->setHref($portal->getURI()) + ->setObject($portal); + + $list->addItem($item); + } + + return id(new PhabricatorApplicationSearchResultView()) + ->setObjectList($list) + ->setNoDataString(pht('No portals found.')); + } + +} diff --git a/src/applications/dashboard/storage/PhabricatorDashboardPortal.php b/src/applications/dashboard/storage/PhabricatorDashboardPortal.php new file mode 100644 index 0000000000..1930a23bd4 --- /dev/null +++ b/src/applications/dashboard/storage/PhabricatorDashboardPortal.php @@ -0,0 +1,104 @@ +setName('') + ->setViewPolicy(PhabricatorPolicies::getMostOpenPolicy()) + ->setEditPolicy(PhabricatorPolicies::POLICY_USER) + ->setStatus(PhabricatorDashboardPortalStatus::STATUS_ACTIVE); + } + + protected function getConfiguration() { + return array( + self::CONFIG_AUX_PHID => true, + self::CONFIG_SERIALIZATION => array( + 'properties' => self::SERIALIZATION_JSON, + ), + self::CONFIG_COLUMN_SCHEMA => array( + 'name' => 'text255', + 'status' => 'text32', + ), + ) + parent::getConfiguration(); + } + + public function getPHIDType() { + return PhabricatorDashboardPortalPHIDType::TYPECONST; + } + + public function getPortalProperty($key, $default = null) { + return idx($this->properties, $key, $default); + } + + public function setPortalProperty($key, $value) { + $this->properties[$key] = $value; + return $this; + } + + public function getObjectName() { + return pht('Portal %d', $this->getID()); + } + + public function getURI() { + return '/portal/view/'.$this->getID().'/'; + } + + +/* -( PhabricatorApplicationTransactionInterface )------------------------- */ + + + public function getApplicationTransactionEditor() { + return new PhabricatorDashboardPortalEditor(); + } + + public function getApplicationTransactionTemplate() { + return new PhabricatorDashboardPortalTransaction(); + } + + +/* -( PhabricatorPolicyInterface )----------------------------------------- */ + + + 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 false; + } + + +/* -( PhabricatorDestructibleInterface )----------------------------------- */ + + + public function destroyObjectPermanently( + PhabricatorDestructionEngine $engine) { + $this->delete(); + } + + +} diff --git a/src/applications/dashboard/storage/PhabricatorDashboardPortalTransaction.php b/src/applications/dashboard/storage/PhabricatorDashboardPortalTransaction.php new file mode 100644 index 0000000000..7861394b98 --- /dev/null +++ b/src/applications/dashboard/storage/PhabricatorDashboardPortalTransaction.php @@ -0,0 +1,18 @@ +getName(); + } + + public function applyInternalEffects($object, $value) { + $object->setName($value); + } + + public function getTitle() { + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + return pht( + '%s renamed this portal from %s to %s.', + $this->renderAuthor(), + $this->renderOldValue(), + $this->renderNewValue()); + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + $max_length = $object->getColumnMaximumByteLength('name'); + foreach ($xactions as $xaction) { + $new = $xaction->getNewValue(); + if (!strlen($new)) { + $errors[] = $this->newInvalidError( + pht('Portals must have a title.'), + $xaction); + continue; + } + + if (strlen($new) > $max_length) { + $errors[] = $this->newInvalidError( + pht( + 'Portal names must not be longer than %s characters.', + $max_length)); + continue; + } + } + + if (!$errors) { + if ($this->isEmptyTextTransaction($object->getName(), $xactions)) { + $errors[] = $this->newRequiredError( + pht('Portals must have a title.')); + } + } + + return $errors; + } + + public function getTransactionTypeForConduit($xaction) { + return 'name'; + } + + public function getFieldValuesForConduit($xaction, $data) { + return array( + 'old' => $xaction->getOldValue(), + 'new' => $xaction->getNewValue(), + ); + } + +} diff --git a/src/applications/dashboard/xaction/portal/PhabricatorDashboardPortalTransactionType.php b/src/applications/dashboard/xaction/portal/PhabricatorDashboardPortalTransactionType.php new file mode 100644 index 0000000000..1855312f26 --- /dev/null +++ b/src/applications/dashboard/xaction/portal/PhabricatorDashboardPortalTransactionType.php @@ -0,0 +1,4 @@ +