MembersList.vue 4.65 KB
<script setup lang="ts">
import type { DropdownMenuItem } from '@nuxt/ui'
import type { Member } from '~/types'
import type { ButtonPermissionKey } from '~/utils/permission'

defineProps<{
  members: Member[]
}>()
const emit = defineEmits<{
  updated: []
  edit: [member: Member]
}>()

const { t } = useAppI18n()
const toast = useToast()
const settingsApi = useSettingsApi()
const { can, getDeniedReason } = usePermission()
const updatingUsername = ref('')
const removingUsername = ref('')

const pagePath = '/settings/members'
const canEditMember = computed(() => can(pagePath, 'members.edit'))
const canUpdateRole = computed(() => can(pagePath, 'members.role.update'))
const canRemoveMember = computed(() => can(pagePath, 'members.remove'))

function showNoPermissionToast(buttonKey: ButtonPermissionKey) {
  toast.add({
    title: t('permission.toast.title'),
    description: t(getDeniedReason(pagePath, buttonKey)),
    icon: 'i-lucide-shield-alert',
    color: 'warning'
  })
}

const roleItems = computed(() => [
  { label: t('settings.members.role.member'), value: 'member' },
  { label: t('settings.members.role.admin'), value: 'admin' },
  { label: t('settings.members.role.customer'), value: 'customer' }
])

async function updateRole(member: Member, role: Member['role']) {
  if (!canUpdateRole.value) {
    showNoPermissionToast('members.role.update')
    return
  }

  if (updatingUsername.value) {
    return
  }

  updatingUsername.value = member.username

  try {
    const result = await settingsApi.updateMemberRole(member.username, role)

    if (!result.success) {
      toast.add({
        title: t('common.error'),
        description: result.message,
        icon: 'i-lucide-circle-alert',
        color: 'error'
      })
      return
    }

    emit('updated')
  } catch {
    toast.add({
      title: t('common.error'),
      description: t('common.requestFailed'),
      icon: 'i-lucide-circle-alert',
      color: 'error'
    })
  } finally {
    updatingUsername.value = ''
  }
}

async function removeMember(member: Member) {
  if (!canRemoveMember.value) {
    showNoPermissionToast('members.remove')
    return
  }

  if (removingUsername.value) {
    return
  }

  removingUsername.value = member.username

  try {
    const result = await settingsApi.removeMember(member.username)

    if (!result.success) {
      toast.add({
        title: t('common.error'),
        description: result.message,
        icon: 'i-lucide-circle-alert',
        color: 'error'
      })
      return
    }

    emit('updated')
  } catch {
    toast.add({
      title: t('common.error'),
      description: t('common.requestFailed'),
      icon: 'i-lucide-circle-alert',
      color: 'error'
    })
  } finally {
    removingUsername.value = ''
  }
}

function getItems(member: Member): DropdownMenuItem[] {
  return [{
    label: t('settings.members.editMember'),
    disabled: !canEditMember.value,
    onSelect: () => {
      if (!canEditMember.value) {
        showNoPermissionToast('members.edit')
        return
      }

      emit('edit', member)
    }
  }, {
    label: t('settings.members.removeMember'),
    color: 'error' as const,
    disabled: removingUsername.value === member.username || !canRemoveMember.value,
    onSelect: () => {
      if (!canRemoveMember.value) {
        showNoPermissionToast('members.remove')
        return
      }

      removeMember(member)
    }
  }]
}
</script>

<template>
  <ul role="list" class="divide-y divide-default">
    <li
      v-for="(member, index) in members"
      :key="index"
      class="flex items-center justify-between gap-3 py-3 px-4 sm:px-6"
    >
      <div class="flex items-center gap-3 min-w-0">
        <UAvatar
          v-bind="member.avatar"
          size="md"
        />

        <div class="text-sm min-w-0">
          <p class="text-highlighted font-medium truncate">
            {{ member.name }}
          </p>
          <p class="text-muted truncate">
            {{ member.username }}
          </p>
        </div>
      </div>

      <div class="flex items-center gap-3">
        <USelect
          :model-value="member.role"
          :items="roleItems"
          :disabled="updatingUsername === member.username || !canUpdateRole"
          color="neutral"
          :ui="{ value: 'capitalize', item: 'capitalize' }"
          @update:model-value="(value) => updateRole(member, value as Member['role'])"
        />

        <UDropdownMenu :items="getItems(member)" :content="{ align: 'end' }">
          <UButton
            icon="i-lucide-ellipsis-vertical"
            color="neutral"
            variant="ghost"
            :loading="removingUsername === member.username"
          />
        </UDropdownMenu>
      </div>
    </li>
  </ul>
</template>