Files
RenewCMS/web/src/components/DeleteRoleDialog.vue
T

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>