Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 56 additions & 55 deletions js/modules/Forms/ServiceCatalogController.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export class GlpiFormServiceCatalogController
*/
constructor(sort_icons)
{
this.breadcrumb = [];
this.sort_icons = sort_icons;

const input = this.#getFilterInput();
Expand All @@ -49,34 +50,22 @@ export class GlpiFormServiceCatalogController
false
);
input.addEventListener('input', filterFormsDebounced);

// Initialize breadcrumb with root level
this.breadcrumb = [{
title: __('Service catalog'),
params: 'category=0'
}];
// Handle page load with URL parameters
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.size > 0) {
this.#loadItems(urlParams.toString());
}

// Handle back/forward navigation
window.addEventListener('popstate', (event) => {
if (event.state && event.state.url_params) {
this.#loadItems(event.state.url_params);
if (event.state.breadcrumb) {
this.breadcrumb = event.state.breadcrumb;
this.#updateBreadcrumb();
}
}
});

// Push initial state to history
history.replaceState(
{
url_params: 'category=0',
breadcrumb: this.breadcrumb
},
'',
window.location.pathname
);

// Handle composite, breadcrumb and pagination clicks
document.addEventListener('click', (e) => {
const compositeItem = $(e.target).closest('[data-composite-item]');
Expand All @@ -94,16 +83,8 @@ export class GlpiFormServiceCatalogController

const index = this.breadcrumb.findIndex(item => item.params === breadcrumbItem.data('childrenUrlParameters'));
this.breadcrumb = this.breadcrumb.slice(0, index + 1);
this.#updateBreadcrumb();
this.#loadItems(breadcrumbItem.data('childrenUrlParameters'));
history.pushState(
{
url_params: breadcrumbItem.data('childrenUrlParameters'),
breadcrumb: this.breadcrumb
},
'',
window.location.pathname
);
this.#updateHistory(breadcrumbItem.data('childrenUrlParameters'));
}

const pageLink = $(e.target).closest('[data-pagination-item]');
Expand Down Expand Up @@ -142,37 +123,24 @@ export class GlpiFormServiceCatalogController
const search_input = this.#getFilterInput();
search_input.value = '';

const title = element.querySelector('.card-title') ? element.querySelector('.card-title').textContent : '';
const url_params = element.dataset['childrenUrlParameters'];

// Update breadcrumb if it doesn't already contain the current item
if (!this.breadcrumb.some(item => item.params === url_params)) {
this.breadcrumb.push({
title: title,
params: url_params
});
this.#updateBreadcrumb();
}

// Get children items from backend
this.#loadItems(url_params);

// Push state to history with breadcrumb
history.pushState(
{
url_params,
breadcrumb: this.breadcrumb
},
'',
window.location.pathname
);
this.#updateHistory(url_params);
}

async #loadItems(url_params)
{
const url = `${CFG_GLPI.root_doc}/ServiceCatalog/Items`;
const response = await fetch(`${url}?${url_params}`);
let response = await fetch(`${url}?${url_params}`);
if (!response.ok) { // We fallback the response to the root page
response = await fetch(`${url}`);
this.#updateHistory('');
}

this.#getFormsArea().innerHTML = await response.text();
this.#updateBreadcrumb();
}

async #loadPage(element) {
Expand All @@ -182,14 +150,7 @@ export class GlpiFormServiceCatalogController
this.#loadItems(url_params);

// Push state to history with breadcrumb
history.pushState(
{
url_params: url_params,
breadcrumb: this.breadcrumb
},
'',
window.location.pathname
);
this.#updateHistory(url_params);
}

async #applySortStrategy(sort_strategy) {
Expand All @@ -201,6 +162,22 @@ export class GlpiFormServiceCatalogController
}

#updateBreadcrumb() {
const categoryAncestors = document.querySelector('#category-ancestors');
if (categoryAncestors) {
this.breadcrumb = [{
title: __('Service catalog'),
params: 'category=0'
}];

const ancestors = JSON.parse(categoryAncestors.dataset.ancestors);
ancestors.forEach(ancestor => {
this.breadcrumb.push({
title: ancestor.name,
params: `category=${ancestor.id}`
});
});
}

const breadcrumbContainer = document.querySelector('.breadcrumb');
breadcrumbContainer.innerHTML = '';

Expand All @@ -224,6 +201,30 @@ export class GlpiFormServiceCatalogController
});
}

#updateHistory(url_params)
{
const location = new URL(window.location.href);
location.search = '';

const params = new URLSearchParams(url_params);
params.forEach((value, key) => {
if (key === 'category' && value === '0') {
return;
}

location.searchParams.set(key, value);
});
// Push state to history with breadcrumb
history.pushState(
{
url_params,
breadcrumb: this.breadcrumb
},
'',
location
);
}

#getFilterInput()
{
return document.querySelector("[data-glpi-service-catalog-filter-items]");
Expand Down
9 changes: 9 additions & 0 deletions src/CommonTreeDropdown.php
Original file line number Diff line number Diff line change
Expand Up @@ -872,6 +872,15 @@ public function haveChildren()
return (countElementsInTable($this->getTable(), [$fk => $id]) > 0);
}

