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 @@ -1,136 +1,134 @@ <?php /* * Copyright 2011 Facebook, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ class PhabricatorProjectListController extends PhabricatorProjectController { public function processRequest() { $projects = id(new PhabricatorProject())->loadAllWhere( '1 = 1 ORDER BY id DESC limit 100'); $project_phids = mpull($projects, 'getPHID'); $profiles = array(); if ($projects) { $profiles = id(new PhabricatorProjectProfile())->loadAllWhere( 'projectPHID in (%Ls)', $project_phids); $profiles = mpull($profiles, null, 'getProjectPHID'); } $affil_groups = array(); if ($projects) { $affil_groups = PhabricatorProjectAffiliation::loadAllForProjectPHIDs( $project_phids); } $author_phids = mpull($projects, 'getAuthorPHID'); $handles = id(new PhabricatorObjectHandleData($author_phids)) ->loadHandles(); - $project_phids = mpull($projects, 'getPHID'); - $query = id(new ManiphestTaskQuery()) ->withProjects($project_phids) ->withAnyProject(true) ->withStatus(ManiphestTaskQuery::STATUS_OPEN) ->setLimit(PHP_INT_MAX); $tasks = $query->execute(); $groups = array(); foreach ($tasks as $task) { foreach ($task->getProjectPHIDs() as $phid) { $groups[$phid][] = $task; } } $rows = array(); foreach ($projects as $project) { $phid = $project->getPHID(); $profile = $profiles[$phid]; $affiliations = $affil_groups[$phid]; $group = idx($groups, $phid, array()); $task_count = count($group); $population = count($affiliations); $status = PhabricatorProjectStatus::getNameForStatus( $project->getStatus()); $blurb = $profile->getBlurb(); $blurb = phutil_utf8_shorten($blurb, $columns = 100); $rows[] = array( phutil_escape_html($project->getName()), phutil_escape_html($blurb), $handles[$project->getAuthorPHID()]->renderLink(), phutil_escape_html($population), phutil_escape_html($status), phutil_render_tag( 'a', array( 'href' => '/maniphest/view/all/?projects='.$phid, ), phutil_escape_html($task_count)), phutil_render_tag( 'a', array( 'class' => 'small grey button', 'href' => '/project/view/'.$project->getID().'/', ), 'View Project Profile'), ); } $table = new AphrontTableView($rows); $table->setHeaders( array( 'Project', 'Description', 'Mastermind', 'Population', 'Status', 'Open Tasks', '', )); $table->setColumnClasses( array( 'pri', 'wide', '', 'right', '', 'right', 'action', )); $panel = new AphrontPanelView(); $panel->appendChild($table); $panel->setHeader('Project'); $panel->setCreateButton('Create New Project', '/project/create/'); return $this->buildStandardPageResponse( $panel, array( 'title' => 'Projects', )); } } 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 @@ -1,237 +1,297 @@ <?php /* * Copyright 2011 Facebook, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ class PhabricatorProjectProfileController extends PhabricatorProjectController { private $id; private $page; public function willProcessRequest(array $data) { $this->id = idx($data, 'id'); $this->page = idx($data, 'page'); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $uri = $request->getRequestURI(); $project = id(new PhabricatorProject())->load($this->id); if (!$project) { return new Aphront404Response(); } $profile = $project->loadProfile(); if (!$profile) { $profile = new PhabricatorProjectProfile(); } $src_phid = $profile->getProfileImagePHID(); if (!$src_phid) { $src_phid = $user->getProfileImagePHID(); } $picture = PhabricatorFileURI::getViewURIForPHID($src_phid); $pages = array( /* '<h2>Active Documents</h2>', 'tasks' => 'Maniphest Tasks', 'revisions' => 'Differential Revisions', '<hr />', '<h2>Workflow</h2>', 'goals' => 'Goals', 'statistics' => 'Statistics', '<hr />', */ '<h2>Information</h2>', - '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) { default: $content = $this->renderBasicInformation($project, $profile); break; } $profile = new PhabricatorProfileView(); $profile->setProfilePicture($picture); $profile->setProfileNames($project->getName()); foreach ($pages as $page => $name) { if (is_integer($page)) { $profile->addProfileItem( phutil_render_tag( 'span', array(), $name)); } else { $uri->setPath('/project/'.$page.'/'.$project->getID().'/'); $profile->addProfileItem( phutil_render_tag( 'a', array( 'href' => $uri, 'class' => ($this->page == $page) ? 'phabricator-profile-item-selected' : null, ), phutil_escape_html($name))); } } $profile->appendChild($content); return $this->buildStandardPageResponse( $profile, array( 'title' => $project->getName(), )); } + //---------------------------------------------------------------------------- + // Helper functions + private function renderBasicInformation($project, $profile) { $blurb = nonempty( $profile->getBlurb(), '//Nothing is known about this elusive project.//'); $engine = PhabricatorMarkupEngine::newProfileMarkupEngine(); $blurb = $engine->markupText($blurb); $affiliations = $project->loadAffiliations(); $phids = array_merge( array($project->getAuthorPHID()), - mpull($affiliations, 'getUserPHID')); + $project->getSubprojectPHIDs(), + mpull($affiliations, 'getUserPHID') + ); + $phids = array_unique($phids); $handles = id(new PhabricatorObjectHandleData($phids)) ->loadHandles(); $affiliated = array(); foreach ($affiliations as $affiliation) { $user = $handles[$affiliation->getUserPHID()]->renderLink(); $role = phutil_escape_html($affiliation->getRole()); $status = null; if ($affiliation->getStatus() == 'former') { $role = '<em>Former '.$role.'</em>'; } $affiliated[] = '<li>'.$user.' — '.$role.$status.'</li>'; } + if ($affiliated) { $affiliated = '<ul>'.implode("\n", $affiliated).'</ul>'; } else { $affiliated = '<p><em>No one is affiliated with this project.</em></p>'; } + if ($project->getSubprojectPHIDs()) { + $table = $this->renderSubprojectTable( + $handles, + $project->getSubprojectPHIDs()); + $subproject_list = $table->render(); + } else { + $subproject_list = + '<p><em>There are no projects attached for such specie.</em></p>'; + } + + $timestamp = phabricator_format_timestamp($project->getDateCreated()); $status = PhabricatorProjectStatus::getNameForStatus( $project->getStatus()); $content = '<div class="phabricator-profile-info-group"> <h1 class="phabricator-profile-info-header">Basic Information</h1> <div class="phabricator-profile-info-pane"> <table class="phabricator-profile-info-table"> <tr> <th>Creator</th> <td>'.$handles[$project->getAuthorPHID()]->renderLink().'</td> </tr> <tr> <th>Status</th> <td><strong>'.phutil_escape_html($status).'</strong></td> </tr> <tr> <th>Created</th> <td>'.$timestamp.'</td> </tr> <tr> <th>PHID</th> <td>'.phutil_escape_html($project->getPHID()).'</td> </tr> <tr> <th>Blurb</th> <td>'.$blurb.'</td> </tr> </table> </div> </div>'; $content .= - '<div class="phabricator-profile-info-group"> - <h1 class="phabricator-profile-info-header">Resources</h1> - <div class="phabricator-profile-info-pane">'. + '<div class="phabricator-profile-info-group">'. + '<h1 class="phabricator-profile-info-header">Resources</h1>'. + '<div class="phabricator-profile-info-pane">'. $affiliated. - '</div> - </div>'; + '</div>'. + '</div>'; + + $content .= '<div class="phabricator-profile-info-group">'. + '<h1 class="phabricator-profile-info-header">Subprojects</h1>'. + '<div class="phabricator-profile-info-pane">'. + $subproject_list. + '</div>'. + '</div>'; $query = id(new ManiphestTaskQuery()) ->withProjects(array($project->getPHID())) ->withStatus(ManiphestTaskQuery::STATUS_OPEN) ->setOrderBy(ManiphestTaskQuery::ORDER_PRIORITY) ->setLimit(10) ->setCalculateRows(true); $tasks = $query->execute(); $count = $query->getRowCount(); $phids = mpull($tasks, 'getOwnerPHID'); $phids = array_filter($phids); $handles = id(new PhabricatorObjectHandleData($phids)) ->loadHandles(); $task_views = array(); foreach ($tasks as $task) { $view = id(new ManiphestTaskSummaryView()) ->setTask($task) ->setHandles($handles) ->setUser($this->getRequest()->getUser()); $task_views[] = $view->render(); } if (empty($tasks)) { $task_views = '<em>No open tasks.</em>'; } else { $task_views = implode('', $task_views); } $open = number_format($count); $more_link = phutil_render_tag( 'a', array( 'href' => '/maniphest/view/all/?projects='.$project->getPHID(), ), "View All Open Tasks \xC2\xBB"); $content .= '<div class="phabricator-profile-info-group"> <h1 class="phabricator-profile-info-header">'. "Open Tasks ({$open})". '</h1>'. '<div class="phabricator-profile-info-pane">'. $task_views. '<div class="phabricator-profile-info-pane-more-link">'. $more_link. '</div>'. '</div> </div>'; 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 @@ -1,26 +1,27 @@ <?php /** * This file is automatically generated. Lint this module to rebuild it. * @generated */ phutil_require_module('phabricator', 'aphront/response/404'); phutil_require_module('phabricator', 'applications/files/uri'); phutil_require_module('phabricator', 'applications/maniphest/query'); phutil_require_module('phabricator', 'applications/maniphest/view/tasksummary'); phutil_require_module('phabricator', 'applications/markup/engine'); 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'); 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'); phutil_require_module('phutil', 'markup'); phutil_require_module('phutil', 'utils'); phutil_require_source('PhabricatorProjectProfileController.php'); 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 @@ -1,147 +1,163 @@ <?php /* * Copyright 2011 Facebook, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ class PhabricatorProjectProfileEditController extends PhabricatorProjectController { public function willProcessRequest(array $data) { $this->id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $project = id(new PhabricatorProject())->load($this->id); if (!$project) { return new Aphront404Response(); } $profile = $project->loadProfile(); if (empty($profile)) { $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; $errors = array(); 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())) { $e_name = 'Required'; $errors[] = 'Project name is required.'; } else { $e_name = null; } if (!empty($_FILES['image'])) { $err = idx($_FILES['image'], 'error'); if ($err != UPLOAD_ERR_NO_FILE) { $file = PhabricatorFile::newFromPHPUpload( $_FILES['image'], array( 'authorPHID' => $user->getPHID(), )); $okay = $file->isTransformableImage(); if ($okay) { $xformer = new PhabricatorImageTransformer(); $xformed = $xformer->executeProfileTransform( $file, $width = 280, $min_height = 140, $max_height = 420); $profile->setProfileImagePHID($xformed->getPHID()); } else { $errors[] = 'Only valid image files (jpg, jpeg, png or gif) '. 'will be accepted.'; } } } if (!$errors) { $project->save(); $profile->setProjectPHID($project->getPHID()); $profile->save(); return id(new AphrontRedirectResponse()) ->setURI('/project/view/'.$project->getID().'/'); } } $error_view = null; if ($errors) { $error_view = new AphrontErrorView(); $error_view->setTitle('Form Errors'); $error_view->setErrors($errors); } $header_name = 'Edit Project'; $title = 'Edit Project'; $action = '/project/edit/'.$project->getID().'/'; $form = new AphrontFormView(); $form ->setUser($user) ->setAction($action) ->setEncType('multipart/form-data') ->appendChild( id(new AphrontFormTextControl()) ->setLabel('Name') ->setName('name') ->setValue($project->getName()) ->setError($e_name)) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel('Project Status') ->setName('status') ->setOptions($options) ->setValue($project->getStatus())) ->appendChild( id(new AphrontFormTextAreaControl()) ->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') ->setName('image')) ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton('/project/view/'.$project->getID().'/') ->setValue('Save')); $panel = new AphrontPanelView(); $panel->setHeader($header_name); - $panel->setWidth(AphrontPanelView::WIDTH_FORM); + $panel->setWidth(AphrontPanelView::WIDTH_WIDE); $panel->appendChild($form); return $this->buildStandardPageResponse( array( $error_view, $panel, ), array( 'title' => $title, )); } } 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 @@ -1,29 +1,31 @@ <?php /** * This file is automatically generated. Lint this module to rebuild it. * @generated */ 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'); phutil_require_module('phabricator', 'applications/project/storage/project'); phutil_require_module('phabricator', 'view/form/base'); phutil_require_module('phabricator', 'view/form/control/file'); 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'); phutil_require_module('phutil', 'utils'); phutil_require_source('PhabricatorProjectProfileEditController.php'); 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 @@ -1,49 +1,75 @@ <?php /* * Copyright 2011 Facebook, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ class PhabricatorProject extends PhabricatorProjectDAO { protected $name; 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(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( 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', $this->getPHID()); return $profile; } public function loadAffiliations() { $affils = PhabricatorProjectAffiliation::loadAllForProjectPHIDs( 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 @@ -1,19 +1,20 @@ <?php /** * This file is automatically generated. Lint this module to rebuild it. * @generated */ phutil_require_module('phabricator', 'applications/phid/constants'); phutil_require_module('phabricator', 'applications/phid/storage/phid'); 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'); phutil_require_source('PhabricatorProject.php'); 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 @@ +<?php + +/* + * Copyright 2011 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * This is a DAO for the Task -> 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 @@ +<?php +/** + * This file is automatically generated. Lint this module to rebuild it. + * @generated + */ + + + +phutil_require_module('phabricator', 'applications/project/storage/base'); +phutil_require_module('phabricator', 'storage/qsprintf'); +phutil_require_module('phabricator', 'storage/queryfx'); + + +phutil_require_source('PhabricatorProjectSubproject.php'); \ No newline at end of file