diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 775414b101..87c5ad5b85 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -412,16 +412,16 @@ return array( 'rsrc/js/application/phortune/phortune-credit-card-form.js' => 'd12d214f', 'rsrc/js/application/policy/behavior-policy-control.js' => '0eaa33a9', 'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '9347f172', - 'rsrc/js/application/projects/WorkboardBoard.js' => '44f71637', + 'rsrc/js/application/projects/WorkboardBoard.js' => '34c2f539', 'rsrc/js/application/projects/WorkboardCard.js' => '0392a5d8', 'rsrc/js/application/projects/WorkboardCardTemplate.js' => '2a61f8d4', 'rsrc/js/application/projects/WorkboardColumn.js' => 'c3d24e63', - 'rsrc/js/application/projects/WorkboardController.js' => '42c7a5a7', + 'rsrc/js/application/projects/WorkboardController.js' => 'b9d0c2f3', 'rsrc/js/application/projects/WorkboardDropEffect.js' => '8e0aa661', 'rsrc/js/application/projects/WorkboardHeader.js' => '111bfd2d', 'rsrc/js/application/projects/WorkboardHeaderTemplate.js' => 'ebe83a6b', 'rsrc/js/application/projects/WorkboardOrderTemplate.js' => '03e8891f', - 'rsrc/js/application/projects/behavior-project-boards.js' => 'aad45445', + 'rsrc/js/application/projects/behavior-project-boards.js' => '58cb6a88', 'rsrc/js/application/projects/behavior-project-create.js' => '34c53422', 'rsrc/js/application/projects/behavior-reorder-columns.js' => '8ac32fd9', 'rsrc/js/application/releeph/releeph-preview-branch.js' => '75184d68', @@ -667,7 +667,7 @@ return array( 'javelin-behavior-phuix-example' => 'c2c500a7', 'javelin-behavior-policy-control' => '0eaa33a9', 'javelin-behavior-policy-rule-editor' => '9347f172', - 'javelin-behavior-project-boards' => 'aad45445', + 'javelin-behavior-project-boards' => '58cb6a88', 'javelin-behavior-project-create' => '34c53422', 'javelin-behavior-quicksand-blacklist' => '5a6f6a06', 'javelin-behavior-read-only-warning' => 'b9109f8f', @@ -743,11 +743,11 @@ return array( 'javelin-view-renderer' => '9aae2b66', 'javelin-view-visitor' => '308f9fe4', 'javelin-websocket' => 'fdc13e4e', - 'javelin-workboard-board' => '44f71637', + 'javelin-workboard-board' => '34c2f539', 'javelin-workboard-card' => '0392a5d8', 'javelin-workboard-card-template' => '2a61f8d4', 'javelin-workboard-column' => 'c3d24e63', - 'javelin-workboard-controller' => '42c7a5a7', + 'javelin-workboard-controller' => 'b9d0c2f3', 'javelin-workboard-drop-effect' => '8e0aa661', 'javelin-workboard-header' => '111bfd2d', 'javelin-workboard-header-template' => 'ebe83a6b', @@ -1202,6 +1202,18 @@ return array( 'javelin-install', 'javelin-util', ), + '34c2f539' => array( + 'javelin-install', + 'javelin-dom', + 'javelin-util', + 'javelin-stratcom', + 'javelin-workflow', + 'phabricator-draggable-list', + 'javelin-workboard-column', + 'javelin-workboard-header-template', + 'javelin-workboard-card-template', + 'javelin-workboard-order-template', + ), '34c53422' => array( 'javelin-behavior', 'javelin-dom', @@ -1264,16 +1276,6 @@ return array( '4234f572' => array( 'syntax-default-css', ), - '42c7a5a7' => array( - 'javelin-install', - 'javelin-dom', - 'javelin-util', - 'javelin-vector', - 'javelin-stratcom', - 'javelin-workflow', - 'phabricator-drag-and-drop-file-upload', - 'javelin-workboard-board', - ), '4370900d' => array( 'javelin-install', 'javelin-util', @@ -1294,18 +1296,6 @@ return array( '43bc9360' => array( 'javelin-install', ), - '44f71637' => array( - 'javelin-install', - 'javelin-dom', - 'javelin-util', - 'javelin-stratcom', - 'javelin-workflow', - 'phabricator-draggable-list', - 'javelin-workboard-column', - 'javelin-workboard-header-template', - 'javelin-workboard-card-template', - 'javelin-workboard-order-template', - ), '46116c01' => array( 'javelin-request', 'javelin-behavior', @@ -1424,6 +1414,16 @@ return array( 'javelin-vector', 'javelin-typeahead-static-source', ), + '58cb6a88' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-util', + 'javelin-vector', + 'javelin-stratcom', + 'javelin-workflow', + 'javelin-workboard-controller', + 'javelin-workboard-drop-effect', + ), '5902260c' => array( 'javelin-util', 'javelin-magical-init', @@ -1852,16 +1852,6 @@ return array( 'javelin-dom', 'javelin-util', ), - 'aad45445' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-util', - 'javelin-vector', - 'javelin-stratcom', - 'javelin-workflow', - 'javelin-workboard-controller', - 'javelin-workboard-drop-effect', - ), 'ab85e184' => array( 'javelin-install', 'javelin-dom', @@ -1952,6 +1942,16 @@ return array( 'javelin-uri', 'phabricator-notification', ), + 'b9d0c2f3' => array( + 'javelin-install', + 'javelin-dom', + 'javelin-util', + 'javelin-vector', + 'javelin-stratcom', + 'javelin-workflow', + 'phabricator-drag-and-drop-file-upload', + 'javelin-workboard-board', + ), 'bde53589' => array( 'phui-inline-comment-view-css', ), diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 963666fe82..095e7231fb 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -4163,6 +4163,7 @@ phutil_register_library_map(array( 'PhabricatorProjectBoardFilterController' => 'applications/project/controller/PhabricatorProjectBoardFilterController.php', 'PhabricatorProjectBoardImportController' => 'applications/project/controller/PhabricatorProjectBoardImportController.php', 'PhabricatorProjectBoardManageController' => 'applications/project/controller/PhabricatorProjectBoardManageController.php', + 'PhabricatorProjectBoardReloadController' => 'applications/project/controller/PhabricatorProjectBoardReloadController.php', 'PhabricatorProjectBoardReorderController' => 'applications/project/controller/PhabricatorProjectBoardReorderController.php', 'PhabricatorProjectBoardViewController' => 'applications/project/controller/PhabricatorProjectBoardViewController.php', 'PhabricatorProjectBuiltinsExample' => 'applications/uiexample/examples/PhabricatorProjectBuiltinsExample.php', @@ -10431,6 +10432,7 @@ phutil_register_library_map(array( 'PhabricatorProjectBoardFilterController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectBoardImportController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectBoardManageController' => 'PhabricatorProjectBoardController', + 'PhabricatorProjectBoardReloadController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectBoardReorderController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectBoardViewController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectBuiltinsExample' => 'PhabricatorUIExample', diff --git a/src/applications/project/application/PhabricatorProjectApplication.php b/src/applications/project/application/PhabricatorProjectApplication.php index 1c04e0c2f9..af4a06fcc4 100644 --- a/src/applications/project/application/PhabricatorProjectApplication.php +++ b/src/applications/project/application/PhabricatorProjectApplication.php @@ -99,6 +99,8 @@ final class PhabricatorProjectApplication extends PhabricatorApplication { => 'PhabricatorProjectBoardDefaultController', 'filter/(?:query/(?P[^/]+)/)?' => 'PhabricatorProjectBoardFilterController', + 'reload/' + => 'PhabricatorProjectBoardReloadController', ), 'column/' => array( 'remove/(?P\d+)/' => diff --git a/src/applications/project/controller/PhabricatorProjectBoardReloadController.php b/src/applications/project/controller/PhabricatorProjectBoardReloadController.php new file mode 100644 index 0000000000..43752e0cd5 --- /dev/null +++ b/src/applications/project/controller/PhabricatorProjectBoardReloadController.php @@ -0,0 +1,38 @@ +getViewer(); + + $response = $this->loadProject(); + if ($response) { + return $response; + } + + $project = $this->getProject(); + $state = $this->getViewState(); + $board_uri = $state->newWorkboardURI(); + + $layout_engine = $state->getLayoutEngine(); + + $board_phid = $project->getPHID(); + + $objects = $state->getObjects(); + $object_phids = mpull($objects, 'getPHID'); + + $engine = id(new PhabricatorBoardResponseEngine()) + ->setViewer($viewer) + ->setBoardPHID($board_phid) + ->setUpdatePHIDs($object_phids); + + // TODO: We don't currently process "order" properly. If a user is viewing + // a board grouped by "Owner", and another user changes a task to be owned + // by a user who currently owns nothing on the board, the new header won't + // generate correctly if the first user presses "R". + + return $engine->buildResponse(); + } + +} diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php index 4979f9e78e..13a75c5a73 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php @@ -286,6 +286,7 @@ final class PhabricatorProjectBoardViewController 'moveURI' => $this->getApplicationURI('move/'.$project->getID().'/'), 'uploadURI' => '/file/dropupload/', 'coverURI' => $this->getApplicationURI('cover/'), + 'reloadURI' => phutil_string_cast($state->newWorkboardURI('reload/')), 'chunkThreshold' => PhabricatorFileStorageEngine::getChunkThreshold(), 'pointsEnabled' => ManiphestTaskPoints::getIsEnabled(), diff --git a/src/applications/project/engine/PhabricatorBoardResponseEngine.php b/src/applications/project/engine/PhabricatorBoardResponseEngine.php index f22254e43a..dbd2e31a3d 100644 --- a/src/applications/project/engine/PhabricatorBoardResponseEngine.php +++ b/src/applications/project/engine/PhabricatorBoardResponseEngine.php @@ -6,6 +6,7 @@ final class PhabricatorBoardResponseEngine extends Phobject { private $boardPHID; private $objectPHID; private $visiblePHIDs; + private $updatePHIDs = array(); private $ordering; private $sounds; @@ -45,6 +46,15 @@ final class PhabricatorBoardResponseEngine extends Phobject { return $this->visiblePHIDs; } + public function setUpdatePHIDs(array $update_phids) { + $this->updatePHIDs = $update_phids; + return $this; + } + + public function getUpdatePHIDs() { + return $this->updatePHIDs; + } + public function setOrdering(PhabricatorProjectColumnOrder $ordering) { $this->ordering = $ordering; return $this; @@ -71,36 +81,41 @@ final class PhabricatorBoardResponseEngine extends Phobject { // Load all the other tasks that are visible in the affected columns and // perform layout for them. - $visible_phids = $this->getAllVisiblePHIDs(); + $all_phids = $this->getAllVisiblePHIDs(); $layout_engine = id(new PhabricatorBoardLayoutEngine()) ->setViewer($viewer) ->setBoardPHIDs(array($board_phid)) - ->setObjectPHIDs($visible_phids) + ->setObjectPHIDs($all_phids) ->executeLayout(); - $object_columns = $layout_engine->getObjectColumns( - $board_phid, - $object_phid); - $natural = array(); - foreach ($object_columns as $column_phid => $column) { + + $update_phids = $this->getAllUpdatePHIDs(); + $update_columns = array(); + foreach ($update_phids as $update_phid) { + $update_columns += $layout_engine->getObjectColumns( + $board_phid, + $update_phid); + } + + foreach ($update_columns as $column_phid => $column) { $column_object_phids = $layout_engine->getColumnObjectPHIDs( $board_phid, $column_phid); $natural[$column_phid] = array_values($column_object_phids); } - $all_visible = id(new ManiphestTaskQuery()) + $all_objects = id(new ManiphestTaskQuery()) ->setViewer($viewer) - ->withPHIDs($visible_phids) + ->withPHIDs($all_phids) ->execute(); - $all_visible = mpull($all_visible, null, 'getPHID'); + $all_objects = mpull($all_objects, null, 'getPHID'); if ($ordering) { - $vectors = $ordering->getSortVectorsForObjects($all_visible); - $header_keys = $ordering->getHeaderKeysForObjects($all_visible); - $headers = $ordering->getHeadersForObjects($all_visible); + $vectors = $ordering->getSortVectorsForObjects($all_objects); + $header_keys = $ordering->getHeaderKeysForObjects($all_objects); + $headers = $ordering->getHeadersForObjects($all_objects); $headers = mpull($headers, 'toDictionary'); } else { $vectors = array(); @@ -108,19 +123,10 @@ final class PhabricatorBoardResponseEngine extends Phobject { $headers = array(); } - $object = id(new ManiphestTaskQuery()) - ->setViewer($viewer) - ->withPHIDs(array($object_phid)) - ->needProjectPHIDs(true) - ->executeOne(); - if (!$object) { - return new Aphront404Response(); - } - - $template = $this->buildTemplate($object); + $templates = $this->newCardTemplates(); $cards = array(); - foreach ($all_visible as $card_phid => $object) { + foreach ($all_objects as $card_phid => $object) { $card = array( 'vectors' => array(), 'headers' => array(), @@ -144,8 +150,11 @@ final class PhabricatorBoardResponseEngine extends Phobject { $card['properties'] = self::newTaskProperties($object); } - if ($card_phid === $object_phid) { - $card['nodeHTMLTemplate'] = hsprintf('%s', $template); + if (isset($templates[$card_phid])) { + $card['nodeHTMLTemplate'] = hsprintf('%s', $templates[$card_phid]); + $card['update'] = true; + } else { + $card['update'] = false; } $card['vectors'] = (object)$card['vectors']; @@ -156,7 +165,6 @@ final class PhabricatorBoardResponseEngine extends Phobject { } $payload = array( - 'objectPHID' => $object_phid, 'columnMaps' => $natural, 'cards' => $cards, 'headers' => $headers, @@ -176,22 +184,6 @@ final class PhabricatorBoardResponseEngine extends Phobject { ); } - private function buildTemplate($object) { - $viewer = $this->getViewer(); - $object_phid = $this->getObjectPHID(); - - $excluded_phids = $this->loadExcludedProjectPHIDs(); - - $rendering_engine = id(new PhabricatorBoardRenderingEngine()) - ->setViewer($viewer) - ->setObjects(array($object)) - ->setExcludedProjectPHIDs($excluded_phids); - - $card = $rendering_engine->renderCard($object_phid); - - return hsprintf('%s', $card->getItem()); - } - private function loadExcludedProjectPHIDs() { $viewer = $this->getViewer(); $board_phid = $this->getBoardPHID(); @@ -211,10 +203,67 @@ final class PhabricatorBoardResponseEngine extends Phobject { } private function getAllVisiblePHIDs() { - $visible_phids = $this->getVisiblePHIDs(); - $visible_phids[] = $this->getObjectPHID(); - $visible_phids = array_fuse($visible_phids); - return $visible_phids; + $phids = $this->getAllUpdatePHIDs(); + + foreach ($this->getVisiblePHIDs() as $phid) { + $phids[] = $phid; + } + + $phids = array_fuse($phids); + + return $phids; + } + + private function getAllUpdatePHIDs() { + $phids = $this->getUpdatePHIDs(); + + $object_phid = $this->getObjectPHID(); + if ($object_phid) { + $phids[] = $object_phid; + } + + $phids = array_fuse($phids); + + return $phids; + } + + private function newCardTemplates() { + $viewer = $this->getViewer(); + + $update_phids = $this->getAllUpdatePHIDs(); + if (!$update_phids) { + return array(); + } + + $objects = id(new ManiphestTaskQuery()) + ->setViewer($viewer) + ->withPHIDs($update_phids) + ->needProjectPHIDs(true) + ->execute(); + + if (!$objects) { + return array(); + } + + $excluded_phids = $this->loadExcludedProjectPHIDs(); + + $rendering_engine = id(new PhabricatorBoardRenderingEngine()) + ->setViewer($viewer) + ->setObjects($objects) + ->setExcludedProjectPHIDs($excluded_phids); + + $templates = array(); + foreach ($objects as $object) { + $object_phid = $object->getPHID(); + + $card = $rendering_engine->renderCard($object_phid); + $item = $card->getItem(); + $template = hsprintf('%s', $item); + + $templates[$object_phid] = $template; + } + + return $templates; } } diff --git a/webroot/rsrc/js/application/projects/WorkboardBoard.js b/webroot/rsrc/js/application/projects/WorkboardBoard.js index 64a5d64b7e..ba015f592d 100644 --- a/webroot/rsrc/js/application/projects/WorkboardBoard.js +++ b/webroot/rsrc/js/application/projects/WorkboardBoard.js @@ -129,6 +129,13 @@ JX.install('WorkboardBoard', { start: function() { this._setupDragHandlers(); + // TODO: This is temporary code to make it easier to debug this workflow + // by pressing the "R" key. + var on_reload = JX.bind(this, this._reloadCards); + new JX.KeyboardShortcut('R', 'Reload Card State (Prototype)') + .setHandler(on_reload) + .register(); + for (var k in this._columns) { this._columns[k].redraw(); } @@ -551,15 +558,6 @@ JX.install('WorkboardBoard', { }, _oncardupdate: function(list, src_phid, dst_phid, after_phid, response) { - var src_column = this.getColumn(src_phid); - var dst_column = this.getColumn(dst_phid); - - var card = src_column.removeCard(response.objectPHID); - dst_column.addCard(card, after_phid); - - src_column.markForRedraw(); - dst_column.markForRedraw(); - this.updateCard(response); var sounds = response.sounds || []; @@ -572,37 +570,51 @@ JX.install('WorkboardBoard', { updateCard: function(response) { var columns = this.getColumns(); + var column_phid; + var card_phid; + var card_data; - var phid = response.objectPHID; + // The server may send us a full or partial update for a card. If we've + // received a full update, we're going to redraw the entire card and may + // need to change which columns it appears in. - for (var add_phid in response.columnMaps) { - var target_column = this.getColumn(add_phid); + // For a partial update, we've just received supplemental sorting or + // property information and do not need to perform a full redraw. + + // When we reload card state, edit a card, or move a card, we get a full + // update for the card. + + // Ween we move a card in a column, we may get a partial update for other + // visible cards in the column. + + + // Figure out which columns each card now appears in. For cards that + // have received a full update, we'll use this map to move them into + // the correct columns. + var update_map = {}; + for (column_phid in response.columnMaps) { + var target_column = this.getColumn(column_phid); if (!target_column) { // If the column isn't visible, don't try to add a card to it. continue; } - target_column.newCard(phid); - } + var column_map = response.columnMaps[column_phid]; - var column_maps = response.columnMaps; - var natural_column; - for (var natural_phid in column_maps) { - natural_column = this.getColumn(natural_phid); - if (!natural_column) { - // Our view of the board may be out of date, so we might get back - // information about columns that aren't visible. Just ignore the - // position information for any columns we aren't displaying on the - // client. - continue; + for (var ii = 0; ii < column_map.length; ii++) { + card_phid = column_map[ii]; + if (!update_map[card_phid]) { + update_map[card_phid] = {}; + } + update_map[card_phid][column_phid] = true; } - - natural_column.setNaturalOrder(column_maps[natural_phid]); } - for (var card_phid in response.cards) { - var card_data = response.cards[card_phid]; + // Process partial updates for cards. This is supplemental data which + // we can just merge in without any special handling. + for (card_phid in response.cards) { + card_data = response.cards[card_phid]; var card_template = this.getCardTemplate(card_phid); if (card_data.nodeHTMLTemplate) { @@ -623,6 +635,57 @@ JX.install('WorkboardBoard', { } } + + // Process full updates for cards which we have a full update for. This + // may involve moving them between columns. + for (card_phid in response.cards) { + card_data = response.cards[card_phid]; + + if (!card_data.update) { + continue; + } + + for (column_phid in columns) { + var column = columns[column_phid]; + var card = column.getCard(card_phid); + + if (card) { + card.redraw(); + column.markForRedraw(); + } + + // Compare the server state to the client state, and add or remove + // cards on the client as necessary to synchronize them. + + if (update_map[card_phid][column_phid]) { + if (!card) { + column.newCard(card_phid); + column.markForRedraw(); + } + } else { + if (card) { + column.removeCard(card_phid); + column.markForRedraw(); + } + } + } + } + + var column_maps = response.columnMaps; + var natural_column; + for (var natural_phid in column_maps) { + natural_column = this.getColumn(natural_phid); + if (!natural_column) { + // Our view of the board may be out of date, so we might get back + // information about columns that aren't visible. Just ignore the + // position information for any columns we aren't displaying on the + // client. + continue; + } + + natural_column.setNaturalOrder(column_maps[natural_phid]); + } + var headers = response.headers; for (var jj = 0; jj < headers.length; jj++) { var header = headers[jj]; @@ -634,22 +697,6 @@ JX.install('WorkboardBoard', { .setEditProperties(header.editProperties); } - for (var column_phid in columns) { - var column = columns[column_phid]; - - var cards = column.getCards(); - for (var object_phid in cards) { - if (object_phid !== phid) { - continue; - } - - var card = cards[object_phid]; - card.redraw(); - - column.markForRedraw(); - } - } - this._redrawColumns(); }, @@ -660,6 +707,19 @@ JX.install('WorkboardBoard', { columns[k].redraw(); } } + }, + + _reloadCards: function() { + var data = {}; + var on_reload = JX.bind(this, this._onReloadResponse); + + new JX.Request(this.getController().getReloadURI(), on_reload) + .setData(data) + .send(); + }, + + _onReloadResponse: function(response) { + this.updateCard(response); } } diff --git a/webroot/rsrc/js/application/projects/WorkboardController.js b/webroot/rsrc/js/application/projects/WorkboardController.js index 8fe88eb50c..da5d177bb9 100644 --- a/webroot/rsrc/js/application/projects/WorkboardController.js +++ b/webroot/rsrc/js/application/projects/WorkboardController.js @@ -21,6 +21,7 @@ JX.install('WorkboardController', { uploadURI: null, coverURI: null, moveURI: null, + reloadURI: null, chunkThreshold: null }, diff --git a/webroot/rsrc/js/application/projects/behavior-project-boards.js b/webroot/rsrc/js/application/projects/behavior-project-boards.js index bba6db7a49..26e5d90f8e 100644 --- a/webroot/rsrc/js/application/projects/behavior-project-boards.js +++ b/webroot/rsrc/js/application/projects/behavior-project-boards.js @@ -71,6 +71,7 @@ JX.behavior('project-boards', function(config, statics) { .setUploadURI(config.uploadURI) .setCoverURI(config.coverURI) .setMoveURI(config.moveURI) + .setReloadURI(config.reloadURI) .setChunkThreshold(config.chunkThreshold) .start(); }