use item component for tools item

This commit is contained in:
pa
2026-03-11 16:06:48 +09:00
parent 8ed3cff0e9
commit f757366121
6 changed files with 242 additions and 284 deletions

View File

@@ -226,7 +226,7 @@ describe('UserDialogInfoTab.vue', () => {
wrapper.vm.onTabActivated();
await flushPromises();
expect(creditsSpy).toHaveBeenCalledTimes(1);
expect(creditsSpy).toHaveBeenCalledTimes(0);
});
});

View File

@@ -89,8 +89,8 @@ describe('query policy configuration', () => {
test('file-related policies', () => {
expect(entityQueryPolicies.fileAnalysis).toMatchObject({
staleTime: 600000,
gcTime: 3600000,
staleTime: 3600000,
gcTime: 14400000,
retry: 1,
refetchOnWindowFocus: false
});
@@ -105,8 +105,8 @@ describe('query policy configuration', () => {
test('world persist data policy', () => {
expect(entityQueryPolicies.worldPersistData).toMatchObject({
staleTime: 120000,
gcTime: 600000,
staleTime: 1800000,
gcTime: 7200000,
retry: 1,
refetchOnWindowFocus: false
});
@@ -114,8 +114,8 @@ describe('query policy configuration', () => {
test('user relation policies (mutualCounts, representedGroup)', () => {
expect(entityQueryPolicies.mutualCounts).toMatchObject({
staleTime: 120000,
gcTime: 600000,
staleTime: 900000,
gcTime: 3600000,
retry: 1,
refetchOnWindowFocus: false
});
@@ -130,8 +130,8 @@ describe('query policy configuration', () => {
test('visits policy has longer staleTime for slow-changing data', () => {
expect(entityQueryPolicies.visits).toMatchObject({
staleTime: 300000,
gcTime: 900000,
staleTime: 1800000,
gcTime: 7200000,
retry: 1,
refetchOnWindowFocus: false
});
@@ -139,8 +139,8 @@ describe('query policy configuration', () => {
test('avatarStyles policy has very long staleTime for static config data', () => {
expect(entityQueryPolicies.avatarStyles).toMatchObject({
staleTime: 600000,
gcTime: 3600000,
staleTime: 3600000,
gcTime: 14400000,
retry: 1,
refetchOnWindowFocus: false
});

View File

@@ -12,32 +12,16 @@
<span class="category-title">{{ t('view.tools.pictures.header') }}</span>
</div>
<div class="tools-grid" v-show="!categoryCollapsed['image']">
<Card class="tool-card x-hover-card p-0 gap-0 hover:bg-accent hover:shadow-sm">
<div class="tool-content text-2xl" @click="showScreenshotMetadataPage">
<div class="tool-icon">
<Camera />
</div>
<div class="tool-info">
<div class="tool-name">{{ t('view.tools.pictures.screenshot') }}</div>
<div class="tool-description">
{{ t('view.tools.pictures.screenshot_description') }}
</div>
</div>
</div>
</Card>
<Card class="tool-card p-0 gap-0">
<div class="tool-content" @click="showGalleryPage">
<div class="tool-icon text-2xl">
<Images />
</div>
<div class="tool-info">
<div class="tool-name">{{ t('view.tools.pictures.inventory') }}</div>
<div class="tool-description">
{{ t('view.tools.pictures.inventory_description') }}
</div>
</div>
</div>
</Card>
<ToolItem
:icon="Camera"
:title="t('view.tools.pictures.screenshot')"
:description="t('view.tools.pictures.screenshot_description')"
@click="showScreenshotMetadataPage" />
<ToolItem
:icon="Images"
:title="t('view.tools.pictures.inventory')"
:description="t('view.tools.pictures.inventory_description')"
@click="showGalleryPage" />
</div>
</div>
@@ -49,73 +33,31 @@
<span class="category-title">{{ t('view.tools.shortcuts.header') }}</span>
</div>
<div class="tools-grid" v-show="!categoryCollapsed['shortcuts']">
<Card class="tool-card p-0 gap-0">
<div class="tool-content" @click="openVrcPhotosFolder">
<div class="tool-icon text-2xl">
<Folder />
</div>
<div class="tool-info">
<div class="tool-name">{{ t('view.tools.pictures.pictures.vrc_photos') }}</div>
<div class="tool-description">
{{ t('view.tools.pictures.pictures.vrc_photos_description') }}
</div>
</div>
</div>
</Card>
<Card class="tool-card p-0 gap-0">
<div class="tool-content" @click="openVrcScreenshotsFolder">
<div class="tool-icon text-2xl">
<Folder />
</div>
<div class="tool-info">
<div class="tool-name">
{{ t('view.tools.pictures.pictures.steam_screenshots') }}
</div>
<div class="tool-description">
{{ t('view.tools.pictures.pictures.steam_screenshots_description') }}
</div>
</div>
</div>
</Card>
<Card class="tool-card p-0 gap-0">
<div class="tool-content" @click="openVrcxAppDataFolder">
<div class="tool-icon text-2xl">
<Folder />
</div>
<div class="tool-info">
<div class="tool-name">{{ t('view.tools.shortcuts.vrcx_data') }}</div>
<div class="tool-description">
{{ t('view.tools.shortcuts.vrcx_data_description') }}
</div>
</div>
</div>
</Card>
<Card class="tool-card p-0 gap-0">
<div class="tool-content" @click="openVrcAppDataFolder">
<div class="tool-icon text-2xl">
<Folder />
</div>
<div class="tool-info">
<div class="tool-name">{{ t('view.tools.shortcuts.vrchat_data') }}</div>
<div class="tool-description">
{{ t('view.tools.shortcuts.vrchat_data_description') }}
</div>
</div>
</div>
</Card>
<Card class="tool-card p-0 gap-0">
<div class="tool-content" @click="openCrashVrcCrashDumps">
<div class="tool-icon text-2xl">
<Folder />
</div>
<div class="tool-info">
<div class="tool-name">{{ t('view.tools.shortcuts.crash_dumps') }}</div>
<div class="tool-description">
{{ t('view.tools.shortcuts.crash_dumps_description') }}
</div>
</div>
</div>
</Card>
<ToolItem
:icon="Folder"
:title="t('view.tools.pictures.pictures.vrc_photos')"
:description="t('view.tools.pictures.pictures.vrc_photos_description')"
@click="openVrcPhotosFolder" />
<ToolItem
:icon="Folder"
:title="t('view.tools.pictures.pictures.steam_screenshots')"
:description="t('view.tools.pictures.pictures.steam_screenshots_description')"
@click="openVrcScreenshotsFolder" />
<ToolItem
:icon="Folder"
:title="t('view.tools.shortcuts.vrcx_data')"
:description="t('view.tools.shortcuts.vrcx_data_description')"
@click="openVrcxAppDataFolder" />
<ToolItem
:icon="Folder"
:title="t('view.tools.shortcuts.vrchat_data')"
:description="t('view.tools.shortcuts.vrchat_data_description')"
@click="openVrcAppDataFolder" />
<ToolItem
:icon="Folder"
:title="t('view.tools.shortcuts.crash_dumps')"
:description="t('view.tools.shortcuts.crash_dumps_description')"
@click="openCrashVrcCrashDumps" />
</div>
</div>
@@ -127,64 +69,26 @@
<span class="category-title">{{ t('view.tools.system_tools.header') }}</span>
</div>
<div class="tools-grid" v-show="!categoryCollapsed['system']">
<Card class="tool-card p-0 gap-0">
<div class="tool-content" @click="showVRChatConfig">
<div class="tool-icon text-2xl">
<Settings />
</div>
<div class="tool-info">
<div class="tool-name">{{ t('view.tools.system_tools.vrchat_config') }}</div>
<div class="tool-description">
{{ t('view.tools.system_tools.vrchat_config_description') }}
</div>
</div>
</div>
</Card>
<Card class="tool-card p-0 gap-0">
<div class="tool-content" @click="showLaunchOptions">
<div class="tool-icon text-2xl">
<Settings />
</div>
<div class="tool-info">
<div class="tool-name">
{{ t('view.settings.advanced.advanced.launch_options') }}
</div>
<div class="tool-description">
{{ t('view.tools.system_tools.launch_options_description') }}
</div>
</div>
</div>
</Card>
<Card class="tool-card p-0 gap-0">
<div class="tool-content" @click="showRegistryBackupDialog">
<div class="tool-icon text-2xl">
<Settings />
</div>
<div class="tool-info">
<div class="tool-name">
{{ t('view.settings.advanced.advanced.vrc_registry_backup') }}
</div>
<div class="tool-description">
{{ t('view.tools.system_tools.registry_backup_description') }}
</div>
</div>
</div>
</Card>
<Card class="tool-card p-0 gap-0">
<div class="tool-content" @click="showAutoChangeStatusDialog">
<div class="tool-icon text-2xl">
<Settings />
</div>
<div class="tool-info">
<div class="tool-name">
{{ t('view.settings.general.automation.auto_change_status') }}
</div>
<div class="tool-description">
{{ t('view.settings.general.automation.auto_state_change_tooltip') }}
</div>
</div>
</div>
</Card>
<ToolItem
:icon="Settings"
:title="t('view.tools.system_tools.vrchat_config')"
:description="t('view.tools.system_tools.vrchat_config_description')"
@click="showVRChatConfig" />
<ToolItem
:icon="Settings"
:title="t('view.settings.advanced.advanced.launch_options')"
:description="t('view.tools.system_tools.launch_options_description')"
@click="showLaunchOptions" />
<ToolItem
:icon="Settings"
:title="t('view.settings.advanced.advanced.vrc_registry_backup')"
:description="t('view.tools.system_tools.registry_backup_description')"
@click="showRegistryBackupDialog" />
<ToolItem
:icon="Settings"
:title="t('view.settings.general.automation.auto_change_status')"
:description="t('view.settings.general.automation.auto_state_change_tooltip')"
@click="showAutoChangeStatusDialog" />
</div>
</div>
@@ -196,17 +100,11 @@
<span class="category-title">{{ t('view.tools.group.header') }}</span>
</div>
<div class="tools-grid" v-show="!categoryCollapsed['group']">
<Card class="tool-card p-0 gap-0">
<div class="tool-content" @click="showGroupCalendarDialog">
<div class="tool-icon text-2xl">
<CalendarDays />
</div>
<div class="tool-info">
<div class="tool-name">{{ t('view.tools.group.calendar') }}</div>
<div class="tool-description">{{ t('view.tools.group.calendar_description') }}</div>
</div>
</div>
</Card>
<ToolItem
:icon="CalendarDays"
:title="t('view.tools.group.calendar')"
:description="t('view.tools.group.calendar_description')"
@click="showGroupCalendarDialog" />
</div>
</div>
@@ -217,59 +115,26 @@
</div>
<div class="tools-grid" v-show="!categoryCollapsed['user']">
<Card class="tool-card p-0 gap-0">
<div class="tool-content" @click="showExportDiscordNamesDialog">
<div class="tool-icon text-2xl">
<FolderInput />
</div>
<div class="tool-info">
<div class="tool-name">{{ t('view.tools.export.discord_names') }}</div>
<div class="tool-description">
{{ t('view.tools.user.discord_names_description') }}
</div>
</div>
</div>
</Card>
<Card class="tool-card p-0 gap-0">
<div class="tool-content" @click="showNoteExportDialog">
<div class="tool-icon text-2xl">
<FolderInput />
</div>
<div class="tool-info">
<div class="tool-name">{{ t('view.tools.export.export_notes') }}</div>
<div class="tool-description">
{{ t('view.tools.export.export_notes_description') }}
</div>
</div>
</div>
</Card>
<Card class="tool-card p-0 gap-0">
<div class="tool-content" @click="showExportFriendsListDialog">
<div class="tool-icon text-2xl">
<FolderInput />
</div>
<div class="tool-info">
<div class="tool-name">{{ t('view.tools.export.export_friend_list') }}</div>
<div class="tool-description">
{{ t('view.tools.user.export_friend_list_description') }}
</div>
</div>
</div>
</Card>
<Card class="tool-card p-0 gap-0">
<div class="tool-content" @click="showExportAvatarsListDialog">
<div class="tool-icon text-2xl">
<FolderInput />
</div>
<div class="tool-info">
<div class="tool-name">{{ t('view.tools.export.export_own_avatars') }}</div>
<div class="tool-description">
{{ t('view.tools.user.export_own_avatars_description') }}
</div>
</div>
</div>
</Card>
<ToolItem
:icon="FolderInput"
:title="t('view.tools.export.discord_names')"
:description="t('view.tools.user.discord_names_description')"
@click="showExportDiscordNamesDialog" />
<ToolItem
:icon="FolderInput"
:title="t('view.tools.export.export_notes')"
:description="t('view.tools.export.export_notes_description')"
@click="showNoteExportDialog" />
<ToolItem
:icon="FolderInput"
:title="t('view.tools.export.export_friend_list')"
:description="t('view.tools.user.export_friend_list_description')"
@click="showExportFriendsListDialog" />
<ToolItem
:icon="FolderInput"
:title="t('view.tools.export.export_own_avatars')"
:description="t('view.tools.user.export_own_avatars_description')"
@click="showExportAvatarsListDialog" />
</div>
</div>
@@ -281,19 +146,11 @@
<span class="category-title">{{ t('view.tools.other.header') }}</span>
</div>
<div class="tools-grid" v-show="!categoryCollapsed['other']">
<Card class="tool-card p-0 gap-0">
<div class="tool-content" @click="showEditInviteMessageDialog">
<div class="tool-icon text-2xl">
<SquarePen />
</div>
<div class="tool-info">
<div class="tool-name">{{ t('view.tools.other.edit_invite_message') }}</div>
<div class="tool-description">
{{ t('view.tools.other.edit_invite_message_description') }}
</div>
</div>
</div>
</Card>
<ToolItem
:icon="SquarePen"
:title="t('view.tools.other.edit_invite_message')"
:description="t('view.tools.other.edit_invite_message_description')"
@click="showEditInviteMessageDialog" />
</div>
</div>
</div>
@@ -336,7 +193,7 @@
} from 'lucide-vue-next';
import { computed, defineAsyncComponent, onMounted, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { Card } from '@/components/ui/card';
import ToolItem from './components/ToolItem.vue';
import { storeToRefs } from 'pinia';
import { toast } from 'vue-sonner';
import { useI18n } from 'vue-i18n';
@@ -529,9 +386,6 @@
margin-bottom: 12px;
transition: all 0.2s ease;
&:hover {
}
.rotation-transition {
font-size: 14px;
margin-right: 8px;
@@ -553,48 +407,7 @@
margin-left: 16px;
}
.tool-card {
position: relative;
overflow: visible;
border-radius: var(--radius-lg);
cursor: pointer;
width: 100%;
.tool-content {
display: flex;
align-items: center;
padding: 20px 16px;
.tool-icon {
width: 56px;
height: 56px;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-xl);
margin-right: 20px;
i {
font-size: 28px;
}
}
.tool-info {
flex: 1;
.tool-name {
font-size: 18px;
font-weight: 600;
margin-bottom: 4px;
}
.tool-description {
font-size: 14px;
opacity: 0.8;
}
}
}
}
.is-rotated {
transform: rotate(-90deg);

View File

@@ -0,0 +1,99 @@
import { beforeEach, describe, expect, test, vi } from 'vitest';
import { ref } from 'vue';
import { flushPromises, mount } from '@vue/test-utils';
const push = vi.fn();
const showGalleryPage = vi.fn();
const showVRChatConfig = vi.fn();
const showLaunchOptions = vi.fn();
const showRegistryBackupDialog = vi.fn();
const getString = vi.fn();
const setString = vi.fn();
const friends = ref([]);
vi.mock('vue-router', () => ({
useRouter: () => ({ push }),
useRoute: () => ({ name: 'not-tools' })
}));
vi.mock('vue-i18n', () => ({
useI18n: () => ({ t: (key) => key })
}));
vi.mock('pinia', async (importOriginal) => {
const actual = await importOriginal();
return {
...actual,
storeToRefs: (store) => store
};
});
vi.mock('../../../stores', () => ({
useFriendStore: () => ({ friends }),
useGalleryStore: () => ({ showGalleryPage })
}));
vi.mock('../../../stores/settings/advanced', () => ({
useAdvancedSettingsStore: () => ({ showVRChatConfig })
}));
vi.mock('../../../stores/launch', () => ({
useLaunchStore: () => ({ showLaunchOptions })
}));
vi.mock('../../../stores/vrcx', () => ({
useVrcxStore: () => ({ showRegistryBackupDialog })
}));
vi.mock('../../../services/config.js', () => ({
default: {
getString: (...args) => getString(...args),
setString: (...args) => setString(...args)
}
}));
vi.mock('../dialogs/AutoChangeStatusDialog.vue', () => ({
default: { template: '<div />' }
}));
import Tools from '../Tools.vue';
describe('Tools.vue', () => {
beforeEach(() => {
vi.clearAllMocks();
getString.mockResolvedValue('{}');
});
test('clicking screenshot tool navigates to screenshot metadata', async () => {
const wrapper = mount(Tools);
await flushPromises();
const screenshotItem = wrapper.findAllComponents({ name: 'ToolItem' })[0];
await screenshotItem.trigger('click');
expect(push).toHaveBeenCalledWith({ name: 'screenshot-metadata' });
});
test('clicking inventory tool calls showGalleryPage', async () => {
const wrapper = mount(Tools);
await flushPromises();
const inventoryItem = wrapper.findAllComponents({ name: 'ToolItem' })[1];
await inventoryItem.trigger('click');
expect(showGalleryPage).toHaveBeenCalled();
});
test('toggle category persists collapsed state', async () => {
const wrapper = mount(Tools);
await flushPromises();
const firstCategoryHeader = wrapper.find('.category-header');
await firstCategoryHeader.trigger('click');
expect(setString).toHaveBeenCalledWith(
'VRCX_toolsCategoryCollapsed',
expect.any(String)
);
});
});

View File

@@ -0,0 +1,21 @@
<script setup>
import { Item, ItemContent, ItemDescription, ItemMedia, ItemTitle } from '@/components/ui/item';
defineProps({
icon: { type: [Object, Function], required: true },
title: { type: String, required: true },
description: { type: String, required: true }
});
</script>
<template>
<Item variant="outline" class="cursor-pointer hover:bg-accent/50">
<ItemMedia variant="icon">
<component :is="icon" />
</ItemMedia>
<ItemContent>
<ItemTitle>{{ title }}</ItemTitle>
<ItemDescription>{{ description }}</ItemDescription>
</ItemContent>
</Item>
</template>

View File

@@ -0,0 +1,25 @@
import { describe, expect, test } from 'vitest';
import { defineComponent, markRaw } from 'vue';
import { mount } from '@vue/test-utils';
import ToolItem from '../ToolItem.vue';
describe('ToolItem.vue', () => {
test('renders icon, title and description', () => {
const MockIcon = defineComponent({
template: '<svg data-test="mock-icon" />'
});
const wrapper = mount(ToolItem, {
props: {
icon: markRaw(MockIcon),
title: 'Test title',
description: 'Test description'
}
});
expect(wrapper.find('[data-test="mock-icon"]').exists()).toBe(true);
expect(wrapper.text()).toContain('Test title');
expect(wrapper.text()).toContain('Test description');
});
});