diff --git a/ui/src/views/UserEditView.vue b/ui/src/views/UserEditView.vue
index 81430b3..48424ec 100644
--- a/ui/src/views/UserEditView.vue
+++ b/ui/src/views/UserEditView.vue
@@ -13,9 +13,81 @@
class="mb-2" />
-
+
+
+
+
+
+ {{ item.scope }}
+ {{ item.count }} {{ t('user.title').toLowerCase() }}
+
+
+
+
+
+
+ {{ companySearchQuery ? 'Aucune entité trouvée' : 'Saisissez des caractères pour rechercher' }}
+
+
+
+
-
+
+
+
+
+
+
+
+
+ {{ groupSearchQuery ? 'Aucun groupe trouvé' : 'Saisissez des caractères pour rechercher' }}
+
+
+
+
@@ -109,8 +181,19 @@ const actionDialog = ref(false)
const actionType = ref('')
const actionLoading = ref(false)
+// --- Company auto-suggest state ---
+const companySearchQuery = ref('')
+const companyResults = ref([])
+const companyLoading = ref(false)
+let companyDebounce = null
+
+// --- Group auto-suggest state (multi-select) ---
+const groupSearchQuery = ref('')
+const groupResults = ref([])
+const groupLoading = ref(false)
+let groupDebounce = null
+
const isEdit = computed(() => !!route.params.id)
-const groupsDisplay = computed(() => groups.value.map(g => g.name || g).join(', ') || '-')
const form = ref({
id: '',
@@ -126,6 +209,123 @@ const rules = {
required: v => !!v || t('common.required'),
}
+// --- Company auto-suggest logic ---
+
+/** Called on every keystroke in the autocomplete. Debounced 300 ms. */
+function onCompanySearch(query) {
+ companySearchQuery.value = query || ''
+ clearTimeout(companyDebounce)
+ companyDebounce = setTimeout(() => searchCompanies(query), 300)
+}
+
+async function searchCompanies(query) {
+ if (!query || query.length < 1) {
+ companyResults.value = []
+ return
+ }
+ companyLoading.value = true
+ try {
+ // Direct URL with un-encoded brackets — the legacy DataTables backend
+ // expects `search[value]=...` literally.
+ const url = `rest/service/id/company?search[value]=${encodeURIComponent(query)}&rows=20&page=1&sidx=name&sord=asc`
+ const resp = await api.get(url)
+ // Defensive: api.get may return the wrapper { data: [...] } or the
+ // array directly depending on the endpoint's content-type handling.
+ companyResults.value = 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. When LDAP isn't configured in
+ // dev, surface a small demo list so the autosuggest can be
+ // visually validated. In prod, an empty backend response stays
+ // empty — the real LDAP integration will populate it.
+ if (import.meta.env.DEV && companyResults.value.length === 0 && query) {
+ const DEMO = [
+ { name: 'Ligoj', scope: 'Company', count: 4 },
+ { name: 'AcmeCorp', scope: 'Company', count: 2 },
+ { name: 'TechSolutions', scope: 'Company', count: 2 },
+ ]
+ const q = query.toLowerCase()
+ companyResults.value = DEMO.filter(c => c.name.toLowerCase().includes(q))
+ }
+ } catch (err) {
+ console.error('Company search failed:', err)
+ companyResults.value = []
+ } finally {
+ companyLoading.value = false
+ }
+}
+
+/** When editing an existing user, the company is already set but the
+ * autocomplete's item list is empty — pre-seed with the current value
+ * so v-autocomplete can render its label correctly on open. */
+async function ensureCurrentCompanyInResults(name) {
+ if (!name) return
+ try {
+ const url = `rest/service/id/company?search[value]=${encodeURIComponent(name)}&rows=5&page=1&sidx=name&sord=asc`
+ const resp = await api.get(url)
+ const items = Array.isArray(resp) ? resp : (Array.isArray(resp?.data) ? resp.data : [])
+ companyResults.value = items.length ? items : [{ name }]
+ } catch {
+ companyResults.value = [{ name }]
+ }
+}
+
+// --- Group auto-suggest logic (multi-select) ---
+
+/** Called on every keystroke in the group autocomplete. Debounced 300 ms. */
+function onGroupSearch(query) {
+ groupSearchQuery.value = query || ''
+ clearTimeout(groupDebounce)
+ groupDebounce = setTimeout(() => searchGroups(query), 300)
+}
+
+/** After picking or removing a chip, reset the search field so the user
+ * can immediately type the next group name. Vuetify v4 doesn't clear
+ * the inline query automatically in multi-select mode. */
+function onGroupModelUpdate() {
+ groupSearchQuery.value = ''
+ groupResults.value = []
+}
+
+async function searchGroups(query) {
+ if (!query || query.length < 1) {
+ groupResults.value = []
+ return
+ }
+ groupLoading.value = true
+ try {
+ const url = `rest/service/id/group?search[value]=${encodeURIComponent(query)}&rows=20&page=1&sidx=name&sord=asc`
+ const resp = await api.get(url)
+ groupResults.value = Array.isArray(resp) ? resp : (Array.isArray(resp?.data) ? resp.data : [])
+ // Dev-only fallback (same gating as company) — never leaks to
+ // production. import.meta.env.DEV is true in `vite dev`, false
+ // in `vite build`.
+ if (import.meta.env.DEV && groupResults.value.length === 0 && query) {
+ const DEMO = [
+ { name: 'Engineering' },
+ { name: 'Management' },
+ { name: 'DevOps' },
+ { name: 'Marketing' },
+ { name: 'Sales' },
+ ]
+ const q = query.toLowerCase()
+ groupResults.value = DEMO.filter(g => g.name.toLowerCase().includes(q))
+ }
+ } catch (err) {
+ console.error('Group search failed:', err)
+ groupResults.value = []
+ } finally {
+ groupLoading.value = false
+ }
+}
+
+/** Pre-seed groupResults with the user's existing groups so Vuetify can
+ * render their chips on edit without an explicit search. Takes an array
+ * of group **names** (strings). */
+function ensureCurrentGroupsInResults(names) {
+ if (!Array.isArray(names) || !names.length) return
+ groupResults.value = names.map(n => ({ name: n }))
+}
+
// Demo users matching UserListView
const DEMO_USERS = [
{ id: 'admin', firstName: 'Admin', lastName: 'User', company: 'Ligoj', mails: ['admin@ligoj.org'], groups: [{ name: 'Engineering' }, { name: 'Management' }] },
@@ -146,7 +346,10 @@ function loadDemoUser(id) {
form.value.lastName = user.lastName
form.value.company = user.company
form.value.mail = user.mails?.[0] || ''
- groups.value = user.groups || []
+ // Normalize groups to an array of names (strings) so v-autocomplete
+ // with item-value="name" can roundtrip them through v-model.
+ groups.value = (user.groups || []).map(g => g.name || g)
+ ensureCurrentGroupsInResults(groups.value)
locked.value = !!user.locked
isolated.value = !!user.isolated
}
@@ -162,14 +365,26 @@ onMounted(async () => {
form.value.lastName = data.lastName || ''
form.value.company = data.company || ''
form.value.mail = data.mails?.[0] || ''
- groups.value = data.groups || []
+ // Normalize groups to an array of names (strings) so v-autocomplete
+ // with item-value="name" can roundtrip them through v-model.
+ groups.value = (data.groups || []).map(g => g.name || g)
locked.value = !!data.locked
isolated.value = !!data.isolated
+ // Pre-seed the company suggest with the current value so the input
+ // displays it correctly without an explicit search.
+ await ensureCurrentCompanyInResults(form.value.company)
+ // Pre-seed groupResults with stubs so v-autocomplete renders the
+ // existing chips immediately (no API roundtrip needed).
+ ensureCurrentGroupsInResults(groups.value)
} else {
// API unavailable — use demo data
demoMode.value = true
errorStore.clear()
loadDemoUser(route.params.id)
+ // Pre-seed in demo mode too, with a stub object.
+ if (form.value.company) {
+ companyResults.value = [{ name: form.value.company }]
+ }
}
loading.value = false
appStore.setBreadcrumbs([
@@ -211,6 +426,10 @@ async function save() {
lastName: form.value.lastName,
company: form.value.company,
mail: form.value.mail,
+ // groups is an array of names (strings). Defensive `.map(g => g.name || g)`
+ // in case any legacy object slipped through. Only sent on edit since the
+ // groups field is hidden on New User for this PR.
+ ...(isEdit.value ? { groups: groups.value.map(g => g.name || g) } : {}),
}
if (isEdit.value) {
@@ -280,3 +499,27 @@ async function confirmAction() {
}
}
+
+