3 final class PhabricatorApplicationSearchController
4 extends PhabricatorSearchBaseController
{
12 public function setPreface($preface) {
13 $this->preface
= $preface;
17 public function getPreface() {
18 return $this->preface
;
21 public function setQueryKey($query_key) {
22 $this->queryKey
= $query_key;
26 protected function getQueryKey() {
27 return $this->queryKey
;
30 public function setNavigation(AphrontSideNavFilterView
$navigation) {
31 $this->navigation
= $navigation;
35 protected function getNavigation() {
36 return $this->navigation
;
39 public function setSearchEngine(
40 PhabricatorApplicationSearchEngine
$search_engine) {
41 $this->searchEngine
= $search_engine;
45 protected function getSearchEngine() {
46 return $this->searchEngine
;
49 protected function getActiveQuery() {
50 if (!$this->activeQuery
) {
51 throw new Exception(pht('There is no active query yet.'));
54 return $this->activeQuery
;
57 protected function validateDelegatingController() {
58 $parent = $this->getDelegatingController();
62 pht('You must delegate to this controller, not invoke it directly.'));
65 $engine = $this->getSearchEngine();
67 throw new PhutilInvalidStateException('setEngine');
70 $engine->setViewer($this->getRequest()->getUser());
72 $parent = $this->getDelegatingController();
75 public function processRequest() {
76 $this->validateDelegatingController();
78 $query_action = $this->getRequest()->getURIData('queryAction');
79 if ($query_action == 'export') {
80 return $this->processExportRequest();
83 if ($query_action === 'customize') {
84 return $this->processCustomizeRequest();
87 $key = $this->getQueryKey();
89 return $this->processEditRequest();
91 return $this->processSearchRequest();
95 private function processSearchRequest() {
96 $parent = $this->getDelegatingController();
97 $request = $this->getRequest();
98 $user = $request->getUser();
99 $engine = $this->getSearchEngine();
100 $nav = $this->getNavigation();
102 $nav = $this->buildNavigation();
105 if ($request->isFormPost()) {
106 $saved_query = $engine->buildSavedQueryFromRequest($request);
107 $engine->saveQuery($saved_query);
108 return id(new AphrontRedirectResponse())->setURI(
109 $engine->getQueryResultsPageURI($saved_query->getQueryKey()).'#R');
114 $query_key = $this->queryKey
;
115 if ($query_key == 'advanced') {
117 $query_key = $request->getStr('query');
118 } else if ($query_key === null ||
!strlen($query_key)) {
119 $found_query_data = false;
121 if ($request->isHTTPGet() ||
$request->isQuicksand()) {
122 // If this is a GET request and it has some query data, don't
123 // do anything unless it's only before= or after=. We'll build and
124 // execute a query from it below. This allows external tools to build
125 // URIs like "/query/?users=a,b".
126 $pt_data = $request->getPassthroughRequestData();
132 'overheated' => true,
135 foreach ($pt_data as $pt_key => $pt_value) {
136 if (isset($exempt[$pt_key])) {
140 $found_query_data = true;
145 if (!$found_query_data) {
146 // Otherwise, there's no query data so just run the user's default
147 // query for this application.
148 $query_key = $engine->getDefaultQueryKey();
152 if ($engine->isBuiltinQuery($query_key)) {
153 $saved_query = $engine->buildSavedQueryFromBuiltin($query_key);
154 $named_query = idx($engine->loadEnabledNamedQueries(), $query_key);
155 } else if ($query_key) {
156 $saved_query = id(new PhabricatorSavedQueryQuery())
158 ->withQueryKeys(array($query_key))
162 return new Aphront404Response();
165 $named_query = idx($engine->loadEnabledNamedQueries(), $query_key);
167 $saved_query = $engine->buildSavedQueryFromRequest($request);
169 // Save the query to generate a query key, so "Save Custom Query..." and
170 // other features like "Bulk Edit" and "Export Data" work correctly.
171 $engine->saveQuery($saved_query);
174 $this->activeQuery
= $saved_query;
177 'query/'.$saved_query->getQueryKey(),
180 $form = id(new AphrontFormView())
182 ->setAction($request->getPath());
184 $engine->buildSearchForm($form, $saved_query);
186 $errors = $engine->getErrors();
191 $submit = id(new AphrontFormSubmitControl())
192 ->setValue(pht('Search'));
194 if ($run_query && !$named_query && $user->isLoggedIn()) {
195 $save_button = id(new PHUIButtonView())
197 ->setColor(PHUIButtonView
::GREY
)
198 ->setHref('/search/edit/key/'.$saved_query->getQueryKey().'/')
199 ->setText(pht('Save Query'))
200 ->setIcon('fa-bookmark');
201 $submit->addButton($save_button);
204 $form->appendChild($submit);
207 if ($this->getPreface()) {
208 $body[] = $this->getPreface();
212 $title = $named_query->getQueryName();
214 $title = pht('Advanced Search');
217 $header = id(new PHUIHeaderView())
219 ->setProfileHeader(true);
221 $box = id(new PHUIObjectBoxView())
223 ->addClass('application-search-results');
225 if ($run_query ||
$named_query) {
230 $this->getApplicationURI('query/advanced/?query='.$query_key),
231 (!$named_query ?
true : false));
233 $box->setForm($form);
240 $exec_errors = array();
243 id(new PhabricatorAnchorView())
244 ->setAnchorName('R'));
247 $engine->setRequest($request);
249 $query = $engine->buildQueryFromSavedQuery($saved_query);
251 $pager = $engine->newPagerForSavedQuery($saved_query);
252 $pager->readFromRequest($request);
254 $query->setReturnPartialResultsOnOverheat(true);
256 $objects = $engine->executeQuery($query, $pager);
258 $force_nux = $request->getBool('nux');
259 if (!$objects ||
$force_nux) {
260 $nux_view = $this->renderNewUserView($engine, $force_nux);
266 $pager->willShowPagingControls() &&
267 $engine->getResultBucket($saved_query);
269 $force_overheated = $request->getBool('overheated');
270 $is_overheated = $query->getIsOverheated() ||
$force_overheated;
273 $box->appendChild($nux_view);
275 $list = $engine->renderResults($objects, $saved_query);
277 if (!($list instanceof PhabricatorApplicationSearchResultView
)) {
280 'SearchEngines must render a "%s" object, but this engine '.
281 '(of class "%s") rendered something else ("%s").',
282 'PhabricatorApplicationSearchResultView',
284 phutil_describe_type($list)));
287 if ($list->getObjectList()) {
288 $box->setObjectList($list->getObjectList());
290 if ($list->getTable()) {
291 $box->setTable($list->getTable());
293 if ($list->getInfoView()) {
294 $box->setInfoView($list->getInfoView());
297 if ($is_overflowing) {
298 $box->appendChild($this->newOverflowingView());
301 if ($list->getContent()) {
302 $box->appendChild($list->getContent());
305 if ($is_overheated) {
306 $box->appendChild($this->newOverheatedView($objects));
309 $result_header = $list->getHeader();
310 if ($result_header) {
311 $box->setHeader($result_header);
312 $header = $result_header;
315 $actions = $list->getActions();
317 foreach ($actions as $action) {
318 $header->addActionLink($action);
322 $use_actions = $engine->newUseResultsActions($saved_query);
324 // TODO: Eventually, modularize all this stuff.
325 $builtin_use_actions = $this->newBuiltinUseActions();
326 if ($builtin_use_actions) {
327 foreach ($builtin_use_actions as $builtin_use_action) {
328 $use_actions[] = $builtin_use_action;
333 $use_dropdown = $this->newUseResultsDropdown(
336 $header->addActionLink($use_dropdown);
339 $more_crumbs = $list->getCrumbs();
341 if ($pager->willShowPagingControls()) {
342 $pager_box = id(new PHUIBoxView())
343 ->setColor(PHUIBoxView
::GREY
)
344 ->addClass('application-search-pager')
345 ->appendChild($pager);
346 $body[] = $pager_box;
349 } catch (PhabricatorTypeaheadInvalidTokenException
$ex) {
350 $exec_errors[] = pht(
351 'This query specifies an invalid parameter. Review the '.
352 'query parameters and correct errors.');
353 } catch (PhutilSearchQueryCompilerSyntaxException
$ex) {
354 $exec_errors[] = $ex->getMessage();
355 } catch (PhabricatorSearchConstraintException
$ex) {
356 $exec_errors[] = $ex->getMessage();
357 } catch (PhabricatorInvalidQueryCursorException
$ex) {
358 $exec_errors[] = $ex->getMessage();
361 // The engine may have encountered additional errors during rendering;
362 // merge them in and show everything.
363 foreach ($engine->getErrors() as $error) {
364 $exec_errors[] = $error;
367 $errors = $exec_errors;
371 $box->setFormErrors($errors, pht('Query Errors'));
375 ->buildApplicationCrumbs()
379 $query_uri = $engine->getQueryResultsPageURI($saved_query->getQueryKey());
380 $crumbs->addTextCrumb($title, $query_uri);
382 foreach ($more_crumbs as $crumb) {
383 $crumbs->addCrumb($crumb);
386 $crumbs->addTextCrumb($title);
389 require_celerity_resource('application-search-view-css');
391 return $this->newPage()
392 ->setTitle(pht('Query: %s', $title))
394 ->setNavigation($nav)
395 ->addClass('application-search-view')
396 ->appendChild($body);
399 private function processExportRequest() {
400 $viewer = $this->getViewer();
401 $engine = $this->getSearchEngine();
402 $request = $this->getRequest();
404 if (!$this->canExport()) {
405 return new Aphront404Response();
408 $query_key = $this->getQueryKey();
409 if ($engine->isBuiltinQuery($query_key)) {
410 $saved_query = $engine->buildSavedQueryFromBuiltin($query_key);
411 } else if ($query_key) {
412 $saved_query = id(new PhabricatorSavedQueryQuery())
414 ->withQueryKeys(array($query_key))
421 return new Aphront404Response();
424 $cancel_uri = $engine->getQueryResultsPageURI($query_key);
426 $named_query = idx($engine->loadEnabledNamedQueries(), $query_key);
429 $filename = $named_query->getQueryName();
430 $sheet_title = $named_query->getQueryName();
432 $filename = $engine->getResultTypeDescription();
433 $sheet_title = $engine->getResultTypeDescription();
435 $filename = phutil_utf8_strtolower($filename);
436 $filename = PhabricatorFile
::normalizeFileName($filename);
438 $all_formats = PhabricatorExportFormat
::getAllExportFormats();
440 $available_options = array();
441 $unavailable_options = array();
443 $unavailable_formats = array();
444 foreach ($all_formats as $key => $format) {
445 if ($format->isExportFormatEnabled()) {
446 $available_options[$key] = $format->getExportFormatName();
447 $formats[$key] = $format;
449 $unavailable_options[$key] = pht(
450 '%s (Not Available)',
451 $format->getExportFormatName());
452 $unavailable_formats[$key] = $format;
455 $format_options = $available_options +
$unavailable_options;
457 // Try to default to the format the user used last time. If you just
458 // exported to Excel, you probably want to export to Excel again.
459 $format_key = $this->readExportFormatPreference();
460 if (!isset($formats[$format_key])) {
461 $format_key = head_key($format_options);
464 // Check if this is a large result set or not. If we're exporting a
465 // large amount of data, we'll build the actual export file in the daemons.
468 $query = $engine->buildQueryFromSavedQuery($saved_query);
469 $pager = $engine->newPagerForSavedQuery($saved_query);
470 $pager->setPageSize($threshold +
1);
471 $objects = $engine->executeQuery($query, $pager);
472 $object_count = count($objects);
473 $is_large_export = ($object_count > $threshold);
478 if ($request->isFormPost()) {
479 $format_key = $request->getStr('format');
481 if (isset($unavailable_formats[$format_key])) {
482 $unavailable = $unavailable_formats[$format_key];
483 $instructions = $unavailable->getInstallInstructions();
485 $markup = id(new PHUIRemarkupView($viewer, $instructions))
487 PHUIRemarkupView
::OPTION_PRESERVE_LINEBREAKS
,
490 return $this->newDialog()
491 ->setTitle(pht('Export Format Not Available'))
492 ->appendChild($markup)
493 ->addCancelButton($cancel_uri, pht('Done'));
496 $format = idx($formats, $format_key);
499 $e_format = pht('Invalid');
500 $errors[] = pht('Choose a valid export format.');
504 $this->writeExportFormatPreference($format_key);
506 $export_engine = id(new PhabricatorExportEngine())
508 ->setSearchEngine($engine)
509 ->setSavedQuery($saved_query)
510 ->setTitle($sheet_title)
511 ->setFilename($filename)
512 ->setExportFormat($format);
514 if ($is_large_export) {
515 $job = $export_engine->newBulkJob($request);
517 return id(new AphrontRedirectResponse())
518 ->setURI($job->getMonitorURI());
520 $file = $export_engine->exportFile();
521 return $file->newDownloadResponse();
526 $export_form = id(new AphrontFormView())
529 id(new AphrontFormSelectControl())
531 ->setLabel(pht('Format'))
532 ->setError($e_format)
533 ->setValue($format_key)
534 ->setOptions($format_options));
536 if ($is_large_export) {
537 $submit_button = pht('Continue');
539 $submit_button = pht('Download Data');
542 return $this->newDialog()
543 ->setTitle(pht('Export Results'))
545 ->appendForm($export_form)
546 ->addCancelButton($cancel_uri)
547 ->addSubmitButton($submit_button);
550 private function processEditRequest() {
551 $parent = $this->getDelegatingController();
552 $request = $this->getRequest();
553 $viewer = $request->getUser();
554 $engine = $this->getSearchEngine();
556 $nav = $this->getNavigation();
558 $nav = $this->buildNavigation();
561 $named_queries = $engine->loadAllNamedQueries();
563 $can_global = $viewer->getIsAdmin();
567 'name' => pht('Personal Saved Queries'),
572 'name' => pht('Global Saved Queries'),
574 'edit' => $can_global,
578 foreach ($named_queries as $named_query) {
579 if ($named_query->isGlobal()) {
585 $groups[$group]['items'][] = $named_query;
588 $default_key = $engine->getDefaultQueryKey();
591 foreach ($groups as $group) {
592 $lists[] = $this->newQueryListView(
600 ->buildApplicationCrumbs()
601 ->addTextCrumb(pht('Saved Queries'), $engine->getQueryManagementURI())
604 $nav->selectFilter('query/edit');
606 $header = id(new PHUIHeaderView())
607 ->setHeader(pht('Saved Queries'))
608 ->setProfileHeader(true);
610 $view = id(new PHUITwoColumnView())
614 return $this->newPage()
615 ->setTitle(pht('Saved Queries'))
617 ->setNavigation($nav)
618 ->appendChild($view);
621 private function newQueryListView(
623 array $named_queries,
627 $engine = $this->getSearchEngine();
628 $viewer = $this->getViewer();
630 $list = id(new PHUIObjectItemListView())
631 ->setViewer($viewer);
634 $list_id = celerity_generate_unique_node_id();
635 $list->setID($list_id);
637 Javelin
::initBehavior(
638 'search-reorder-queries',
640 'listID' => $list_id,
641 'orderURI' => '/search/order/'.get_class($engine).'/',
645 foreach ($named_queries as $named_query) {
646 $class = get_class($engine);
647 $key = $named_query->getQueryKey();
649 $item = id(new PHUIObjectItemView())
650 ->setHeader($named_query->getQueryName())
651 ->setHref($engine->getQueryResultsPageURI($key));
653 if ($named_query->getIsDisabled()) {
655 $item->setDisabled(true);
657 // If an item is disabled and you don't have permission to edit it,
664 if ($named_query->getIsBuiltin() && $named_query->getIsDisabled()) {
666 $disable_name = pht('Enable');
669 if ($named_query->getIsBuiltin()) {
670 $disable_name = pht('Disable');
672 $disable_name = pht('Delete');
676 if ($named_query->getID()) {
677 $disable_href = '/search/delete/id/'.$named_query->getID().'/';
679 $disable_href = '/search/delete/key/'.$key.'/'.$class.'/';
683 id(new PHUIListItemView())
685 ->setHref($disable_href)
686 ->setRenderNameAsTooltip(true)
687 ->setName($disable_name)
688 ->setWorkflow(true));
691 $default_disabled = $named_query->getIsDisabled();
692 $default_icon = 'fa-thumb-tack';
694 if ($default_key === $key) {
695 $default_color = 'green';
697 $default_color = null;
701 id(new PHUIListItemView())
702 ->setIcon("{$default_icon} {$default_color}")
703 ->setHref('/search/default/'.$key.'/'.$class.'/')
704 ->setRenderNameAsTooltip(true)
705 ->setName(pht('Make Default'))
707 ->setDisabled($default_disabled));
710 if ($named_query->getIsBuiltin()) {
711 $edit_icon = 'fa-lock lightgreytext';
712 $edit_disabled = true;
713 $edit_name = pht('Builtin');
716 $edit_icon = 'fa-pencil';
717 $edit_disabled = false;
718 $edit_name = pht('Edit');
719 $edit_href = '/search/edit/id/'.$named_query->getID().'/';
723 id(new PHUIListItemView())
724 ->setIcon($edit_icon)
725 ->setHref($edit_href)
726 ->setRenderNameAsTooltip(true)
727 ->setName($edit_name)
728 ->setDisabled($edit_disabled));
731 $item->setGrippable($can_edit);
732 $item->addSigil('named-query');
735 'queryKey' => $named_query->getQueryKey(),
738 $list->addItem($item);
741 $list->setNoDataString(pht('No saved queries.'));
743 return id(new PHUIObjectBoxView())
744 ->setHeaderText($list_name)
745 ->setBackground(PHUIObjectBoxView
::BLUE_PROPERTY
)
746 ->setObjectList($list);
749 public function buildApplicationMenu() {
750 $menu = $this->getDelegatingController()
751 ->buildApplicationMenu();
753 if ($menu instanceof PHUIApplicationMenuView
) {
754 $menu->setSearchEngine($this->getSearchEngine());
760 private function buildNavigation() {
761 $viewer = $this->getViewer();
762 $engine = $this->getSearchEngine();
764 $nav = id(new AphrontSideNavFilterView())
766 ->setBaseURI(new PhutilURI($this->getApplicationURI()));
768 $engine->addNavigationItems($nav->getMenu());
773 private function renderNewUserView(
774 PhabricatorApplicationSearchEngine
$engine,
777 // Don't render NUX if the user has clicked away from the default page.
778 if ($this->getQueryKey() !== null && strlen($this->getQueryKey())) {
782 // Don't put NUX in panels because it would be weird.
783 if ($engine->isPanelContext()) {
787 // Try to render the view itself first, since this should be very cheap
788 // (just returning some text).
789 $nux_view = $engine->renderNewUserView();
795 $query = $engine->newQuery();
800 // Try to load any object at all. If we can, the application has seen some
801 // use so we just render the normal view.
804 ->setViewer(PhabricatorUser
::getOmnipotentUser())
806 ->setReturnPartialResultsOnOverheat(true)
816 private function newUseResultsDropdown(
817 PhabricatorSavedQuery
$query,
818 array $dropdown_items) {
820 $viewer = $this->getViewer();
822 $action_list = id(new PhabricatorActionListView())
823 ->setViewer($viewer);
824 foreach ($dropdown_items as $dropdown_item) {
825 $action_list->addAction($dropdown_item);
828 return id(new PHUIButtonView())
831 ->setText(pht('Use Results'))
833 ->setDropdownMenu($action_list)
834 ->addClass('dropdown');
837 private function newOverflowingView() {
839 'The query matched more than one page of results. Results are '.
840 'paginated before bucketing, so later pages may contain additional '.
841 'results in any bucket.');
843 return id(new PHUIInfoView())
844 ->setSeverity(PHUIInfoView
::SEVERITY_WARNING
)
846 ->setTitle(pht('Buckets Overflowing'))
853 public static function newOverheatedError($has_results) {
854 $overheated_link = phutil_tag(
857 'href' => 'https://phurl.io/u/overheated',
858 'target' => '_blank',
864 'This query took too long, so only some results are shown. %s',
868 'This query took too long. %s',
875 private function newOverheatedView(array $results) {
876 $message = self
::newOverheatedError((bool)$results);
878 return id(new PHUIInfoView())
879 ->setSeverity(PHUIInfoView
::SEVERITY_WARNING
)
881 ->setTitle(pht('Query Overheated'))
888 private function newBuiltinUseActions() {
890 $request = $this->getRequest();
891 $viewer = $request->getUser();
893 $is_dev = PhabricatorEnv
::getEnvConfig('phabricator.developer-mode');
895 $engine = $this->getSearchEngine();
896 $engine_class = get_class($engine);
898 $query_key = $this->getActiveQuery()->getQueryKey();
900 $can_use = $engine->canUseInPanelContext();
901 $is_installed = PhabricatorApplication
::isClassInstalledForViewer(
902 'PhabricatorDashboardApplication',
905 if ($can_use && $is_installed) {
906 $actions[] = id(new PhabricatorActionView())
907 ->setIcon('fa-dashboard')
908 ->setName(pht('Add to Dashboard'))
910 ->setHref("/dashboard/panel/install/{$engine_class}/{$query_key}/");
913 if ($this->canExport()) {
914 $export_uri = $engine->getExportURI($query_key);
915 $actions[] = id(new PhabricatorActionView())
916 ->setIcon('fa-download')
917 ->setName(pht('Export Data'))
919 ->setHref($export_uri);
923 $engine = $this->getSearchEngine();
924 $nux_uri = $engine->getQueryBaseURI();
925 $nux_uri = id(new PhutilURI($nux_uri))
926 ->replaceQueryParam('nux', true);
928 $actions[] = id(new PhabricatorActionView())
929 ->setIcon('fa-user-plus')
930 ->setName(pht('DEV: New User State'))
935 $overheated_uri = $this->getRequest()->getRequestURI()
936 ->replaceQueryParam('overheated', true);
938 $actions[] = id(new PhabricatorActionView())
940 ->setName(pht('DEV: Overheated State'))
941 ->setHref($overheated_uri);
947 private function canExport() {
948 $engine = $this->getSearchEngine();
949 if (!$engine->canExport()) {
953 // Don't allow logged-out users to perform exports. There's no technical
954 // or policy reason they can't, but we don't normally give them access
955 // to write files or jobs. For now, just err on the side of caution.
957 $viewer = $this->getViewer();
958 if (!$viewer->getPHID()) {
965 private function readExportFormatPreference() {
966 $viewer = $this->getViewer();
967 $export_key = PhabricatorExportFormatSetting
::SETTINGKEY
;
968 $value = $viewer->getUserSetting($export_key);
970 if (is_string($value)) {
977 private function writeExportFormatPreference($value) {
978 $viewer = $this->getViewer();
979 $request = $this->getRequest();
981 if (!$viewer->isLoggedIn()) {
985 $export_key = PhabricatorExportFormatSetting
::SETTINGKEY
;
986 $preferences = PhabricatorUserPreferences
::loadUserPreferences($viewer);
988 $editor = id(new PhabricatorUserPreferencesEditor())
990 ->setContentSourceFromRequest($request)
991 ->setContinueOnNoEffect(true)
992 ->setContinueOnMissingFields(true);
995 $xactions[] = $preferences->newTransaction($export_key, $value);
996 $editor->applyTransactions($preferences, $xactions);
999 private function processCustomizeRequest() {
1000 $viewer = $this->getViewer();
1001 $engine = $this->getSearchEngine();
1002 $request = $this->getRequest();
1004 $object_phid = $request->getStr('search.objectPHID');
1005 $context_phid = $request->getStr('search.contextPHID');
1007 // For now, the object can only be a dashboard panel, so just use a panel
1008 // query explicitly.
1009 $object = id(new PhabricatorDashboardPanelQuery())
1010 ->setViewer($viewer)
1011 ->withPHIDs(array($object_phid))
1012 ->requireCapabilities(
1014 PhabricatorPolicyCapability
::CAN_VIEW
,
1015 PhabricatorPolicyCapability
::CAN_EDIT
,
1019 return new Aphront404Response();
1022 $object_name = pht('%s %s', $object->getMonogram(), $object->getName());
1024 // Likewise, the context object can only be a dashboard.
1025 if ($context_phid !== null && !strlen($context_phid)) {
1026 $context = id(new PhabricatorDashboardQuery())
1027 ->setViewer($viewer)
1028 ->withPHIDs(array($context_phid))
1031 return new Aphront404Response();
1037 $done_uri = $context->getURI();
1039 if ($request->isFormPost()) {
1040 $saved_query = $engine->buildSavedQueryFromRequest($request);
1041 $engine->saveQuery($saved_query);
1042 $query_key = $saved_query->getQueryKey();
1044 $query_key = $this->getQueryKey();
1045 if ($engine->isBuiltinQuery($query_key)) {
1046 $saved_query = $engine->buildSavedQueryFromBuiltin($query_key);
1047 } else if ($query_key) {
1048 $saved_query = id(new PhabricatorSavedQueryQuery())
1049 ->setViewer($viewer)
1050 ->withQueryKeys(array($query_key))
1053 $saved_query = null;
1057 if (!$saved_query) {
1058 return new Aphront404Response();
1061 $form = id(new AphrontFormView())
1062 ->setViewer($viewer)
1063 ->addHiddenInput('search.objectPHID', $object_phid)
1064 ->addHiddenInput('search.contextPHID', $context_phid)
1065 ->setAction($request->getPath());
1067 $engine->buildSearchForm($form, $saved_query);
1069 $errors = $engine->getErrors();
1070 if ($request->isFormPost()) {
1072 $xactions = array();
1074 // Since this workflow is currently used only by dashboard panels,
1075 // we can hard-code how the edit works.
1076 $xactions[] = $object->getApplicationTransactionTemplate()
1077 ->setTransactionType(
1078 PhabricatorDashboardQueryPanelQueryTransaction
::TRANSACTIONTYPE
)
1079 ->setNewValue($query_key);
1081 $editor = $object->getApplicationTransactionEditor()
1083 ->setContentSourceFromRequest($request)
1084 ->setContinueOnNoEffect(true)
1085 ->setContinueOnMissingFields(true);
1087 $editor->applyTransactions($object, $xactions);
1089 return id(new AphrontRedirectResponse())->setURI($done_uri);
1093 return $this->newDialog()
1094 ->setTitle(pht('Customize Query: %s', $object_name))
1095 ->setErrors($errors)
1096 ->setWidth(AphrontDialogView
::WIDTH_FULL
)
1098 ->addCancelButton($done_uri)
1099 ->addSubmitButton(pht('Save Changes'));