From 3b0150fb882dccd887700d10d9231b05d1fc1d0b Mon Sep 17 00:00:00 2001 From: Norman Niati Date: Wed, 27 May 2026 16:08:27 +0200 Subject: [PATCH] feat(user): polish list + edit dialog + harmonized confirmations - D1: align action gears (UserListView column actions -> align: end, row gear sized x-small to match the table tools gear in the header). Status column header centered above the lock icons. - D2: bold+red name in all sensitive confirmations (lock/unlock/isolate/restore/resetPassword + delete) across UserListView, UserEditDialog (in-popup action menu) and GroupMembersView, using the LigojConfirmDialog default slot. Bulk-delete count also rendered bold+red via two plugin-local fragments. CompanyEditView and GroupEditView already follow the pattern, no-op there. - D4: emails as a list (v-chip in UserListView + v-combobox multiple in UserEditDialog). Fixes a latent bug where editing a user with multiple emails would drop all but the first at save time. Note: the equivalent upgrade on DelegateEditDialog (introduced by PR #35) is intentionally deferred to a follow-up mini-commit once #35 has merged, to keep the two PRs independent. --- ui/src/i18n/en.js | 24 ++++++- ui/src/i18n/fr.js | 24 ++++++- ui/src/views/GroupMembersView.vue | 8 ++- ui/src/views/UserEditDialog.vue | 46 ++++++++++--- ui/src/views/UserListView.vue | 105 ++++++++++++++++++------------ 5 files changed, 153 insertions(+), 54 deletions(-) diff --git a/ui/src/i18n/en.js b/ui/src/i18n/en.js index 4153cf8..40971b6 100644 --- a/ui/src/i18n/en.js +++ b/ui/src/i18n/en.js @@ -34,6 +34,26 @@ export default { 'delegate.resourceDnHint': 'LDAP DN of the subtree (e.g. ou=project,dc=acme,dc=com)', 'user.deleteConfirmBefore': 'Are you sure you want to delete ', 'user.deleteConfirmAfter': '?', + // Chantier D4 — multi-email input (v-combobox) + 'user.emailHint': 'Press Enter or Tab to confirm each email', + // Chantier D2 (rattrapage) — fragments wrapping the bulk-delete count + // in bold red. + 'common.bulkDeleteConfirmBefore': 'Are you sure you want to delete ', + 'common.bulkDeleteConfirmAfter': ' items? This cannot be undone.', + // Chantier D2 — sensitive confirmations split in two fragments so the + // login can be wrapped in bold red between them. The monolithic + // `user.Confirm` keys stay on the host side for any other + // consumer; we just stop relying on them here. + 'user.lockConfirmBefore': 'Lock user ', + 'user.lockConfirmAfter': '? They will no longer be able to log in.', + 'user.unlockConfirmBefore': 'Unlock user ', + 'user.unlockConfirmAfter': '? They will be able to log in again.', + 'user.isolateConfirmBefore': 'Isolate user ', + 'user.isolateConfirmAfter': '? This will remove all group memberships.', + 'user.restoreConfirmBefore': 'Restore user ', + 'user.restoreConfirmAfter': '?', + 'user.resetPasswordConfirmBefore': 'Reset password for user ', + 'user.resetPasswordConfirmAfter': '? A new password will be sent.', 'group.deleteConfirmBefore': 'Are you sure you want to delete ', 'group.deleteConfirmAfter': '?', 'company.deleteConfirmBefore': 'Are you sure you want to delete ', @@ -53,7 +73,9 @@ export default { 'id.group.add': 'Add', 'id.group.addedToast': 'Added {user} to {group}', 'id.group.removeTitle': 'Remove member', - 'id.group.removeConfirm': 'Remove {user} from group {group}?', + // Chantier D2 — fragments wrapping the user identifier in bold red. + 'id.group.removeConfirmBefore': 'Remove ', + 'id.group.removeConfirmAfter': ' from group {group}?', 'id.group.removedToast': 'Removed {user} from {group}', 'id.group.transitive': 'Indirect member through a sub-group — manage them on the parent.', } diff --git a/ui/src/i18n/fr.js b/ui/src/i18n/fr.js index 0d37d8f..a903df4 100644 --- a/ui/src/i18n/fr.js +++ b/ui/src/i18n/fr.js @@ -27,6 +27,26 @@ export default { 'delegate.resourceDnHint': 'DN LDAP du sous-arbre (ex. ou=project,dc=acme,dc=com)', 'user.deleteConfirmBefore': 'Êtes-vous certain de supprimer ', 'user.deleteConfirmAfter': ' ?', + // Chantier D4 — saisie multi-email (v-combobox) + 'user.emailHint': 'Appuyez sur Entrée ou Tab pour valider chaque email', + // Chantier D2 (rattrapage) — fragments encadrant le nombre d'éléments + // en gras-rouge pour la suppression en masse. + 'common.bulkDeleteConfirmBefore': 'Supprimer ', + 'common.bulkDeleteConfirmAfter': ' éléments ? Cette action est irréversible.', + // Chantier D2 — confirmations sensibles découpées en deux fragments + // pour insérer l'identifiant en gras-rouge entre eux. Les clés + // monolithiques `user.Confirm` restent côté host pour + // d'éventuels autres usages, on les surcharge plus. + 'user.lockConfirmBefore': 'Verrouiller l\'utilisateur ', + 'user.lockConfirmAfter': ' ? Il ne pourra plus se connecter.', + 'user.unlockConfirmBefore': 'Déverrouiller l\'utilisateur ', + 'user.unlockConfirmAfter': ' ? Il pourra à nouveau se connecter.', + 'user.isolateConfirmBefore': 'Isoler l\'utilisateur ', + 'user.isolateConfirmAfter': ' ? Cela supprimera toutes ses appartenances aux groupes.', + 'user.restoreConfirmBefore': 'Restaurer l\'utilisateur ', + 'user.restoreConfirmAfter': ' ?', + 'user.resetPasswordConfirmBefore': 'Réinitialiser le mot de passe de l\'utilisateur ', + 'user.resetPasswordConfirmAfter': ' ? Un nouveau mot de passe lui sera envoyé.', 'group.deleteConfirmBefore': 'Êtes-vous certain de supprimer ', 'group.deleteConfirmAfter': ' ?', 'company.deleteConfirmBefore': 'Êtes-vous certain de supprimer ', @@ -45,7 +65,9 @@ export default { 'id.group.add': 'Ajouter', 'id.group.addedToast': '{user} ajouté à {group}', 'id.group.removeTitle': 'Retirer un membre', - 'id.group.removeConfirm': 'Retirer {user} du groupe {group} ?', + // Chantier D2 — fragments encadrant l'identifiant utilisateur en gras-rouge. + 'id.group.removeConfirmBefore': 'Retirer ', + 'id.group.removeConfirmAfter': ' du groupe {group} ?', 'id.group.removedToast': '{user} retiré de {group}', 'id.group.transitive': 'Membre indirect via un sous-groupe — à gérer depuis le groupe parent.', } diff --git a/ui/src/views/GroupMembersView.vue b/ui/src/views/GroupMembersView.vue index a7366e6..435d22b 100644 --- a/ui/src/views/GroupMembersView.vue +++ b/ui/src/views/GroupMembersView.vue @@ -121,15 +121,19 @@ + + > + {{ t('id.group.removeConfirmBefore') }}{{ removeTarget?.id }}{{ t('id.group.removeConfirmAfter', { group: groupName }) }} + diff --git a/ui/src/views/UserEditDialog.vue b/ui/src/views/UserEditDialog.vue index d673fb5..046b419 100644 --- a/ui/src/views/UserEditDialog.vue +++ b/ui/src/views/UserEditDialog.vue @@ -61,7 +61,21 @@ - + + + > + {{ t('user.' + actionType + 'ConfirmBefore') }}{{ form.id }}{{ t('user.' + actionType + 'ConfirmAfter') }} + @@ -219,7 +239,9 @@ const form = ref({ firstName: '', lastName: '', company: '', - mail: '', + // Chantier D4: emails as a list. Backend response carries `mails: [...]`; + // a string fallback at load time keeps tolerance for any legacy payload. + mails: [], }) // useFormGuard still drives the dirty tracking (snapshot + deep watch) and @@ -399,7 +421,9 @@ function loadDemoUser(id) { form.value.firstName = user.firstName form.value.lastName = user.lastName form.value.company = user.company - form.value.mail = user.mails?.[0] || '' + // Chantier D4: hydrate the full mails list (the demo seed already + // carries an array, but be defensive against any string fallback). + form.value.mails = Array.isArray(user.mails) ? [...user.mails] : user.mails ? [user.mails] : [] // 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) @@ -412,7 +436,7 @@ function loadDemoUser(id) { /** Reset all form state to a clean create-mode baseline. Called every time * the dialog opens so a previous edit never bleeds into the next one. */ function resetForm() { - form.value = { id: '', firstName: '', lastName: '', company: '', mail: '' } + form.value = { id: '', firstName: '', lastName: '', company: '', mails: [] } groups.value = [] locked.value = false isolated.value = false @@ -440,7 +464,10 @@ async function loadOnOpen() { form.value.firstName = data.firstName || '' form.value.lastName = data.lastName || '' form.value.company = data.company || '' - form.value.mail = data.mails?.[0] || '' + // Chantier D4: keep the entire mails array, with string fallback + // for any legacy payload that stored a single email as `mail`. + form.value.mails = Array.isArray(data.mails) ? [...data.mails] + : data.mail ? [data.mail] : [] // 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) @@ -553,7 +580,10 @@ async function save() { firstName: form.value.firstName, lastName: form.value.lastName, company: form.value.company, - mail: form.value.mail, + // Chantier D4: send the full mails list — fixes a latent bug where + // the previous single-string `mail` field would drop every address + // past the first at save time. + mails: form.value.mails, // groups is an array of names (strings). Defensive `.map(g => g.name || g)` // in case any legacy object slipped through. groups: groups.value.map(g => g.name || g), diff --git a/ui/src/views/UserListView.vue b/ui/src/views/UserListView.vue index a29a472..f27fbfa 100644 --- a/ui/src/views/UserListView.vue +++ b/ui/src/views/UserListView.vue @@ -35,8 +35,17 @@ + - - - {{ t('user.deleteTitle') }} - - {{ t('user.deleteConfirmBefore') }}{{ deleteTarget?.id }}{{ t('user.deleteConfirmAfter') }} - - - - {{ t('common.cancel') }} - {{ t('common.delete') }} - - - + + + {{ t('user.deleteConfirmBefore') }}{{ deleteTarget?.id }}{{ t('user.deleteConfirmAfter') }} + - - - {{ t('common.bulkDeleteTitle') }} - {{ t('common.bulkDeleteConfirm', { count: selected.length }) }} - - - {{ t('common.cancel') }} - {{ t('common.delete') }} - - - + + + {{ t('common.bulkDeleteConfirmBefore') }}{{ selected.length }}{{ t('common.bulkDeleteConfirmAfter') }} + - - - {{ t('user.' + actionType) }} - {{ t('user.' + actionType + 'Confirm', { id: actionTarget?.id }) }} - - - {{ t('common.cancel') }} - {{ t('common.confirm') }} - - - + triggered from the row gear menu (chantier D2): the login is + now bolded and coloured via two i18n fragments around it + (user.ConfirmBefore / user.ConfirmAfter). --> + + {{ t('user.' + actionType + 'ConfirmBefore') }}{{ actionTarget?.id }}{{ t('user.' + actionType + 'ConfirmAfter') }} + @@ -130,7 +141,7 @@