Skip to content

Commit b93718b

Browse files
authored
Merge pull request #31 from ligoj/norman/feat-user-list-actions-menu
feat(plugin-id/ui): replace user list row actions with a gear menu
2 parents 008e7f5 + f1b5671 commit b93718b

1 file changed

Lines changed: 79 additions & 6 deletions

File tree

ui/src/views/UserListView.vue

Lines changed: 79 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,34 @@
5151
<v-icon v-else color="success" size="small">mdi-lock-open-variant</v-icon>
5252
</template>
5353
<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>
6082
</template>
6183
</LigojDataTableServer>
6284

@@ -85,6 +107,21 @@
85107
</v-card-actions>
86108
</v-card>
87109
</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>
88125
</div>
89126
</template>
90127

@@ -120,6 +157,12 @@ const deleteTarget = ref(null)
120157
const deleting = ref(false)
121158
const bulkDeleteDialog = ref(false)
122159
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+
123166
const headers = computed(() => [
124167
{ title: t('user.login'), key: 'id', sortable: true },
125168
{ title: t('user.firstName'), key: 'firstName', sortable: true },
@@ -180,6 +223,36 @@ async function confirmBulkDelete() {
180223
dt.load({ page: 1, itemsPerPage: itemsPerPage.value })
181224
}
182225
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+
183256
onMounted(() => {
184257
appStore.setBreadcrumbs(
185258
[

0 commit comments

Comments
 (0)