Skip to content

Commit

Permalink
v2.0.0
Browse files Browse the repository at this point in the history
  • Loading branch information
motla committed Nov 22, 2021
1 parent 6ee6816 commit 5555679
Show file tree
Hide file tree
Showing 28 changed files with 782 additions and 1,720 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.DS_Store
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
## v2.0.0
Phixator 2 initial release
1 change: 1 addition & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
MIT License

Copyright (c) 2020 Romain Lamothe
Copyright (c) 2017 4xxi

Permission is hereby granted, free of charge, to any person obtaining a copy
Expand Down
30 changes: 27 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,29 @@
Phabricator extensions collection.
# :hourglass: Phixator 2

Run arcanist/bin/arc liberate phabricator/src according to manual https://secure.phabricator.com/book/phabcontrib/article/adding_new_classes/ after add new extension.
Task Work Log extension for Phabricator / Phorge

Updated September 15 2020
## Screenshots
| <img src="screenshot/phixator-list.png" height="220"> | <img src="screenshot/phixator-menu.png" height="220"> |
| - | - |

## Features

- Log hours of work on Maniphest tasks with the spent time, description and date of the work
- Show the list of work logs, filtered by users, projects, spaces, tasks or time periods
- Embed work log queries in Dashboards, with choice of displayed information
- Export your query results in .csv, .json, .txt
- French translation

## Installation

1. Clone or unzip this repository in the extensions folder of Phabricator in your server (`phabricator/src/extensions`)
2. Browse the application in Phabricator (`your-url.com/phixator`)
3. Subscribe/Watch this repository for new releases and bug fixes

## To do
- [ ] Time budgets

## Credits
Project originated by [4xxi](https://github.com/4xxi), updated by [ssnd292](https://github.com/ssnd292), rewritten by [motla](https://github.com/motla).

[MIT License](LICENSE)
49 changes: 49 additions & 0 deletions application/PhabricatorPhixatorApplication.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

class PhabricatorPhixatorApplication extends PhabricatorApplication {

public function getName() {
return pht('Phixator');
}

public function getBaseURI() {
return '/phixator/';
}

public function getShortDescription() {
return pht('Log work time on tasks');
}

public function getIcon() {
return 'fa-hourglass-half';
}

public function getTitleGlyph() {
return "\xE2\x8F\xB3";
}

public function getApplicationGroup() {
return self::GROUP_UTILITIES;
}

public function getApplicationOrder() {
return 0.110;
}

public function getEventListeners() {
return [new PhixatorUIEventListener()];
}

public function getRoutes() {
return [
'/phixator/' => [
$this->getQueryRoutePattern() => 'PhixatorWorkLogListController',
'log/' => [
'edit/(?P<transactionPHID>[^/]+)/' => 'PhixatorWorkLogEditController',
'delete/(?P<transactionPHID>[^/]+)/' => 'PhixatorWorkLogDeleteController',
'(?P<taskPHID>[^/]+)/' => 'PhixatorWorkLogEditController',
]
],
];
}
}
37 changes: 37 additions & 0 deletions controller/PhixatorWorkLogDeleteController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php
/**
* Displays and manage time log deletion
*/
final class PhixatorWorkLogDeleteController extends PhabricatorController {

public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$transactionPHID = $request->getURIData('transactionPHID');
$return_uri = $request->getStr('return_to') ?? '/phixator';

// Get the transaction to delete, check that the viewer can edit it
$transaction = (new PhixatorWorkLogQuery())
->setViewer($viewer)
->withPHIDs([$transactionPHID])
->requireCapabilities([PhabricatorPolicyCapability::CAN_EDIT])
->executeOne();
if (!$transaction) return new Aphront404Response();

// If the delete confirmation was submitted, "delete" the transaction (just rename its type)
if ($request->isFormPost()) {
$transaction->setTransactionType(ManiphestTaskWorkLogTransaction::TRANSACTIONTYPE.":deleted");
$transaction->save();
return (new AphrontRedirectResponse())->setURI($return_uri);
}

// Else display delete confirmation to the user
$dialog = (new AphrontDialogView())
->setViewer($viewer)
->setTitle(pht('Delete Time Log?'))
->appendChild(pht('Are you sure to delete this time log?'))
->addSubmitButton(pht('Delete'))
->addCancelButton($return_uri);
return (new AphrontDialogResponse())->setDialog($dialog);
}

}
95 changes: 95 additions & 0 deletions controller/PhixatorWorkLogEditController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php
/**
* Base class to display and manage the forms to create or edit work time logs
*/
class PhixatorWorkLogEditController extends PhabricatorController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$taskPHID = $request->getURIData('taskPHID');
$transactionPHID = $request->getURIData('transactionPHID');
$is_edit = (bool)$transactionPHID;
$return_uri = $request->getStr('return_to');
if(!$return_uri) $return_uri = '/phixator';

