mirror of https://github.com/requarks/wiki
parent
99ccdc1326
commit
57cda5162c
@ -0,0 +1,184 @@
|
||||
<template>
|
||||
<w-btn
|
||||
class="header-actions-btn ml-4"
|
||||
flat
|
||||
round
|
||||
dense
|
||||
icon="la:ellipsis-v"
|
||||
aria-label="More Actions">
|
||||
<w-menu ref="menu" class="translucent-menu" anchor="bottom right" self="top right">
|
||||
<!--
|
||||
Every row's icon takes its colour as a literal `text-*` class rather than through `WIcon`'s
|
||||
`color` prop, which builds `text-${color}` at runtime: Tailwind generates a utility only for a
|
||||
class name it can find as literal text in the source, so a constructed one resolves to nothing
|
||||
and the icon falls back to the menu's own ink. `color="positive"` happens to work because that
|
||||
string is written literally elsewhere in the app; `color="amber"` and `color="blue-4"` are not,
|
||||
and rendered black. The colours themselves match the buttons these rows stand in for.
|
||||
-->
|
||||
<w-list padding style="min-width: 250px">
|
||||
<!--
|
||||
Who is signed in. The account button this stands in for led with the same two lines, and
|
||||
nothing else in the phone header says whose session this is.
|
||||
-->
|
||||
<template v-if="userStore.authenticated">
|
||||
<w-item>
|
||||
<w-item-section avatar>
|
||||
<w-avatar v-if="userStore.hasAvatar" size="32px">
|
||||
<img :src="`/_user/current/avatar`" />
|
||||
</w-avatar>
|
||||
<w-icon v-else name="la:user-circle" />
|
||||
</w-item-section>
|
||||
<w-item-section>
|
||||
<w-item-label>{{ userStore.name }}</w-item-label>
|
||||
<w-item-label caption>{{ userStore.email }}</w-item-label>
|
||||
</w-item-section>
|
||||
</w-item>
|
||||
<w-separator class="my-2" />
|
||||
</template>
|
||||
<!--
|
||||
A submenu, because New Page is a choice of editor rather than a single action -- the same
|
||||
choice, from the same component, as the header button on a wide screen. It anchors to this row
|
||||
because `WMenu` takes the nearest `.w-item` as its trigger.
|
||||
|
||||
`hide-asset-btn` because this menu has a File Manager row of its own directly below, and
|
||||
`@new-page` closes this menu once the submenu has acted: the editor opens behind it otherwise,
|
||||
with the menu still floating over it.
|
||||
-->
|
||||
<w-item v-if="userStore.can(`write:pages`)" clickable>
|
||||
<w-item-section avatar>
|
||||
<w-icon name="la:plus-circle" class="text-blue-4" />
|
||||
</w-item-section>
|
||||
<w-item-section>Create New Page</w-item-section>
|
||||
<w-item-section side>
|
||||
<w-icon name="la:angle-right" />
|
||||
</w-item-section>
|
||||
<page-new-menu hide-asset-btn @new-page="close" />
|
||||
</w-item>
|
||||
<!--
|
||||
-> Whoever may put a file somewhere: `write:assets` outright, or `write:pages` for an author
|
||||
whose rules cover the pages but not the assets beside them, since the editor sends them
|
||||
here to insert an image. Every folder and every file is checked again by the endpoints
|
||||
behind the manager, which answer per path, so this decides only whether the door is shown.
|
||||
-->
|
||||
<w-item v-if="canUseFileManager" clickable @click="openFileManager">
|
||||
<w-item-section avatar>
|
||||
<w-icon name="la:folder-open" class="text-positive" />
|
||||
</w-item-section>
|
||||
<w-item-section>File Manager</w-item-section>
|
||||
</w-item>
|
||||
<w-item v-if="userStore.authenticated" clickable to="/_inbox" @click="close">
|
||||
<w-item-section avatar>
|
||||
<w-icon name="mdi:inbox-full" class="text-amber" />
|
||||
</w-item-section>
|
||||
<w-item-section>{{ t('inbox.title') }}</w-item-section>
|
||||
</w-item>
|
||||
<w-item v-if="userStore.can(`access:admin`)" clickable to="/_admin" @click="close">
|
||||
<w-item-section avatar>
|
||||
<w-icon name="la:tools" class="text-pink" />
|
||||
</w-item-section>
|
||||
<w-item-section>{{ t('common.header.admin') }}</w-item-section>
|
||||
</w-item>
|
||||
<w-separator class="my-2" />
|
||||
<!--
|
||||
The account rows, flattened into this list rather than opened as a second submenu: they are two
|
||||
plain actions, and the panel they live in on a wide screen is 300px of card -- wider than this
|
||||
menu is on the screens it exists for.
|
||||
-->
|
||||
<template v-if="userStore.authenticated">
|
||||
<w-item clickable to="/_profile" @click="close">
|
||||
<w-item-section avatar>
|
||||
<w-icon name="la:user-alt" class="text-primary" />
|
||||
</w-item-section>
|
||||
<w-item-section>{{ t('common.header.profile') }}</w-item-section>
|
||||
</w-item>
|
||||
<w-item clickable @click="logout">
|
||||
<w-item-section avatar>
|
||||
<w-icon name="la:sign-out-alt" class="text-red" />
|
||||
</w-item-section>
|
||||
<w-item-section>{{ t('common.header.logout') }}</w-item-section>
|
||||
</w-item>
|
||||
</template>
|
||||
<!-- -> The header's Login button is one of the ones this menu stands in for, so a guest has to
|
||||
find it in here -->
|
||||
<w-item v-else clickable to="/login" @click="close">
|
||||
<w-item-section avatar>
|
||||
<w-icon name="la:sign-in-alt" class="text-primary" />
|
||||
</w-item-section>
|
||||
<w-item-section>{{ t('common.actions.login') }}</w-item-section>
|
||||
</w-item>
|
||||
</w-list>
|
||||
</w-menu>
|
||||
</w-btn>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useSiteStore } from '@/stores/site'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
import PageNewMenu from '@/components/PageNewMenu.vue'
|
||||
|
||||
/**
|
||||
* The phone header's overflow menu: one button standing in for every action the bar shows as its own
|
||||
* icon on a wide screen -- New Page, File Manager, Inbox, Administration, and the account.
|
||||
*
|
||||
* Rendered only below the `sm` breakpoint; see `HeaderNav`. The rows repeat the permission tests the
|
||||
* buttons they replace make, rather than being handed a list, so the two cannot drift apart.
|
||||
*/
|
||||
|
||||
// STORES
|
||||
|
||||
const siteStore = useSiteStore()
|
||||
const userStore = useUserStore()
|
||||
|
||||
// I18N
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
// REFS
|
||||
|
||||
const menu = ref(null)
|
||||
|
||||
// COMPUTED
|
||||
|
||||
const canUseFileManager = computed(
|
||||
() => userStore.can('write:assets') || userStore.can('write:pages')
|
||||
)
|
||||
|
||||
// METHODS
|
||||
|
||||
/**
|
||||
* Dismiss the menu.
|
||||
*
|
||||
* Every row calls it, rather than the menu carrying `auto-close`: that closes on ANY click inside,
|
||||
* including the New Page row -- which would take the submenu's trigger out of the document in the same
|
||||
* tick it was pressed.
|
||||
*/
|
||||
function close() {
|
||||
menu.value?.hide()
|
||||
}
|
||||
|
||||
/*
|
||||
Closed before the manager opens: it is a full-screen overlay, and a menu teleported to the body
|
||||
outranks it -- so the menu would be left floating over the panel it had just opened.
|
||||
*/
|
||||
function openFileManager() {
|
||||
close()
|
||||
siteStore.openFileManager()
|
||||
}
|
||||
|
||||
function logout() {
|
||||
close()
|
||||
userStore.logout()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
// -> Where the button gets its colour, so it carries no `color` prop: `WBtn` emits an inline `color`,
|
||||
// which would outrank this rule. Matches `.account-avbtn`, the button it stands in for.
|
||||
.header-actions-btn {
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in new issue