diff --git a/src/applications/maniphest/storage/ManiphestTask.php b/src/applications/maniphest/storage/ManiphestTask.php index 5ac290078b..93a5f5562c 100644 --- a/src/applications/maniphest/storage/ManiphestTask.php +++ b/src/applications/maniphest/storage/ManiphestTask.php @@ -1,466 +1,473 @@ setViewer($actor) ->withClasses(array('PhabricatorManiphestApplication')) ->executeOne(); $view_policy = $app->getPolicy(ManiphestDefaultViewCapability::CAPABILITY); $edit_policy = $app->getPolicy(ManiphestDefaultEditCapability::CAPABILITY); return id(new ManiphestTask()) ->setStatus(ManiphestTaskStatus::getDefaultStatus()) ->setPriority(ManiphestTaskPriority::getDefaultPriority()) ->setAuthorPHID($actor->getPHID()) ->setViewPolicy($view_policy) ->setEditPolicy($edit_policy) ->setSpacePHID($actor->getDefaultSpacePHID()) ->attachProjectPHIDs(array()) ->attachSubscriberPHIDs(array()); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'properties' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'ownerPHID' => 'phid?', 'status' => 'text12', 'priority' => 'uint32', 'title' => 'sort', 'originalTitle' => 'text', 'description' => 'text', 'mailKey' => 'bytes20', 'ownerOrdering' => 'text64?', 'originalEmailSource' => 'text255?', 'subpriority' => 'double', 'points' => 'double?', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), 'unique' => true, ), 'priority' => array( 'columns' => array('priority', 'status'), ), 'status' => array( 'columns' => array('status'), ), 'ownerPHID' => array( 'columns' => array('ownerPHID', 'status'), ), 'authorPHID' => array( 'columns' => array('authorPHID', 'status'), ), 'ownerOrdering' => array( 'columns' => array('ownerOrdering'), ), 'priority_2' => array( 'columns' => array('priority', 'subpriority'), ), 'key_dateCreated' => array( 'columns' => array('dateCreated'), ), 'key_dateModified' => array( 'columns' => array('dateModified'), ), 'key_title' => array( 'columns' => array('title(64)'), ), ), ) + parent::getConfiguration(); } public function loadDependsOnTaskPHIDs() { return PhabricatorEdgeQuery::loadDestinationPHIDs( $this->getPHID(), ManiphestTaskDependsOnTaskEdgeType::EDGECONST); } public function loadDependedOnByTaskPHIDs() { return PhabricatorEdgeQuery::loadDestinationPHIDs( $this->getPHID(), ManiphestTaskDependedOnByTaskEdgeType::EDGECONST); } public function generatePHID() { return PhabricatorPHID::generateNewPHID(ManiphestTaskPHIDType::TYPECONST); } public function getSubscriberPHIDs() { return $this->assertAttached($this->subscriberPHIDs); } public function getProjectPHIDs() { return $this->assertAttached($this->edgeProjectPHIDs); } public function attachProjectPHIDs(array $phids) { $this->edgeProjectPHIDs = $phids; return $this; } public function attachSubscriberPHIDs(array $phids) { $this->subscriberPHIDs = $phids; return $this; } public function setOwnerPHID($phid) { $this->ownerPHID = nonempty($phid, null); return $this; } public function setTitle($title) { $this->title = $title; if (!$this->getID()) { $this->originalTitle = $title; } return $this; } public function getMonogram() { return 'T'.$this->getID(); } public function attachGroupByProjectPHID($phid) { $this->groupByProjectPHID = $phid; return $this; } public function getGroupByProjectPHID() { return $this->assertAttached($this->groupByProjectPHID); } public function save() { if (!$this->mailKey) { $this->mailKey = Filesystem::readRandomCharacters(20); } $result = parent::save(); return $result; } public function isClosed() { return ManiphestTaskStatus::isClosedStatus($this->getStatus()); } public function setProperty($key, $value) { $this->properties[$key] = $value; return $this; } public function getProperty($key, $default = null) { return idx($this->properties, $key, $default); } public function getCoverImageFilePHID() { return idx($this->properties, 'cover.filePHID'); } public function getCoverImageThumbnailPHID() { return idx($this->properties, 'cover.thumbnailPHID'); } public function getWorkboardOrderVectors() { return array( PhabricatorProjectColumn::ORDER_PRIORITY => array( (int)-$this->getPriority(), (double)-$this->getSubpriority(), (int)-$this->getID(), ), ); } + public function getWorkboardProperties() { + return array( + 'status' => $this->getStatus(), + 'points' => (double)$this->getPoints(), + ); + } + /* -( PhabricatorSubscribableInterface )----------------------------------- */ public function isAutomaticallySubscribed($phid) { return ($phid == $this->getOwnerPHID()); } public function shouldShowSubscribersProperty() { return true; } /* -( Markup Interface )--------------------------------------------------- */ /** * @task markup */ public function getMarkupFieldKey($field) { $hash = PhabricatorHash::digest($this->getMarkupText($field)); $id = $this->getID(); return "maniphest:T{$id}:{$field}:{$hash}"; } /** * @task markup */ public function getMarkupText($field) { return $this->getDescription(); } /** * @task markup */ public function newMarkupEngine($field) { return PhabricatorMarkupEngine::newManiphestMarkupEngine(); } /** * @task markup */ public function didMarkupText( $field, $output, PhutilMarkupEngine $engine) { return $output; } /** * @task markup */ public function shouldUseMarkupCache($field) { return (bool)$this->getID(); } /* -( Policy Interface )--------------------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return $this->getEditPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $user) { // The owner of a task can always view and edit it. $owner_phid = $this->getOwnerPHID(); if ($owner_phid) { $user_phid = $user->getPHID(); if ($user_phid == $owner_phid) { return true; } } return false; } public function describeAutomaticCapability($capability) { return pht('The owner of a task can always view and edit it.'); } /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ public function getUsersToNotifyOfTokenGiven() { // Sort of ambiguous who this was intended for; just let them both know. return array_filter( array_unique( array( $this->getAuthorPHID(), $this->getOwnerPHID(), ))); } /* -( PhabricatorCustomFieldInterface )------------------------------------ */ public function getCustomFieldSpecificationForRole($role) { return PhabricatorEnv::getEnvConfig('maniphest.fields'); } public function getCustomFieldBaseClass() { return 'ManiphestCustomField'; } public function getCustomFields() { return $this->assertAttached($this->customFields); } public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) { $this->customFields = $fields; return $this; } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $this->delete(); $this->saveTransaction(); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new ManiphestTransactionEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new ManiphestTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( PhabricatorSpacesInterface )----------------------------------------- */ public function getSpacePHID() { return $this->spacePHID; } /* -( PhabricatorConduitResultInterface )---------------------------------- */ public function getFieldSpecificationsForConduit() { return array( id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('title') ->setType('string') ->setDescription(pht('The title of the task.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('authorPHID') ->setType('phid') ->setDescription(pht('Original task author.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('ownerPHID') ->setType('phid?') ->setDescription(pht('Current task owner, if task is assigned.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('status') ->setType('map') ->setDescription(pht('Information about task status.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('priority') ->setType('map') ->setDescription(pht('Information about task priority.')), ); } public function getFieldValuesForConduit() { $status_value = $this->getStatus(); $status_info = array( 'value' => $status_value, 'name' => ManiphestTaskStatus::getTaskStatusName($status_value), 'color' => ManiphestTaskStatus::getStatusColor($status_value), ); $priority_value = (int)$this->getPriority(); $priority_info = array( 'value' => $priority_value, 'subpriority' => (double)$this->getSubpriority(), 'name' => ManiphestTaskPriority::getTaskPriorityName($priority_value), 'color' => ManiphestTaskPriority::getTaskPriorityColor($priority_value), ); return array( 'name' => $this->getTitle(), 'authorPHID' => $this->getAuthorPHID(), 'ownerPHID' => $this->getOwnerPHID(), 'status' => $status_info, 'priority' => $priority_info, ); } public function getConduitSearchAttachments() { return array(); } /* -( PhabricatorFulltextInterface )--------------------------------------- */ public function newFulltextEngine() { return new ManiphestTaskFulltextEngine(); } } diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php index 2138367ca9..c075072d9f 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php @@ -1,982 +1,988 @@ getUser(); $response = $this->loadProject(); if ($response) { return $response; } $project = $this->getProject(); $this->readRequestState(); $board_uri = $this->getApplicationURI('board/'.$project->getID().'/'); $search_engine = id(new ManiphestTaskSearchEngine()) ->setViewer($viewer) ->setBaseURI($board_uri) ->setIsBoardView(true); if ($request->isFormPost() && !$request->getBool('initialize')) { $saved = $search_engine->buildSavedQueryFromRequest($request); $search_engine->saveQuery($saved); $filter_form = id(new AphrontFormView()) ->setUser($viewer); $search_engine->buildSearchForm($filter_form, $saved); if ($search_engine->getErrors()) { return $this->newDialog() ->setWidth(AphrontDialogView::WIDTH_FULL) ->setTitle(pht('Advanced Filter')) ->appendChild($filter_form->buildLayoutView()) ->setErrors($search_engine->getErrors()) ->setSubmitURI($board_uri) ->addSubmitButton(pht('Apply Filter')) ->addCancelButton($board_uri); } return id(new AphrontRedirectResponse())->setURI( $this->getURIWithState( $search_engine->getQueryResultsPageURI($saved->getQueryKey()))); } $query_key = $request->getURIData('queryKey'); if (!$query_key) { $query_key = 'open'; } $this->queryKey = $query_key; $custom_query = null; if ($search_engine->isBuiltinQuery($query_key)) { $saved = $search_engine->buildSavedQueryFromBuiltin($query_key); } else { $saved = id(new PhabricatorSavedQueryQuery()) ->setViewer($viewer) ->withQueryKeys(array($query_key)) ->executeOne(); if (!$saved) { return new Aphront404Response(); } $custom_query = $saved; } if ($request->getURIData('filter')) { $filter_form = id(new AphrontFormView()) ->setUser($viewer); $search_engine->buildSearchForm($filter_form, $saved); return $this->newDialog() ->setWidth(AphrontDialogView::WIDTH_FULL) ->setTitle(pht('Advanced Filter')) ->appendChild($filter_form->buildLayoutView()) ->setSubmitURI($board_uri) ->addSubmitButton(pht('Apply Filter')) ->addCancelButton($board_uri); } $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_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()) ->setViewer($viewer) ->setBoardPHIDs(array($board_phid)) ->setObjectPHIDs(array_keys($tasks)) ->setFetchAllBoards(true) ->executeLayout(); $columns = $layout_engine->getColumns($board_phid); if (!$columns || !$project->getHasWorkboard()) { $has_normal_columns = false; foreach ($columns as $column) { if (!$column->getProxyPHID()) { $has_normal_columns = true; break; } } $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $project, PhabricatorPolicyCapability::CAN_EDIT); if (!$has_normal_columns) { if (!$can_edit) { $content = $this->buildNoAccessContent($project); } else { $content = $this->buildInitializeContent($project); } } else { if (!$can_edit) { $content = $this->buildDisabledContent($project); } else { $content = $this->buildEnableContent($project); } } if ($content instanceof AphrontResponse) { return $content; } $nav = $this->getProfileMenu(); $nav->selectFilter(PhabricatorProject::PANEL_WORKBOARD); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Workboard')); return $this->newPage() ->setTitle( array( $project->getDisplayName(), pht('Workboard'), )) ->setNavigation($nav) ->setCrumbs($crumbs) ->appendChild($content); } $task_can_edit_map = id(new PhabricatorPolicyFilter()) ->setViewer($viewer) ->requireCapabilities(array(PhabricatorPolicyCapability::CAN_EDIT)) ->apply($tasks); // If this is a batch edit, select the editable tasks in the chosen column // and ship the user into the batch editor. $batch_edit = $request->getStr('batch'); if ($batch_edit) { if ($batch_edit !== self::BATCH_EDIT_ALL) { $column_id_map = mpull($columns, null, 'getID'); $batch_column = idx($column_id_map, $batch_edit); if (!$batch_column) { return new Aphront404Response(); } $batch_task_phids = $layout_engine->getColumnObjectPHIDs( $board_phid, $batch_column->getPHID()); foreach ($batch_task_phids as $key => $batch_task_phid) { if (empty($task_can_edit_map[$batch_task_phid])) { unset($batch_task_phids[$key]); } } $batch_tasks = array_select_keys($tasks, $batch_task_phids); } else { $batch_tasks = $task_can_edit_map; } if (!$batch_tasks) { $cancel_uri = $this->getURIWithState($board_uri); return $this->newDialog() ->setTitle(pht('No Editable Tasks')) ->appendParagraph( pht( 'The selected column contains no visible tasks which you '. 'have permission to edit.')) ->addCancelButton($board_uri); } $batch_ids = mpull($batch_tasks, 'getID'); $batch_ids = implode(',', $batch_ids); $batch_uri = new PhutilURI('/maniphest/batch/'); $batch_uri->setQueryParam('board', $this->id); $batch_uri->setQueryParam('batch', $batch_ids); return id(new AphrontRedirectResponse()) ->setURI($batch_uri); } $board_id = celerity_generate_unique_node_id(); $board = id(new PHUIWorkboardView()) ->setUser($viewer) ->setID($board_id) ->addSigil('jx-workboard') ->setMetadata( array( 'boardPHID' => $project->getPHID(), )); $visible_columns = array(); $column_phids = array(); $visible_phids = array(); foreach ($columns as $column) { if (!$this->showHidden) { if ($column->isHidden()) { continue; } } $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()); $column_tasks = array_select_keys($tasks, $task_phids); // If we aren't using "natural" order, reorder the column by the original // query order. if ($this->sortKey != PhabricatorProjectColumn::ORDER_NATURAL) { $column_tasks = array_select_keys($column_tasks, array_keys($tasks)); } $column_phid = $column->getPHID(); $visible_columns[$column_phid] = $column; $column_phids[$column_phid] = $column_tasks; foreach ($column_tasks as $phid => $task) { $visible_phids[$phid] = $phid; } } $rendering_engine = id(new PhabricatorBoardRenderingEngine()) ->setViewer($viewer) ->setObjects(array_select_keys($tasks, $visible_phids)) ->setEditMap($task_can_edit_map) ->setExcludedProjectPHIDs($select_phids); $templates = array(); $column_maps = array(); $all_tasks = array(); foreach ($visible_columns as $column_phid => $column) { $column_tasks = $column_phids[$column_phid]; $panel = id(new PHUIWorkpanelView()) ->setHeader($column->getDisplayName()) ->setSubHeader($column->getDisplayType()) ->addSigil('workpanel'); $proxy = $column->getProxy(); if ($proxy) { $proxy_id = $proxy->getID(); $href = $this->getApplicationURI("view/{$proxy_id}/"); $panel->setHref($href); } $header_icon = $column->getHeaderIcon(); if ($header_icon) { $panel->setHeaderIcon($header_icon); } $display_class = $column->getDisplayClass(); if ($display_class) { $panel->addClass($display_class); } if ($column->isHidden()) { $panel->addClass('project-panel-hidden'); } $column_menu = $this->buildColumnMenu($project, $column); $panel->addHeaderAction($column_menu); - $tag_id = celerity_generate_unique_node_id(); - $tag_content_id = celerity_generate_unique_node_id(); - $count_tag = id(new PHUITagView()) ->setType(PHUITagView::TYPE_SHADE) ->setShade(PHUITagView::COLOR_BLUE) - ->setID($tag_id) - ->setName(phutil_tag('span', array('id' => $tag_content_id), '-')) + ->addSigil('column-points') + ->setName( + javelin_tag( + 'span', + array( + 'sigil' => 'column-points-content', + ), + pht('-'))) ->setStyle('display: none'); $panel->setHeaderTag($count_tag); $cards = id(new PHUIObjectItemListView()) ->setUser($viewer) ->setFlush(true) ->setAllowEmptyList(true) ->addSigil('project-column') ->setItemClass('phui-workcard') ->setMetadata( array( 'columnPHID' => $column->getPHID(), - 'countTagID' => $tag_id, - 'countTagContentID' => $tag_content_id, 'pointLimit' => $column->getPointLimit(), )); foreach ($column_tasks as $task) { $object_phid = $task->getPHID(); $card = $rendering_engine->renderCard($object_phid); $templates[$object_phid] = hsprintf('%s', $card->getItem()); $column_maps[$column_phid][] = $object_phid; $all_tasks[$object_phid] = $task; } $panel->setCards($cards); $board->addPanel($panel); } $behavior_config = array( - 'boardID' => $board_id, - 'projectPHID' => $project->getPHID(), 'moveURI' => $this->getApplicationURI('move/'.$project->getID().'/'), 'createURI' => $this->getCreateURI(), 'uploadURI' => '/file/dropupload/', 'coverURI' => $this->getApplicationURI('cover/'), 'chunkThreshold' => PhabricatorFileStorageEngine::getChunkThreshold(), + 'pointsEnabled' => ManiphestTaskPoints::getIsEnabled(), + + 'boardPHID' => $project->getPHID(), 'order' => $this->sortKey, 'templateMap' => $templates, 'columnMaps' => $column_maps, 'orderMaps' => mpull($all_tasks, 'getWorkboardOrderVectors'), + 'propertyMaps' => mpull($all_tasks, 'getWorkboardProperties'), + + 'boardID' => $board_id, + 'projectPHID' => $project->getPHID(), ); $this->initBehavior('project-boards', $behavior_config); $sort_menu = $this->buildSortMenu( $viewer, $this->sortKey); $filter_menu = $this->buildFilterMenu( $viewer, $custom_query, $search_engine, $query_key); $manage_menu = $this->buildManageMenu($project, $this->showHidden); $header_link = phutil_tag( 'a', array( 'href' => $this->getApplicationURI('profile/'.$project->getID().'/'), ), $project->getName()); $board_box = id(new PHUIBoxView()) ->appendChild($board) ->addClass('project-board-wrapper'); $nav = $this->getProfileMenu(); $divider = id(new PHUIListItemView()) ->setType(PHUIListItemView::TYPE_DIVIDER); $fullscreen = $this->buildFullscreenMenu(); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Workboard')); $crumbs->setBorder(true); $crumbs->addAction($sort_menu); $crumbs->addAction($filter_menu); $crumbs->addAction($divider); $crumbs->addAction($manage_menu); $crumbs->addAction($fullscreen); return $this->newPage() ->setTitle( array( $project->getDisplayName(), pht('Workboard'), )) ->setPageObjectPHIDs(array($project->getPHID())) ->setShowFooter(false) ->setNavigation($nav) ->setCrumbs($crumbs) ->addQuicksandConfig( array( 'boardConfig' => $behavior_config, )) ->appendChild( array( $board_box, )); } private function readRequestState() { $request = $this->getRequest(); $project = $this->getProject(); $this->showHidden = $request->getBool('hidden'); $this->id = $project->getID(); $sort_key = $request->getStr('order'); switch ($sort_key) { case PhabricatorProjectColumn::ORDER_NATURAL: case PhabricatorProjectColumn::ORDER_PRIORITY: break; default: $sort_key = PhabricatorProjectColumn::DEFAULT_ORDER; break; } $this->sortKey = $sort_key; } private function buildSortMenu( PhabricatorUser $viewer, $sort_key) { $sort_icon = id(new PHUIIconView()) ->setIcon('fa-sort-amount-asc bluegrey'); $named = array( PhabricatorProjectColumn::ORDER_NATURAL => pht('Natural'), PhabricatorProjectColumn::ORDER_PRIORITY => pht('Sort by Priority'), ); $base_uri = $this->getURIWithState(); $items = array(); foreach ($named as $key => $name) { $is_selected = ($key == $sort_key); if ($is_selected) { $active_order = $name; } $item = id(new PhabricatorActionView()) ->setIcon('fa-sort-amount-asc') ->setSelected($is_selected) ->setName($name); $uri = $base_uri->alter('order', $key); $item->setHref($uri); $items[] = $item; } $sort_menu = id(new PhabricatorActionListView()) ->setUser($viewer); foreach ($items as $item) { $sort_menu->addAction($item); } $sort_button = id(new PHUIListItemView()) ->setName($active_order) ->setIcon('fa-sort-amount-asc') ->setHref('#') ->addSigil('boards-dropdown-menu') ->setMetadata( array( 'items' => hsprintf('%s', $sort_menu), )); return $sort_button; } private function buildFilterMenu( PhabricatorUser $viewer, $custom_query, PhabricatorApplicationSearchEngine $engine, $query_key) { $named = array( 'open' => pht('Open Tasks'), 'all' => pht('All Tasks'), ); if ($viewer->isLoggedIn()) { $named['assigned'] = pht('Assigned to Me'); } if ($custom_query) { $named[$custom_query->getQueryKey()] = pht('Custom Filter'); } $items = array(); foreach ($named as $key => $name) { $is_selected = ($key == $query_key); if ($is_selected) { $active_filter = $name; } $is_custom = false; if ($custom_query) { $is_custom = ($key == $custom_query->getQueryKey()); } $item = id(new PhabricatorActionView()) ->setIcon('fa-search') ->setSelected($is_selected) ->setName($name); if ($is_custom) { $uri = $this->getApplicationURI( 'board/'.$this->id.'/filter/query/'.$key.'/'); $item->setWorkflow(true); } else { $uri = $engine->getQueryResultsPageURI($key); } $uri = $this->getURIWithState($uri); $item->setHref($uri); $items[] = $item; } $items[] = id(new PhabricatorActionView()) ->setIcon('fa-cog') ->setHref($this->getApplicationURI('board/'.$this->id.'/filter/')) ->setWorkflow(true) ->setName(pht('Advanced Filter...')); $filter_menu = id(new PhabricatorActionListView()) ->setUser($viewer); foreach ($items as $item) { $filter_menu->addAction($item); } $filter_button = id(new PHUIListItemView()) ->setName($active_filter) ->setIcon('fa-search') ->setHref('#') ->addSigil('boards-dropdown-menu') ->setMetadata( array( 'items' => hsprintf('%s', $filter_menu), )); return $filter_button; } private function buildManageMenu( PhabricatorProject $project, $show_hidden) { $request = $this->getRequest(); $viewer = $request->getUser(); $id = $project->getID(); $disable_uri = $this->getApplicationURI("board/{$id}/disable/"); $add_uri = $this->getApplicationURI("board/{$id}/edit/"); $reorder_uri = $this->getApplicationURI("board/{$id}/reorder/"); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $project, PhabricatorPolicyCapability::CAN_EDIT); $manage_items = array(); $manage_items[] = id(new PhabricatorActionView()) ->setIcon('fa-plus') ->setName(pht('Add Column')) ->setHref($add_uri) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit); $manage_items[] = id(new PhabricatorActionView()) ->setIcon('fa-exchange') ->setName(pht('Reorder Columns')) ->setHref($reorder_uri) ->setDisabled(!$can_edit) ->setWorkflow(true); if ($show_hidden) { $hidden_uri = $this->getURIWithState() ->setQueryParam('hidden', null); $hidden_icon = 'fa-eye-slash'; $hidden_text = pht('Hide Hidden Columns'); } else { $hidden_uri = $this->getURIWithState() ->setQueryParam('hidden', 'true'); $hidden_icon = 'fa-eye'; $hidden_text = pht('Show Hidden Columns'); } $manage_items[] = id(new PhabricatorActionView()) ->setIcon($hidden_icon) ->setName($hidden_text) ->setHref($hidden_uri); $batch_edit_uri = $request->getRequestURI(); $batch_edit_uri->setQueryParam('batch', self::BATCH_EDIT_ALL); $can_batch_edit = PhabricatorPolicyFilter::hasCapability( $viewer, PhabricatorApplication::getByClass('PhabricatorManiphestApplication'), ManiphestBulkEditCapability::CAPABILITY); $manage_items[] = id(new PhabricatorActionView()) ->setIcon('fa-list-ul') ->setName(pht('Batch Edit Visible Tasks...')) ->setHref($batch_edit_uri) ->setDisabled(!$can_batch_edit); $manage_items[] = id(new PhabricatorActionView()) ->setIcon('fa-ban') ->setName(pht('Disable Workboard')) ->setHref($disable_uri) ->setWorkflow(true) ->setDisabled(!$can_edit); $manage_menu = id(new PhabricatorActionListView()) ->setUser($viewer); foreach ($manage_items as $item) { $manage_menu->addAction($item); } $manage_button = id(new PHUIListItemView()) ->setIcon('fa-cog') ->setHref('#') ->addSigil('boards-dropdown-menu') ->addSigil('has-tooltip') ->setMetadata( array( 'tip' => pht('Manage'), 'align' => 'S', 'items' => hsprintf('%s', $manage_menu), )); return $manage_button; } private function buildFullscreenMenu() { $up = id(new PHUIListItemView()) ->setIcon('fa-arrows-alt') ->setHref('#') ->addClass('phui-workboard-expand-icon') ->addSigil('jx-toggle-class') ->addSigil('has-tooltip') ->setMetaData(array( 'tip' => pht('Fullscreen'), 'map' => array( 'phabricator-standard-page' => 'phui-workboard-fullscreen', ), )); return $up; } private function buildColumnMenu( PhabricatorProject $project, PhabricatorProjectColumn $column) { $request = $this->getRequest(); $viewer = $request->getUser(); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $project, PhabricatorPolicyCapability::CAN_EDIT); $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...')) ->setHref($this->getCreateURI()) ->addSigil('column-add-task') ->setMetadata( array( 'columnPHID' => $column->getPHID(), 'projectPHID' => $default_phid, )); $batch_edit_uri = $request->getRequestURI(); $batch_edit_uri->setQueryParam('batch', $column->getID()); $can_batch_edit = PhabricatorPolicyFilter::hasCapability( $viewer, PhabricatorApplication::getByClass('PhabricatorManiphestApplication'), ManiphestBulkEditCapability::CAPABILITY); $column_items[] = id(new PhabricatorActionView()) ->setIcon('fa-list-ul') ->setName(pht('Batch Edit Tasks...')) ->setHref($batch_edit_uri) ->setDisabled(!$can_batch_edit); $detail_uri = $this->getApplicationURI( 'board/'.$this->id.'/column/'.$column->getID().'/'); $column_items[] = id(new PhabricatorActionView()) ->setIcon('fa-columns') ->setName(pht('Column Details')) ->setHref($detail_uri); $can_hide = ($can_edit && !$column->isDefaultColumn()); $hide_uri = 'board/'.$this->id.'/hide/'.$column->getID().'/'; $hide_uri = $this->getApplicationURI($hide_uri); $hide_uri = $this->getURIWithState($hide_uri); if (!$column->isHidden()) { $column_items[] = id(new PhabricatorActionView()) ->setName(pht('Hide Column')) ->setIcon('fa-eye-slash') ->setHref($hide_uri) ->setDisabled(!$can_hide) ->setWorkflow(true); } else { $column_items[] = id(new PhabricatorActionView()) ->setName(pht('Show Column')) ->setIcon('fa-eye') ->setHref($hide_uri) ->setDisabled(!$can_hide) ->setWorkflow(true); } $column_menu = id(new PhabricatorActionListView()) ->setUser($viewer); foreach ($column_items as $item) { $column_menu->addAction($item); } $column_button = id(new PHUIIconView()) ->setIcon('fa-caret-down') ->setHref('#') ->addSigil('boards-dropdown-menu') ->setMetadata( array( 'items' => hsprintf('%s', $column_menu), )); return $column_button; } /** * Add current state parameters (like order and the visibility of hidden * columns) to a URI. * * This allows actions which toggle or adjust one piece of state to keep * the rest of the board state persistent. If no URI is provided, this method * starts with the request URI. * * @param string|null URI to add state parameters to. * @return PhutilURI URI with state parameters. */ private function getURIWithState($base = null) { if ($base === null) { $base = $this->getRequest()->getRequestURI(); } $base = new PhutilURI($base); if ($this->sortKey != PhabricatorProjectColumn::DEFAULT_ORDER) { $base->setQueryParam('order', $this->sortKey); } else { $base->setQueryParam('order', null); } $base->setQueryParam('hidden', $this->showHidden ? 'true' : null); return $base; } private function getCreateURI() { $viewer = $this->getViewer(); // TODO: This should be cleaned up, but maybe we're going to make options // for each column or board? $edit_config = id(new ManiphestEditEngine()) ->setViewer($viewer) ->loadDefaultEditConfiguration(); if ($edit_config) { $form_key = $edit_config->getIdentifier(); $create_uri = "/maniphest/task/edit/form/{$form_key}/"; } else { $create_uri = '/maniphest/task/edit/'; } return $create_uri; } private function buildInitializeContent(PhabricatorProject $project) { $request = $this->getRequest(); $viewer = $this->getViewer(); $type = $request->getStr('initialize-type'); $id = $project->getID(); $profile_uri = $this->getApplicationURI("profile/{$id}/"); $board_uri = $this->getApplicationURI("board/{$id}/"); $import_uri = $this->getApplicationURI("board/{$id}/import/"); $set_default = $request->getBool('default'); if ($set_default) { $this ->getProfilePanelEngine() ->adjustDefault(PhabricatorProject::PANEL_WORKBOARD); } if ($request->isFormPost()) { if ($type == 'backlog-only') { $column = PhabricatorProjectColumn::initializeNewColumn($viewer) ->setSequence(0) ->setProperty('isDefault', true) ->setProjectPHID($project->getPHID()) ->save(); $project->setHasWorkboard(1)->save(); return id(new AphrontRedirectResponse()) ->setURI($board_uri); } else { return id(new AphrontRedirectResponse()) ->setURI($import_uri); } } // 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') ->setValue('backlog-only') ->addButton( 'backlog-only', pht('New Empty Board'), pht('Create a new board with just a backlog column.')) ->addButton( 'import', pht('Import Columns'), pht('Import board columns from another project.')); $default_checkbox = id(new AphrontFormCheckboxControl()) ->setLabel(pht('Make Default')) ->addCheckbox( 'default', 1, pht('Make the workboard the default view for this project.'), true); $form = id(new AphrontFormView()) ->setUser($viewer) ->addHiddenInput('initialize', 1) ->appendRemarkupInstructions( pht('The workboard for this project has not been created yet.')) ->appendControl($new_selector) ->appendControl($default_checkbox) ->appendControl( id(new AphrontFormSubmitControl()) ->addCancelButton($profile_uri) ->setValue(pht('Create Workboard'))); $box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Create Workboard')) ->setForm($form); return $box; } private function buildNoAccessContent(PhabricatorProject $project) { $viewer = $this->getViewer(); $id = $project->getID(); $profile_uri = $this->getApplicationURI("profile/{$id}/"); return $this->newDialog() ->setTitle(pht('Unable to Create Workboard')) ->appendParagraph( pht( 'The workboard for this project has not been created yet, '. 'but you do not have permission to create it. Only users '. 'who can edit this project can create a workboard for it.')) ->addCancelButton($profile_uri); } private function buildEnableContent(PhabricatorProject $project) { $request = $this->getRequest(); $viewer = $this->getViewer(); $id = $project->getID(); $profile_uri = $this->getApplicationURI("profile/{$id}/"); $board_uri = $this->getApplicationURI("board/{$id}/"); if ($request->isFormPost()) { $xactions = array(); $xactions[] = id(new PhabricatorProjectTransaction()) ->setTransactionType(PhabricatorProjectTransaction::TYPE_HASWORKBOARD) ->setNewValue(1); id(new PhabricatorProjectTransactionEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true) ->applyTransactions($project, $xactions); return id(new AphrontRedirectResponse()) ->setURI($board_uri); } return $this->newDialog() ->setTitle(pht('Workboard Disabled')) ->addHiddenInput('initialize', 1) ->appendParagraph( pht( 'This workboard has been disabled, but can be restored to its '. 'former glory.')) ->addCancelButton($profile_uri) ->addSubmitButton(pht('Enable Workboard')); } private function buildDisabledContent(PhabricatorProject $project) { $viewer = $this->getViewer(); $id = $project->getID(); $profile_uri = $this->getApplicationURI("profile/{$id}/"); return $this->newDialog() ->setTitle(pht('Workboard Disabled')) ->appendParagraph( pht( 'This workboard has been disabled, and you do not have permission '. 'to enable it. Only users who can edit this project can restore '. 'the workboard.')) ->addCancelButton($profile_uri); } } diff --git a/src/applications/project/engine/PhabricatorBoardResponseEngine.php b/src/applications/project/engine/PhabricatorBoardResponseEngine.php index d1fd3f33a8..969dfa3bc8 100644 --- a/src/applications/project/engine/PhabricatorBoardResponseEngine.php +++ b/src/applications/project/engine/PhabricatorBoardResponseEngine.php @@ -1,146 +1,149 @@ viewer = $viewer; return $this; } public function getViewer() { return $this->viewer; } public function setBoardPHID($board_phid) { $this->boardPHID = $board_phid; return $this; } public function getBoardPHID() { return $this->boardPHID; } public function setObjectPHID($object_phid) { $this->objectPHID = $object_phid; return $this; } public function getObjectPHID() { return $this->objectPHID; } public function setVisiblePHIDs(array $visible_phids) { $this->visiblePHIDs = $visible_phids; return $this; } public function getVisiblePHIDs() { return $this->visiblePHIDs; } public function buildResponse() { $viewer = $this->getViewer(); $object_phid = $this->getObjectPHID(); $board_phid = $this->getBoardPHID(); // Load all the other tasks that are visible in the affected columns and // perform layout for them. $visible_phids = $this->getAllVisiblePHIDs(); $layout_engine = id(new PhabricatorBoardLayoutEngine()) ->setViewer($viewer) ->setBoardPHIDs(array($board_phid)) ->setObjectPHIDs($visible_phids) ->executeLayout(); $object_columns = $layout_engine->getObjectColumns( $board_phid, $object_phid); $natural = array(); foreach ($object_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()) ->setViewer($viewer) ->withPHIDs($visible_phids) ->execute(); $order_maps = array(); foreach ($all_visible as $visible) { $order_maps[$visible->getPHID()] = $visible->getWorkboardOrderVectors(); } - $template = $this->buildTemplate(); + $object = id(new ManiphestTaskQuery()) + ->setViewer($viewer) + ->withPHIDs(array($object_phid)) + ->needProjectPHIDs(true) + ->executeOne(); + if (!$object) { + return new Aphront404Response(); + } + + $template = $this->buildTemplate($object); $payload = array( 'objectPHID' => $object_phid, 'cardHTML' => $template, 'columnMaps' => $natural, 'orderMaps' => $order_maps, + 'propertyMaps' => array( + $object_phid => $object->getWorkboardProperties(), + ), ); return id(new AphrontAjaxResponse()) ->setContent($payload); } - private function buildTemplate() { + private function buildTemplate($object) { $viewer = $this->getViewer(); $object_phid = $this->getObjectPHID(); $excluded_phids = $this->loadExcludedProjectPHIDs(); - $object = id(new ManiphestTaskQuery()) - ->setViewer($viewer) - ->withPHIDs(array($object_phid)) - ->needProjectPHIDs(true) - ->executeOne(); - if (!$object) { - return new Aphront404Response(); - } - $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(); $exclude_phids = array($board_phid); $descendants = id(new PhabricatorProjectQuery()) ->setViewer($viewer) ->withAncestorProjectPHIDs($exclude_phids) ->execute(); foreach ($descendants as $descendant) { $exclude_phids[] = $descendant->getPHID(); } return array_fuse($exclude_phids); } private function getAllVisiblePHIDs() { $visible_phids = $this->getVisiblePHIDs(); $visible_phids[] = $this->getObjectPHID(); $visible_phids = array_fuse($visible_phids); return $visible_phids; } } diff --git a/webroot/rsrc/js/application/projects/WorkboardBoard.js b/webroot/rsrc/js/application/projects/WorkboardBoard.js index 343086d569..4506041cca 100644 --- a/webroot/rsrc/js/application/projects/WorkboardBoard.js +++ b/webroot/rsrc/js/application/projects/WorkboardBoard.js @@ -1,221 +1,258 @@ /** * @provides javelin-workboard-board * @requires javelin-install * javelin-dom * javelin-util * javelin-stratcom * javelin-workflow * phabricator-draggable-list * javelin-workboard-column * @javelin */ JX.install('WorkboardBoard', { construct: function(controller, phid, root) { this._controller = controller; this._phid = phid; this._root = root; this._templates = {}; this._orderMaps = {}; + this._propertiesMap = {}; this._buildColumns(); }, properties: { order: null, + pointsEnabled: false }, members: { _controller: null, _phid: null, _root: null, _columns: null, _templates: null, _orderMaps: null, + _propertiesMap: null, getRoot: function() { return this._root; }, getColumns: function() { return this._columns; }, getColumn: function(k) { return this._columns[k]; }, getPHID: function() { return this._phid; }, setCardTemplate: function(phid, template) { this._templates[phid] = template; return this; }, + setObjectProperties: function(phid, properties) { + this._propertiesMap[phid] = properties; + return this; + }, + + getObjectProperties: function(phid) { + return this._propertiesMap[phid]; + }, + getCardTemplate: function(phid) { return this._templates[phid]; }, getController: function() { return this._controller; }, setOrderMap: function(phid, map) { this._orderMaps[phid] = map; return this; }, getOrderVector: function(phid, key) { return this._orderMaps[phid][key]; }, start: function() { this._setupDragHandlers(); for (var k in this._columns) { this._columns[k].redraw(); } }, _buildColumns: function() { var nodes = JX.DOM.scry(this.getRoot(), 'ul', 'project-column'); this._columns = {}; for (var ii = 0; ii < nodes.length; ii++) { var node = nodes[ii]; var data = JX.Stratcom.getData(node); var phid = data.columnPHID; this._columns[phid] = new JX.WorkboardColumn(this, phid, node); } }, _setupDragHandlers: function() { var columns = this.getColumns(); var lists = []; for (var k in columns) { var column = columns[k]; var list = new JX.DraggableList('project-card', column.getRoot()) .setOuterContainer(this.getRoot()) .setFindItemsHandler(JX.bind(column, column.getCardNodes)) .setCanDragX(true) .setHasInfiniteHeight(true); list.listen('didDrop', JX.bind(this, this._onmovecard, list)); lists.push(list); } for (var ii = 0; ii < lists.length; ii++) { lists[ii].setGroup(lists); } }, _findCardsInColumn: function(column_node) { return JX.DOM.scry(column_node, 'li', 'project-card'); }, _onmovecard: function(list, item, after_node, src_list) { list.lock(); JX.DOM.alterClass(item, 'drag-sending', true); var src_phid = JX.Stratcom.getData(src_list.getRootNode()).columnPHID; var dst_phid = JX.Stratcom.getData(list.getRootNode()).columnPHID; var item_phid = JX.Stratcom.getData(item).objectPHID; var data = { objectPHID: item_phid, columnPHID: dst_phid, order: this.getOrder() }; if (after_node) { data.afterPHID = JX.Stratcom.getData(after_node).objectPHID; } var before_node = item.nextSibling; if (before_node) { var before_phid = JX.Stratcom.getData(before_node).objectPHID; if (before_phid) { data.beforePHID = before_phid; } } var visible_phids = []; var column = this.getColumn(dst_phid); for (var object_phid in column.getCards()) { visible_phids.push(object_phid); } data.visiblePHIDs = visible_phids.join(','); var onupdate = JX.bind( this, this._oncardupdate, list, src_phid, dst_phid, data.afterPHID); new JX.Workflow(this.getController().getMoveURI(), data) .setHandler(onupdate) .start(); }, _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); list.unlock(); }, - updateCard: function(response) { + updateCard: function(response, options) { + options = options || {}; + options.dirtyColumns = options.dirtyColumns || {}; + var columns = this.getColumns(); var phid = response.objectPHID; if (!this._templates[phid]) { for (var add_phid in response.columnMaps) { this.getColumn(add_phid).newCard(phid); } } this.setCardTemplate(phid, response.cardHTML); var order_maps = response.orderMaps; for (var order_phid in order_maps) { this.setOrderMap(order_phid, order_maps[order_phid]); } var column_maps = response.columnMaps; for (var natural_phid in column_maps) { this.getColumn(natural_phid).setNaturalOrder(column_maps[natural_phid]); } + var property_maps = response.propertyMaps; + for (var property_phid in property_maps) { + this.setObjectProperties(property_phid, property_maps[property_phid]); + } + for (var column_phid in columns) { - var cards = columns[column_phid].getCards(); + 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(); + }, + + _redrawColumns: function() { + var columns = this.getColumns(); + for (var k in columns) { + if (columns[k].isMarkedForRedraw()) { + columns[k].redraw(); } - columns[column_phid].redraw(); } } } }); diff --git a/webroot/rsrc/js/application/projects/WorkboardCard.js b/webroot/rsrc/js/application/projects/WorkboardCard.js index 69294219c0..b506e655c1 100644 --- a/webroot/rsrc/js/application/projects/WorkboardCard.js +++ b/webroot/rsrc/js/application/projects/WorkboardCard.js @@ -1,56 +1,68 @@ /** * @provides javelin-workboard-card * @requires javelin-install * @javelin */ JX.install('WorkboardCard', { construct: function(column, phid) { this._column = column; this._phid = phid; }, members: { _column: null, _phid: null, _root: null, getPHID: function() { return this._phid; }, getColumn: function() { return this._column; }, setColumn: function(column) { this._column = column; }, + getProperties: function() { + return this.getColumn().getBoard().getObjectProperties(this.getPHID()); + }, + + getPoints: function() { + return this.getProperties().points; + }, + + getStatus: function() { + return this.getProperties().status; + }, + getNode: function() { if (!this._root) { var phid = this.getPHID(); var template = this.getColumn().getBoard().getCardTemplate(phid); this._root = JX.$H(template).getFragment().firstChild; JX.Stratcom.getData(this._root).objectPHID = this.getPHID(); } return this._root; }, redraw: function() { var old_node = this._root; this._root = null; var new_node = this.getNode(); if (old_node && old_node.parentNode) { JX.DOM.replace(old_node, new_node); } return this; } } }); diff --git a/webroot/rsrc/js/application/projects/WorkboardColumn.js b/webroot/rsrc/js/application/projects/WorkboardColumn.js index 77f9ca3146..738cf151c2 100644 --- a/webroot/rsrc/js/application/projects/WorkboardColumn.js +++ b/webroot/rsrc/js/application/projects/WorkboardColumn.js @@ -1,177 +1,284 @@ /** * @provides javelin-workboard-column * @requires javelin-install * javelin-workboard-card * @javelin */ JX.install('WorkboardColumn', { construct: function(board, phid, root) { this._board = board; this._phid = phid; this._root = root; + this._panel = JX.DOM.findAbove(root, 'div', 'workpanel'); + this._pointsNode = JX.DOM.find(this._panel, 'span', 'column-points'); + + this._pointsContentNode = JX.DOM.find( + this._panel, + 'span', + 'column-points-content'); + this._cards = {}; this._naturalOrder = []; }, members: { _phid: null, _root: null, _board: null, _cards: null, _naturalOrder: null, + _panel: null, + _pointsNode: null, + _pointsContentNode: null, + _dirty: true, getPHID: function() { return this._phid; }, getRoot: function() { return this._root; }, getCards: function() { return this._cards; }, getCard: function(phid) { return this._cards[phid]; }, getBoard: function() { return this._board; }, setNaturalOrder: function(order) { this._naturalOrder = order; return this; }, + getPointsNode: function() { + return this._pointsNode; + }, + + getPointsContentNode: function() { + return this._pointsContentNode; + }, + + getWorkpanelNode: function() { + return this._panel; + }, + newCard: function(phid) { var card = new JX.WorkboardCard(this, phid); this._cards[phid] = card; this._naturalOrder.push(phid); return card; }, removeCard: function(phid) { var card = this._cards[phid]; delete this._cards[phid]; for (var ii = 0; ii < this._naturalOrder.length; ii++) { if (this._naturalOrder[ii] == phid) { this._naturalOrder.splice(ii, 1); break; } } return card; }, addCard: function(card, after) { var phid = card.getPHID(); card.setColumn(this); this._cards[phid] = card; var index = 0; if (after) { for (var ii = 0; ii < this._naturalOrder.length; ii++) { if (this._naturalOrder[ii] == after) { index = ii + 1; break; } } } if (index > this._naturalOrder.length) { this._naturalOrder.push(phid); } else { this._naturalOrder.splice(index, 0, phid); } return this; }, getCardNodes: function() { var cards = this.getCards(); var nodes = []; for (var k in cards) { nodes.push(cards[k].getNode()); } return nodes; }, getCardPHIDs: function() { return JX.keys(this.getCards()); }, + getPointLimit: function() { + return JX.Stratcom.getData(this.getRoot()).pointLimit; + }, + + markForRedraw: function() { + this._dirty = true; + }, + + isMarkedForRedraw: function() { + return this._dirty; + }, + redraw: function() { - var order = this.getBoard().getOrder(); + var board = this.getBoard(); + var order = board.getOrder(); var list; if (order == 'natural') { list = this._getCardsSortedNaturally(); } else { list = this._getCardsSortedByKey(order); } var content = []; for (var ii = 0; ii < list.length; ii++) { - var node = list[ii].getNode(); + var card = list[ii]; + + var node = card.getNode(); content.push(node); + } JX.DOM.setContent(this.getRoot(), content); + + this._redrawFrame(); + + this._dirty = false; }, _getCardsSortedNaturally: function() { var list = []; for (var ii = 0; ii < this._naturalOrder.length; ii++) { var phid = this._naturalOrder[ii]; list.push(this.getCard(phid)); } return list; }, _getCardsSortedByKey: function(order) { var cards = this.getCards(); var list = []; for (var k in cards) { list.push(cards[k]); } list.sort(JX.bind(this, this._sortCards, order)); return list; }, _sortCards: function(order, u, v) { var ud = this.getBoard().getOrderVector(u.getPHID(), order); var vd = this.getBoard().getOrderVector(v.getPHID(), order); for (var ii = 0; ii < ud.length; ii++) { if (ud[ii] > vd[ii]) { return 1; } if (ud[ii] < vd[ii]) { return -1; } } return 0; + }, + + _redrawFrame: function() { + var cards = this.getCards(); + var board = this.getBoard(); + + var points = {}; + for (var phid in cards) { + var card = cards[phid]; + + var card_points; + if (board.getPointsEnabled()) { + card_points = card.getPoints(); + } else { + card_points = 1; + } + + if (card_points !== null) { + var status = card.getStatus(); + if (!points[status]) { + points[status] = 0; + } + points[status] += card_points; + } + } + + var total_points = 0; + for (var k in points) { + total_points += points[k]; + } + + var limit = this.getPointLimit(); + + var display_value; + if (limit !== null && limit !== 0) { + display_value = total_points + ' / ' + limit; + } else { + display_value = total_points; + } + + var over_limit = ((limit !== null) && (total_points > limit)); + + var content_node = this.getPointsContentNode(); + var points_node = this.getPointsNode(); + + JX.DOM.setContent(content_node, display_value); + + var is_empty = !this.getCardPHIDs().length; + var panel = JX.DOM.findAbove(this.getRoot(), 'div', 'workpanel'); + JX.DOM.alterClass(panel, 'project-panel-empty', is_empty); + JX.DOM.alterClass(panel, 'project-panel-over-limit', over_limit); + + var color_map = { + 'phui-tag-shade-disabled': (total_points === 0), + 'phui-tag-shade-blue': (total_points > 0 && !over_limit), + 'phui-tag-shade-red': (over_limit) + }; + + for (var c in color_map) { + JX.DOM.alterClass(points_node, c, !!color_map[c]); + } + + JX.DOM.show(points_node); } } }); diff --git a/webroot/rsrc/js/application/projects/behavior-project-boards.js b/webroot/rsrc/js/application/projects/behavior-project-boards.js index 5267a09fc2..8fccb1dc91 100644 --- a/webroot/rsrc/js/application/projects/behavior-project-boards.js +++ b/webroot/rsrc/js/application/projects/behavior-project-boards.js @@ -1,161 +1,119 @@ /** * @provides javelin-behavior-project-boards * @requires javelin-behavior * javelin-dom * javelin-util * javelin-vector * javelin-stratcom * javelin-workflow * javelin-workboard-controller */ JX.behavior('project-boards', function(config, statics) { - - function onupdate(col) { - var data = JX.Stratcom.getData(col); - var cards = finditems(col); - - // Update the count of tasks in the column header. - if (!data.countTagNode) { - data.countTagNode = JX.$(data.countTagID); - JX.DOM.show(data.countTagNode); - } - - var sum = 0; - for (var ii = 0; ii < cards.length; ii++) { - // TODO: Allow this to be computed in some more clever way. - sum += 1; - } - - // TODO: This is a little bit hacky, but we don't have a PHUIX version of - // this element yet. - - var over_limit = (data.pointLimit && (sum > data.pointLimit)); - - var display_value = sum; - if (data.pointLimit) { - display_value = sum + ' / ' + data.pointLimit; - } - JX.DOM.setContent(JX.$(data.countTagContentID), display_value); - - - var panel_map = { - 'project-panel-empty': !cards.length, - 'project-panel-over-limit': over_limit - }; - var panel = JX.DOM.findAbove(col, 'div', 'workpanel'); - for (var p in panel_map) { - JX.DOM.alterClass(panel, p, !!panel_map[p]); - } - - var color_map = { - 'phui-tag-shade-disabled': (sum === 0), - 'phui-tag-shade-blue': (sum > 0 && !over_limit), - 'phui-tag-shade-red': (over_limit) - }; - for (var c in color_map) { - JX.DOM.alterClass(data.countTagNode, c, !!color_map[c]); - } - } - function update_statics(update_config) { statics.boardID = update_config.boardID; statics.projectPHID = update_config.projectPHID; statics.order = update_config.order; statics.moveURI = update_config.moveURI; statics.createURI = update_config.createURI; } function setup() { JX.Stratcom.listen('click', 'boards-dropdown-menu', function(e) { var data = e.getNodeData('boards-dropdown-menu'); if (data.menu) { return; } e.kill(); var list = JX.$H(data.items).getFragment().firstChild; var button = e.getNode('boards-dropdown-menu'); data.menu = new JX.PHUIXDropdownMenu(button); data.menu.setContent(list); data.menu.open(); JX.DOM.listen(list, 'click', 'tag:a', function(e) { if (!e.isNormalClick()) { return; } data.menu.close(); }); }); JX.Stratcom.listen( 'quicksand-redraw', null, function (e) { var data = e.getData(); if (!data.newResponse.boardConfig) { return; } var new_config; if (data.fromServer) { new_config = data.newResponse.boardConfig; statics.boardConfigCache[data.newResponseID] = new_config; } else { new_config = statics.boardConfigCache[data.newResponseID]; statics.boardID = new_config.boardID; } update_statics(new_config); }); return true; } if (!statics.setup) { update_statics(config); var current_page_id = JX.Quicksand.getCurrentPageID(); statics.boardConfigCache = {}; statics.boardConfigCache[current_page_id] = config; statics.setup = setup(); } if (!statics.workboard) { statics.workboard = new JX.WorkboardController() .setUploadURI(config.uploadURI) .setCoverURI(config.coverURI) .setMoveURI(config.moveURI) .setCreateURI(config.createURI) .setChunkThreshold(config.chunkThreshold) .start(); } var board_phid = config.projectPHID; var board_node = JX.$(config.boardID); var board = statics.workboard.newBoard(board_phid, board_node) - .setOrder(config.order); + .setOrder(config.order) + .setPointsEnabled(config.pointsEnabled); var templates = config.templateMap; for (var k in templates) { board.setCardTemplate(k, templates[k]); } var column_maps = config.columnMaps; for (var column_phid in column_maps) { var column = board.getColumn(column_phid); var column_map = column_maps[column_phid]; for (var ii = 0; ii < column_map.length; ii++) { column.newCard(column_map[ii]); } } var order_maps = config.orderMaps; for (var object_phid in order_maps) { board.setOrderMap(object_phid, order_maps[object_phid]); } + var property_maps = config.propertyMaps; + for (var property_phid in property_maps) { + board.setObjectProperties(property_phid, property_maps[property_phid]); + } + board.start(); });