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
31 changes: 14 additions & 17 deletions js/src/vue/EntitySelector/EntitySelector.vue
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@
tree_data,
loadTreeData,
changeFullStructure: doChangeFullStructure,
changeEntity: doChangeEntity
} = useEntitySelector();
changeEntity
} = useEntitySelector(useTemplateRef('entity_selector'));

/**
* Function for enumerating tree data and performing an action on each node.
Expand Down Expand Up @@ -208,20 +208,10 @@
}
});
}

function changeEntity(entity_id, is_recursive) {
doChangeEntity(entity_id, is_recursive).then(response => {
if (response.ok) {
window.location.reload();
} else {
window.glpi_toast_error(__('An error occurred while changing the entity. Please try again.'));
}
});
}
</script>

<template>
<div>
<div ref="entity_selector">
<a ref="entity_dropdown_toggle" href="#" class="dropdown-item dropdown-toggle entity-dropdown-toggle" data-bs-toggle="dropdown"
data-bs-auto-close="outside" :title="current_entity" :aria-label="__('Select the desired entity')">
<i class="fa-fw ti ti-stack"></i>
Expand All @@ -247,12 +237,18 @@
<div v-if="!loading" class="flexbox-item-grow mt-2 position-relative" :style="`height: calc(30px + ${32 * max_items}px)`">
<div class="w-100 h-100 overflow-x-auto overflow-y-hidden">
<ul class="w-100 list-group rounded-0" @wheel.prevent.stop="onListScroll">
<li v-for="node in visible_in_dom" :key="node.key" :class="`list-group-item p-0 border-0 cursor-pointer`" :style="`${node.selected ? 'background-color: var(--tblr-primary)' : ''}`">
<div :style="{paddingLeft: node.level * indent_size + 'px'}" :data-node-id="node.key" class="text-nowrap d-flex align-items-center pt-1">
<li v-for="node in visible_in_dom" :key="node.key" :class="`list-group-item p-0 border-0 cursor-pointer`"
:style="`${node.selected ? 'background-color: var(--tblr-primary)' : ''}`"
tabindex="0" :data-node-level="node.level" :data-has-children="node.children.length > 0"
:data-key="node.key"
:aria-expanded="node.children.length > 0 ? node.expanded : undefined"
>
<div :style="{paddingLeft: node.level * indent_size + 'px'}" class="text-nowrap d-flex align-items-center pt-1">
<button v-if="node.children.length" :title="node.expanded ? __('Collapse') : __('Expand')"
:aria-label="node.expanded ? __('Collapse') : __('Expand')"
class="btn btn-ghost-secondary btn-sm btn-icon p-1 cursor-pointer collapse-item"
@click.prevent.stop="onExpandToggleClick(node)">
@click.prevent.stop="onExpandToggleClick(node)"
tabindex="-1" aria-hidden="true">
<i :class="node.expanded ? 'ti ti-chevron-down' : 'ti ti-chevron-right'"></i>
</button>
<div v-else style="width: 25px"></div>
Expand All @@ -264,7 +260,8 @@
<button v-if="node.children.length" class="btn btn-ghost-secondary btn-sm btn-icon p-1"
:title="__('Select this entity and all its children')"
:aria-label="__('Select this entity and all its children')"
@click.prevent.stop="changeEntity(node.key, true)" data-bs-toggle="tooltip" data-bs-placement="top">
@click.prevent.stop="changeEntity(node.key, true)" data-bs-toggle="tooltip" data-bs-placement="top"
tabindex="-1" aria-hidden="true">
<i class="ti ti-chevrons-down"></i>
</button>
</div>
Expand Down
90 changes: 82 additions & 8 deletions js/src/vue/EntitySelector/useEntitySelector.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,85 @@
* ---------------------------------------------------------------------
*/

import { ref } from 'vue';
import {nextTick, onMounted, ref} from 'vue';

