Page MenuHomePhabricator

D15171.id36647.diff
No OneTemporary

D15171.id36647.diff

diff --git a/resources/builtin/image-200x200.png b/resources/builtin/image-200x200.png
new file mode 100644
index 0000000000000000000000000000000000000000..53bc1e785c395c4c5882c1aec3bfc9eac13d4159
GIT binary patch
literal 1261
zc%17D@N?(olHy`uVBq!ia0vp^CqS5k2}mkgS)K$^k|nMYCBgY=CFO}lsSJ)O`AMk?
zp1FzXsX?iUDV2pMQ*9U+So%F(978H@y}9l0EnO*b;NvP`ZAXzFZmwIQH=1vJekW)1
zev>-?KEC&_E>7OppL)Hy>Z@!AyLgUw=I#s5N9`4Qm6|R}{eR<J_hy3El|?T#=krS#
zJyvOdcsspr@{Mi1_5W(w&Q3de-e1|?LeS*masT<Z=d$i&PgK-tY2Dxu!NHx_xIsYd
z5EDp%^^g-#04OdX1{7D+0g5|CD9~Qv$Ga(hj$5Pt{$6jdJ@wSpt5+}IU%Pm5a!SgR
z?f3tm*yIzTt2@cc)^_gf+1X(c+}zyHpFh8Q_wJ#rkDaB3wY9MtB4y>}w}If+ExGS@
z=H})f?G`Uy{PD-r?hPwUC$^pa^{eXewo8oPyXUE?sa-mFzi7{fsAq1=(kd!`>{2@_
zIC1G;{(Uv+>CdD2gu}GkrcU3oe}8>RiOB5Q+FEC4=b!oSC$-JlP;;jsJwj0|T3uZ|
z*7T!_Tj1XpyS#lY5;<2FMhLpIgy>CA-aECZQ@y>toj+{qB%x^=-tC^V^lJIOOJ0jt
z+<Sd}{q<|t@~%$X&wMDWK=aI|H6dD|tFMZL*>s+*X=n{!AGf#ivszff#c7?YYufD#
zJvM6{l9HBw{qm*d+D`S2lGee|A9w8BIrFNFjLebHPuHYQv-7^=lJ;NYp}sET@#Dva
z>n@$!W|e<`UoZ2euZCvHvGxWA57q@l@UPz(6!Eol$={DVcJ11=cdzXBuAkHGrKP2-
ztE+>9gMS+OE%r`)y!+6fG=u5yEC2til?wm4%g(Fn`@2}HW5;>b!X6iv{(q`d7p)<p
zr>A!*r2F`cXrOC8Pk)l%RG%2HdoS4J>-UDQX=!P{e*aE(t&bD-PcklZ^U*ZzE*D<?
z@yM$;Z+x=Xv>g}eKKk<I%O7vwyvfM{8g%cE{-mz!vmy><?Ku_|74_@G!^5$`>bkFz
zKh86oK7D#}-qY^2KydP;=kmY^-a}p&?-aXs{{_mtoZL|{iO>7^@oe3DtyeA9NZ4G>
z`ugwiqUF=|<Bv_=ym@m-#9y0Ysn)9%3ZYt4*{^%PZ1}bES^w{r(s;d%X<E}y_eV6i
zhklIGE>FCBPRVxNnl(00^Y2~Oy7xlpTJG2EWyjCjJP(N2-dyqP`0Ra#BG<w%J%4sK
zOR3&GaqF&i>(>4J`SVZxGn>?;qc^mtdbLg4Fi(_guKoW{uRne)3>Lc^IqmD$sq^gL
z3x%!L7VFlJ`(t5YabwNXsF11kZ<W`C&5LNgcQ5Yw!ksm&1E1#*3XhggK6)NlF3jba
e_1d11jp1+WwB`DOCszW?F9uInKbLh*2~7ZxuS3ZI
literal 0
Hc$@<O00001
diff --git a/resources/celerity/map.php b/resources/celerity/map.php
--- a/resources/celerity/map.php
+++ b/resources/celerity/map.php
@@ -413,7 +413,7 @@
'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 @@
'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 @@
'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 @@
'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',
diff --git a/resources/sql/autopatches/20160202.board.1.proxy.sql b/resources/sql/autopatches/20160202.board.1.proxy.sql
new file mode 100644
--- /dev/null
+++ b/resources/sql/autopatches/20160202.board.1.proxy.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_project.project_column
+ ADD proxyPHID VARBINARY(64);
diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -1889,6 +1889,7 @@
'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 @@
'PhabricatorDestructibleInterface',
'PhabricatorFulltextInterface',
'PhabricatorConduitResultInterface',
+ 'PhabricatorColumnProxyInterface',
),
'PhabricatorProjectAddHeraldAction' => 'PhabricatorProjectHeraldAction',
'PhabricatorProjectApplication' => 'PhabricatorApplication',
diff --git a/src/applications/maniphest/editor/ManiphestEditEngine.php b/src/applications/maniphest/editor/ManiphestEditEngine.php
--- a/src/applications/maniphest/editor/ManiphestEditEngine.php
+++ b/src/applications/maniphest/editor/ManiphestEditEngine.php
@@ -280,10 +280,23 @@
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)
diff --git a/src/applications/maniphest/editor/ManiphestTransactionEditor.php b/src/applications/maniphest/editor/ManiphestTransactionEditor.php
--- a/src/applications/maniphest/editor/ManiphestTransactionEditor.php
+++ b/src/applications/maniphest/editor/ManiphestTransactionEditor.php
@@ -222,12 +222,22 @@
// 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');
diff --git a/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php b/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php
--- a/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php
+++ b/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php
@@ -972,7 +972,45 @@
$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 @@
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 @@
$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 @@
$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;
}
diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php
--- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php
+++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php
@@ -95,17 +95,27 @@
$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 @@
}
}
+ $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 @@
$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 @@
$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 @@
->setMetadata(
array(
'columnPHID' => $column->getPHID(),
+ 'projectPHID' => $default_phid,
));
$batch_edit_uri = $request->getRequestURI();
@@ -738,6 +767,10 @@
}
}
+ // 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')
diff --git a/src/applications/project/controller/PhabricatorProjectMoveController.php b/src/applications/project/controller/PhabricatorProjectMoveController.php
--- a/src/applications/project/controller/PhabricatorProjectMoveController.php
+++ b/src/applications/project/controller/PhabricatorProjectMoveController.php
@@ -139,7 +139,33 @@
->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 @@
->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 @@
return id(new AphrontAjaxResponse())->setContent(
array('task' => $card));
- }
+ }
}
diff --git a/src/applications/project/controller/PhabricatorProjectSubprojectWarningController.php b/src/applications/project/controller/PhabricatorProjectSubprojectWarningController.php
--- a/src/applications/project/controller/PhabricatorProjectSubprojectWarningController.php
+++ b/src/applications/project/controller/PhabricatorProjectSubprojectWarningController.php
@@ -35,7 +35,7 @@
$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.",
diff --git a/src/applications/project/engine/PhabricatorBoardLayoutEngine.php b/src/applications/project/engine/PhabricatorBoardLayoutEngine.php
--- a/src/applications/project/engine/PhabricatorBoardLayoutEngine.php
+++ b/src/applications/project/engine/PhabricatorBoardLayoutEngine.php
@@ -320,8 +320,63 @@
$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 @@
array $columns,
array $positions) {
+ $viewer = $this->getViewer();
+
$board_phid = $board->getPHID();
$position_groups = mgroup($positions, 'getObjectPHID');
@@ -363,32 +420,143 @@
}
}
+ // 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);
+
+ $proxy_hits = array_fuse($proxy_hits);
+
+ // 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]);
+ }
+ }
- $this->addQueue[] = $new_position;
+ // 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);
- $positions = array(
- $new_position,
- );
+ $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) {
diff --git a/src/applications/project/interface/PhabricatorColumnProxyInterface.php b/src/applications/project/interface/PhabricatorColumnProxyInterface.php
new file mode 100644
--- /dev/null
+++ b/src/applications/project/interface/PhabricatorColumnProxyInterface.php
@@ -0,0 +1,7 @@
+<?php
+
+interface PhabricatorColumnProxyInterface {
+
+ public function getProxyColumnName();
+
+}
diff --git a/src/applications/project/query/PhabricatorProjectColumnQuery.php b/src/applications/project/query/PhabricatorProjectColumnQuery.php
--- a/src/applications/project/query/PhabricatorProjectColumnQuery.php
+++ b/src/applications/project/query/PhabricatorProjectColumnQuery.php
@@ -6,6 +6,7 @@
private $ids;
private $phids;
private $projectPHIDs;
+ private $proxyPHIDs;
private $statuses;
public function withIDs(array $ids) {
@@ -23,6 +24,11 @@
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 @@
$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 @@
$this->projectPHIDs);
}
+ if ($this->proxyPHIDs !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'proxyPHID IN (%Ls)',
+ $this->proxyPHIDs);
+ }
+
if ($this->statuses !== null) {
$where[] = qsprintf(
$conn,
diff --git a/src/applications/project/storage/PhabricatorProject.php b/src/applications/project/storage/PhabricatorProject.php
--- a/src/applications/project/storage/PhabricatorProject.php
+++ b/src/applications/project/storage/PhabricatorProject.php
@@ -9,7 +9,8 @@
PhabricatorCustomFieldInterface,
PhabricatorDestructibleInterface,
PhabricatorFulltextInterface,
- PhabricatorConduitResultInterface {
+ PhabricatorConduitResultInterface,
+ PhabricatorColumnProxyInterface {
protected $name;
protected $status = PhabricatorProjectStatus::STATUS_ACTIVE;
@@ -663,4 +664,25 @@
);
}
+
+/* -( 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;
+ }
+
+
}
diff --git a/src/applications/project/storage/PhabricatorProjectColumn.php b/src/applications/project/storage/PhabricatorProjectColumn.php
--- a/src/applications/project/storage/PhabricatorProjectColumn.php
+++ b/src/applications/project/storage/PhabricatorProjectColumn.php
@@ -17,10 +17,12 @@
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 @@
'name' => 'text255',
'status' => 'uint32',
'sequence' => 'uint32',
+ 'proxyPHID' => 'phid?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_status' => array(
@@ -46,6 +49,10 @@
'key_sequence' => array(
'columns' => array('projectPHID', 'sequence'),
),
+ 'key_proxy' => array(
+ 'columns' => array('projectPHID', 'proxyPHID'),
+ 'unique' => true,
+ ),
),
) + parent::getConfiguration();
}
@@ -64,6 +71,15 @@
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 @@
}
public function getDisplayName() {
+ $proxy = $this->getProxy();
+ if ($proxy) {
+ return $proxy->getProxyColumnName();
+ }
+
$name = $this->getName();
if (strlen($name)) {
return $name;
@@ -96,11 +117,23 @@
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;
diff --git a/src/docs/user/userguide/projects.diviner b/src/docs/user/userguide/projects.diviner
--- a/src/docs/user/userguide/projects.diviner
+++ b/src/docs/user/userguide/projects.diviner
@@ -162,7 +162,6 @@
|---|---|---|---|---|
| //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 @@
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
diff --git a/src/view/phui/PHUIWorkpanelView.php b/src/view/phui/PHUIWorkpanelView.php
--- a/src/view/phui/PHUIWorkpanelView.php
+++ b/src/view/phui/PHUIWorkpanelView.php
@@ -10,8 +10,8 @@
private $headerTag;
private $headerIcon;
- public function setHeaderIcon(PHUIIconView $header_icon) {
- $this->headerIcon = $header_icon;
+ public function setHeaderIcon($icon) {
+ $this->headerIcon = $icon;
return $this;
}
diff --git a/webroot/rsrc/js/application/projects/behavior-project-boards.js b/webroot/rsrc/js/application/projects/behavior-project-boards.js
--- a/webroot/rsrc/js/application/projects/behavior-project-boards.js
+++ b/webroot/rsrc/js/application/projects/behavior-project-boards.js
@@ -280,13 +280,16 @@
// 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;

File Metadata

Mime Type
text/plain
Expires
Tue, Nov 5, 4:15 PM (1 w, 5 d ago)
Storage Engine
amazon-s3
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
phabricator/secure/hb/px/6sbadywfook2eumq
Default Alt Text
D15171.id36647.diff (32 KB)

Event Timeline