mirror of
https://we.phorge.it/source/phorge.git
synced 2024-12-18 11:30:55 +01:00
Roughly implement milestone columns on workboards
Summary: Ref T10010. These aren't perfect but I think (?) they aren't horribly broken. - When a project is a parent project, destroy (as far as the user can tell) any custom columns. - When a project has milestones, automatically generate columns on the project's workboard (if it has a workboard). - When you move tasks between milestones, add the proper milestone tag. - When you move tasks out of milestones back into the backlog, add the proper parent project tag. - (Plenty of UI / design stuff to adjust.) Test Plan: - Dragged stuff between milestone columns. - Used a normal workboard. - Wasn't able to find any egregiously bad cases that did anything terrible. {F1088224} Reviewers: chad Reviewed By: chad Maniphest Tasks: T10010 Differential Revision: https://secure.phabricator.com/D15171
This commit is contained in:
parent
00165424d0
commit
90a0459821
18 changed files with 496 additions and 57 deletions
BIN
resources/builtin/image-200x200.png
Normal file
BIN
resources/builtin/image-200x200.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
|
@ -413,7 +413,7 @@ return array(
|
||||||
'rsrc/js/application/phortune/phortune-credit-card-form.js' => '2290aeef',
|
'rsrc/js/application/phortune/phortune-credit-card-form.js' => '2290aeef',
|
||||||
'rsrc/js/application/policy/behavior-policy-control.js' => 'ae45872f',
|
'rsrc/js/application/policy/behavior-policy-control.js' => 'ae45872f',
|
||||||
'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '5e9f347c',
|
'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '5e9f347c',
|
||||||
'rsrc/js/application/projects/behavior-project-boards.js' => 'c05fb42a',
|
'rsrc/js/application/projects/behavior-project-boards.js' => '48470f95',
|
||||||
'rsrc/js/application/projects/behavior-project-create.js' => '065227cc',
|
'rsrc/js/application/projects/behavior-project-create.js' => '065227cc',
|
||||||
'rsrc/js/application/projects/behavior-reorder-columns.js' => 'e1d25dfb',
|
'rsrc/js/application/projects/behavior-reorder-columns.js' => 'e1d25dfb',
|
||||||
'rsrc/js/application/releeph/releeph-preview-branch.js' => 'b2b4fbaf',
|
'rsrc/js/application/releeph/releeph-preview-branch.js' => 'b2b4fbaf',
|
||||||
|
@ -653,7 +653,7 @@ return array(
|
||||||
'javelin-behavior-phui-profile-menu' => '12884df9',
|
'javelin-behavior-phui-profile-menu' => '12884df9',
|
||||||
'javelin-behavior-policy-control' => 'ae45872f',
|
'javelin-behavior-policy-control' => 'ae45872f',
|
||||||
'javelin-behavior-policy-rule-editor' => '5e9f347c',
|
'javelin-behavior-policy-rule-editor' => '5e9f347c',
|
||||||
'javelin-behavior-project-boards' => 'c05fb42a',
|
'javelin-behavior-project-boards' => '48470f95',
|
||||||
'javelin-behavior-project-create' => '065227cc',
|
'javelin-behavior-project-create' => '065227cc',
|
||||||
'javelin-behavior-quicksand-blacklist' => '7927a7d3',
|
'javelin-behavior-quicksand-blacklist' => '7927a7d3',
|
||||||
'javelin-behavior-recurring-edit' => '5f1c4d5f',
|
'javelin-behavior-recurring-edit' => '5f1c4d5f',
|
||||||
|
@ -1151,6 +1151,15 @@ return array(
|
||||||
'javelin-dom',
|
'javelin-dom',
|
||||||
'javelin-workflow',
|
'javelin-workflow',
|
||||||
),
|
),
|
||||||
|
'48470f95' => array(
|
||||||
|
'javelin-behavior',
|
||||||
|
'javelin-dom',
|
||||||
|
'javelin-util',
|
||||||
|
'javelin-vector',
|
||||||
|
'javelin-stratcom',
|
||||||
|
'javelin-workflow',
|
||||||
|
'phabricator-draggable-list',
|
||||||
|
),
|
||||||
'49b73b36' => array(
|
'49b73b36' => array(
|
||||||
'javelin-behavior',
|
'javelin-behavior',
|
||||||
'javelin-dom',
|
'javelin-dom',
|
||||||
|
@ -1779,15 +1788,6 @@ return array(
|
||||||
'javelin-install',
|
'javelin-install',
|
||||||
'javelin-dom',
|
'javelin-dom',
|
||||||
),
|
),
|
||||||
'c05fb42a' => array(
|
|
||||||
'javelin-behavior',
|
|
||||||
'javelin-dom',
|
|
||||||
'javelin-util',
|
|
||||||
'javelin-vector',
|
|
||||||
'javelin-stratcom',
|
|
||||||
'javelin-workflow',
|
|
||||||
'phabricator-draggable-list',
|
|
||||||
),
|
|
||||||
'c1700f6f' => array(
|
'c1700f6f' => array(
|
||||||
'javelin-install',
|
'javelin-install',
|
||||||
'javelin-util',
|
'javelin-util',
|
||||||
|
|
2
resources/sql/autopatches/20160202.board.1.proxy.sql
Normal file
2
resources/sql/autopatches/20160202.board.1.proxy.sql
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
ALTER TABLE {$NAMESPACE}_project.project_column
|
||||||
|
ADD proxyPHID VARBINARY(64);
|
|
@ -1889,6 +1889,7 @@ phutil_register_library_map(array(
|
||||||
'PhabricatorChatLogQuery' => 'applications/chatlog/query/PhabricatorChatLogQuery.php',
|
'PhabricatorChatLogQuery' => 'applications/chatlog/query/PhabricatorChatLogQuery.php',
|
||||||
'PhabricatorChunkedFileStorageEngine' => 'applications/files/engine/PhabricatorChunkedFileStorageEngine.php',
|
'PhabricatorChunkedFileStorageEngine' => 'applications/files/engine/PhabricatorChunkedFileStorageEngine.php',
|
||||||
'PhabricatorClusterConfigOptions' => 'applications/config/option/PhabricatorClusterConfigOptions.php',
|
'PhabricatorClusterConfigOptions' => 'applications/config/option/PhabricatorClusterConfigOptions.php',
|
||||||
|
'PhabricatorColumnProxyInterface' => 'applications/project/interface/PhabricatorColumnProxyInterface.php',
|
||||||
'PhabricatorCommentEditEngineExtension' => 'applications/transactions/engineextension/PhabricatorCommentEditEngineExtension.php',
|
'PhabricatorCommentEditEngineExtension' => 'applications/transactions/engineextension/PhabricatorCommentEditEngineExtension.php',
|
||||||
'PhabricatorCommentEditField' => 'applications/transactions/editfield/PhabricatorCommentEditField.php',
|
'PhabricatorCommentEditField' => 'applications/transactions/editfield/PhabricatorCommentEditField.php',
|
||||||
'PhabricatorCommentEditType' => 'applications/transactions/edittype/PhabricatorCommentEditType.php',
|
'PhabricatorCommentEditType' => 'applications/transactions/edittype/PhabricatorCommentEditType.php',
|
||||||
|
@ -7262,6 +7263,7 @@ phutil_register_library_map(array(
|
||||||
'PhabricatorDestructibleInterface',
|
'PhabricatorDestructibleInterface',
|
||||||
'PhabricatorFulltextInterface',
|
'PhabricatorFulltextInterface',
|
||||||
'PhabricatorConduitResultInterface',
|
'PhabricatorConduitResultInterface',
|
||||||
|
'PhabricatorColumnProxyInterface',
|
||||||
),
|
),
|
||||||
'PhabricatorProjectAddHeraldAction' => 'PhabricatorProjectHeraldAction',
|
'PhabricatorProjectAddHeraldAction' => 'PhabricatorProjectHeraldAction',
|
||||||
'PhabricatorProjectApplication' => 'PhabricatorApplication',
|
'PhabricatorProjectApplication' => 'PhabricatorApplication',
|
||||||
|
|
|
@ -280,10 +280,23 @@ final class ManiphestEditEngine
|
||||||
return new Aphront404Response();
|
return new Aphront404Response();
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the workboard's project has been removed from the card's project
|
// If the workboard's project and all descendant projects have been removed
|
||||||
// list, we are going to remove it from the board completely.
|
// from the card's project list, we are going to remove it from the board
|
||||||
|
// completely.
|
||||||
|
|
||||||
|
// TODO: If the user did something sneaky and changed a subproject, we'll
|
||||||
|
// currently leave the card where it was but should really move it to the
|
||||||
|
// proper new column.
|
||||||
|
|
||||||
|
$descendant_projects = id(new PhabricatorProjectQuery())
|
||||||
|
->setViewer($viewer)
|
||||||
|
->withAncestorProjectPHIDs(array($column->getProjectPHID()))
|
||||||
|
->execute();
|
||||||
|
$board_phids = mpull($descendant_projects, 'getPHID', 'getPHID');
|
||||||
|
$board_phids[$column->getProjectPHID()] = $column->getProjectPHID();
|
||||||
|
|
||||||
$project_map = array_fuse($task->getProjectPHIDs());
|
$project_map = array_fuse($task->getProjectPHIDs());
|
||||||
$remove_card = empty($project_map[$column->getProjectPHID()]);
|
$remove_card = !array_intersect_key($board_phids, $project_map);
|
||||||
|
|
||||||
$positions = id(new PhabricatorProjectColumnPositionQuery())
|
$positions = id(new PhabricatorProjectColumnPositionQuery())
|
||||||
->setViewer($viewer)
|
->setViewer($viewer)
|
||||||
|
|
|
@ -222,12 +222,22 @@ final class ManiphestTransactionEditor
|
||||||
// can't see.
|
// can't see.
|
||||||
$omnipotent_viewer = PhabricatorUser::getOmnipotentUser();
|
$omnipotent_viewer = PhabricatorUser::getOmnipotentUser();
|
||||||
|
|
||||||
|
$select_phids = array($board_phid);
|
||||||
|
|
||||||
|
$descendants = id(new PhabricatorProjectQuery())
|
||||||
|
->setViewer($omnipotent_viewer)
|
||||||
|
->withAncestorProjectPHIDs($select_phids)
|
||||||
|
->execute();
|
||||||
|
foreach ($descendants as $descendant) {
|
||||||
|
$select_phids[] = $descendant->getPHID();
|
||||||
|
}
|
||||||
|
|
||||||
$board_tasks = id(new ManiphestTaskQuery())
|
$board_tasks = id(new ManiphestTaskQuery())
|
||||||
->setViewer($omnipotent_viewer)
|
->setViewer($omnipotent_viewer)
|
||||||
->withEdgeLogicPHIDs(
|
->withEdgeLogicPHIDs(
|
||||||
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
|
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
|
||||||
PhabricatorQueryConstraint::OPERATOR_AND,
|
PhabricatorQueryConstraint::OPERATOR_ANCESTOR,
|
||||||
array($board_phid))
|
array($select_phids))
|
||||||
->execute();
|
->execute();
|
||||||
|
|
||||||
$object_phids = mpull($board_tasks, 'getPHID');
|
$object_phids = mpull($board_tasks, 'getPHID');
|
||||||
|
|
|
@ -972,7 +972,45 @@ final class PhabricatorProjectCoreTestCase extends PhabricatorTestCase {
|
||||||
$task1->getPHID(),
|
$task1->getPHID(),
|
||||||
);
|
);
|
||||||
$this->assertTasksInColumn($expect, $user, $board, $column);
|
$this->assertTasksInColumn($expect, $user, $board, $column);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMilestoneMoves() {
|
||||||
|
$user = $this->createUser();
|
||||||
|
$user->save();
|
||||||
|
|
||||||
|
$board = $this->createProject($user);
|
||||||
|
|
||||||
|
$backlog = $this->addColumn($user, $board, 0);
|
||||||
|
|
||||||
|
// Create a task into the backlog.
|
||||||
|
$task = $this->newTask($user, array($board));
|
||||||
|
$expect = array(
|
||||||
|
$backlog->getPHID(),
|
||||||
|
);
|
||||||
|
$this->assertColumns($expect, $user, $board, $task);
|
||||||
|
|
||||||
|
$milestone = $this->createProject($user, $board, true);
|
||||||
|
|
||||||
|
$this->addProjectTags($user, $task, array($milestone->getPHID()));
|
||||||
|
|
||||||
|
// We just want the side effect of looking at the board: creation of the
|
||||||
|
// milestone column.
|
||||||
|
$this->loadColumns($user, $board, $task);
|
||||||
|
|
||||||
|
$column = id(new PhabricatorProjectColumnQuery())
|
||||||
|
->setViewer($user)
|
||||||
|
->withProjectPHIDs(array($board->getPHID()))
|
||||||
|
->withProxyPHIDs(array($milestone->getPHID()))
|
||||||
|
->executeOne();
|
||||||
|
|
||||||
|
$this->assertTrue((bool)$column);
|
||||||
|
|
||||||
|
// Moving the task to the milestone should have moved it to the milestone
|
||||||
|
// column.
|
||||||
|
$expect = array(
|
||||||
|
$column->getPHID(),
|
||||||
|
);
|
||||||
|
$this->assertColumns($expect, $user, $board, $task);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function moveToColumn(
|
private function moveToColumn(
|
||||||
|
@ -1014,7 +1052,14 @@ final class PhabricatorProjectCoreTestCase extends PhabricatorTestCase {
|
||||||
PhabricatorUser $viewer,
|
PhabricatorUser $viewer,
|
||||||
PhabricatorProject $board,
|
PhabricatorProject $board,
|
||||||
ManiphestTask $task) {
|
ManiphestTask $task) {
|
||||||
|
$column_phids = $this->loadColumns($viewer, $board, $task);
|
||||||
|
$this->assertEqual($expect, $column_phids);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function loadColumns(
|
||||||
|
PhabricatorUser $viewer,
|
||||||
|
PhabricatorProject $board,
|
||||||
|
ManiphestTask $task) {
|
||||||
$engine = id(new PhabricatorBoardLayoutEngine())
|
$engine = id(new PhabricatorBoardLayoutEngine())
|
||||||
->setViewer($viewer)
|
->setViewer($viewer)
|
||||||
->setBoardPHIDs(array($board->getPHID()))
|
->setBoardPHIDs(array($board->getPHID()))
|
||||||
|
@ -1028,7 +1073,7 @@ final class PhabricatorProjectCoreTestCase extends PhabricatorTestCase {
|
||||||
$column_phids = mpull($columns, 'getPHID');
|
$column_phids = mpull($columns, 'getPHID');
|
||||||
$column_phids = array_values($column_phids);
|
$column_phids = array_values($column_phids);
|
||||||
|
|
||||||
$this->assertEqual($expect, $column_phids);
|
return $column_phids;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function assertTasksInColumn(
|
private function assertTasksInColumn(
|
||||||
|
@ -1236,6 +1281,16 @@ final class PhabricatorProjectCoreTestCase extends PhabricatorTestCase {
|
||||||
|
|
||||||
$this->applyTransactions($project, $user, $xactions);
|
$this->applyTransactions($project, $user, $xactions);
|
||||||
|
|
||||||
|
// Force these values immediately; they are normally updated by the
|
||||||
|
// index engine.
|
||||||
|
if ($parent) {
|
||||||
|
if ($is_milestone) {
|
||||||
|
$parent->setHasMilestones(1)->save();
|
||||||
|
} else {
|
||||||
|
$parent->setHasSubprojects(1)->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return $project;
|
return $project;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -95,17 +95,27 @@ final class PhabricatorProjectBoardViewController
|
||||||
|
|
||||||
$task_query = $search_engine->buildQueryFromSavedQuery($saved);
|
$task_query = $search_engine->buildQueryFromSavedQuery($saved);
|
||||||
|
|
||||||
|
$select_phids = array($project->getPHID());
|
||||||
|
if ($project->getHasSubprojects() || $project->getHasMilestones()) {
|
||||||
|
$descendants = id(new PhabricatorProjectQuery())
|
||||||
|
->setViewer($viewer)
|
||||||
|
->withAncestorProjectPHIDs($select_phids)
|
||||||
|
->execute();
|
||||||
|
foreach ($descendants as $descendant) {
|
||||||
|
$select_phids[] = $descendant->getPHID();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$tasks = $task_query
|
$tasks = $task_query
|
||||||
->withEdgeLogicPHIDs(
|
->withEdgeLogicPHIDs(
|
||||||
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
|
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
|
||||||
PhabricatorQueryConstraint::OPERATOR_AND,
|
PhabricatorQueryConstraint::OPERATOR_ANCESTOR,
|
||||||
array($project->getPHID()))
|
array($select_phids))
|
||||||
->setOrder(ManiphestTaskQuery::ORDER_PRIORITY)
|
->setOrder(ManiphestTaskQuery::ORDER_PRIORITY)
|
||||||
->setViewer($viewer)
|
->setViewer($viewer)
|
||||||
->execute();
|
->execute();
|
||||||
$tasks = mpull($tasks, null, 'getPHID');
|
$tasks = mpull($tasks, null, 'getPHID');
|
||||||
|
|
||||||
|
|
||||||
$board_phid = $project->getPHID();
|
$board_phid = $project->getPHID();
|
||||||
|
|
||||||
$layout_engine = id(new PhabricatorBoardLayoutEngine())
|
$layout_engine = id(new PhabricatorBoardLayoutEngine())
|
||||||
|
@ -225,6 +235,13 @@ final class PhabricatorProjectBoardViewController
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$proxy = $column->getProxy();
|
||||||
|
if ($proxy && !$proxy->isMilestone()) {
|
||||||
|
// TODO: For now, don't show subproject columns because we can't
|
||||||
|
// handle tasks with multiple positions yet.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
$task_phids = $layout_engine->getColumnObjectPHIDs(
|
$task_phids = $layout_engine->getColumnObjectPHIDs(
|
||||||
$board_phid,
|
$board_phid,
|
||||||
$column->getPHID());
|
$column->getPHID());
|
||||||
|
@ -247,6 +264,11 @@ final class PhabricatorProjectBoardViewController
|
||||||
$panel->setHeaderIcon($header_icon);
|
$panel->setHeaderIcon($header_icon);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$display_class = $column->getDisplayClass();
|
||||||
|
if ($display_class) {
|
||||||
|
$panel->addClass($display_class);
|
||||||
|
}
|
||||||
|
|
||||||
if ($column->isHidden()) {
|
if ($column->isHidden()) {
|
||||||
$panel->addClass('project-panel-hidden');
|
$panel->addClass('project-panel-hidden');
|
||||||
}
|
}
|
||||||
|
@ -582,6 +604,12 @@ final class PhabricatorProjectBoardViewController
|
||||||
|
|
||||||
$column_items = array();
|
$column_items = array();
|
||||||
|
|
||||||
|
if ($column->getProxyPHID()) {
|
||||||
|
$default_phid = $column->getProxyPHID();
|
||||||
|
} else {
|
||||||
|
$default_phid = $column->getProjectPHID();
|
||||||
|
}
|
||||||
|
|
||||||
$column_items[] = id(new PhabricatorActionView())
|
$column_items[] = id(new PhabricatorActionView())
|
||||||
->setIcon('fa-plus')
|
->setIcon('fa-plus')
|
||||||
->setName(pht('Create Task...'))
|
->setName(pht('Create Task...'))
|
||||||
|
@ -590,6 +618,7 @@ final class PhabricatorProjectBoardViewController
|
||||||
->setMetadata(
|
->setMetadata(
|
||||||
array(
|
array(
|
||||||
'columnPHID' => $column->getPHID(),
|
'columnPHID' => $column->getPHID(),
|
||||||
|
'projectPHID' => $default_phid,
|
||||||
));
|
));
|
||||||
|
|
||||||
$batch_edit_uri = $request->getRequestURI();
|
$batch_edit_uri = $request->getRequestURI();
|
||||||
|
@ -738,6 +767,10 @@ final class PhabricatorProjectBoardViewController
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Tailor this UI if the project is already a parent project. We
|
||||||
|
// should not offer options for creating a parent project workboard, since
|
||||||
|
// they can't have their own columns.
|
||||||
|
|
||||||
$new_selector = id(new AphrontFormRadioButtonControl())
|
$new_selector = id(new AphrontFormRadioButtonControl())
|
||||||
->setLabel(pht('Columns'))
|
->setLabel(pht('Columns'))
|
||||||
->setName('initialize-type')
|
->setName('initialize-type')
|
||||||
|
|
|
@ -139,7 +139,33 @@ final class PhabricatorProjectMoveController
|
||||||
->setTransactionType(ManiphestTransaction::TYPE_SUBPRIORITY)
|
->setTransactionType(ManiphestTransaction::TYPE_SUBPRIORITY)
|
||||||
->setNewValue($sub);
|
->setNewValue($sub);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$proxy = $column->getProxy();
|
||||||
|
if ($proxy) {
|
||||||
|
// We're moving the task into a subproject or milestone column, so add
|
||||||
|
// the subproject or milestone.
|
||||||
|
$add_projects = array($proxy->getPHID());
|
||||||
|
} else if ($project->getHasSubprojects() || $project->getHasMilestones()) {
|
||||||
|
// We're moving the task into the "Backlog" column on the parent project,
|
||||||
|
// so add the parent explicitly. This gets rid of any subproject or
|
||||||
|
// milestone tags.
|
||||||
|
$add_projects = array($project->getPHID());
|
||||||
|
} else {
|
||||||
|
$add_projects = array();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($add_projects) {
|
||||||
|
$project_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST;
|
||||||
|
|
||||||
|
$xactions[] = id(new ManiphestTransaction())
|
||||||
|
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
|
||||||
|
->setMetadataValue('edge:type', $project_type)
|
||||||
|
->setNewValue(
|
||||||
|
array(
|
||||||
|
'+' => array_fuse($add_projects),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
$editor = id(new ManiphestTransactionEditor())
|
$editor = id(new ManiphestTransactionEditor())
|
||||||
->setActor($viewer)
|
->setActor($viewer)
|
||||||
|
@ -157,6 +183,18 @@ final class PhabricatorProjectMoveController
|
||||||
->executeOne();
|
->executeOne();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reload the object so it reflects edits which have been applied.
|
||||||
|
$object = id(new ManiphestTaskQuery())
|
||||||
|
->setViewer($viewer)
|
||||||
|
->withPHIDs(array($object_phid))
|
||||||
|
->needProjectPHIDs(true)
|
||||||
|
->requireCapabilities(
|
||||||
|
array(
|
||||||
|
PhabricatorPolicyCapability::CAN_VIEW,
|
||||||
|
PhabricatorPolicyCapability::CAN_EDIT,
|
||||||
|
))
|
||||||
|
->executeOne();
|
||||||
|
|
||||||
$card = id(new ProjectBoardTaskCard())
|
$card = id(new ProjectBoardTaskCard())
|
||||||
->setViewer($viewer)
|
->setViewer($viewer)
|
||||||
->setTask($object)
|
->setTask($object)
|
||||||
|
@ -169,6 +207,6 @@ final class PhabricatorProjectMoveController
|
||||||
|
|
||||||
return id(new AphrontAjaxResponse())->setContent(
|
return id(new AphrontAjaxResponse())->setContent(
|
||||||
array('task' => $card));
|
array('task' => $card));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,7 +35,7 @@ final class PhabricatorProjectSubprojectWarningController
|
||||||
|
|
||||||
$conversion_help = pht(
|
$conversion_help = pht(
|
||||||
"Creating a project's first subproject **moves all ".
|
"Creating a project's first subproject **moves all ".
|
||||||
"members** and **destroys all workboard columns**.".
|
"members** to become members of the subproject instead".
|
||||||
"\n\n".
|
"\n\n".
|
||||||
"See [[ %s | Projects User Guide ]] in the documentation for details. ".
|
"See [[ %s | Projects User Guide ]] in the documentation for details. ".
|
||||||
"This process can not be undone.",
|
"This process can not be undone.",
|
||||||
|
|
|
@ -320,8 +320,63 @@ final class PhabricatorBoardLayoutEngine extends Phobject {
|
||||||
$columns = msort($columns, 'getSequence');
|
$columns = msort($columns, 'getSequence');
|
||||||
$columns = mpull($columns, null, 'getPHID');
|
$columns = mpull($columns, null, 'getPHID');
|
||||||
|
|
||||||
$this->columnMap = $columns;
|
$need_children = array();
|
||||||
|
foreach ($boards as $phid => $board) {
|
||||||
|
if ($board->getHasMilestones() || $board->getHasSubprojects()) {
|
||||||
|
$need_children[] = $phid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($need_children) {
|
||||||
|
$children = id(new PhabricatorProjectQuery())
|
||||||
|
->setViewer($viewer)
|
||||||
|
->withParentProjectPHIDs($need_children)
|
||||||
|
->execute();
|
||||||
|
$children = mpull($children, null, 'getPHID');
|
||||||
|
$children = mgroup($children, 'getParentProjectPHID');
|
||||||
|
} else {
|
||||||
|
$children = array();
|
||||||
|
}
|
||||||
|
|
||||||
$columns = mgroup($columns, 'getProjectPHID');
|
$columns = mgroup($columns, 'getProjectPHID');
|
||||||
|
foreach ($boards as $board_phid => $board) {
|
||||||
|
$board_columns = idx($columns, $board_phid, array());
|
||||||
|
|
||||||
|
// If the project has milestones, create any missing columns.
|
||||||
|
if ($board->getHasMilestones() || $board->getHasSubprojects()) {
|
||||||
|
$child_projects = idx($children, $board_phid, array());
|
||||||
|
|
||||||
|
$next_sequence = last($board_columns)->getSequence() + 1;
|
||||||
|
$proxy_columns = mpull($board_columns, null, 'getProxyPHID');
|
||||||
|
foreach ($child_projects as $child_phid => $child) {
|
||||||
|
if (isset($proxy_columns[$child_phid])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$new_column = PhabricatorProjectColumn::initializeNewColumn($viewer)
|
||||||
|
->attachProject($board)
|
||||||
|
->attachProxy($child)
|
||||||
|
->setSequence($next_sequence++)
|
||||||
|
->setProjectPHID($board_phid)
|
||||||
|
->setProxyPHID($child_phid);
|
||||||
|
|
||||||
|
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
|
||||||
|
$new_column->save();
|
||||||
|
unset($unguarded);
|
||||||
|
|
||||||
|
$board_columns[$new_column->getPHID()] = $new_column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$columns[$board_phid] = $board_columns;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($columns as $board_phid => $board_columns) {
|
||||||
|
foreach ($board_columns as $board_column) {
|
||||||
|
$column_phid = $board_column->getPHID();
|
||||||
|
$this->columnMap[$column_phid] = $board_column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return $columns;
|
return $columns;
|
||||||
}
|
}
|
||||||
|
@ -350,6 +405,8 @@ final class PhabricatorBoardLayoutEngine extends Phobject {
|
||||||
array $columns,
|
array $columns,
|
||||||
array $positions) {
|
array $positions) {
|
||||||
|
|
||||||
|
$viewer = $this->getViewer();
|
||||||
|
|
||||||
$board_phid = $board->getPHID();
|
$board_phid = $board->getPHID();
|
||||||
$position_groups = mgroup($positions, 'getObjectPHID');
|
$position_groups = mgroup($positions, 'getObjectPHID');
|
||||||
|
|
||||||
|
@ -363,32 +420,143 @@ final class PhabricatorBoardLayoutEngine extends Phobject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Find all the columns which are proxies for other objects.
|
||||||
|
$proxy_map = array();
|
||||||
|
foreach ($columns as $column) {
|
||||||
|
$proxy_phid = $column->getProxyPHID();
|
||||||
|
if ($proxy_phid) {
|
||||||
|
$proxy_map[$proxy_phid] = $column->getPHID();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$object_phids = $this->getObjectPHIDs();
|
$object_phids = $this->getObjectPHIDs();
|
||||||
|
|
||||||
|
// If we have proxies, we need to force cards into the correct proxy
|
||||||
|
// columns.
|
||||||
|
if ($proxy_map) {
|
||||||
|
$edge_query = id(new PhabricatorEdgeQuery())
|
||||||
|
->withSourcePHIDs($object_phids)
|
||||||
|
->withEdgeTypes(
|
||||||
|
array(
|
||||||
|
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
|
||||||
|
));
|
||||||
|
$edge_query->execute();
|
||||||
|
|
||||||
|
$project_phids = $edge_query->getDestinationPHIDs();
|
||||||
|
$project_phids = array_fuse($project_phids);
|
||||||
|
} else {
|
||||||
|
$project_phids = array();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($project_phids) {
|
||||||
|
$projects = id(new PhabricatorProjectQuery())
|
||||||
|
->setViewer($viewer)
|
||||||
|
->withPHIDs($project_phids)
|
||||||
|
->execute();
|
||||||
|
$projects = mpull($projects, null, 'getPHID');
|
||||||
|
} else {
|
||||||
|
$projects = array();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build a map from every project that any task is tagged with to the
|
||||||
|
// ancestor project which has a column on this board, if one exists.
|
||||||
|
$ancestor_map = array();
|
||||||
|
foreach ($projects as $phid => $project) {
|
||||||
|
if (isset($proxy_map[$phid])) {
|
||||||
|
$ancestor_map[$phid] = $proxy_map[$phid];
|
||||||
|
} else {
|
||||||
|
$seen = array($phid);
|
||||||
|
foreach ($project->getAncestorProjects() as $ancestor) {
|
||||||
|
$ancestor_phid = $ancestor->getPHID();
|
||||||
|
$seen[] = $ancestor_phid;
|
||||||
|
if (isset($proxy_map[$ancestor_phid])) {
|
||||||
|
foreach ($seen as $project_phid) {
|
||||||
|
$ancestor_map[$project_phid] = $proxy_map[$ancestor_phid];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
foreach ($object_phids as $object_phid) {
|
foreach ($object_phids as $object_phid) {
|
||||||
$positions = idx($position_groups, $object_phid, array());
|
$positions = idx($position_groups, $object_phid, array());
|
||||||
|
|
||||||
// Remove any positions in columns which no longer exist.
|
// First, check for objects that have corresponding proxy columns. We're
|
||||||
foreach ($positions as $key => $position) {
|
// going to overwrite normal column positions if a tag belongs to a proxy
|
||||||
$column_phid = $position->getColumnPHID();
|
// column, since you can't be in normal columns if you're in proxy
|
||||||
if (empty($columns[$column_phid])) {
|
// columns.
|
||||||
$this->remQueue[] = $position;
|
$proxy_hits = array();
|
||||||
unset($positions[$key]);
|
if ($proxy_map) {
|
||||||
|
$object_project_phids = $edge_query->getDestinationPHIDs(
|
||||||
|
array(
|
||||||
|
$object_phid,
|
||||||
|
));
|
||||||
|
|
||||||
|
foreach ($object_project_phids as $project_phid) {
|
||||||
|
if (isset($ancestor_map[$project_phid])) {
|
||||||
|
$proxy_hits[] = $ancestor_map[$project_phid];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the object has no position, put it on the default column.
|
if ($proxy_hits) {
|
||||||
if (!$positions) {
|
// TODO: For now, only one column hit is permissible.
|
||||||
$new_position = id(new PhabricatorProjectColumnPosition())
|
$proxy_hits = array_slice($proxy_hits, 0, 1);
|
||||||
->setBoardPHID($board_phid)
|
|
||||||
->setColumnPHID($default_phid)
|
|
||||||
->setObjectPHID($object_phid)
|
|
||||||
->setSequence(0);
|
|
||||||
|
|
||||||
$this->addQueue[] = $new_position;
|
$proxy_hits = array_fuse($proxy_hits);
|
||||||
|
|
||||||
$positions = array(
|
// Check the object positions: we hope to find a position in each
|
||||||
$new_position,
|
// column the object should be part of. We're going to drop any
|
||||||
);
|
// invalid positions and create new positions where positions are
|
||||||
|
// missing.
|
||||||
|
foreach ($positions as $key => $position) {
|
||||||
|
$column_phid = $position->getColumnPHID();
|
||||||
|
if (isset($proxy_hits[$column_phid])) {
|
||||||
|
// Valid column, mark the position as found.
|
||||||
|
unset($proxy_hits[$column_phid]);
|
||||||
|
} else {
|
||||||
|
// Invalid column, ignore the position.
|
||||||
|
unset($positions[$key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new positions for anything we haven't found.
|
||||||
|
foreach ($proxy_hits as $proxy_hit) {
|
||||||
|
$new_position = id(new PhabricatorProjectColumnPosition())
|
||||||
|
->setBoardPHID($board_phid)
|
||||||
|
->setColumnPHID($proxy_hit)
|
||||||
|
->setObjectPHID($object_phid)
|
||||||
|
->setSequence(0);
|
||||||
|
|
||||||
|
$this->addQueue[] = $new_position;
|
||||||
|
|
||||||
|
$positions[] = $new_position;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Ignore any positions in columns which no longer exist. We don't
|
||||||
|
// actively destory them because the rest of the code ignores them and
|
||||||
|
// there's no real need to destroy the data.
|
||||||
|
foreach ($positions as $key => $position) {
|
||||||
|
$column_phid = $position->getColumnPHID();
|
||||||
|
if (empty($columns[$column_phid])) {
|
||||||
|
unset($positions[$key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the object has no position, put it on the default column.
|
||||||
|
if (!$positions) {
|
||||||
|
$new_position = id(new PhabricatorProjectColumnPosition())
|
||||||
|
->setBoardPHID($board_phid)
|
||||||
|
->setColumnPHID($default_phid)
|
||||||
|
->setObjectPHID($object_phid)
|
||||||
|
->setSequence(0);
|
||||||
|
|
||||||
|
$this->addQueue[] = $new_position;
|
||||||
|
|
||||||
|
$positions = array(
|
||||||
|
$new_position,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($positions as $position) {
|
foreach ($positions as $position) {
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
interface PhabricatorColumnProxyInterface {
|
||||||
|
|
||||||
|
public function getProxyColumnName();
|
||||||
|
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ final class PhabricatorProjectColumnQuery
|
||||||
private $ids;
|
private $ids;
|
||||||
private $phids;
|
private $phids;
|
||||||
private $projectPHIDs;
|
private $projectPHIDs;
|
||||||
|
private $proxyPHIDs;
|
||||||
private $statuses;
|
private $statuses;
|
||||||
|
|
||||||
public function withIDs(array $ids) {
|
public function withIDs(array $ids) {
|
||||||
|
@ -23,6 +24,11 @@ final class PhabricatorProjectColumnQuery
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function withProxyPHIDs(array $proxy_phids) {
|
||||||
|
$this->proxyPHIDs = $proxy_phids;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
public function withStatuses(array $status) {
|
public function withStatuses(array $status) {
|
||||||
$this->statuses = $status;
|
$this->statuses = $status;
|
||||||
return $this;
|
return $this;
|
||||||
|
@ -60,6 +66,55 @@ final class PhabricatorProjectColumnQuery
|
||||||
$column->attachProject($project);
|
$column->attachProject($project);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$proxy_phids = array_filter(mpull($page, 'getProjectPHID'));
|
||||||
|
|
||||||
|
return $page;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function didFilterPage(array $page) {
|
||||||
|
$proxy_phids = array();
|
||||||
|
foreach ($page as $column) {
|
||||||
|
$proxy_phid = $column->getProxyPHID();
|
||||||
|
if ($proxy_phid !== null) {
|
||||||
|
$proxy_phids[$proxy_phid] = $proxy_phid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($proxy_phids) {
|
||||||
|
$proxies = id(new PhabricatorObjectQuery())
|
||||||
|
->setParentQuery($this)
|
||||||
|
->setViewer($this->getViewer())
|
||||||
|
->withPHIDs($proxy_phids)
|
||||||
|
->execute();
|
||||||
|
$proxies = mpull($proxies, null, 'getPHID');
|
||||||
|
} else {
|
||||||
|
$proxies = array();
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($page as $key => $column) {
|
||||||
|
$proxy_phid = $column->getProxyPHID();
|
||||||
|
|
||||||
|
if ($proxy_phid !== null) {
|
||||||
|
$proxy = idx($proxies, $proxy_phid);
|
||||||
|
|
||||||
|
// Only attach valid proxies, so we don't end up getting surprsied if
|
||||||
|
// an install somehow gets junk into their database.
|
||||||
|
if (!($proxy instanceof PhabricatorColumnProxyInterface)) {
|
||||||
|
$proxy = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$proxy) {
|
||||||
|
$this->didRejectResult($column);
|
||||||
|
unset($page[$key]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$proxy = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$column->attachProxy($proxy);
|
||||||
|
}
|
||||||
|
|
||||||
return $page;
|
return $page;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,6 +142,13 @@ final class PhabricatorProjectColumnQuery
|
||||||
$this->projectPHIDs);
|
$this->projectPHIDs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($this->proxyPHIDs !== null) {
|
||||||
|
$where[] = qsprintf(
|
||||||
|
$conn,
|
||||||
|
'proxyPHID IN (%Ls)',
|
||||||
|
$this->proxyPHIDs);
|
||||||
|
}
|
||||||
|
|
||||||
if ($this->statuses !== null) {
|
if ($this->statuses !== null) {
|
||||||
$where[] = qsprintf(
|
$where[] = qsprintf(
|
||||||
$conn,
|
$conn,
|
||||||
|
|
|
@ -9,7 +9,8 @@ final class PhabricatorProject extends PhabricatorProjectDAO
|
||||||
PhabricatorCustomFieldInterface,
|
PhabricatorCustomFieldInterface,
|
||||||
PhabricatorDestructibleInterface,
|
PhabricatorDestructibleInterface,
|
||||||
PhabricatorFulltextInterface,
|
PhabricatorFulltextInterface,
|
||||||
PhabricatorConduitResultInterface {
|
PhabricatorConduitResultInterface,
|
||||||
|
PhabricatorColumnProxyInterface {
|
||||||
|
|
||||||
protected $name;
|
protected $name;
|
||||||
protected $status = PhabricatorProjectStatus::STATUS_ACTIVE;
|
protected $status = PhabricatorProjectStatus::STATUS_ACTIVE;
|
||||||
|
@ -663,4 +664,25 @@ final class PhabricatorProject extends PhabricatorProjectDAO
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* -( PhabricatorColumnProxyInterface )------------------------------------ */
|
||||||
|
|
||||||
|
|
||||||
|
public function getProxyColumnName() {
|
||||||
|
return $this->getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getProxyColumnIcon() {
|
||||||
|
return $this->getDisplayIconIcon();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getProxyColumnClass() {
|
||||||
|
if ($this->isMilestone()) {
|
||||||
|
return 'phui-workboard-column-milestone';
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,10 +17,12 @@ final class PhabricatorProjectColumn
|
||||||
protected $name;
|
protected $name;
|
||||||
protected $status;
|
protected $status;
|
||||||
protected $projectPHID;
|
protected $projectPHID;
|
||||||
|
protected $proxyPHID;
|
||||||
protected $sequence;
|
protected $sequence;
|
||||||
protected $properties = array();
|
protected $properties = array();
|
||||||
|
|
||||||
private $project = self::ATTACHABLE;
|
private $project = self::ATTACHABLE;
|
||||||
|
private $proxy = self::ATTACHABLE;
|
||||||
|
|
||||||
public static function initializeNewColumn(PhabricatorUser $user) {
|
public static function initializeNewColumn(PhabricatorUser $user) {
|
||||||
return id(new PhabricatorProjectColumn())
|
return id(new PhabricatorProjectColumn())
|
||||||
|
@ -38,6 +40,7 @@ final class PhabricatorProjectColumn
|
||||||
'name' => 'text255',
|
'name' => 'text255',
|
||||||
'status' => 'uint32',
|
'status' => 'uint32',
|
||||||
'sequence' => 'uint32',
|
'sequence' => 'uint32',
|
||||||
|
'proxyPHID' => 'phid?',
|
||||||
),
|
),
|
||||||
self::CONFIG_KEY_SCHEMA => array(
|
self::CONFIG_KEY_SCHEMA => array(
|
||||||
'key_status' => array(
|
'key_status' => array(
|
||||||
|
@ -46,6 +49,10 @@ final class PhabricatorProjectColumn
|
||||||
'key_sequence' => array(
|
'key_sequence' => array(
|
||||||
'columns' => array('projectPHID', 'sequence'),
|
'columns' => array('projectPHID', 'sequence'),
|
||||||
),
|
),
|
||||||
|
'key_proxy' => array(
|
||||||
|
'columns' => array('projectPHID', 'proxyPHID'),
|
||||||
|
'unique' => true,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
) + parent::getConfiguration();
|
) + parent::getConfiguration();
|
||||||
}
|
}
|
||||||
|
@ -64,6 +71,15 @@ final class PhabricatorProjectColumn
|
||||||
return $this->assertAttached($this->project);
|
return $this->assertAttached($this->project);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function attachProxy($proxy) {
|
||||||
|
$this->proxy = $proxy;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getProxy() {
|
||||||
|
return $this->assertAttached($this->proxy);
|
||||||
|
}
|
||||||
|
|
||||||
public function isDefaultColumn() {
|
public function isDefaultColumn() {
|
||||||
return (bool)$this->getProperty('isDefault');
|
return (bool)$this->getProperty('isDefault');
|
||||||
}
|
}
|
||||||
|
@ -73,6 +89,11 @@ final class PhabricatorProjectColumn
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getDisplayName() {
|
public function getDisplayName() {
|
||||||
|
$proxy = $this->getProxy();
|
||||||
|
if ($proxy) {
|
||||||
|
return $proxy->getProxyColumnName();
|
||||||
|
}
|
||||||
|
|
||||||
$name = $this->getName();
|
$name = $this->getName();
|
||||||
if (strlen($name)) {
|
if (strlen($name)) {
|
||||||
return $name;
|
return $name;
|
||||||
|
@ -96,11 +117,23 @@ final class PhabricatorProjectColumn
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getDisplayClass() {
|
||||||
|
$proxy = $this->getProxy();
|
||||||
|
if ($proxy) {
|
||||||
|
return $proxy->getProxyColumnClass();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public function getHeaderIcon() {
|
public function getHeaderIcon() {
|
||||||
$icon = null;
|
$proxy = $this->getProxy();
|
||||||
|
if ($proxy) {
|
||||||
|
return $proxy->getProxyColumnIcon();
|
||||||
|
}
|
||||||
|
|
||||||
if ($this->isHidden()) {
|
if ($this->isHidden()) {
|
||||||
$icon = 'fa-eye-slash';
|
return 'fa-eye-slash';
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -162,7 +162,6 @@ subprojects, parent projects, and milestones.
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| //Members// | Yes | Union of Subprojects | Yes | Same as Parent |
|
| //Members// | Yes | Union of Subprojects | Yes | Same as Parent |
|
||||||
| //Policies// | Yes | Yes | Affected by Parent | Same as Parent |
|
| //Policies// | Yes | Yes | Affected by Parent | Same as Parent |
|
||||||
| //Workboard// | Yes | No Custom Columns | Yes | Yes |
|
|
||||||
| //Hashtags// | Yes | Yes | Yes | Special |
|
| //Hashtags// | Yes | Yes | Yes | Special |
|
||||||
|
|
||||||
|
|
||||||
|
@ -257,14 +256,6 @@ parent project is an ancestor of the new subproject.
|
||||||
You can edit the project afterward to change or remove members if you want to
|
You can edit the project afterward to change or remove members if you want to
|
||||||
split membership apart in a more granular way across multiple new subprojects.
|
split membership apart in a more granular way across multiple new subprojects.
|
||||||
|
|
||||||
**No Workboard Columns**: Parent projects can not have their own workboard
|
|
||||||
columns: instead, the workboard of a parent project shows columns representing
|
|
||||||
the child projects.
|
|
||||||
|
|
||||||
Thus, a project's workboard columns are destroyed when you add the first
|
|
||||||
subproject. All objects on the workboard will be returned to the project's
|
|
||||||
backlog. The new board will show columns for subprojects instead.
|
|
||||||
|
|
||||||
**Searching**: When you search for a parent project, results for any subproject
|
**Searching**: When you search for a parent project, results for any subproject
|
||||||
are returned. For example, if you search for {nav Engineering}, your query will
|
are returned. For example, if you search for {nav Engineering}, your query will
|
||||||
match results in {nav Engineering} itself, but also subprojects like
|
match results in {nav Engineering} itself, but also subprojects like
|
||||||
|
|
|
@ -10,8 +10,8 @@ final class PHUIWorkpanelView extends AphrontTagView {
|
||||||
private $headerTag;
|
private $headerTag;
|
||||||
private $headerIcon;
|
private $headerIcon;
|
||||||
|
|
||||||
public function setHeaderIcon(PHUIIconView $header_icon) {
|
public function setHeaderIcon($icon) {
|
||||||
$this->headerIcon = $header_icon;
|
$this->headerIcon = $icon;
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -280,13 +280,16 @@ JX.behavior('project-boards', function(config, statics) {
|
||||||
// close the dropdown, but don't want to follow the link.
|
// close the dropdown, but don't want to follow the link.
|
||||||
e.prevent();
|
e.prevent();
|
||||||
|
|
||||||
var column_phid = e.getNodeData('column-add-task').columnPHID;
|
var column_data = e.getNodeData('column-add-task');
|
||||||
|
var column_phid = column_data.columnPHID;
|
||||||
|
|
||||||
var request_data = {
|
var request_data = {
|
||||||
responseType: 'card',
|
responseType: 'card',
|
||||||
columnPHID: column_phid,
|
columnPHID: column_phid,
|
||||||
projects: statics.projectPHID,
|
projects: column_data.projectPHID,
|
||||||
order: statics.order
|
order: statics.order
|
||||||
};
|
};
|
||||||
|
|
||||||
var cols = getcolumns();
|
var cols = getcolumns();
|
||||||
var ii;
|
var ii;
|
||||||
var column;
|
var column;
|
||||||
|
|
Loading…
Reference in a new issue