// Declare default parameters
$params = array('minutes' => 0, 'description' => '', 'started' => time());

// In case of an edit, check if the viewer can edit this time log, then get time log values
if($is_edit) {
$transaction = (new PhixatorWorkLogQuery())
->setViewer($viewer)
->withPHIDs([$transactionPHID])
->requireCapabilities([PhabricatorPolicyCapability::CAN_EDIT])
->executeOne();
if(!$transaction) return new Aphront404Response();
$taskPHID = $transaction->getObjectPHID();
$params = $transaction->getNewValue();
}

// Get the task and check if the viewer can edit the task
$task = (new ManiphestTaskQuery())->setViewer($viewer)->withPHIDs([$taskPHID])->needSubscriberPHIDs(true)->executeOne();
if(!$task) return new Aphront404Response();
PhabricatorPolicyFilter::requireCapability($viewer, $task, PhabricatorPolicyCapability::CAN_EDIT);

// If the form was submitted and is correct: validate it and create/edit the transaction
$formErrors = [];
if($request->isDialogFormPost()) {
// Get fields submitted by the user
$work_time = strtolower(trim($request->getStr('work_time')));
$params['minutes'] = PhixatorUtil::timeStringToMinutes($work_time);
$params['description'] = trim($request->getStr('description'));
$timestamp = AphrontFormDateControlValue::newFromRequest($request, 'started');
if(!$timestamp->isValid()) $formErrors[] = pht('Please choose a valid date');
$params['started'] = $timestamp->getEpoch();
if(!PhixatorUtil::isTimeStringFormatCorrect($work_time)) $formErrors[] = pht('Work time is incorrect. Allowed format is: 1h 1m');
if(!$formErrors) {
// Edit time log
if($is_edit) {
$transaction->setNewValue($params)->setOldValue($params['started']); // set oldValue to 'started' date for query ordering purpose
$transaction->save();
return (new AphrontRedirectResponse())->setURI($return_uri);
}
// Or create new time log
else {
$transaction = (new ManiphestTransaction())->setTransactionType(ManiphestTaskWorkLogTransaction::TRANSACTIONTYPE)->setNewValue($params);
$editor = (new ManiphestTransactionEditor())->setActor($viewer)->setContentSource(PhabricatorContentSource::newFromRequest($request))->setContinueOnNoEffect(true);
$editor->applyTransactions($task, [$transaction]);
return (new AphrontRedirectResponse())->setURI('/'.$task->getMonogram());
}
}
}

// If no submitted form or the submitted form was not correct, display the new/edit form
$dialog = $this->newDialog()
->setTitle($is_edit ? pht('Edit Work Time Log') : pht('Log Work Time'))
->setWidth(AphrontDialogView::WIDTH_FORM)
->setErrors($formErrors);
$form = new PHUIFormLayoutView();
$form->appendChild((new AphrontFormStaticControl())
->setLabel(pht('Task'))
->setValue($task->getTitle()));
$form->appendChild((new AphrontFormTextControl())
->setViewer($viewer)
->setName('work_time')
->setLabel(pht('Work Time'))
->setPlaceholder('1h 30m')
->setAutofocus(true)
->setValue(PhixatorUtil::minutesToTimeString($params['minutes'])));
$form->appendChild((new AphrontFormTextAreaControl())
->setViewer($viewer)
->setName('description')
->setLabel(pht('Work Description'))
->setValue($params['description']));
$form->appendChild((new AphrontFormDateControl())
->setViewer($viewer)
->setName('started')
->setLabel(pht('Work Date'))
->setIsTimeDisabled(true)
->setValue($params['started']));
$dialog->appendChild($form);
$dialog->addHiddenInput('return_to', $return_uri);
$dialog->addCancelButton($return_uri, pht('Close'));
$dialog->addSubmitButton($is_edit ? pht('Edit') : pht('Add'));
return $dialog;
}
}
37 changes: 37 additions & 0 deletions controller/PhixatorWorkLogListController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

