feat: SPA Roles screens
This commit is contained in:
1 parent
907949aa12
commit
3db490a615
47 files changed
+6947
-9
No files matched your search
@@ -0,0 +1,153 @@
|
||||
<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]
|
||||
}>()
|
||||
|
||||
const open = defineModel<boolean>('open', { default: true })
|
||||
|
||||
const { reassignRole, deleteRole } = useRoles()
|
||||
|
||||
const target = ref<number | undefined>(undefined)
|
||||
const pending = ref(false)
|
||||
const notice = ref<DirectoryNotice | null>(null)
|
||||
const members = ref(props.role.members)
|
||||
|
||||
const stage = computed<'builtin' | 'blocked' | 'confirm'>(() => {
|
||||
if (props.role.built_in) {
|
||||
return 'builtin'
|
||||
}
|
||||
return members.value > 0 ? 'blocked' : 'confirm'
|
||||
})
|
||||
|
||||
const title = computed(() => t('Delete {0}?', [props.role.name]))
|
||||
|
||||
const description = computed(() => {
|
||||
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'
|
||||
? t('Move {0} members and delete role', [members.value])
|
||||
: t('Delete role')
|
||||
})
|
||||
|
||||
const pendingLabel = computed(() =>
|
||||
stage.value === 'blocked' ? t('Moving and deleting…') : t('Deleting…'),
|
||||
)
|
||||
|
||||
function showRefusal(error: unknown) {
|
||||
if (!(error instanceof ApiError)) {
|
||||
notice.value = { description: t("Couldn't delete the role. Try again in a moment.") }
|
||||
return
|
||||
}
|
||||
if (error.code === 'role_has_members') {
|
||||
members.value = Math.max(members.value, 1)
|
||||
target.value = undefined
|
||||
return
|
||||
}
|
||||
if (error.code === 'role_not_found') {
|
||||
emit('gone', props.role)
|
||||
open.value = false
|
||||
return
|
||||
}
|
||||
const copy = error.code === undefined ? undefined : refusalCopy(error.code, error.permission)
|
||||
notice.value =
|
||||
copy === undefined
|
||||
? { description: t("Couldn't delete the role. Try again in a moment.") }
|
||||
: { title: copy.title, description: copy.description ?? '' }
|
||||
}
|
||||
|
||||
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) {
|
||||
await reassignRole(props.role.id, target.value)
|
||||
members.value = 0
|
||||
}
|
||||
await deleteRole(props.role.id)
|
||||
emit('deleted', props.role)
|
||||
open.value = false
|
||||
} catch (error) {
|
||||
showRefusal(error)
|
||||
} finally {
|
||||
pending.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DirectoryDialog
|
||||
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"
|
||||
:confirm-disabled="stage === 'blocked' && target === undefined"
|
||||
@confirm="onConfirm"
|
||||
>
|
||||
<template v-if="stage === 'blocked'">
|
||||
<UFormField
|
||||
:label="t('Move members to')"
|
||||
:help="t('They keep their API keys. Keys follow the new role.')"
|
||||
>
|
||||
<RolePicker
|
||||
v-model:selected="target"
|
||||
:roles="roles"
|
||||
: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>
|
||||
Reference in new issue
Block a user