187 lines
6.0 KiB
Vue
187 lines
6.0 KiB
Vue
<template>
|
|
<MalioDrawer v-model="isOpen">
|
|
<template #header>
|
|
<h2 class="text-xl font-bold">
|
|
{{ isEditing ? $t('admin.roles.editRole') : $t('admin.roles.addRole') }}
|
|
</h2>
|
|
</template>
|
|
<form class="flex flex-col gap-3" @submit.prevent="handleSubmit">
|
|
<MalioInputText
|
|
v-model="form.code"
|
|
:label="$t('admin.roles.code')"
|
|
input-class="w-full"
|
|
:disabled="isEditing"
|
|
:hint="isEditing ? $t('admin.roles.codeImmutable') : $t('admin.roles.codeHint')"
|
|
:error="touched.code && !codeValid ? $t('admin.roles.codeInvalid') : ''"
|
|
@blur="touched.code = true"
|
|
/>
|
|
<MalioInputText
|
|
v-model="form.label"
|
|
:label="$t('admin.roles.label')"
|
|
input-class="w-full"
|
|
:error="touched.label && !form.label.trim() ? $t('admin.roles.labelRequired') : ''"
|
|
@blur="touched.label = true"
|
|
/>
|
|
<MalioInputTextArea
|
|
v-model="form.description"
|
|
:label="$t('admin.roles.description')"
|
|
input-class="w-full"
|
|
/>
|
|
|
|
<div class="mt-2">
|
|
<label class="text-sm font-semibold text-neutral-700">
|
|
{{ $t('admin.roles.permissions') }}
|
|
</label>
|
|
<p v-if="permissions.length === 0" class="mt-2 text-xs text-neutral-400">
|
|
{{ $t('admin.roles.noPermissions') }}
|
|
</p>
|
|
<div
|
|
v-for="group in groupedPermissions"
|
|
:key="group.module"
|
|
class="mt-3 rounded-lg border border-neutral-200 p-3"
|
|
>
|
|
<p class="mb-2 text-xs font-bold uppercase tracking-wide text-neutral-500">
|
|
{{ group.module }}
|
|
</p>
|
|
<div class="flex flex-col gap-2">
|
|
<label
|
|
v-for="perm in group.permissions"
|
|
:key="perm.id"
|
|
class="flex items-start gap-2 text-sm text-neutral-700"
|
|
>
|
|
<input
|
|
v-model="form.permissions"
|
|
type="checkbox"
|
|
:value="perm['@id']"
|
|
class="mt-0.5 rounded border-neutral-300"
|
|
/>
|
|
<span>
|
|
{{ perm.label }}
|
|
<span class="block text-xs text-neutral-400">{{ perm.code }}</span>
|
|
</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-4 flex justify-end">
|
|
<MalioButton
|
|
:label="$t('common.save')"
|
|
button-class="w-auto px-6"
|
|
:disabled="isSubmitting"
|
|
@click="handleSubmit"
|
|
/>
|
|
</div>
|
|
</form>
|
|
</MalioDrawer>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import type { Role, RoleWrite } from '~/modules/core/services/roles'
|
|
import { useRoleService } from '~/modules/core/services/roles'
|
|
import type { Permission } from '~/modules/core/services/permissions'
|
|
|
|
const props = defineProps<{
|
|
modelValue: boolean
|
|
item: Role | null
|
|
permissions: Permission[]
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
(e: 'update:modelValue', value: boolean): void
|
|
(e: 'saved'): void
|
|
}>()
|
|
|
|
const isOpen = computed({
|
|
get: () => props.modelValue,
|
|
set: (v) => emit('update:modelValue', v),
|
|
})
|
|
|
|
const isEditing = computed(() => !!props.item)
|
|
const isSubmitting = ref(false)
|
|
|
|
const form = reactive({
|
|
code: '',
|
|
label: '',
|
|
description: '',
|
|
permissions: [] as string[],
|
|
})
|
|
|
|
const touched = reactive({
|
|
code: false,
|
|
label: false,
|
|
})
|
|
|
|
const codeValid = computed(() => /^[a-z][a-z0-9_]*$/.test(form.code))
|
|
|
|
const groupedPermissions = computed(() => {
|
|
const byModule = new Map<string, Permission[]>()
|
|
for (const perm of props.permissions) {
|
|
const list = byModule.get(perm.module) ?? []
|
|
list.push(perm)
|
|
byModule.set(perm.module, list)
|
|
}
|
|
return [...byModule.entries()]
|
|
.map(([module, permissions]) => ({ module, permissions }))
|
|
.sort((a, b) => a.module.localeCompare(b.module))
|
|
})
|
|
|
|
watch(() => props.modelValue, (open) => {
|
|
if (open) {
|
|
if (props.item) {
|
|
form.code = props.item.code
|
|
form.label = props.item.label
|
|
form.description = props.item.description ?? ''
|
|
form.permissions = props.item.permissions
|
|
.map((p) => p['@id'])
|
|
.filter((iri): iri is string => !!iri)
|
|
} else {
|
|
form.code = ''
|
|
form.label = ''
|
|
form.description = ''
|
|
form.permissions = []
|
|
}
|
|
touched.code = false
|
|
touched.label = false
|
|
}
|
|
})
|
|
|
|
const { create, update } = useRoleService()
|
|
|
|
async function handleSubmit() {
|
|
touched.code = true
|
|
touched.label = true
|
|
if (!form.label.trim()) {
|
|
return
|
|
}
|
|
if (!isEditing.value && !codeValid.value) {
|
|
return
|
|
}
|
|
|
|
isSubmitting.value = true
|
|
try {
|
|
if (isEditing.value && props.item) {
|
|
const payload: Partial<RoleWrite> = {
|
|
label: form.label.trim(),
|
|
description: form.description.trim() || null,
|
|
permissions: form.permissions,
|
|
}
|
|
await update(props.item.id, payload)
|
|
} else {
|
|
const payload: RoleWrite = {
|
|
code: form.code.trim(),
|
|
label: form.label.trim(),
|
|
description: form.description.trim() || null,
|
|
permissions: form.permissions,
|
|
}
|
|
await create(payload)
|
|
}
|
|
|
|
emit('saved')
|
|
isOpen.value = false
|
|
} finally {
|
|
isSubmitting.value = false
|
|
}
|
|
}
|
|
</script>
|