mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-27 18:53:47 +02:00
add test
This commit is contained in:
@@ -73,6 +73,7 @@
|
||||
import { toast } from 'vue-sonner';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import { formatCsvField, formatCsvRow } from '../../../shared/utils';
|
||||
import { useAvatarStore, useFavoriteStore } from '../../../stores';
|
||||
|
||||
const { t } = useI18n();
|
||||
@@ -114,6 +115,11 @@
|
||||
{ label: 'Thumbnail', value: 'thumbnailImageUrl' }
|
||||
]);
|
||||
|
||||
/**
|
||||
*
|
||||
* @param label
|
||||
* @param checked
|
||||
*/
|
||||
function toggleAvatarExportOption(label, checked) {
|
||||
const selection = exportSelectedOptions.value;
|
||||
const index = selection.indexOf(label);
|
||||
@@ -143,6 +149,9 @@
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function showAvatarExportDialog() {
|
||||
avatarExportFavoriteGroup.value = null;
|
||||
avatarExportLocalFavoriteGroup.value = null;
|
||||
@@ -151,6 +160,10 @@
|
||||
updateAvatarExportDialog();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param value
|
||||
*/
|
||||
function handleAvatarExportFavoriteGroupSelect(value) {
|
||||
avatarExportFavoriteGroupSelection.value = value;
|
||||
if (value === AVATAR_EXPORT_ALL_VALUE) {
|
||||
@@ -161,6 +174,10 @@
|
||||
selectAvatarExportGroup(group);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param value
|
||||
*/
|
||||
function handleAvatarExportLocalFavoriteGroupSelect(value) {
|
||||
avatarExportLocalFavoriteGroupSelection.value = value;
|
||||
if (value === AVATAR_EXPORT_NONE_VALUE) {
|
||||
@@ -169,6 +186,10 @@
|
||||
}
|
||||
selectAvatarExportLocalGroup(value);
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param event
|
||||
*/
|
||||
function handleCopyAvatarExportData(event) {
|
||||
if (event.target.tagName === 'TEXTAREA') {
|
||||
event.target.select();
|
||||
@@ -183,38 +204,14 @@
|
||||
toast.error('Copy failed!');
|
||||
});
|
||||
}
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function updateAvatarExportDialog() {
|
||||
const needsCsvQuotes = (text) => {
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
if (text.charCodeAt(i) < 0x20) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return text.includes(',') || text.includes('"');
|
||||
};
|
||||
|
||||
const formatter = function (value) {
|
||||
if (value === null || typeof value === 'undefined') {
|
||||
return '';
|
||||
}
|
||||
const text = String(value);
|
||||
if (needsCsvQuotes(text)) {
|
||||
return `"${text.replace(/"/g, '""')}"`;
|
||||
}
|
||||
return text;
|
||||
};
|
||||
const propsForQuery = exportSelectOptions.value
|
||||
.filter((option) => exportSelectedOptions.value.includes(option.label))
|
||||
.map((option) => option.value);
|
||||
|
||||
function resText(ref) {
|
||||
let resArr = [];
|
||||
propsForQuery.forEach((e) => {
|
||||
resArr.push(formatter(ref?.[e]));
|
||||
});
|
||||
return resArr.join(',');
|
||||
}
|
||||
|
||||
const lines = [exportSelectedOptions.value.join(',')];
|
||||
|
||||
if (avatarExportFavoriteGroup.value) {
|
||||
@@ -222,7 +219,7 @@
|
||||
if (!avatarExportFavoriteGroup.value || avatarExportFavoriteGroup.value === group) {
|
||||
favoriteAvatars.value.forEach((ref) => {
|
||||
if (group.key === ref.groupKey) {
|
||||
lines.push(resText(ref.ref));
|
||||
lines.push(formatCsvRow(ref.ref, propsForQuery));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -234,23 +231,27 @@
|
||||
}
|
||||
for (let i = 0; i < favoriteGroup.length; ++i) {
|
||||
const ref = favoriteGroup[i];
|
||||
lines.push(resText(ref));
|
||||
lines.push(formatCsvRow(ref, propsForQuery));
|
||||
}
|
||||
} else {
|
||||
// export all
|
||||
favoriteAvatars.value.forEach((ref) => {
|
||||
lines.push(resText(ref.ref));
|
||||
lines.push(formatCsvRow(ref.ref, propsForQuery));
|
||||
});
|
||||
for (let i = 0; i < localAvatarFavoritesList.value.length; ++i) {
|
||||
const avatarId = localAvatarFavoritesList.value[i];
|
||||
const ref = cachedAvatars.get(avatarId);
|
||||
if (typeof ref !== 'undefined') {
|
||||
lines.push(resText(ref));
|
||||
lines.push(formatCsvRow(ref, propsForQuery));
|
||||
}
|
||||
}
|
||||
}
|
||||
avatarExportContent.value = lines.reverse().join('\n');
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param group
|
||||
*/
|
||||
function selectAvatarExportGroup(group) {
|
||||
avatarExportFavoriteGroup.value = group;
|
||||
avatarExportLocalFavoriteGroup.value = null;
|
||||
@@ -258,6 +259,10 @@
|
||||
avatarExportLocalFavoriteGroupSelection.value = AVATAR_EXPORT_NONE_VALUE;
|
||||
updateAvatarExportDialog();
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param group
|
||||
*/
|
||||
function selectAvatarExportLocalGroup(group) {
|
||||
avatarExportLocalFavoriteGroup.value = group;
|
||||
avatarExportFavoriteGroup.value = null;
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import { useFavoriteStore, useUserStore } from '../../../stores';
|
||||
import { formatCsvField } from '../../../shared/utils';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
@@ -111,12 +112,19 @@
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function showFriendExportDialog() {
|
||||
friendExportFavoriteGroup.value = null;
|
||||
friendExportFavoriteGroupSelection.value = FRIEND_EXPORT_ALL_VALUE;
|
||||
updateFriendExportDialog();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param value
|
||||
*/
|
||||
function handleFriendExportGroupSelect(value) {
|
||||
friendExportFavoriteGroupSelection.value = value;
|
||||
if (value === FRIEND_EXPORT_ALL_VALUE) {
|
||||
@@ -127,6 +135,10 @@
|
||||
selectFriendExportGroup(group);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param value
|
||||
*/
|
||||
function handleFriendExportLocalGroupSelect(value) {
|
||||
friendExportLocalFavoriteGroupSelection.value = value;
|
||||
if (value === FRIEND_EXPORT_NONE_VALUE) {
|
||||
@@ -136,6 +148,10 @@
|
||||
selectFriendExportLocalGroup(value);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param event
|
||||
*/
|
||||
function handleCopyFriendExportData(event) {
|
||||
if (event.target.tagName === 'TEXTAREA') {
|
||||
event.target.select();
|
||||
@@ -151,26 +167,10 @@
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function updateFriendExportDialog() {
|
||||
const needsCsvQuotes = (text) => {
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
if (text.charCodeAt(i) < 0x20) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return text.includes(',') || text.includes('"');
|
||||
};
|
||||
|
||||
const formatter = function (value) {
|
||||
if (value === null || typeof value === 'undefined') {
|
||||
return '';
|
||||
}
|
||||
const text = String(value);
|
||||
if (needsCsvQuotes(text)) {
|
||||
return `"${text.replace(/"/g, '""')}"`;
|
||||
}
|
||||
return text;
|
||||
};
|
||||
const lines = ['UserID,Name'];
|
||||
|
||||
if (friendExportFavoriteGroup.value) {
|
||||
@@ -178,7 +178,7 @@
|
||||
if (friendExportFavoriteGroup.value === group) {
|
||||
favoriteFriends.value.forEach((ref) => {
|
||||
if (group.key === ref.groupKey) {
|
||||
lines.push(`${formatter(ref.id)},${formatter(ref.name)}`);
|
||||
lines.push(`${formatCsvField(ref.id)},${formatCsvField(ref.name)}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -191,25 +191,29 @@
|
||||
favoriteGroup.forEach((userId) => {
|
||||
const ref = cachedUsers.value.get(userId);
|
||||
if (typeof ref !== 'undefined') {
|
||||
lines.push(`${formatter(ref.id)},${formatter(ref.displayName)}`);
|
||||
lines.push(`${formatCsvField(ref.id)},${formatCsvField(ref.displayName)}`);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// export all
|
||||
favoriteFriends.value.forEach((ref) => {
|
||||
lines.push(`${formatter(ref.id)},${formatter(ref.name)}`);
|
||||
lines.push(`${formatCsvField(ref.id)},${formatCsvField(ref.name)}`);
|
||||
});
|
||||
for (let i = 0; i < localFriendFavoritesList.value.length; ++i) {
|
||||
const userId = localFriendFavoritesList.value[i];
|
||||
const ref = cachedUsers.value.get(userId);
|
||||
if (typeof ref !== 'undefined') {
|
||||
lines.push(`${formatter(ref.id)},${formatter(ref.displayName)}`);
|
||||
lines.push(`${formatCsvField(ref.id)},${formatCsvField(ref.displayName)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
friendExportContent.value = lines.reverse().join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param group
|
||||
*/
|
||||
function selectFriendExportGroup(group) {
|
||||
friendExportFavoriteGroup.value = group;
|
||||
friendExportLocalFavoriteGroup.value = null;
|
||||
@@ -218,6 +222,10 @@
|
||||
updateFriendExportDialog();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param groupName
|
||||
*/
|
||||
function selectFriendExportLocalGroup(groupName) {
|
||||
friendExportLocalFavoriteGroup.value = groupName;
|
||||
friendExportFavoriteGroup.value = null;
|
||||
|
||||
@@ -76,6 +76,7 @@
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import { useFavoriteStore, useWorldStore } from '../../../stores';
|
||||
import { formatCsvRow } from '../../../shared/utils';
|
||||
|
||||
const props = defineProps({
|
||||
worldExportDialogVisible: {
|
||||
@@ -117,6 +118,11 @@
|
||||
{ label: 'Thumbnail', value: 'thumbnailImageUrl' }
|
||||
]);
|
||||
|
||||
/**
|
||||
*
|
||||
* @param label
|
||||
* @param checked
|
||||
*/
|
||||
function toggleWorldExportOption(label, checked) {
|
||||
const selection = exportSelectedOptions.value;
|
||||
const index = selection.indexOf(label);
|
||||
@@ -146,6 +152,9 @@
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function showWorldExportDialog() {
|
||||
worldExportFavoriteGroup.value = null;
|
||||
worldExportLocalFavoriteGroup.value = null;
|
||||
@@ -154,6 +163,10 @@
|
||||
updateWorldExportDialog();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param value
|
||||
*/
|
||||
function handleWorldExportGroupSelect(value) {
|
||||
worldExportFavoriteGroupSelection.value = value;
|
||||
if (value === WORLD_EXPORT_ALL_VALUE) {
|
||||
@@ -164,6 +177,10 @@
|
||||
selectWorldExportGroup(group);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param value
|
||||
*/
|
||||
function handleWorldExportLocalGroupSelect(value) {
|
||||
worldExportLocalFavoriteGroupSelection.value = value;
|
||||
if (value === WORLD_EXPORT_NONE_VALUE) {
|
||||
@@ -173,6 +190,10 @@
|
||||
selectWorldExportLocalGroup(value);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param event
|
||||
*/
|
||||
function handleCopyWorldExportData(event) {
|
||||
if (event.target.tagName === 'TEXTAREA') {
|
||||
event.target.select();
|
||||
@@ -188,26 +209,14 @@
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function updateWorldExportDialog() {
|
||||
const formatter = function (str) {
|
||||
if (/[\x00-\x1f,"]/.test(str) === true) {
|
||||
return `"${str.replace(/"/g, '""')}"`;
|
||||
}
|
||||
return str;
|
||||
};
|
||||
|
||||
const propsForQuery = exportSelectOptions.value
|
||||
.filter((option) => exportSelectedOptions.value.includes(option.label))
|
||||
.map((option) => option.value);
|
||||
|
||||
function resText(ref) {
|
||||
let resArr = [];
|
||||
propsForQuery.forEach((e) => {
|
||||
resArr.push(formatter(ref?.[e]));
|
||||
});
|
||||
return resArr.join(',');
|
||||
}
|
||||
|
||||
const lines = [exportSelectedOptions.value.join(',')];
|
||||
|
||||
if (worldExportFavoriteGroup.value) {
|
||||
@@ -215,7 +224,7 @@
|
||||
if (worldExportFavoriteGroup.value === group) {
|
||||
favoriteWorlds.value.forEach((ref) => {
|
||||
if (group.key === ref.groupKey) {
|
||||
lines.push(resText(ref.ref));
|
||||
lines.push(formatCsvRow(ref.ref, propsForQuery));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -227,24 +236,28 @@
|
||||
}
|
||||
for (let i = 0; i < favoriteGroup.length; ++i) {
|
||||
const ref = favoriteGroup[i];
|
||||
lines.push(resText(ref));
|
||||
lines.push(formatCsvRow(ref, propsForQuery));
|
||||
}
|
||||
} else {
|
||||
// export all
|
||||
favoriteWorlds.value.forEach((ref) => {
|
||||
lines.push(resText(ref.ref));
|
||||
lines.push(formatCsvRow(ref.ref, propsForQuery));
|
||||
});
|
||||
for (let i = 0; i < localWorldFavoritesList.length; ++i) {
|
||||
const worldId = localWorldFavoritesList[i];
|
||||
const ref = cachedWorlds.get(worldId);
|
||||
if (typeof ref !== 'undefined') {
|
||||
lines.push(resText(ref));
|
||||
lines.push(formatCsvRow(ref, propsForQuery));
|
||||
}
|
||||
}
|
||||
}
|
||||
worldExportContent.value = lines.reverse().join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param group
|
||||
*/
|
||||
function selectWorldExportGroup(group) {
|
||||
worldExportFavoriteGroup.value = group;
|
||||
worldExportLocalFavoriteGroup.value = null;
|
||||
@@ -253,6 +266,10 @@
|
||||
updateWorldExportDialog();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param group
|
||||
*/
|
||||
function selectWorldExportLocalGroup(group) {
|
||||
worldExportLocalFavoriteGroup.value = group;
|
||||
worldExportFavoriteGroup.value = null;
|
||||
|
||||
241
src/views/Login/__tests__/Login.test.js
Normal file
241
src/views/Login/__tests__/Login.test.js
Normal file
@@ -0,0 +1,241 @@
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
import { createI18n } from 'vue-i18n';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { ref } from 'vue';
|
||||
|
||||
vi.mock('../../../views/Feed/Feed.vue', () => ({
|
||||
default: { template: '<div />' }
|
||||
}));
|
||||
vi.mock('../../../views/Feed/columns.jsx', () => ({ columns: [] }));
|
||||
vi.mock('../../../plugin/router', () => ({
|
||||
router: {
|
||||
beforeEach: vi.fn(),
|
||||
push: vi.fn(),
|
||||
replace: vi.fn(),
|
||||
currentRoute: ref({ path: '/login', name: 'login', meta: {} }),
|
||||
isReady: vi.fn().mockResolvedValue(true)
|
||||
},
|
||||
initRouter: vi.fn()
|
||||
}));
|
||||
vi.mock('vue-router', async (importOriginal) => {
|
||||
const actual = await importOriginal();
|
||||
return {
|
||||
...actual,
|
||||
useRouter: vi.fn(() => ({
|
||||
push: vi.fn(),
|
||||
replace: vi.fn(),
|
||||
currentRoute: ref({ path: '/login', name: 'login', meta: {} })
|
||||
})),
|
||||
useRoute: vi.fn(() => ({ query: {} }))
|
||||
};
|
||||
});
|
||||
vi.mock('../../../plugin/interopApi', () => ({ initInteropApi: vi.fn() }));
|
||||
vi.mock('../../../service/database', () => ({
|
||||
database: new Proxy(
|
||||
{},
|
||||
{
|
||||
get: (_target, prop) => {
|
||||
if (prop === '__esModule') return false;
|
||||
return vi.fn().mockResolvedValue(null);
|
||||
}
|
||||
}
|
||||
)
|
||||
}));
|
||||
vi.mock('../../../service/config', () => ({
|
||||
default: {
|
||||
init: vi.fn(),
|
||||
getString: vi.fn().mockImplementation((_k, d) => d ?? '{}'),
|
||||
setString: vi.fn(),
|
||||
getBool: vi.fn().mockImplementation((_k, d) => d ?? false),
|
||||
setBool: vi.fn(),
|
||||
getInt: vi.fn().mockImplementation((_k, d) => d ?? 0),
|
||||
setInt: vi.fn(),
|
||||
getFloat: vi.fn().mockImplementation((_k, d) => d ?? 0),
|
||||
setFloat: vi.fn(),
|
||||
getObject: vi.fn().mockReturnValue(null),
|
||||
setObject: vi.fn(),
|
||||
getArray: vi.fn().mockReturnValue([]),
|
||||
setArray: vi.fn(),
|
||||
remove: vi.fn()
|
||||
}
|
||||
}));
|
||||
vi.mock('../../../service/jsonStorage', () => ({ default: vi.fn() }));
|
||||
vi.mock('../../../service/watchState', () => ({
|
||||
watchState: {
|
||||
isLoggedIn: false,
|
||||
isFriendsLoaded: false,
|
||||
isFavoritesLoaded: false
|
||||
}
|
||||
}));
|
||||
vi.mock('vee-validate', () => ({
|
||||
Field: {
|
||||
name: 'VeeField',
|
||||
props: ['name'],
|
||||
setup(_props, { slots }) {
|
||||
return () =>
|
||||
slots.default?.({
|
||||
field: { value: '', onChange: () => {}, onBlur: () => {} },
|
||||
errors: []
|
||||
});
|
||||
}
|
||||
},
|
||||
useForm: vi.fn(() => ({
|
||||
handleSubmit: (fn) => fn,
|
||||
resetForm: vi.fn(),
|
||||
values: {}
|
||||
}))
|
||||
}));
|
||||
vi.mock('@vee-validate/zod', () => ({ toTypedSchema: vi.fn((s) => s) }));
|
||||
|
||||
import Login from '../Login.vue';
|
||||
import en from '../../../localization/en.json';
|
||||
|
||||
const i18n = createI18n({
|
||||
locale: 'en',
|
||||
fallbackLocale: 'en',
|
||||
legacy: false,
|
||||
globalInjection: false,
|
||||
missingWarn: false,
|
||||
fallbackWarn: false,
|
||||
messages: { en }
|
||||
});
|
||||
|
||||
const stubs = {
|
||||
LoginSettingsDialog: { template: '<div class="login-settings-stub" />' },
|
||||
TooltipWrapper: {
|
||||
template: '<span><slot /></span>',
|
||||
props: ['side', 'content']
|
||||
},
|
||||
DropdownMenu: { template: '<div class="dropdown-stub"><slot /></div>' },
|
||||
DropdownMenuTrigger: {
|
||||
template: '<span><slot /></span>',
|
||||
props: ['asChild']
|
||||
},
|
||||
DropdownMenuContent: { template: '<div><slot /></div>' },
|
||||
DropdownMenuCheckboxItem: {
|
||||
template: '<div><slot /></div>',
|
||||
props: ['modelValue']
|
||||
},
|
||||
Button: {
|
||||
template:
|
||||
'<button :type="type || \'button\'" :id="id"><slot /></button>',
|
||||
props: ['type', 'variant', 'size', 'id']
|
||||
},
|
||||
Checkbox: { template: '<input type="checkbox" />', props: ['modelValue'] },
|
||||
Field: { template: '<div><slot /></div>' },
|
||||
FieldContent: { template: '<div><slot /></div>' },
|
||||
FieldError: { template: '<span />', props: ['errors'] },
|
||||
FieldGroup: { template: '<div><slot /></div>' },
|
||||
FieldLabel: { template: '<label><slot /></label>', props: ['for'] },
|
||||
InputGroupField: {
|
||||
template:
|
||||
'<input :id="id" :value="modelValue" :placeholder="placeholder" />',
|
||||
props: [
|
||||
'id',
|
||||
'modelValue',
|
||||
'type',
|
||||
'autocomplete',
|
||||
'name',
|
||||
'placeholder',
|
||||
'ariaInvalid',
|
||||
'clearable'
|
||||
],
|
||||
emits: ['update:modelValue', 'blur']
|
||||
},
|
||||
ArrowBigDownDash: { template: '<span />' },
|
||||
Languages: { template: '<span />' },
|
||||
Trash2: { template: '<span />' }
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param storeOverrides
|
||||
*/
|
||||
function mountLogin(storeOverrides = {}) {
|
||||
const pinia = createTestingPinia({
|
||||
stubActions: false,
|
||||
initialState: {
|
||||
Auth: {
|
||||
loginForm: {
|
||||
loading: false,
|
||||
username: '',
|
||||
password: '',
|
||||
endpoint: '',
|
||||
websocket: '',
|
||||
saveCredentials: false,
|
||||
lastUserLoggedIn: ''
|
||||
},
|
||||
...storeOverrides
|
||||
}
|
||||
}
|
||||
});
|
||||
return mount(Login, {
|
||||
global: {
|
||||
plugins: [i18n, pinia],
|
||||
stubs
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
describe('Login.vue', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('form rendering', () => {
|
||||
test('renders username and password input fields', () => {
|
||||
const wrapper = mountLogin();
|
||||
const usernameInput = wrapper.find('#login-form-username');
|
||||
const passwordInput = wrapper.find('#login-form-password');
|
||||
expect(usernameInput.exists()).toBe(true);
|
||||
expect(passwordInput.exists()).toBe(true);
|
||||
});
|
||||
|
||||
test('renders a login submit button', () => {
|
||||
const wrapper = mountLogin();
|
||||
const form = wrapper.find('#login-form');
|
||||
expect(form.exists()).toBe(true);
|
||||
const submitBtn = form.find('button[type="submit"]');
|
||||
expect(submitBtn.exists()).toBe(true);
|
||||
});
|
||||
|
||||
test('renders a register button', () => {
|
||||
const wrapper = mountLogin();
|
||||
const buttons = wrapper.findAll('button');
|
||||
const registerBtn = buttons.find(
|
||||
(b) => b.text() === en.view.login.register
|
||||
);
|
||||
expect(registerBtn).toBeTruthy();
|
||||
});
|
||||
|
||||
test('renders save credentials checkbox', () => {
|
||||
const wrapper = mountLogin();
|
||||
const checkbox = wrapper.find('input[type="checkbox"]');
|
||||
expect(checkbox.exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('saved accounts section', () => {
|
||||
test('does not render saved accounts when credentials are empty', () => {
|
||||
const wrapper = mountLogin();
|
||||
const divider = wrapper.find('.x-vertical-divider');
|
||||
expect(divider.exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('legal notice', () => {
|
||||
test('renders legal notice section', () => {
|
||||
const wrapper = mountLogin();
|
||||
const legalNotice = wrapper.find('.x-legal-notice-container');
|
||||
expect(legalNotice.exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('login settings', () => {
|
||||
test('renders LoginSettingsDialog stub', () => {
|
||||
const wrapper = mountLogin();
|
||||
expect(wrapper.find('.login-settings-stub').exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -257,6 +257,7 @@
|
||||
useGroupStore,
|
||||
useNotificationStore
|
||||
} from '../../stores';
|
||||
import { normalizeFavoriteGroupsChange, resolveFavoriteGroups } from './sidebarSettingsUtils';
|
||||
import { useGlobalSearchStore } from '../../stores/globalSearch';
|
||||
|
||||
import FriendsSidebar from './components/FriendsSidebar.vue';
|
||||
@@ -317,30 +318,16 @@
|
||||
return keys;
|
||||
});
|
||||
|
||||
const resolvedSidebarFavoriteGroups = computed(() => {
|
||||
if (sidebarFavoriteGroups.value.length === 0) {
|
||||
return allFavoriteGroupKeys.value;
|
||||
}
|
||||
return sidebarFavoriteGroups.value;
|
||||
});
|
||||
const resolvedSidebarFavoriteGroups = computed(() =>
|
||||
resolveFavoriteGroups(sidebarFavoriteGroups.value, allFavoriteGroupKeys.value)
|
||||
);
|
||||
|
||||
/**
|
||||
*
|
||||
* @param value
|
||||
*/
|
||||
function handleFavoriteGroupsChange(value) {
|
||||
if (!value || value.length === 0) {
|
||||
// Deselected all → reset to all (store as empty)
|
||||
setSidebarFavoriteGroups([]);
|
||||
return;
|
||||
}
|
||||
// If all groups are selected, store as empty (= all)
|
||||
const allKeys = allFavoriteGroupKeys.value;
|
||||
if (value.length >= allKeys.length && allKeys.every((k) => value.includes(k))) {
|
||||
setSidebarFavoriteGroups([]);
|
||||
return;
|
||||
}
|
||||
setSidebarFavoriteGroups(value);
|
||||
setSidebarFavoriteGroups(normalizeFavoriteGroupsChange(value, allFavoriteGroupKeys.value));
|
||||
}
|
||||
|
||||
const selectedFavGroupLabel = computed(() => {
|
||||
|
||||
145
src/views/Sidebar/__tests__/friendsSidebarUtils.test.js
Normal file
145
src/views/Sidebar/__tests__/friendsSidebarUtils.test.js
Normal file
@@ -0,0 +1,145 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import {
|
||||
buildFriendRow,
|
||||
buildInstanceHeaderRow,
|
||||
buildToggleRow,
|
||||
estimateRowSize
|
||||
} from '../friendsSidebarUtils';
|
||||
|
||||
// ─── buildToggleRow ──────────────────────────────────────────────────
|
||||
|
||||
describe('buildToggleRow', () => {
|
||||
test('creates a toggle-header row with defaults', () => {
|
||||
const row = buildToggleRow({ key: 'online', label: 'Online' });
|
||||
expect(row).toEqual({
|
||||
type: 'toggle-header',
|
||||
key: 'online',
|
||||
label: 'Online',
|
||||
count: null,
|
||||
expanded: true,
|
||||
headerPadding: null,
|
||||
paddingBottom: null,
|
||||
onClick: null
|
||||
});
|
||||
});
|
||||
|
||||
test('accepts all optional parameters', () => {
|
||||
const onClick = () => {};
|
||||
const row = buildToggleRow({
|
||||
key: 'vip',
|
||||
label: 'VIP',
|
||||
count: 5,
|
||||
expanded: false,
|
||||
headerPadding: 10,
|
||||
paddingBottom: 8,
|
||||
onClick
|
||||
});
|
||||
expect(row.count).toBe(5);
|
||||
expect(row.expanded).toBe(false);
|
||||
expect(row.headerPadding).toBe(10);
|
||||
expect(row.paddingBottom).toBe(8);
|
||||
expect(row.onClick).toBe(onClick);
|
||||
});
|
||||
|
||||
test('always sets type to toggle-header', () => {
|
||||
const row = buildToggleRow({ key: 'x', label: 'X' });
|
||||
expect(row.type).toBe('toggle-header');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── buildFriendRow ──────────────────────────────────────────────────
|
||||
|
||||
describe('buildFriendRow', () => {
|
||||
const friend = { id: 'usr_123', displayName: 'TestUser' };
|
||||
|
||||
test('creates a friend-item row with defaults', () => {
|
||||
const row = buildFriendRow(friend, 'friend:usr_123');
|
||||
expect(row).toEqual({
|
||||
type: 'friend-item',
|
||||
key: 'friend:usr_123',
|
||||
friend,
|
||||
isGroupByInstance: undefined,
|
||||
paddingBottom: undefined,
|
||||
itemStyle: undefined
|
||||
});
|
||||
});
|
||||
|
||||
test('passes options through', () => {
|
||||
const style = { opacity: 0.5 };
|
||||
const row = buildFriendRow(friend, 'k', {
|
||||
isGroupByInstance: true,
|
||||
paddingBottom: 4,
|
||||
itemStyle: style
|
||||
});
|
||||
expect(row.isGroupByInstance).toBe(true);
|
||||
expect(row.paddingBottom).toBe(4);
|
||||
expect(row.itemStyle).toBe(style);
|
||||
});
|
||||
|
||||
test('always sets type to friend-item', () => {
|
||||
const row = buildFriendRow(friend, 'k');
|
||||
expect(row.type).toBe('friend-item');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── buildInstanceHeaderRow ──────────────────────────────────────────
|
||||
|
||||
describe('buildInstanceHeaderRow', () => {
|
||||
test('creates an instance-header row', () => {
|
||||
const row = buildInstanceHeaderRow(
|
||||
'wrld_123:456~private',
|
||||
3,
|
||||
'inst:wrld_123'
|
||||
);
|
||||
expect(row).toEqual({
|
||||
type: 'instance-header',
|
||||
key: 'inst:wrld_123',
|
||||
location: 'wrld_123:456~private',
|
||||
count: 3,
|
||||
paddingBottom: 4
|
||||
});
|
||||
});
|
||||
|
||||
test('always has paddingBottom of 4', () => {
|
||||
const row = buildInstanceHeaderRow('loc', 1, 'k');
|
||||
expect(row.paddingBottom).toBe(4);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── estimateRowSize ─────────────────────────────────────────────────
|
||||
|
||||
describe('estimateRowSize', () => {
|
||||
test('returns 44 for null/undefined', () => {
|
||||
expect(estimateRowSize(null)).toBe(44);
|
||||
expect(estimateRowSize(undefined)).toBe(44);
|
||||
});
|
||||
|
||||
test('returns 28 + paddingBottom for toggle-header', () => {
|
||||
expect(estimateRowSize({ type: 'toggle-header' })).toBe(28);
|
||||
expect(
|
||||
estimateRowSize({ type: 'toggle-header', paddingBottom: 8 })
|
||||
).toBe(36);
|
||||
});
|
||||
|
||||
test('returns 24 + paddingBottom for vip-subheader', () => {
|
||||
expect(estimateRowSize({ type: 'vip-subheader' })).toBe(24);
|
||||
expect(
|
||||
estimateRowSize({ type: 'vip-subheader', paddingBottom: 4 })
|
||||
).toBe(28);
|
||||
});
|
||||
|
||||
test('returns 26 + paddingBottom for instance-header', () => {
|
||||
expect(estimateRowSize({ type: 'instance-header' })).toBe(26);
|
||||
expect(
|
||||
estimateRowSize({ type: 'instance-header', paddingBottom: 4 })
|
||||
).toBe(30);
|
||||
});
|
||||
|
||||
test('returns 52 + paddingBottom for any other type (friend-item)', () => {
|
||||
expect(estimateRowSize({ type: 'friend-item' })).toBe(52);
|
||||
expect(estimateRowSize({ type: 'friend-item', paddingBottom: 6 })).toBe(
|
||||
58
|
||||
);
|
||||
});
|
||||
});
|
||||
159
src/views/Sidebar/__tests__/groupsSidebarUtils.test.js
Normal file
159
src/views/Sidebar/__tests__/groupsSidebarUtils.test.js
Normal file
@@ -0,0 +1,159 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import {
|
||||
buildGroupHeaderRow,
|
||||
buildGroupItemRow,
|
||||
estimateGroupRowSize,
|
||||
getGroupId
|
||||
} from '../groupsSidebarUtils';
|
||||
|
||||
// ─── getGroupId ──────────────────────────────────────────────────────
|
||||
|
||||
describe('getGroupId', () => {
|
||||
test('extracts groupId from first element', () => {
|
||||
const group = [{ group: { groupId: 'grp_abc' } }];
|
||||
expect(getGroupId(group)).toBe('grp_abc');
|
||||
});
|
||||
|
||||
test('returns empty string for empty array', () => {
|
||||
expect(getGroupId([])).toBe('');
|
||||
});
|
||||
|
||||
test('returns empty string when group property is missing', () => {
|
||||
expect(getGroupId([{}])).toBe('');
|
||||
expect(getGroupId([{ group: {} }])).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── buildGroupHeaderRow ─────────────────────────────────────────────
|
||||
|
||||
describe('buildGroupHeaderRow', () => {
|
||||
const group = [
|
||||
{ group: { groupId: 'grp_1', name: 'Test Group' } },
|
||||
{ group: { groupId: 'grp_1', name: 'Test Group' } }
|
||||
];
|
||||
|
||||
test('builds header row with correct properties', () => {
|
||||
const cfg = { grp_1: { isCollapsed: false } };
|
||||
const row = buildGroupHeaderRow(group, 0, cfg);
|
||||
expect(row).toEqual({
|
||||
type: 'group-header',
|
||||
key: 'group-header:grp_1',
|
||||
groupId: 'grp_1',
|
||||
label: 'Test Group',
|
||||
count: 2,
|
||||
isCollapsed: false,
|
||||
headerPaddingTop: '0px'
|
||||
});
|
||||
});
|
||||
|
||||
test('sets headerPaddingTop to 10px for non-first groups', () => {
|
||||
const cfg = {};
|
||||
const row = buildGroupHeaderRow(group, 1, cfg);
|
||||
expect(row.headerPaddingTop).toBe('10px');
|
||||
});
|
||||
|
||||
test('reflects collapsed state from config', () => {
|
||||
const cfg = { grp_1: { isCollapsed: true } };
|
||||
const row = buildGroupHeaderRow(group, 0, cfg);
|
||||
expect(row.isCollapsed).toBe(true);
|
||||
});
|
||||
|
||||
test('defaults to not collapsed when cfg entry is missing', () => {
|
||||
const cfg = {};
|
||||
const row = buildGroupHeaderRow(group, 0, cfg);
|
||||
expect(row.isCollapsed).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── buildGroupItemRow ───────────────────────────────────────────────
|
||||
|
||||
describe('buildGroupItemRow', () => {
|
||||
const ref = {
|
||||
group: { iconUrl: 'https://example.com/icon.png', name: 'My Group' },
|
||||
instance: {
|
||||
id: 'inst_123',
|
||||
ownerId: 'usr_456',
|
||||
userCount: 5,
|
||||
capacity: 16,
|
||||
location: 'wrld_abc:inst_123~private'
|
||||
}
|
||||
};
|
||||
|
||||
test('builds item row with correct properties', () => {
|
||||
const row = buildGroupItemRow(ref, 0, 'grp_1', true);
|
||||
expect(row).toEqual({
|
||||
type: 'group-item',
|
||||
key: 'group-item:grp_1:inst_123',
|
||||
ownerId: 'usr_456',
|
||||
iconUrl: 'https://example.com/icon.png',
|
||||
name: 'My Group',
|
||||
userCount: 5,
|
||||
capacity: 16,
|
||||
location: 'wrld_abc:inst_123~private',
|
||||
isVisible: true
|
||||
});
|
||||
});
|
||||
|
||||
test('uses index as fallback key when instance id is missing', () => {
|
||||
const row = buildGroupItemRow({}, 7, 'grp_1', true);
|
||||
expect(row.key).toBe('group-item:grp_1:7');
|
||||
});
|
||||
|
||||
test('defaults to empty/zero values for missing properties', () => {
|
||||
const row = buildGroupItemRow({}, 0, 'grp_1', true);
|
||||
expect(row.ownerId).toBe('');
|
||||
expect(row.iconUrl).toBe('');
|
||||
expect(row.name).toBe('');
|
||||
expect(row.userCount).toBe(0);
|
||||
expect(row.capacity).toBe(0);
|
||||
expect(row.location).toBe('');
|
||||
});
|
||||
|
||||
test('hides age-gated instances when isAgeGatedVisible is false', () => {
|
||||
const ageGatedRef = { ...ref, ageGate: true };
|
||||
const row = buildGroupItemRow(ageGatedRef, 0, 'grp_1', false);
|
||||
expect(row.isVisible).toBe(false);
|
||||
});
|
||||
|
||||
test('shows age-gated instances when isAgeGatedVisible is true', () => {
|
||||
const ageGatedRef = { ...ref, ageGate: true };
|
||||
const row = buildGroupItemRow(ageGatedRef, 0, 'grp_1', true);
|
||||
expect(row.isVisible).toBe(true);
|
||||
});
|
||||
|
||||
test('detects age gate from location string', () => {
|
||||
const refWithAgeGateLocation = {
|
||||
...ref,
|
||||
location: 'wrld_abc:inst_123~ageGate'
|
||||
};
|
||||
const row = buildGroupItemRow(
|
||||
refWithAgeGateLocation,
|
||||
0,
|
||||
'grp_1',
|
||||
false
|
||||
);
|
||||
expect(row.isVisible).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── estimateGroupRowSize ────────────────────────────────────────────
|
||||
|
||||
describe('estimateGroupRowSize', () => {
|
||||
test('returns 44 for null/undefined', () => {
|
||||
expect(estimateGroupRowSize(null)).toBe(44);
|
||||
expect(estimateGroupRowSize(undefined)).toBe(44);
|
||||
});
|
||||
|
||||
test('returns 30 for group-header', () => {
|
||||
expect(estimateGroupRowSize({ type: 'group-header' })).toBe(30);
|
||||
});
|
||||
|
||||
test('returns 52 for group-item', () => {
|
||||
expect(estimateGroupRowSize({ type: 'group-item' })).toBe(52);
|
||||
});
|
||||
|
||||
test('returns 52 for unknown type', () => {
|
||||
expect(estimateGroupRowSize({ type: 'unknown' })).toBe(52);
|
||||
});
|
||||
});
|
||||
69
src/views/Sidebar/__tests__/sidebarSettingsUtils.test.js
Normal file
69
src/views/Sidebar/__tests__/sidebarSettingsUtils.test.js
Normal file
@@ -0,0 +1,69 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import {
|
||||
normalizeFavoriteGroupsChange,
|
||||
resolveFavoriteGroups
|
||||
} from '../sidebarSettingsUtils';
|
||||
|
||||
// ─── resolveFavoriteGroups ───────────────────────────────────────────
|
||||
|
||||
describe('resolveFavoriteGroups', () => {
|
||||
const allKeys = ['group_1', 'group_2', 'local:MyGroup'];
|
||||
|
||||
test('returns allKeys when stored is empty (= all)', () => {
|
||||
expect(resolveFavoriteGroups([], allKeys)).toEqual(allKeys);
|
||||
});
|
||||
|
||||
test('returns stored value when not empty', () => {
|
||||
const stored = ['group_1'];
|
||||
expect(resolveFavoriteGroups(stored, allKeys)).toEqual(stored);
|
||||
});
|
||||
|
||||
test('returns stored even if it equals allKeys', () => {
|
||||
expect(resolveFavoriteGroups([...allKeys], allKeys)).toEqual(allKeys);
|
||||
});
|
||||
|
||||
test('handles empty allKeys', () => {
|
||||
expect(resolveFavoriteGroups([], [])).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── normalizeFavoriteGroupsChange ───────────────────────────────────
|
||||
|
||||
describe('normalizeFavoriteGroupsChange', () => {
|
||||
const allKeys = ['group_1', 'group_2', 'local:MyGroup'];
|
||||
|
||||
test('returns [] when value is null', () => {
|
||||
expect(normalizeFavoriteGroupsChange(null, allKeys)).toEqual([]);
|
||||
});
|
||||
|
||||
test('returns [] when value is empty array', () => {
|
||||
expect(normalizeFavoriteGroupsChange([], allKeys)).toEqual([]);
|
||||
});
|
||||
|
||||
test('returns [] when all groups are selected', () => {
|
||||
expect(normalizeFavoriteGroupsChange([...allKeys], allKeys)).toEqual(
|
||||
[]
|
||||
);
|
||||
});
|
||||
|
||||
test('returns [] when value is superset of allKeys', () => {
|
||||
expect(
|
||||
normalizeFavoriteGroupsChange([...allKeys, 'extra'], allKeys)
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
test('returns filter subset when not all selected', () => {
|
||||
const subset = ['group_1'];
|
||||
expect(normalizeFavoriteGroupsChange(subset, allKeys)).toEqual(subset);
|
||||
});
|
||||
|
||||
test('returns filter subset with two items', () => {
|
||||
const subset = ['group_1', 'group_2'];
|
||||
expect(normalizeFavoriteGroupsChange(subset, allKeys)).toEqual(subset);
|
||||
});
|
||||
|
||||
test('treats non-empty value as all-selected when allKeys is empty (vacuous truth)', () => {
|
||||
expect(normalizeFavoriteGroupsChange(['group_1'], [])).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -152,6 +152,7 @@
|
||||
useLocationStore,
|
||||
useUserStore
|
||||
} from '../../../stores';
|
||||
import { buildFriendRow, buildInstanceHeaderRow, buildToggleRow, estimateRowSize } from '../friendsSidebarUtils';
|
||||
import { getFriendsSortFunction, isRealInstance, userImage, userStatusClass } from '../../../shared/utils';
|
||||
import { getFriendsLocations } from '../../../shared/utils/location.js';
|
||||
import { userRequest } from '../../../api';
|
||||
@@ -214,6 +215,10 @@
|
||||
|
||||
const shouldHideSameInstance = computed(() => isSidebarGroupByInstance.value && isHideFriendsInSameInstance.value);
|
||||
|
||||
/**
|
||||
*
|
||||
* @param list
|
||||
*/
|
||||
function excludeSameInstance(list) {
|
||||
if (!shouldHideSameInstance.value) {
|
||||
return list;
|
||||
@@ -323,41 +328,6 @@
|
||||
});
|
||||
});
|
||||
|
||||
const buildToggleRow = ({
|
||||
key,
|
||||
label,
|
||||
count = null,
|
||||
expanded = true,
|
||||
headerPadding = null,
|
||||
paddingBottom = null,
|
||||
onClick = null
|
||||
}) => ({
|
||||
type: 'toggle-header',
|
||||
key,
|
||||
label,
|
||||
count,
|
||||
expanded,
|
||||
headerPadding,
|
||||
paddingBottom,
|
||||
onClick
|
||||
});
|
||||
const buildFriendRow = (friend, key, options = {}) => ({
|
||||
type: 'friend-item',
|
||||
key,
|
||||
friend,
|
||||
isGroupByInstance: options.isGroupByInstance,
|
||||
paddingBottom: options.paddingBottom,
|
||||
itemStyle: options.itemStyle
|
||||
});
|
||||
|
||||
const buildInstanceHeaderRow = (location, count, key) => ({
|
||||
type: 'instance-header',
|
||||
key,
|
||||
location,
|
||||
count,
|
||||
paddingBottom: 4
|
||||
});
|
||||
|
||||
const virtualRows = computed(() => {
|
||||
const rows = [];
|
||||
|
||||
@@ -521,22 +491,6 @@
|
||||
return rows;
|
||||
});
|
||||
|
||||
const estimateRowSize = (row) => {
|
||||
if (!row) {
|
||||
return 44;
|
||||
}
|
||||
if (row.type === 'toggle-header') {
|
||||
return 28 + (row.paddingBottom || 0);
|
||||
}
|
||||
if (row.type === 'vip-subheader') {
|
||||
return 24 + (row.paddingBottom || 0);
|
||||
}
|
||||
if (row.type === 'instance-header') {
|
||||
return 26 + (row.paddingBottom || 0);
|
||||
}
|
||||
return 52 + (row.paddingBottom || 0);
|
||||
};
|
||||
|
||||
const virtualizer = useVirtualizer(
|
||||
computed(() => ({
|
||||
count: virtualRows.value.length,
|
||||
@@ -568,6 +522,9 @@
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function saveFriendsGroupStates() {
|
||||
configRepository.setBool('VRCX_isFriendsGroupMe', isFriendsGroupMe.value);
|
||||
configRepository.setBool('VRCX_isFriendsGroupFavorites', isVIPFriends.value);
|
||||
@@ -576,6 +533,9 @@
|
||||
configRepository.setBool('VRCX_isFriendsGroupOffline', isOfflineFriends.value);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
async function loadFriendsGroupStates() {
|
||||
isFriendsGroupMe.value = await configRepository.getBool('VRCX_isFriendsGroupMe', true);
|
||||
isVIPFriends.value = await configRepository.getBool('VRCX_isFriendsGroupFavorites', true);
|
||||
@@ -588,31 +548,49 @@
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function toggleSwitchGroupByInstanceCollapsed() {
|
||||
isSidebarGroupByInstanceCollapsed.value = !isSidebarGroupByInstanceCollapsed.value;
|
||||
configRepository.setBool('VRCX_sidebarGroupByInstanceCollapsed', isSidebarGroupByInstanceCollapsed.value);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function toggleFriendsGroupMe() {
|
||||
isFriendsGroupMe.value = !isFriendsGroupMe.value;
|
||||
saveFriendsGroupStates();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function toggleVIPFriends() {
|
||||
isVIPFriends.value = !isVIPFriends.value;
|
||||
saveFriendsGroupStates();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function toggleOnlineFriends() {
|
||||
isOnlineFriends.value = !isOnlineFriends.value;
|
||||
saveFriendsGroupStates();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function toggleActiveFriends() {
|
||||
isActiveFriends.value = !isActiveFriends.value;
|
||||
saveFriendsGroupStates();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function toggleOfflineFriends() {
|
||||
isOfflineFriends.value = !isOfflineFriends.value;
|
||||
saveFriendsGroupStates();
|
||||
@@ -660,12 +638,20 @@
|
||||
return history.slice(0, 10);
|
||||
});
|
||||
|
||||
/**
|
||||
*
|
||||
* @param value
|
||||
*/
|
||||
function changeStatus(value) {
|
||||
userRequest.saveCurrentUser({ status: value }).then(() => {
|
||||
toast.success('Status updated');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param status
|
||||
*/
|
||||
function setStatusFromHistory(status) {
|
||||
userRequest.saveCurrentUser({ statusDescription: status }).then(() => {
|
||||
toast.success('Status updated');
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useVirtualizer } from '@tanstack/vue-virtual';
|
||||
|
||||
import { buildGroupHeaderRow, buildGroupItemRow, estimateGroupRowSize, getGroupId } from '../groupsSidebarUtils';
|
||||
import { useAppearanceSettingsStore, useGroupStore } from '../../../stores';
|
||||
import { convertFileUrlToImageUrl } from '../../../shared/utils';
|
||||
|
||||
@@ -98,50 +99,27 @@
|
||||
return Array.from(groupMap.values()).sort(sortGroupInstancesByInGame);
|
||||
});
|
||||
|
||||
const buildGroupHeaderRow = (group, index) => ({
|
||||
type: 'group-header',
|
||||
key: `group-header:${getGroupId(group)}`,
|
||||
groupId: getGroupId(group),
|
||||
label: group[0]?.group?.name ?? '',
|
||||
count: group.length,
|
||||
isCollapsed: Boolean(groupInstancesCfg.value[getGroupId(group)]?.isCollapsed),
|
||||
headerPaddingTop: index === 0 ? '0px' : '10px'
|
||||
});
|
||||
const buildGroupHeaderRowLocal = (group, index) => buildGroupHeaderRow(group, index, groupInstancesCfg.value);
|
||||
|
||||
const buildGroupItemRow = (ref, index, groupId) => ({
|
||||
type: 'group-item',
|
||||
key: `group-item:${groupId}:${ref?.instance?.id ?? index}`,
|
||||
ownerId: ref?.instance?.ownerId ?? '',
|
||||
iconUrl: ref?.group?.iconUrl ?? '',
|
||||
name: ref?.group?.name ?? '',
|
||||
userCount: ref?.instance?.userCount ?? 0,
|
||||
capacity: ref?.instance?.capacity ?? 0,
|
||||
location: ref?.instance?.location ?? '',
|
||||
isVisible: Boolean(isAgeGatedInstancesVisible.value || !(ref?.ageGate || ref?.location?.includes('~ageGate')))
|
||||
});
|
||||
const buildGroupItemRowLocal = (ref, index, groupId) =>
|
||||
buildGroupItemRow(ref, index, groupId, isAgeGatedInstancesVisible.value);
|
||||
|
||||
const virtualRows = computed(() => {
|
||||
const rows = [];
|
||||
groupedGroupInstances.value.forEach((group, index) => {
|
||||
if (!group?.length) return;
|
||||
const groupId = getGroupId(group);
|
||||
rows.push(buildGroupHeaderRow(group, index));
|
||||
rows.push(buildGroupHeaderRowLocal(group, index));
|
||||
if (!groupInstancesCfg.value[groupId]?.isCollapsed) {
|
||||
group.forEach((ref, idx) => {
|
||||
rows.push(buildGroupItemRow(ref, idx, groupId));
|
||||
rows.push(buildGroupItemRowLocal(ref, idx, groupId));
|
||||
});
|
||||
}
|
||||
});
|
||||
return rows;
|
||||
});
|
||||
|
||||
const estimateRowSize = (row) => {
|
||||
if (!row) return 44;
|
||||
if (row.type === 'group-header') {
|
||||
return 30;
|
||||
}
|
||||
return 52;
|
||||
};
|
||||
const estimateRowSize = (row) => estimateGroupRowSize(row);
|
||||
|
||||
const virtualizer = useVirtualizer(
|
||||
computed(() => ({
|
||||
@@ -170,18 +148,22 @@
|
||||
transform: `translateY(${item.virtualItem.start}px)`
|
||||
});
|
||||
|
||||
/**
|
||||
*
|
||||
* @param url
|
||||
*/
|
||||
function getSmallGroupIconUrl(url) {
|
||||
return convertFileUrlToImageUrl(url);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param groupId
|
||||
*/
|
||||
function toggleGroupSidebarCollapse(groupId) {
|
||||
groupInstancesCfg.value[groupId].isCollapsed = !groupInstancesCfg.value[groupId].isCollapsed;
|
||||
}
|
||||
|
||||
function getGroupId(group) {
|
||||
return group[0]?.group?.groupId || '';
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
virtualizer.value?.measure?.();
|
||||
|
||||
88
src/views/Sidebar/friendsSidebarUtils.js
Normal file
88
src/views/Sidebar/friendsSidebarUtils.js
Normal file
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* @param {object} opts
|
||||
* @param {string} opts.key - Unique key
|
||||
* @param {string} opts.label - Display label
|
||||
* @param {number|null} [opts.count] - Item count
|
||||
* @param {boolean} [opts.expanded] - Whether section is expanded
|
||||
* @param {number|null} [opts.headerPadding] - Top padding in px
|
||||
* @param {number|null} [opts.paddingBottom] - Bottom padding in px
|
||||
* @param {Function|null} [opts.onClick] - Click handler
|
||||
* @returns {object} Row object
|
||||
*/
|
||||
export function buildToggleRow({
|
||||
key,
|
||||
label,
|
||||
count = null,
|
||||
expanded = true,
|
||||
headerPadding = null,
|
||||
paddingBottom = null,
|
||||
onClick = null
|
||||
}) {
|
||||
return {
|
||||
type: 'toggle-header',
|
||||
key,
|
||||
label,
|
||||
count,
|
||||
expanded,
|
||||
headerPadding,
|
||||
paddingBottom,
|
||||
onClick
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} friend - Friend data object
|
||||
* @param {string} key - Unique key
|
||||
* @param {object} [options] - Additional options
|
||||
* @param {boolean} [options.isGroupByInstance] - Whether grouped by instance
|
||||
* @param {number} [options.paddingBottom] - Bottom padding
|
||||
* @param {object} [options.itemStyle] - Additional style
|
||||
* @returns {object} Row object
|
||||
*/
|
||||
export function buildFriendRow(friend, key, options = {}) {
|
||||
return {
|
||||
type: 'friend-item',
|
||||
key,
|
||||
friend,
|
||||
isGroupByInstance: options.isGroupByInstance,
|
||||
paddingBottom: options.paddingBottom,
|
||||
itemStyle: options.itemStyle
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} location - Instance location string
|
||||
* @param {number} count - Number of friends in instance
|
||||
* @param {string} key - Unique key
|
||||
* @returns {object} Row object
|
||||
*/
|
||||
export function buildInstanceHeaderRow(location, count, key) {
|
||||
return {
|
||||
type: 'instance-header',
|
||||
key,
|
||||
location,
|
||||
count,
|
||||
paddingBottom: 4
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimate pixel height for a virtual row.
|
||||
* @param {object} row - Row object with type property
|
||||
* @returns {number} Estimated height in pixels
|
||||
*/
|
||||
export function estimateRowSize(row) {
|
||||
if (!row) {
|
||||
return 44;
|
||||
}
|
||||
if (row.type === 'toggle-header') {
|
||||
return 28 + (row.paddingBottom || 0);
|
||||
}
|
||||
if (row.type === 'vip-subheader') {
|
||||
return 24 + (row.paddingBottom || 0);
|
||||
}
|
||||
if (row.type === 'instance-header') {
|
||||
return 26 + (row.paddingBottom || 0);
|
||||
}
|
||||
return 52 + (row.paddingBottom || 0);
|
||||
}
|
||||
62
src/views/Sidebar/groupsSidebarUtils.js
Normal file
62
src/views/Sidebar/groupsSidebarUtils.js
Normal file
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* @param {Array} group - Array of group instance refs
|
||||
* @returns {string} The groupId, or empty string
|
||||
*/
|
||||
export function getGroupId(group) {
|
||||
return group[0]?.group?.groupId || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array} group - Array of group instance refs
|
||||
* @param {number} index - Index of the group in the list
|
||||
* @param {object} cfg - Collapsed state config object
|
||||
* @returns {object} Row object
|
||||
*/
|
||||
export function buildGroupHeaderRow(group, index, cfg) {
|
||||
const groupId = getGroupId(group);
|
||||
return {
|
||||
type: 'group-header',
|
||||
key: `group-header:${groupId}`,
|
||||
groupId,
|
||||
label: group[0]?.group?.name ?? '',
|
||||
count: group.length,
|
||||
isCollapsed: Boolean(cfg[groupId]?.isCollapsed),
|
||||
headerPaddingTop: index === 0 ? '0px' : '10px'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} ref - Group instance ref object
|
||||
* @param {number} index - Index within the group
|
||||
* @param {string} groupId - Parent group ID
|
||||
* @param {boolean} isAgeGatedVisible - Whether age-gated instances should be visible
|
||||
* @returns {object} Row object
|
||||
*/
|
||||
export function buildGroupItemRow(ref, index, groupId, isAgeGatedVisible) {
|
||||
return {
|
||||
type: 'group-item',
|
||||
key: `group-item:${groupId}:${ref?.instance?.id ?? index}`,
|
||||
ownerId: ref?.instance?.ownerId ?? '',
|
||||
iconUrl: ref?.group?.iconUrl ?? '',
|
||||
name: ref?.group?.name ?? '',
|
||||
userCount: ref?.instance?.userCount ?? 0,
|
||||
capacity: ref?.instance?.capacity ?? 0,
|
||||
location: ref?.instance?.location ?? '',
|
||||
isVisible: Boolean(
|
||||
isAgeGatedVisible ||
|
||||
!(ref?.ageGate || ref?.location?.includes('~ageGate'))
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} row - Row object with type property
|
||||
* @returns {number} Estimated height in pixels
|
||||
*/
|
||||
export function estimateGroupRowSize(row) {
|
||||
if (!row) return 44;
|
||||
if (row.type === 'group-header') {
|
||||
return 30;
|
||||
}
|
||||
return 52;
|
||||
}
|
||||
29
src/views/Sidebar/sidebarSettingsUtils.js
Normal file
29
src/views/Sidebar/sidebarSettingsUtils.js
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* @param {string[]} stored - Stored favorite groups selection
|
||||
* @param {string[]} allKeys - All available group keys
|
||||
* @returns {string[]} Resolved group keys
|
||||
*/
|
||||
export function resolveFavoriteGroups(stored, allKeys) {
|
||||
if (stored.length === 0) {
|
||||
return allKeys;
|
||||
}
|
||||
return stored;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string[]|null} value - New selection value
|
||||
* @param {string[]} allKeys - All available group keys
|
||||
* @returns {string[]} Value to store
|
||||
*/
|
||||
export function normalizeFavoriteGroupsChange(value, allKeys) {
|
||||
if (!value || value.length === 0) {
|
||||
return [];
|
||||
}
|
||||
if (
|
||||
value.length >= allKeys.length &&
|
||||
allKeys.every((k) => value.includes(k))
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
return value;
|
||||
}
|
||||
Reference in New Issue
Block a user