export function useEntitySelector()
export function useEntitySelector(container_el)
{
const loading = ref(false);
const tree_data = ref([]);

function loadTreeData()
{
onMounted(() => {
container_el.value.addEventListener('shown.bs.dropdown', () => {
container_el.value.querySelector('input[name="entsearchtext"]').focus();
});
// Add key listeners for navigating the tree with the keyboard
container_el.value.addEventListener('keyup', (event) => {
const list_item = event.target.closest('li');
if (!list_item) {
return;
}
if (event.key === 'ArrowDown') {
event.preventDefault();
// Focus the next visible list item
let next = list_item.nextElementSibling;
while (next && next.offsetParent === null) {
next = next.nextElementSibling;
}
if (next) {
next.focus();
}
} else if (event.key === 'ArrowUp') {
event.preventDefault();
// Focus the previous visible list item
let prev = list_item.previousElementSibling;
while (prev && prev.offsetParent === null) {
prev = prev.previousElementSibling;
}
if (prev) {
prev.focus();
}
} else if (event.key === 'ArrowRight') {
event.preventDefault();
if (list_item.dataset.hasChildren === 'true' && list_item.ariaExpanded === 'false') {
list_item.querySelector('.collapse-item').click();
// Need to wait for DOM changes since child items are not in the DOM until the parent is expanded
nextTick().then(() => {
let next = list_item.nextElementSibling;
while (next && next.offsetParent === null) {
next = next.nextElementSibling;
}
if (next && parseInt(next.dataset.nodeLevel) > parseInt(list_item.dataset.nodeLevel)) {
next.focus();
}
});
}
} else if (event.key === 'ArrowLeft') {
event.preventDefault();
if (list_item.dataset.hasChildren === 'true' && list_item.ariaExpanded === 'true') {
list_item.querySelector('.collapse-item').click();
} else {
// Focus the parent list item
const level = parseInt(list_item.dataset.nodeLevel);
if (level > 0) {
let prev = list_item.previousElementSibling;
while (prev && (prev.offsetParent === null || parseInt(prev.dataset.nodeLevel) >= level)) {
prev = prev.previousElementSibling;
}
if (prev) {
prev.focus();
}
}
}
} else if (event.key === 'Enter') {
const select_children = event.metaKey || event.ctrlKey; // Allow selecting an entity and all its children by holding Ctrl or Cmd
event.preventDefault();
event.stopPropagation();
changeEntity(list_item.dataset.key, select_children);
}
});
});

function loadTreeData() {
loading.value = true;
return fetch(`${window.CFG_GLPI.root_doc}/ajax/entitytreesons.php`, {
method: 'GET',
Expand Down Expand Up @@ -73,8 +143,7 @@ export function useEntitySelector()
/**
* Change entity to "Full structure" which means access to all of the user's entities.
*/
function changeFullStructure()
{
function changeFullStructure() {
return fetch(`${window.CFG_GLPI.root_doc}/Session/ChangeEntity`, {
method: 'POST',
headers: {
Expand All @@ -87,8 +156,7 @@ export function useEntitySelector()
});
}

function changeEntity(entity_id, is_recursive)
{
function changeEntity(entity_id, is_recursive) {
return fetch(`${window.CFG_GLPI.root_doc}/Session/ChangeEntity`, {
method: 'POST',
headers: {
Expand All @@ -99,6 +167,12 @@ export function useEntitySelector()
id: entity_id,
is_recursive: is_recursive,
}),
}).then(response => {
if (response.ok) {
window.location.reload();
} else {
window.glpi_toast_error(__('An error occurred while changing the entity. Please try again.'));
}
});
}

Expand Down
5 changes: 4 additions & 1 deletion tests/e2e/pages/GlpiPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,10 @@ export class GlpiPage
entity_name: string
): Promise<void> {
// eslint-disable-next-line playwright/no-raw-locators
await this.getButton(entity_name).locator('//ancestor::li').getByRole('button', { name: 'Select this entity and all its children' }).click();
await this.getButton(entity_name).locator('//ancestor::li').getByRole('button', {
name: 'Select this entity and all its children',
includeHidden: true, // Button is hidden in the accessibility tree because it is replaced by keyboard controls.
}).click();
}

public async doSwitchToEntityWithoutRecursion(
Expand Down
Loading