@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

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

+496 -57
resources/builtin/image-200x200.png

This is a binary file and will not be displayed.

+11 -11
resources/celerity/map.php
··· 413 413 'rsrc/js/application/phortune/phortune-credit-card-form.js' => '2290aeef', 414 414 'rsrc/js/application/policy/behavior-policy-control.js' => 'ae45872f', 415 415 'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '5e9f347c', 416 - 'rsrc/js/application/projects/behavior-project-boards.js' => 'c05fb42a', 416 + 'rsrc/js/application/projects/behavior-project-boards.js' => '48470f95', 417 417 'rsrc/js/application/projects/behavior-project-create.js' => '065227cc', 418 418 'rsrc/js/application/projects/behavior-reorder-columns.js' => 'e1d25dfb', 419 419 'rsrc/js/application/releeph/releeph-preview-branch.js' => 'b2b4fbaf', ··· 653 653 'javelin-behavior-phui-profile-menu' => '12884df9', 654 654 'javelin-behavior-policy-control' => 'ae45872f', 655 655 'javelin-behavior-policy-rule-editor' => '5e9f347c', 656 - 'javelin-behavior-project-boards' => 'c05fb42a', 656 + 'javelin-behavior-project-boards' => '48470f95', 657 657 'javelin-behavior-project-create' => '065227cc', 658 658 'javelin-behavior-quicksand-blacklist' => '7927a7d3', 659 659 'javelin-behavior-recurring-edit' => '5f1c4d5f', ··· 1151 1151 'javelin-dom', 1152 1152 'javelin-workflow', 1153 1153 ), 1154 + '48470f95' => array( 1155 + 'javelin-behavior', 1156 + 'javelin-dom', 1157 + 'javelin-util', 1158 + 'javelin-vector', 1159 + 'javelin-stratcom', 1160 + 'javelin-workflow', 1161 + 'phabricator-draggable-list', 1162 + ), 1154 1163 '49b73b36' => array( 1155 1164 'javelin-behavior', 1156 1165 'javelin-dom', ··· 1778 1787 'bff6884b' => array( 1779 1788 'javelin-install', 1780 1789 'javelin-dom', 1781 - ), 1782 - 'c05fb42a' => array( 1783 - 'javelin-behavior', 1784 - 'javelin-dom', 1785 - 'javelin-util', 1786 - 'javelin-vector', 1787 - 'javelin-stratcom', 1788 - 'javelin-workflow', 1789 - 'phabricator-draggable-list', 1790 1790 ), 1791 1791 'c1700f6f' => array( 1792 1792 'javelin-install',
+2
resources/sql/autopatches/20160202.board.1.proxy.sql
··· 1 + ALTER TABLE {$NAMESPACE}_project.project_column 2 + ADD proxyPHID VARBINARY(64);
+2
src/__phutil_library_map__.php
··· 1889 1889 'PhabricatorChatLogQuery' => 'applications/chatlog/query/PhabricatorChatLogQuery.php', 1890 1890 'PhabricatorChunkedFileStorageEngine' => 'applications/files/engine/PhabricatorChunkedFileStorageEngine.php', 1891 1891 'PhabricatorClusterConfigOptions' => 'applications/config/option/PhabricatorClusterConfigOptions.php', 1892 + 'PhabricatorColumnProxyInterface' => 'applications/project/interface/PhabricatorColumnProxyInterface.php', 1892 1893 'PhabricatorCommentEditEngineExtension' => 'applications/transactions/engineextension/PhabricatorCommentEditEngineExtension.php', 1893 1894 'PhabricatorCommentEditField' => 'applications/transactions/editfield/PhabricatorCommentEditField.php', 1894 1895 'PhabricatorCommentEditType' => 'applications/transactions/edittype/PhabricatorCommentEditType.php', ··· 7262 7263 'PhabricatorDestructibleInterface', 7263 7264 'PhabricatorFulltextInterface', 7264 7265 'PhabricatorConduitResultInterface', 7266 + 'PhabricatorColumnProxyInterface', 7265 7267 ), 7266 7268 'PhabricatorProjectAddHeraldAction' => 'PhabricatorProjectHeraldAction', 7267 7269 'PhabricatorProjectApplication' => 'PhabricatorApplication',
+16 -3
src/applications/maniphest/editor/ManiphestEditEngine.php
··· 280 280 return new Aphront404Response(); 281 281 } 282 282 283 - // If the workboard's project has been removed from the card's project 284 - // list, we are going to remove it from the board completely. 283 + // If the workboard's project and all descendant projects have been removed 284 + // from the card's project list, we are going to remove it from the board 285 + // completely. 286 + 287 + // TODO: If the user did something sneaky and changed a subproject, we'll 288 + // currently leave the card where it was but should really move it to the 289 + // proper new column. 290 + 291 + $descendant_projects = id(new PhabricatorProjectQuery()) 292 + ->setViewer($viewer) 293 + ->withAncestorProjectPHIDs(array($column->getProjectPHID())) 294 + ->execute(); 295 + $board_phids = mpull($descendant_projects, 'getPHID', 'getPHID'); 296 + $board_phids[$column->getProjectPHID()] = $column->getProjectPHID(); 297 + 285 298 $project_map = array_fuse($task->getProjectPHIDs()); 286 - $remove_card = empty($project_map[$column->getProjectPHID()]); 299 + $remove_card = !array_intersect_key($board_phids, $project_map); 287 300 288 301 $positions = id(new PhabricatorProjectColumnPositionQuery()) 289 302 ->setViewer($viewer)
+12 -2
src/applications/maniphest/editor/ManiphestTransactionEditor.php
··· 222 222 // can't see. 223 223 $omnipotent_viewer = PhabricatorUser::getOmnipotentUser(); 224 224 225 + $select_phids = array($board_phid); 226 + 227 + $descendants = id(new PhabricatorProjectQuery()) 228 + ->setViewer($omnipotent_viewer) 229 + ->withAncestorProjectPHIDs($select_phids) 230 + ->execute(); 231 + foreach ($descendants as $descendant) { 232 + $select_phids[] = $descendant->getPHID(); 233 + } 234 + 225 235 $board_tasks = id(new ManiphestTaskQuery()) 226 236 ->setViewer($omnipotent_viewer) 227 237 ->withEdgeLogicPHIDs( 228 238 PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, 229 - PhabricatorQueryConstraint::OPERATOR_AND, 230 - array($board_phid)) 239 + PhabricatorQueryConstraint::OPERATOR_ANCESTOR, 240 + array($select_phids)) 231 241 ->execute(); 232 242 233 243 $object_phids = mpull($board_tasks, 'getPHID');
+56 -1
src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php
··· 972 972 $task1->getPHID(), 973 973 ); 974 974 $this->assertTasksInColumn($expect, $user, $board, $column); 975 + } 976 + 977 + public function testMilestoneMoves() { 978 + $user = $this->createUser(); 979 + $user->save(); 980 + 981 + $board = $this->createProject($user); 982 + 983 + $backlog = $this->addColumn($user, $board, 0); 984 + 985 + // Create a task into the backlog. 986 + $task = $this->newTask($user, array($board)); 987 + $expect = array( 988 + $backlog->getPHID(), 989 + ); 990 + $this->assertColumns($expect, $user, $board, $task); 991 + 992 + $milestone = $this->createProject($user, $board, true); 993 + 994 + $this->addProjectTags($user, $task, array($milestone->getPHID())); 995 + 996 + // We just want the side effect of looking at the board: creation of the 997 + // milestone column. 998 + $this->loadColumns($user, $board, $task); 975 999 1000 + $column = id(new PhabricatorProjectColumnQuery()) 1001 + ->setViewer($user) 1002 + ->withProjectPHIDs(array($board->getPHID())) 1003 + ->withProxyPHIDs(array($milestone->getPHID())) 1004 + ->executeOne(); 1005 + 1006 + $this->assertTrue((bool)$column); 1007 + 1008 + // Moving the task to the milestone should have moved it to the milestone 1009 + // column. 1010 + $expect = array( 1011 + $column->getPHID(), 1012 + ); 1013 + $this->assertColumns($expect, $user, $board, $task); 976 1014 } 977 1015 978 1016 private function moveToColumn( ··· 1014 1052 PhabricatorUser $viewer, 1015 1053 PhabricatorProject $board, 1016 1054 ManiphestTask $task) { 1055 + $column_phids = $this->loadColumns($viewer, $board, $task); 1056 + $this->assertEqual($expect, $column_phids); 1057 + } 1017 1058 1059 + private function loadColumns( 1060 + PhabricatorUser $viewer, 1061 + PhabricatorProject $board, 1062 + ManiphestTask $task) { 1018 1063 $engine = id(new PhabricatorBoardLayoutEngine()) 1019 1064 ->setViewer($viewer) 1020 1065 ->setBoardPHIDs(array($board->getPHID())) ··· 1028 1073 $column_phids = mpull($columns, 'getPHID'); 1029 1074 $column_phids = array_values($column_phids); 1030 1075 1031 - $this->assertEqual($expect, $column_phids); 1076 + return $column_phids; 1032 1077 } 1033 1078 1034 1079 private function assertTasksInColumn( ··· 1235 1280 } 1236 1281 1237 1282 $this->applyTransactions($project, $user, $xactions); 1283 + 1284 + // Force these values immediately; they are normally updated by the 1285 + // index engine. 1286 + if ($parent) { 1287 + if ($is_milestone) { 1288 + $parent->setHasMilestones(1)->save(); 1289 + } else { 1290 + $parent->setHasSubprojects(1)->save(); 1291 + } 1292 + } 1238 1293 1239 1294 return $project; 1240 1295 }
+36 -3
src/applications/project/controller/PhabricatorProjectBoardViewController.php
··· 95 95 96 96 $task_query = $search_engine->buildQueryFromSavedQuery($saved); 97 97 98 + $select_phids = array($project->getPHID()); 99 + if ($project->getHasSubprojects() || $project->getHasMilestones()) { 100 + $descendants = id(new PhabricatorProjectQuery()) 101 + ->setViewer($viewer) 102 + ->withAncestorProjectPHIDs($select_phids) 103 + ->execute(); 104 + foreach ($descendants as $descendant) { 105 + $select_phids[] = $descendant->getPHID(); 106 + } 107 + } 108 + 98 109 $tasks = $task_query 99 110 ->withEdgeLogicPHIDs( 100 111 PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, 101 - PhabricatorQueryConstraint::OPERATOR_AND, 102 - array($project->getPHID())) 112 + PhabricatorQueryConstraint::OPERATOR_ANCESTOR, 113 + array($select_phids)) 103 114 ->setOrder(ManiphestTaskQuery::ORDER_PRIORITY) 104 115 ->setViewer($viewer) 105 116 ->execute(); 106 117 $tasks = mpull($tasks, null, 'getPHID'); 107 - 108 118 109 119 $board_phid = $project->getPHID(); 110 120 ··· 225 235 } 226 236 } 227 237 238 + $proxy = $column->getProxy(); 239 + if ($proxy && !$proxy->isMilestone()) { 240 + // TODO: For now, don't show subproject columns because we can't 241 + // handle tasks with multiple positions yet. 242 + continue; 243 + } 244 + 228 245 $task_phids = $layout_engine->getColumnObjectPHIDs( 229 246 $board_phid, 230 247 $column->getPHID()); ··· 245 262 $header_icon = $column->getHeaderIcon(); 246 263 if ($header_icon) { 247 264 $panel->setHeaderIcon($header_icon); 265 + } 266 + 267 + $display_class = $column->getDisplayClass(); 268 + if ($display_class) { 269 + $panel->addClass($display_class); 248 270 } 249 271 250 272 if ($column->isHidden()) { ··· 582 604 583 605 $column_items = array(); 584 606 607 + if ($column->getProxyPHID()) { 608 + $default_phid = $column->getProxyPHID(); 609 + } else { 610 + $default_phid = $column->getProjectPHID(); 611 + } 612 + 585 613 $column_items[] = id(new PhabricatorActionView()) 586 614 ->setIcon('fa-plus') 587 615 ->setName(pht('Create Task...')) ··· 590 618 ->setMetadata( 591 619 array( 592 620 'columnPHID' => $column->getPHID(), 621 + 'projectPHID' => $default_phid, 593 622 )); 594 623 595 624 $batch_edit_uri = $request->getRequestURI(); ··· 737 766 ->setURI($import_uri); 738 767 } 739 768 } 769 + 770 + // TODO: Tailor this UI if the project is already a parent project. We 771 + // should not offer options for creating a parent project workboard, since 772 + // they can't have their own columns. 740 773 741 774 $new_selector = id(new AphrontFormRadioButtonControl()) 742 775 ->setLabel(pht('Columns'))
+40 -2
src/applications/project/controller/PhabricatorProjectMoveController.php
··· 139 139 ->setTransactionType(ManiphestTransaction::TYPE_SUBPRIORITY) 140 140 ->setNewValue($sub); 141 141 } 142 - } 142 + } 143 + 144 + $proxy = $column->getProxy(); 145 + if ($proxy) { 146 + // We're moving the task into a subproject or milestone column, so add 147 + // the subproject or milestone. 148 + $add_projects = array($proxy->getPHID()); 149 + } else if ($project->getHasSubprojects() || $project->getHasMilestones()) { 150 + // We're moving the task into the "Backlog" column on the parent project, 151 + // so add the parent explicitly. This gets rid of any subproject or 152 + // milestone tags. 153 + $add_projects = array($project->getPHID()); 154 + } else { 155 + $add_projects = array(); 156 + } 157 + 158 + if ($add_projects) { 159 + $project_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST; 160 + 161 + $xactions[] = id(new ManiphestTransaction()) 162 + ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) 163 + ->setMetadataValue('edge:type', $project_type) 164 + ->setNewValue( 165 + array( 166 + '+' => array_fuse($add_projects), 167 + )); 168 + } 143 169 144 170 $editor = id(new ManiphestTransactionEditor()) 145 171 ->setActor($viewer) ··· 157 183 ->executeOne(); 158 184 } 159 185 186 + // Reload the object so it reflects edits which have been applied. 187 + $object = id(new ManiphestTaskQuery()) 188 + ->setViewer($viewer) 189 + ->withPHIDs(array($object_phid)) 190 + ->needProjectPHIDs(true) 191 + ->requireCapabilities( 192 + array( 193 + PhabricatorPolicyCapability::CAN_VIEW, 194 + PhabricatorPolicyCapability::CAN_EDIT, 195 + )) 196 + ->executeOne(); 197 + 160 198 $card = id(new ProjectBoardTaskCard()) 161 199 ->setViewer($viewer) 162 200 ->setTask($object) ··· 169 207 170 208 return id(new AphrontAjaxResponse())->setContent( 171 209 array('task' => $card)); 172 - } 210 + } 173 211 174 212 }
+1 -1
src/applications/project/controller/PhabricatorProjectSubprojectWarningController.php
··· 35 35 36 36 $conversion_help = pht( 37 37 "Creating a project's first subproject **moves all ". 38 - "members** and **destroys all workboard columns**.". 38 + "members** to become members of the subproject instead". 39 39 "\n\n". 40 40 "See [[ %s | Projects User Guide ]] in the documentation for details. ". 41 41 "This process can not be undone.",
+186 -18
src/applications/project/engine/PhabricatorBoardLayoutEngine.php
··· 320 320 $columns = msort($columns, 'getSequence'); 321 321 $columns = mpull($columns, null, 'getPHID'); 322 322 323 - $this->columnMap = $columns; 323 + $need_children = array(); 324 + foreach ($boards as $phid => $board) { 325 + if ($board->getHasMilestones() || $board->getHasSubprojects()) { 326 + $need_children[] = $phid; 327 + } 328 + } 329 + 330 + if ($need_children) { 331 + $children = id(new PhabricatorProjectQuery()) 332 + ->setViewer($viewer) 333 + ->withParentProjectPHIDs($need_children) 334 + ->execute(); 335 + $children = mpull($children, null, 'getPHID'); 336 + $children = mgroup($children, 'getParentProjectPHID'); 337 + } else { 338 + $children = array(); 339 + } 340 + 324 341 $columns = mgroup($columns, 'getProjectPHID'); 342 + foreach ($boards as $board_phid => $board) { 343 + $board_columns = idx($columns, $board_phid, array()); 344 + 345 + // If the project has milestones, create any missing columns. 346 + if ($board->getHasMilestones() || $board->getHasSubprojects()) { 347 + $child_projects = idx($children, $board_phid, array()); 348 + 349 + $next_sequence = last($board_columns)->getSequence() + 1; 350 + $proxy_columns = mpull($board_columns, null, 'getProxyPHID'); 351 + foreach ($child_projects as $child_phid => $child) { 352 + if (isset($proxy_columns[$child_phid])) { 353 + continue; 354 + } 355 + 356 + $new_column = PhabricatorProjectColumn::initializeNewColumn($viewer) 357 + ->attachProject($board) 358 + ->attachProxy($child) 359 + ->setSequence($next_sequence++) 360 + ->setProjectPHID($board_phid) 361 + ->setProxyPHID($child_phid); 362 + 363 + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 364 + $new_column->save(); 365 + unset($unguarded); 366 + 367 + $board_columns[$new_column->getPHID()] = $new_column; 368 + } 369 + } 370 + 371 + $columns[$board_phid] = $board_columns; 372 + } 373 + 374 + foreach ($columns as $board_phid => $board_columns) { 375 + foreach ($board_columns as $board_column) { 376 + $column_phid = $board_column->getPHID(); 377 + $this->columnMap[$column_phid] = $board_column; 378 + } 379 + } 325 380 326 381 return $columns; 327 382 } ··· 350 405 array $columns, 351 406 array $positions) { 352 407 408 + $viewer = $this->getViewer(); 409 + 353 410 $board_phid = $board->getPHID(); 354 411 $position_groups = mgroup($positions, 'getObjectPHID'); 355 412 ··· 363 420 } 364 421 } 365 422 423 + // Find all the columns which are proxies for other objects. 424 + $proxy_map = array(); 425 + foreach ($columns as $column) { 426 + $proxy_phid = $column->getProxyPHID(); 427 + if ($proxy_phid) { 428 + $proxy_map[$proxy_phid] = $column->getPHID(); 429 + } 430 + } 431 + 366 432 $object_phids = $this->getObjectPHIDs(); 433 + 434 + // If we have proxies, we need to force cards into the correct proxy 435 + // columns. 436 + if ($proxy_map) { 437 + $edge_query = id(new PhabricatorEdgeQuery()) 438 + ->withSourcePHIDs($object_phids) 439 + ->withEdgeTypes( 440 + array( 441 + PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, 442 + )); 443 + $edge_query->execute(); 444 + 445 + $project_phids = $edge_query->getDestinationPHIDs(); 446 + $project_phids = array_fuse($project_phids); 447 + } else { 448 + $project_phids = array(); 449 + } 450 + 451 + if ($project_phids) { 452 + $projects = id(new PhabricatorProjectQuery()) 453 + ->setViewer($viewer) 454 + ->withPHIDs($project_phids) 455 + ->execute(); 456 + $projects = mpull($projects, null, 'getPHID'); 457 + } else { 458 + $projects = array(); 459 + } 460 + 461 + // Build a map from every project that any task is tagged with to the 462 + // ancestor project which has a column on this board, if one exists. 463 + $ancestor_map = array(); 464 + foreach ($projects as $phid => $project) { 465 + if (isset($proxy_map[$phid])) { 466 + $ancestor_map[$phid] = $proxy_map[$phid]; 467 + } else { 468 + $seen = array($phid); 469 + foreach ($project->getAncestorProjects() as $ancestor) { 470 + $ancestor_phid = $ancestor->getPHID(); 471 + $seen[] = $ancestor_phid; 472 + if (isset($proxy_map[$ancestor_phid])) { 473 + foreach ($seen as $project_phid) { 474 + $ancestor_map[$project_phid] = $proxy_map[$ancestor_phid]; 475 + } 476 + } 477 + } 478 + } 479 + } 480 + 367 481 foreach ($object_phids as $object_phid) { 368 482 $positions = idx($position_groups, $object_phid, array()); 369 483 370 - // Remove any positions in columns which no longer exist. 371 - foreach ($positions as $key => $position) { 372 - $column_phid = $position->getColumnPHID(); 373 - if (empty($columns[$column_phid])) { 374 - $this->remQueue[] = $position; 375 - unset($positions[$key]); 484 + // First, check for objects that have corresponding proxy columns. We're 485 + // going to overwrite normal column positions if a tag belongs to a proxy 486 + // column, since you can't be in normal columns if you're in proxy 487 + // columns. 488 + $proxy_hits = array(); 489 + if ($proxy_map) { 490 + $object_project_phids = $edge_query->getDestinationPHIDs( 491 + array( 492 + $object_phid, 493 + )); 494 + 495 + foreach ($object_project_phids as $project_phid) { 496 + if (isset($ancestor_map[$project_phid])) { 497 + $proxy_hits[] = $ancestor_map[$project_phid]; 498 + } 376 499 } 377 500 } 378 501 379 - // If the object has no position, put it on the default column. 380 - if (!$positions) { 381 - $new_position = id(new PhabricatorProjectColumnPosition()) 382 - ->setBoardPHID($board_phid) 383 - ->setColumnPHID($default_phid) 384 - ->setObjectPHID($object_phid) 385 - ->setSequence(0); 502 + if ($proxy_hits) { 503 + // TODO: For now, only one column hit is permissible. 504 + $proxy_hits = array_slice($proxy_hits, 0, 1); 386 505 387 - $this->addQueue[] = $new_position; 506 + $proxy_hits = array_fuse($proxy_hits); 388 507 389 - $positions = array( 390 - $new_position, 391 - ); 508 + // Check the object positions: we hope to find a position in each 509 + // column the object should be part of. We're going to drop any 510 + // invalid positions and create new positions where positions are 511 + // missing. 512 + foreach ($positions as $key => $position) { 513 + $column_phid = $position->getColumnPHID(); 514 + if (isset($proxy_hits[$column_phid])) { 515 + // Valid column, mark the position as found. 516 + unset($proxy_hits[$column_phid]); 517 + } else { 518 + // Invalid column, ignore the position. 519 + unset($positions[$key]); 520 + } 521 + } 522 + 523 + // Create new positions for anything we haven't found. 524 + foreach ($proxy_hits as $proxy_hit) { 525 + $new_position = id(new PhabricatorProjectColumnPosition()) 526 + ->setBoardPHID($board_phid) 527 + ->setColumnPHID($proxy_hit) 528 + ->setObjectPHID($object_phid) 529 + ->setSequence(0); 530 + 531 + $this->addQueue[] = $new_position; 532 + 533 + $positions[] = $new_position; 534 + } 535 + } else { 536 + // Ignore any positions in columns which no longer exist. We don't 537 + // actively destory them because the rest of the code ignores them and 538 + // there's no real need to destroy the data. 539 + foreach ($positions as $key => $position) { 540 + $column_phid = $position->getColumnPHID(); 541 + if (empty($columns[$column_phid])) { 542 + unset($positions[$key]); 543 + } 544 + } 545 + 546 + // If the object has no position, put it on the default column. 547 + if (!$positions) { 548 + $new_position = id(new PhabricatorProjectColumnPosition()) 549 + ->setBoardPHID($board_phid) 550 + ->setColumnPHID($default_phid) 551 + ->setObjectPHID($object_phid) 552 + ->setSequence(0); 553 + 554 + $this->addQueue[] = $new_position; 555 + 556 + $positions = array( 557 + $new_position, 558 + ); 559 + } 392 560 } 393 561 394 562 foreach ($positions as $position) {
+7
src/applications/project/interface/PhabricatorColumnProxyInterface.php
··· 1 + <?php 2 + 3 + interface PhabricatorColumnProxyInterface { 4 + 5 + public function getProxyColumnName(); 6 + 7 + }
+62
src/applications/project/query/PhabricatorProjectColumnQuery.php
··· 6 6 private $ids; 7 7 private $phids; 8 8 private $projectPHIDs; 9 + private $proxyPHIDs; 9 10 private $statuses; 10 11 11 12 public function withIDs(array $ids) { ··· 20 21 21 22 public function withProjectPHIDs(array $project_phids) { 22 23 $this->projectPHIDs = $project_phids; 24 + return $this; 25 + } 26 + 27 + public function withProxyPHIDs(array $proxy_phids) { 28 + $this->proxyPHIDs = $proxy_phids; 23 29 return $this; 24 30 } 25 31 ··· 60 66 $column->attachProject($project); 61 67 } 62 68 69 + $proxy_phids = array_filter(mpull($page, 'getProjectPHID')); 70 + 71 + return $page; 72 + } 73 + 74 + protected function didFilterPage(array $page) { 75 + $proxy_phids = array(); 76 + foreach ($page as $column) { 77 + $proxy_phid = $column->getProxyPHID(); 78 + if ($proxy_phid !== null) { 79 + $proxy_phids[$proxy_phid] = $proxy_phid; 80 + } 81 + } 82 + 83 + if ($proxy_phids) { 84 + $proxies = id(new PhabricatorObjectQuery()) 85 + ->setParentQuery($this) 86 + ->setViewer($this->getViewer()) 87 + ->withPHIDs($proxy_phids) 88 + ->execute(); 89 + $proxies = mpull($proxies, null, 'getPHID'); 90 + } else { 91 + $proxies = array(); 92 + } 93 + 94 + foreach ($page as $key => $column) { 95 + $proxy_phid = $column->getProxyPHID(); 96 + 97 + if ($proxy_phid !== null) { 98 + $proxy = idx($proxies, $proxy_phid); 99 + 100 + // Only attach valid proxies, so we don't end up getting surprsied if 101 + // an install somehow gets junk into their database. 102 + if (!($proxy instanceof PhabricatorColumnProxyInterface)) { 103 + $proxy = null; 104 + } 105 + 106 + if (!$proxy) { 107 + $this->didRejectResult($column); 108 + unset($page[$key]); 109 + continue; 110 + } 111 + } else { 112 + $proxy = null; 113 + } 114 + 115 + $column->attachProxy($proxy); 116 + } 117 + 63 118 return $page; 64 119 } 65 120 ··· 85 140 $conn, 86 141 'projectPHID IN (%Ls)', 87 142 $this->projectPHIDs); 143 + } 144 + 145 + if ($this->proxyPHIDs !== null) { 146 + $where[] = qsprintf( 147 + $conn, 148 + 'proxyPHID IN (%Ls)', 149 + $this->proxyPHIDs); 88 150 } 89 151 90 152 if ($this->statuses !== null) {
+23 -1
src/applications/project/storage/PhabricatorProject.php
··· 9 9 PhabricatorCustomFieldInterface, 10 10 PhabricatorDestructibleInterface, 11 11 PhabricatorFulltextInterface, 12 - PhabricatorConduitResultInterface { 12 + PhabricatorConduitResultInterface, 13 + PhabricatorColumnProxyInterface { 13 14 14 15 protected $name; 15 16 protected $status = PhabricatorProjectStatus::STATUS_ACTIVE; ··· 662 663 ->setAttachmentKey('watchers'), 663 664 ); 664 665 } 666 + 667 + 668 + /* -( PhabricatorColumnProxyInterface )------------------------------------ */ 669 + 670 + 671 + public function getProxyColumnName() { 672 + return $this->getName(); 673 + } 674 + 675 + public function getProxyColumnIcon() { 676 + return $this->getDisplayIconIcon(); 677 + } 678 + 679 + public function getProxyColumnClass() { 680 + if ($this->isMilestone()) { 681 + return 'phui-workboard-column-milestone'; 682 + } 683 + 684 + return null; 685 + } 686 + 665 687 666 688 }
+35 -2
src/applications/project/storage/PhabricatorProjectColumn.php
··· 17 17 protected $name; 18 18 protected $status; 19 19 protected $projectPHID; 20 + protected $proxyPHID; 20 21 protected $sequence; 21 22 protected $properties = array(); 22 23 23 24 private $project = self::ATTACHABLE; 25 + private $proxy = self::ATTACHABLE; 24 26 25 27 public static function initializeNewColumn(PhabricatorUser $user) { 26 28 return id(new PhabricatorProjectColumn()) ··· 38 40 'name' => 'text255', 39 41 'status' => 'uint32', 40 42 'sequence' => 'uint32', 43 + 'proxyPHID' => 'phid?', 41 44 ), 42 45 self::CONFIG_KEY_SCHEMA => array( 43 46 'key_status' => array( ··· 46 49 'key_sequence' => array( 47 50 'columns' => array('projectPHID', 'sequence'), 48 51 ), 52 + 'key_proxy' => array( 53 + 'columns' => array('projectPHID', 'proxyPHID'), 54 + 'unique' => true, 55 + ), 49 56 ), 50 57 ) + parent::getConfiguration(); 51 58 } ··· 64 71 return $this->assertAttached($this->project); 65 72 } 66 73 74 + public function attachProxy($proxy) { 75 + $this->proxy = $proxy; 76 + return $this; 77 + } 78 + 79 + public function getProxy() { 80 + return $this->assertAttached($this->proxy); 81 + } 82 + 67 83 public function isDefaultColumn() { 68 84 return (bool)$this->getProperty('isDefault'); 69 85 } ··· 73 89 } 74 90 75 91 public function getDisplayName() { 92 + $proxy = $this->getProxy(); 93 + if ($proxy) { 94 + return $proxy->getProxyColumnName(); 95 + } 96 + 76 97 $name = $this->getName(); 77 98 if (strlen($name)) { 78 99 return $name; ··· 96 117 return null; 97 118 } 98 119 120 + public function getDisplayClass() { 121 + $proxy = $this->getProxy(); 122 + if ($proxy) { 123 + return $proxy->getProxyColumnClass(); 124 + } 125 + 126 + return null; 127 + } 128 + 99 129 public function getHeaderIcon() { 100 - $icon = null; 130 + $proxy = $this->getProxy(); 131 + if ($proxy) { 132 + return $proxy->getProxyColumnIcon(); 133 + } 101 134 102 135 if ($this->isHidden()) { 103 - $icon = 'fa-eye-slash'; 136 + return 'fa-eye-slash'; 104 137 } 105 138 106 139 return null;
-9
src/docs/user/userguide/projects.diviner
··· 162 162 |---|---|---|---|---| 163 163 | //Members// | Yes | Union of Subprojects | Yes | Same as Parent | 164 164 | //Policies// | Yes | Yes | Affected by Parent | Same as Parent | 165 - | //Workboard// | Yes | No Custom Columns | Yes | Yes | 166 165 | //Hashtags// | Yes | Yes | Yes | Special | 167 166 168 167 ··· 256 255 257 256 You can edit the project afterward to change or remove members if you want to 258 257 split membership apart in a more granular way across multiple new subprojects. 259 - 260 - **No Workboard Columns**: Parent projects can not have their own workboard 261 - columns: instead, the workboard of a parent project shows columns representing 262 - the child projects. 263 - 264 - Thus, a project's workboard columns are destroyed when you add the first 265 - subproject. All objects on the workboard will be returned to the project's 266 - backlog. The new board will show columns for subprojects instead. 267 258 268 259 **Searching**: When you search for a parent project, results for any subproject 269 260 are returned. For example, if you search for {nav Engineering}, your query will
+2 -2
src/view/phui/PHUIWorkpanelView.php
··· 10 10 private $headerTag; 11 11 private $headerIcon; 12 12 13 - public function setHeaderIcon(PHUIIconView $header_icon) { 14 - $this->headerIcon = $header_icon; 13 + public function setHeaderIcon($icon) { 14 + $this->headerIcon = $icon; 15 15 return $this; 16 16 } 17 17
+5 -2
webroot/rsrc/js/application/projects/behavior-project-boards.js
··· 280 280 // close the dropdown, but don't want to follow the link. 281 281 e.prevent(); 282 282 283 - var column_phid = e.getNodeData('column-add-task').columnPHID; 283 + var column_data = e.getNodeData('column-add-task'); 284 + var column_phid = column_data.columnPHID; 285 + 284 286 var request_data = { 285 287 responseType: 'card', 286 288 columnPHID: column_phid, 287 - projects: statics.projectPHID, 289 + projects: column_data.projectPHID, 288 290 order: statics.order 289 291 }; 292 + 290 293 var cols = getcolumns(); 291 294 var ii; 292 295 var column;