This commit is contained in:
pa
2026-03-10 23:09:04 +09:00
parent 607e09d271
commit 1c9e4621f5
10 changed files with 216 additions and 49 deletions

View File

@@ -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;

View File

@@ -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)

View File

@@ -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"

View File

@@ -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>

View File

@@ -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,

View File

@@ -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 };

View File

@@ -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' }
);

View File

@@ -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>

View 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>');
});
});

View File

@@ -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, '&amp;')
.replaceAll(/</g, '&lt;')
.replaceAll(/>/g, '&gt;')