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
@@ -226,7 +226,7 @@ describe('UserDialogInfoTab.vue', () => {
wrapper.vm.onTabActivated(); wrapper.vm.onTabActivated();
await flushPromises(); await flushPromises();
expect(creditsSpy).toHaveBeenCalledTimes(1); expect(creditsSpy).toHaveBeenCalledTimes(0);
}); });
}); });
+10 -10
View File
@@ -89,8 +89,8 @@ describe('query policy configuration', () => {
test('file-related policies', () => { test('file-related policies', () => {
expect(entityQueryPolicies.fileAnalysis).toMatchObject({ expect(entityQueryPolicies.fileAnalysis).toMatchObject({
staleTime: 600000, staleTime: 3600000,
gcTime: 3600000, gcTime: 14400000,
retry: 1, retry: 1,
refetchOnWindowFocus: false refetchOnWindowFocus: false
}); });
@@ -105,8 +105,8 @@ describe('query policy configuration', () => {
test('world persist data policy', () => { test('world persist data policy', () => {
expect(entityQueryPolicies.worldPersistData).toMatchObject({ expect(entityQueryPolicies.worldPersistData).toMatchObject({
staleTime: 120000, staleTime: 1800000,
gcTime: 600000, gcTime: 7200000,
retry: 1, retry: 1,
refetchOnWindowFocus: false refetchOnWindowFocus: false
}); });
@@ -114,8 +114,8 @@ describe('query policy configuration', () => {
test('user relation policies (mutualCounts, representedGroup)', () => { test('user relation policies (mutualCounts, representedGroup)', () => {
expect(entityQueryPolicies.mutualCounts).toMatchObject({ expect(entityQueryPolicies.mutualCounts).toMatchObject({
staleTime: 120000, staleTime: 900000,
gcTime: 600000, gcTime: 3600000,
retry: 1, retry: 1,
refetchOnWindowFocus: false refetchOnWindowFocus: false
}); });
@@ -130,8 +130,8 @@ describe('query policy configuration', () => {
test('visits policy has longer staleTime for slow-changing data', () => { test('visits policy has longer staleTime for slow-changing data', () => {
expect(entityQueryPolicies.visits).toMatchObject({ expect(entityQueryPolicies.visits).toMatchObject({
staleTime: 300000, staleTime: 1800000,
gcTime: 900000, gcTime: 7200000,
retry: 1, retry: 1,
refetchOnWindowFocus: false refetchOnWindowFocus: false
}); });
@@ -139,8 +139,8 @@ describe('query policy configuration', () => {
test('avatarStyles policy has very long staleTime for static config data', () => { test('avatarStyles policy has very long staleTime for static config data', () => {
expect(entityQueryPolicies.avatarStyles).toMatchObject({ expect(entityQueryPolicies.avatarStyles).toMatchObject({
staleTime: 600000, staleTime: 3600000,
gcTime: 3600000, gcTime: 14400000,
retry: 1, retry: 1,
refetchOnWindowFocus: false refetchOnWindowFocus: false
}); });
+86 -273
View File
@@ -12,32 +12,16 @@
<span class="category-title">{{ t('view.tools.pictures.header') }}</span> <span class="category-title">{{ t('view.tools.pictures.header') }}</span>
</div> </div>
<div class="tools-grid" v-show="!categoryCollapsed['image']"> <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"> <ToolItem
<div class="tool-content text-2xl" @click="showScreenshotMetadataPage"> :icon="Camera"
<div class="tool-icon"> :title="t('view.tools.pictures.screenshot')"
<Camera /> :description="t('view.tools.pictures.screenshot_description')"
</div> @click="showScreenshotMetadataPage" />
<div class="tool-info"> <ToolItem
<div class="tool-name">{{ t('view.tools.pictures.screenshot') }}</div> :icon="Images"
<div class="tool-description"> :title="t('view.tools.pictures.inventory')"
{{ t('view.tools.pictures.screenshot_description') }} :description="t('view.tools.pictures.inventory_description')"
</div> @click="showGalleryPage" />
</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>
</div> </div>
</div> </div>
@@ -49,73 +33,31 @@
<span class="category-title">{{ t('view.tools.shortcuts.header') }}</span> <span class="category-title">{{ t('view.tools.shortcuts.header') }}</span>
</div> </div>
<div class="tools-grid" v-show="!categoryCollapsed['shortcuts']"> <div class="tools-grid" v-show="!categoryCollapsed['shortcuts']">
<Card class="tool-card p-0 gap-0"> <ToolItem
<div class="tool-content" @click="openVrcPhotosFolder"> :icon="Folder"
<div class="tool-icon text-2xl"> :title="t('view.tools.pictures.pictures.vrc_photos')"
<Folder /> :description="t('view.tools.pictures.pictures.vrc_photos_description')"
</div> @click="openVrcPhotosFolder" />
<div class="tool-info"> <ToolItem
<div class="tool-name">{{ t('view.tools.pictures.pictures.vrc_photos') }}</div> :icon="Folder"
<div class="tool-description"> :title="t('view.tools.pictures.pictures.steam_screenshots')"
{{ t('view.tools.pictures.pictures.vrc_photos_description') }} :description="t('view.tools.pictures.pictures.steam_screenshots_description')"
</div> @click="openVrcScreenshotsFolder" />
</div> <ToolItem
</div> :icon="Folder"
</Card> :title="t('view.tools.shortcuts.vrcx_data')"
<Card class="tool-card p-0 gap-0"> :description="t('view.tools.shortcuts.vrcx_data_description')"
<div class="tool-content" @click="openVrcScreenshotsFolder"> @click="openVrcxAppDataFolder" />
<div class="tool-icon text-2xl"> <ToolItem
<Folder /> :icon="Folder"
</div> :title="t('view.tools.shortcuts.vrchat_data')"
<div class="tool-info"> :description="t('view.tools.shortcuts.vrchat_data_description')"
<div class="tool-name"> @click="openVrcAppDataFolder" />
{{ t('view.tools.pictures.pictures.steam_screenshots') }} <ToolItem
</div> :icon="Folder"
<div class="tool-description"> :title="t('view.tools.shortcuts.crash_dumps')"
{{ t('view.tools.pictures.pictures.steam_screenshots_description') }} :description="t('view.tools.shortcuts.crash_dumps_description')"
</div> @click="openCrashVrcCrashDumps" />
</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>
</div> </div>
</div> </div>
@@ -127,64 +69,26 @@
<span class="category-title">{{ t('view.tools.system_tools.header') }}</span> <span class="category-title">{{ t('view.tools.system_tools.header') }}</span>
</div> </div>
<div class="tools-grid" v-show="!categoryCollapsed['system']"> <div class="tools-grid" v-show="!categoryCollapsed['system']">
<Card class="tool-card p-0 gap-0"> <ToolItem
<div class="tool-content" @click="showVRChatConfig"> :icon="Settings"
<div class="tool-icon text-2xl"> :title="t('view.tools.system_tools.vrchat_config')"
<Settings /> :description="t('view.tools.system_tools.vrchat_config_description')"
</div> @click="showVRChatConfig" />
<div class="tool-info"> <ToolItem
<div class="tool-name">{{ t('view.tools.system_tools.vrchat_config') }}</div> :icon="Settings"
<div class="tool-description"> :title="t('view.settings.advanced.advanced.launch_options')"
{{ t('view.tools.system_tools.vrchat_config_description') }} :description="t('view.tools.system_tools.launch_options_description')"
</div> @click="showLaunchOptions" />
</div> <ToolItem
</div> :icon="Settings"
</Card> :title="t('view.settings.advanced.advanced.vrc_registry_backup')"
<Card class="tool-card p-0 gap-0"> :description="t('view.tools.system_tools.registry_backup_description')"
<div class="tool-content" @click="showLaunchOptions"> @click="showRegistryBackupDialog" />
<div class="tool-icon text-2xl"> <ToolItem
<Settings /> :icon="Settings"
</div> :title="t('view.settings.general.automation.auto_change_status')"
<div class="tool-info"> :description="t('view.settings.general.automation.auto_state_change_tooltip')"
<div class="tool-name"> @click="showAutoChangeStatusDialog" />
{{ 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>
</div> </div>
</div> </div>
@@ -196,17 +100,11 @@
<span class="category-title">{{ t('view.tools.group.header') }}</span> <span class="category-title">{{ t('view.tools.group.header') }}</span>
</div> </div>
<div class="tools-grid" v-show="!categoryCollapsed['group']"> <div class="tools-grid" v-show="!categoryCollapsed['group']">
<Card class="tool-card p-0 gap-0"> <ToolItem
<div class="tool-content" @click="showGroupCalendarDialog"> :icon="CalendarDays"
<div class="tool-icon text-2xl"> :title="t('view.tools.group.calendar')"
<CalendarDays /> :description="t('view.tools.group.calendar_description')"
</div> @click="showGroupCalendarDialog" />
<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>
</div> </div>
</div> </div>
@@ -217,59 +115,26 @@
</div> </div>
<div class="tools-grid" v-show="!categoryCollapsed['user']"> <div class="tools-grid" v-show="!categoryCollapsed['user']">
<Card class="tool-card p-0 gap-0"> <ToolItem
<div class="tool-content" @click="showExportDiscordNamesDialog"> :icon="FolderInput"
<div class="tool-icon text-2xl"> :title="t('view.tools.export.discord_names')"
<FolderInput /> :description="t('view.tools.user.discord_names_description')"
</div> @click="showExportDiscordNamesDialog" />
<div class="tool-info"> <ToolItem
<div class="tool-name">{{ t('view.tools.export.discord_names') }}</div> :icon="FolderInput"
<div class="tool-description"> :title="t('view.tools.export.export_notes')"
{{ t('view.tools.user.discord_names_description') }} :description="t('view.tools.export.export_notes_description')"
</div> @click="showNoteExportDialog" />
</div> <ToolItem
</div> :icon="FolderInput"
</Card> :title="t('view.tools.export.export_friend_list')"
<Card class="tool-card p-0 gap-0"> :description="t('view.tools.user.export_friend_list_description')"
<div class="tool-content" @click="showNoteExportDialog"> @click="showExportFriendsListDialog" />
<div class="tool-icon text-2xl"> <ToolItem
<FolderInput /> :icon="FolderInput"
</div> :title="t('view.tools.export.export_own_avatars')"
<div class="tool-info"> :description="t('view.tools.user.export_own_avatars_description')"
<div class="tool-name">{{ t('view.tools.export.export_notes') }}</div> @click="showExportAvatarsListDialog" />
<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>
</div> </div>
</div> </div>
@@ -281,19 +146,11 @@
<span class="category-title">{{ t('view.tools.other.header') }}</span> <span class="category-title">{{ t('view.tools.other.header') }}</span>
</div> </div>
<div class="tools-grid" v-show="!categoryCollapsed['other']"> <div class="tools-grid" v-show="!categoryCollapsed['other']">
<Card class="tool-card p-0 gap-0"> <ToolItem
<div class="tool-content" @click="showEditInviteMessageDialog"> :icon="SquarePen"
<div class="tool-icon text-2xl"> :title="t('view.tools.other.edit_invite_message')"
<SquarePen /> :description="t('view.tools.other.edit_invite_message_description')"
</div> @click="showEditInviteMessageDialog" />
<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>
</div> </div>
</div> </div>
</div> </div>
@@ -336,7 +193,7 @@
} from 'lucide-vue-next'; } from 'lucide-vue-next';
import { computed, defineAsyncComponent, onMounted, ref } from 'vue'; import { computed, defineAsyncComponent, onMounted, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { Card } from '@/components/ui/card'; import ToolItem from './components/ToolItem.vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { toast } from 'vue-sonner'; import { toast } from 'vue-sonner';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
@@ -529,9 +386,6 @@
margin-bottom: 12px; margin-bottom: 12px;
transition: all 0.2s ease; transition: all 0.2s ease;
&:hover {
}
.rotation-transition { .rotation-transition {
font-size: 14px; font-size: 14px;
margin-right: 8px; margin-right: 8px;
@@ -553,48 +407,7 @@
margin-left: 16px; 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 { .is-rotated {
transform: rotate(-90deg); transform: rotate(-90deg);
+99
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)
);
});
});
+21
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>
@@ -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');
});
});