|
51 | 51 | <v-icon v-else color="success" size="small">mdi-lock-open-variant</v-icon> |
52 | 52 | </template> |
53 | 53 | <template #item.actions="{ item }"> |
54 | | - <v-btn icon size="small" variant="text" @click.stop="router.push('/id/user/' + item.id)"> |
55 | | - <v-icon size="small">mdi-pencil</v-icon> |
56 | | - </v-btn> |
57 | | - <v-btn icon size="small" variant="text" color="error" @click.stop="startDelete(item)"> |
58 | | - <v-icon size="small">mdi-delete</v-icon> |
59 | | - </v-btn> |
| 54 | + <!-- Single gear button opening a menu of row actions (Fabrice |
| 55 | + review, chantier I.1). @click.stop on the gear activator |
| 56 | + keeps the click off the row, whose @click:row navigates to |
| 57 | + the edit page. The menu items deliberately carry NO .stop: |
| 58 | + VMenu teleports its content outside the <tr>, so item clicks |
| 59 | + can never reach @click:row anyway — and a .stop there would |
| 60 | + swallow the bubbling click that VMenu's close-on-content-click |
| 61 | + relies on, leaving the menu open behind the dialogs. |
| 62 | + Lock/Isolate labels and icons are contextual: the API omits |
| 63 | + `locked`/`isolated` when null, so a truthy value means the |
| 64 | + user is locked/isolated. --> |
| 65 | + <v-menu> |
| 66 | + <template #activator="{ props }"> |
| 67 | + <v-btn icon size="small" variant="text" :aria-label="t('user.actions')" v-bind="props" @click.stop> |
| 68 | + <v-icon size="small">mdi-cog</v-icon> |
| 69 | + </v-btn> |
| 70 | + </template> |
| 71 | + <v-list density="compact" min-width="220"> |
| 72 | + <v-list-item prepend-icon="mdi-pencil" :title="t('user.edit')" @click="router.push('/id/user/' + item.id)" /> |
| 73 | + <v-list-item prepend-icon="mdi-delete" base-color="error" :title="t('common.delete')" @click="startDelete(item)" /> |
| 74 | + <v-divider class="my-1" /> |
| 75 | + <v-list-item :prepend-icon="item.locked ? 'mdi-lock-open-variant' : 'mdi-lock'" :title="item.locked ? t('user.unlock') : t('user.lock')" |
| 76 | + @click="startUserAction(item, item.locked ? 'unlock' : 'lock')" /> |
| 77 | + <v-list-item :prepend-icon="item.isolated ? 'mdi-account-check' : 'mdi-account-off'" :title="item.isolated ? t('user.restore') : t('user.isolate')" |
| 78 | + @click="startUserAction(item, item.isolated ? 'restore' : 'isolate')" /> |
| 79 | + <v-list-item prepend-icon="mdi-lock-reset" :title="t('user.resetPassword')" @click="startUserAction(item, 'resetPassword')" /> |
| 80 | + </v-list> |
| 81 | + </v-menu> |
60 | 82 | </template> |
61 | 83 | </LigojDataTableServer> |
62 | 84 |
|
|
85 | 107 | </v-card-actions> |
86 | 108 | </v-card> |
87 | 109 | </v-dialog> |
| 110 | + |
| 111 | + <!-- Confirmation for the sensitive lock/isolate/reset actions |
| 112 | + triggered from the row gear menu. Mirrors the actionDialog |
| 113 | + pattern of UserEditView so the two screens stay consistent. --> |
| 114 | + <v-dialog v-model="actionDialog" max-width="400"> |
| 115 | + <v-card> |
| 116 | + <v-card-title>{{ t('user.' + actionType) }}</v-card-title> |
| 117 | + <v-card-text>{{ t('user.' + actionType + 'Confirm', { id: actionTarget?.id }) }}</v-card-text> |
| 118 | + <v-card-actions> |
| 119 | + <v-spacer /> |
| 120 | + <v-btn variant="text" @click="actionDialog = false">{{ t('common.cancel') }}</v-btn> |
| 121 | + <v-btn color="primary" variant="elevated" :loading="actionLoading" @click="confirmUserAction">{{ t('common.confirm') }}</v-btn> |
| 122 | + </v-card-actions> |
| 123 | + </v-card> |
| 124 | + </v-dialog> |
88 | 125 | </div> |
89 | 126 | </template> |
90 | 127 |
|
@@ -120,6 +157,12 @@ const deleteTarget = ref(null) |
120 | 157 | const deleting = ref(false) |
121 | 158 | const bulkDeleteDialog = ref(false) |
122 | 159 |
|
| 160 | +// Row gear-menu action state (lock/unlock, isolate/restore, reset). |
| 161 | +const actionDialog = ref(false) |
| 162 | +const actionType = ref('') |
| 163 | +const actionTarget = ref(null) |
| 164 | +const actionLoading = ref(false) |
| 165 | +
|
123 | 166 | const headers = computed(() => [ |
124 | 167 | { title: t('user.login'), key: 'id', sortable: true }, |
125 | 168 | { title: t('user.firstName'), key: 'firstName', sortable: true }, |
@@ -180,6 +223,36 @@ async function confirmBulkDelete() { |
180 | 223 | dt.load({ page: 1, itemsPerPage: itemsPerPage.value }) |
181 | 224 | } |
182 | 225 |
|
| 226 | +function startUserAction(item, type) { |
| 227 | + actionTarget.value = item |
| 228 | + actionType.value = type |
| 229 | + actionDialog.value = true |
| 230 | +} |
| 231 | +
|
| 232 | +async function confirmUserAction() { |
| 233 | + if (dt.demoMode.value) { |
| 234 | + errorStore.push({ message: t('user.demoAction'), status: 0 }) |
| 235 | + actionDialog.value = false |
| 236 | + return |
| 237 | + } |
| 238 | + actionLoading.value = true |
| 239 | + const id = actionTarget.value.id |
| 240 | + const actions = { |
| 241 | + lock: () => api.del(`rest/service/id/user/${id}/lock`), |
| 242 | + unlock: () => api.put(`rest/service/id/user/${id}/unlock`), |
| 243 | + isolate: () => api.del(`rest/service/id/user/${id}/isolate`), |
| 244 | + restore: () => api.put(`rest/service/id/user/${id}/restore`), |
| 245 | + resetPassword: () => api.put(`rest/service/id/user/${id}/reset`), |
| 246 | + } |
| 247 | + await actions[actionType.value]() |
| 248 | + actionLoading.value = false |
| 249 | + actionDialog.value = false |
| 250 | + actionTarget.value = null |
| 251 | + // Reload the current page so the status icon and the contextual |
| 252 | + // lock/isolate menu labels reflect the new state. |
| 253 | + dt.load(lastOptions) |
| 254 | +} |
| 255 | +
|
183 | 256 | onMounted(() => { |
184 | 257 | appStore.setBreadcrumbs( |
185 | 258 | [ |
|
0 commit comments