mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-05-06 22:46:06 +02:00
fix
This commit is contained in:
@@ -80,8 +80,7 @@ vi.mock('../../../../services/request', () => ({
|
|||||||
}));
|
}));
|
||||||
vi.mock('vue-i18n', () => ({
|
vi.mock('vue-i18n', () => ({
|
||||||
useI18n: () => ({
|
useI18n: () => ({
|
||||||
t: (key) => key
|
t: (key) => key,
|
||||||
,
|
|
||||||
locale: require('vue').ref('en')
|
locale: require('vue').ref('en')
|
||||||
}),
|
}),
|
||||||
createI18n: () => ({
|
createI18n: () => ({
|
||||||
@@ -96,7 +95,7 @@ vi.mock('worker-timers', () => ({
|
|||||||
|
|
||||||
import { useGroupMembers } from '../useGroupMembers';
|
import { useGroupMembers } from '../useGroupMembers';
|
||||||
import { groupRequest, queryRequest } from '../../../../api';
|
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 () => {
|
test('marks done on error', async () => {
|
||||||
const groupDialog = createGroupDialog();
|
const groupDialog = createGroupDialog();
|
||||||
queryRequest.fetch.mockRejectedValue(
|
queryRequest.fetch.mockRejectedValue(new Error('fail'));
|
||||||
new Error('fail')
|
|
||||||
);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
loadMoreGroupMembers,
|
loadMoreGroupMembers,
|
||||||
@@ -450,7 +447,7 @@ describe('useGroupMembers', () => {
|
|||||||
describe('setGroupMemberFilter', () => {
|
describe('setGroupMemberFilter', () => {
|
||||||
test('does not reload when filter unchanged', async () => {
|
test('does not reload when filter unchanged', async () => {
|
||||||
const { markRaw } = require('vue');
|
const { markRaw } = require('vue');
|
||||||
const filter = markRaw(groupDialogFilterOptions.everyone);
|
const filter = markRaw(FILTER_EVERYONE);
|
||||||
const groupDialog = createGroupDialog();
|
const groupDialog = createGroupDialog();
|
||||||
// Use markRaw to prevent Vue from wrapping the filter in a Proxy
|
// Use markRaw to prevent Vue from wrapping the filter in a Proxy
|
||||||
groupDialog.value.memberFilter = filter;
|
groupDialog.value.memberFilter = filter;
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ import { computed, ref } from 'vue';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
groupDialogFilterOptions,
|
groupDialogFilterOptions,
|
||||||
groupDialogSortingOptions
|
groupDialogSortingOptions,
|
||||||
|
FILTER_EVERYONE,
|
||||||
|
FILTER_NO_ROLE
|
||||||
} from '../../../shared/constants';
|
} from '../../../shared/constants';
|
||||||
import { groupRequest, queryRequest } from '../../../api';
|
import { groupRequest, queryRequest } from '../../../api';
|
||||||
import { debounce } from '../../../shared/utils';
|
import { debounce } from '../../../shared/utils';
|
||||||
@@ -39,7 +41,7 @@ export function useGroupMembers(
|
|||||||
return groupDialog.value?.memberSortOrder?.value ?? '';
|
return groupDialog.value?.memberSortOrder?.value ?? '';
|
||||||
},
|
},
|
||||||
set(value) {
|
set(value) {
|
||||||
const option = Object.values(groupDialogSortingOptions).find(
|
const option = groupDialogSortingOptions.find(
|
||||||
(item) => item.value === value
|
(item) => item.value === value
|
||||||
);
|
);
|
||||||
if (option) {
|
if (option) {
|
||||||
@@ -61,11 +63,11 @@ export function useGroupMembers(
|
|||||||
if (!key) return;
|
if (!key) return;
|
||||||
|
|
||||||
if (key === 'everyone') {
|
if (key === 'everyone') {
|
||||||
setGroupMemberFilter(groupDialogFilterOptions.everyone);
|
setGroupMemberFilter(FILTER_EVERYONE);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (key === 'usersWithNoRole') {
|
if (key === 'usersWithNoRole') {
|
||||||
setGroupMemberFilter(groupDialogFilterOptions.usersWithNoRole);
|
setGroupMemberFilter(FILTER_NO_ROLE);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,8 +84,7 @@ export function useGroupMembers(
|
|||||||
});
|
});
|
||||||
|
|
||||||
const groupDialogMemberFilterGroups = computed(() => {
|
const groupDialogMemberFilterGroups = computed(() => {
|
||||||
const filterItems = Object.values(groupDialogFilterOptions).map(
|
const filterItems = groupDialogFilterOptions.map((item) => ({
|
||||||
(item) => ({
|
|
||||||
value:
|
value:
|
||||||
item.id === null
|
item.id === null
|
||||||
? 'everyone'
|
? 'everyone'
|
||||||
@@ -92,8 +93,7 @@ export function useGroupMembers(
|
|||||||
: `role:${item.id}`,
|
: `role:${item.id}`,
|
||||||
label: t(item.name),
|
label: t(item.name),
|
||||||
search: t(item.name)
|
search: t(item.name)
|
||||||
})
|
}));
|
||||||
);
|
|
||||||
|
|
||||||
const roleItems = (groupDialog.value?.ref?.roles ?? [])
|
const roleItems = (groupDialog.value?.ref?.roles ?? [])
|
||||||
.filter((role) => !role.defaultRole)
|
.filter((role) => !role.defaultRole)
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
<script setup>
|
<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 { inject, onBeforeUnmount, ref, watch } from 'vue';
|
||||||
import { X } from 'lucide-vue-next';
|
import { X } from 'lucide-vue-next';
|
||||||
import { acquireModalPortalLayer } from '@/lib/modalPortalLayers';
|
import { acquireModalPortalLayer } from '@/lib/modalPortalLayers';
|
||||||
@@ -71,6 +78,10 @@
|
|||||||
">
|
">
|
||||||
<slot />
|
<slot />
|
||||||
|
|
||||||
|
<VisuallyHidden as-child>
|
||||||
|
<DialogDescription />
|
||||||
|
</VisuallyHidden>
|
||||||
|
|
||||||
<DialogClose
|
<DialogClose
|
||||||
v-if="showCloseButton"
|
v-if="showCloseButton"
|
||||||
data-slot="dialog-close"
|
data-slot="dialog-close"
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
<script setup>
|
<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 { inject, onBeforeUnmount, ref, watch } from 'vue';
|
||||||
import { X } from 'lucide-vue-next';
|
import { X } from 'lucide-vue-next';
|
||||||
import { acquireModalPortalLayer } from '@/lib/modalPortalLayers';
|
import { acquireModalPortalLayer } from '@/lib/modalPortalLayers';
|
||||||
@@ -76,6 +84,10 @@
|
|||||||
">
|
">
|
||||||
<slot />
|
<slot />
|
||||||
|
|
||||||
|
<VisuallyHidden as-child>
|
||||||
|
<DialogDescription />
|
||||||
|
</VisuallyHidden>
|
||||||
|
|
||||||
<DialogClose class="absolute top-4 right-4 p-0.5 transition-colors rounded-md hover:bg-secondary">
|
<DialogClose class="absolute top-4 right-4 p-0.5 transition-colors rounded-md hover:bg-secondary">
|
||||||
<X class="w-4 h-4" />
|
<X class="w-4 h-4" />
|
||||||
<span class="sr-only">Close</span>
|
<span class="sr-only">Close</span>
|
||||||
|
|||||||
@@ -5,12 +5,11 @@ import { i18n } from '../plugins/i18n';
|
|||||||
import {
|
import {
|
||||||
convertFileUrlToImageUrl,
|
convertFileUrlToImageUrl,
|
||||||
createDefaultGroupRef,
|
createDefaultGroupRef,
|
||||||
sanitizeEntityJson,
|
sanitizeEntityJson
|
||||||
replaceBioSymbols
|
|
||||||
} from '../shared/utils';
|
} from '../shared/utils';
|
||||||
import { groupRequest, instanceRequest, queryRequest } from '../api';
|
import { groupRequest, instanceRequest, queryRequest } from '../api';
|
||||||
import { database } from '../services/database';
|
import { database } from '../services/database';
|
||||||
import { groupDialogFilterOptions } from '../shared/constants/';
|
import { FILTER_EVERYONE } from '../shared/constants/';
|
||||||
import { patchGroupFromEvent } from '../queries';
|
import { patchGroupFromEvent } from '../queries';
|
||||||
import { useGameStore } from '../stores/game';
|
import { useGameStore } from '../stores/game';
|
||||||
import { useInstanceStore } from '../stores/instance';
|
import { useInstanceStore } from '../stores/instance';
|
||||||
@@ -318,7 +317,7 @@ export function showGroupDialog(groupId, options = {}) {
|
|||||||
D.memberSearchResults = [];
|
D.memberSearchResults = [];
|
||||||
D.galleries = {};
|
D.galleries = {};
|
||||||
D.members = [];
|
D.members = [];
|
||||||
D.memberFilter = groupDialogFilterOptions.everyone;
|
D.memberFilter = FILTER_EVERYONE;
|
||||||
D.calendar = [];
|
D.calendar = [];
|
||||||
const loadGroupRequest = groupRequest.getGroup({
|
const loadGroupRequest = groupRequest.getGroup({
|
||||||
groupId,
|
groupId,
|
||||||
|
|||||||
@@ -1,22 +1,29 @@
|
|||||||
const groupDialogSortingOptions = {
|
const groupDialogSortingOptions = [
|
||||||
joinedAtDesc: {
|
{
|
||||||
name: 'dialog.group.members.sorting.joined_at_desc',
|
name: 'dialog.group.members.sorting.joined_at_desc',
|
||||||
value: 'joinedAt:desc'
|
value: 'joinedAt:desc'
|
||||||
},
|
},
|
||||||
joinedAtAsc: {
|
{
|
||||||
name: 'dialog.group.members.sorting.joined_at_asc',
|
name: 'dialog.group.members.sorting.joined_at_asc',
|
||||||
value: 'joinedAt:asc'
|
value: 'joinedAt:asc'
|
||||||
}
|
}
|
||||||
};
|
];
|
||||||
|
|
||||||
const groupDialogFilterOptions = {
|
const FILTER_EVERYONE = {
|
||||||
everyone: {
|
|
||||||
name: 'dialog.group.members.filters.everyone',
|
name: 'dialog.group.members.filters.everyone',
|
||||||
id: null
|
id: null
|
||||||
},
|
};
|
||||||
usersWithNoRole: {
|
|
||||||
|
const FILTER_NO_ROLE = {
|
||||||
name: 'dialog.group.members.filters.users_with_no_role',
|
name: 'dialog.group.members.filters.users_with_no_role',
|
||||||
id: ''
|
id: ''
|
||||||
}
|
|
||||||
};
|
};
|
||||||
export { groupDialogSortingOptions, groupDialogFilterOptions };
|
|
||||||
|
const groupDialogFilterOptions = [FILTER_EVERYONE, FILTER_NO_ROLE];
|
||||||
|
|
||||||
|
export {
|
||||||
|
groupDialogSortingOptions,
|
||||||
|
groupDialogFilterOptions,
|
||||||
|
FILTER_EVERYONE,
|
||||||
|
FILTER_NO_ROLE
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { reactive, ref, watch } from 'vue';
|
import { reactive, ref, watch } from 'vue';
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
|
|
||||||
import { playerModerationRequest } from '../api';
|
import { runRefreshPlayerModerationsFlow } from '../coordinators/moderationCoordinator';
|
||||||
import { useUserStore } from './user';
|
import { useUserStore } from './user';
|
||||||
import { watchState } from '../services/watchState';
|
import { watchState } from '../services/watchState';
|
||||||
|
|
||||||
@@ -35,6 +35,9 @@ export const useModerationStore = defineStore('Moderation', () => {
|
|||||||
cachedPlayerModerationsUserIds.clear();
|
cachedPlayerModerationsUserIds.clear();
|
||||||
playerModerationTable.value.loading = false;
|
playerModerationTable.value.loading = false;
|
||||||
playerModerationTable.value.data = [];
|
playerModerationTable.value.data = [];
|
||||||
|
if (isLoggedIn) {
|
||||||
|
runRefreshPlayerModerationsFlow();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{ flush: 'sync' }
|
{ flush: 'sync' }
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -243,3 +243,20 @@
|
|||||||
feedTableLookup();
|
feedTableLookup();
|
||||||
}
|
}
|
||||||
</script>
|
</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>
|
||||||
|
|||||||
@@ -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') {
|
if (type === 'Bio') {
|
||||||
return (
|
return (
|
||||||
<div class="block w-full min-w-0 truncate">
|
<span class="block w-full min-w-0 truncate">
|
||||||
{original.bio}
|
{original.bio}
|
||||||
</div>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -415,7 +415,7 @@ function formatDifference(
|
|||||||
markerDeletion = '<span class="x-text-removed">{{text}}</span>'
|
markerDeletion = '<span class="x-text-removed">{{text}}</span>'
|
||||||
) {
|
) {
|
||||||
[oldString, newString] = [oldString, newString].map((s) =>
|
[oldString, newString] = [oldString, newString].map((s) =>
|
||||||
s
|
String(s ?? '')
|
||||||
.replaceAll(/&/g, '&')
|
.replaceAll(/&/g, '&')
|
||||||
.replaceAll(/</g, '<')
|
.replaceAll(/</g, '<')
|
||||||
.replaceAll(/>/g, '>')
|
.replaceAll(/>/g, '>')
|
||||||
|
|||||||
Reference in New Issue
Block a user