mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 08:21:39 -06:00
feat(books): add send via email action to details page
The details page now includes a 'Send via Email' button in the more options menu if the user has the email_send permission. Selecting it opens the SendBookDialog. Closes #289
This commit is contained in:
@@ -12,6 +12,7 @@ import {
|
||||
MoreHorizontal,
|
||||
Pencil,
|
||||
RotateCcw,
|
||||
Send,
|
||||
Star,
|
||||
Trash2,
|
||||
TriangleAlert,
|
||||
@@ -37,6 +38,7 @@ import { usePermissions } from '@/features/auth/composables/usePermissions'
|
||||
import { useDeleteBook } from '@/features/book/composables/useDeleteBook'
|
||||
import { useMetadataLocks } from '@/features/book/composables/useMetadataLocks'
|
||||
import DeleteBookDialog from '@/features/book/components/DeleteBookDialog.vue'
|
||||
import SendBookDialog from '@/features/email/components/SendBookDialog.vue'
|
||||
import AddToCollectionSheet from '@/features/collection/components/AddToCollectionSheet.vue'
|
||||
import MetadataScoreBadge from '@/features/metadata-score/components/MetadataScoreBadge.vue'
|
||||
import MetadataScoreBreakdown from '@/features/metadata-score/components/MetadataScoreBreakdown.vue'
|
||||
@@ -84,6 +86,7 @@ const moreMenuOpen = ref(false)
|
||||
const mobileMoreMenuOpen = ref(false)
|
||||
const readMenuOpen = ref(false)
|
||||
const mobileReadMenuOpen = ref(false)
|
||||
const showSendDialog = ref(false)
|
||||
|
||||
const { weights: scoreWeights, fetchWeights } = useMetadataScoreWeights()
|
||||
const { bookProgress: koreaderBookProgress, fetchBookProgress: fetchKoreaderProgress } = useKoreaderBookProgress()
|
||||
@@ -746,6 +749,12 @@ function handleDeleteFromMenu() {
|
||||
promptDelete(props.book.id)
|
||||
}
|
||||
|
||||
function handleSendFromMenu() {
|
||||
moreMenuOpen.value = false
|
||||
mobileMoreMenuOpen.value = false
|
||||
showSendDialog.value = true
|
||||
}
|
||||
|
||||
function handleCoverLoad(ratio: number | null) {
|
||||
coverLoaded.value = true
|
||||
coverFailed.value = false
|
||||
@@ -1133,14 +1142,27 @@ watch(
|
||||
>
|
||||
<Library class="size-3.5" />
|
||||
</button>
|
||||
<Popover v-if="hasPermission('library_delete_books')" :open="mobileMoreMenuOpen" @update:open="(v) => (mobileMoreMenuOpen = v)">
|
||||
<Popover
|
||||
v-if="hasPermission('library_delete_books') || hasPermission('email_send')"
|
||||
:open="mobileMoreMenuOpen"
|
||||
@update:open="(v) => (mobileMoreMenuOpen = v)"
|
||||
>
|
||||
<PopoverTrigger as-child>
|
||||
<button class="flex items-center justify-center h-9 w-9 rounded-md border border-input bg-background hover:bg-muted transition-colors">
|
||||
<MoreHorizontal class="size-3.5" />
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-36 p-1" align="end">
|
||||
<PopoverContent class="w-44 p-1" align="end">
|
||||
<button
|
||||
v-if="hasPermission('email_send')"
|
||||
class="flex w-full items-center gap-2 px-2 py-1.5 rounded text-sm text-foreground hover:bg-muted transition-colors"
|
||||
@click="handleSendFromMenu"
|
||||
>
|
||||
<Send class="size-3.5" />
|
||||
Send via Email
|
||||
</button>
|
||||
<button
|
||||
v-if="hasPermission('library_delete_books')"
|
||||
class="flex w-full items-center gap-2 px-2 py-1.5 rounded text-sm text-destructive hover:bg-destructive/10 transition-colors"
|
||||
@click="handleDeleteFromMenu"
|
||||
>
|
||||
@@ -1271,7 +1293,11 @@ watch(
|
||||
>
|
||||
<Library class="size-3.5" />
|
||||
</button>
|
||||
<Popover v-if="hasPermission('library_delete_books')" :open="moreMenuOpen" @update:open="(v) => (moreMenuOpen = v)">
|
||||
<Popover
|
||||
v-if="hasPermission('library_delete_books') || hasPermission('email_send')"
|
||||
:open="moreMenuOpen"
|
||||
@update:open="(v) => (moreMenuOpen = v)"
|
||||
>
|
||||
<PopoverTrigger as-child>
|
||||
<button
|
||||
class="flex flex-1 items-center justify-center h-9 rounded-md border border-input bg-background text-sm hover:bg-muted transition-colors"
|
||||
@@ -1279,8 +1305,17 @@ watch(
|
||||
<MoreHorizontal class="size-3.5" />
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-36 p-1" align="end">
|
||||
<PopoverContent class="w-40 p-1" align="end">
|
||||
<button
|
||||
v-if="hasPermission('email_send')"
|
||||
class="flex w-full items-center gap-2 px-2 py-1.5 rounded text-sm text-foreground hover:bg-muted transition-colors"
|
||||
@click="handleSendFromMenu"
|
||||
>
|
||||
<Send class="size-3.5" />
|
||||
Send via Email
|
||||
</button>
|
||||
<button
|
||||
v-if="hasPermission('library_delete_books')"
|
||||
class="flex w-full items-center gap-2 px-2 py-1.5 rounded text-sm text-destructive hover:bg-destructive/10 transition-colors"
|
||||
@click="handleDeleteFromMenu"
|
||||
>
|
||||
@@ -1757,6 +1792,8 @@ watch(
|
||||
@done="void loadSupplemental()"
|
||||
/>
|
||||
|
||||
<SendBookDialog v-model:open="showSendDialog" :book-ids="[book.id]" :book-title="book.title ?? undefined" :book-files="book.files" />
|
||||
|
||||
<DeleteBookDialog :open="deleteBookId !== null" :deleting="deletingBook" @confirm="confirmDelete" @cancel="cancelDelete" />
|
||||
|
||||
<!-- Cover lightbox -->
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useDisplaySettings } from '@/composables/useDisplaySettings'
|
||||
const mocks = vi.hoisted(() => ({
|
||||
api: vi.fn<(input: RequestInfo | URL, init?: RequestInit) => Promise<Response>>(),
|
||||
push: vi.fn<(to: unknown) => void>(),
|
||||
hasPermission: vi.fn<(...args: unknown[]) => boolean>(),
|
||||
}))
|
||||
|
||||
vi.mock('vue-router', async (importOriginal) => {
|
||||
@@ -23,7 +24,7 @@ vi.mock('@/lib/api', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('@/features/auth/composables/usePermissions', () => ({
|
||||
usePermissions: () => ({ hasPermission: () => true }),
|
||||
usePermissions: () => ({ hasPermission: mocks.hasPermission }),
|
||||
}))
|
||||
|
||||
function makeBook(overrides: Partial<BookDetail> = {}): BookDetail {
|
||||
@@ -102,6 +103,9 @@ function mountDetails(book: BookDetail) {
|
||||
BookCoverArtwork: false,
|
||||
BookCoverSurface: false,
|
||||
RouterLink: RouterLinkStub,
|
||||
Popover: { template: '<div><slot /><slot name="content" /></div>' },
|
||||
PopoverTrigger: { template: '<div><slot /></div>' },
|
||||
PopoverContent: { template: '<div><slot /></div>' },
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -124,6 +128,8 @@ describe('DetailsTab cover surface', () => {
|
||||
beforeEach(() => {
|
||||
mocks.api.mockReset()
|
||||
mocks.push.mockReset()
|
||||
mocks.hasPermission.mockReset()
|
||||
mocks.hasPermission.mockReturnValue(true)
|
||||
|
||||
mocks.api.mockImplementation(async (input) => {
|
||||
const url = String(input)
|
||||
@@ -246,4 +252,30 @@ describe('DetailsTab cover surface', () => {
|
||||
])
|
||||
expect(wrapper.text()).toContain('Author One, Author Two')
|
||||
})
|
||||
|
||||
it('renders Send via Email button and opens dialog when user has email_send permission', async () => {
|
||||
mocks.hasPermission.mockImplementation((perm) => perm === 'email_send')
|
||||
const wrapper = mountDetails(makeBook())
|
||||
await flushPromises()
|
||||
|
||||
const sendButtons = wrapper.findAll('button').filter((b) => b.text().includes('Send via Email'))
|
||||
expect(sendButtons.length).toBeGreaterThan(0) // Desktop and mobile menus
|
||||
|
||||
// Find the stubbed SendBookDialog
|
||||
const sendDialog = wrapper.findComponent({ name: 'SendBookDialog' })
|
||||
expect(sendDialog.exists()).toBe(true)
|
||||
expect(sendDialog.props('open')).toBe(false)
|
||||
|
||||
await sendButtons[0]!.trigger('click')
|
||||
expect(sendDialog.props('open')).toBe(true)
|
||||
})
|
||||
|
||||
it('does not render Send via Email button when user lacks email_send permission', async () => {
|
||||
mocks.hasPermission.mockImplementation((perm) => perm !== 'email_send')
|
||||
const wrapper = mountDetails(makeBook())
|
||||
await flushPromises()
|
||||
|
||||
const sendButtons = wrapper.findAll('button').filter((b) => b.text().includes('Send via Email'))
|
||||
expect(sendButtons.length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user