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

View File

@@ -287,7 +287,8 @@ export function useAvatarDialogCommands(
title: t('confirm.title'),
description: t('confirm.command_question', {
command: t('dialog.avatar.actions.block')
})
}),
destructive: true
}),
handler: (id) => {
avatarModerationRequest
@@ -369,7 +370,8 @@ export function useAvatarDialogCommands(
title: t('confirm.title'),
description: t('confirm.command_question', {
command: t('dialog.avatar.actions.delete')
})
}),
destructive: true
}),
handler: (id) => {
avatarRequest
@@ -399,7 +401,8 @@ export function useAvatarDialogCommands(
title: t('confirm.title'),
description: t('confirm.command_question', {
command: t('dialog.avatar.actions.delete_impostor')
})
}),
destructive: true
}),
handler: (id) => {
avatarRequest
@@ -432,7 +435,8 @@ export function useAvatarDialogCommands(
title: t('confirm.title'),
description: t('confirm.command_question', {
command: t('dialog.avatar.actions.regenerate_impostor')
})
}),
destructive: true
}),
handler: (id) => {
avatarRequest

View File

@@ -770,7 +770,8 @@
const dashboardId = String(dashboardKey || '').replace(DASHBOARD_NAV_KEY_PREFIX, '');
const { ok } = await modalStore.confirm({
title: t('dashboard.confirmations.delete_title'),
description: t('dashboard.confirmations.delete_description')
description: t('dashboard.confirmations.delete_description'),
destructive: true
});
if (!ok) {
return;

View File

@@ -561,7 +561,8 @@
modalStore
.confirm({
description: t('confirm.delete_post'),
title: t('confirm.title')
title: t('confirm.title'),
destructive: true
})
.then(({ ok }) => {
if (!ok) return;

View File

@@ -84,7 +84,8 @@ export function useGroupDialogCommands(
'Block Group': {
confirm: () => ({
title: t('confirm.title'),
description: t('confirm.block_group')
description: t('confirm.block_group'),
destructive: true
}),
handler: (id) => {
groupRequest.blockGroup({ groupId: id }).then((args) => {

View File

@@ -486,7 +486,8 @@ export function useUserDialogCommands(
title: t('confirm.title'),
description: t('confirm.command_question', {
command: t('dialog.user.actions.moderation_block')
})
}),
destructive: true
}),
handler: async (userId) => {
const args =
@@ -518,7 +519,8 @@ export function useUserDialogCommands(
title: t('confirm.title'),
description: t('confirm.command_question', {
command: t('dialog.user.actions.moderation_mute')
})
}),
destructive: true
}),
handler: async (userId) => {
const args =
@@ -554,7 +556,8 @@ export function useUserDialogCommands(
command: t(
'dialog.user.actions.moderation_disable_avatar_interaction'
)
})
}),
destructive: true
}),
handler: async (userId) => {
const args =
@@ -590,7 +593,8 @@ export function useUserDialogCommands(
command: t(
'dialog.user.actions.moderation_disable_chatbox'
)
})
}),
destructive: true
}),
handler: async (userId) => {
const args =
@@ -622,7 +626,8 @@ export function useUserDialogCommands(
title: t('confirm.title'),
description: t('confirm.command_question', {
command: t('dialog.user.actions.unfriend')
})
}),
destructive: true
}),
handler: async (userId) => {
const args = await friendRequest.deleteFriend(

View File

@@ -528,7 +528,8 @@ export function useWorldDialogCommands(
title: t('confirm.title'),
description: t('confirm.command_question', {
command: t('dialog.world.actions.delete')
})
}),
destructive: true
}),
handler: (id) => {
worldRequest.deleteWorld({ worldId: id }).then((args) => {

View File

@@ -50,11 +50,11 @@
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem
:disabled="!hasNotifications"
v-if="hasNotifications"
@click="clearAllNotifications">
{{ t('nav_menu.mark_all_read') }}
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuSeparator v-if="hasNotifications" />
<template v-if="isDashboardItem(item)">
<ContextMenuItem @click="handleEditDashboard(item)">
{{ t('nav_menu.edit_dashboard') }}
@@ -101,10 +101,10 @@
</SidebarContent>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem :disabled="!hasNotifications" @click="clearAllNotifications">
<ContextMenuItem v-if="hasNotifications" @click="clearAllNotifications">
{{ t('nav_menu.mark_all_read') }}
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuSeparator v-if="hasNotifications" />
<ContextMenuItem @click="handleQuickCreateDashboard">
{{ t('dashboard.new_dashboard') }}
</ContextMenuItem>
@@ -361,7 +361,8 @@
}
const { ok } = await modalStore.confirm({
title: t('dashboard.confirmations.delete_title'),
description: t('dashboard.confirmations.delete_description')
description: t('dashboard.confirmations.delete_description'),
destructive: true
});
if (!ok) {
return;

View File

@@ -94,11 +94,11 @@
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem
:disabled="!hasNotifications"
v-if="hasNotifications"
@click="emit('clear-notifications')">
{{ t('nav_menu.mark_all_read') }}
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuSeparator v-if="hasNotifications" />
<template v-if="isDashboardItem(entry)">
<ContextMenuItem @click="emit('edit-dashboard', entry)">
{{ t('nav_menu.edit_dashboard') }}
@@ -126,10 +126,10 @@
</div>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem :disabled="!hasNotifications" @click="emit('clear-notifications')">
<ContextMenuItem v-if="hasNotifications" @click="emit('clear-notifications')">
{{ t('nav_menu.mark_all_read') }}
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuSeparator v-if="hasNotifications" />
<ContextMenuItem @click="emit('create-dashboard')">
{{ t('dashboard.new_dashboard') }}
</ContextMenuItem>

View File

@@ -7,14 +7,15 @@
const props = defineProps({
asChild: { type: Boolean, required: false },
as: { type: null, required: false },
variant: { type: String, required: false },
class: { type: null, required: false }
});
const delegatedProps = reactiveOmit(props, 'class');
const delegatedProps = reactiveOmit(props, 'class', 'variant');
</script>
<template>
<AlertDialogAction v-bind="delegatedProps" :class="cn(buttonVariants(), props.class)">
<AlertDialogAction v-bind="delegatedProps" :class="cn(buttonVariants({ variant: props.variant }), props.class)">
<slot />
</AlertDialogAction>
</template>

View File

@@ -14,7 +14,7 @@
const modalStore = useModalStore();
const { alertOpen, alertMode, alertTitle, alertDescription, alertOkText, alertCancelText, alertDismissible } =
const { alertOpen, alertMode, alertTitle, alertDescription, alertOkText, alertCancelText, alertDismissible, alertDestructive } =
storeToRefs(modalStore);
function onEscapeKeyDown(event) {
@@ -60,7 +60,7 @@
{{ alertCancelText }}
</AlertDialogCancel>
<AlertDialogAction @click="modalStore.handleOk">
<AlertDialogAction :variant="alertDestructive ? 'destructive' : undefined" @click="modalStore.handleOk">
{{ alertOkText }}
</AlertDialogAction>
</AlertDialogFooter>

View File

@@ -232,7 +232,8 @@ export function promptClearAvatarHistory() {
modalStore
.confirm({
description: t('confirm.clear_avatar_history'),
title: 'Confirm'
title: t('confirm.title'),
destructive: true
})
.then(({ ok }) => {
if (!ok) return;
@@ -472,7 +473,7 @@ export function selectAvatarWithConfirmation(id) {
modalStore
.confirm({
description: t('confirm.select_avatar'),
title: 'Confirm'
title: t('confirm.title')
})
.then(({ ok }) => {
if (!ok) return;

View File

@@ -746,7 +746,8 @@ export function leaveGroupPrompt(groupId) {
modalStore
.confirm({
description: t('confirm.leave_group'),
title: t('confirm.title')
title: t('confirm.title'),
destructive: true
})
.then(({ ok }) => {
if (!ok) return;

View File

@@ -187,7 +187,8 @@
"tab_group": "Group",
"tab_other": "Other",
"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 File

@@ -15,6 +15,7 @@ import { useI18n } from 'vue-i18n';
* @property {string=} confirmText
* @property {string=} cancelText
* @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=} confirmText
* @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 alertCancelText = ref('');
const alertDismissible = ref(true);
const alertDestructive = ref(false);
const promptOpen = ref(false);
const promptTitle = ref('');
@@ -155,6 +158,7 @@ export const useModalStore = defineStore('Modal', () => {
alertTitle.value = options.title;
alertDescription.value = options.description;
alertDismissible.value = options.dismissible !== false;
alertDestructive.value = options.destructive === true;
if (mode === 'alert') {
alertOkText.value =
@@ -381,6 +385,7 @@ export const useModalStore = defineStore('Modal', () => {
alertOkText,
alertCancelText,
alertDismissible,
alertDestructive,
promptOpen,
promptTitle,
promptDescription,

View File

@@ -212,7 +212,8 @@
const handleDelete = async () => {
const { ok } = await modalStore.confirm({
title: t('dashboard.confirmations.delete_title'),
description: t('dashboard.confirmations.delete_description')
description: t('dashboard.confirmations.delete_description'),
destructive: true
});
if (!ok) {
return;

View File

@@ -880,7 +880,8 @@
invalidIdsText,
title: t('view.favorite.avatars.confirm_delete_invalid'),
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) {
@@ -1000,7 +1001,8 @@
modalStore
.confirm({
description: t('confirm.clear_group'),
title: t('confirm.title')
title: t('confirm.title'),
destructive: true
})
.then(({ ok }) => {
if (ok) {
@@ -1050,7 +1052,8 @@
modalStore
.confirm({
description: t('confirm.delete_group', { name: group }),
title: t('confirm.title')
title: t('confirm.title'),
destructive: true
})
.then(({ ok }) => {
if (ok) {
@@ -1223,9 +1226,9 @@
const total = selectedFavoriteAvatars.value.length;
modalStore
.confirm({
description: `Are you sure you want to unfavorite ${total} favorites?
This action cannot be undone.`,
title: `Delete ${total} favorites?`
description: t('confirm.bulk_unfavorite', { count: total }),
title: t('confirm.bulk_unfavorite_title', { count: total }),
destructive: true
})
.then(({ ok }) => {
if (ok) {

View File

@@ -26,7 +26,7 @@
<RefreshCw v-else />
</Button>
</TooltipWrapper>
<ContextMenu>
<ContextMenu v-if="hasUnseenNotifications">
<ContextMenuTrigger as-child>
<TooltipWrapper side="bottom" :content="t('side_panel.notification_center.title')">
<Button
@@ -36,17 +36,26 @@
@click="isNotificationCenterOpen = !isNotificationCenterOpen">
<Bell />
<span
v-if="hasUnseenNotifications"
class="absolute top-1 right-1.25 size-1.5 rounded-full bg-red-500" />
</Button>
</TooltipWrapper>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem :disabled="!hasUnseenNotifications" @click="markNotificationsRead">
<ContextMenuItem @click="markNotificationsRead">
{{ t('nav_menu.mark_all_read') }}
</ContextMenuItem>
</ContextMenuContent>
</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">
<PopoverTrigger as-child>
<Button class="rounded-full" variant="ghost" size="icon-sm">
@@ -246,6 +255,7 @@
SelectValue
} from '@/components/ui/select';
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 { Field, FieldContent, FieldLabel } from '@/components/ui/field';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';