mirror of
https://we.phorge.it/source/phorge.git
synced 2024-12-18 03:20:59 +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/policy/behavior-policy-control.js' => 'ae45872f',
|
||||
'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-reorder-columns.js' => 'e1d25dfb',
|
||||
'rsrc/js/application/releeph/releeph-preview-branch.js' => 'b2b4fbaf',
|
||||
|
@ -653,7 +653,7 @@ return array(
|
|||
'javelin-behavior-phui-profile-menu' => '12884df9',
|
||||
'javelin-behavior-policy-control' => 'ae45872f',
|
||||
'javelin-behavior-policy-rule-editor' => '5e9f347c',
|
||||
'javelin-behavior-project-boards' => 'c05fb42a',
|
||||
'javelin-behavior-project-boards' => '48470f95',
|
||||
'javelin-behavior-project-create' => '065227cc',
|
||||
'javelin-behavior-quicksand-blacklist' => '7927a7d3',
|
||||
'javelin-behavior-recurring-edit' => '5f1c4d5f',
|
||||
|
@ -1151,6 +1151,15 @@ return array(
|
|||
'javelin-dom',
|
||||
'javelin-workflow',
|
||||
),
|
||||
'48470f95' => array(
|
||||
'javelin-behavior',
|
||||
'javelin-dom',
|
||||
'javelin-util',
|
||||
'javelin-vector',
|
||||
'javelin-stratcom',
|
||||
'javelin-workflow',
|
||||
'phabricator-draggable-list',
|
||||
),
|
||||
'49b73b36' => array(
|
||||
'javelin-behavior',
|
||||
'javelin-dom',
|
||||
|
@ -1779,15 +1788,6 @@ return array(
|
|||
'javelin-install',
|
||||
'javelin-dom',
|
||||
),
|
||||
'c05fb42a' => array(
|
||||
'javelin-behavior',
|
||||
'javelin-dom',
|
||||
'javelin-util',
|
||||
'javelin-vector',
|
||||
'javelin-stratcom',
|
||||
'javelin-workflow',
|
||||
'phabricator-draggable-list',
|
||||
),
|
||||
'c1700f6f' => array(
|
||||
'javelin-install',
|
||||
'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',
|
||||
'PhabricatorChunkedFileStorageEngine' => 'applications/files/engine/PhabricatorChunkedFileStorageEngine.php',
|
||||
'PhabricatorClusterConfigOptions' => 'applications/config/option/PhabricatorClusterConfigOptions.php',
|
||||
'PhabricatorColumnProxyInterface' => 'applications/project/interface/PhabricatorColumnProxyInterface.php',
|
||||
'PhabricatorCommentEditEngineExtension' => 'applications/transactions/engineextension/PhabricatorCommentEditEngineExtension.php',
|
||||
'PhabricatorCommentEditField' => 'applications/transactions/editfield/PhabricatorCommentEditField.php',
|
||||
'PhabricatorCommentEditType' => 'applications/transactions/edittype/PhabricatorCommentEditType.php',
|
||||
|
@ -7262,6 +7263,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorDestructibleInterface',
|
||||
'PhabricatorFulltextInterface',
|
||||
'PhabricatorConduitResultInterface',
|
||||
'PhabricatorColumnProxyInterface',
|
||||
),
|
||||
'PhabricatorProjectAddHeraldAction' => 'PhabricatorProjectHeraldAction',
|
||||
'PhabricatorProjectApplication' => 'PhabricatorApplication',
|
||||
|
|
|
@ -280,10 +280,23 @@ final class ManiphestEditEngine
|
|||
return new Aphront404Response();
|
||||
}
|
||||
|
||||
// If the workboard's project has been removed from the card's project
|
||||
// list, we are going to remove it from the board completely.
|
||||
// If the workboard's project and all descendant projects have been removed
|
||||
// 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());
|
||||
$remove_card = empty($project_map[$column->getProjectPHID()]);
|
||||
$remove_card = !array_intersect_key($board_phids, $project_map);
|
||||
|
||||
$positions = id(new PhabricatorProjectColumnPositionQuery())
|
||||
->setViewer($viewer)
|
||||
|
|
|
@ -222,12 +222,22 @@ final class ManiphestTransactionEditor
|
|||
// can't see.
|
||||
$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())
|
||||
->setViewer($omnipotent_viewer)
|
||||
->withEdgeLogicPHIDs(
|
||||
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
|
||||
PhabricatorQueryConstraint::OPERATOR_AND,
|
||||
array($board_phid))
|
||||
PhabricatorQueryConstraint::OPERATOR_ANCESTOR,
|
||||
array($select_phids))
|
||||
->execute();
|
||||
|
||||
$object_phids = mpull($board_tasks, 'getPHID');
|
||||
|
|
|
@ -972,7 +972,45 @@ final class PhabricatorProjectCoreTestCase extends PhabricatorTestCase {
|
|||
$task1->getPHID(),
|
||||
);
|
||||
$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(
|
||||
|
@ -1014,7 +1052,14 @@ final class PhabricatorProjectCoreTestCase extends PhabricatorTestCase {
|
|||
PhabricatorUser $viewer,
|
||||
PhabricatorProject $board,
|
||||
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())
|
||||
->setViewer($viewer)
|
||||
->setBoardPHIDs(array($board->getPHID()))
|
||||
|
@ -1028,7 +1073,7 @@ final class PhabricatorProjectCoreTestCase extends PhabricatorTestCase {
|
|||
$column_phids = mpull($columns, 'getPHID');
|
||||
$column_phids = array_values($column_phids);
|
||||
|
||||
$this->assertEqual($expect, $column_phids);
|
||||
return $column_phids;
|
||||
}
|
||||
|
||||
private function assertTasksInColumn(
|
||||
|
@ -1236,6 +1281,16 @@ final class PhabricatorProjectCoreTestCase extends PhabricatorTestCase {
|
|||
|
||||
$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;
|
||||
}
|
||||
|
||||
|
|
|
@ -95,17 +95,27 @@ final class PhabricatorProjectBoardViewController
|
|||
|
||||
$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
|
||||
->withEdgeLogicPHIDs(
|
||||
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
|
||||
PhabricatorQueryConstraint::OPERATOR_AND,
|
||||
array($project->getPHID()))
|
||||
PhabricatorQueryConstraint::OPERATOR_ANCESTOR,
|
||||
array($select_phids))
|
||||
->setOrder(ManiphestTaskQuery::ORDER_PRIORITY)
|
||||
->setViewer($viewer)
|
||||
->execute();
|
||||
$tasks = mpull($tasks, null, 'getPHID');
|
||||
|
||||
|
||||
$board_phid = $project->getPHID();
|
||||
|
||||
$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(
|
||||
$board_phid,
|
||||
$column->getPHID());
|
||||
|
@ -247,6 +264,11 @@ final class PhabricatorProjectBoardViewController
|
|||
$panel->setHeaderIcon($header_icon);
|
||||
}
|
||||
|
||||
$display_class = $column->getDisplayClass();
|
||||
if ($display_class) {
|
||||
$panel->addClass($display_class);
|
||||
}
|
||||
|
||||
if ($column->isHidden()) {
|
||||
$panel->addClass('project-panel-hidden');
|
||||
}
|
||||
|
@ -582,6 +604,12 @@ final class PhabricatorProjectBoardViewController
|
|||
|
||||
$column_items = array();
|
||||
|
||||
if ($column->getProxyPHID()) {
|
||||
$default_phid = $column->getProxyPHID();
|
||||
} else {
|
||||
$default_phid = $column->getProjectPHID();
|
||||
}
|
||||
|
||||
$column_items[] = id(new PhabricatorActionView())
|
||||
->setIcon('fa-plus')
|
||||
->setName(pht('Create Task...'))
|
||||
|
@ -590,6 +618,7 @@ final class PhabricatorProjectBoardViewController
|
|||
->setMetadata(
|
||||
array(
|
||||
'columnPHID' => $column->getPHID(),
|
||||
'projectPHID' => $default_phid,
|
||||
));
|
||||
|
||||
$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())
|
||||
->setLabel(pht('Columns'))
|
||||
->setName('initialize-type')
|
||||
|
|
|
@ -139,7 +139,33 @@ final class PhabricatorProjectMoveController
|
|||
->setTransactionType(ManiphestTransaction::TYPE_SUBPRIORITY)
|
||||
->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())
|
||||
->setActor($viewer)
|
||||
|
@ -157,6 +183,18 @@ final class PhabricatorProjectMoveController
|
|||
->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())
|
||||
->setViewer($viewer)
|
||||
->setTask($object)
|
||||
|
@ -169,6 +207,6 @@ final class PhabricatorProjectMoveController
|
|||
|
||||
return id(new AphrontAjaxResponse())->setContent(
|
||||
array('task' => $card));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ final class PhabricatorProjectSubprojectWarningController
|
|||
|
||||
$conversion_help = pht(
|
||||
"Creating a project's first subproject **moves all ".
|
||||
"members** and **destroys all workboard columns**.".
|
||||
"members** to become members of the subproject instead".
|
||||
"\n\n".
|
||||
"See [[ %s | Projects User Guide ]] in the documentation for details. ".
|
||||
"This process can not be undone.",
|
||||
|
|
|
@ -320,8 +320,63 @@ final class PhabricatorBoardLayoutEngine extends Phobject {
|
|||
$columns = msort($columns, 'getSequence');
|
||||
$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');
|
||||
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;
|
||||
}
|
||||
|
@ -350,6 +405,8 @@ final class PhabricatorBoardLayoutEngine extends Phobject {
|
|||
array $columns,
|
||||
array $positions) {
|
||||
|
||||
$viewer = $this->getViewer();
|
||||
|
||||
$board_phid = $board->getPHID();
|
||||
$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();
|
||||
|
||||
// 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) {
|
||||
$positions = idx($position_groups, $object_phid, array());
|
||||
|
||||
// Remove any positions in columns which no longer exist.
|
||||
foreach ($positions as $key => $position) {
|
||||
$column_phid = $position->getColumnPHID();
|
||||
if (empty($columns[$column_phid])) {
|
||||
$this->remQueue[] = $position;
|
||||
unset($positions[$key]);
|
||||
// First, check for objects that have corresponding proxy columns. We're
|
||||
// going to overwrite normal column positions if a tag belongs to a proxy
|
||||
// column, since you can't be in normal columns if you're in proxy
|
||||
// columns.
|
||||
$proxy_hits = array();
|
||||
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 (!$positions) {
|
||||
$new_position = id(new PhabricatorProjectColumnPosition())
|
||||
->setBoardPHID($board_phid)
|
||||
->setColumnPHID($default_phid)
|
||||
->setObjectPHID($object_phid)
|
||||
->setSequence(0);
|
||||
if ($proxy_hits) {
|
||||
// TODO: For now, only one column hit is permissible.
|
||||
$proxy_hits = array_slice($proxy_hits, 0, 1);
|
||||
|
||||
$this->addQueue[] = $new_position;
|
||||
$proxy_hits = array_fuse($proxy_hits);
|
||||
|
||||
$positions = array(
|
||||
$new_position,
|
||||
);
|
||||
// Check the object positions: we hope to find a position in each
|
||||
// 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) {
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
|
||||
interface PhabricatorColumnProxyInterface {
|
||||
|
||||
public function getProxyColumnName();
|
||||
|
||||
}
|
|
@ -6,6 +6,7 @@ final class PhabricatorProjectColumnQuery
|
|||
private $ids;
|
||||
private $phids;
|
||||
private $projectPHIDs;
|
||||
private $proxyPHIDs;
|
||||
private $statuses;
|
||||
|
||||
public function withIDs(array $ids) {
|
||||
|
@ -23,6 +24,11 @@ final class PhabricatorProjectColumnQuery
|
|||
return $this;
|
||||
}
|
||||
|
||||
public function withProxyPHIDs(array $proxy_phids) {
|
||||
$this->proxyPHIDs = $proxy_phids;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function withStatuses(array $status) {
|
||||
$this->statuses = $status;
|
||||
return $this;
|
||||
|
@ -60,6 +66,55 @@ final class PhabricatorProjectColumnQuery
|
|||
$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;
|
||||
}
|
||||
|
||||
|
@ -87,6 +142,13 @@ final class PhabricatorProjectColumnQuery
|
|||
$this->projectPHIDs);
|
||||
}
|
||||
|
||||
if ($this->proxyPHIDs !== null) {
|
||||
$where[] = qsprintf(
|
||||
$conn,
|
||||
'proxyPHID IN (%Ls)',
|
||||
$this->proxyPHIDs);
|
||||
}
|
||||
|
||||
if ($this->statuses !== null) {
|
||||
$where[] = qsprintf(
|
||||
$conn,
|
||||
|
|
|
@ -9,7 +9,8 @@ final class PhabricatorProject extends PhabricatorProjectDAO
|
|||
PhabricatorCustomFieldInterface,
|
||||
PhabricatorDestructibleInterface,
|
||||
PhabricatorFulltextInterface,
|
||||
PhabricatorConduitResultInterface {
|
||||
PhabricatorConduitResultInterface,
|
||||
PhabricatorColumnProxyInterface {
|
||||
|
||||
protected $name;
|
||||
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 $status;
|
||||
protected $projectPHID;
|
||||
protected $proxyPHID;
|
||||
protected $sequence;
|
||||
protected $properties = array();
|
||||
|
||||
private $project = self::ATTACHABLE;
|
||||
private $proxy = self::ATTACHABLE;
|
||||
|
||||
public static function initializeNewColumn(PhabricatorUser $user) {
|
||||
return id(new PhabricatorProjectColumn())
|
||||
|
@ -38,6 +40,7 @@ final class PhabricatorProjectColumn
|
|||
'name' => 'text255',
|
||||
'status' => 'uint32',
|
||||
'sequence' => 'uint32',
|
||||
'proxyPHID' => 'phid?',
|
||||
),
|
||||
self::CONFIG_KEY_SCHEMA => array(
|
||||
'key_status' => array(
|
||||
|
@ -46,6 +49,10 @@ final class PhabricatorProjectColumn
|
|||
'key_sequence' => array(
|
||||
'columns' => array('projectPHID', 'sequence'),
|
||||
),
|
||||
'key_proxy' => array(
|
||||
'columns' => array('projectPHID', 'proxyPHID'),
|
||||
'unique' => true,
|
||||
),
|
||||
),
|
||||
) + parent::getConfiguration();
|
||||
}
|
||||
|
@ -64,6 +71,15 @@ final class PhabricatorProjectColumn
|
|||
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() {
|
||||
return (bool)$this->getProperty('isDefault');
|
||||
}
|
||||
|
@ -73,6 +89,11 @@ final class PhabricatorProjectColumn
|
|||
}
|
||||
|
||||
public function getDisplayName() {
|
||||
$proxy = $this->getProxy();
|
||||
if ($proxy) {
|
||||
return $proxy->getProxyColumnName();
|
||||
}
|
||||
|
||||
$name = $this->getName();
|
||||
if (strlen($name)) {
|
||||
return $name;
|
||||
|
@ -96,11 +117,23 @@ final class PhabricatorProjectColumn
|
|||
return null;
|
||||
}
|
||||
|
||||
public function getDisplayClass() {
|
||||
$proxy = $this->getProxy();
|
||||
if ($proxy) {
|
||||
return $proxy->getProxyColumnClass();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getHeaderIcon() {
|
||||
$icon = null;
|
||||
$proxy = $this->getProxy();
|
||||
if ($proxy) {
|
||||
return $proxy->getProxyColumnIcon();
|
||||
}
|
||||
|
||||
if ($this->isHidden()) {
|
||||
$icon = 'fa-eye-slash';
|
||||
return 'fa-eye-slash';
|
||||
}
|
||||
|
||||
return null;
|
||||
|
|
|
@ -162,7 +162,6 @@ subprojects, parent projects, and milestones.
|
|||
|---|---|---|---|---|
|
||||
| //Members// | Yes | Union of Subprojects | Yes | Same as Parent |
|
||||
| //Policies// | Yes | Yes | Affected by Parent | Same as Parent |
|
||||
| //Workboard// | Yes | No Custom Columns | Yes | Yes |
|
||||
| //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
|
||||
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
|
||||
are returned. For example, if you search for {nav Engineering}, your query will
|
||||
match results in {nav Engineering} itself, but also subprojects like
|
||||
|
|
|
@ -10,8 +10,8 @@ final class PHUIWorkpanelView extends AphrontTagView {
|
|||
private $headerTag;
|
||||
private $headerIcon;
|
||||
|
||||
public function setHeaderIcon(PHUIIconView $header_icon) {
|
||||
$this->headerIcon = $header_icon;
|
||||
public function setHeaderIcon($icon) {
|
||||
$this->headerIcon = $icon;
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
|
|
@ -280,13 +280,16 @@ JX.behavior('project-boards', function(config, statics) {
|
|||
// close the dropdown, but don't want to follow the link.
|
||||
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 = {
|
||||
responseType: 'card',
|
||||
columnPHID: column_phid,
|
||||
projects: statics.projectPHID,
|
||||
projects: column_data.projectPHID,
|
||||
order: statics.order
|
||||
};
|
||||
|
||||
var cols = getcolumns();
|
||||
var ii;
|
||||
var column;
|
||||
|
|
Loading…
Reference in a new issue