diff --git a/resources/sql/patches/064.subporjects.sql b/resources/sql/patches/064.subporjects.sql new file mode 100644 index 0000000000..379e1bdc77 --- /dev/null +++ b/resources/sql/patches/064.subporjects.sql @@ -0,0 +1,11 @@ +ALTER TABLE phabricator_project.project + ADD subprojectPHIDs longblob NOT NULL; +UPDATE phabricator_project.project + SET subprojectPHIDs = '[]'; + +CREATE TABLE phabricator_project.project_subproject ( + projectPHID varchar(64) BINARY NOT NULL, + subprojectPHID varchar(64) BINARY NOT NULL, + PRIMARY KEY (subprojectPHID, projectPHID), + UNIQUE KEY (projectPHID, subprojectPHID) +); \ No newline at end of file diff --git a/src/applications/project/controller/list/PhabricatorProjectListController.php b/src/applications/project/controller/list/PhabricatorProjectListController.php index 05faacf307..25eaf353c7 100644 --- a/src/applications/project/controller/list/PhabricatorProjectListController.php +++ b/src/applications/project/controller/list/PhabricatorProjectListController.php @@ -43,8 +43,6 @@ class PhabricatorProjectListController $handles = id(new PhabricatorObjectHandleData($author_phids)) ->loadHandles(); - $project_phids = mpull($projects, 'getPHID'); - $query = id(new ManiphestTaskQuery()) ->withProjects($project_phids) ->withAnyProject(true) diff --git a/src/applications/project/controller/profile/PhabricatorProjectProfileController.php b/src/applications/project/controller/profile/PhabricatorProjectProfileController.php index 01d336326c..ed5ac31534 100644 --- a/src/applications/project/controller/profile/PhabricatorProjectProfileController.php +++ b/src/applications/project/controller/profile/PhabricatorProjectProfileController.php @@ -58,12 +58,12 @@ class PhabricatorProjectProfileController 'statistics' => 'Statistics', '
', */ '

Information

