You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
wiki/frontend/src/components/PageActionsCol.vue

407 lines
12 KiB

<template>
<!--
`flex flex-col` in place of the `column` this carried: that was Quasar's flex helper and nothing
defines it any more, so the rail was a plain block. Which is why `items-stretch` never stretched
the buttons to its width, and why `<w-space />` -- a `flex-grow: 1` spacer -- could not push the
last group to the bottom.
-->
<!--
Page Properties keeps the rail's full square; every other button is 48px. The primary action for
the page reads as the largest target, and the rest sit quieter beneath it.
-->
<div
class="page-actions flex flex-col items-stretch order-last"
:class="editorStore.isActive ? `is-editor` : ``">
<template v-if="userStore.can(`write:pages`)">
<w-btn
class="aspect-square"
flat
icon="la:pen-nib"
:color="editorStore.isActive ? `white` : `deep-orange-9`"
aria-label="Page Properties"
@click="togglePageProperties">
<w-tooltip anchor="center left" self="center right">Page Properties</w-tooltip>
</w-btn>
<w-btn
class="h-12"
v-if="flagsStore.experimental"
flat
icon="la:project-diagram"
:color="editorStore.isActive ? `white` : `deep-orange-9`"
aria-label="Page Data"
@click="togglePageData"
disable>
<w-tooltip anchor="center left" self="center right">Page Data</w-tooltip>
</w-btn>
<w-btn
class="h-12"
v-if="editorStore.isActive"
flat
color="white"
:text-color="hasPendingAssets ? `white` : `deep-orange-3`"
aria-label="Pending Asset Uploads">
<!-- Outside the icon for the same reason as the review badge above -->
<w-icon name="mdi:image-sync-outline" />
<w-badge
class="page-actions-pending-badge"
v-if="hasPendingAssets"
color="white"
text-color="orange-9"
rounded
floating>
<strong>{{ editorStore.pendingAssets.length * 1 }}</strong>
</w-badge>
<w-tooltip anchor="center left" self="center right">Pending Asset Uploads</w-tooltip>
<w-menu ref="menuPendingAssets" anchor="top left" self="top right" :offset="[10, 0]">
<w-card style="width: 450px">
<w-card-section class="card-header">
<w-icon name="img:/_assets/icons/color-data-pending.svg" left size="sm" />
<span>Pending Asset Uploads</span>
</w-card-section>
<w-card-section v-if="!hasPendingAssets"
>There are no assets pending uploads.</w-card-section
>
<w-list v-else separator>
<w-item v-for="item of editorStore.pendingAssets" :key="item.id">
<w-item-section side><w-icon name="la:file-image" /></w-item-section>
<w-item-section>{{ item.fileName }}</w-item-section>
<w-item-section side>
<w-btn
class="acrylic-btn"
color="negative"
round
icon="la:times"
size="xs"
flat
@click="removePendingAsset(item)" />
</w-item-section>
</w-item>
</w-list>
<w-card-section class="card-actions">
<em class="text-caption"
>Assets that are pasted or dropped onto this page will be held here until the page
is saved.</em
>
</w-card-section>
</w-card>
</w-menu>
</w-btn>
<w-separator class="my-2" inset />
</template>
<!--
`read:history` is the permission that exists to say who may see what a page used to contain, so
the button follows it rather than page read access. The API asks the same question.
-->
<w-btn
class="h-12"
v-if="userStore.can(`read:history`)"
flat
icon="la:history"
:color="editorStore.isActive ? `white` : `grey`"
aria-label="Page History"
@click="viewPageHistory">
<w-tooltip anchor="center left" self="center right">Page History</w-tooltip>
</w-btn>
<w-btn
class="h-12"
flat
icon="la:code"
:color="editorStore.isActive ? `white` : `grey`"
aria-label="Page Source"
@click="viewPageSource">
<w-tooltip anchor="center left" self="center right">Page Source</w-tooltip>
</w-btn>
<template v-if="!(editorStore.isActive && editorStore.mode === `create`)">
<w-separator class="my-2" inset />
<w-btn
class="h-12"
flat
icon="la:ellipsis-h"
:color="editorStore.isActive ? `deep-orange-2` : `grey`"
aria-label="Page Actions">
<w-tooltip anchor="center left" self="center right">Page Actions</w-tooltip>
<!--
Literal colour classes, not WIcon's `color` prop: that builds `text-<name>` at runtime and
Tailwind only emits a utility it can see spelled out, so these three icons had been drawing
in the inherited text colour rather than the rail's orange.
-->
<w-menu
class="translucent-menu"
anchor="top left"
self="top right"
auto-close
transition-show="jump-left">
<w-list padding style="min-width: 225px">
<w-item clickable disabled v-if="userStore.can(`manage:pages`)">
<w-item-section class="items-center" avatar>
<w-icon class="text-deep-orange-9" name="la:atom" size="sm" />
</w-item-section>
<w-item-section><w-item-label>Convert Page</w-item-label></w-item-section>
</w-item>
<w-item clickable v-if="userStore.can(`write:pages`)" @click="rerenderPage">
<w-item-section class="items-center" avatar>
<w-icon class="text-deep-orange-9" name="la:magic" size="sm" />
</w-item-section>
<w-item-section><w-item-label>Rerender Page</w-item-label></w-item-section>
</w-item>
<w-item clickable disabled>
<w-item-section class="items-center" avatar>
<w-icon class="text-deep-orange-9" name="la:sun" size="sm" />
</w-item-section>
<w-item-section><w-item-label>View Backlinks</w-item-label></w-item-section>
</w-item>
</w-list>
</w-menu>
</w-btn>
</template>
<w-space />
<!--
Hidden outright while a suggestion is being written: duplicating, moving or deleting the page is
not part of suggesting a change to it, and a submitter who happens to hold those rights elsewhere
would otherwise find them here.
-->
<template v-if="!(editorStore.isActive && [`create`, `suggest`].includes(editorStore.mode))">
<w-btn
class="h-12"
v-if="userStore.can(`write:pages`)"
flat
icon="la:copy"
:color="editorStore.isActive ? `deep-orange-2` : `grey`"
aria-label="Duplicate Page"
@click="duplicatePage">
<w-tooltip anchor="center left" self="center right">Duplicate Page</w-tooltip>
</w-btn>
<w-btn
class="h-12"
v-if="userStore.can(`manage:pages`)"
flat
icon="la:share"
:color="editorStore.isActive ? `deep-orange-2` : `grey`"
aria-label="Rename / Move Page"
@click="renamePage">
<w-tooltip anchor="center left" self="center right">Rename / Move Page</w-tooltip>
</w-btn>
<w-btn
class="h-12"
v-if="userStore.can(`delete:pages`)"
flat
icon="la:trash"
:color="editorStore.isActive ? `deep-orange-2` : `grey`"
aria-label="Delete Page"
@click="deletePage">
<w-tooltip anchor="center left" self="center right">Delete Page</w-tooltip>
</w-btn>
</template>
<!-- What the rail says instead: which of the two write modes the editor is in. -->
<span class="page-actions-mode" v-else>{{
editorStore.mode === `suggest`
? t('common.actions.suggestedEdit')
: t('common.actions.newPage')
}}</span>
</div>
</template>
<script setup>
import { computed, defineAsyncComponent, ref } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { dialog } from '@/composables/dialog'
import { notify } from '@/composables/notify'
import { useEditorStore } from '@/stores/editor'
import { useFlagsStore } from '@/stores/flags'
import { usePageStore } from '@/stores/page'
import { useSiteStore } from '@/stores/site'
import { useUserStore } from '@/stores/user'
// STORES
const editorStore = useEditorStore()
const flagsStore = useFlagsStore()
const pageStore = usePageStore()
const siteStore = useSiteStore()
const userStore = useUserStore()
// ROUTER
const router = useRouter()
const route = useRoute()
// I18N
const { t } = useI18n()
// REFS
const menuPendingAssets = ref(null)
// DATA
// COMPUTED
const hasPendingAssets = computed(() => editorStore.pendingAssets?.length > 0)
// METHODS
function togglePageProperties() {
siteStore.$patch({
sideDialogComponent: 'PagePropertiesDialog',
sideDialogShown: true
})
}
function togglePageData() {
siteStore.$patch({
sideDialogComponent: 'PageDataDialog',
sideDialogShown: true
})
}
function viewPageHistory() {
siteStore.$patch({ overlay: 'PageHistory', overlayOpts: {} })
}
function viewPageSource() {
siteStore.$patch({ overlay: 'PageSource', overlayOpts: {} })
}
function rerenderPage() {
dialog({
component: defineAsyncComponent(() => import('../components/RerenderPageDialog.vue')),
componentProps: {
id: pageStore.id
}
}).onOk(() => {
pageStore.pageLoad({ id: pageStore.id })
})
}
function duplicatePage() {
dialog({
component: defineAsyncComponent(() => import('../components/TreeBrowserDialog.vue')),
componentProps: {
mode: 'duplicatePage',
folderPath: '',
itemId: pageStore.id,
itemTitle: pageStore.title,
itemFileName: pageStore.path
}
}).onOk((newPageOpts) => {
pageStore.pageDuplicate({
sourecePageId: pageStore.id,
path: newPageOpts.path,
title: newPageOpts.title
})
})
}
function renamePage() {
dialog({
component: defineAsyncComponent(() => import('../components/TreeBrowserDialog.vue')),
componentProps: {
mode: 'renamePage',
folderPath: '',
itemId: pageStore.id,
itemTitle: pageStore.title,
itemFileName: pageStore.path
}
}).onOk(async (renamedPageOpts) => {
try {
if (renamedPageOpts.path === pageStore.path) {
await pageStore.pageRename({ id: pageStore.id, title: renamedPageOpts.title })
notify({
type: 'positive',
message: 'Page renamed successfully.'
})
} else {
await pageStore.pageMove({
id: pageStore.id,
path: renamedPageOpts.path,
title: renamedPageOpts.title
})
notify({
type: 'positive',
message: 'Page moved successfully.'
})
}
} catch (err) {
notify({
type: 'negative',
message: err.message
})
}
})
}
function deletePage() {
dialog({
component: defineAsyncComponent(() => import('../components/PageDeleteDialog.vue')),
componentProps: {
pageId: pageStore.id,
pageName: pageStore.title
}
}).onOk(() => {
router.replace('/')
})
}
function removePendingAsset(item) {
URL.revokeObjectURL(item.blobUrl)
editorStore.pendingAssets = editorStore.pendingAssets.filter((a) => a.id !== item.id)
if (editorStore.pendingAssets.length < 1) {
menuPendingAssets.value.hide()
}
}
</script>
<style lang="scss">
.page-actions {
flex: 0 0 56px;
@at-root .body--light & {
background-color: $grey-3;
}
@at-root .body--dark & {
background-color: $dark-4;
}
&.is-editor {
@at-root .body--light & {
background-color: $deep-orange-9;
}
@at-root .body--dark & {
background-color: $deep-orange-9;
}
}
/* -> Taller than the shell only on a very short window, and then it scrolls rather than clipping */
overflow-y: auto;
scrollbar-width: none;
&-mode {
writing-mode: vertical-rl;
text-orientation: mixed;
padding: 1.75rem 1rem 1.75rem 0;
color: $deep-orange-3;
font-weight: 500;
}
&-pending-badge {
animation: pageActionsBadgePulsate 2s ease infinite;
}
}
@keyframes pageActionsBadgePulsate {
0% {
transform: translate(0, 0);
}
50% {
transform: translate(3px, -3px);
}
100% {
transform: translate(0, 0);
}
}
</style>