diff --git a/src/components/dialogs/AvatarDialog/useAvatarDialogCommands.js b/src/components/dialogs/AvatarDialog/useAvatarDialogCommands.js index 6a2d0201..b04da142 100644 --- a/src/components/dialogs/AvatarDialog/useAvatarDialogCommands.js +++ b/src/components/dialogs/AvatarDialog/useAvatarDialogCommands.js @@ -203,27 +203,11 @@ export function useAvatarDialogCommands( // --- Command map --- // Direct commands: function // String commands: delegate to component callback - // Confirmed commands: { confirm: true, label: string, handler: fn } + // Confirmed commands: { confirm: () => ({title, description, ...}), handler: fn } function buildCommandMap() { const D = () => avatarDialog.value; - const confirmLabelMap = { - 'Delete Favorite': () => - t('dialog.avatar.actions.favorite_tooltip'), - 'Select Fallback Avatar': () => - t('dialog.avatar.actions.select_fallback'), - 'Block Avatar': () => t('dialog.avatar.actions.block'), - 'Unblock Avatar': () => t('dialog.avatar.actions.unblock'), - 'Make Public': () => t('dialog.avatar.actions.make_public'), - 'Make Private': () => t('dialog.avatar.actions.make_private'), - Delete: () => t('dialog.avatar.actions.delete'), - 'Delete Imposter': () => t('dialog.avatar.actions.delete_impostor'), - 'Create Imposter': () => t('dialog.avatar.actions.create_impostor'), - 'Regenerate Imposter': () => - t('dialog.avatar.actions.regenerate_impostor') - }; - return { // --- Direct commands --- Refresh: () => { @@ -254,15 +238,23 @@ export function useAvatarDialogCommands( // --- Confirmed commands --- 'Delete Favorite': { - confirm: true, - label: confirmLabelMap['Delete Favorite'], + confirm: () => ({ + title: t('confirm.title'), + description: t('confirm.command_question', { + command: t('dialog.avatar.actions.favorite_tooltip') + }) + }), handler: (id) => { favoriteRequest.deleteFavorite({ objectId: id }); } }, 'Select Fallback Avatar': { - confirm: true, - label: confirmLabelMap['Select Fallback Avatar'], + confirm: () => ({ + title: t('confirm.title'), + description: t('confirm.command_question', { + command: t('dialog.avatar.actions.select_fallback') + }) + }), handler: (id) => { avatarRequest .selectFallbackAvatar({ avatarId: id }) @@ -273,8 +265,12 @@ export function useAvatarDialogCommands( } }, 'Block Avatar': { - confirm: true, - label: confirmLabelMap['Block Avatar'], + confirm: () => ({ + title: t('confirm.title'), + description: t('confirm.command_question', { + command: t('dialog.avatar.actions.block') + }) + }), handler: (id) => { avatarModerationRequest .sendAvatarModeration({ @@ -290,8 +286,12 @@ export function useAvatarDialogCommands( } }, 'Unblock Avatar': { - confirm: true, - label: confirmLabelMap['Unblock Avatar'], + confirm: () => ({ + title: t('confirm.title'), + description: t('confirm.command_question', { + command: t('dialog.avatar.actions.unblock') + }) + }), handler: (id) => { avatarModerationRequest .deleteAvatarModeration({ @@ -313,8 +313,12 @@ export function useAvatarDialogCommands( } }, 'Make Public': { - confirm: true, - label: confirmLabelMap['Make Public'], + confirm: () => ({ + title: t('confirm.title'), + description: t('confirm.command_question', { + command: t('dialog.avatar.actions.make_public') + }) + }), handler: (id) => { avatarRequest .saveAvatar({ id, releaseStatus: 'public' }) @@ -326,8 +330,12 @@ export function useAvatarDialogCommands( } }, 'Make Private': { - confirm: true, - label: confirmLabelMap['Make Private'], + confirm: () => ({ + title: t('confirm.title'), + description: t('confirm.command_question', { + command: t('dialog.avatar.actions.make_private') + }) + }), handler: (id) => { avatarRequest .saveAvatar({ id, releaseStatus: 'private' }) @@ -339,8 +347,12 @@ export function useAvatarDialogCommands( } }, Delete: { - confirm: true, - label: confirmLabelMap['Delete'], + confirm: () => ({ + title: t('confirm.title'), + description: t('confirm.command_question', { + command: t('dialog.avatar.actions.delete') + }) + }), handler: (id) => { avatarRequest .deleteAvatar({ avatarId: id }) @@ -365,8 +377,12 @@ export function useAvatarDialogCommands( } }, 'Delete Imposter': { - confirm: true, - label: confirmLabelMap['Delete Imposter'], + confirm: () => ({ + title: t('confirm.title'), + description: t('confirm.command_question', { + command: t('dialog.avatar.actions.delete_impostor') + }) + }), handler: (id) => { avatarRequest .deleteImposter({ avatarId: id }) @@ -378,8 +394,12 @@ export function useAvatarDialogCommands( } }, 'Create Imposter': { - confirm: true, - label: confirmLabelMap['Create Imposter'], + confirm: () => ({ + title: t('confirm.title'), + description: t('confirm.command_question', { + command: t('dialog.avatar.actions.create_impostor') + }) + }), handler: (id) => { avatarRequest .createImposter({ avatarId: id }) @@ -390,8 +410,12 @@ export function useAvatarDialogCommands( } }, 'Regenerate Imposter': { - confirm: true, - label: confirmLabelMap['Regenerate Imposter'], + confirm: () => ({ + title: t('confirm.title'), + description: t('confirm.command_question', { + command: t('dialog.avatar.actions.regenerate_impostor') + }) + }), handler: (id) => { avatarRequest .deleteImposter({ avatarId: id }) @@ -456,20 +480,11 @@ export function useAvatarDialogCommands( // Confirmed command if (entry.confirm) { - const displayLabel = - typeof entry.label === 'function' ? entry.label() : command; - modalStore - .confirm({ - title: t('confirm.title'), - description: t('confirm.command_question', { - command: displayLabel - }) - }) - .then(({ ok }) => { - if (ok) { - entry.handler(D.id); - } - }); + modalStore.confirm(entry.confirm()).then(({ ok }) => { + if (ok) { + entry.handler(D.id); + } + }); } } diff --git a/src/components/dialogs/GroupDialog/GroupDialog.vue b/src/components/dialogs/GroupDialog/GroupDialog.vue index b792cdc1..0f164625 100644 --- a/src/components/dialogs/GroupDialog/GroupDialog.vue +++ b/src/components/dialogs/GroupDialog/GroupDialog.vue @@ -413,6 +413,7 @@ import DialogJsonTab from '../DialogJsonTab.vue'; import GroupDialogInfoTab from './GroupDialogInfoTab.vue'; + import { useGroupDialogCommands } from './useGroupDialogCommands'; import GroupDialogMembersTab from './GroupDialogMembersTab.vue'; import GroupDialogPhotosTab from './GroupDialogPhotosTab.vue'; import GroupDialogPostsTab from './GroupDialogPostsTab.vue'; @@ -444,6 +445,28 @@ const { showFullscreenImageDialog } = useGalleryStore(); + const { groupDialogCommand } = useGroupDialogCommands(groupDialog, { + t, + modalStore, + currentUser, + showGroupDialog, + leaveGroupPrompt, + setGroupVisibility, + setGroupSubscription, + showGroupMemberModerationDialog, + showInviteGroupDialog: (groupId, userId) => { + if (groupId) { + inviteGroupDialog.value.groupId = groupId; + } + if (userId) { + inviteGroupDialog.value.userId = userId; + } + inviteGroupDialog.value.visible = true; + }, + showGroupPostEditDialog, + groupRequest + }); + const groupDialogTabCurrentName = ref('0'); const treeData = ref({}); const membersTabRef = ref(null); @@ -474,20 +497,7 @@ } ); - /** - * - * @param groupId - * @param userId - */ - function showInviteGroupDialog(groupId, userId) { - if (groupId) { - inviteGroupDialog.value.groupId = groupId; - } - if (userId) { - inviteGroupDialog.value.userId = userId; - } - inviteGroupDialog.value.visible = true; - } + /** * @@ -594,109 +604,7 @@ * @param gallery */ - /** - * - * @param command - */ - function groupDialogCommand(command) { - const D = groupDialog.value; - if (D.visible === false) { - return; - } - switch (command) { - case 'Share': - copyToClipboard(groupDialog.value.ref.$url); - break; - case 'Create Post': - showGroupPostEditDialog(groupDialog.value.id, null); - break; - case 'Moderation Tools': - showGroupMemberModerationDialog(groupDialog.value.id); - break; - case 'Invite To Group': - showInviteGroupDialog(D.id, ''); - break; - case 'Refresh': - const groupId = D.id; - showGroupDialog(groupId, { forceRefresh: true }); - break; - case 'Leave Group': - leaveGroupPrompt(D.id); - break; - case 'Block Group': - blockGroup(D.id); - break; - case 'Unblock Group': - unblockGroup(D.id); - break; - case 'Visibility Everyone': - setGroupVisibility(D.id, 'visible'); - break; - case 'Visibility Friends': - setGroupVisibility(D.id, 'friends'); - break; - case 'Visibility Hidden': - setGroupVisibility(D.id, 'hidden'); - break; - case 'Subscribe To Announcements': - setGroupSubscription(D.id, true); - break; - case 'Unsubscribe To Announcements': - setGroupSubscription(D.id, false); - break; - } - } - /** - * - * @param groupId - */ - function blockGroup(groupId) { - modalStore - .confirm({ - description: t('confirm.block_group'), - title: t('confirm.title') - }) - .then(({ ok }) => { - if (!ok) return; - groupRequest - .blockGroup({ - groupId - }) - .then((args) => { - if (groupDialog.value.visible && groupDialog.value.id === args.params.groupId) { - showGroupDialog(args.params.groupId); - } - }); - }) - .catch(() => {}); - } - - /** - * - * @param groupId - */ - function unblockGroup(groupId) { - modalStore - .confirm({ - description: t('confirm.unblock_group'), - title: t('confirm.title') - }) - .then(({ ok }) => { - if (!ok) return; - groupRequest - .unblockGroup({ - groupId, - userId: currentUser.value.id - }) - .then((args) => { - if (groupDialog.value.visible && groupDialog.value.id === args.params.groupId) { - showGroupDialog(args.params.groupId); - } - }); - }) - .catch(() => {}); - } /** * diff --git a/src/components/dialogs/GroupDialog/__tests__/useGroupDialogCommands.test.js b/src/components/dialogs/GroupDialog/__tests__/useGroupDialogCommands.test.js new file mode 100644 index 00000000..23ca2865 --- /dev/null +++ b/src/components/dialogs/GroupDialog/__tests__/useGroupDialogCommands.test.js @@ -0,0 +1,140 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { ref } from 'vue'; + +import { useGroupDialogCommands } from '../useGroupDialogCommands'; + +vi.mock('../../../../shared/utils', () => ({ + copyToClipboard: vi.fn() +})); + +const { copyToClipboard } = await import('../../../../shared/utils'); + +function createGroupDialog(overrides = {}) { + return ref({ + visible: true, + id: 'grp_123', + ref: { + $url: 'https://vrchat.com/home/group/grp_123' + }, + ...overrides + }); +} + +function createDeps(overrides = {}) { + return { + t: vi.fn((key) => key), + modalStore: { + confirm: vi.fn().mockResolvedValue({ ok: true }) + }, + currentUser: ref({ id: 'usr_current' }), + showGroupDialog: vi.fn(), + leaveGroupPrompt: vi.fn(), + setGroupVisibility: vi.fn(), + setGroupSubscription: vi.fn(), + showGroupMemberModerationDialog: vi.fn(), + showInviteGroupDialog: vi.fn(), + showGroupPostEditDialog: vi.fn(), + groupRequest: { + blockGroup: vi.fn().mockResolvedValue({ + params: { groupId: 'grp_123' } + }), + unblockGroup: vi.fn().mockResolvedValue({ + params: { groupId: 'grp_123' } + }) + }, + ...overrides + }; +} + +describe('useGroupDialogCommands', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('returns early when dialog is not visible', () => { + const groupDialog = createGroupDialog({ visible: false }); + const deps = createDeps(); + const { groupDialogCommand } = useGroupDialogCommands(groupDialog, deps); + + groupDialogCommand('Refresh'); + expect(deps.showGroupDialog).not.toHaveBeenCalled(); + }); + + it('Share copies group URL', () => { + const groupDialog = createGroupDialog(); + const deps = createDeps(); + const { groupDialogCommand } = useGroupDialogCommands(groupDialog, deps); + + groupDialogCommand('Share'); + expect(copyToClipboard).toHaveBeenCalledWith( + 'https://vrchat.com/home/group/grp_123' + ); + }); + + it('Invite To Group dispatches invite callback', () => { + const groupDialog = createGroupDialog(); + const deps = createDeps(); + const { groupDialogCommand } = useGroupDialogCommands(groupDialog, deps); + + groupDialogCommand('Invite To Group'); + expect(deps.showInviteGroupDialog).toHaveBeenCalledWith('grp_123', ''); + }); + + it('Refresh calls showGroupDialog with forceRefresh', () => { + const groupDialog = createGroupDialog(); + const deps = createDeps(); + const { groupDialogCommand } = useGroupDialogCommands(groupDialog, deps); + + groupDialogCommand('Refresh'); + expect(deps.showGroupDialog).toHaveBeenCalledWith('grp_123', { + forceRefresh: true + }); + }); + + it('Block Group confirms and calls blockGroup', async () => { + const groupDialog = createGroupDialog(); + const deps = createDeps(); + const { groupDialogCommand } = useGroupDialogCommands(groupDialog, deps); + + groupDialogCommand('Block Group'); + await vi.waitFor(() => { + expect(deps.modalStore.confirm).toHaveBeenCalled(); + expect(deps.groupRequest.blockGroup).toHaveBeenCalledWith({ + groupId: 'grp_123' + }); + expect(deps.showGroupDialog).toHaveBeenCalledWith('grp_123'); + }); + }); + + it('Unblock Group confirms and calls unblockGroup', async () => { + const groupDialog = createGroupDialog(); + const deps = createDeps(); + const { groupDialogCommand } = useGroupDialogCommands(groupDialog, deps); + + groupDialogCommand('Unblock Group'); + await vi.waitFor(() => { + expect(deps.modalStore.confirm).toHaveBeenCalled(); + expect(deps.groupRequest.unblockGroup).toHaveBeenCalledWith({ + groupId: 'grp_123', + userId: 'usr_current' + }); + expect(deps.showGroupDialog).toHaveBeenCalledWith('grp_123'); + }); + }); + + it('does not run confirmed action when confirmation is cancelled', async () => { + const groupDialog = createGroupDialog(); + const deps = createDeps({ + modalStore: { + confirm: vi.fn().mockResolvedValue({ ok: false }) + } + }); + const { groupDialogCommand } = useGroupDialogCommands(groupDialog, deps); + + groupDialogCommand('Block Group'); + await vi.waitFor(() => { + expect(deps.modalStore.confirm).toHaveBeenCalled(); + }); + expect(deps.groupRequest.blockGroup).not.toHaveBeenCalled(); + }); +}); diff --git a/src/components/dialogs/GroupDialog/useGroupDialogCommands.js b/src/components/dialogs/GroupDialog/useGroupDialogCommands.js new file mode 100644 index 00000000..d9e110bb --- /dev/null +++ b/src/components/dialogs/GroupDialog/useGroupDialogCommands.js @@ -0,0 +1,164 @@ +import { copyToClipboard } from '../../../shared/utils'; + +/** + * Composable for GroupDialog command dispatch. + * Uses a command map pattern consistent with Avatar/World/User dialogs. + * @param {import('vue').Ref} groupDialog - reactive ref to the group dialog state + * @param {object} deps - external dependencies + * @param deps.t + * @param deps.modalStore + * @param deps.currentUser + * @param deps.showGroupDialog + * @param deps.leaveGroupPrompt + * @param deps.setGroupVisibility + * @param deps.setGroupSubscription + * @param deps.showGroupMemberModerationDialog + * @param deps.showInviteGroupDialog + * @param deps.showGroupPostEditDialog + * @param deps.groupRequest + * @returns {object} command composable API + */ +export function useGroupDialogCommands( + groupDialog, + { + t, + modalStore, + currentUser, + showGroupDialog, + leaveGroupPrompt, + setGroupVisibility, + setGroupSubscription, + showGroupMemberModerationDialog, + showInviteGroupDialog, + showGroupPostEditDialog, + groupRequest + } +) { + // --- Command map --- + // Direct commands: function + // Confirmed commands: { confirm: () => ({title, description, ...}), handler: fn } + + /** + * + */ + function buildCommandMap() { + const D = () => groupDialog.value; + + return { + // --- Direct commands --- + Share: () => { + copyToClipboard(D().ref.$url); + }, + 'Create Post': () => { + showGroupPostEditDialog(D().id, null); + }, + 'Moderation Tools': () => { + showGroupMemberModerationDialog(D().id); + }, + 'Invite To Group': () => { + showInviteGroupDialog(D().id, ''); + }, + Refresh: () => { + showGroupDialog(D().id, { forceRefresh: true }); + }, + 'Leave Group': () => { + leaveGroupPrompt(D().id); + }, + 'Visibility Everyone': () => { + setGroupVisibility(D().id, 'visible'); + }, + 'Visibility Friends': () => { + setGroupVisibility(D().id, 'friends'); + }, + 'Visibility Hidden': () => { + setGroupVisibility(D().id, 'hidden'); + }, + 'Subscribe To Announcements': () => { + setGroupSubscription(D().id, true); + }, + 'Unsubscribe To Announcements': () => { + setGroupSubscription(D().id, false); + }, + + // --- Confirmed commands --- + 'Block Group': { + confirm: () => ({ + title: t('confirm.title'), + description: t('confirm.block_group') + }), + handler: (id) => { + groupRequest.blockGroup({ groupId: id }).then((args) => { + if ( + groupDialog.value.visible && + groupDialog.value.id === args.params.groupId + ) { + showGroupDialog(args.params.groupId); + } + }); + } + }, + 'Unblock Group': { + confirm: () => ({ + title: t('confirm.title'), + description: t('confirm.unblock_group') + }), + handler: (id) => { + groupRequest + .unblockGroup({ + groupId: id, + userId: currentUser.value.id + }) + .then((args) => { + if ( + groupDialog.value.visible && + groupDialog.value.id === args.params.groupId + ) { + showGroupDialog(args.params.groupId); + } + }); + } + } + }; + } + + const commandMap = buildCommandMap(); + + /** + * Dispatch a group dialog command. + * @param {string} command + */ + function groupDialogCommand(command) { + const D = groupDialog.value; + if (D.visible === false) { + return; + } + + const entry = commandMap[command]; + + if (!entry) { + return; + } + + // Direct function + if (typeof entry === 'function') { + entry(); + return; + } + + // Confirmed command + if (entry.confirm) { + modalStore + .confirm(entry.confirm()) + .then(({ ok }) => { + if (ok) { + entry.handler(D.id); + } + }) + .catch(() => {}); + } + } + + return { + groupDialogCommand + }; +} diff --git a/src/components/dialogs/UserDialog/useUserDialogCommands.js b/src/components/dialogs/UserDialog/useUserDialogCommands.js index 39b7f6e7..8b5bb3d4 100644 --- a/src/components/dialogs/UserDialog/useUserDialogCommands.js +++ b/src/components/dialogs/UserDialog/useUserDialogCommands.js @@ -216,7 +216,7 @@ export function useUserDialogCommands( // --- Command map --- // Direct commands: function - // Confirmed commands: { confirm: true, handler: fn } + // Confirmed commands: { confirm: () => ({title, description, ...}), handler: fn } /** * @@ -360,7 +360,12 @@ export function useUserDialogCommands( // --- Confirmed commands --- 'Delete Favorite': { - confirm: true, + confirm: () => ({ + title: t('confirm.title'), + description: t('confirm.command_question', { + command: t('dialog.user.actions.delete_favorite') + }) + }), handler: (userId) => { favoriteRequest.deleteFavorite({ objectId: userId @@ -368,7 +373,12 @@ export function useUserDialogCommands( } }, 'Accept Friend Request': { - confirm: true, + confirm: () => ({ + title: t('confirm.title'), + description: t('confirm.command_question', { + command: t('dialog.user.actions.accept_friend_request') + }) + }), handler: async (userId) => { const key = getFriendRequest(userId); if (key === '') { @@ -401,7 +411,12 @@ export function useUserDialogCommands( } }, 'Decline Friend Request': { - confirm: true, + confirm: () => ({ + title: t('confirm.title'), + description: t('confirm.command_question', { + command: t('dialog.user.actions.decline_friend_request') + }) + }), handler: async (userId) => { const key = getFriendRequest(userId); if (key === '') { @@ -423,7 +438,12 @@ export function useUserDialogCommands( } }, 'Cancel Friend Request': { - confirm: true, + confirm: () => ({ + title: t('confirm.title'), + description: t('confirm.command_question', { + command: t('dialog.user.actions.cancel_friend_request') + }) + }), handler: async (userId) => { const args = await friendRequest.cancelFriendRequest({ userId @@ -432,7 +452,12 @@ export function useUserDialogCommands( } }, 'Send Friend Request': { - confirm: true, + confirm: () => ({ + title: t('confirm.title'), + description: t('confirm.command_question', { + command: t('dialog.user.actions.send_friend_request') + }) + }), handler: async (userId) => { const args = await friendRequest.sendFriendRequest({ userId @@ -441,7 +466,12 @@ export function useUserDialogCommands( } }, 'Moderation Unblock': { - confirm: true, + confirm: () => ({ + title: t('confirm.title'), + description: t('confirm.command_question', { + command: t('dialog.user.actions.moderation_unblock') + }) + }), handler: async (userId) => { const args = await playerModerationRequest.deletePlayerModeration({ @@ -452,7 +482,12 @@ export function useUserDialogCommands( } }, 'Moderation Block': { - confirm: true, + confirm: () => ({ + title: t('confirm.title'), + description: t('confirm.command_question', { + command: t('dialog.user.actions.moderation_block') + }) + }), handler: async (userId) => { const args = await playerModerationRequest.sendPlayerModeration({ @@ -463,7 +498,12 @@ export function useUserDialogCommands( } }, 'Moderation Unmute': { - confirm: true, + confirm: () => ({ + title: t('confirm.title'), + description: t('confirm.command_question', { + command: t('dialog.user.actions.moderation_unmute') + }) + }), handler: async (userId) => { const args = await playerModerationRequest.deletePlayerModeration({ @@ -474,7 +514,12 @@ export function useUserDialogCommands( } }, 'Moderation Mute': { - confirm: true, + confirm: () => ({ + title: t('confirm.title'), + description: t('confirm.command_question', { + command: t('dialog.user.actions.moderation_mute') + }) + }), handler: async (userId) => { const args = await playerModerationRequest.sendPlayerModeration({ @@ -485,7 +530,14 @@ export function useUserDialogCommands( } }, 'Moderation Enable Avatar Interaction': { - confirm: true, + confirm: () => ({ + title: t('confirm.title'), + description: t('confirm.command_question', { + command: t( + 'dialog.user.actions.moderation_enable_avatar_interaction' + ) + }) + }), handler: async (userId) => { const args = await playerModerationRequest.deletePlayerModeration({ @@ -496,7 +548,14 @@ export function useUserDialogCommands( } }, 'Moderation Disable Avatar Interaction': { - confirm: true, + confirm: () => ({ + title: t('confirm.title'), + description: t('confirm.command_question', { + command: t( + 'dialog.user.actions.moderation_disable_avatar_interaction' + ) + }) + }), handler: async (userId) => { const args = await playerModerationRequest.sendPlayerModeration({ @@ -507,7 +566,14 @@ export function useUserDialogCommands( } }, 'Moderation Enable Chatbox': { - confirm: true, + confirm: () => ({ + title: t('confirm.title'), + description: t('confirm.command_question', { + command: t( + 'dialog.user.actions.moderation_enable_chatbox' + ) + }) + }), handler: async (userId) => { const args = await playerModerationRequest.deletePlayerModeration({ @@ -518,7 +584,14 @@ export function useUserDialogCommands( } }, 'Moderation Disable Chatbox': { - confirm: true, + confirm: () => ({ + title: t('confirm.title'), + description: t('confirm.command_question', { + command: t( + 'dialog.user.actions.moderation_disable_chatbox' + ) + }) + }), handler: async (userId) => { const args = await playerModerationRequest.sendPlayerModeration({ @@ -529,7 +602,12 @@ export function useUserDialogCommands( } }, 'Report Hacking': { - confirm: true, + confirm: () => ({ + title: t('confirm.title'), + description: t('confirm.command_question', { + command: t('dialog.user.actions.report_hacking') + }) + }), handler: (userId) => { miscRequest.reportUser({ userId, @@ -540,7 +618,12 @@ export function useUserDialogCommands( } }, Unfriend: { - confirm: true, + confirm: () => ({ + title: t('confirm.title'), + description: t('confirm.command_question', { + command: t('dialog.user.actions.unfriend') + }) + }), handler: async (userId) => { const args = await friendRequest.deleteFriend( { @@ -601,23 +684,8 @@ export function useUserDialogCommands( // Confirmed command if (entry.confirm) { - const i18nPreFix = 'dialog.user.actions.'; - const formattedCommand = command.toLowerCase().replace(/ /g, '_'); - const displayCommandText = t( - `${i18nPreFix}${formattedCommand}` - ).includes('i18nPreFix') - ? command - : t(`${i18nPreFix}${formattedCommand}`); - modalStore - .confirm({ - description: t('confirm.message', { - command: displayCommandText - }), - title: t('confirm.title'), - confirmText: t('confirm.confirm_button'), - cancelText: t('confirm.cancel_button') - }) + .confirm(entry.confirm()) .then(({ ok }) => { if (ok) { entry.handler(D.id); diff --git a/src/components/dialogs/WorldDialog/WorldDialog.vue b/src/components/dialogs/WorldDialog/WorldDialog.vue index 4b396500..6caba9e7 100644 --- a/src/components/dialogs/WorldDialog/WorldDialog.vue +++ b/src/components/dialogs/WorldDialog/WorldDialog.vue @@ -385,7 +385,6 @@ import { useAdvancedSettingsStore, - useAppearanceSettingsStore, useFavoriteStore, useGalleryStore, useGameStore, @@ -442,7 +441,9 @@ worldDialogCommand, onFileChangeWorldImage, onCropConfirmWorld, - copyWorldName + copyWorldName, + showWorldAllowedDomainsDialog, + registerCallbacks } = useWorldDialogCommands(worldDialog, { t, toast, @@ -456,6 +457,18 @@ showFullscreenImageDialog }); + registerCallbacks({ + showSetWorldTagsDialog: () => { + isSetWorldTagsDialogVisible.value = true; + }, + showWorldAllowedDomainsDialog: () => { + showWorldAllowedDomainsDialog(); + }, + showChangeWorldImageDialog: () => { + document.getElementById('WorldImageUploadButton').click(); + } + }); + const worldDialogTabs = computed(() => [ { value: 'Instances', label: t('dialog.world.instances.header') }, { value: 'Info', label: t('dialog.world.info.header') }, diff --git a/src/components/dialogs/WorldDialog/__tests__/useWorldDialogCommands.test.js b/src/components/dialogs/WorldDialog/__tests__/useWorldDialogCommands.test.js index ccc22bce..92e0e5f1 100644 --- a/src/components/dialogs/WorldDialog/__tests__/useWorldDialogCommands.test.js +++ b/src/components/dialogs/WorldDialog/__tests__/useWorldDialogCommands.test.js @@ -184,8 +184,17 @@ describe('useWorldDialogCommands', () => { test('Change Tags sets isSetWorldTagsDialogVisible to true', () => { const worldDialog = createWorldDialog(); const deps = createDeps(); - const { worldDialogCommand, isSetWorldTagsDialogVisible } = - useWorldDialogCommands(worldDialog, deps); + const { + worldDialogCommand, + isSetWorldTagsDialogVisible, + registerCallbacks + } = useWorldDialogCommands(worldDialog, deps); + + registerCallbacks({ + showSetWorldTagsDialog: () => { + isSetWorldTagsDialogVisible.value = true; + } + }); worldDialogCommand('Change Tags'); expect(isSetWorldTagsDialogVisible.value).toBe(true); @@ -225,8 +234,18 @@ describe('useWorldDialogCommands', () => { test('Change Allowed Domains opens the allowed domains dialog', () => { const worldDialog = createWorldDialog(); const deps = createDeps(); - const { worldDialogCommand, worldAllowedDomainsDialog } = - useWorldDialogCommands(worldDialog, deps); + const { + worldDialogCommand, + worldAllowedDomainsDialog, + showWorldAllowedDomainsDialog, + registerCallbacks + } = useWorldDialogCommands(worldDialog, deps); + + registerCallbacks({ + showWorldAllowedDomainsDialog: () => { + showWorldAllowedDomainsDialog(); + } + }); worldDialogCommand('Change Allowed Domains'); expect(worldAllowedDomainsDialog.value.visible).toBe(true); diff --git a/src/components/dialogs/WorldDialog/useWorldDialogCommands.js b/src/components/dialogs/WorldDialog/useWorldDialogCommands.js index 790f6fcb..5fda0352 100644 --- a/src/components/dialogs/WorldDialog/useWorldDialogCommands.js +++ b/src/components/dialogs/WorldDialog/useWorldDialogCommands.js @@ -57,13 +57,6 @@ export function useWorldDialogCommands( const cropDialogFile = ref(null); const changeWorldImageLoading = ref(false); - /** - * - */ - function showChangeWorldImageDialog() { - document.getElementById('WorldImageUploadButton').click(); - } - /** * * @param e @@ -372,207 +365,240 @@ export function useWorldDialogCommands( .catch(() => {}); } + // --- Command map --- + // Direct commands: function + // String commands: delegate to component callback + // Confirmed commands: { confirm: () => ({title, description, ...}), handler: fn } + + function buildCommandMap() { + const D = () => worldDialog.value; + + return { + // --- Direct commands --- + Refresh: () => { + const { tag, shortName } = D().$location; + showWorldDialog(tag, shortName, { forceRefresh: true }); + }, + Share: () => { + copyWorldUrl(); + }, + 'Previous Instances': () => { + showPreviousInstancesListDialog(D().ref); + }, + 'New Instance': () => { + showNewInstanceDialog(D().$location.tag); + }, + 'New Instance and Self Invite': () => { + newInstanceSelfInvite(D().id); + }, + 'Add Favorite': () => { + showFavoriteDialog('world', D().id); + }, + 'Download Unity Package': () => { + openExternalLink(replaceVrcPackageUrl(D().ref.unityPackageUrl)); + }, + Rename: () => { + promptRenameWorld(D()); + }, + 'Change Description': () => { + promptChangeWorldDescription(D()); + }, + 'Change Capacity': () => { + promptChangeWorldCapacity(D()); + }, + 'Change Recommended Capacity': () => { + promptChangeWorldRecommendedCapacity(D()); + }, + 'Change YouTube Preview': () => { + promptChangeWorldYouTubePreview(D()); + }, + + // --- Delegated to component --- + 'Change Tags': 'showSetWorldTagsDialog', + 'Change Allowed Domains': 'showWorldAllowedDomainsDialog', + 'Change Image': 'showChangeWorldImageDialog', + + // --- Confirmed commands --- + 'Delete Favorite': { + confirm: () => ({ + title: t('confirm.title'), + description: t('confirm.command_question', { + command: t('dialog.world.actions.favorites_tooltip') + }) + }), + handler: (id) => { + favoriteRequest.deleteFavorite({ objectId: id }); + } + }, + 'Make Home': { + confirm: () => ({ + title: t('confirm.title'), + description: t('confirm.command_question', { + command: t('dialog.world.actions.make_home') + }) + }), + handler: (id) => { + userRequest + .saveCurrentUser({ homeLocation: id }) + .then((args) => { + toast.success(t('message.world.home_updated')); + return args; + }); + } + }, + 'Reset Home': { + confirm: () => ({ + title: t('confirm.title'), + description: t('confirm.command_question', { + command: t('dialog.world.actions.reset_home') + }) + }), + handler: () => { + userRequest + .saveCurrentUser({ homeLocation: '' }) + .then((args) => { + toast.success(t('message.world.home_reset')); + return args; + }); + } + }, + Publish: { + confirm: () => ({ + title: t('confirm.title'), + description: t('confirm.command_question', { + command: t('dialog.world.actions.publish_to_labs') + }) + }), + handler: (id) => { + worldRequest.publishWorld({ worldId: id }).then((args) => { + toast.success(t('message.world.published')); + return args; + }); + } + }, + Unpublish: { + confirm: () => ({ + title: t('confirm.title'), + description: t('confirm.command_question', { + command: t('dialog.world.actions.unpublish') + }) + }), + handler: (id) => { + worldRequest + .unpublishWorld({ worldId: id }) + .then((args) => { + toast.success(t('message.world.unpublished')); + return args; + }); + } + }, + 'Delete Persistent Data': { + confirm: () => ({ + title: t('confirm.title'), + description: t('confirm.command_question', { + command: t( + 'dialog.world.actions.delete_persistent_data' + ) + }) + }), + handler: (id) => { + miscRequest + .deleteWorldPersistData({ worldId: id }) + .then((args) => { + if ( + args.params.worldId === worldDialog.value.id && + worldDialog.value.visible + ) { + worldDialog.value.hasPersistData = false; + } + toast.success( + t('message.world.persistent_data_deleted') + ); + return args; + }); + } + }, + Delete: { + confirm: () => ({ + title: t('confirm.title'), + description: t('confirm.command_question', { + command: t('dialog.world.actions.delete') + }) + }), + handler: (id) => { + worldRequest.deleteWorld({ worldId: id }).then((args) => { + const { json } = args; + cachedWorlds.delete(json.id); + if (worldDialog.value.ref.authorId === json.authorId) { + const map = new Map(); + for (const ref of cachedWorlds.values()) { + if (ref.authorId === json.authorId) { + map.set(ref.id, ref); + } + } + const array = Array.from(map.values()); + userDialog.value.worlds = array; + } + toast.success(t('message.world.deleted')); + worldDialog.value.visible = false; + return args; + }); + } + } + }; + } + + const commandMap = buildCommandMap(); + + // Callbacks for string-type commands (delegated to component) + let componentCallbacks = {}; + /** - * - * @param command + * Register component-level callbacks for string-type commands. + * @param {object} callbacks + */ + function registerCallbacks(callbacks) { + componentCallbacks = callbacks; + } + + /** + * Dispatch a world dialog command. + * @param {string} command */ function worldDialogCommand(command) { const D = worldDialog.value; if (D.visible === false) { return; } - switch (command) { - case 'Delete Favorite': - case 'Make Home': - case 'Reset Home': - case 'Publish': - case 'Unpublish': - case 'Delete Persistent Data': - case 'Delete': - const commandLabelMap = { - 'Delete Favorite': t( - 'dialog.world.actions.favorites_tooltip' - ), - 'Make Home': t('dialog.world.actions.make_home'), - 'Reset Home': t('dialog.world.actions.reset_home'), - Publish: t('dialog.world.actions.publish_to_labs'), - Unpublish: t('dialog.world.actions.unpublish'), - 'Delete Persistent Data': t( - 'dialog.world.actions.delete_persistent_data' - ), - Delete: t('dialog.world.actions.delete') - }; - modalStore - .confirm({ - description: t('confirm.command_question', { - command: commandLabelMap[command] ?? command - }), - title: t('confirm.title') - }) - .then(({ ok }) => { - if (!ok) return; - switch (command) { - case 'Delete Favorite': - favoriteRequest.deleteFavorite({ - objectId: D.id - }); - break; - case 'Make Home': - userRequest - .saveCurrentUser({ - homeLocation: D.id - }) - .then((args) => { - toast.success( - t('message.world.home_updated') - ); - return args; - }); - break; - case 'Reset Home': - userRequest - .saveCurrentUser({ - homeLocation: '' - }) - .then((args) => { - toast.success( - t('message.world.home_reset') - ); - return args; - }); - break; - case 'Publish': - worldRequest - .publishWorld({ - worldId: D.id - }) - .then((args) => { - toast.success( - t('message.world.published') - ); - return args; - }); - break; - case 'Unpublish': - worldRequest - .unpublishWorld({ - worldId: D.id - }) - .then((args) => { - toast.success( - t('message.world.unpublished') - ); - return args; - }); - break; - case 'Delete Persistent Data': - miscRequest - .deleteWorldPersistData({ - worldId: D.id - }) - .then((args) => { - if ( - args.params.worldId === - worldDialog.value.id && - worldDialog.value.visible - ) { - worldDialog.value.hasPersistData = false; - } - toast.success( - t( - 'message.world.persistent_data_deleted' - ) - ); - return args; - }); - break; - case 'Delete': - worldRequest - .deleteWorld({ - worldId: D.id - }) - .then((args) => { - const { json } = args; - cachedWorlds.delete(json.id); - if ( - worldDialog.value.ref.authorId === - json.authorId - ) { - const map = new Map(); - for (const ref of cachedWorlds.values()) { - if ( - ref.authorId === - json.authorId - ) { - map.set(ref.id, ref); - } - } - const array = Array.from( - map.values() - ); - userDialog.value.worlds = array; - } - toast.success( - t('message.world.deleted') - ); - D.visible = false; - return args; - }); - break; - } - }) - .catch(() => {}); - break; - case 'Previous Instances': - showPreviousInstancesListDialog(D.ref); - break; - case 'Share': - copyWorldUrl(); - break; - case 'Change Allowed Domains': - showWorldAllowedDomainsDialog(); - break; - case 'Change Tags': - isSetWorldTagsDialogVisible.value = true; - break; - case 'Download Unity Package': - openExternalLink( - replaceVrcPackageUrl(worldDialog.value.ref.unityPackageUrl) - ); - break; - case 'Change Image': - showChangeWorldImageDialog(); - break; - case 'Refresh': - const { tag, shortName } = worldDialog.value.$location; - showWorldDialog(tag, shortName, { forceRefresh: true }); - break; - case 'New Instance': - showNewInstanceDialog(D.$location.tag); - break; - case 'Add Favorite': - showFavoriteDialog('world', D.id); - break; - case 'New Instance and Self Invite': - newInstanceSelfInvite(D.id); - break; - case 'Rename': - promptRenameWorld(D); - break; - case 'Change Description': - promptChangeWorldDescription(D); - break; - case 'Change Capacity': - promptChangeWorldCapacity(D); - break; - case 'Change Recommended Capacity': - promptChangeWorldRecommendedCapacity(D); - break; - case 'Change YouTube Preview': - promptChangeWorldYouTubePreview(D); - break; - default: - break; + + const entry = commandMap[command]; + + if (!entry) { + return; + } + + // String entry => delegate to component callback + if (typeof entry === 'string') { + const cb = componentCallbacks[entry]; + if (cb) { + cb(); + } + return; + } + + // Direct function + if (typeof entry === 'function') { + entry(); + return; + } + + // Confirmed command + if (entry.confirm) { + modalStore.confirm(entry.confirm()).then(({ ok }) => { + if (ok) { + entry.handler(D.id); + } + }); } } @@ -596,6 +622,7 @@ export function useWorldDialogCommands( promptChangeWorldDescription, promptChangeWorldCapacity, promptChangeWorldRecommendedCapacity, - promptChangeWorldYouTubePreview + promptChangeWorldYouTubePreview, + registerCallbacks }; } diff --git a/src/localization/en.json b/src/localization/en.json index dcbb11c3..48ae4f57 100644 --- a/src/localization/en.json +++ b/src/localization/en.json @@ -2076,7 +2076,7 @@ "send_invite": "Continue? Send Invite", "decline_type": "Continue? Decline {type}", "delete_type": "Continue? {type}", - "command_question": "Continue? {command}", + "command_question": "Are you sure you want to {command}?", "clear_group": "Continue? Clear Group", "delete_group": "Continue? Delete Group {name}", "delete_post": "Are you sure you want to delete this post?",