feat: users row actions "Cancel invite" on Pending rows and "Renew invite" on Expired rows

This commit is contained in:
fsylvain committed 2026-09-30 02:48:49 +02:00
1 parent 1ece778caf
commit 142325730d
14 files changed
+1156 -35

No files matched your search

+11 -9
View File
@@ -85,22 +85,24 @@ function onCancel() {
}"
:ui="{
content:
'divide-y-0 shadow-(--shadow-modal) ring-0 sm:top-[220px] sm:max-w-110 sm:translate-y-0 max-sm:!inset-x-4 max-sm:!top-auto max-sm:!bottom-4 max-sm:!left-4 max-sm:!w-auto max-sm:!max-w-none max-sm:!translate-x-0 max-sm:!translate-y-0',
header: 'min-h-0 px-6 pt-6 pb-0 sm:px-6 max-sm:px-5 max-sm:pt-5',
title: 'text-lg max-sm:text-[1.0625rem]/6',
description: 'mt-2 text-base text-toned max-sm:text-[0.9375rem]/[23px]',
body: 'px-6 pt-4 pb-0 sm:px-6 sm:pt-4 sm:pb-0 max-sm:px-5',
footer: 'p-6 sm:px-6 max-sm:p-5',
'divide-y-0 shadow-(--shadow-modal) ring-0 sm:top-35 sm:max-w-115 sm:translate-y-0 max-sm:!inset-x-4 max-sm:!top-auto max-sm:!bottom-4 max-sm:!left-4 max-sm:!w-auto max-sm:!max-w-none max-sm:!translate-x-0 max-sm:!translate-y-0',
header: 'min-h-0 px-6 pt-5 pb-0 sm:px-6',
title: 'text-lg/6 font-semibold',
description: 'mt-1 text-sm text-muted text-pretty',
body: 'px-6 pt-5 pb-0 sm:px-6 sm:pt-5 sm:pb-0',
footer: 'px-6 py-5 sm:px-6',
}"
>
<template v-if="error" #body>
<template #body>
<UAlert
v-if="error"
color="error"
icon="i-lucide-circle-x"
:description="error"
variant="soft"
:close="false"
role="alert"
:ui="{ root: 'gap-2 px-3 py-2.5', description: 'text-sm text-toned' }"
/>
</template>
<template #footer>
@@ -110,7 +112,7 @@ function onCancel() {
color="neutral"
variant="outline"
block
class="h-11 justify-center text-[0.9375rem] text-highlighted disabled:text-dimmed disabled:opacity-100 sm:h-8 sm:w-auto sm:px-3 sm:text-sm"
class="h-11 justify-center text-sm text-highlighted disabled:text-dimmed disabled:opacity-100 sm:h-8 sm:w-auto sm:px-3"
:disabled="pending"
@click="onCancel"
>
@@ -121,7 +123,7 @@ function onCancel() {
:loading="pending"
:disabled="pending"
block
class="h-11 justify-center text-[0.9375rem] sm:h-8 sm:w-auto sm:px-3 sm:text-sm"
class="h-11 justify-center text-sm sm:h-8 sm:w-auto sm:px-3"
@click="onConfirm"
>
{{ confirmText }}
+17
View File
@@ -1,6 +1,7 @@
<script setup lang="ts">
import InviteStatusBadge from '@/components/InviteStatusBadge.vue'
import RoleBadge from '@/components/RoleBadge.vue'
import UserRowAction from '@/components/UserRowAction.vue'
import UserEmailCell from '@/components/UserEmailCell.vue'
import { formatCreatedDate } from '@/lib/relativeDate'
import {
@@ -16,6 +17,13 @@ defineProps<{
loading: boolean
sessionUserId?: number
highlightedKey?: string
canManage?: boolean
busyKeys?: string[]
}>()
const emit = defineEmits<{
cancel: [item: UserListItem]
renew: [item: UserListItem]
}>()
function rowKey(item: UserListItem) {
@@ -56,6 +64,15 @@ function rowKey(item: UserListItem) {
<InviteStatusBadge :status="item.status" size="sm" />
<span>Created {{ formatCreatedDate(item.created_at) }}</span>
</div>
<div v-if="canManage && item.kind === 'invite'" class="flex pt-1">
<UserRowAction
:item="item"
large
:busy="busyKeys?.includes(rowKey(item))"
@cancel="emit('cancel', item)"
@renew="emit('renew', item)"
/>
</div>
</li>
</template>
</ul>
+66
View File
@@ -0,0 +1,66 @@
<script setup lang="ts">
import { computed } from 'vue'
import type { UserListItem } from '@/composables/useUsers'
const props = defineProps<{
item: UserListItem
busy?: boolean
large?: boolean
}>()
const emit = defineEmits<{ cancel: []; renew: [] }>()
const actions = {
pending: {
label: 'Cancel invite',
icon: 'i-lucide-user-x',
textClass: 'text-error',
event: 'cancel',
},
expired: {
label: 'Renew invite',
icon: 'i-lucide-refresh-cw',
textClass: 'text-highlighted',
event: 'renew',
},
} as const
const action = computed(() => {
return props.item.kind === 'invite' ? actions[props.item.status] : null
})
const label = computed(() => (props.busy ? 'Renewing…' : (action.value?.label ?? '')))
const icon = computed(() => (props.busy ? 'i-lucide-loader-circle' : action.value?.icon))
const sizeClass = computed(() =>
props.large ? 'h-11 gap-2 px-3.5 text-sm' : 'h-7 gap-1.5 px-2.25 text-xs',
)
const iconClass = computed(() => (props.large ? 'size-3.5' : 'size-3.25'))
function onClick() {
if (props.busy || !action.value) {
return
}
if (action.value.event === 'cancel') {
emit('cancel')
} else {
emit('renew')
}
}
</script>
<template>
<UButton
v-if="action"
color="neutral"
variant="outline"
size="xs"
:icon="icon"
:disabled="busy"
class="shrink-0 border border-accented bg-default font-medium whitespace-nowrap ring-0"
:class="[sizeClass, action.textClass, { 'opacity-50': busy }]"
:ui="{ leadingIcon: iconClass }"
@click="onClick"
>
{{ label }}
</UButton>
</template>
+17 -1
View File
@@ -1,6 +1,7 @@
<script setup lang="ts">
import InviteStatusBadge from '@/components/InviteStatusBadge.vue'
import RoleBadge from '@/components/RoleBadge.vue'
import UserRowAction from '@/components/UserRowAction.vue'
import UserEmailCell from '@/components/UserEmailCell.vue'
import { formatCreatedDate } from '@/lib/relativeDate'
import {
@@ -19,6 +20,13 @@ defineProps<{
loading: boolean
sessionUserId?: number
highlightedKey?: string
canManage?: boolean
busyKeys?: string[]
}>()
const emit = defineEmits<{
cancel: [item: UserListItem]
renew: [item: UserListItem]
}>()
function rowKey(item: UserListItem) {
@@ -74,7 +82,15 @@ function rowKey(item: UserListItem) {
<span><RoleBadge :role="userRole(item)" /></span>
<span><InviteStatusBadge :status="item.status" /></span>
<span class="text-muted">{{ formatCreatedDate(item.created_at) }}</span>
<span></span>
<span class="flex justify-end">
<UserRowAction
v-if="canManage"
:item="item"
:busy="busyKeys?.includes(rowKey(item))"
@cancel="emit('cancel', item)"
@renew="emit('renew', item)"
/>
</span>
</div>
</template>
</div>