', - 'edit' => 'Edit Profile', + 'edit' => 'Edit Project', 'affiliation' => 'Edit Affiliation', ); if (empty($pages[$this->page])) { - $this->page = 'action'; // key($pages); + $this->page = 'action'; } switch ($this->page) { @@ -106,6 +106,9 @@ class PhabricatorProjectProfileController )); } + //---------------------------------------------------------------------------- + // Helper functions + private function renderBasicInformation($project, $profile) { $blurb = nonempty( $profile->getBlurb(), @@ -118,7 +121,10 @@ class PhabricatorProjectProfileController $phids = array_merge( array($project->getAuthorPHID()), - mpull($affiliations, 'getUserPHID')); + $project->getSubprojectPHIDs(), + mpull($affiliations, 'getUserPHID') + ); + $phids = array_unique($phids); $handles = id(new PhabricatorObjectHandleData($phids)) ->loadHandles(); @@ -134,12 +140,24 @@ class PhabricatorProjectProfileController $affiliated[] = '
  • '.$user.' — '.$role.$status.'
  • '; } + if ($affiliated) { $affiliated = ''; } else { $affiliated = '

    No one is affiliated with this project.

    '; } + if ($project->getSubprojectPHIDs()) { + $table = $this->renderSubprojectTable( + $handles, + $project->getSubprojectPHIDs()); + $subproject_list = $table->render(); + } else { + $subproject_list = + '

    There are no projects attached for such specie.

    '; + } + + $timestamp = phabricator_format_timestamp($project->getDateCreated()); $status = PhabricatorProjectStatus::getNameForStatus( $project->getStatus()); @@ -174,12 +192,19 @@ class PhabricatorProjectProfileController '; $content .= - '
    -

    Resources

    -
    '. + '
    '. + '

    Resources

    '. + '
    '. $affiliated. - '
    -
    '; + '
    '. + '
    '; + + $content .= '
    '. + '

    Subprojects

    '. + '
    '. + $subproject_list. + '
    '. + '
    '; $query = id(new ManiphestTaskQuery()) ->withProjects(array($project->getPHID())) @@ -234,4 +259,39 @@ class PhabricatorProjectProfileController return $content; } + + private function renderSubprojectTable( + PhabricatorObjectHandleData $handles, + $subprojects_phids) { + + $rows = array(); + foreach ($subprojects_phids as $subproject_phid) { + $phid = $handles[$subproject_phid]->getPHID(); + + $rows[] = array( + phutil_escape_html($handles[$phid]->getFullName()), + phutil_render_tag( + 'a', + array( + 'class' => 'small grey button', + 'href' => $handles[$phid]->getURI(), + ), + 'View Project Profile'), + ); + } + + $table = new AphrontTableView($rows); + $table->setHeaders( + array( + 'Name', + '', + )); + $table->setColumnClasses( + array( + 'pri', + 'action right', + )); + + return $table; + } } diff --git a/src/applications/project/controller/profile/__init__.php b/src/applications/project/controller/profile/__init__.php index 0a31f17672..b9241382bc 100644 --- a/src/applications/project/controller/profile/__init__.php +++ b/src/applications/project/controller/profile/__init__.php @@ -16,6 +16,7 @@ phutil_require_module('phabricator', 'applications/project/constants/status'); phutil_require_module('phabricator', 'applications/project/controller/base'); phutil_require_module('phabricator', 'applications/project/storage/profile'); phutil_require_module('phabricator', 'applications/project/storage/project'); +phutil_require_module('phabricator', 'view/control/table'); phutil_require_module('phabricator', 'view/layout/profile'); phutil_require_module('phabricator', 'view/utils'); diff --git a/src/applications/project/controller/profileedit/PhabricatorProjectProfileEditController.php b/src/applications/project/controller/profileedit/PhabricatorProjectProfileEditController.php index 55b0c1c218..ae9710eca2 100644 --- a/src/applications/project/controller/profileedit/PhabricatorProjectProfileEditController.php +++ b/src/applications/project/controller/profileedit/PhabricatorProjectProfileEditController.php @@ -38,6 +38,15 @@ class PhabricatorProjectProfileEditController $profile = new PhabricatorProjectProfile(); } + if ($project->getSubprojectPHIDs()) { + $phids = $project->getSubprojectPHIDs(); + $handles = id(new PhabricatorObjectHandleData($phids)) + ->loadHandles(); + $subprojects = mpull($handles, 'getFullName', 'getPHID'); + } else { + $subprojects = array(); + } + $options = PhabricatorProjectStatus::getStatusMap(); $e_name = true; @@ -45,6 +54,7 @@ class PhabricatorProjectProfileEditController if ($request->isFormPost()) { $project->setName($request->getStr('name')); $project->setStatus($request->getStr('status')); + $project->setSubprojectPHIDs($request->getArr('set_subprojects')); $profile->setBlurb($request->getStr('blurb')); if (!strlen($project->getName())) { @@ -121,6 +131,12 @@ class PhabricatorProjectProfileEditController ->setLabel('Blurb') ->setName('blurb') ->setValue($profile->getBlurb())) + ->appendChild( + id(new AphrontFormTokenizerControl()) + ->setDatasource('/typeahead/common/projects/') + ->setLabel('Subprojects') + ->setName('set_subprojects') + ->setValue($subprojects)) ->appendChild( id(new AphrontFormFileControl()) ->setLabel('Change Image') @@ -132,7 +148,7 @@ class PhabricatorProjectProfileEditController $panel = new AphrontPanelView(); $panel->setHeader($header_name); - $panel->setWidth(AphrontPanelView::WIDTH_FORM); + $panel->setWidth(AphrontPanelView::WIDTH_WIDE); $panel->appendChild($form); return $this->buildStandardPageResponse( diff --git a/src/applications/project/controller/profileedit/__init__.php b/src/applications/project/controller/profileedit/__init__.php index a03f8883d2..46a80365db 100644 --- a/src/applications/project/controller/profileedit/__init__.php +++ b/src/applications/project/controller/profileedit/__init__.php @@ -10,6 +10,7 @@ phutil_require_module('phabricator', 'aphront/response/404'); phutil_require_module('phabricator', 'aphront/response/redirect'); phutil_require_module('phabricator', 'applications/files/storage/file'); phutil_require_module('phabricator', 'applications/files/transform'); +phutil_require_module('phabricator', 'applications/phid/handle/data'); phutil_require_module('phabricator', 'applications/project/constants/status'); phutil_require_module('phabricator', 'applications/project/controller/base'); phutil_require_module('phabricator', 'applications/project/storage/profile'); @@ -20,6 +21,7 @@ phutil_require_module('phabricator', 'view/form/control/select'); phutil_require_module('phabricator', 'view/form/control/submit'); phutil_require_module('phabricator', 'view/form/control/text'); phutil_require_module('phabricator', 'view/form/control/textarea'); +phutil_require_module('phabricator', 'view/form/control/tokenizer'); phutil_require_module('phabricator', 'view/form/error'); phutil_require_module('phabricator', 'view/layout/panel'); diff --git a/src/applications/project/storage/project/PhabricatorProject.php b/src/applications/project/storage/project/PhabricatorProject.php index d9da01e716..4558eb5723 100644 --- a/src/applications/project/storage/project/PhabricatorProject.php +++ b/src/applications/project/storage/project/PhabricatorProject.php @@ -22,10 +22,16 @@ class PhabricatorProject extends PhabricatorProjectDAO { protected $phid; protected $status = PhabricatorProjectStatus::UNKNOWN; protected $authorPHID; + protected $subprojectPHIDs = array(); + + private $subprojectsNeedUpdate; public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, + self::CONFIG_SERIALIZATION => array( + 'subprojectPHIDs' => self::SERIALIZATION_JSON, + ), ) + parent::getConfiguration(); } @@ -34,6 +40,12 @@ class PhabricatorProject extends PhabricatorProjectDAO { PhabricatorPHIDConstants::PHID_TYPE_PROJ); } + public function setSubprojectPHIDs(array $phids) { + $this->subprojectPHIDs = $phids; + $this->subprojectsNeedUpdate = true; + return $this; + } + public function loadProfile() { $profile = id(new PhabricatorProjectProfile())->loadOneWhere( 'projectPHID = %s', @@ -46,4 +58,18 @@ class PhabricatorProject extends PhabricatorProjectDAO { array($this->getPHID())); return $affils[$this->getPHID()]; } + + public function save() { + $result = parent::save(); + + if ($this->subprojectsNeedUpdate) { + // If we've changed the project PHIDs for this task, update the link + // table. + PhabricatorProjectSubproject::updateProjectSubproject($this); + $this->subprojectsNeedUpdate = false; + } + + return $result; + } + } diff --git a/src/applications/project/storage/project/__init__.php b/src/applications/project/storage/project/__init__.php index bc7599691d..12477e8dc7 100644 --- a/src/applications/project/storage/project/__init__.php +++ b/src/applications/project/storage/project/__init__.php @@ -12,6 +12,7 @@ phutil_require_module('phabricator', 'applications/project/constants/status'); phutil_require_module('phabricator', 'applications/project/storage/affiliation'); phutil_require_module('phabricator', 'applications/project/storage/base'); phutil_require_module('phabricator', 'applications/project/storage/profile'); +phutil_require_module('phabricator', 'applications/project/storage/subproject'); phutil_require_module('phutil', 'utils'); diff --git a/src/applications/project/storage/subproject/PhabricatorProjectSubproject.php b/src/applications/project/storage/subproject/PhabricatorProjectSubproject.php new file mode 100644 index 0000000000..43832e7275 --- /dev/null +++ b/src/applications/project/storage/subproject/PhabricatorProjectSubproject.php @@ -0,0 +1,67 @@ + Project table, which denormalizes the + * relationship between tasks and projects into a link table so it can be + * efficiently queried. This table is not authoritative; the projectPHIDs field + * of ManiphestTask is. The rows in this table are regenerated when transactions + * are applied to tasks which affected their associated projects. + * + * @group maniphest + */ +final class PhabricatorProjectSubproject extends PhabricatorProjectDAO { + + protected $projectPHID; + protected $subprojectPHID; + + public function getConfiguration() { + return array( + self::CONFIG_IDS => self::IDS_MANUAL, + self::CONFIG_TIMESTAMPS => false, + ); + } + + public static function updateProjectSubproject(PhabricatorProject $project) { + $dao = new PhabricatorProjectSubproject(); + $conn = $dao->establishConnection('w'); + + $sql = array(); + foreach ($project->getSubprojectPHIDs() as $subproject_phid) { + $sql[] = qsprintf( + $conn, + '(%s, %s)', + $project->getPHID(), + $subproject_phid); + } + + queryfx( + $conn, + 'DELETE FROM %T WHERE projectPHID = %s', + $dao->getTableName(), + $project->getPHID()); + if ($sql) { + queryfx( + $conn, + 'INSERT INTO %T (projectPHID, subprojectPHID) VALUES %Q', + $dao->getTableName(), + implode(', ', $sql)); + } + } + +} diff --git a/src/applications/project/storage/subproject/__init__.php b/src/applications/project/storage/subproject/__init__.php new file mode 100644 index 0000000000..cffb2495f9 --- /dev/null +++ b/src/applications/project/storage/subproject/__init__.php @@ -0,0 +1,14 @@ +