diff --git a/ui/src/views/GroupEditView.vue b/ui/src/views/GroupEditView.vue index e5c4236..342be7d 100644 --- a/ui/src/views/GroupEditView.vue +++ b/ui/src/views/GroupEditView.vue @@ -11,7 +11,30 @@ - + + + + @@ -73,8 +96,13 @@ const saving = ref(false) const deleting = ref(false) const confirmDelete = ref(false) const demoMode = ref(false) -const availableGroups = ref([]) const availableScopes = ref([]) +// Parent group autosuggest — server-backed, loaded lazily on dropdown open. +const parentResults = ref([]) +const parentLoading = ref(false) +const parentSearchQuery = ref('') +const parentLoaded = ref(false) +let parentDebounce = null // Full scope objects ({id, name, ...}) — used at save() to resolve the // Integer ID expected by the backend from the name held in form.value.scope. const scopeAll = ref([]) @@ -130,18 +158,55 @@ async function loadGroupScopes() { } } +// --- Parent group autosuggest (lazy, server-backed) --- + +/** First-batch load when the Parent dropdown opens. Nothing is fetched + * before this — the former mount-time bulk GET of every group does not + * scale (100k+ groups at DGFIP). */ +function onParentMenu(open) { + if (open && !parentLoaded.value) loadParentGroups('') +} + +/** Debounced (300 ms) search as the user types in the Parent field. */ +function onParentSearch(query) { + parentSearchQuery.value = query || '' + clearTimeout(parentDebounce) + parentDebounce = setTimeout(() => loadParentGroups(query), 300) +} + +/** Fetch one page of groups (20) matching `query`; an empty query returns + * the first page. Mirrors the Company/Group autosuggests of UserEditView. */ +async function loadParentGroups(query) { + parentLoaded.value = true + parentLoading.value = true + try { + // Direct URL with un-encoded brackets — the legacy DataTables backend + // expects `search[value]=...` literally. + const url = `rest/service/id/group?search[value]=${encodeURIComponent(query || '')}&rows=20&page=1&sidx=name&sord=asc` + const resp = await api.get(url) + let rows = Array.isArray(resp) ? resp : (Array.isArray(resp?.data) ? resp.data : []) + // Dev-only fallback, gated behind import.meta.env.DEV so demo data + // never leaks to production. + if (import.meta.env.DEV && rows.length === 0) { + const q = (query || '').toLowerCase() + rows = DEMO_GROUPS.filter(g => g.name.toLowerCase().includes(q)) + } + // Always keep the currently selected parent in the list so the + // autocomplete can render its label, even when it is off this page. + if (form.value.parent && !rows.some(g => (g.name || g) === form.value.parent)) { + rows = [{ name: form.value.parent }, ...rows] + } + parentResults.value = rows + } catch (err) { + console.error('Parent group search failed:', err) + parentResults.value = form.value.parent ? [{ name: form.value.parent }] : [] + } finally { + parentLoading.value = false + } +} + onMounted(async () => { loadGroupScopes() - // Load available groups for parent selector - const groupList = await api.get('rest/service/id/group') - if (groupList && Array.isArray(groupList)) { - availableGroups.value = groupList.map(g => g.name || g.id || g).filter(Boolean) - } else if (groupList?.data && Array.isArray(groupList.data)) { - availableGroups.value = groupList.data.map(g => g.name || g.id || g).filter(Boolean) - } else { - // Demo fallback - availableGroups.value = DEMO_GROUPS.map(g => g.name) - } if (isEdit.value) { loading.value = true @@ -150,6 +215,9 @@ onMounted(async () => { form.value.name = data.name || '' form.value.scope = data.scope || '' form.value.parent = data.parent || '' + // Pre-seed only the current parent so the field renders its label + // without loading the whole group list (chantier H). + if (form.value.parent) parentResults.value = [{ name: form.value.parent }] } else { demoMode.value = true errorStore.clear()