/** @return iterable<static> */
public function getAncestors(): iterable
{
$ancestor_ids = getAncestorsOf($this->getTable(), $this->getID());
if (empty($ancestor_ids)) {
return [];
}
return static::getSeveralFromDBByCrit(['id' => $ancestor_ids]);
}

/**
* reformat text field describing a tree (such as completename)
Expand Down
11 changes: 10 additions & 1 deletion src/Glpi/Controller/ServiceCatalog/ItemsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
use Glpi\Form\AccessControl\FormAccessParameters;
use Glpi\Form\Category;
use Glpi\Form\ServiceCatalog\ItemRequest;
use Glpi\Form\ServiceCatalog\Provider\CategoryProvider;
use Glpi\Form\ServiceCatalog\ServiceCatalogManager;
use Glpi\Form\ServiceCatalog\SortStrategy\SortStrategyEnum;
use Glpi\Http\Firewall;
Expand Down Expand Up @@ -69,7 +70,11 @@ public function __construct()
public function __invoke(Request $request): Response
{
// Read category
$category_id = $request->query->getInt('category', 0);
$category_id = $request->query->get('category', 0);
if (!is_numeric($category_id)) { // User type anything...
throw new NotFoundHttpException();
}

if ($category_id > 0) {
if (Category::getById($category_id) === false) {
throw new NotFoundHttpException();
Expand Down Expand Up @@ -119,11 +124,15 @@ public function __invoke(Request $request): Response
);
$result = $this->service_catalog_manager->getItems($item_request);

$category_provider = new CategoryProvider();
$ancestors = $category_provider->getAncestors($item_request);

return $this->render(
'components/helpdesk_forms/service_catalog_items.html.twig',
[
'category_id' => $category_id,
'filter' => $filter,
'ancestors' => $ancestors,
'items' => $result['items'],
'total' => $result['total'],
'current_page' => $page,
Expand Down
34 changes: 34 additions & 0 deletions src/Glpi/Form/ServiceCatalog/Provider/CategoryProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,38 @@ public function getItems(ItemRequest $item_request): array

return $categories;
}

/**
* @param ItemRequest $item_request
* @return array<array{id: int, name: string}>
*/
public function getAncestors(ItemRequest $item_request): array
{
$category_id = $item_request->getCategoryID();
$category = Category::getById($category_id);
if (!$category) {
return [];
}

$categories = [];
$current_category = [
'id' => $category->getID(),
'name' => $category->fields['name'],
];

/** @var Category[] $ancestors */
$ancestors = iterator_to_array($category->getAncestors());
foreach ($ancestors as $ancestor) {
$categories[] = [
'id' => $ancestor->getID(),
'name' => $ancestor->fields['name'],
];
}

if (!in_array($current_category['id'], array_column($categories, 'id'), true)) {
$categories[] = $current_category;
}

return $categories;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@
# ---------------------------------------------------------------------
#}

{% if ancestors is defined %}
<span id="category-ancestors" data-ancestors="{{ ancestors|json_encode }}" style="display: none;"></span>
{% endif %}

{% for item in items %}
{% if item is instanceof("Glpi\\Form\\ServiceCatalog\\ServiceCatalogLeafInterface") %}
{{ include(
Expand Down
66 changes: 66 additions & 0 deletions tests/functional/DropdownTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1964,6 +1964,72 @@ public function testDropdownParent()
);
}

public function testDropdownAncestors()
{
// Create tree
$state = new State();
$state_1_id = $state->add(
[
'name' => 'State 1',
'states_id' => 0,
]
);
$this->assertEmpty($state->getAncestors());

$state = new State();
$state_1_1_id = $state->add(
[
'name' => 'State 1.1',
'states_id' => $state_1_id,
]
);
$ancestors = iterator_to_array($state->getAncestors());
$this->assertCount(1, $ancestors);
$this->assertEquals('State 1', $ancestors[0]->fields['name']);

$state = new State();
$state->add(
[
'name' => 'State 1.1.1',
'states_id' => $state_1_1_id,
]
);
$ancestors = iterator_to_array($state->getAncestors());
$this->assertCount(2, $ancestors);
$this->assertEquals('State 1', $ancestors[0]->fields['name']);
$this->assertEquals('State 1.1', $ancestors[1]->fields['name']);

$state = new State();
$state->add(
[
'name' => 'State 1.2',
'states_id' => $state_1_id,
]
);
$ancestors = iterator_to_array($state->getAncestors());
$this->assertCount(1, $ancestors);
$this->assertEquals('State 1', $ancestors[0]->fields['name']);


$state_2_id = $state->add(
[
'name' => 'State 2',
'states_id' => 0,
]
);
$this->assertEmpty($state->getAncestors());

$state->add(
[
'name' => 'State 2.1',
'states_id' => $state_2_id,
]
);
$ancestors = iterator_to_array($state->getAncestors());
$this->assertCount(1, $ancestors);
$this->assertEquals('State 2', $ancestors[0]->fields['name']);
}

/**
* Data provider for testDropdownNumber
*
Expand Down
Loading
Loading