Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import { h, onMounted } from 'vue'
import { MESSAGES_DOCK_ID } from '../../constants'
import { addToast } from '../../state/toasts'
import { message } from '../../stories/fixtures'
import ToastOverlay from './ToastOverlay.vue'
Expand Down Expand Up @@ -36,3 +37,23 @@ export const Stack: Story = {
},
}),
}

/** A toast whose message carries an `activate` action, rendered alongside the dismiss control. */
export const WithAction: Story = {
render: () => ({
setup() {
onMounted(() => {
addToast(message({
level: 'warn',
message: 'New a11y violation detected',
notify: true,
autoDismiss: 10 ** 7,
actions: [
{ id: 'view', label: 'View', kind: 'activate', activate: { dockId: MESSAGES_DOCK_ID } },
],
}))
})
return () => h(ToastOverlay)
},
}),
}
23 changes: 23 additions & 0 deletions packages/hub-ui/src/client/components/display/ToastOverlay.vue
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<script setup lang="ts">
import type { DevframeMessageAction } from '@devframes/hub'
import type { DocksContext } from '@devframes/hub/client'
import { MESSAGES_DOCK_ID } from '../../constants'
import { selectMessage, useMessages } from '../../state/messages'
Expand All @@ -23,6 +24,20 @@ function openMessages(toastId: string) {
// (its dock id is the plugin's devframe id).
props.context?.docks.switchEntry(MESSAGES_DOCK_ID)
}

/**
* Mirrors `@devframes/plugin-messages`'s own `onActivate` dispatch (its
* `App.vue`) — the toast's own `entry.actions` buttons behave identically to
* the ones in the messages panel's detail view, without waiting for that
* panel to be open. `context.docks`/`context.commands` reach the hub
* directly here, whereas the plugin (an iframe) goes through RPC.
*/
function dispatchAction(action: DevframeMessageAction) {
if (action.kind === 'activate')
props.context?.docks.switchEntry(action.activate.dockId)
else if (action.kind === 'command')
props.context?.commands.execute(action.command.id, ...(action.command.params ?? []))
}
</script>

<template>
Expand All @@ -44,6 +59,14 @@ function openMessages(toastId: string) {
>
<MessageItem :entry="toast.entry" compact class="px-3 py-2.5">
<template #actions>
<button
v-for="action of toast.entry.actions"
:key="action.id"
class="flex-none text-xs px-1.5 py-0.5 rounded border border-base op70 hover:op100 hover:bg-active transition"
@click.stop="dispatchAction(action)"
>
{{ action.label }}
</button>
<button
class="flex-none op30 hover:op100 p-0.5 rounded hover:bg-active transition"
@click.stop="dismissToast(toast.id)"
Expand Down
Loading