diff --git a/resources/sql/patches/120.noop.sql b/resources/sql/patches/120.noop.sql new file mode 100644 index 0000000000..b730eaa0d3 --- /dev/null +++ b/resources/sql/patches/120.noop.sql @@ -0,0 +1,2 @@ +/* Do nothing, patch 121 got committed before there was a patch 120. */ +SELECT 1; \ No newline at end of file diff --git a/resources/sql/patches/122.flag.sql b/resources/sql/patches/122.flag.sql new file mode 100644 index 0000000000..fa242caeed --- /dev/null +++ b/resources/sql/patches/122.flag.sql @@ -0,0 +1,16 @@ +CREATE DATABASE phabricator_flag COLLATE utf8_general_ci; + +CREATE TABLE phabricator_flag.flag ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + ownerPHID varchar(64) COLLATE utf8_bin NOT NULL, + type varchar(4) COLLATE utf8_bin NOT NULL, + objectPHID varchar(64) COLLATE utf8_bin NOT NULL, + reasonPHID varchar(64) COLLATE utf8_bin NOT NULL, + color INT UNSIGNED NOT NULL, + note varchar(255) COLLATE utf8_general_ci, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + + UNIQUE KEY (ownerPHID, type, objectPHID), + KEY (objectPHID) +) ENGINE=InnoDB; \ No newline at end of file diff --git a/src/__celerity_resource_map__.php b/src/__celerity_resource_map__.php index 780a0df33b..0b0c457f18 100644 --- a/src/__celerity_resource_map__.php +++ b/src/__celerity_resource_map__.php @@ -172,7 +172,7 @@ celerity_register_resource_map(array( ), 'differential-changeset-view-css' => array( - 'uri' => '/res/13983f98/rsrc/css/application/differential/changeset-view.css', + 'uri' => '/res/238f435d/rsrc/css/application/differential/changeset-view.css', 'type' => 'css', 'requires' => array( @@ -1595,6 +1595,15 @@ celerity_register_resource_map(array( ), 'disk' => '/rsrc/css/application/feed/feed.css', ), + 'phabricator-flag-css' => + array( + 'uri' => '/res/81d9e551/rsrc/css/application/flag/flag.css', + 'type' => 'css', + 'requires' => + array( + ), + 'disk' => '/rsrc/css/application/flag/flag.css', + ), 'phabricator-jump-nav' => array( 'uri' => '/res/8bdc0fc3/rsrc/css/application/directory/phabricator-jump-nav.css', @@ -2030,7 +2039,7 @@ celerity_register_resource_map(array( 'uri' => '/res/pkg/21d01ed8/core.pkg.js', 'type' => 'js', ), - '8a5929ca' => + '90c90e95' => array( 'name' => 'differential.pkg.css', 'symbols' => @@ -2048,7 +2057,7 @@ celerity_register_resource_map(array( 10 => 'phabricator-content-source-view-css', 11 => 'differential-local-commits-view-css', ), - 'uri' => '/res/pkg/8a5929ca/differential.pkg.css', + 'uri' => '/res/pkg/90c90e95/differential.pkg.css', 'type' => 'css', ), '9b256876' => @@ -2155,7 +2164,7 @@ celerity_register_resource_map(array( 'aphront-crumbs-view-css' => '82263727', 'aphront-dialog-view-css' => '82263727', 'aphront-form-view-css' => '82263727', - 'aphront-headsup-action-list-view-css' => '8a5929ca', + 'aphront-headsup-action-list-view-css' => '90c90e95', 'aphront-list-filter-view-css' => '82263727', 'aphront-pager-view-css' => '82263727', 'aphront-panel-view-css' => '82263727', @@ -2163,16 +2172,16 @@ celerity_register_resource_map(array( 'aphront-table-view-css' => '82263727', 'aphront-tokenizer-control-css' => '82263727', 'aphront-typeahead-control-css' => '82263727', - 'differential-changeset-view-css' => '8a5929ca', - 'differential-core-view-css' => '8a5929ca', + 'differential-changeset-view-css' => '90c90e95', + 'differential-core-view-css' => '90c90e95', 'differential-inline-comment-editor' => '9b256876', - 'differential-local-commits-view-css' => '8a5929ca', - 'differential-revision-add-comment-css' => '8a5929ca', - 'differential-revision-comment-css' => '8a5929ca', - 'differential-revision-comment-list-css' => '8a5929ca', - 'differential-revision-detail-css' => '8a5929ca', - 'differential-revision-history-css' => '8a5929ca', - 'differential-table-of-contents-css' => '8a5929ca', + 'differential-local-commits-view-css' => '90c90e95', + 'differential-revision-add-comment-css' => '90c90e95', + 'differential-revision-comment-css' => '90c90e95', + 'differential-revision-comment-list-css' => '90c90e95', + 'differential-revision-detail-css' => '90c90e95', + 'differential-revision-history-css' => '90c90e95', + 'differential-table-of-contents-css' => '90c90e95', 'diffusion-commit-view-css' => '61f9d480', 'javelin-behavior' => '4fbae2af', 'javelin-behavior-aphront-basic-tokenizer' => '2af849fb', @@ -2221,7 +2230,7 @@ celerity_register_resource_map(array( 'maniphest-task-summary-css' => '31583232', 'maniphest-transaction-detail-css' => '31583232', 'phabricator-app-buttons-css' => '82263727', - 'phabricator-content-source-view-css' => '8a5929ca', + 'phabricator-content-source-view-css' => '90c90e95', 'phabricator-core-buttons-css' => '82263727', 'phabricator-core-css' => '82263727', 'phabricator-directory-css' => '82263727', @@ -2231,7 +2240,7 @@ celerity_register_resource_map(array( 'phabricator-keyboard-shortcut' => '21d01ed8', 'phabricator-keyboard-shortcut-manager' => '21d01ed8', 'phabricator-menu-item' => '21d01ed8', - 'phabricator-object-selector-css' => '8a5929ca', + 'phabricator-object-selector-css' => '90c90e95', 'phabricator-paste-file-upload' => '21d01ed8', 'phabricator-remarkup-css' => '82263727', 'phabricator-shaped-request' => '9b256876', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 0341982497..2e6c12832f 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -142,6 +142,8 @@ phutil_register_library_map(array( 'ConduitAPI_file_download_Method' => 'applications/conduit/method/file/download', 'ConduitAPI_file_info_Method' => 'applications/conduit/method/file/info', 'ConduitAPI_file_upload_Method' => 'applications/conduit/method/file/upload', + 'ConduitAPI_flag_Method' => 'applications/conduit/method/flag/base', + 'ConduitAPI_flag_query_Method' => 'applications/conduit/method/flag/query', 'ConduitAPI_macro_Method' => 'applications/conduit/method/macro/base', 'ConduitAPI_macro_query_Method' => 'applications/conduit/method/macro/query', 'ConduitAPI_maniphest_Method' => 'applications/conduit/method/maniphest/base', @@ -594,6 +596,16 @@ phutil_register_library_map(array( 'PhabricatorFileUploadController' => 'applications/files/controller/upload', 'PhabricatorFileUploadException' => 'applications/files/exception/upload', 'PhabricatorFileUploadView' => 'applications/files/view/upload', + 'PhabricatorFlag' => 'applications/flag/storage/flag', + 'PhabricatorFlagColor' => 'applications/flag/constants/color', + 'PhabricatorFlagConstants' => 'applications/flag/constants/base', + 'PhabricatorFlagController' => 'applications/flag/controller/base', + 'PhabricatorFlagDAO' => 'applications/flag/storage/base', + 'PhabricatorFlagDeleteController' => 'applications/flag/controller/delete', + 'PhabricatorFlagEditController' => 'applications/flag/controller/edit', + 'PhabricatorFlagListController' => 'applications/flag/controller/list', + 'PhabricatorFlagListView' => 'applications/flag/view/list', + 'PhabricatorFlagQuery' => 'applications/flag/query/flag', 'PhabricatorGarbageCollectorDaemon' => 'infrastructure/daemon/garbagecollector', 'PhabricatorGoodForNothingWorker' => 'infrastructure/daemon/workers/worker/goodfornothing', 'PhabricatorHandleObjectSelectorDataView' => 'applications/phid/handle/view/selector', @@ -1052,6 +1064,8 @@ phutil_register_library_map(array( 'ConduitAPI_file_download_Method' => 'ConduitAPIMethod', 'ConduitAPI_file_info_Method' => 'ConduitAPIMethod', 'ConduitAPI_file_upload_Method' => 'ConduitAPIMethod', + 'ConduitAPI_flag_Method' => 'ConduitAPIMethod', + 'ConduitAPI_flag_query_Method' => 'ConduitAPI_flag_Method', 'ConduitAPI_macro_Method' => 'ConduitAPIMethod', 'ConduitAPI_macro_query_Method' => 'ConduitAPI_macro_Method', 'ConduitAPI_maniphest_Method' => 'ConduitAPIMethod', @@ -1409,6 +1423,14 @@ phutil_register_library_map(array( 'PhabricatorFileTransformController' => 'PhabricatorFileController', 'PhabricatorFileUploadController' => 'PhabricatorFileController', 'PhabricatorFileUploadView' => 'AphrontView', + 'PhabricatorFlag' => 'PhabricatorFlagDAO', + 'PhabricatorFlagColor' => 'PhabricatorFlagConstants', + 'PhabricatorFlagController' => 'PhabricatorController', + 'PhabricatorFlagDAO' => 'PhabricatorLiskDAO', + 'PhabricatorFlagDeleteController' => 'PhabricatorFlagController', + 'PhabricatorFlagEditController' => 'PhabricatorFlagController', + 'PhabricatorFlagListController' => 'PhabricatorFlagController', + 'PhabricatorFlagListView' => 'AphrontView', 'PhabricatorGarbageCollectorDaemon' => 'PhabricatorDaemon', 'PhabricatorGoodForNothingWorker' => 'PhabricatorWorker', 'PhabricatorHelpController' => 'PhabricatorController', diff --git a/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php b/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php index 49f2310680..7137b8e1bc 100644 --- a/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php +++ b/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php @@ -409,6 +409,13 @@ class AphrontDefaultApplicationConfiguration ), '/aphlict/' => 'PhabricatorAphlictTestPageController', + + '/flag/' => array( + '' => 'PhabricatorFlagListController', + 'view/(?P[^/]+)/' => 'PhabricatorFlagListController', + 'edit/(?P[^/]+)/' => 'PhabricatorFlagEditController', + 'delete/(?P\d+)/' => 'PhabricatorFlagDeleteController', + ), ); } diff --git a/src/applications/conduit/method/flag/base/ConduitAPI_flag_Method.php b/src/applications/conduit/method/flag/base/ConduitAPI_flag_Method.php new file mode 100644 index 0000000000..06fadcadb2 --- /dev/null +++ b/src/applications/conduit/method/flag/base/ConduitAPI_flag_Method.php @@ -0,0 +1,25 @@ + 'optional list', + 'types' => 'optional list', + 'objectPHIDs' => 'optional list', + + 'offset' => 'optional int', + 'limit' => 'optional int (default = 100)', + ); + } + + public function defineReturnType() { + return 'list'; + } + + public function defineErrorTypes() { + return array( + ); + } + + protected function execute(ConduitAPIRequest $request) { + + $query = new PhabricatorFlagQuery(); + + $owner_phids = $request->getValue('ownerPHIDs', array()); + if ($owner_phids) { + $query->withOwnerPHIDs($owner_phids); + } + + $object_phids = $request->getValue('objectPHIDs', array()); + if ($object_phids) { + $query->withObjectPHIDs($object_phids); + } + + $types = $request->getValue('types', array()); + if ($types) { + $query->withTypes($types); + } + + $query->needHandles(true); + + $query->setOffset($request->getValue('offset', 0)); + $query->setLimit($request->getValue('limit', 100)); + + $flags = $query->execute(); + + $results = array(); + foreach ($flags as $flag) { + $color = $flag->getColor(); + $uri = PhabricatorEnv::getProductionURI($flag->getHandle()->getURI()); + + $results[] = array( + 'id' => $flag->getID(), + 'ownerPHID' => $flag->getOwnerPHID(), + 'type' => $flag->getType(), + 'objectPHID' => $flag->getObjectPHID(), + 'reasonPHID' => $flag->getReasonPHID(), + 'color' => $color, + 'colorName' => PhabricatorFlagColor::getColorName($color), + 'note' => $flag->getNote(), + 'handle' => array( + 'uri' => $uri, + 'name' => $flag->getHandle()->getName(), + ), + 'dateCreated' => $flag->getDateCreated(), + 'dateModified' => $flag->getDateModified(), + ); + } + + return $results; + } + +} diff --git a/src/applications/conduit/method/flag/query/__init__.php b/src/applications/conduit/method/flag/query/__init__.php new file mode 100644 index 0000000000..7c0e1a22da --- /dev/null +++ b/src/applications/conduit/method/flag/query/__init__.php @@ -0,0 +1,15 @@ +buildViews($this->filter, $params['phid'], $revisions); - $view_objects = ipull($views, 'view'); + $view_objects = array(); + foreach ($views as $view) { + if (empty($view['special'])) { + $view_objects[] = $view['view']; + } + } $phids = array_mergev(mpull($view_objects, 'getRequiredHandlePHIDs')); $phids[] = $params['phid']; $handles = id(new PhabricatorObjectHandleData($phids))->loadHandles(); foreach ($views as $view) { - $view['view']->setHandles($handles); + if (empty($view['special'])) { + $view['view']->setHandles($handles); + } $panel = new AphrontPanelView(); $panel->setHeader($view['title']); $panel->appendChild($view['view']); @@ -436,6 +443,26 @@ final class DifferentialRevisionListController extends DifferentialController { 'view' => $view, ); + // Flags are sort of private, so only show the flag panel if you're + // looking at your own requests. + if ($user_phid == $user->getPHID()) { + $flags = id(new PhabricatorFlagQuery()) + ->withOwnerPHIDs(array($user_phid)) + ->withTypes(array(PhabricatorPHIDConstants::PHID_TYPE_DREV)) + ->needHandles(true) + ->execute(); + + $view = id(new PhabricatorFlagListView()) + ->setFlags($flags) + ->setUser($user); + + $views[] = array( + 'title' => 'Flagged Revisions', + 'view' => $view, + 'special' => true, + ); + } + $view = id(clone $template) ->setRevisions($waiting) ->setNoDataString("You have no active revisions waiting on others."); diff --git a/src/applications/differential/controller/revisionlist/__init__.php b/src/applications/differential/controller/revisionlist/__init__.php index 202761bb78..62aab2422a 100644 --- a/src/applications/differential/controller/revisionlist/__init__.php +++ b/src/applications/differential/controller/revisionlist/__init__.php @@ -11,7 +11,10 @@ phutil_require_module('phabricator', 'aphront/response/redirect'); phutil_require_module('phabricator', 'applications/differential/controller/base'); phutil_require_module('phabricator', 'applications/differential/query/revision'); phutil_require_module('phabricator', 'applications/differential/view/revisionlist'); +phutil_require_module('phabricator', 'applications/flag/query/flag'); +phutil_require_module('phabricator', 'applications/flag/view/list'); phutil_require_module('phabricator', 'applications/people/storage/user'); +phutil_require_module('phabricator', 'applications/phid/constants'); phutil_require_module('phabricator', 'applications/phid/handle/data'); phutil_require_module('phabricator', 'view/control/pager'); phutil_require_module('phabricator', 'view/form/base'); diff --git a/src/applications/differential/controller/revisionview/DifferentialRevisionViewController.php b/src/applications/differential/controller/revisionview/DifferentialRevisionViewController.php index 89185c28b0..d1cb96c3a7 100644 --- a/src/applications/differential/controller/revisionview/DifferentialRevisionViewController.php +++ b/src/applications/differential/controller/revisionview/DifferentialRevisionViewController.php @@ -358,7 +358,8 @@ final class DifferentialRevisionViewController extends DifferentialController { } private function getRevisionActions(DifferentialRevision $revision) { - $viewer_phid = $this->getRequest()->getUser()->getPHID(); + $user = $this->getRequest()->getUser(); + $viewer_phid = $user->getPHID(); $viewer_is_owner = ($revision->getAuthorPHID() == $viewer_phid); $viewer_is_reviewer = in_array($viewer_phid, $revision->getReviewers()); $viewer_is_cc = in_array($viewer_phid, $revision->getCCPHIDs()); @@ -378,6 +379,28 @@ final class DifferentialRevisionViewController extends DifferentialController { } if (!$viewer_is_anonymous) { + + require_celerity_resource('phabricator-flag-css'); + + $flag = PhabricatorFlagQuery::loadUserFlag($user, $revision_phid); + if ($flag) { + $class = PhabricatorFlagColor::getCSSClass($flag->getColor()); + $color = PhabricatorFlagColor::getColorName($flag->getColor()); + $links[] = array( + 'class' => 'flag-clear '.$class, + 'href' => '/flag/delete/'.$flag->getID().'/', + 'name' => phutil_escape_html('Remove '.$color.' Flag'), + 'sigil' => 'workflow', + ); + } else { + $links[] = array( + 'class' => 'flag-add phabricator-flag-ghost', + 'href' => '/flag/edit/'.$revision_phid.'/', + 'name' => 'Flag Revision', + 'sigil' => 'workflow', + ); + } + if (!$viewer_is_owner && !$viewer_is_reviewer) { $action = $viewer_is_cc ? 'rem' : 'add'; $links[] = array( diff --git a/src/applications/differential/controller/revisionview/__init__.php b/src/applications/differential/controller/revisionview/__init__.php index a6dc324295..4ed36fa2f2 100644 --- a/src/applications/differential/controller/revisionview/__init__.php +++ b/src/applications/differential/controller/revisionview/__init__.php @@ -28,6 +28,8 @@ phutil_require_module('phabricator', 'applications/differential/view/revisioncom phutil_require_module('phabricator', 'applications/differential/view/revisiondetail'); phutil_require_module('phabricator', 'applications/differential/view/revisionupdatehistory'); phutil_require_module('phabricator', 'applications/draft/storage/draft'); +phutil_require_module('phabricator', 'applications/flag/constants/color'); +phutil_require_module('phabricator', 'applications/flag/query/flag'); phutil_require_module('phabricator', 'applications/markup/syntax'); phutil_require_module('phabricator', 'applications/phid/handle/data'); phutil_require_module('phabricator', 'infrastructure/celerity/api'); diff --git a/src/applications/directory/controller/main/PhabricatorDirectoryMainController.php b/src/applications/directory/controller/main/PhabricatorDirectoryMainController.php index c1b5056d8e..bfc4e2d6d6 100644 --- a/src/applications/directory/controller/main/PhabricatorDirectoryMainController.php +++ b/src/applications/directory/controller/main/PhabricatorDirectoryMainController.php @@ -74,6 +74,8 @@ final class PhabricatorDirectoryMainController $tasks_panel = null; } + $flagged_panel = $this->buildFlaggedPanel(); + $jump_panel = $this->buildJumpPanel(); $revision_panel = $this->buildRevisionPanel(); $app_panel = $this->buildAppPanel(); @@ -87,6 +89,7 @@ final class PhabricatorDirectoryMainController $triage_panel, $revision_panel, $tasks_panel, + $flagged_panel, $audit_panel, $commit_panel, ); @@ -193,6 +196,43 @@ final class PhabricatorDirectoryMainController return $panel; } + private function buildFlaggedPanel() { + $user = $this->getRequest()->getUser(); + + $flag_query = id(new PhabricatorFlagQuery()) + ->withOwnerPHIDs(array($user->getPHID())) + ->needHandles(true) + ->setLimit(10); + + $flags = $flag_query->execute(); + + if (!$flags) { + return $this->renderMiniPanel( + 'No Flags', + "You haven't flagged anything."); + } + + $panel = new AphrontPanelView(); + $panel->setHeader('Flagged Objects'); + $panel->setCaption("Objects you've flagged."); + + $flag_view = new PhabricatorFlagListView(); + $flag_view->setFlags($flags); + $flag_view->setUser($user); + $panel->appendChild($flag_view); + + $panel->addButton( + phutil_render_tag( + 'a', + array( + 'href' => '/flag/', + 'class' => 'grey button', + ), + "View All Flags \xC2\xBB")); + + return $panel; + } + private function buildNeedsTriagePanel(array $projects) { $user = $this->getRequest()->getUser(); $user_phid = $user->getPHID(); diff --git a/src/applications/directory/controller/main/__init__.php b/src/applications/directory/controller/main/__init__.php index 8292e9a503..010aa5ebe7 100644 --- a/src/applications/directory/controller/main/__init__.php +++ b/src/applications/directory/controller/main/__init__.php @@ -17,6 +17,8 @@ phutil_require_module('phabricator', 'applications/differential/view/revisionlis phutil_require_module('phabricator', 'applications/directory/controller/base'); phutil_require_module('phabricator', 'applications/feed/builder/feed'); phutil_require_module('phabricator', 'applications/feed/query'); +phutil_require_module('phabricator', 'applications/flag/query/flag'); +phutil_require_module('phabricator', 'applications/flag/view/list'); phutil_require_module('phabricator', 'applications/maniphest/constants/priority'); phutil_require_module('phabricator', 'applications/maniphest/query'); phutil_require_module('phabricator', 'applications/maniphest/view/tasklist'); diff --git a/src/applications/flag/constants/base/PhabricatorFlagConstants.php b/src/applications/flag/constants/base/PhabricatorFlagConstants.php new file mode 100644 index 0000000000..e2d6b52993 --- /dev/null +++ b/src/applications/flag/constants/base/PhabricatorFlagConstants.php @@ -0,0 +1,21 @@ + 'Red', + self::COLOR_ORANGE => 'Orange', + self::COLOR_YELLOW => 'Yellow', + self::COLOR_GREEN => 'Green', + self::COLOR_BLUE => 'Blue', + self::COLOR_PINK => 'Pink', + self::COLOR_PURPLE => 'Purple', + self::COLOR_CHECKERED => 'Checkered', + ); + } + + public static function getColorName($color) { + return idx(self::getColorNameMap(), $color, 'Unknown'); + } + + public static function getCSSClass($color) { + return 'phabricator-flag-color-'.(int)$color; + } + +} diff --git a/src/applications/flag/constants/color/__init__.php b/src/applications/flag/constants/color/__init__.php new file mode 100644 index 0000000000..17a970aa26 --- /dev/null +++ b/src/applications/flag/constants/color/__init__.php @@ -0,0 +1,14 @@ +buildStandardPageView(); + + $page->setApplicationName('Flag'); + $page->setBaseURI('/flag/'); + $page->setTitle(idx($data, 'title')); + $page->setGlyph("\xE2\x9A\x90"); // Subtle! + $page->appendChild($view); + + $response = new AphrontWebpageResponse(); + return $response->setContent($page->render()); + + } +} diff --git a/src/applications/flag/controller/base/__init__.php b/src/applications/flag/controller/base/__init__.php new file mode 100644 index 0000000000..277a311c5a --- /dev/null +++ b/src/applications/flag/controller/base/__init__.php @@ -0,0 +1,15 @@ +id = $data['id']; + } + + public function processRequest() { + $request = $this->getRequest(); + $user = $request->getUser(); + + $flag = id(new PhabricatorFlag())->load($this->id); + if (!$flag) { + return new Aphront404Response(); + } + + if ($flag->getOwnerPHID() != $user->getPHID()) { + return new Aphront400Response(); + } + + $flag->delete(); + + return id(new AphrontReloadResponse())->setURI('/flag/'); + } + +} diff --git a/src/applications/flag/controller/delete/__init__.php b/src/applications/flag/controller/delete/__init__.php new file mode 100644 index 0000000000..2db190fade --- /dev/null +++ b/src/applications/flag/controller/delete/__init__.php @@ -0,0 +1,18 @@ +phid = $data['phid']; + } + + public function processRequest() { + $request = $this->getRequest(); + $user = $request->getUser(); + + $phid = $this->phid; + $handles = id(new PhabricatorObjectHandleData(array($phid))) + ->loadHandles(); + $handle = $handles[$phid]; + + if (!$handle->isComplete()) { + return new Aphront404Response(); + } + + $flag = PhabricatorFlagQuery::loadUserFlag($user, $phid); + + if (!$flag) { + $flag = new PhabricatorFlag(); + $flag->setOwnerPHID($user->getPHID()); + $flag->setType($handle->getType()); + $flag->setObjectPHID($handle->getPHID()); + $flag->setReasonPHID($user->getPHID()); + } + + if ($request->isDialogFormPost()) { + $flag->setColor($request->getInt('color')); + $flag->setNote($request->getStr('note')); + $flag->save(); + + return id(new AphrontReloadResponse())->setURI('/flag/'); + } + + $type_name = $handle->getTypeName(); + + $dialog = new AphrontDialogView(); + $dialog->setUser($user); + + $dialog->setTitle("Flag {$type_name}"); + + $form = new AphrontFormLayoutView(); + + $is_new = !$flag->getID(); + + if ($is_new) { + $form + ->appendChild( + "

You can flag this {$type_name} if you want to remember to look ". + "at it later.


"); + } + + $form + ->appendChild( + id(new AphrontFormSelectControl()) + ->setName('color') + ->setLabel('Flag Color') + ->setValue($flag->getColor()) + ->setOptions(PhabricatorFlagColor::getColorNameMap())) + ->appendChild( + id(new AphrontFormTextAreaControl()) + ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_SHORT) + ->setName('note') + ->setLabel('Note') + ->setValue($flag->getNote())); + + $dialog->appendChild($form); + + $dialog->addCancelButton($handle->getURI()); + $dialog->addSubmitButton( + $is_new ? "Flag {$type_name}" : 'Save'); + + return id(new AphrontDialogResponse())->setDialog($dialog); + } + +} diff --git a/src/applications/flag/controller/edit/__init__.php b/src/applications/flag/controller/edit/__init__.php new file mode 100644 index 0000000000..08f7b0bbcd --- /dev/null +++ b/src/applications/flag/controller/edit/__init__.php @@ -0,0 +1,25 @@ +getRequest(); + $user = $request->getUser(); + + $nav = new AphrontSideNavFilterView(); + $nav->setBaseURI(new PhutilURI('/flag/view/')); + $nav->addFilter('all', 'Flags'); + $nav->selectFilter('all', 'all'); + + $query = new PhabricatorFlagQuery(); + $query->withOwnerPHIDs(array($user->getPHID())); + $query->needHandles(true); + + $flags = $query->execute(); + + $view = new PhabricatorFlagListView(); + $view->setFlags($flags); + $view->setUser($user); + + $panel = new AphrontPanelView(); + $panel->setHeader('Flags'); + $panel->appendChild($view); + + $nav->appendChild($panel); + + return $this->buildStandardPageResponse( + $nav, + array( + 'title' => 'Flags', + )); + } + +} diff --git a/src/applications/flag/controller/list/__init__.php b/src/applications/flag/controller/list/__init__.php new file mode 100644 index 0000000000..f101c5e1a3 --- /dev/null +++ b/src/applications/flag/controller/list/__init__.php @@ -0,0 +1,18 @@ +ownerPHIDs = $owner_phids; + return $this; + } + + public function withTypes(array $types) { + $this->types = $types; + return $this; + } + + public function withObjectPHIDs(array $object_phids) { + $this->objectPHIDs = $object_phids; + return $this; + } + + public function needHandles($need) { + $this->needHandles = $need; + return $this; + } + + public function needObjects($need) { + $this->needObjects = $need; + return $this; + } + + public function setLimit($limit) { + $this->limit = $limit; + return $this; + } + + public function setOffset($offset) { + $this->offset = $offset; + return $this; + } + + public static function loadUserFlag(PhabricatorUser $user, $object_phid) { + // Specifying the type in the query allows us to use a key. + return id(new PhabricatorFlag())->loadOneWhere( + 'ownerPHID = %s AND type = %s AND objectPHID = %s', + $user->getPHID(), + PhabricatorObjectHandleData::lookupType($object_phid), + $object_phid); + } + + + public function execute() { + $table = new PhabricatorFlag(); + $conn_r = $table->establishConnection('r'); + + $where = $this->buildWhereClause($conn_r); + $limit = $this->buildLimitClause($conn_r); + $order = $this->buildOrderClause($conn_r); + + $data = queryfx_all( + $conn_r, + 'SELECT * FROM %T flag %Q %Q %Q', + $table->getTableName(), + $where, + $order, + $limit); + + $flags = $table->loadAllFromArray($data); + + if ($this->needHandles || $this->needObjects) { + $phids = ipull($data, 'objectPHID'); + $query = new PhabricatorObjectHandleData($phids); + + if ($this->needHandles) { + $handles = $query->loadHandles(); + foreach ($flags as $flag) { + $handle = idx($handles, $flag->getObjectPHID()); + if ($handle) { + $flag->attachHandle($handle); + } + } + } + + if ($this->needObjects) { + $objects = $query->loadObjects(); + foreach ($flags as $flag) { + $object = idx($objects, $flag->getObjectPHID()); + if ($object) { + $flag->attachObject($object); + } + } + } + } + + return $flags; + } + + private function buildWhereClause($conn_r) { + + $where = array(); + + if ($this->ownerPHIDs) { + $where[] = qsprintf( + $conn_r, + 'flag.ownerPHID IN (%Ls)', + $this->ownerPHIDs); + } + + if ($this->types) { + $where[] = qsprintf( + $conn_r, + 'flag.type IN (%Ls)', + $this->types); + } + + if ($this->objectPHIDs) { + $where[] = qsprintf( + $conn_r, + 'flag.objectPHID IN (%Ls)', + $this->objectPHIDs); + } + + if ($where) { + return 'WHERE ('.implode(') AND (', $where).')'; + } else { + return ''; + } + } + + private function buildOrderClause($conn_r) { + return 'ORDER BY id DESC'; + } + + private function buildLimitClause($conn_r) { + if ($this->limit && $this->offset) { + return qsprintf($conn_r, 'LIMIT %d, %d', $this->offset, $this->limit); + } else if ($this->limit) { + return qsprintf($conn_r, 'LIMIT %d', $this->limit); + } else if ($this->offset) { + return qsprintf($conn_r, 'LIMIT %d, %d', $this->offset, PHP_INT_MAX); + } else { + return ''; + } + } + +} diff --git a/src/applications/flag/query/flag/__init__.php b/src/applications/flag/query/flag/__init__.php new file mode 100644 index 0000000000..30e59cd290 --- /dev/null +++ b/src/applications/flag/query/flag/__init__.php @@ -0,0 +1,17 @@ +object === false) { + throw new Exception('Call attachObject() before getObject()!'); + } + return $this->object; + } + + public function attachObject($object) { + $this->object = $object; + return $this; + } + + public function getHandle() { + if ($this->handle === false) { + throw new Exception('Call attachHandle() before getHandle()!'); + } + return $this->handle; + } + + public function attachHandle(PhabricatorObjectHandle $handle) { + $this->handle = $handle; + return $this; + } + +} diff --git a/src/applications/flag/storage/flag/__init__.php b/src/applications/flag/storage/flag/__init__.php new file mode 100644 index 0000000000..0386fd500a --- /dev/null +++ b/src/applications/flag/storage/flag/__init__.php @@ -0,0 +1,13 @@ +flags = $flags; + return $this; + } + + public function setUser(PhabricatorUser $user) { + $this->user = $user; + return $this; + } + + public function render() { + $user = $this->user; + + require_celerity_resource('phabricator-flag-css'); + + $rows = array(); + foreach ($this->flags as $flag) { + $class = PhabricatorFlagColor::getCSSClass($flag->getColor()); + + $rows[] = array( + phutil_render_tag( + 'div', + array( + 'class' => 'phabricator-flag-icon '.$class, + ), + ''), + $flag->getHandle()->renderLink(), + phutil_escape_html($flag->getNote()), + phabricator_datetime($flag->getDateCreated(), $user), + phabricator_render_form( + $user, + array( + 'method' => 'POST', + 'action' => '/flag/edit/'.$flag->getObjectPHID().'/', + 'sigil' => 'workflow', + ), + phutil_render_tag( + 'button', + array( + 'class' => 'small grey', + ), + 'Edit Flag')), + phabricator_render_form( + $user, + array( + 'method' => 'POST', + 'action' => '/flag/delete/'.$flag->getID().'/', + 'sigil' => 'workflow', + ), + phutil_render_tag( + 'button', + array( + 'class' => 'small grey', + ), + 'Remove Flag')), + ); + } + + $table = new AphrontTableView($rows); + $table->setHeaders( + array( + '', + 'Flagged Object', + 'Note', + 'Flagged On', + '', + '', + )); + $table->setColumnClasses( + array( + '', + 'pri', + 'wide', + '', + 'action', + 'action', + )); + $table->setNoDataString('No flags.'); + + return $table->render(); + } +} diff --git a/src/applications/flag/view/list/__init__.php b/src/applications/flag/view/list/__init__.php new file mode 100644 index 0000000000..affe0c6169 --- /dev/null +++ b/src/applications/flag/view/list/__init__.php @@ -0,0 +1,19 @@ +setClass('action-edit'); $actions[] = $action; + require_celerity_resource('phabricator-flag-css'); + $flag = PhabricatorFlagQuery::loadUserFlag($user, $task->getPHID()); + if ($flag) { + $class = PhabricatorFlagColor::getCSSClass($flag->getColor()); + $color = PhabricatorFlagColor::getColorName($flag->getColor()); + + $action = new AphrontHeadsupActionView(); + $action->setClass('flag-clear '.$class); + $action->setURI('/flag/delete/'.$flag->getID().'/'); + $action->setName('Remove '.$color.' Flag'); + $action->setWorkflow(true); + $actions[] = $action; + } else { + $action = new AphrontHeadsupActionView(); + $action->setClass('phabricator-flag-ghost'); + $action->setURI('/flag/edit/'.$task->getPHID().'/'); + $action->setName('Flag Task'); + $action->setWorkflow(true); + $actions[] = $action; + } + require_celerity_resource('phabricator-object-selector-css'); require_celerity_resource('javelin-behavior-phabricator-object-selector'); diff --git a/src/applications/maniphest/controller/taskdetail/__init__.php b/src/applications/maniphest/controller/taskdetail/__init__.php index 53ac7baef7..4bd258840b 100644 --- a/src/applications/maniphest/controller/taskdetail/__init__.php +++ b/src/applications/maniphest/controller/taskdetail/__init__.php @@ -9,6 +9,8 @@ phutil_require_module('phabricator', 'aphront/response/404'); phutil_require_module('phabricator', 'applications/draft/storage/draft'); phutil_require_module('phabricator', 'applications/files/storage/file'); +phutil_require_module('phabricator', 'applications/flag/constants/color'); +phutil_require_module('phabricator', 'applications/flag/query/flag'); phutil_require_module('phabricator', 'applications/maniphest/constants/priority'); phutil_require_module('phabricator', 'applications/maniphest/constants/status'); phutil_require_module('phabricator', 'applications/maniphest/constants/transactiontype'); diff --git a/src/applications/phid/handle/data/PhabricatorObjectHandleData.php b/src/applications/phid/handle/data/PhabricatorObjectHandleData.php index fa545780bb..b357444ecc 100644 --- a/src/applications/phid/handle/data/PhabricatorObjectHandleData.php +++ b/src/applications/phid/handle/data/PhabricatorObjectHandleData.php @@ -27,7 +27,7 @@ final class PhabricatorObjectHandleData { public function loadObjects() { $types = array(); foreach ($this->phids as $phid) { - $type = $this->lookupType($phid); + $type = self::lookupType($phid); $types[$type][] = $phid; } @@ -95,7 +95,7 @@ final class PhabricatorObjectHandleData { $types = array(); foreach ($this->phids as $phid) { - $type = $this->lookupType($phid); + $type = self::lookupType($phid); $types[$type][] = $phid; } @@ -493,7 +493,7 @@ final class PhabricatorObjectHandleData { return $handles; } - private function lookupType($phid) { + public static function lookupType($phid) { $matches = null; if (preg_match('/^PHID-([^-]{4})-/', $phid, $matches)) { return $matches[1]; diff --git a/webroot/rsrc/css/application/flag/flag.css b/webroot/rsrc/css/application/flag/flag.css new file mode 100644 index 0000000000..33789dcc0b --- /dev/null +++ b/webroot/rsrc/css/application/flag/flag.css @@ -0,0 +1,44 @@ +/** + * @provides phabricator-flag-css + */ + +.phabricator-flag-icon { + padding: 8px; + background: transparent 0 0 no-repeat; +} + +.phabricator-flag-color-0 { + background-image: url(/rsrc/image/icon/fatcow/flag_red.png); +} + +.phabricator-flag-color-1 { + background-image: url(/rsrc/image/icon/fatcow/flag_orange.png); +} + +.phabricator-flag-color-2 { + background-image: url(/rsrc/image/icon/fatcow/flag_yellow.png); +} + +.phabricator-flag-color-3 { + background-image: url(/rsrc/image/icon/fatcow/flag_green.png); +} + +.phabricator-flag-color-4 { + background-image: url(/rsrc/image/icon/fatcow/flag_blue.png); +} + +.phabricator-flag-color-5 { + background-image: url(/rsrc/image/icon/fatcow/flag_pink.png); +} + +.phabricator-flag-color-6 { + background-image: url(/rsrc/image/icon/fatcow/flag_purple.png); +} + +.phabricator-flag-color-7 { + background-image: url(/rsrc/image/icon/fatcow/flag_finish.png); +} + +.phabricator-flag-ghost { + background-image: url(/rsrc/image/icon/fatcow/flag_ghost.png); +} diff --git a/webroot/rsrc/image/icon/fatcow/flag_blue.png b/webroot/rsrc/image/icon/fatcow/flag_blue.png new file mode 100755 index 0000000000..1a79762acf Binary files /dev/null and b/webroot/rsrc/image/icon/fatcow/flag_blue.png differ diff --git a/webroot/rsrc/image/icon/fatcow/flag_finish.png b/webroot/rsrc/image/icon/fatcow/flag_finish.png new file mode 100755 index 0000000000..71695d3620 Binary files /dev/null and b/webroot/rsrc/image/icon/fatcow/flag_finish.png differ diff --git a/webroot/rsrc/image/icon/fatcow/flag_ghost.png b/webroot/rsrc/image/icon/fatcow/flag_ghost.png new file mode 100644 index 0000000000..23a4b7bcaf Binary files /dev/null and b/webroot/rsrc/image/icon/fatcow/flag_ghost.png differ diff --git a/webroot/rsrc/image/icon/fatcow/flag_green.png b/webroot/rsrc/image/icon/fatcow/flag_green.png new file mode 100755 index 0000000000..c8c61b0780 Binary files /dev/null and b/webroot/rsrc/image/icon/fatcow/flag_green.png differ diff --git a/webroot/rsrc/image/icon/fatcow/flag_orange.png b/webroot/rsrc/image/icon/fatcow/flag_orange.png new file mode 100755 index 0000000000..945dfa988d Binary files /dev/null and b/webroot/rsrc/image/icon/fatcow/flag_orange.png differ diff --git a/webroot/rsrc/image/icon/fatcow/flag_pink.png b/webroot/rsrc/image/icon/fatcow/flag_pink.png new file mode 100755 index 0000000000..2cdae0174a Binary files /dev/null and b/webroot/rsrc/image/icon/fatcow/flag_pink.png differ diff --git a/webroot/rsrc/image/icon/fatcow/flag_purple.png b/webroot/rsrc/image/icon/fatcow/flag_purple.png new file mode 100755 index 0000000000..0cb00ee851 Binary files /dev/null and b/webroot/rsrc/image/icon/fatcow/flag_purple.png differ diff --git a/webroot/rsrc/image/icon/fatcow/flag_red.png b/webroot/rsrc/image/icon/fatcow/flag_red.png new file mode 100755 index 0000000000..b99e36b283 Binary files /dev/null and b/webroot/rsrc/image/icon/fatcow/flag_red.png differ diff --git a/webroot/rsrc/image/icon/fatcow/flag_yellow.png b/webroot/rsrc/image/icon/fatcow/flag_yellow.png new file mode 100755 index 0000000000..8f087bbb64 Binary files /dev/null and b/webroot/rsrc/image/icon/fatcow/flag_yellow.png differ