262 lines
6.6 KiB
Vue
262 lines
6.6 KiB
Vue
<script setup lang="ts">
|
|
import { t } from '@/i18n'
|
|
import { computed, ref } from 'vue'
|
|
import DirectoryDialog from '@/components/DirectoryDialog.vue'
|
|
import RolePicker from '@/components/RolePicker.vue'
|
|
import { ApiError } from '@/composables/useApi'
|
|
import { useRoles } from '@/composables/useRoles'
|
|
import { refusalCopy } from '@/lib/permissionCopy'
|
|
import type { DirectoryNotice } from '@/lib/directoryTypes'
|
|
import type { RoleItem } from '@/lib/roles'
|
|
|
|
const props = defineProps<{
|
|
role: RoleItem
|
|
roles: RoleItem[]
|
|
canAssign: (role: RoleItem) => boolean
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
deleted: [role: RoleItem]
|
|
gone: [role: RoleItem]
|
|
duplicate: [role: RoleItem]
|
|
conflict: [role: RoleItem]
|
|
refreshed: [roles: RoleItem[]]
|
|
}>()
|
|
|
|
const open = defineModel<boolean>('open', { default: true })
|
|
|
|
const { reassignRole, deleteRole, listRoles } = useRoles()
|
|
|
|
const target = ref<number | undefined>(undefined)
|
|
const pending = ref(false)
|
|
const notice = ref<DirectoryNotice | null>(null)
|
|
const members = ref(props.role.members)
|
|
const missingTargets = ref<number[]>([])
|
|
const gainedMembers = ref(false)
|
|
|
|
const targets = computed(() =>
|
|
props.roles.filter((item) => !missingTargets.value.includes(item.id)),
|
|
)
|
|
|
|
const stage = computed<'builtin' | 'blocked' | 'confirm'>(() => {
|
|
if (props.role.built_in) {
|
|
return 'builtin'
|
|
}
|
|
return members.value > 0 ? 'blocked' : 'confirm'
|
|
})
|
|
|
|
const deleteTop = computed(() => (notice.value ? 25 : 35))
|
|
|
|
const title = computed(() => t('Delete {0}?', [props.role.name]))
|
|
|
|
const description = computed(() => {
|
|
if (gainedMembers.value) {
|
|
return ''
|
|
}
|
|
switch (stage.value) {
|
|
case 'builtin':
|
|
return t("Built-in roles can't be deleted.")
|
|
case 'blocked':
|
|
return t(
|
|
'{0} members have this role. Move them to another role first. Then {1} can be deleted.',
|
|
[members.value, props.role.name],
|
|
)
|
|
default:
|
|
return t("No one has this role. This can't be undone.")
|
|
}
|
|
})
|
|
|
|
const confirmLabel = computed(() => {
|
|
if (stage.value === 'builtin') {
|
|
return t('Duplicate role')
|
|
}
|
|
return stage.value === 'blocked'
|
|
? members.value === 1
|
|
? t('Move 1 member and delete role')
|
|
: t('Move {0} members and delete role', [members.value])
|
|
: t('Delete role')
|
|
})
|
|
|
|
const pendingLabel = computed(() =>
|
|
stage.value === 'blocked' ? t('Moving and deleting…') : t('Deleting…'),
|
|
)
|
|
|
|
const GENERIC_FAILURE = "Couldn't delete the role. Try again in a moment."
|
|
|
|
async function liveMemberCount() {
|
|
try {
|
|
const roles = await listRoles()
|
|
const live = roles.find((item) => item.id === props.role.id)
|
|
return Math.max(live?.members ?? 0, 1)
|
|
} catch {
|
|
return Math.max(members.value, 1)
|
|
}
|
|
}
|
|
|
|
async function showGainedMembers() {
|
|
const count = await liveMemberCount()
|
|
members.value = count
|
|
gainedMembers.value = true
|
|
target.value = undefined
|
|
notice.value = {
|
|
tone: 'warning',
|
|
title: t('Role still has members'),
|
|
description:
|
|
count === 1
|
|
? t('{0} now has 1 member. Move them to another role first.', [props.role.name])
|
|
: t('{0} now has {1} members. Move them to another role first.', [
|
|
props.role.name,
|
|
count,
|
|
]),
|
|
}
|
|
}
|
|
|
|
async function showTargetGone() {
|
|
const missing = props.roles.find((item) => item.id === target.value)
|
|
if (missing) {
|
|
missingTargets.value = [...missingTargets.value, missing.id]
|
|
}
|
|
notice.value = {
|
|
title: t('{0} no longer exists', [missing?.name ?? '']),
|
|
description: t(
|
|
'It was deleted somewhere else. Choose another role to move the members to.',
|
|
),
|
|
}
|
|
target.value = undefined
|
|
try {
|
|
emit('refreshed', await listRoles())
|
|
} catch {
|
|
return
|
|
}
|
|
}
|
|
|
|
function showMoveFailure() {
|
|
notice.value = {
|
|
title: t("Couldn't move the members"),
|
|
description: t('Nothing was changed. Try again in a moment.'),
|
|
}
|
|
}
|
|
|
|
function showRefusal(error: unknown) {
|
|
if (!(error instanceof ApiError)) {
|
|
notice.value = { description: t(GENERIC_FAILURE) }
|
|
return
|
|
}
|
|
if (error.code === 'role_not_found') {
|
|
emit('gone', props.role)
|
|
open.value = false
|
|
return
|
|
}
|
|
if (error.code === 'role_conflict') {
|
|
emit('conflict', props.role)
|
|
open.value = false
|
|
return
|
|
}
|
|
const copy = error.code === undefined ? undefined : refusalCopy(error.code, error.permission)
|
|
notice.value =
|
|
copy === undefined
|
|
? { description: t(GENERIC_FAILURE) }
|
|
: { title: copy.title, description: copy.description ?? '' }
|
|
}
|
|
|
|
async function showMoveRefusal(error: unknown) {
|
|
if (!(error instanceof ApiError)) {
|
|
showMoveFailure()
|
|
return
|
|
}
|
|
if (error.code === 'target_role_not_found') {
|
|
await showTargetGone()
|
|
return
|
|
}
|
|
if (error.code === 'invalid_reassign') {
|
|
target.value = undefined
|
|
showRefusal(error)
|
|
return
|
|
}
|
|
if (error.code === 'role_conflict' || error.code === 'role_not_found') {
|
|
showRefusal(error)
|
|
return
|
|
}
|
|
if (error.code === undefined || refusalCopy(error.code, error.permission) === undefined) {
|
|
showMoveFailure()
|
|
return
|
|
}
|
|
showRefusal(error)
|
|
}
|
|
|
|
async function onConfirm() {
|
|
if (stage.value === 'builtin') {
|
|
emit('duplicate', props.role)
|
|
open.value = false
|
|
return
|
|
}
|
|
if (stage.value === 'blocked' && target.value === undefined) {
|
|
return
|
|
}
|
|
pending.value = true
|
|
notice.value = null
|
|
try {
|
|
if (stage.value === 'blocked' && target.value !== undefined) {
|
|
try {
|
|
await reassignRole(props.role.id, target.value)
|
|
} catch (error) {
|
|
await showMoveRefusal(error)
|
|
return
|
|
}
|
|
members.value = 0
|
|
}
|
|
await deleteRole(props.role.id)
|
|
emit('deleted', props.role)
|
|
open.value = false
|
|
} catch (error) {
|
|
if (error instanceof ApiError && error.code === 'role_has_members') {
|
|
await showGainedMembers()
|
|
return
|
|
}
|
|
showRefusal(error)
|
|
} finally {
|
|
pending.value = false
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<DirectoryDialog
|
|
closable
|
|
v-model:open="open"
|
|
:title="title"
|
|
:description="description"
|
|
:confirm-label="confirmLabel"
|
|
:pending-label="pendingLabel"
|
|
:cancel-label="stage === 'builtin' ? t('Close') : t('Cancel')"
|
|
:tone="stage === 'builtin' ? 'neutral' : 'danger'"
|
|
:pending="pending"
|
|
:notice="notice"
|
|
:top="deleteTop"
|
|
:confirm-disabled="stage === 'blocked' && target === undefined"
|
|
@confirm="onConfirm"
|
|
>
|
|
<template v-if="stage === 'blocked'">
|
|
<UFormField
|
|
:label="t('Move members to')"
|
|
required
|
|
:help="t('They keep their API keys. Keys follow the new role.')"
|
|
>
|
|
<RolePicker
|
|
v-model:selected="target"
|
|
:roles="targets"
|
|
:source-id="role.id"
|
|
:can-assign="canAssign"
|
|
/>
|
|
</UFormField>
|
|
</template>
|
|
<p v-else-if="stage === 'builtin'" class="flex flex-col gap-1 text-muted">
|
|
<span class="font-medium text-highlighted">{{
|
|
t('{0} is built in', [role.name])
|
|
}}</span>
|
|
<span>{{ t('Duplicate it to make a custom role you can change or delete.') }}</span>
|
|
</p>
|
|
<p v-else class="text-muted">{{ t("Deleting a role doesn't affect content.") }}</p>
|
|
</DirectoryDialog>
|
|
</template>
|