mirror of
https://github.com/vrcx-team/VRCX.git
synced 2026-04-06 00:32:02 +02:00
fix
This commit is contained in:
@@ -80,12 +80,11 @@ vi.mock('../../../../services/request', () => ({
|
||||
}));
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key) => key
|
||||
,
|
||||
locale: require('vue').ref('en')
|
||||
}),
|
||||
t: (key) => key,
|
||||
locale: require('vue').ref('en')
|
||||
}),
|
||||
createI18n: () => ({
|
||||
global: { t: (key) => key , locale: require('vue').ref('en') },
|
||||
global: { t: (key) => key, locale: require('vue').ref('en') },
|
||||
install: vi.fn()
|
||||
})
|
||||
}));
|
||||
@@ -96,7 +95,7 @@ vi.mock('worker-timers', () => ({
|
||||
|
||||
import { useGroupMembers } from '../useGroupMembers';
|
||||
import { groupRequest, queryRequest } from '../../../../api';
|
||||
import { groupDialogFilterOptions } from '../../../../shared/constants';
|
||||
import { FILTER_EVERYONE } from '../../../../shared/constants';
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -409,9 +408,7 @@ describe('useGroupMembers', () => {
|
||||
|
||||
test('marks done on error', async () => {
|
||||
const groupDialog = createGroupDialog();
|
||||
queryRequest.fetch.mockRejectedValue(
|
||||
new Error('fail')
|
||||
);
|
||||
queryRequest.fetch.mockRejectedValue(new Error('fail'));
|
||||
|
||||
const {
|
||||
loadMoreGroupMembers,
|
||||
@@ -450,7 +447,7 @@ describe('useGroupMembers', () => {
|
||||
describe('setGroupMemberFilter', () => {
|
||||
test('does not reload when filter unchanged', async () => {
|
||||
const { markRaw } = require('vue');
|
||||
const filter = markRaw(groupDialogFilterOptions.everyone);
|
||||
const filter = markRaw(FILTER_EVERYONE);
|
||||
const groupDialog = createGroupDialog();
|
||||
// Use markRaw to prevent Vue from wrapping the filter in a Proxy
|
||||
groupDialog.value.memberFilter = filter;
|
||||
|
||||
@@ -2,7 +2,9 @@ import { computed, ref } from 'vue';
|
||||
|
||||
import {
|
||||
groupDialogFilterOptions,
|
||||
groupDialogSortingOptions
|
||||
groupDialogSortingOptions,
|
||||
FILTER_EVERYONE,
|
||||
FILTER_NO_ROLE
|
||||
} from '../../../shared/constants';
|
||||
import { groupRequest, queryRequest } from '../../../api';
|
||||
import { debounce } from '../../../shared/utils';
|
||||
@@ -39,7 +41,7 @@ export function useGroupMembers(
|
||||
return groupDialog.value?.memberSortOrder?.value ?? '';
|
||||
},
|
||||
set(value) {
|
||||
const option = Object.values(groupDialogSortingOptions).find(
|
||||
const option = groupDialogSortingOptions.find(
|
||||
(item) => item.value === value
|
||||
);
|
||||
if (option) {
|
||||
@@ -61,11 +63,11 @@ export function useGroupMembers(
|
||||
if (!key) return;
|
||||
|
||||
if (key === 'everyone') {
|
||||
setGroupMemberFilter(groupDialogFilterOptions.everyone);
|
||||
setGroupMemberFilter(FILTER_EVERYONE);
|
||||
return;
|
||||
}
|
||||
if (key === 'usersWithNoRole') {
|
||||
setGroupMemberFilter(groupDialogFilterOptions.usersWithNoRole);
|
||||
setGroupMemberFilter(FILTER_NO_ROLE);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -82,18 +84,16 @@ export function useGroupMembers(
|
||||
});
|
||||
|
||||
const groupDialogMemberFilterGroups = computed(() => {
|
||||
const filterItems = Object.values(groupDialogFilterOptions).map(
|
||||
(item) => ({
|
||||
value:
|
||||
item.id === null
|
||||
? 'everyone'
|
||||
: item.id === ''
|
||||
? 'usersWithNoRole'
|
||||
: `role:${item.id}`,
|
||||
label: t(item.name),
|
||||
search: t(item.name)
|
||||
})
|
||||
);
|
||||
const filterItems = groupDialogFilterOptions.map((item) => ({
|
||||
value:
|
||||
item.id === null
|
||||
? 'everyone'
|
||||
: item.id === ''
|
||||
? 'usersWithNoRole'
|
||||
: `role:${item.id}`,
|
||||
label: t(item.name),
|
||||
search: t(item.name)
|
||||
}));
|
||||
|
||||
const roleItems = (groupDialog.value?.ref?.roles ?? [])
|
||||
.filter((role) => !role.defaultRole)
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
<script setup>
|
||||
import { DialogClose, DialogContent, DialogPortal, useForwardPropsEmits } from 'reka-ui';
|
||||
import {
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogPortal,
|
||||
useForwardPropsEmits,
|
||||
VisuallyHidden
|
||||
} from 'reka-ui';
|
||||
import { inject, onBeforeUnmount, ref, watch } from 'vue';
|
||||
import { X } from 'lucide-vue-next';
|
||||
import { acquireModalPortalLayer } from '@/lib/modalPortalLayers';
|
||||
@@ -71,6 +78,10 @@
|
||||
">
|
||||
<slot />
|
||||
|
||||
<VisuallyHidden as-child>
|
||||
<DialogDescription />
|
||||
</VisuallyHidden>
|
||||
|
||||
<DialogClose
|
||||
v-if="showCloseButton"
|
||||
data-slot="dialog-close"
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
<script setup>
|
||||
import { DialogClose, DialogContent, DialogOverlay, DialogPortal, useForwardPropsEmits } from 'reka-ui';
|
||||
import {
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
useForwardPropsEmits,
|
||||
VisuallyHidden
|
||||
} from 'reka-ui';
|
||||
import { inject, onBeforeUnmount, ref, watch } from 'vue';
|
||||
import { X } from 'lucide-vue-next';
|
||||
import { acquireModalPortalLayer } from '@/lib/modalPortalLayers';
|
||||
@@ -76,6 +84,10 @@
|
||||
">
|
||||
<slot />
|
||||
|
||||
<VisuallyHidden as-child>
|
||||
<DialogDescription />
|
||||
</VisuallyHidden>
|
||||
|
||||
<DialogClose class="absolute top-4 right-4 p-0.5 transition-colors rounded-md hover:bg-secondary">
|
||||
<X class="w-4 h-4" />
|
||||
<span class="sr-only">Close</span>
|
||||
|
||||
@@ -5,12 +5,11 @@ import { i18n } from '../plugins/i18n';
|
||||
import {
|
||||
convertFileUrlToImageUrl,
|
||||
createDefaultGroupRef,
|
||||
sanitizeEntityJson,
|
||||
replaceBioSymbols
|
||||
sanitizeEntityJson
|
||||
} from '../shared/utils';
|
||||
import { groupRequest, instanceRequest, queryRequest } from '../api';
|
||||
import { database } from '../services/database';
|
||||
import { groupDialogFilterOptions } from '../shared/constants/';
|
||||
import { FILTER_EVERYONE } from '../shared/constants/';
|
||||
import { patchGroupFromEvent } from '../queries';
|
||||
import { useGameStore } from '../stores/game';
|
||||
import { useInstanceStore } from '../stores/instance';
|
||||
@@ -318,7 +317,7 @@ export function showGroupDialog(groupId, options = {}) {
|
||||
D.memberSearchResults = [];
|
||||
D.galleries = {};
|
||||
D.members = [];
|
||||
D.memberFilter = groupDialogFilterOptions.everyone;
|
||||
D.memberFilter = FILTER_EVERYONE;
|
||||
D.calendar = [];
|
||||
const loadGroupRequest = groupRequest.getGroup({
|
||||
groupId,
|
||||
|
||||
@@ -1,22 +1,29 @@
|
||||
const groupDialogSortingOptions = {
|
||||
joinedAtDesc: {
|
||||
const groupDialogSortingOptions = [
|
||||
{
|
||||
name: 'dialog.group.members.sorting.joined_at_desc',
|
||||
value: 'joinedAt:desc'
|
||||
},
|
||||
joinedAtAsc: {
|
||||
{
|
||||
name: 'dialog.group.members.sorting.joined_at_asc',
|
||||
value: 'joinedAt:asc'
|
||||
}
|
||||
];
|
||||
|
||||
const FILTER_EVERYONE = {
|
||||
name: 'dialog.group.members.filters.everyone',
|
||||
id: null
|
||||
};
|
||||
|
||||
const groupDialogFilterOptions = {
|
||||
everyone: {
|
||||
name: 'dialog.group.members.filters.everyone',
|
||||
id: null
|
||||
},
|
||||
usersWithNoRole: {
|
||||
name: 'dialog.group.members.filters.users_with_no_role',
|
||||
id: ''
|
||||
}
|
||||
const FILTER_NO_ROLE = {
|
||||
name: 'dialog.group.members.filters.users_with_no_role',
|
||||
id: ''
|
||||
};
|
||||
|
||||
const groupDialogFilterOptions = [FILTER_EVERYONE, FILTER_NO_ROLE];
|
||||
|
||||
export {
|
||||
groupDialogSortingOptions,
|
||||
groupDialogFilterOptions,
|
||||
FILTER_EVERYONE,
|
||||
FILTER_NO_ROLE
|
||||
};
|
||||
export { groupDialogSortingOptions, groupDialogFilterOptions };
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { reactive, ref, watch } from 'vue';
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
import { playerModerationRequest } from '../api';
|
||||
import { runRefreshPlayerModerationsFlow } from '../coordinators/moderationCoordinator';
|
||||
import { useUserStore } from './user';
|
||||
import { watchState } from '../services/watchState';
|
||||
|
||||
@@ -35,6 +35,9 @@ export const useModerationStore = defineStore('Moderation', () => {
|
||||
cachedPlayerModerationsUserIds.clear();
|
||||
playerModerationTable.value.loading = false;
|
||||
playerModerationTable.value.data = [];
|
||||
if (isLoggedIn) {
|
||||
runRefreshPlayerModerationsFlow();
|
||||
}
|
||||
},
|
||||
{ flush: 'sync' }
|
||||
);
|
||||
|
||||
@@ -243,3 +243,20 @@
|
||||
feedTableLookup();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.feed :deep(.x-text-removed) {
|
||||
text-decoration: line-through;
|
||||
color: #ff0000;
|
||||
background-color: rgba(255, 0, 0, 0.2);
|
||||
padding: 2px 2px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.feed :deep(.x-text-added) {
|
||||
color: rgb(35, 188, 35);
|
||||
background-color: rgba(76, 255, 80, 0.2);
|
||||
padding: 2px 2px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
121
src/views/Feed/__tests__/columns.test.js
Normal file
121
src/views/Feed/__tests__/columns.test.js
Normal file
@@ -0,0 +1,121 @@
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
vi.mock('../../../plugins/i18n', () => ({
|
||||
i18n: {
|
||||
global: {
|
||||
t: (key) => key
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
vi.mock('../../../stores', () => ({
|
||||
useGalleryStore: () => ({
|
||||
showFullscreenImageDialog: vi.fn()
|
||||
})
|
||||
}));
|
||||
|
||||
vi.mock('../../../coordinators/userCoordinator', () => ({
|
||||
showUserDialog: vi.fn()
|
||||
}));
|
||||
|
||||
vi.mock('../../../shared/utils', () => ({
|
||||
formatDateFilter: (value) => value,
|
||||
statusClass: (value) => value,
|
||||
timeToText: (value) => value
|
||||
}));
|
||||
|
||||
vi.mock('../../../components/AvatarInfo.vue', () => ({
|
||||
default: 'AvatarInfo'
|
||||
}));
|
||||
|
||||
vi.mock('../../../components/Location.vue', () => ({
|
||||
default: 'Location'
|
||||
}));
|
||||
|
||||
vi.mock('../../../components/ui/badge', () => ({
|
||||
Badge: 'Badge'
|
||||
}));
|
||||
|
||||
vi.mock('../../../components/ui/button', () => ({
|
||||
Button: 'Button'
|
||||
}));
|
||||
|
||||
vi.mock('../../../components/ui/tooltip', () => ({
|
||||
Tooltip: 'Tooltip',
|
||||
TooltipContent: 'TooltipContent',
|
||||
TooltipTrigger: 'TooltipTrigger'
|
||||
}));
|
||||
|
||||
vi.mock('lucide-vue-next', () => ({
|
||||
ArrowDown: 'ArrowDown',
|
||||
ArrowRight: 'ArrowRight',
|
||||
ArrowUpDown: 'ArrowUpDown',
|
||||
ChevronDown: 'ChevronDown',
|
||||
ChevronRight: 'ChevronRight'
|
||||
}));
|
||||
|
||||
import { columns } from '../columns.jsx';
|
||||
|
||||
function createElement(type, props, ...children) {
|
||||
return {
|
||||
type,
|
||||
props: props ?? {},
|
||||
children: children.flat()
|
||||
};
|
||||
}
|
||||
|
||||
function findNode(node, predicate) {
|
||||
if (!node) return null;
|
||||
if (Array.isArray(node)) {
|
||||
for (const item of node) {
|
||||
const result = findNode(item, predicate);
|
||||
if (result) return result;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (predicate(node)) return node;
|
||||
if (!node.children) return null;
|
||||
return findNode(node.children, predicate);
|
||||
}
|
||||
|
||||
describe('views/Feed/columns.jsx', () => {
|
||||
beforeEach(() => {
|
||||
globalThis.React = { createElement };
|
||||
});
|
||||
|
||||
test('renders current bio text in detail cell', () => {
|
||||
const detailCol = columns.find((c) => c.id === 'detail');
|
||||
const row = {
|
||||
original: {
|
||||
type: 'Bio',
|
||||
previousBio: 'hello\nold',
|
||||
bio: 'hello\nnew'
|
||||
}
|
||||
};
|
||||
|
||||
const vnode = detailCol.cell({ row });
|
||||
|
||||
expect(vnode.type).toBe('span');
|
||||
expect(vnode.children).toContain('hello\nnew');
|
||||
expect(vnode.props.innerHTML).toBeUndefined();
|
||||
});
|
||||
|
||||
test('keeps multiline bio diff in expanded row', () => {
|
||||
const expanderCol = columns.find((c) => c.id === 'expander');
|
||||
const expanded = expanderCol.meta.expandedRow({
|
||||
row: {
|
||||
original: {
|
||||
type: 'Bio',
|
||||
previousBio: 'line1\nline2',
|
||||
bio: 'line1\nline3'
|
||||
}
|
||||
}
|
||||
});
|
||||
const preNode = findNode(expanded, (n) => n.type === 'pre');
|
||||
|
||||
expect(preNode).toBeTruthy();
|
||||
expect(preNode.props.innerHTML).toContain('x-text-added');
|
||||
expect(preNode.props.innerHTML).toContain('x-text-removed');
|
||||
expect(preNode.props.innerHTML).toContain('<br>');
|
||||
});
|
||||
});
|
||||
@@ -386,9 +386,9 @@ export const columns = [
|
||||
|
||||
if (type === 'Bio') {
|
||||
return (
|
||||
<div class="block w-full min-w-0 truncate">
|
||||
<span class="block w-full min-w-0 truncate">
|
||||
{original.bio}
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -415,7 +415,7 @@ function formatDifference(
|
||||
markerDeletion = '<span class="x-text-removed">{{text}}</span>'
|
||||
) {
|
||||
[oldString, newString] = [oldString, newString].map((s) =>
|
||||
s
|
||||
String(s ?? '')
|
||||
.replaceAll(/&/g, '&')
|
||||
.replaceAll(/</g, '<')
|
||||
.replaceAll(/>/g, '>')
|
||||
|
||||
Reference in New Issue
Block a user