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 = ''.implode("\n", $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 .=
- '
-
-
'.
+ '
'.
+ ''.
+ '
'.
$affiliated.
- '
-
';
+ '
'.
+ '
';
+
+ $content .= ''.
+ ''.
+ '
'.
+ $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 @@
+