From fa6a5a46ba579b5cf0aaf6c556f516790d8597e9 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 5 Oct 2016 16:22:54 -0700 Subject: [PATCH] Make more of the Calendar export workflow work Summary: Ref T10747. - Adds a "Use Results..." dropdown to query result pages, with actions you can take with search results (today: create export; in future: bulk edit, export as excel, make dashboard panel, etc). - Allows you to create an export against a query key. - I'm just using a text edit field for this for now. - Fleshes out export modes. I plan to support: public (as though you were logged out), privileged (as though you were logged in) and availability (event times, but not details). This does not actually export stuff yet. Test Plan: Created some exports. Viewed and listed exports. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10747 Differential Revision: https://secure.phabricator.com/D16676 --- src/__phutil_library_map__.php | 4 + ...habricatorCalendarExportViewController.php | 154 ++++++++++++++++++ .../PhabricatorCalendarExportEditEngine.php | 39 ++++- .../PhabricatorCalendarExportEditor.php | 4 + .../PhabricatorCalendarEventSearchEngine.php | 26 ++- .../PhabricatorCalendarExportSearchEngine.php | 10 ++ ...bricatorCalendarExportTransactionQuery.php | 10 ++ .../storage/PhabricatorCalendarExport.php | 66 +++++++- ...catorCalendarExportQueryKeyTransaction.php | 10 ++ ...PhabricatorApplicationSearchController.php | 40 ++++- .../PhabricatorApplicationSearchEngine.php | 4 + 11 files changed, 352 insertions(+), 15 deletions(-) create mode 100644 src/applications/calendar/controller/PhabricatorCalendarExportViewController.php create mode 100644 src/applications/calendar/query/PhabricatorCalendarExportTransactionQuery.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 73787b0dee..2dab43aa5d 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2088,7 +2088,9 @@ phutil_register_library_map(array( 'PhabricatorCalendarExportQueryKeyTransaction' => 'applications/calendar/xaction/PhabricatorCalendarExportQueryKeyTransaction.php', 'PhabricatorCalendarExportSearchEngine' => 'applications/calendar/query/PhabricatorCalendarExportSearchEngine.php', 'PhabricatorCalendarExportTransaction' => 'applications/calendar/storage/PhabricatorCalendarExportTransaction.php', + 'PhabricatorCalendarExportTransactionQuery' => 'applications/calendar/query/PhabricatorCalendarExportTransactionQuery.php', 'PhabricatorCalendarExportTransactionType' => 'applications/calendar/xaction/PhabricatorCalendarExportTransactionType.php', + 'PhabricatorCalendarExportViewController' => 'applications/calendar/controller/PhabricatorCalendarExportViewController.php', 'PhabricatorCalendarHoliday' => 'applications/calendar/storage/PhabricatorCalendarHoliday.php', 'PhabricatorCalendarHolidayTestCase' => 'applications/calendar/storage/__tests__/PhabricatorCalendarHolidayTestCase.php', 'PhabricatorCalendarIconSet' => 'applications/calendar/icon/PhabricatorCalendarIconSet.php', @@ -6857,7 +6859,9 @@ phutil_register_library_map(array( 'PhabricatorCalendarExportQueryKeyTransaction' => 'PhabricatorCalendarExportTransactionType', 'PhabricatorCalendarExportSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorCalendarExportTransaction' => 'PhabricatorModularTransaction', + 'PhabricatorCalendarExportTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorCalendarExportTransactionType' => 'PhabricatorModularTransactionType', + 'PhabricatorCalendarExportViewController' => 'PhabricatorCalendarController', 'PhabricatorCalendarHoliday' => 'PhabricatorCalendarDAO', 'PhabricatorCalendarHolidayTestCase' => 'PhabricatorTestCase', 'PhabricatorCalendarIconSet' => 'PhabricatorIconSet', diff --git a/src/applications/calendar/controller/PhabricatorCalendarExportViewController.php b/src/applications/calendar/controller/PhabricatorCalendarExportViewController.php new file mode 100644 index 0000000000..3763fe30ae --- /dev/null +++ b/src/applications/calendar/controller/PhabricatorCalendarExportViewController.php @@ -0,0 +1,154 @@ +getViewer(); + + $export = id(new PhabricatorCalendarExportQuery()) + ->setViewer($viewer) + ->withIDs(array($request->getURIData('id'))) + ->executeOne(); + if (!$export) { + return new Aphront404Response(); + } + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb( + pht('Exports'), + '/calendar/export/'); + $crumbs->addTextCrumb(pht('Export %d', $export->getID())); + $crumbs->setBorder(true); + + $timeline = $this->buildTransactionTimeline( + $export, + new PhabricatorCalendarExportTransactionQuery()); + $timeline->setShouldTerminate(true); + + $header = $this->buildHeaderView($export); + $curtain = $this->buildCurtain($export); + $details = $this->buildPropertySection($export); + + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setMainColumn( + array( + $timeline, + )) + ->setCurtain($curtain) + ->addPropertySection(pht('Details'), $details); + + $page_title = pht('Export %d %s', $export->getID(), $export->getName()); + + return $this->newPage() + ->setTitle($page_title) + ->setCrumbs($crumbs) + ->setPageObjectPHIDs(array($export->getPHID())) + ->appendChild($view); + } + + private function buildHeaderView( + PhabricatorCalendarExport $export) { + $viewer = $this->getViewer(); + $id = $export->getID(); + + if ($export->getIsDisabled()) { + $icon = 'fa-ban'; + $color = 'grey'; + $status = pht('Disabled'); + } else { + $icon = 'fa-check'; + $color = 'bluegrey'; + $status = pht('Active'); + } + + $header = id(new PHUIHeaderView()) + ->setUser($viewer) + ->setHeader($export->getName()) + ->setStatus($icon, $color, $status) + ->setPolicyObject($export); + + return $header; + } + + private function buildCurtain(PhabricatorCalendarExport $export) { + $viewer = $this->getRequest()->getUser(); + $id = $export->getID(); + + $curtain = $this->newCurtainView($export); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $export, + PhabricatorPolicyCapability::CAN_EDIT); + + $ics_uri = $export->getICSURI(); + + $edit_uri = "export/edit/{$id}/"; + $edit_uri = $this->getApplicationURI($edit_uri); + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Edit Export')) + ->setIcon('fa-pencil') + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit) + ->setHref($edit_uri)); + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Export as .ics')) + ->setIcon('fa-download') + ->setHref($ics_uri)); + + return $curtain; + } + + private function buildPropertySection( + PhabricatorCalendarExport $export) { + $viewer = $this->getViewer(); + + $properties = id(new PHUIPropertyListView()) + ->setUser($viewer); + + $mode = $export->getPolicyMode(); + + $policy_icon = PhabricatorCalendarExport::getPolicyModeIcon($mode); + $policy_name = PhabricatorCalendarExport::getPolicyModeName($mode); + $policy_desc = PhabricatorCalendarExport::getPolicyModeDescription($mode); + $policy_color = PhabricatorCalendarExport::getPolicyModeColor($mode); + + $policy_view = id(new PHUIStatusListView()) + ->addItem( + id(new PHUIStatusItemView()) + ->setIcon($policy_icon, $policy_color) + ->setTarget($policy_name) + ->setNote($policy_desc)); + + $properties->addProperty(pht('Mode'), $policy_view); + + $query_key = $export->getQueryKey(); + $query_link = phutil_tag( + 'a', + array( + 'href' => $this->getApplicationURI("/query/{$query_key}/"), + ), + $query_key); + $properties->addProperty(pht('Query'), $query_link); + + $ics_uri = $export->getICSURI(); + $ics_uri = PhabricatorEnv::getURI($ics_uri); + + $properties->addProperty( + pht('ICS URI'), + phutil_tag( + 'a', + array( + 'href' => $ics_uri, + ), + $ics_uri)); + + return $properties; + } +} diff --git a/src/applications/calendar/editor/PhabricatorCalendarExportEditEngine.php b/src/applications/calendar/editor/PhabricatorCalendarExportEditEngine.php index f637200597..6ebbc9e1e0 100644 --- a/src/applications/calendar/editor/PhabricatorCalendarExportEditEngine.php +++ b/src/applications/calendar/editor/PhabricatorCalendarExportEditEngine.php @@ -43,7 +43,7 @@ final class PhabricatorCalendarExportEditEngine } protected function getObjectEditShortText($object) { - return $object->getMonogram(); + return pht('Export %d', $object->getID()); } protected function getObjectCreateShortText() { @@ -65,6 +65,22 @@ final class PhabricatorCalendarExportEditEngine protected function buildCustomEditFields($object) { $viewer = $this->getViewer(); + $export_modes = PhabricatorCalendarExport::getAvailablePolicyModes(); + $export_modes = array_fuse($export_modes); + + $current_mode = $object->getPolicyMode(); + if (empty($export_modes[$current_mode])) { + array_shift($export_modes, $current_mode); + } + + $mode_options = array(); + foreach ($export_modes as $export_mode) { + $mode_name = PhabricatorCalendarExport::getPolicyModeName($export_mode); + $mode_summary = PhabricatorCalendarExport::getPolicyModeSummary( + $export_mode); + $mode_options[$export_mode] = pht('%s: %s', $mode_name, $mode_summary); + } + $fields = array( id(new PhabricatorTextEditField()) ->setKey('name') @@ -87,6 +103,27 @@ final class PhabricatorCalendarExportEditEngine ->setConduitDescription(pht('Disable or restore the export.')) ->setConduitTypeDescription(pht('True to cancel the export.')) ->setValue($object->getIsDisabled()), + id(new PhabricatorTextEditField()) + ->setKey('queryKey') + ->setLabel(pht('Query Key')) + ->setDescription(pht('Query to execute.')) + ->setIsRequired(true) + ->setTransactionType( + PhabricatorCalendarExportQueryKeyTransaction::TRANSACTIONTYPE) + ->setConduitDescription(pht('Change the export query key.')) + ->setConduitTypeDescription(pht('New export query key.')) + ->setValue($object->getQueryKey()), + id(new PhabricatorSelectEditField()) + ->setKey('mode') + ->setLabel(pht('Mode')) + ->setTransactionType( + PhabricatorCalendarExportModeTransaction::TRANSACTIONTYPE) + ->setOptions($mode_options) + ->setDescription(pht('Change the policy mode for the export.')) + ->setConduitDescription(pht('Adjust export mode.')) + ->setConduitTypeDescription(pht('New export mode.')) + ->setValue($current_mode), + ); return $fields; diff --git a/src/applications/calendar/editor/PhabricatorCalendarExportEditor.php b/src/applications/calendar/editor/PhabricatorCalendarExportEditor.php index efdcb0e522..6ddd172d58 100644 --- a/src/applications/calendar/editor/PhabricatorCalendarExportEditor.php +++ b/src/applications/calendar/editor/PhabricatorCalendarExportEditor.php @@ -11,4 +11,8 @@ final class PhabricatorCalendarExportEditor return pht('Calendar Exports'); } + public function getCreateObjectTitle($author, $object) { + return pht('%s created this export.', $author); + } + } diff --git a/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php b/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php index 5058457cde..64c0c39aec 100644 --- a/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php +++ b/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php @@ -255,11 +255,20 @@ final class PhabricatorCalendarEventSearchEngine array $handles) { if ($this->isMonthView($query)) { - return $this->buildCalendarMonthView($events, $query); + $result = $this->buildCalendarMonthView($events, $query); } else if ($this->isDayView($query)) { - return $this->buildCalendarDayView($events, $query); + $result = $this->buildCalendarDayView($events, $query); + } else { + $result = $this->buildCalendarListView($events, $query); } + return $result; + } + + private function buildCalendarListView( + array $events, + PhabricatorSavedQuery $query) { + assert_instances_of($events, 'PhabricatorCalendarEvent'); $viewer = $this->requireViewer(); $list = new PHUIObjectItemListView(); @@ -562,4 +571,17 @@ final class PhabricatorCalendarEventSearchEngine return false; } + public function newUseResultsActions(PhabricatorSavedQuery $saved) { + $viewer = $this->requireViewer(); + $can_export = $viewer->isLoggedIn(); + + return array( + id(new PhabricatorActionView()) + ->setIcon('fa-download') + ->setName(pht('Export Query as .ics')) + ->setDisabled(!$can_export) + ->setHref('/calendar/export/edit/?queryKey='.$saved->getQueryKey()), + ); + } + } diff --git a/src/applications/calendar/query/PhabricatorCalendarExportSearchEngine.php b/src/applications/calendar/query/PhabricatorCalendarExportSearchEngine.php index 462bdaeea1..4a65bfd099 100644 --- a/src/applications/calendar/query/PhabricatorCalendarExportSearchEngine.php +++ b/src/applications/calendar/query/PhabricatorCalendarExportSearchEngine.php @@ -64,6 +64,7 @@ final class PhabricatorCalendarExportSearchEngine foreach ($exports as $export) { $item = id(new PHUIObjectItemView()) ->setViewer($viewer) + ->setObjectName(pht('Export %d', $export->getID())) ->setHeader($export->getName()) ->setHref($export->getURI()); @@ -71,6 +72,15 @@ final class PhabricatorCalendarExportSearchEngine $item->setDisabled(true); } + $mode = $export->getPolicyMode(); + $policy_icon = PhabricatorCalendarExport::getPolicyModeIcon($mode); + $policy_name = PhabricatorCalendarExport::getPolicyModeName($mode); + $policy_color = PhabricatorCalendarExport::getPolicyModeColor($mode); + + $item->addIcon( + "{$policy_icon} {$policy_color}", + $policy_name); + $list->addItem($item); } diff --git a/src/applications/calendar/query/PhabricatorCalendarExportTransactionQuery.php b/src/applications/calendar/query/PhabricatorCalendarExportTransactionQuery.php new file mode 100644 index 0000000000..32b9d71b65 --- /dev/null +++ b/src/applications/calendar/query/PhabricatorCalendarExportTransactionQuery.php @@ -0,0 +1,10 @@ +setAuthorPHID($actor->getPHID()) - ->setPolicyMode(self::MODE_PRIVATE) + ->setPolicyMode(self::MODE_PRIVILEGED) ->setIsDisabled(0); } @@ -65,10 +65,23 @@ final class PhabricatorCalendarExport extends PhabricatorCalendarDAO private static function getPolicyModeMap() { return array( self::MODE_PUBLIC => array( + 'icon' => 'fa-globe', 'name' => pht('Public'), + 'color' => 'bluegrey', + 'summary' => pht( + 'Export only public data.'), + 'description' => pht( + 'Only publicly available data is exported.'), ), - self::MODE_PRIVATE => array( - 'name' => pht('Private'), + self::MODE_PRIVILEGED => array( + 'icon' => 'fa-unlock-alt', + 'name' => pht('Privileged'), + 'color' => 'red', + 'summary' => pht( + 'Export private data.'), + 'description' => pht( + 'Anyone who knows the URI for this export can view all event '. + 'details as though they were logged in with your account.'), ), ); } @@ -78,14 +91,55 @@ final class PhabricatorCalendarExport extends PhabricatorCalendarDAO } public static function getPolicyModeName($const) { - $map = self::getPolicyModeSpec($const); - return idx($map, 'name', $const); + $spec = self::getPolicyModeSpec($const); + return idx($spec, 'name', $const); + } + + public static function getPolicyModeIcon($const) { + $spec = self::getPolicyModeSpec($const); + return idx($spec, 'icon', $const); + } + + public static function getPolicyModeColor($const) { + $spec = self::getPolicyModeSpec($const); + return idx($spec, 'color', $const); + } + + public static function getPolicyModeSummary($const) { + $spec = self::getPolicyModeSpec($const); + return idx($spec, 'summary', $const); + } + + public static function getPolicyModeDescription($const) { + $spec = self::getPolicyModeSpec($const); + return idx($spec, 'description', $const); } public static function getPolicyModes() { return array_keys(self::getPolicyModeMap()); } + public static function getAvailablePolicyModes() { + $modes = array(); + + if (PhabricatorEnv::getEnvConfig('policy.allow-public')) { + $modes[] = self::MODE_PUBLIC; + } + + $modes[] = self::MODE_PRIVILEGED; + + return $modes; + } + + public function getICSFilename() { + return PhabricatorSlug::normalizeProjectSlug($this->getName()).'.ics'; + } + + public function getICSURI() { + $secret_key = $this->getSecretKey(); + $ics_name = $this->getICSFilename(); + return "/calendar/export/ics/{$secret_key}/{$ics_name}"; + } /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/applications/calendar/xaction/PhabricatorCalendarExportQueryKeyTransaction.php b/src/applications/calendar/xaction/PhabricatorCalendarExportQueryKeyTransaction.php index 25b19591e6..bcdecf09a3 100644 --- a/src/applications/calendar/xaction/PhabricatorCalendarExportQueryKeyTransaction.php +++ b/src/applications/calendar/xaction/PhabricatorCalendarExportQueryKeyTransaction.php @@ -20,12 +20,15 @@ final class PhabricatorCalendarExportQueryKeyTransaction } public function validateTransactions($object, array $xactions) { + $actor = $this->getActor(); + $errors = array(); foreach ($xactions as $xaction) { $value = $xaction->getNewValue(); $query = id(new PhabricatorSavedQueryQuery()) + ->setViewer($actor) ->withEngineClassNames(array('PhabricatorCalendarEventSearchEngine')) ->withQueryKeys(array($value)) ->executeOne(); @@ -33,6 +36,13 @@ final class PhabricatorCalendarExportQueryKeyTransaction continue; } + $builtin = id(new PhabricatorCalendarEventSearchEngine()) + ->setViewer($actor) + ->getBuiltinQueries($actor); + if (isset($builtin[$value])) { + continue; + } + $errors[] = $this->newInvalidError( pht( 'Query key "%s" does not identify a valid event query.', diff --git a/src/applications/search/controller/PhabricatorApplicationSearchController.php b/src/applications/search/controller/PhabricatorApplicationSearchController.php index d36d279c55..6350df5e02 100644 --- a/src/applications/search/controller/PhabricatorApplicationSearchController.php +++ b/src/applications/search/controller/PhabricatorApplicationSearchController.php @@ -252,12 +252,6 @@ final class PhabricatorApplicationSearchController get_class($engine))); } - if ($list->getActions()) { - foreach ($list->getActions() as $action) { - $header->addActionLink($action); - } - } - if ($list->getObjectList()) { $box->setObjectList($list->getObjectList()); } @@ -274,6 +268,21 @@ final class PhabricatorApplicationSearchController $result_header = $list->getHeader(); if ($result_header) { $box->setHeader($result_header); + $header = $result_header; + } + + if ($list->getActions()) { + foreach ($list->getActions() as $action) { + $header->addActionLink($action); + } + } + + $use_actions = $engine->newUseResultsActions($saved_query); + if ($use_actions) { + $use_dropdown = $this->newUseResultsDropdown( + $saved_query, + $use_actions); + $header->addActionLink($use_dropdown); } $more_crumbs = $list->getCrumbs(); @@ -496,5 +505,24 @@ final class PhabricatorApplicationSearchController return $nux_view; } + private function newUseResultsDropdown( + PhabricatorSavedQuery $query, + array $dropdown_items) { + + $viewer = $this->getViewer(); + + $action_list = id(new PhabricatorActionListView()) + ->setViewer($viewer); + foreach ($dropdown_items as $dropdown_item) { + $action_list->addAction($dropdown_item); + } + + return id(new PHUIButtonView()) + ->setTag('a') + ->setHref('#') + ->setText(pht('Use Results...')) + ->setIcon('fa-road') + ->setDropdownMenu($action_list); + } } diff --git a/src/applications/search/engine/PhabricatorApplicationSearchEngine.php b/src/applications/search/engine/PhabricatorApplicationSearchEngine.php index 13606c542f..a1279ad8ae 100644 --- a/src/applications/search/engine/PhabricatorApplicationSearchEngine.php +++ b/src/applications/search/engine/PhabricatorApplicationSearchEngine.php @@ -1390,4 +1390,8 @@ abstract class PhabricatorApplicationSearchEngine extends Phobject { return null; } + public function newUseResultsActions(PhabricatorSavedQuery $saved) { + return array(); + } + }