final class PhixatorWorkLogListController extends PhabricatorController {

private $queryKey;

public function shouldAllowPublic() {
return true;
}

public function willProcessRequest(array $data) {
$this->queryKey = idx($data, 'queryKey');
}

public function getQueryKey() {
return $this->queryKey;
}

public function processRequest() {
$controller = (new PhabricatorApplicationSearchController())
->setQueryKey($this->queryKey)
->setSearchEngine(new PhixatorWorkLogSearchEngine())
->setNavigation($this->buildSideNavView());

return $this->delegateToController($controller);
}

protected function buildSideNavView() {
$user = $this->getRequest()->getUser();
$nav = new AphrontSideNavFilterView();
$nav->setBaseURI(new PhutilURI($this->getApplicationURI()));
(new PhixatorWorkLogSearchEngine())->setViewer($user)->addNavigationItems($nav->getMenu());
$nav->selectFilter(null);
return $nav;
}

}
64 changes: 64 additions & 0 deletions engineextension/PhixatorCurtainExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php
/**
* Displays the total work time summary in Maniphest tasks curtain
*/
class PhixatorCurtainExtension extends PHUICurtainExtension {
const EXTENSIONKEY = 'phixator.log_history';

public function shouldEnableForObject($object) {
return ($object instanceof ManiphestTask);
}

public function getExtensionApplication() {
return new PhabricatorPhixatorApplication();
}

public function buildCurtainPanel($object) {
// Create the curtain panel and its status view
$panel = $this->newPanel()->setHeaderText(pht('Work Time Summary'))->setOrder(40000);
$status_view = new PHUIStatusListView();
$panel->appendChild($status_view);

// Get the transactions corresponding to the task currently displaying the curtain block
$transactions = (new ManiphestTransactionQuery())
->setViewer($this->getViewer())
->withObjectPHIDs([$object->getPHID()])
->withTransactionTypes([ManiphestTaskWorkLogTransaction::TRANSACTIONTYPE])
->needComments(true)
->execute();
if(!$transactions) return;
$transactions = array_reverse($transactions);

// Compute the contributing user list with their corresponding work time
$summarySpendByUser = [];
foreach ($transactions as $transaction) {
$authorPHID = $transaction->getAuthorPHID();
$minutes = intval($transaction->getNewValue()['minutes'] ?? 0);
if (!isset($summarySpendByUser[$authorPHID])) $summarySpendByUser[$authorPHID] = 0;
$summarySpendByUser[$authorPHID] += $minutes;
}

// Display all contributing users with their cumulated work times
foreach ($summarySpendByUser as $authorPHID => $spendMinutes) {
$author = (new PhabricatorPeopleQuery())->setViewer($this->getViewer())->withPHIDs([$authorPHID])->executeOne();
if(!$author) continue;
$authorUrl = (new PhabricatorObjectHandle())
->setType(phid_get_type($authorPHID))
->setPHID($authorPHID)
->setName($author->getFullName())
->setURI('/p/'.$author->getUsername().'/')
->renderLink();
$item = (new PHUIStatusItemView())
->setIcon('fa-hourglass-half')
->setTarget(pht('<b>%s</b> by %s', PhixatorUtil::minutesToTimeString($spendMinutes), $authorUrl));
$status_view->addItem($item);
}

// Display link to get the work logs list for this task
$status_view->addItem((new PHUIStatusItemView())->setTarget(
(new PhabricatorObjectHandle())->setName(pht('Show details...'))->setURI('/phixator/query/advanced/?taskIDs='.$object->getMonogram())->renderLink())
);

return $panel;
}
}
37 changes: 37 additions & 0 deletions event/PhixatorUIEventListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

class PhixatorUIEventListener extends PhabricatorEventListener {
public function register() {
$this->listen(PhabricatorEventType::TYPE_UI_DIDRENDERACTIONS);
}

public function handleEvent(PhutilEvent $event) {
switch ($event->getType()) {
case PhabricatorEventType::TYPE_UI_DIDRENDERACTIONS:
$this->handleActionEvent($event);
break;
}
}

private function handleActionEvent(PhabricatorEvent $event) {
$viewer = $event->getUser();
$object = $event->getValue('object');

if (!$object || !$object->getPHID()) return;
if (!($object instanceof ManiphestTask)) return;
if (!$this->canUseApplication($event->getUser())) return;

$actionView = (new PhabricatorActionView())
->setName(pht('Log Work Time'))
->setIcon('fa-hourglass-half')
->setWorkflow(true)
->setHref('/phixator/log/' . $object->getPHID() . '/');

$can_edit = PhabricatorPolicyFilter::hasCapability($viewer, $object, PhabricatorPolicyCapability::CAN_EDIT);
if (!$viewer->isLoggedIn() || !$can_edit) {
$actionView->setDisabled(true);
}

$this->addActionMenuItems($event, $actionView);
}
}
Loading

0 comments on commit 5555679

Please sign in to comment.