diff --git a/src/__celerity_resource_map__.php b/src/__celerity_resource_map__.php index ef74552bad..aca5273560 100644 --- a/src/__celerity_resource_map__.php +++ b/src/__celerity_resource_map__.php @@ -188,6 +188,33 @@ celerity_register_resource_map(array( ), 'disk' => '/rsrc/css/application/directory/phabricator-directory.css', ), + 'mainphest-task-detail-css' => + array( + 'uri' => '/res/e5f3beca/rsrc/css/application/maniphest/task-detail.css', + 'type' => 'css', + 'requires' => + array( + ), + 'disk' => '/rsrc/css/application/maniphest/task-detail.css', + ), + 'maniphest-task-summary-css' => + array( + 'uri' => '/res/bed1edf0/rsrc/css/application/maniphest/task-summary.css', + 'type' => 'css', + 'requires' => + array( + ), + 'disk' => '/rsrc/css/application/maniphest/task-summary.css', + ), + 'maniphest-transaction-detail-css' => + array( + 'uri' => '/res/436b83d7/rsrc/css/application/maniphest/transaction-detail.css', + 'type' => 'css', + 'requires' => + array( + ), + 'disk' => '/rsrc/css/application/maniphest/transaction-detail.css', + ), 'phabricator-core-buttons-css' => array( 'uri' => '/res/fe74ba44/rsrc/css/core/buttons.css', @@ -332,6 +359,16 @@ celerity_register_resource_map(array( ), 'disk' => '/rsrc/js/application/differential/behavior-show-more.js', ), + 'javelin-behavior-maniphest-transaction-controls' => + array( + 'uri' => '/res/fc6a8722/rsrc/js/application/maniphest/behavior-transaction-controls.js', + 'type' => 'js', + 'requires' => + array( + 0 => 'javelin-lib-dev', + ), + 'disk' => '/rsrc/js/application/maniphest/behavior-transaction-controls.js', + ), 'javelin-magical-init' => array( 'uri' => '/res/76614f84/rsrc/js/javelin/init.dev.js', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 5f80b68b39..da9c0cc81c 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -133,6 +133,22 @@ phutil_register_library_map(array( 'DifferentialUnitStatus' => 'applications/differential/constants/unitstatus', 'Javelin' => 'infrastructure/javelin/api', 'LiskDAO' => 'storage/lisk/dao', + 'ManiphestController' => 'applications/maniphest/controller/base', + 'ManiphestDAO' => 'applications/maniphest/storage/base', + 'ManiphestTask' => 'applications/maniphest/storage/task', + 'ManiphestTaskCreateController' => 'applications/maniphest/controller/createtask', + 'ManiphestTaskDetailController' => 'applications/maniphest/controller/taskdetail', + 'ManiphestTaskListController' => 'applications/maniphest/controller/tasklist', + 'ManiphestTaskListView' => 'applications/maniphest/view/tasklist', + 'ManiphestTaskPriority' => 'applications/maniphest/constants/priority', + 'ManiphestTaskStatus' => 'applications/maniphest/constants/status', + 'ManiphestTaskSummaryView' => 'applications/maniphest/view/tasksummary', + 'ManiphestTransaction' => 'applications/maniphest/storage/transaction', + 'ManiphestTransactionDetailView' => 'applications/maniphest/view/transactiondetail', + 'ManiphestTransactionEditor' => 'applications/maniphest/editor/transaction', + 'ManiphestTransactionListView' => 'applications/maniphest/view/transactionlist', + 'ManiphestTransactionSaveController' => 'applications/maniphest/controller/transactionsave', + 'ManiphestTransactionType' => 'applications/maniphest/constants/transactiontype', 'Phabricator404Controller' => 'applications/base/controller/404', 'PhabricatorAuthController' => 'applications/auth/controller/base', 'PhabricatorConduitAPIController' => 'applications/conduit/controller/api', @@ -320,6 +336,18 @@ phutil_register_library_map(array( 'DifferentialRevisionListController' => 'DifferentialController', 'DifferentialRevisionUpdateHistoryView' => 'AphrontView', 'DifferentialRevisionViewController' => 'DifferentialController', + 'ManiphestController' => 'PhabricatorController', + 'ManiphestDAO' => 'PhabricatorLiskDAO', + 'ManiphestTask' => 'ManiphestDAO', + 'ManiphestTaskCreateController' => 'ManiphestController', + 'ManiphestTaskDetailController' => 'ManiphestController', + 'ManiphestTaskListController' => 'ManiphestController', + 'ManiphestTaskListView' => 'AphrontView', + 'ManiphestTaskSummaryView' => 'AphrontView', + 'ManiphestTransaction' => 'ManiphestDAO', + 'ManiphestTransactionDetailView' => 'AphrontView', + 'ManiphestTransactionListView' => 'AphrontView', + 'ManiphestTransactionSaveController' => 'ManiphestController', 'Phabricator404Controller' => 'PhabricatorController', 'PhabricatorAuthController' => 'PhabricatorController', 'PhabricatorConduitAPIController' => 'PhabricatorConduitController', diff --git a/src/aphront/console/plugin/request/DarkConsoleRequestPlugin.php b/src/aphront/console/plugin/request/DarkConsoleRequestPlugin.php index 1c155eec4b..77dea1566c 100755 --- a/src/aphront/console/plugin/request/DarkConsoleRequestPlugin.php +++ b/src/aphront/console/plugin/request/DarkConsoleRequestPlugin.php @@ -53,7 +53,7 @@ class DarkConsoleRequestPlugin extends DarkConsolePlugin { foreach ($map as $key => $value) { $rows[] = array( phutil_escape_html($key), - phutil_escape_html($value), + phutil_escape_html(is_array($value) ? json_encode($value) : $value), ); } diff --git a/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php b/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php index 82240be5db..1d9a30a173 100644 --- a/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php +++ b/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php @@ -139,6 +139,19 @@ class AphrontDefaultApplicationConfiguration '/settings/' => array( '(?:page/(?P[^/]+)/)?$' => 'PhabricatorUserSettingsController', ), + + '/maniphest/' => array( + '$' => 'ManiphestTaskListController', + 'view/(?P\w+)/$' => 'ManiphestTaskListController', + 'task/' => array( + 'create/' => 'ManiphestTaskCreateController', + ), + 'transaction/' => array( + 'save/' => 'ManiphestTransactionSaveController', + ), + ), + + '/T(?P\d+)$' => 'ManiphestTaskDetailController', ); } diff --git a/src/applications/maniphest/constants/priority/ManiphestTaskPriority.php b/src/applications/maniphest/constants/priority/ManiphestTaskPriority.php new file mode 100644 index 0000000000..fa52e3d9d6 --- /dev/null +++ b/src/applications/maniphest/constants/priority/ManiphestTaskPriority.php @@ -0,0 +1,42 @@ + 'Unbreak Now!', + self::PRIORITY_TRIAGE => 'Needs Triage', + self::PRIORITY_HIGH => 'High', + self::PRIORITY_NORMAL => 'Normal', + self::PRIORITY_LOW => 'Low', + self::PRIORITY_WISH => 'Wishlist', + ); + } + + public static function getTaskPriorityName($priority) { + return idx(self::getTaskPriorityMap(), $priority, '???'); + } +} diff --git a/src/applications/maniphest/constants/priority/__init__.php b/src/applications/maniphest/constants/priority/__init__.php new file mode 100644 index 0000000000..791832bac4 --- /dev/null +++ b/src/applications/maniphest/constants/priority/__init__.php @@ -0,0 +1,12 @@ + 'Open', + self::STATUS_CLOSED_RESOLVED => 'Resolved', + self::STATUS_CLOSED_WONTFIX => 'Wontfix', + self::STATUS_CLOSED_INVALID => 'Invalid', + self::STATUS_CLOSED_DUPLICATE => 'Duplicate', + self::STATUS_CLOSED_SPITE => 'Spite', + ); + } + + public static function getTaskStatusFullName($status) { + $map = array( + self::STATUS_OPEN => 'Open', + self::STATUS_CLOSED_RESOLVED => 'Closed, Resolved', + self::STATUS_CLOSED_WONTFIX => 'Closed, Wontfix', + self::STATUS_CLOSED_INVALID => 'Closed, Invalid', + self::STATUS_CLOSED_DUPLICATE => 'Closed, Duplicate', + self::STATUS_CLOSED_SPITE => 'Closed out of Spite', + ); + return idx($map, $status, '???'); + } + +} diff --git a/src/applications/maniphest/constants/status/__init__.php b/src/applications/maniphest/constants/status/__init__.php new file mode 100644 index 0000000000..0b7212d908 --- /dev/null +++ b/src/applications/maniphest/constants/status/__init__.php @@ -0,0 +1,12 @@ + 'Comment', + self::TYPE_STATUS => 'Close Task', + self::TYPE_OWNER => 'Reassign / Claim', + self::TYPE_CCS => 'Add CCs', + self::TYPE_PRIORITY => 'Change Priority', + ); + } + +} diff --git a/src/applications/maniphest/constants/transactiontype/__init__.php b/src/applications/maniphest/constants/transactiontype/__init__.php new file mode 100644 index 0000000000..2d1616cc7d --- /dev/null +++ b/src/applications/maniphest/constants/transactiontype/__init__.php @@ -0,0 +1,10 @@ +buildStandardPageView(); + + $page->setApplicationName('Maniphest'); + $page->setBaseURI('/maniphest/'); + $page->setTitle(idx($data, 'title')); + $page->setGlyph("\xE2\x9A\x93"); + $page->appendChild($view); + + $response = new AphrontWebpageResponse(); + return $response->setContent($page->render()); + } + +} diff --git a/src/applications/maniphest/controller/base/__init__.php b/src/applications/maniphest/controller/base/__init__.php new file mode 100644 index 0000000000..04b0fcd7dc --- /dev/null +++ b/src/applications/maniphest/controller/base/__init__.php @@ -0,0 +1,15 @@ +getRequest(); + $user = $request->getUser(); + + $task = new ManiphestTask(); + + $task->setPriority(ManiphestTaskPriority::PRIORITY_TRIAGE); + + $errors = array(); + $e_title = true; + + if ($request->isFormPost()) { + $task->setTitle($request->getStr('title')); + $task->setAuthorPHID($user->getPHID()); + $owner_tokenizer = $request->getArr('assigned_to'); + $task->setOwnerPHID(reset($owner_tokenizer)); + $task->setCCPHIDs($request->getArr('cc')); + $task->setPriority($request->getInt('priority')); + $task->setDescription($request->getStr('description')); + + if (!strlen($task->getTitle())) { + $e_title = 'Required'; + $errors[] = 'Title is required.'; + } + + if (!$errors) { + $transaction = new ManiphestTransaction(); + $transaction->setAuthorPHID($user->getPHID()); + $transaction->setTransactionType(ManiphestTransactionType::TYPE_STATUS); + $transaction->setNewValue(ManiphestTaskStatus::STATUS_OPEN); + + $editor = new ManiphestTransactionEditor(); + $editor->applyTransaction($task, $transaction); + + return id(new AphrontRedirectResponse()) + ->setURI('/T'.$task->getID()); + } + } + + $phids = array_merge( + array($task->getOwnerPHID()), + nonempty($task->getCCPHIDs(), array())); + $phids = array_filter($phids); + $phids = array_unique($phids); + + $handles = id(new PhabricatorObjectHandleData($phids)) + ->loadHandles($phids); + + $tvalues = mpull($handles, 'getFullName', 'getPHID'); + + $error_view = null; + if ($errors) { + $error_view = new AphrontErrorView(); + $error_view->setErrors($errors); + $error_view->setTitle('Form Errors'); + } + + $priority_map = ManiphestTaskPriority::getTaskPriorityMap(); + + if ($task->getOwnerPHID()) { + $assigned_value = array( + $task->getOwnerPHID() => $handles[$task->getOwnerPHID()]->getFullName(), + ); + } else { + $assigned_value = array(); + } + + if ($task->getCCPHIDs()) { + $cc_value = array_select_keys($tvalues, $task->getCCPHIDs()); + } else { + $cc_value = array(); + } + + $form = new AphrontFormView(); + $form + ->setUser($user) + ->appendChild( + id(new AphrontFormTextAreaControl()) + ->setLabel('Title') + ->setName('title') + ->setError($e_title) + ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_SHORT) + ->setValue($task->getTitle())) + ->appendChild( + id(new AphrontFormTokenizerControl()) + ->setLabel('Assigned To') + ->setName('assigned_to') + ->setValue($assigned_value) + ->setDatasource('/typeahead/common/users/') + ->setLimit(1)) + ->appendChild( + id(new AphrontFormTokenizerControl()) + ->setLabel('CC') + ->setName('cc') + ->setValue($cc_value) + ->setDatasource('/typeahead/common/mailable/')) + ->appendChild( + id(new AphrontFormSelectControl()) + ->setLabel('Priority') + ->setName('priority') + ->setOptions($priority_map) + ->setValue($task->getPriority())) + ->appendChild( + id(new AphrontFormTextAreaControl()) + ->setLabel('Description') + ->setName('description') + ->setValue($task->getDescription())) + ->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue('Create Task')); + + $panel = new AphrontPanelView(); + $panel->setWidth(AphrontPanelView::WIDTH_FULL); + $panel->setHeader('Create New Task'); + $panel->appendChild($form); + + return $this->buildStandardPageResponse( + array( + $error_view, + $panel, + ), + array( + 'title' => 'Create Task', + )); + } +} diff --git a/src/applications/maniphest/controller/createtask/__init__.php b/src/applications/maniphest/controller/createtask/__init__.php new file mode 100644 index 0000000000..580a9fec9a --- /dev/null +++ b/src/applications/maniphest/controller/createtask/__init__.php @@ -0,0 +1,26 @@ +id = $data['id']; + } + + public function processRequest() { + + $request = $this->getRequest(); + $user = $request->getUser(); + + $e_title = null; + + $priority_map = ManiphestTaskPriority::getTaskPriorityMap(); + + $task = id(new ManiphestTask())->load($this->id); + + $transactions = id(new ManiphestTransaction())->loadAllWhere( + 'taskID = %d', + $task->getID()); + + $phids = array(); + foreach ($transactions as $transaction) { + foreach ($transaction->extractPHIDs() as $phid) { + $phids[$phid] = true; + } + } + foreach ($task->getCCPHIDs() as $phid) { + $phids[$phid] = true; + } + if ($task->getOwnerPHID()) { + $phids[$task->getOwnerPHID()] = true; + } + $phids[$task->getAuthorPHID()] = true; + $phids = array_keys($phids); + + $handles = id(new PhabricatorObjectHandleData($phids)) + ->loadHandles(); + + $factory = new DifferentialMarkupEngineFactory(); + $engine = $factory->newDifferentialCommentMarkupEngine(); + + $dict = array(); + $dict['Status'] = + ''. + ManiphestTaskStatus::getTaskStatusFullName($task->getStatus()). + ''; + + $dict['Assigned To'] = $task->getOwnerPHID() + ? 'None' + : $handles[$task->getOwnerPHID()]->renderLink(); + + $dict['Priority'] = ManiphestTaskPriority::getTaskPriorityName( + $task->getPriority()); + + $cc = $task->getCCPHIDs(); + if ($cc) { + $cc_links = array(); + foreach ($cc as $phid) { + $cc_links[] = $handles[$phid]->renderLink(); + } + $dict['CC'] = implode(', ', $cc_links); + } else { + $dict['CC'] = 'None'; + } + + $dict['Author'] = $handles[$task->getAuthorPHID()]->renderLink(); + + $dict['Description'] = $engine->markupText($task->getDescription()); + + require_celerity_resource('mainphest-task-detail-css'); + + $table = array(); + foreach ($dict as $key => $value) { + $table[] = + ''. + ''.phutil_escape_html($key).':'. + ''.$value.''. + ''; + } + $table = + ''. + implode("\n", $table). + '
'; + + $panel = + '
'. + '
'. + '

'. + phutil_escape_html('T'.$task->getID().' '.$task->getTitle()). + '

'. + $table. + '
'. + '
'; + + $transaction_types = ManiphestTransactionType::getTransactionTypeMap(); + $resolution_types = ManiphestTaskStatus::getTaskStatusMap(); + + if ($task->getStatus() == ManiphestTaskStatus::STATUS_OPEN) { + $resolution_types = array_select_keys( + $resolution_types, + array( + ManiphestTaskStatus::STATUS_CLOSED_RESOLVED, + ManiphestTaskStatus::STATUS_CLOSED_WONTFIX, + ManiphestTaskStatus::STATUS_CLOSED_INVALID, + ManiphestTaskStatus::STATUS_CLOSED_SPITE, + )); + } else { + $resolution_types = array( + ManiphestTaskStatus::STATUS_OPEN => 'Reopened', + ); + $transaction_types[ManiphestTransactionType::TYPE_STATUS] = + 'Reopen Task'; + unset($transaction_types[ManiphestTransactionType::TYPE_PRIORITY]); + unset($transaction_types[ManiphestTransactionType::TYPE_OWNER]); + } + + $default_claim = array( + $user->getPHID() => $user->getUsername().' ('.$user->getRealName().')', + ); + + $comment_form = new AphrontFormView(); + $comment_form + ->setUser($user) + ->setAction('/maniphest/transaction/save/') + ->addHiddenInput('taskID', $task->getID()) + ->appendChild( + id(new AphrontFormSelectControl()) + ->setLabel('Action') + ->setName('action') + ->setOptions($transaction_types) + ->setID('transaction-action')) + ->appendChild( + id(new AphrontFormSelectControl()) + ->setLabel('Resolution') + ->setName('resolution') + ->setControlID('resolution') + ->setControlStyle('display: none') + ->setOptions($resolution_types)) + ->appendChild( + id(new AphrontFormTokenizerControl()) + ->setLabel('Assign To') + ->setName('assign_to') + ->setControlID('assign_to') + ->setControlStyle('display: none') + ->setID('assign-tokenizer') + ->setDisableBehavior(true)) + ->appendChild( + id(new AphrontFormTokenizerControl()) + ->setLabel('CCs') + ->setName('ccs') + ->setControlID('ccs') + ->setControlStyle('display: none') + ->setID('cc-tokenizer') + ->setDisableBehavior(true)) + ->appendChild( + id(new AphrontFormSelectControl()) + ->setLabel('Priority') + ->setName('priority') + ->setOptions($priority_map) + ->setControlID('priority') + ->setControlStyle('display: none') + ->setValue($task->getPriority())) + ->appendChild( + id(new AphrontFormTextAreaControl()) + ->setLabel('Comments') + ->setName('comments') + ->setValue('')) + ->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue('Avast!')); + + Javelin::initBehavior('maniphest-transaction-controls', array( + 'select' => 'transaction-action', + 'controlMap' => array( + ManiphestTransactionType::TYPE_STATUS => 'resolution', + ManiphestTransactionType::TYPE_OWNER => 'assign_to', + ManiphestTransactionType::TYPE_CCS => 'ccs', + ManiphestTransactionType::TYPE_PRIORITY => 'priority', + ), + 'tokenizers' => array( + ManiphestTransactionType::TYPE_OWNER => array( + 'id' => 'assign-tokenizer', + 'src' => '/typeahead/common/users/', + 'value' => $default_claim, + 'limit' => 1, + ), + ManiphestTransactionType::TYPE_CCS => array( + 'id' => 'cc-tokenizer', + 'src' => '/typeahead/common/mailable/', + ), + ), + )); + + $comment_panel = new AphrontPanelView(); + $comment_panel->appendChild($comment_form); + $comment_panel->setHeader('Leap Into Action'); + + $transaction_view = new ManiphestTransactionListView(); + $transaction_view->setTransactions($transactions); + $transaction_view->setHandles($handles); + $transaction_view->setUser($user); + $transaction_view->setMarkupEngine($engine); + + return $this->buildStandardPageResponse( + array( + $panel, + $transaction_view, + $comment_panel, + ), + array( + 'title' => 'Create Task', + )); + } +} diff --git a/src/applications/maniphest/controller/taskdetail/__init__.php b/src/applications/maniphest/controller/taskdetail/__init__.php new file mode 100644 index 0000000000..98cc671b92 --- /dev/null +++ b/src/applications/maniphest/controller/taskdetail/__init__.php @@ -0,0 +1,28 @@ +view = idx($data, 'view'); + } + + public function processRequest() { + + $views = array( + 'action' => 'Action Required', +// 'activity' => 'Recently Active', +// 'closed' => 'Recently Closed', + 'created' => 'Created', + 'triage' => 'Need Triage', + ); + + if (empty($views[$this->view])) { + $this->view = key($views); + } + + $tasks = $this->loadTasks(); + + $nav = new AphrontSideNavView(); + foreach ($views as $view => $name) { + $nav->addNavItem( + phutil_render_tag( + 'a', + array( + 'href' => '/maniphest/view/'.$view.'/', + 'class' => ($this->view == $view) + ? 'aphront-side-nav-selected' + : null, + ), + phutil_escape_html($name))); + } + + $handle_phids = mpull($tasks, 'getOwnerPHID'); + $handles = id(new PhabricatorObjectHandleData($handle_phids)) + ->loadHandles(); + + $task_list = new ManiphestTaskListView(); + $task_list->setTasks($tasks); + $task_list->setHandles($handles); + + $nav->appendChild( + '
'. + ''. + 'Create New Task'. + ''. + '
'); + $nav->appendChild($task_list); + + return $this->buildStandardPageResponse( + $nav, + array( + 'title' => 'Task List', + )); + } + + private function loadTasks() { + $request = $this->getRequest(); + $user = $request->getUser(); + + $phids = array($user->getPHID()); + + switch ($this->view) { + case 'action': + return id(new ManiphestTask())->loadAllWhere( + 'ownerPHID in (%Ls) AND status = 0', + $phids); + case 'created': + return id(new ManiphestTask())->loadAllWhere( + 'authorPHID in (%Ls) AND status = 0', + $phids); + case 'triage': + return id(new ManiphestTask())->loadAllWhere( + 'status = %d', + ManiphestTaskPriority::PRIORITY_TRIAGE); + } + + return array(); + } + + +} diff --git a/src/applications/maniphest/controller/tasklist/__init__.php b/src/applications/maniphest/controller/tasklist/__init__.php new file mode 100644 index 0000000000..17b4309e18 --- /dev/null +++ b/src/applications/maniphest/controller/tasklist/__init__.php @@ -0,0 +1,20 @@ +getRequest(); + $user = $request->getUser(); + + $task = id(new ManiphestTask())->load($request->getStr('taskID')); + if (!$task) { + return new Aphront404Response(); + } + + $action = $request->getStr('action'); + + $transaction = new ManiphestTransaction(); + $transaction + ->setAuthorPHID($user->getPHID()) + ->setComments($request->getStr('comments')) + ->setTransactionType($action); + + switch ($action) { + case ManiphestTransactionType::TYPE_NONE: + break; + case ManiphestTransactionType::TYPE_STATUS: + $transaction->setNewValue($request->getStr('resolution')); + break; + case ManiphestTransactionType::TYPE_OWNER: + $assign_to = $request->getArr('assign_to'); + $assign_to = reset($assign_to); + $transaction->setNewValue($assign_to); + break; + case ManiphestTransactionType::TYPE_CCS: + $ccs = $request->getArr('ccs'); + $transaction->setNewValue($ccs); + break; + case ManiphestTransactionType::TYPE_PRIORITY: + $transaction->setNewValue($request->getInt('priority')); + break; + default: + throw new Exception('unknown action'); + } + + $editor = new ManiphestTransactionEditor(); + $editor->applyTransaction($task, $transaction); + + return id(new AphrontRedirectResponse()) + ->setURI('/T'.$task->getID()); + } + +} diff --git a/src/applications/maniphest/controller/transactionsave/__init__.php b/src/applications/maniphest/controller/transactionsave/__init__.php new file mode 100644 index 0000000000..abe8106998 --- /dev/null +++ b/src/applications/maniphest/controller/transactionsave/__init__.php @@ -0,0 +1,20 @@ +getTransactionType(); + + $new = $transaction->getNewValue(); + + $email_cc = $task->getCCPHIDs(); + + $email_to = array(); + $email_to[] = $task->getOwnerPHID(); + $email_to[] = $transaction->getAuthorPHID(); + + switch ($type) { + case ManiphestTransactionType::TYPE_NONE: + $old = null; + break; + case ManiphestTransactionType::TYPE_STATUS: + $old = $task->getStatus(); + break; + case ManiphestTransactionType::TYPE_OWNER: + $old = $task->getOwnerPHID(); + break; + case ManiphestTransactionType::TYPE_CCS: + $old = $task->getCCPHIDs(); + $new = array_unique(array_merge($old, $new)); + break; + case ManiphestTransactionType::TYPE_PRIORITY: + $old = $task->getPriority(); + break; + default: + throw new Exception('Unknown action type.'); + } + + if (($old !== null) && ($old == $new)) { + $transaction->setOldValue(null); + $transaction->setNewValue(null); + $transaction->setTransactionType(ManiphestTransactionType::TYPE_NONE); + } else { + switch ($type) { + case ManiphestTransactionType::TYPE_NONE: + break; + case ManiphestTransactionType::TYPE_STATUS: + $task->setStatus($new); + break; + case ManiphestTransactionType::TYPE_OWNER: + $task->setOwnerPHID($new); + break; + case ManiphestTransactionType::TYPE_CCS: + $task->setCCPHIDs($new); + break; + case ManiphestTransactionType::TYPE_PRIORITY: + $task->setPriority($new); + break; + default: + throw new Exception('Unknown action type.'); + } + + $transaction->setOldValue($old); + $transaction->setNewValue($new); + } + + $task->save(); + $transaction->setTaskID($task->getID()); + $transaction->save(); + + $email_to[] = $task->getOwnerPHID(); + $email_cc = array_merge($email_cc, $task->getCCPHIDs()); + + $this->sendEmail($task, $transaction, $email_to, $email_cc); + } + + private function sendEmail($task, $transaction, $email_to, $email_cc) { + $email_to = array_filter(array_unique($email_to)); + $email_cc = array_filter(array_unique($email_cc)); + + $transactions = array($transaction); + + $phids = array(); + foreach ($transactions as $transaction) { + foreach ($transaction->extractPHIDs() as $phid) { + $phids[$phid] = true; + } + } + $phids = array_keys($phids); + + $handles = id(new PhabricatorObjectHandleData($phids)) + ->loadHandles(); + + + $view = new ManiphestTransactionDetailView(); + $view->setTransaction($transaction); + $view->setHandles($handles); + + list($action, $body) = $view->renderForEmail($with_date = false); + + $task_uri = PhabricatorEnv::getURI('/T'.$task->getID()); + + $body .= + "\n\n". + "TASK DETAIL\n". + " ".$task_uri."\n"; + + id(new PhabricatorMetaMTAMail()) + ->setSubject( + '[Maniphest] '.$action.': T'.$task->getID().' '.$task->getTitle()) + ->setFrom($transaction->getAuthorPHID()) + ->addTos($email_to) + ->addCCs($email_cc) + ->setBody($body) + ->save(); + } +} diff --git a/src/applications/maniphest/editor/transaction/__init__.php b/src/applications/maniphest/editor/transaction/__init__.php new file mode 100644 index 0000000000..fd66574fbd --- /dev/null +++ b/src/applications/maniphest/editor/transaction/__init__.php @@ -0,0 +1,18 @@ + true, + self::CONFIG_SERIALIZATION => array( + 'ccPHIDs' => self::SERIALIZATION_JSON, + 'relatedPHIDs' => self::SERIALIZATION_JSON, + ), + ) + parent::getConfiguration(); + } + + public function generatePHID() { + return PhabricatorPHID::generateNewPHID('TASK'); + } + +} diff --git a/src/applications/maniphest/storage/task/__init__.php b/src/applications/maniphest/storage/task/__init__.php new file mode 100644 index 0000000000..2290666547 --- /dev/null +++ b/src/applications/maniphest/storage/task/__init__.php @@ -0,0 +1,13 @@ + array( + 'oldValue' => self::SERIALIZATION_JSON, + 'newValue' => self::SERIALIZATION_JSON, + ), + ) + parent::getConfiguration(); + } + + public function extractPHIDs() { + $phids = array(); + + switch ($this->getTransactionType()) { + case ManiphestTransactionType::TYPE_CCS: + foreach ($this->getOldValue() as $phid) { + $phids[] = $phid; + } + foreach ($this->getNewValue() as $phid) { + $phids[] = $phid; + } + break; + case ManiphestTransactionType::TYPE_OWNER: + $phids[] = $this->getOldValue(); + $phids[] = $this->getNewValue(); + break; + } + + $phids[] = $this->getAuthorPHID(); + + return $phids; + } + +} diff --git a/src/applications/maniphest/storage/transaction/__init__.php b/src/applications/maniphest/storage/transaction/__init__.php new file mode 100644 index 0000000000..c4e475868c --- /dev/null +++ b/src/applications/maniphest/storage/transaction/__init__.php @@ -0,0 +1,13 @@ +tasks = $tasks; + return $this; + } + + public function setHandles(array $handles) { + $this->handles = $handles; + return $this; + } + + public function render() { + + $views = array(); + foreach ($this->tasks as $task) { + $view = new ManiphestTaskSummaryView($task); + $view->setTask($task); + $view->setHandles($this->handles); + $views[] = $view->render(); + } + + return + '
'. + implode("\n", $views). + '
'; + } + +} diff --git a/src/applications/maniphest/view/tasklist/__init__.php b/src/applications/maniphest/view/tasklist/__init__.php new file mode 100644 index 0000000000..5a35a49ef4 --- /dev/null +++ b/src/applications/maniphest/view/tasklist/__init__.php @@ -0,0 +1,13 @@ +task = $task; + return $this; + } + + public function setHandles(array $handles) { + $this->handles = $handles; + return $this; + } + + public function render() { + $task = $this->task; + $handles = $this->handles; + + require_celerity_resource('maniphest-task-summary-css'); + + return + ''. + ''. + ''. + ''. + ''. + ''. + '
'. + 'T'.$task->getID(). + ''. + $handles[$task->getOwnerPHID()]->renderLink(). + ''. + phutil_render_tag( + 'a', + array( + 'href' => '/T'.$task->getID(), + ), + phutil_escape_html($task->getTitle())). + ''. + ManiphestTaskPriority::getTaskPriorityName($task->getPriority()). + ''. + phabricator_format_timestamp($task->getDateModified()). + '
'; + } + +} diff --git a/src/applications/maniphest/view/tasksummary/__init__.php b/src/applications/maniphest/view/tasksummary/__init__.php new file mode 100644 index 0000000000..8c6a3ec782 --- /dev/null +++ b/src/applications/maniphest/view/tasksummary/__init__.php @@ -0,0 +1,17 @@ +transaction = $transaction; + return $this; + } + + public function setHandles(array $handles) { + $this->handles = $handles; + return $this; + } + + public function setMarkupEngine(PhutilMarkupEngine $engine) { + $this->markupEngine = $engine; + return $this; + } + + public function renderForEmail($with_date) { + $this->forEmail = true; + list ($verb, $desc, $classes) = $this->describeAction(); + + $transaction = $this->transaction; + $author = $this->renderHandles(array($transaction->getAuthorPHID())); + + $desc = $author.' '.$desc; + if ($with_date) { + $desc = 'On '.date('M jS \a\t g:i A', $transaction->getDateCreated()). + ', '.$desc; + } + + $comments = $transaction->getComments(); + if (strlen(trim($comments))) { + $desc = $desc.":\n".$comments; + } else { + $desc = $desc."."; + } + + $this->forEmail = false; + return array($verb, $desc); + } + + public function render() { + $transaction = $this->transaction; + $handles = $this->handles; + + require_celerity_resource('maniphest-transaction-detail-css'); + + $author = $this->handles[$transaction->getAuthorPHID()]; + + $comments = $transaction->getCache(); + if (!strlen($comments)) { + $comments = $transaction->getComments(); + if (strlen($comments)) { + $comments = $this->markupEngine->markupText($comments); + $transaction->setCache($comments); + $transaction->save(); + } + } + + list($verb, $desc, $classes) = $this->describeAction( + $transaction->getComments()); + + $more_classes = implode(' ', $classes); + + if (strlen(trim($transaction->getComments()))) { + $punc = ':'; + } else { + $punc = '.'; + } + + return phutil_render_tag( + 'div', + array( + 'class' => "maniphest-transaction-detail-container", + 'style' => "background-image: url('".$author->getImageURI()."')", + ), + '
'. + '
'. + '
'. + phabricator_format_timestamp($transaction->getDateCreated()). + '
'. + ''. + $author->renderLink(). + ' '. + $desc. + $punc. + ''. + '
'. + '
'. + $comments. + '
'. + '
'); + } + + private function describeAction() { + $verb = null; + $desc = null; + $classes = array(); + + $handles = $this->handles; + + $transaction = $this->transaction; + $type = $transaction->getTransactionType(); + $author_phid = $transaction->getAuthorPHID(); + $new = $transaction->getNewValue(); + $old = $transaction->getOldValue(); + switch ($type) { + case ManiphestTransactionType::TYPE_NONE: + $verb = 'Commented On'; + $desc = 'added a comment'; + break; + case ManiphestTransactionType::TYPE_OWNER: + if ($transaction->getAuthorPHID() == $new) { + $verb = 'Claimed'; + $desc = 'claimed this task'; + } else if (!$new) { + $verb = 'Up For Grabs'; + $desc = 'placed this task up for grabs'; + } else if (!$old) { + $verb = 'Assigned'; + $desc = 'assigned this task to '.$this->renderHandles(array($new)); + } else { + $verb = 'Reassigned'; + $desc = 'reassigned this task from '. + $this->renderHandles(array($old)). + ' to '. + $this->renderHandles(array($new)); + } + break; + case ManiphestTransactionType::TYPE_CCS: + $added = array_diff($new, $old); + $removed = array_diff($old, $new); + if ($added && !$removed) { + $verb = 'Added CC'; + if (count($added) == 1) { + $desc = 'added '.$this->renderHandles($added).' to CC'; + } else { + $desc = 'added CCs: '.$this->renderHandles($added); + } + } else if ($removed && !$added) { + $verb = 'Removed CC'; + if (count($removed) == 1) { + $desc = 'removed '.$this->renderHandles($removed).' from CC'; + } else { + $desc = 'removed CCs: '.$this->renderHandles($removed); + } + } else { + $verb = 'Changed CC'; + $desc = 'changed CCs, added: '.$this->renderHandles($added).'; '. + 'removed: '.$this->renderHandles($removed); + } + break; + case ManiphestTransactionType::TYPE_STATUS: + if ($new == ManiphestTaskStatus::STATUS_OPEN) { + if ($old) { + $verb = 'Reopened'; + $desc = 'reopened this task'; + } else { + $verb = 'Created'; + $desc = 'created this task'; + } + } else if ($new == ManiphestTaskStatus::STATUS_CLOSED_SPITE) { + $verb = 'Spited'; + $desc = 'closed this task out of spite'; + } else { + $verb = 'Closed'; + $full = idx(ManiphestTaskStatus::getTaskStatusMap(), $new, '???'); + $desc = 'closed this task as "'.$full.'"'; + } + break; + case ManiphestTransactionType::TYPE_PRIORITY: + $old_name = ManiphestTaskPriority::getTaskPriorityName($old); + $new_name = ManiphestTaskPriority::getTaskPriorityName($new); + + if ($old == ManiphestTaskPriority::PRIORITY_TRIAGE) { + $verb = 'Triaged'; + $desc = 'triaged this task as "'.$new_name.'" priority'; + } else if ($old > $new) { + $verb = 'Lowered Priority'; + $desc = 'lowered the priority of this task from "'.$old_name.'" to '. + '"'.$new_name.'"'; + } else { + $verb = 'Raised Priority'; + $desc = 'raised the priority of this task from "'.$old_name.'" to '. + '"'.$new_name.'"'; + } + break; + default: + return ' brazenly '.$type."'d"; + } + + return array($verb, $desc, $classes); + } + + private function renderHandles($phids) { + $links = array(); + foreach ($phids as $phid) { + if ($this->forEmail) { + $links[] = $this->handles[$phid]->getName(); + } else { + $links[] = $this->handles[$phid]->renderLink(); + } + } + return implode(', ', $links); + } + +} diff --git a/src/applications/maniphest/view/transactiondetail/__init__.php b/src/applications/maniphest/view/transactiondetail/__init__.php new file mode 100644 index 0000000000..9f08794372 --- /dev/null +++ b/src/applications/maniphest/view/transactiondetail/__init__.php @@ -0,0 +1,20 @@ +transactions = $transactions; + return $this; + } + + public function setHandles(array $handles) { + $this->handles = $handles; + return $this; + } + + public function setUser(PhabricatorUser $user) { + $this->user = $user; + return $this; + } + + public function setMarkupEngine(PhutilMarkupEngine $engine) { + $this->markupEngine = $engine; + return $this; + } + + public function render() { + + $views = array(); + foreach ($this->transactions as $transaction) { + $view = new ManiphestTransactionDetailView($transaction); + $view->setTransaction($transaction); + $view->setHandles($this->handles); + $view->setMarkupEngine($this->markupEngine); + $views[] = $view->render(); + } + + return + '
'. + implode("\n", $views). + '
'; + } + +} diff --git a/src/applications/maniphest/view/transactionlist/__init__.php b/src/applications/maniphest/view/transactionlist/__init__.php new file mode 100644 index 0000000000..8c06249c94 --- /dev/null +++ b/src/applications/maniphest/view/transactionlist/__init__.php @@ -0,0 +1,13 @@ +datasource = $datasource; @@ -35,6 +36,11 @@ class AphrontFormTokenizerControl extends AphrontFormControl { return 'aphront-form-control-tokenizer'; } + public function setLimit($limit) { + $this->limit = $limit; + return $this; + } + protected function renderInput() { require_celerity_resource('aphront-tokenizer-control-css'); require_celerity_resource('javelin-typeahead-dev'); @@ -70,6 +76,7 @@ class AphrontFormTokenizerControl extends AphrontFormControl { 'id' => $id, 'src' => $this->datasource, 'value' => $values, + 'limit' => $this->limit, )); } @@ -88,7 +95,6 @@ class AphrontFormTokenizerControl extends AphrontFormControl { array( 'type' => 'text', 'name' => $this->getName(), - 'value' => $this->getValue(), 'disabled' => $this->getDisabled() ? 'disabled' : null, )); } diff --git a/src/view/page/standard/PhabricatorStandardPageView.php b/src/view/page/standard/PhabricatorStandardPageView.php index d3732da0f9..ef0ab6fcc4 100755 --- a/src/view/page/standard/PhabricatorStandardPageView.php +++ b/src/view/page/standard/PhabricatorStandardPageView.php @@ -202,7 +202,6 @@ class PhabricatorStandardPageView extends AphrontPageView { } $foot_links = implode(' · ', $foot_links); - return ($console ? '' : null). '
'. diff --git a/webroot/rsrc/css/application/maniphest/task-detail.css b/webroot/rsrc/css/application/maniphest/task-detail.css new file mode 100644 index 0000000000..602840d9e6 --- /dev/null +++ b/webroot/rsrc/css/application/maniphest/task-detail.css @@ -0,0 +1,44 @@ +/** + * @provides mainphest-task-detail-css + */ + +.maniphest-panel { + margin: .5em 2em .25em; + border: 1px solid #666622; + background: #efefdf; + padding: 15px 20px; + font-size: 13px; +} + +.maniphest-panel h1 { + border-bottom: 1px solid #aaaa99; + padding-bottom: 8px; + margin-bottom: 8px; +} + + +.maniphest-task-properties { + font-size: 12px; + width: 100%; +} + +.maniphest-task-properties tt { + letter-spacing: 1.1px; +} + +.maniphest-task-properties th { + font-weight: bold; + width: 100px; + text-align: right; + padding: 3px 4px 3px 3px; + color: #333333; + white-space: nowrap; +} + +.maniphest-task-properties td { + padding: 3px 2px; +} + +.maniphest-task-detail-core { + margin-right: 265px; +} diff --git a/webroot/rsrc/css/application/maniphest/task-summary.css b/webroot/rsrc/css/application/maniphest/task-summary.css new file mode 100644 index 0000000000..30630cfc5e --- /dev/null +++ b/webroot/rsrc/css/application/maniphest/task-summary.css @@ -0,0 +1,43 @@ +/** + * @provides maniphest-task-summary-css + */ + +.maniphest-task-summary { + border: 1px solid #aaaaaa; + width: 100%; + margin: 2px 0; + border-collapse: separate; + border-spacing: 1px; +} + +.maniphest-task-summary td { + padding: 4px 1%; + background: #dfdfdf; + white-space: nowrap; +} + +.maniphest-task-summary td.maniphest-task-number { + font-weight: bold; + color: #444444; + width: 8%; +} + +.maniphest-task-summary td.maniphest-task-owner { + width: 11%; +} + +.maniphest-task-summary td.maniphest-task-name { + overflow: hidden; + font-weight: bold; + width: 49%; +} + +.maniphest-task-summary td.maniphest-task-priority { + text-align: center; + width: 11%; +} + +.maniphest-task-summary td.maniphest-task-updated { + text-align: center; + width: 11%; +} diff --git a/webroot/rsrc/css/application/maniphest/transaction-detail.css b/webroot/rsrc/css/application/maniphest/transaction-detail.css new file mode 100644 index 0000000000..46a23e52ff --- /dev/null +++ b/webroot/rsrc/css/application/maniphest/transaction-detail.css @@ -0,0 +1,23 @@ +/** + * @provides maniphest-transaction-detail-css + */ + +.maniphest-transaction-detail-container { + margin: 2px 1em 3px; + background: 0px 0px no-repeat; + min-height: 50px; +} + +.maniphest-transaction-detail-view { + margin-left: 54px; + padding: 4px 1em; + border: 1px solid #dddddd; + background: #f6f6f6; + min-height: 40px; +} + +.maniphest-transaction-timestamp { + float: right; + font-size: 11px; + color: #666666; +} diff --git a/webroot/rsrc/js/application/maniphest/behavior-transaction-controls.js b/webroot/rsrc/js/application/maniphest/behavior-transaction-controls.js new file mode 100644 index 0000000000..f384c2aadb --- /dev/null +++ b/webroot/rsrc/js/application/maniphest/behavior-transaction-controls.js @@ -0,0 +1,81 @@ +/** + * @provides javelin-behavior-maniphest-transaction-controls + * @requires javelin-lib-dev + */ + +JX.behavior('maniphest-transaction-controls', function(config) { + + var tokenizers = {}; + + for (var k in config.tokenizers) { + var tconfig = config.tokenizers[k]; + var root = JX.$(tconfig.id); + var datasource = new JX.TypeaheadPreloadedSource(tconfig.src); + + var typeahead = new JX.Typeahead(root); + typeahead.setDatasource(datasource); + + var tokenizer = new JX.Tokenizer(root); + tokenizer.setTypeahead(typeahead); + + if (tconfig.limit) { + tokenizer.setLimit(tconfig.limit); + } + + tokenizer.start(); + + + if (tconfig.value) { + for (var jj in tconfig.value) { + tokenizer.addToken(jj, tconfig.value[jj]); + } + } + + tokenizers[k] = tokenizer; + } + + JX.DOM.listen( + JX.$(config.select), + 'change', + null, + function(e) { + for (var k in config.controlMap) { + if (k == JX.$(config.select).value) { + JX.DOM.show(JX.$(config.controlMap[k])); + if (tokenizers[k]) { + tokenizers[k].refresh(); + } + } else { + JX.DOM.hide(JX.$(config.controlMap[k])); + } + } + }); + +/* + + var root = JX.$(config.tokenizer); + var datasource = new JX.TypeaheadPreloadedSource(config.src); + + var typeahead = new JX.Typeahead(root); + typeahead.setDatasource(datasource); + + var tokenizer = new JX.Tokenizer(root); + tokenizer.setTypeahead(typeahead); + tokenizer.start(); + + JX.DOM.listen( + JX.$(config.select), + 'change', + null, + function(e) { + if (JX.$(config.select).value == 'add_reviewers') { + JX.DOM.show(JX.$(config.row)); + tokenizer.refresh(); + } else { + JX.DOM.hide(JX.$(config.row)); + } + }); + +*/ +}); +