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,330 @@
|
||||
<script setup lang="ts">
|
||||
import { t } from '@/i18n'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useMediaQuery } from '@vueuse/core'
|
||||
import DashboardShell from '@/components/DashboardShell.vue'
|
||||
import PermissionGrid from '@/components/PermissionGrid.vue'
|
||||
import PermissionGroup from '@/components/PermissionGroup.vue'
|
||||
import PresetPills from '@/components/PresetPills.vue'
|
||||
import RoleNotices from '@/components/RoleNotices.vue'
|
||||
import RolePreview from '@/components/RolePreview.vue'
|
||||
import RoleSaveBar from '@/components/RoleSaveBar.vue'
|
||||
import ConfirmDialog from '@/components/ConfirmDialog.vue'
|
||||
import { useLocalizedOverlay } from '@/i18n/presentation'
|
||||
import { useRoleEditor } from '@/composables/useRoleEditor'
|
||||
import type { CallerScope } from '@/lib/grantLocks'
|
||||
import { usePermissionsStore } from '@/stores/permissions'
|
||||
|
||||
const DESKTOP_QUERY = '(min-width: 64rem)'
|
||||
const CONTENT_GROUP = 'content'
|
||||
|
||||
const permissions = usePermissionsStore()
|
||||
const isDesktop = useMediaQuery(DESKTOP_QUERY)
|
||||
const editor = useRoleEditor()
|
||||
const overlay = useLocalizedOverlay()
|
||||
|
||||
const sheetOpen = ref(false)
|
||||
const openGroups = ref<string[]>([])
|
||||
|
||||
const callerScope: CallerScope = (key, options) => permissions.scopeOf(key, options)
|
||||
|
||||
const generalGroups = computed(() =>
|
||||
editor.catalogue.value.filter((group) => group.key !== CONTENT_GROUP),
|
||||
)
|
||||
const allOpen = computed(
|
||||
() => generalGroups.value.length > 0 && openGroups.value.length === generalGroups.value.length,
|
||||
)
|
||||
const title = computed(() => (editor.isNew.value ? t('New role') : (editor.role.value?.name ?? '')))
|
||||
const warnMembers = computed(() => editor.role.value?.built_in === true && !editor.readOnly.value)
|
||||
const canDuplicate = computed(() => permissions.can('roles.manage'))
|
||||
const footerVisible = computed(
|
||||
() => editor.barVisible.value || (!isDesktop.value && editor.loadState.value === 'ready'),
|
||||
)
|
||||
const fieldsDisabled = computed(() => editor.readOnly.value || editor.saving.value)
|
||||
const typeLabel = computed(() => {
|
||||
const role = editor.role.value
|
||||
if (!role) {
|
||||
return ''
|
||||
}
|
||||
return role.built_in ? t('Built-in') : t('Custom')
|
||||
})
|
||||
|
||||
function toggleGroup(key: string, open: boolean) {
|
||||
openGroups.value = open
|
||||
? [...openGroups.value.filter((existing) => existing !== key), key]
|
||||
: openGroups.value.filter((existing) => existing !== key)
|
||||
}
|
||||
|
||||
function toggleAll() {
|
||||
openGroups.value = allOpen.value ? [] : generalGroups.value.map((group) => group.key)
|
||||
}
|
||||
|
||||
function confirmReset() {
|
||||
const target = editor.role.value
|
||||
if (!target) {
|
||||
return
|
||||
}
|
||||
overlay
|
||||
.create(ConfirmDialog, {
|
||||
props: {
|
||||
get title() {
|
||||
return t('Reset {0} to default?', [target.name])
|
||||
},
|
||||
get description() {
|
||||
return t('Its permissions go back to what RenewCMS ships with.')
|
||||
},
|
||||
get confirmLabel() {
|
||||
return t('Reset to default')
|
||||
},
|
||||
get cancelLabel() {
|
||||
return t('Cancel')
|
||||
},
|
||||
get pendingLabel() {
|
||||
return t('Resetting…')
|
||||
},
|
||||
tone: 'danger',
|
||||
action: editor.resetRole,
|
||||
},
|
||||
})
|
||||
.open()
|
||||
}
|
||||
|
||||
onMounted(editor.load)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DashboardShell>
|
||||
<div
|
||||
v-if="editor.loadState.value === 'loading'"
|
||||
class="flex flex-col gap-3"
|
||||
aria-busy="true"
|
||||
>
|
||||
<USkeleton class="h-8 w-60 rounded-none bg-accented" />
|
||||
<USkeleton class="h-40 w-full rounded-none bg-accented" />
|
||||
</div>
|
||||
|
||||
<UAlert
|
||||
v-else-if="editor.loadState.value === 'error'"
|
||||
role="alert"
|
||||
color="error"
|
||||
variant="soft"
|
||||
orientation="vertical"
|
||||
:close="false"
|
||||
icon="i-lucide-circle-x"
|
||||
:title="t('Couldn\'t load this role')"
|
||||
:description="t('Check your connection and try again.')"
|
||||
>
|
||||
<template #actions>
|
||||
<UButton color="neutral" variant="outline" class="h-8" @click="editor.load">
|
||||
{{ t('Try again') }}
|
||||
</UButton>
|
||||
</template>
|
||||
</UAlert>
|
||||
|
||||
<UEmpty
|
||||
v-else-if="editor.loadState.value === 'missing'"
|
||||
variant="naked"
|
||||
icon="i-lucide-shield"
|
||||
:title="t('This role no longer exists')"
|
||||
:description="t('It may have been deleted.')"
|
||||
:ui="{ root: 'gap-3 border border-default bg-default px-6 py-14' }"
|
||||
>
|
||||
<template #actions>
|
||||
<UButton color="neutral" variant="outline" to="/settings/roles">
|
||||
{{ t('Back to Roles') }}
|
||||
</UButton>
|
||||
</template>
|
||||
</UEmpty>
|
||||
|
||||
<div v-else class="flex flex-col gap-4" :class="{ 'pb-20': !isDesktop }">
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<UButton
|
||||
to="/settings/roles"
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
icon="i-lucide-arrow-left"
|
||||
:aria-label="t('Back to Roles')"
|
||||
class="size-8 justify-center max-md:size-11"
|
||||
/>
|
||||
<h1 class="text-xl font-semibold text-highlighted">{{ title }}</h1>
|
||||
<template v-if="editor.role.value">
|
||||
<UBadge
|
||||
v-if="editor.role.value.locked"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-lock"
|
||||
:label="t('Locked')"
|
||||
/>
|
||||
<UBadge color="neutral" variant="subtle" :label="typeLabel" />
|
||||
<span class="text-muted">{{
|
||||
editor.role.value.members === 1
|
||||
? t('1 member')
|
||||
: t('{0} members', [editor.role.value.members])
|
||||
}}</span>
|
||||
</template>
|
||||
<UButton
|
||||
v-if="editor.canReset.value"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
class="ms-auto h-8 px-3"
|
||||
@click="confirmReset"
|
||||
>
|
||||
{{ t('Reset to default') }}
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
<RoleNotices
|
||||
:role="editor.role.value"
|
||||
:reason="editor.readOnlyReason.value"
|
||||
:conflict="editor.conflict.value"
|
||||
:refusal="editor.refusal.value"
|
||||
:can-duplicate="canDuplicate"
|
||||
:warn-members="warnMembers"
|
||||
:reviewing="editor.reviewing.value"
|
||||
:differences="editor.differenceRows.value"
|
||||
@duplicate="editor.duplicate"
|
||||
@reload-role="editor.reloadRole"
|
||||
@reload-page="editor.reloadPage"
|
||||
@review="editor.toggleReview"
|
||||
/>
|
||||
|
||||
<div class="grid gap-6 lg:grid-cols-[minmax(0,1fr)_22rem]">
|
||||
<div
|
||||
class="flex min-w-0 flex-col gap-6"
|
||||
:class="{ 'opacity-60': editor.saving.value }"
|
||||
:aria-busy="editor.saving.value ? 'true' : undefined"
|
||||
>
|
||||
<section class="flex flex-col gap-4">
|
||||
<UFormField
|
||||
:label="t('Name')"
|
||||
:error="editor.nameError.value || undefined"
|
||||
required
|
||||
>
|
||||
<UInput
|
||||
:model-value="editor.form.value.name"
|
||||
:placeholder="t('For example, Contributor')"
|
||||
:disabled="fieldsDisabled"
|
||||
class="w-full"
|
||||
@update:model-value="editor.setName(String($event))"
|
||||
/>
|
||||
</UFormField>
|
||||
<UFormField :label="t('Description (optional)')">
|
||||
<UInput
|
||||
:model-value="editor.form.value.description"
|
||||
:placeholder="t('What this role is for')"
|
||||
:disabled="fieldsDisabled"
|
||||
class="w-full"
|
||||
@update:model-value="editor.setDescription(String($event))"
|
||||
/>
|
||||
</UFormField>
|
||||
</section>
|
||||
|
||||
<section class="flex flex-col gap-3">
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<h2 class="text-base font-semibold text-highlighted">
|
||||
{{ t('Content permissions') }}
|
||||
</h2>
|
||||
<p class="text-muted">
|
||||
{{
|
||||
t(
|
||||
'What this role can do with entries of each content type. Own means entries the person created.',
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<PresetPills
|
||||
:active="editor.preset.value"
|
||||
:disabled="editor.readOnly.value"
|
||||
@select="editor.choosePreset"
|
||||
/>
|
||||
<PermissionGrid
|
||||
:types="editor.gridTypes.value"
|
||||
:grants="editor.form.value.grants"
|
||||
:initial="editor.initial.value.grants"
|
||||
:caller-scope="callerScope"
|
||||
:read-only="editor.readOnly.value"
|
||||
:mobile="!isDesktop"
|
||||
@change="editor.changeCell"
|
||||
@reset="editor.resetType"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section class="flex flex-col gap-3">
|
||||
<div class="flex flex-wrap items-end justify-between gap-2">
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<h2 class="text-base font-semibold text-highlighted">
|
||||
{{ t('General permissions') }}
|
||||
</h2>
|
||||
<p class="text-muted">{{ t('Everything outside entries.') }}</p>
|
||||
</div>
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
class="h-8 px-3 max-md:h-11"
|
||||
@click="toggleAll"
|
||||
>
|
||||
{{ allOpen ? t('Collapse all') : t('Expand all') }}
|
||||
</UButton>
|
||||
</div>
|
||||
<div class="border border-default bg-default">
|
||||
<PermissionGroup
|
||||
v-for="group in generalGroups"
|
||||
:key="group.key"
|
||||
:open="openGroups.includes(group.key)"
|
||||
:group="group"
|
||||
:grants="editor.form.value.grants"
|
||||
:initial="editor.initial.value.grants"
|
||||
:caller-scope="callerScope"
|
||||
:read-only="editor.readOnly.value"
|
||||
@update:open="toggleGroup(group.key, $event)"
|
||||
@change="editor.changeGeneral"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<aside v-if="isDesktop" class="min-w-0">
|
||||
<div class="sticky top-4">
|
||||
<RolePreview
|
||||
:model="editor.preview.value"
|
||||
:read-only="editor.readOnly.value"
|
||||
/>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="footerVisible" class="sticky bottom-0 z-10 -mx-4 mt-4 flex flex-col">
|
||||
<UButton
|
||||
v-if="!isDesktop"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
block
|
||||
class="h-11 justify-center"
|
||||
@click="sheetOpen = true"
|
||||
>
|
||||
{{ t('What this role can do') }}
|
||||
</UButton>
|
||||
<RoleSaveBar
|
||||
v-if="editor.barVisible.value"
|
||||
:count="editor.unsaved.value"
|
||||
:problems="editor.problems.value"
|
||||
:saving="editor.saving.value"
|
||||
:conflicted="editor.conflict.value !== null"
|
||||
:can-save="editor.canSave.value"
|
||||
:name-missing="editor.nameMissing.value"
|
||||
:is-new="editor.isNew.value"
|
||||
@discard="editor.discard"
|
||||
@save="editor.save"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<RolePreview
|
||||
v-if="!isDesktop"
|
||||
v-model:open="sheetOpen"
|
||||
mobile
|
||||
:model="editor.preview.value"
|
||||
:read-only="editor.readOnly.value"
|
||||
/>
|
||||
</DashboardShell>
|
||||
</template>
|
||||
Reference in new issue
Block a user