add destructive variant to alert dialogs for destructive actions

This commit is contained in:
pa
2026-03-13 23:10:29 +09:00
parent 9b6ca42d9d
commit 1f5acd546d
17 changed files with 77 additions and 40 deletions
@@ -287,7 +287,8 @@ export function useAvatarDialogCommands(
title: t('confirm.title'), title: t('confirm.title'),
description: t('confirm.command_question', { description: t('confirm.command_question', {
command: t('dialog.avatar.actions.block') command: t('dialog.avatar.actions.block')
}) }),
destructive: true
}), }),
handler: (id) => { handler: (id) => {
avatarModerationRequest avatarModerationRequest
@@ -369,7 +370,8 @@ export function useAvatarDialogCommands(
title: t('confirm.title'), title: t('confirm.title'),
description: t('confirm.command_question', { description: t('confirm.command_question', {
command: t('dialog.avatar.actions.delete') command: t('dialog.avatar.actions.delete')
}) }),
destructive: true
}), }),
handler: (id) => { handler: (id) => {
avatarRequest avatarRequest
@@ -399,7 +401,8 @@ export function useAvatarDialogCommands(
title: t('confirm.title'), title: t('confirm.title'),
description: t('confirm.command_question', { description: t('confirm.command_question', {
command: t('dialog.avatar.actions.delete_impostor') command: t('dialog.avatar.actions.delete_impostor')
}) }),
destructive: true
}), }),
handler: (id) => { handler: (id) => {
avatarRequest avatarRequest
@@ -432,7 +435,8 @@ export function useAvatarDialogCommands(
title: t('confirm.title'), title: t('confirm.title'),
description: t('confirm.command_question', { description: t('confirm.command_question', {
command: t('dialog.avatar.actions.regenerate_impostor') command: t('dialog.avatar.actions.regenerate_impostor')
}) }),
destructive: true
}), }),
handler: (id) => { handler: (id) => {
avatarRequest avatarRequest
+2 -1
View File
@@ -770,7 +770,8 @@
const dashboardId = String(dashboardKey || '').replace(DASHBOARD_NAV_KEY_PREFIX, ''); const dashboardId = String(dashboardKey || '').replace(DASHBOARD_NAV_KEY_PREFIX, '');
const { ok } = await modalStore.confirm({ const { ok } = await modalStore.confirm({
title: t('dashboard.confirmations.delete_title'), title: t('dashboard.confirmations.delete_title'),
description: t('dashboard.confirmations.delete_description') description: t('dashboard.confirmations.delete_description'),
destructive: true
}); });
if (!ok) { if (!ok) {
return; return;
@@ -561,7 +561,8 @@
modalStore modalStore
.confirm({ .confirm({
description: t('confirm.delete_post'), description: t('confirm.delete_post'),
title: t('confirm.title') title: t('confirm.title'),
destructive: true
}) })
.then(({ ok }) => { .then(({ ok }) => {
if (!ok) return; if (!ok) return;
@@ -84,7 +84,8 @@ export function useGroupDialogCommands(
'Block Group': { 'Block Group': {
confirm: () => ({ confirm: () => ({
title: t('confirm.title'), title: t('confirm.title'),
description: t('confirm.block_group') description: t('confirm.block_group'),
destructive: true
}), }),
handler: (id) => { handler: (id) => {
groupRequest.blockGroup({ groupId: id }).then((args) => { groupRequest.blockGroup({ groupId: id }).then((args) => {
@@ -486,7 +486,8 @@ export function useUserDialogCommands(
title: t('confirm.title'), title: t('confirm.title'),
description: t('confirm.command_question', { description: t('confirm.command_question', {
command: t('dialog.user.actions.moderation_block') command: t('dialog.user.actions.moderation_block')
}) }),
destructive: true
}), }),
handler: async (userId) => { handler: async (userId) => {
const args = const args =
@@ -518,7 +519,8 @@ export function useUserDialogCommands(
title: t('confirm.title'), title: t('confirm.title'),
description: t('confirm.command_question', { description: t('confirm.command_question', {
command: t('dialog.user.actions.moderation_mute') command: t('dialog.user.actions.moderation_mute')
}) }),
destructive: true
}), }),
handler: async (userId) => { handler: async (userId) => {
const args = const args =
@@ -554,7 +556,8 @@ export function useUserDialogCommands(
command: t( command: t(
'dialog.user.actions.moderation_disable_avatar_interaction' 'dialog.user.actions.moderation_disable_avatar_interaction'
) )
}) }),
destructive: true
}), }),
handler: async (userId) => { handler: async (userId) => {
const args = const args =
@@ -590,7 +593,8 @@ export function useUserDialogCommands(
command: t( command: t(
'dialog.user.actions.moderation_disable_chatbox' 'dialog.user.actions.moderation_disable_chatbox'
) )
}) }),
destructive: true
}), }),
handler: async (userId) => { handler: async (userId) => {
const args = const args =
@@ -622,7 +626,8 @@ export function useUserDialogCommands(
title: t('confirm.title'), title: t('confirm.title'),
description: t('confirm.command_question', { description: t('confirm.command_question', {
command: t('dialog.user.actions.unfriend') command: t('dialog.user.actions.unfriend')
}) }),
destructive: true
}), }),
handler: async (userId) => { handler: async (userId) => {
const args = await friendRequest.deleteFriend( const args = await friendRequest.deleteFriend(
@@ -528,7 +528,8 @@ export function useWorldDialogCommands(
title: t('confirm.title'), title: t('confirm.title'),
description: t('confirm.command_question', { description: t('confirm.command_question', {
command: t('dialog.world.actions.delete') command: t('dialog.world.actions.delete')
}) }),
destructive: true
}), }),
handler: (id) => { handler: (id) => {
worldRequest.deleteWorld({ worldId: id }).then((args) => { worldRequest.deleteWorld({ worldId: id }).then((args) => {
+6 -5
View File
@@ -50,11 +50,11 @@
</ContextMenuTrigger> </ContextMenuTrigger>
<ContextMenuContent> <ContextMenuContent>
<ContextMenuItem <ContextMenuItem
:disabled="!hasNotifications" v-if="hasNotifications"
@click="clearAllNotifications"> @click="clearAllNotifications">
{{ t('nav_menu.mark_all_read') }} {{ t('nav_menu.mark_all_read') }}
</ContextMenuItem> </ContextMenuItem>
<ContextMenuSeparator /> <ContextMenuSeparator v-if="hasNotifications" />
<template v-if="isDashboardItem(item)"> <template v-if="isDashboardItem(item)">
<ContextMenuItem @click="handleEditDashboard(item)"> <ContextMenuItem @click="handleEditDashboard(item)">
{{ t('nav_menu.edit_dashboard') }} {{ t('nav_menu.edit_dashboard') }}
@@ -101,10 +101,10 @@
</SidebarContent> </SidebarContent>
</ContextMenuTrigger> </ContextMenuTrigger>
<ContextMenuContent> <ContextMenuContent>
<ContextMenuItem :disabled="!hasNotifications" @click="clearAllNotifications"> <ContextMenuItem v-if="hasNotifications" @click="clearAllNotifications">
{{ t('nav_menu.mark_all_read') }} {{ t('nav_menu.mark_all_read') }}
</ContextMenuItem> </ContextMenuItem>
<ContextMenuSeparator /> <ContextMenuSeparator v-if="hasNotifications" />
<ContextMenuItem @click="handleQuickCreateDashboard"> <ContextMenuItem @click="handleQuickCreateDashboard">
{{ t('dashboard.new_dashboard') }} {{ t('dashboard.new_dashboard') }}
</ContextMenuItem> </ContextMenuItem>
@@ -361,7 +361,8 @@
} }
const { ok } = await modalStore.confirm({ const { ok } = await modalStore.confirm({
title: t('dashboard.confirmations.delete_title'), title: t('dashboard.confirmations.delete_title'),
description: t('dashboard.confirmations.delete_description') description: t('dashboard.confirmations.delete_description'),
destructive: true
}); });
if (!ok) { if (!ok) {
return; return;
@@ -94,11 +94,11 @@
</ContextMenuTrigger> </ContextMenuTrigger>
<ContextMenuContent> <ContextMenuContent>
<ContextMenuItem <ContextMenuItem
:disabled="!hasNotifications" v-if="hasNotifications"
@click="emit('clear-notifications')"> @click="emit('clear-notifications')">
{{ t('nav_menu.mark_all_read') }} {{ t('nav_menu.mark_all_read') }}
</ContextMenuItem> </ContextMenuItem>
<ContextMenuSeparator /> <ContextMenuSeparator v-if="hasNotifications" />
<template v-if="isDashboardItem(entry)"> <template v-if="isDashboardItem(entry)">
<ContextMenuItem @click="emit('edit-dashboard', entry)"> <ContextMenuItem @click="emit('edit-dashboard', entry)">
{{ t('nav_menu.edit_dashboard') }} {{ t('nav_menu.edit_dashboard') }}
@@ -126,10 +126,10 @@
</div> </div>
</ContextMenuTrigger> </ContextMenuTrigger>
<ContextMenuContent> <ContextMenuContent>
<ContextMenuItem :disabled="!hasNotifications" @click="emit('clear-notifications')"> <ContextMenuItem v-if="hasNotifications" @click="emit('clear-notifications')">
{{ t('nav_menu.mark_all_read') }} {{ t('nav_menu.mark_all_read') }}
</ContextMenuItem> </ContextMenuItem>
<ContextMenuSeparator /> <ContextMenuSeparator v-if="hasNotifications" />
<ContextMenuItem @click="emit('create-dashboard')"> <ContextMenuItem @click="emit('create-dashboard')">
{{ t('dashboard.new_dashboard') }} {{ t('dashboard.new_dashboard') }}
</ContextMenuItem> </ContextMenuItem>
@@ -7,14 +7,15 @@
const props = defineProps({ const props = defineProps({
asChild: { type: Boolean, required: false }, asChild: { type: Boolean, required: false },
as: { type: null, required: false }, as: { type: null, required: false },
variant: { type: String, required: false },
class: { type: null, required: false } class: { type: null, required: false }
}); });
const delegatedProps = reactiveOmit(props, 'class'); const delegatedProps = reactiveOmit(props, 'class', 'variant');
</script> </script>
<template> <template>
<AlertDialogAction v-bind="delegatedProps" :class="cn(buttonVariants(), props.class)"> <AlertDialogAction v-bind="delegatedProps" :class="cn(buttonVariants({ variant: props.variant }), props.class)">
<slot /> <slot />
</AlertDialogAction> </AlertDialogAction>
</template> </template>
@@ -14,7 +14,7 @@
const modalStore = useModalStore(); const modalStore = useModalStore();
const { alertOpen, alertMode, alertTitle, alertDescription, alertOkText, alertCancelText, alertDismissible } = const { alertOpen, alertMode, alertTitle, alertDescription, alertOkText, alertCancelText, alertDismissible, alertDestructive } =
storeToRefs(modalStore); storeToRefs(modalStore);
function onEscapeKeyDown(event) { function onEscapeKeyDown(event) {
@@ -60,7 +60,7 @@
{{ alertCancelText }} {{ alertCancelText }}
</AlertDialogCancel> </AlertDialogCancel>
<AlertDialogAction @click="modalStore.handleOk"> <AlertDialogAction :variant="alertDestructive ? 'destructive' : undefined" @click="modalStore.handleOk">
{{ alertOkText }} {{ alertOkText }}
</AlertDialogAction> </AlertDialogAction>
</AlertDialogFooter> </AlertDialogFooter>
+3 -2
View File
@@ -232,7 +232,8 @@ export function promptClearAvatarHistory() {
modalStore modalStore
.confirm({ .confirm({
description: t('confirm.clear_avatar_history'), description: t('confirm.clear_avatar_history'),
title: 'Confirm' title: t('confirm.title'),
destructive: true
}) })
.then(({ ok }) => { .then(({ ok }) => {
if (!ok) return; if (!ok) return;
@@ -472,7 +473,7 @@ export function selectAvatarWithConfirmation(id) {
modalStore modalStore
.confirm({ .confirm({
description: t('confirm.select_avatar'), description: t('confirm.select_avatar'),
title: 'Confirm' title: t('confirm.title')
}) })
.then(({ ok }) => { .then(({ ok }) => {
if (!ok) return; if (!ok) return;
+2 -1
View File
@@ -746,7 +746,8 @@ export function leaveGroupPrompt(groupId) {
modalStore modalStore
.confirm({ .confirm({
description: t('confirm.leave_group'), description: t('confirm.leave_group'),
title: t('confirm.title') title: t('confirm.title'),
destructive: true
}) })
.then(({ ok }) => { .then(({ ok }) => {
if (!ok) return; if (!ok) return;
+2 -1
View File
@@ -187,7 +187,8 @@
"tab_group": "Group", "tab_group": "Group",
"tab_other": "Other", "tab_other": "Other",
"past_notifications": "Last 24 hours", "past_notifications": "Last 24 hours",
"no_new_notifications": "No new notifications in the past 24 hours" "no_new_notifications": "No new notifications in the past 24 hours",
"no_unseen_notifications": "No unread notifications"
} }
}, },
"view": { "view": {
+5
View File
@@ -15,6 +15,7 @@ import { useI18n } from 'vue-i18n';
* @property {string=} confirmText * @property {string=} confirmText
* @property {string=} cancelText * @property {string=} cancelText
* @property {boolean=} dismissible // true: allow esc/outside, false: block * @property {boolean=} dismissible // true: allow esc/outside, false: block
* @property {boolean=} destructive // true: use destructive variant for confirm button
*/ */
/** /**
@@ -23,6 +24,7 @@ import { useI18n } from 'vue-i18n';
* @property {string} description * @property {string} description
* @property {string=} confirmText * @property {string=} confirmText
* @property {boolean=} dismissible * @property {boolean=} dismissible
* @property {boolean=} destructive // true: use destructive variant for confirm button
*/ */
/** /**
@@ -65,6 +67,7 @@ export const useModalStore = defineStore('Modal', () => {
const alertOkText = ref(''); const alertOkText = ref('');
const alertCancelText = ref(''); const alertCancelText = ref('');
const alertDismissible = ref(true); const alertDismissible = ref(true);
const alertDestructive = ref(false);
const promptOpen = ref(false); const promptOpen = ref(false);
const promptTitle = ref(''); const promptTitle = ref('');
@@ -155,6 +158,7 @@ export const useModalStore = defineStore('Modal', () => {
alertTitle.value = options.title; alertTitle.value = options.title;
alertDescription.value = options.description; alertDescription.value = options.description;
alertDismissible.value = options.dismissible !== false; alertDismissible.value = options.dismissible !== false;
alertDestructive.value = options.destructive === true;
if (mode === 'alert') { if (mode === 'alert') {
alertOkText.value = alertOkText.value =
@@ -381,6 +385,7 @@ export const useModalStore = defineStore('Modal', () => {
alertOkText, alertOkText,
alertCancelText, alertCancelText,
alertDismissible, alertDismissible,
alertDestructive,
promptOpen, promptOpen,
promptTitle, promptTitle,
promptDescription, promptDescription,
+2 -1
View File
@@ -212,7 +212,8 @@
const handleDelete = async () => { const handleDelete = async () => {
const { ok } = await modalStore.confirm({ const { ok } = await modalStore.confirm({
title: t('dashboard.confirmations.delete_title'), title: t('dashboard.confirmations.delete_title'),
description: t('dashboard.confirmations.delete_description') description: t('dashboard.confirmations.delete_description'),
destructive: true
}); });
if (!ok) { if (!ok) {
return; return;
+9 -6
View File
@@ -880,7 +880,8 @@
invalidIdsText, invalidIdsText,
title: t('view.favorite.avatars.confirm_delete_invalid'), title: t('view.favorite.avatars.confirm_delete_invalid'),
confirmText: t('confirm.confirm_button'), confirmText: t('confirm.confirm_button'),
cancelText: t('view.favorite.avatars.copy_removed_ids') cancelText: t('view.favorite.avatars.copy_removed_ids'),
destructive: true
}); });
if (!confirmDeleteResult.ok) { if (!confirmDeleteResult.ok) {
@@ -1000,7 +1001,8 @@
modalStore modalStore
.confirm({ .confirm({
description: t('confirm.clear_group'), description: t('confirm.clear_group'),
title: t('confirm.title') title: t('confirm.title'),
destructive: true
}) })
.then(({ ok }) => { .then(({ ok }) => {
if (ok) { if (ok) {
@@ -1050,7 +1052,8 @@
modalStore modalStore
.confirm({ .confirm({
description: t('confirm.delete_group', { name: group }), description: t('confirm.delete_group', { name: group }),
title: t('confirm.title') title: t('confirm.title'),
destructive: true
}) })
.then(({ ok }) => { .then(({ ok }) => {
if (ok) { if (ok) {
@@ -1223,9 +1226,9 @@
const total = selectedFavoriteAvatars.value.length; const total = selectedFavoriteAvatars.value.length;
modalStore modalStore
.confirm({ .confirm({
description: `Are you sure you want to unfavorite ${total} favorites? description: t('confirm.bulk_unfavorite', { count: total }),
This action cannot be undone.`, title: t('confirm.bulk_unfavorite_title', { count: total }),
title: `Delete ${total} favorites?` destructive: true
}) })
.then(({ ok }) => { .then(({ ok }) => {
if (ok) { if (ok) {
+13 -3
View File
@@ -26,7 +26,7 @@
<RefreshCw v-else /> <RefreshCw v-else />
</Button> </Button>
</TooltipWrapper> </TooltipWrapper>
<ContextMenu> <ContextMenu v-if="hasUnseenNotifications">
<ContextMenuTrigger as-child> <ContextMenuTrigger as-child>
<TooltipWrapper side="bottom" :content="t('side_panel.notification_center.title')"> <TooltipWrapper side="bottom" :content="t('side_panel.notification_center.title')">
<Button <Button
@@ -36,17 +36,26 @@
@click="isNotificationCenterOpen = !isNotificationCenterOpen"> @click="isNotificationCenterOpen = !isNotificationCenterOpen">
<Bell /> <Bell />
<span <span
v-if="hasUnseenNotifications"
class="absolute top-1 right-1.25 size-1.5 rounded-full bg-red-500" /> class="absolute top-1 right-1.25 size-1.5 rounded-full bg-red-500" />
</Button> </Button>
</TooltipWrapper> </TooltipWrapper>
</ContextMenuTrigger> </ContextMenuTrigger>
<ContextMenuContent> <ContextMenuContent>
<ContextMenuItem :disabled="!hasUnseenNotifications" @click="markNotificationsRead"> <ContextMenuItem @click="markNotificationsRead">
{{ t('nav_menu.mark_all_read') }} {{ t('nav_menu.mark_all_read') }}
</ContextMenuItem> </ContextMenuItem>
</ContextMenuContent> </ContextMenuContent>
</ContextMenu> </ContextMenu>
<TooltipWrapper v-else side="bottom" :content="t('side_panel.notification_center.title')">
<Button
class="rounded-full relative"
variant="ghost"
size="icon-sm"
@click="isNotificationCenterOpen = !isNotificationCenterOpen"
@contextmenu.prevent="toast.info(t('side_panel.notification_center.no_unseen_notifications'))">
<Bell />
</Button>
</TooltipWrapper>
<Popover v-model:open="isSettingsPopoverOpen"> <Popover v-model:open="isSettingsPopoverOpen">
<PopoverTrigger as-child> <PopoverTrigger as-child>
<Button class="rounded-full" variant="ghost" size="icon-sm"> <Button class="rounded-full" variant="ghost" size="icon-sm">
@@ -246,6 +255,7 @@
SelectValue SelectValue
} from '@/components/ui/select'; } from '@/components/ui/select';
import { Bell, RefreshCw, Search, Settings } from 'lucide-vue-next'; import { Bell, RefreshCw, Search, Settings } from 'lucide-vue-next';
import { toast } from 'vue-sonner';
import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from '@/components/ui/context-menu'; import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from '@/components/ui/context-menu';
import { Field, FieldContent, FieldLabel } from '@/components/ui/field'; import { Field, FieldContent, FieldLabel } from '@/components/ui/field';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';