diff --git a/src/applications/maniphest/controller/ManiphestBatchEditController.php b/src/applications/maniphest/controller/ManiphestBatchEditController.php index 5b6fdbf2cf..27a38fccf5 100644 --- a/src/applications/maniphest/controller/ManiphestBatchEditController.php +++ b/src/applications/maniphest/controller/ManiphestBatchEditController.php @@ -1,316 +1,316 @@ getRequest(); $user = $request->getUser(); $task_ids = $request->getArr('batch'); $tasks = id(new ManiphestTask())->loadAllWhere( 'id IN (%Ld)', $task_ids); $actions = $request->getStr('actions'); if ($actions) { $actions = json_decode($actions, true); } if ($request->isFormPost() && is_array($actions)) { foreach ($tasks as $task) { $xactions = $this->buildTransactions($actions, $task); if ($xactions) { $editor = new ManiphestTransactionEditor(); $editor->setActor($user); $editor->applyTransactions($task, $xactions); } } $task_ids = implode(',', mpull($tasks, 'getID')); return id(new AphrontRedirectResponse()) - ->setURI('/maniphest/view/custom/?s=oc&tasks='.$task_ids); + ->setURI('/maniphest/query/?ids='.$task_ids); } $panel = new AphrontPanelView(); $panel->setHeader(pht('Maniphest Batch Editor')); $panel->setNoBackground(); $handle_phids = mpull($tasks, 'getOwnerPHID'); $handles = $this->loadViewerHandles($handle_phids); $list = new ManiphestTaskListView(); $list->setTasks($tasks); $list->setUser($user); $list->setHandles($handles); $template = new AphrontTokenizerTemplateView(); $template = $template->render(); require_celerity_resource('maniphest-batch-editor'); Javelin::initBehavior( 'maniphest-batch-editor', array( 'root' => 'maniphest-batch-edit-form', 'tokenizerTemplate' => $template, 'sources' => array( 'project' => array( 'src' => '/typeahead/common/projects/', 'placeholder' => pht('Type a project name...'), ), 'owner' => array( 'src' => '/typeahead/common/searchowner/', 'placeholder' => pht('Type a user name...'), 'limit' => 1, ), 'cc' => array( 'src' => '/typeahead/common/mailable/', 'placeholder' => pht('Type a user name...'), ) ), 'input' => 'batch-form-actions', 'priorityMap' => ManiphestTaskPriority::getTaskPriorityMap(), 'statusMap' => ManiphestTaskStatus::getTaskStatusMap(), )); $form = new AphrontFormView(); $form->setUser($user); $form->setID('maniphest-batch-edit-form'); foreach ($tasks as $task) { $form->appendChild( phutil_tag( 'input', array( 'type' => 'hidden', 'name' => 'batch[]', 'value' => $task->getID(), ))); } $form->appendChild( phutil_tag( 'input', array( 'type' => 'hidden', 'name' => 'actions', 'id' => 'batch-form-actions', ))); $form->appendChild( phutil_tag('p', array(), pht('These tasks will be edited:'))); $form->appendChild($list); $form->appendChild( id(new AphrontFormInsetView()) ->setTitle('Actions') ->setRightButton(javelin_tag( 'a', array( 'href' => '#', 'class' => 'button green', 'sigil' => 'add-action', 'mustcapture' => true, ), pht('Add Another Action'))) ->setContent(javelin_tag( 'table', array( 'sigil' => 'maniphest-batch-actions', 'class' => 'maniphest-batch-actions-table', ), ''))) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Update Tasks')) ->addCancelButton('/maniphest/', 'Done')); $panel->appendChild($form); return $this->buildStandardPageResponse( $panel, array( 'title' => pht('Batch Editor'), )); } private function buildTransactions($actions, ManiphestTask $task) { $value_map = array(); $type_map = array( 'add_comment' => ManiphestTransactionType::TYPE_NONE, 'assign' => ManiphestTransactionType::TYPE_OWNER, 'status' => ManiphestTransactionType::TYPE_STATUS, 'priority' => ManiphestTransactionType::TYPE_PRIORITY, 'add_project' => ManiphestTransactionType::TYPE_PROJECTS, 'remove_project' => ManiphestTransactionType::TYPE_PROJECTS, 'add_ccs' => ManiphestTransactionType::TYPE_CCS, 'remove_ccs' => ManiphestTransactionType::TYPE_CCS, ); $edge_edit_types = array( 'add_project' => true, 'remove_project' => true, 'add_ccs' => true, 'remove_ccs' => true, ); $xactions = array(); foreach ($actions as $action) { if (empty($type_map[$action['action']])) { throw new Exception("Unknown batch edit action '{$action}'!"); } $type = $type_map[$action['action']]; // Figure out the current value, possibly after modifications by other // batch actions of the same type. For example, if the user chooses to // "Add Comment" twice, we should add both comments. More notably, if the // user chooses "Remove Project..." and also "Add Project...", we should // avoid restoring the removed project in the second transaction. if (array_key_exists($type, $value_map)) { $current = $value_map[$type]; } else { switch ($type) { case ManiphestTransactionType::TYPE_NONE: $current = null; break; case ManiphestTransactionType::TYPE_OWNER: $current = $task->getOwnerPHID(); break; case ManiphestTransactionType::TYPE_STATUS: $current = $task->getStatus(); break; case ManiphestTransactionType::TYPE_PRIORITY: $current = $task->getPriority(); break; case ManiphestTransactionType::TYPE_PROJECTS: $current = $task->getProjectPHIDs(); break; case ManiphestTransactionType::TYPE_CCS: $current = $task->getCCPHIDs(); break; } } // Check if the value is meaningful / provided, and normalize it if // necessary. This discards, e.g., empty comments and empty owner // changes. $value = $action['value']; switch ($type) { case ManiphestTransactionType::TYPE_NONE: if (!strlen($value)) { continue 2; } break; case ManiphestTransactionType::TYPE_OWNER: if (empty($value)) { continue 2; } $value = head($value); if ($value === ManiphestTaskOwner::OWNER_UP_FOR_GRABS) { $value = null; } break; case ManiphestTransactionType::TYPE_PROJECTS: if (empty($value)) { continue 2; } break; case ManiphestTransactionType::TYPE_CCS: if (empty($value)) { continue 2; } break; } // If the edit doesn't change anything, go to the next action. This // check is only valid for changes like "owner", "status", etc, not // for edge edits, because we should still apply an edit like // "Remove Projects: A, B" to a task with projects "A, B". if (empty($edge_edit_types[$action['action']])) { if ($value == $current) { continue; } } // Apply the value change; for most edits this is just replacement, but // some need to merge the current and edited values (add/remove project). switch ($type) { case ManiphestTransactionType::TYPE_NONE: if (strlen($current)) { $value = $current."\n\n".$value; } break; case ManiphestTransactionType::TYPE_PROJECTS: case ManiphestTransactionType::TYPE_CCS: $remove_actions = array( 'remove_project' => true, 'remove_ccs' => true, ); $is_remove = isset($remove_actions[$action['action']]); $current = array_fill_keys($current, true); $value = array_fill_keys($value, true); $new = $current; $did_something = false; if ($is_remove) { foreach ($value as $phid => $ignored) { if (isset($new[$phid])) { unset($new[$phid]); $did_something = true; } } } else { foreach ($value as $phid => $ignored) { if (empty($new[$phid])) { $new[$phid] = true; $did_something = true; } } } if (!$did_something) { continue 2; } $value = array_keys($new); break; } $value_map[$type] = $value; } $template = new ManiphestTransaction(); $template->setAuthorPHID($this->getRequest()->getUser()->getPHID()); // TODO: Set content source to "batch edit". foreach ($value_map as $type => $value) { $xaction = clone $template; $xaction->setTransactionType($type); switch ($type) { case ManiphestTransactionType::TYPE_NONE: $xaction->setComments($value); break; default: $xaction->setNewValue($value); break; } $xactions[] = $xaction; } return $xactions; } } diff --git a/src/applications/maniphest/controller/ManiphestExportController.php b/src/applications/maniphest/controller/ManiphestExportController.php index effb2220d2..b0c39737fc 100644 --- a/src/applications/maniphest/controller/ManiphestExportController.php +++ b/src/applications/maniphest/controller/ManiphestExportController.php @@ -1,130 +1,133 @@ key = $data['key']; return $this; } /** * @phutil-external-symbol class PHPExcel * @phutil-external-symbol class PHPExcel_IOFactory * @phutil-external-symbol class PHPExcel_Style_NumberFormat * @phutil-external-symbol class PHPExcel_Cell_DataType */ public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $ok = @include_once 'PHPExcel.php'; if (!$ok) { $dialog = new AphrontDialogView(); $dialog->setUser($user); $inst1 = pht( 'This system does not have PHPExcel installed. This software '. 'component is required to export tasks to Excel. Have your system '. 'administrator install it from:'); $inst2 = pht( 'Your PHP "include_path" needs to be updated to include the '. 'PHPExcel Classes directory.'); $dialog->setTitle(pht('Excel Export Not Configured')); $dialog->appendChild(hsprintf( '
%s
'. ''. 'http://www.phpexcel.net/'. '
'. '%s
', $inst1, $inst2)); $dialog->addCancelButton('/maniphest/'); return id(new AphrontDialogResponse())->setDialog($dialog); } // TODO: PHPExcel has a dependency on the PHP zip extension. We should test // for that here, since it fatals if we don't have the ZipArchive class. - $query = id(new PhabricatorSearchQuery())->loadOneWhere( - 'queryKey = %s', - $this->key); - if (!$query) { + $saved = id(new PhabricatorSavedQueryQuery()) + ->setViewer($user) + ->withQueryKeys(array($this->key)) + ->executeOne(); + if (!$saved) { return new Aphront404Response(); } $formats = ManiphestExcelFormat::loadAllFormats(); $export_formats = array(); foreach ($formats as $format_class => $format_object) { $export_formats[$format_class] = $format_object->getName(); } if (!$request->isDialogFormPost()) { $dialog = new AphrontDialogView(); $dialog->setUser($user); $dialog->setTitle(pht('Export Tasks to Excel')); $dialog->appendChild(phutil_tag('p', array(), pht( 'Do you want to export the query results to Excel?'))); $form = id(new PHUIFormLayoutView()) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Format:')) ->setName("excel-format") ->setOptions($export_formats)); $dialog->appendChild($form); $dialog->addCancelButton('/maniphest/'); $dialog->addSubmitButton(pht('Export to Excel')); return id(new AphrontDialogResponse())->setDialog($dialog); - } $format = idx($formats, $request->getStr("excel-format")); if ($format === null) { throw new Exception('Excel format object not found.'); } - $query->setParameter('limit', null); - $query->setParameter('offset', null); - $query->setParameter('order', 'p'); - $query->setParameter('group', 'n'); + $saved->makeEphemeral(); + $saved->setParameter('limit', PHP_INT_MAX); + + $engine = id(new ManiphestTaskSearchEngine()) + ->setViewer($user); - list($tasks, $handles) = ManiphestTaskListController::loadTasks( - $query, - $user); - // Ungroup tasks. - $tasks = array_mergev($tasks); + $query = $engine->buildQueryFromSavedQuery($saved); + $query->setViewer($user); + $tasks = $query->execute(); $all_projects = array_mergev(mpull($tasks, 'getProjectPHIDs')); - $project_handles = $this->loadViewerHandles($all_projects); - $handles += $project_handles; + $all_assigned = mpull($tasks, 'getOwnerPHID'); + + $handles = id(new PhabricatorHandleQuery()) + ->setViewer($user) + ->withPHIDs(array_merge($all_projects, $all_assigned)) + ->execute(); $workbook = new PHPExcel(); $format->buildWorkbook($workbook, $tasks, $handles, $user); $writer = PHPExcel_IOFactory::createWriter($workbook, 'Excel2007'); ob_start(); $writer->save('php://output'); $data = ob_get_clean(); $mime = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; return id(new AphrontFileResponse()) ->setMimeType($mime) ->setDownload($format->getFileName().'.xlsx') ->setContent($data); } }