Unverified Commit 402e00ad authored by Curtis Conard's avatar Curtis Conard Committed by GitHub
Browse files

Kanban Phase 2 (Part 1) (#7928)



* Some ES6 migration

* Show card content preview on card title hover

* Open card edit form in dialog

* Card actions (Just delete for now)

* Add icon to cards to identify type

* Add Go To card action to open full form

Opens the full item form as it previously did when clicking the card title.
While the current behavior of opening the main form in a dialog when clicking the card title may be enough for a lot of cases, it is nice to have the option to go to the full form.

* Implement Kanban rights class

* Fix lint + Final commit for this PR

* Apply suggestions from code review
Co-authored-by: default avatarCédric Anne <cedric.anne@gmail.com>

* Replace some Kanban rights references

* Dark mode fixes

* Convert Goto action to link

* Fix clickable zone on links and links styling

* Update ajax/kanban.php
Co-authored-by: default avatarAlexandre Delaunay <delaunay.alexandre@gmail.com>

* Remove slash before query params
Co-authored-by: default avatarCédric Anne <cedric.anne@gmail.com>
Co-authored-by: default avatarAlexandre Delaunay <delaunay.alexandre@gmail.com>
parent d52a27ba
......@@ -54,7 +54,7 @@ if (!isset($_REQUEST['action'])) {
}
$action = $_REQUEST['action'];
$nonkanban_actions = ['update', 'bulk_add_item', 'add_item', 'move_item'];
$nonkanban_actions = ['update', 'bulk_add_item', 'add_item', 'move_item', 'show_card_edit_form', 'delete_item'];
if (isset($_REQUEST['itemtype'])) {
$traits = class_uses($_REQUEST['itemtype'], true);
if (!in_array($_REQUEST['action'], $nonkanban_actions) && (!$traits || !in_array(Kanban::class, $traits, true))) {
......@@ -93,10 +93,18 @@ if (isset($itemtype)) {
return;
}
}
if (in_array($action, ['delete_item'])) {
$maybe_deleted = $item->maybeDeleted();
if (($maybe_deleted && !$item::canDelete()) && (!$maybe_deleted && $item::canPurge())) {
// Missing rights
http_response_code(403);
return;
}
}
}
// Helper to check required parameters
$checkParams = function($required) {
$checkParams = static function($required) {
foreach ($required as $param) {
if (!isset($_REQUEST[$param])) {
Toolbox::logError("Missing $param parameter");
......@@ -107,20 +115,20 @@ $checkParams = function($required) {
};
// Action Processing
if ($_REQUEST['action'] == 'update') {
if ($_REQUEST['action'] === 'update') {
$checkParams(['column_field', 'column_value']);
// Update project or task based on changes made in the Kanban
$item->update([
'id' => $_REQUEST['items_id'],
$_REQUEST['column_field'] => $_REQUEST['column_value']
]);
} else if ($_REQUEST['action'] == 'add_item') {
} else if ($_REQUEST['action'] === 'add_item') {
$checkParams(['inputs']);
$item = new $itemtype();
$inputs = [];
parse_str($_REQUEST['inputs'], $inputs);
$item->add($inputs);
} else if ($_REQUEST['action'] == 'bulk_add_item') {
} else if ($_REQUEST['action'] === 'bulk_add_item') {
$checkParams(['inputs']);
$item = new $itemtype();
$inputs = [];
......@@ -136,7 +144,7 @@ if ($_REQUEST['action'] == 'update') {
}
}
}
} else if ($_REQUEST['action'] == 'move_item') {
} else if ($_REQUEST['action'] === 'move_item') {
$checkParams(['card', 'column', 'position', 'kanban']);
/** @var Kanban|CommonDBTM $kanban */
$kanban = new $_REQUEST['kanban']['itemtype'];
......@@ -145,35 +153,35 @@ if ($_REQUEST['action'] == 'update') {
Item_Kanban::moveCard($_REQUEST['kanban']['itemtype'], $_REQUEST['kanban']['items_id'],
$_REQUEST['card'], $_REQUEST['column'], $_REQUEST['position']);
}
} else if ($_REQUEST['action'] == 'show_column') {
} else if ($_REQUEST['action'] === 'show_column') {
$checkParams(['column', 'kanban']);
Item_Kanban::showColumn($_REQUEST['kanban']['itemtype'], $_REQUEST['kanban']['items_id'], $_REQUEST['column']);
} else if ($_REQUEST['action'] == 'hide_column') {
} else if ($_REQUEST['action'] === 'hide_column') {
$checkParams(['column', 'kanban']);
Item_Kanban::hideColumn($_REQUEST['kanban']['itemtype'], $_REQUEST['kanban']['items_id'], $_REQUEST['column']);
} else if ($_REQUEST['action'] == 'collapse_column') {
} else if ($_REQUEST['action'] === 'collapse_column') {
$checkParams(['column', 'kanban']);
Item_Kanban::collapseColumn($_REQUEST['kanban']['itemtype'], $_REQUEST['kanban']['items_id'], $_REQUEST['column']);
} else if ($_REQUEST['action'] == 'expand_column') {
} else if ($_REQUEST['action'] === 'expand_column') {
$checkParams(['column', 'kanban']);
Item_Kanban::expandColumn($_REQUEST['kanban']['itemtype'], $_REQUEST['kanban']['items_id'], $_REQUEST['column']);
} else if ($_REQUEST['action'] == 'move_column') {
} else if ($_REQUEST['action'] === 'move_column') {
$checkParams(['column', 'kanban', 'position']);
Item_Kanban::moveColumn($_REQUEST['kanban']['itemtype'], $_REQUEST['kanban']['items_id'],
$_REQUEST['column'], $_REQUEST['position']);
} else if ($_REQUEST['action'] == 'refresh') {
} else if ($_REQUEST['action'] === 'refresh') {
$checkParams(['column_field']);
// Get all columns to refresh the kanban
header("Content-Type: application/json; charset=UTF-8", true);
$force_columns = Item_Kanban::getAllShownColumns($itemtype, $_REQUEST['items_id']);
$columns = $itemtype::getKanbanColumns($_REQUEST['items_id'], $_REQUEST['column_field'], $force_columns, true);
echo json_encode($columns, JSON_FORCE_OBJECT);
} else if ($_REQUEST['action'] == 'get_switcher_dropdown') {
} else if ($_REQUEST['action'] === 'get_switcher_dropdown') {
$values = $itemtype::getAllForKanban();
Dropdown::showFromArray('kanban-board-switcher', $values, [
'value' => isset($_REQUEST['items_id']) ? $_REQUEST['items_id'] : ''
'value' => $_REQUEST['items_id'] ?? ''
]);
} else if ($_REQUEST['action'] == 'get_url') {
} else if ($_REQUEST['action'] === 'get_url') {
$checkParams(['items_id']);
if ($_REQUEST['items_id'] == -1) {
echo $itemtype::getFormURL(true).'?showglobalkanban=1';
......@@ -182,13 +190,13 @@ if ($_REQUEST['action'] == 'update') {
$item->getFromDB($_REQUEST['items_id']);
$tabs = $item->defineTabs();
$tab_id = array_search(__('Kanban'), $tabs);
if (is_null($tab_id) || false === $tab_id) {
if (false === $tab_id || is_null($tab_id)) {
Toolbox::logError("Itemtype does not have a Kanban tab!");
http_response_code(400);
return;
}
echo $itemtype::getFormURLWithID($_REQUEST['items_id'], true)."&forcetab={$tab_id}";
} else if ($_REQUEST['action'] == 'create_column') {
} else if ($_REQUEST['action'] === 'create_column') {
$checkParams(['column_field', 'items_id', 'column_name']);
$column_field = $_REQUEST['column_field'];
$column_itemtype = getItemtypeForForeignKeyField($column_field);
......@@ -205,10 +213,10 @@ if ($_REQUEST['action'] == 'update') {
header("Content-Type: application/json; charset=UTF-8", true);
$column = $itemtype::getKanbanColumns($_REQUEST['items_id'], $column_field, [$column_id]);
echo json_encode($column);
} else if ($_REQUEST['action'] == 'save_column_state') {
} else if ($_REQUEST['action'] === 'save_column_state') {
$checkParams(['items_id', 'state']);
Item_Kanban::saveStateForItem($_REQUEST['itemtype'], $_REQUEST['items_id'], $_REQUEST['state']);
} else if ($_REQUEST['action'] == 'load_column_state') {
} else if ($_REQUEST['action'] === 'load_column_state') {
$checkParams(['items_id', 'last_load']);
header("Content-Type: application/json; charset=UTF-8", true);
$response = [
......@@ -216,13 +224,33 @@ if ($_REQUEST['action'] == 'update') {
'timestamp' => $_SESSION['glpi_currenttime']
];
echo json_encode($response, JSON_FORCE_OBJECT);
} else if ($_REQUEST['action'] == 'list_columns') {
} else if ($_REQUEST['action'] === 'list_columns') {
$checkParams(['column_field']);
header("Content-Type: application/json; charset=UTF-8", true);
echo json_encode($itemtype::getAllKanbanColumns($_REQUEST['column_field']));
} else if ($_REQUEST['action'] == 'get_column') {
} else if ($_REQUEST['action'] === 'get_column') {
$checkParams(['column_id', 'column_field', 'items_id']);
header("Content-Type: application/json; charset=UTF-8", true);
$column = $itemtype::getKanbanColumns($_REQUEST['items_id'], $_REQUEST['column_field'], [$_REQUEST['column_id']]);
echo json_encode($column, JSON_FORCE_OBJECT);
} else if ($_REQUEST['action'] === 'show_card_edit_form') {
$checkParams(['card']);
$item->getFromDB($_REQUEST['card']);
if ($item->canViewItem() && $item->canUpdateItem()) {
$item->showForm($_REQUEST['card']);
} else {
http_response_code(403);
return;
}
} else if ($_REQUEST['action'] === 'delete_item') {
$checkParams(['items_id']);
$item->getFromDB($_REQUEST['items_id']);
// Check if the item can be trashed and if the request isn't forcing deletion (purge)
$maybe_deleted = $item->maybeDeleted() && !($_REQUEST['force'] ?? false);
if (($maybe_deleted && $item->canDeleteItem()) || (!$maybe_deleted && $item->canPurgeItem())) {
$item->delete(['id' => $_REQUEST['items_id']], !$maybe_deleted);
} else {
http_response_code(403);
return;
}
}
......@@ -706,7 +706,9 @@ div.progress {
border: 1px solid #242323;
li {
background-color: #3a3938;
color: white;
&, a, i {
color: white;
}
&:hover {
background-color: #262626;
}
......@@ -745,7 +747,9 @@ div.progress {
.kanban-body {
.kanban-item, .kanban-form {
background-color: #3a3938;
&:not(.deleted) {
background-color: #3a3938;
}
border: #272625 1px solid;
color: #ffffff !important;
input[type=text] {
......
......@@ -6721,13 +6721,21 @@ div.objectlockmessage > .form-group-checkbox {
li {
background-color: white;
color: #333333; // Default text color
padding: 10px;
margin-top: 2px;
cursor: pointer;
position: relative;
&:hover {
background-color: #3a5693;
color: white;
a, span {
color: inherit;
cursor: pointer;
display: block;
font-weight: bold;
padding: 10px;
&:hover {
background-color: #3a5693;
color: white;
}
i {
color: inherit;
}
}
ul {
position: absolute;
......@@ -6748,18 +6756,15 @@ div.objectlockmessage > .form-group-checkbox {
margin-top: 0;
}
li.dropdown-trigger {
&:before {
content: "\f054";
font-family: "Font Awesome\ 5 Free";
font-weight: 900;
float: right;
}
&.active {
background-color: #3a5693;
color: white;
}
a {
color: inherit;
a:after {
content: "\f054";
font-family: "Font Awesome\ 5 Free";
font-weight: 900;
padding-left: 10px;
}
}
}
......@@ -6913,13 +6918,15 @@ div.objectlockmessage > .form-group-checkbox {
.kanban-item, .kanban-add-form {
text-align: left;
padding-left: 0;
background-color: white;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
min-height: 50px;
margin-top: 5px;
border-radius: 5px;
border: #d3d3d3 1px solid;
min-width: 250px;
&:not(.deleted) {
background-color: white;
}
&.filtered-out {
display: none;
}
......@@ -6934,6 +6941,12 @@ div.objectlockmessage > .form-group-checkbox {
float: right;
cursor: pointer;
}
.kanban-item-title .fas {
float: none;
&:last-of-type {
margin-right: 5px;
}
}
padding: 5px 10px 0;
color: black;
font-size: 1.1em;
......
......@@ -2266,8 +2266,10 @@ class Project extends CommonDBTM implements ExtraVisibilityCriteria {
}
$itemtype = $item['_itemtype'];
$card = [
'id' => "{$itemtype}-{$item['id']}",
'title' => Html::link($item['name'], $itemtype::getFormURLWithID($item['id']))
'id' => "{$itemtype}-{$item['id']}",
'title' => '<span class="pointer">'.$item['name'].'</span>',
'title_tooltip' => Html::entities_deep(Html::resume_text(Html::clean($item['content']), 100)),
'is_deleted' => $item['is_deleted'] ?? false,
];
$content = "<div class='kanban-plugin-content'>";
......@@ -2325,6 +2327,7 @@ class Project extends CommonDBTM implements ExtraVisibilityCriteria {
$card['content'] = $content;
$card['_team'] = $item['_team'];
$card['_readonly'] = $item['_readonly'];
$card['_form_link'] = $itemtype::getFormUrlWithID($item['id']);
$columns[$item['projectstates_id']]['items'][] = $card;
}
......@@ -2365,7 +2368,8 @@ class Project extends CommonDBTM implements ExtraVisibilityCriteria {
$supported_itemtypes = [];
if (Project::canCreate()) {
$supported_itemtypes['Project'] = [
'name' => Project::getTypeName(1),
'name' => Project::getTypeName(1),
'icon' => Project::getIcon(),
'fields' => [
'projects_id' => [
'type' => 'hidden',
......@@ -2396,7 +2400,8 @@ class Project extends CommonDBTM implements ExtraVisibilityCriteria {
if (ProjectTask::canCreate()) {
$supported_itemtypes['ProjectTask'] = [
'name' => ProjectTask::getTypeName(1),
'name' => ProjectTask::getTypeName(1),
'icon' => ProjectTask::getIcon(),
'fields' => [
'projects_id' => [
'type' => 'hidden',
......@@ -2447,22 +2452,22 @@ class Project extends CommonDBTM implements ExtraVisibilityCriteria {
echo "<div id='kanban' class='kanban'></div>";
$darkmode = ($_SESSION['glpipalette'] === 'darker') ? 'true' : 'false';
$canadd_item = json_encode(self::canCreate() || ProjectTask::canCreate());
$canmodify_view = json_encode(($ID == 0 || $project->canModifyGlobalState()));
$cancreate_column = json_encode((bool)ProjectState::canCreate());
$limit_addcard_columns = $canmodify_view !== 'false' ? '[]' : json_encode([0]);
$can_order_item = json_encode((bool)$project->canOrderKanbanCard($ID));
$rights = json_encode([
'create_item' => self::canCreate() || ProjectTask::canCreate(),
'delete_item' => self::canDelete() || ProjectTask::canDelete(),
'create_column' => (bool)ProjectState::canCreate(),
'modify_view' => $ID == 0 || $project->canModifyGlobalState(),
'order_card' => (bool)$project->canOrderKanbanCard($ID),
'create_card_limited_columns' => $canmodify_view ? [] : [0]
]);
$js = <<<JAVASCRIPT
$(function(){
// Create Kanban
var kanban = new GLPIKanban({
element: "#kanban",
allow_add_item: $canadd_item,
allow_modify_view: $canmodify_view,
allow_create_column: $cancreate_column,
limit_addcard_columns: $limit_addcard_columns,
allow_order_card: $can_order_item,
rights: $rights,
supported_itemtypes: $supported_itemtypes,
dark_theme: {$darkmode},
max_team_images: 3,
......
......@@ -71,6 +71,10 @@ class ProjectTask extends CommonDBChild implements CalDAVCompatibleItemInterface
return _n('Project task', 'Project tasks', $nb);
}
public static function getIcon() {
return 'fas fa-tasks';
}
static function canPurge() {
return static::canChild('canUpdate');
......
This diff is collapsed.
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment