refactor dialog commands func

This commit is contained in:
pa
2026-03-09 23:09:43 +09:00
parent ca57cd6590
commit 163b5b0127
9 changed files with 762 additions and 408 deletions

View File

@@ -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);
}
});
}
}

View File

@@ -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(() => {});
}
/**
*

View File

@@ -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();
});
});

View File

@@ -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
};
}

View File

@@ -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);

View File

@@ -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') },

View File

@@ -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);

View File

@@ -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
};
}

View File

@@ -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?",