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
@@ -80,12 +80,11 @@ 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: () => ({
global: { t: (key) => key , locale: require('vue').ref('en') }, global: { t: (key) => key, locale: require('vue').ref('en') },
install: vi.fn() install: vi.fn()
}) })
})); }));
@@ -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,18 +84,16 @@ 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' : item.id === ''
: item.id === '' ? 'usersWithNoRole'
? 'usersWithNoRole' : `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)
+12 -1
View File
@@ -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>
+3 -4
View File
@@ -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,
+20 -13
View File
@@ -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 FILTER_EVERYONE = {
name: 'dialog.group.members.filters.everyone',
id: null
}; };
const groupDialogFilterOptions = { const FILTER_NO_ROLE = {
everyone: { name: 'dialog.group.members.filters.users_with_no_role',
name: 'dialog.group.members.filters.everyone', id: ''
id: null };
},
usersWithNoRole: { const groupDialogFilterOptions = [FILTER_EVERYONE, FILTER_NO_ROLE];
name: 'dialog.group.members.filters.users_with_no_role',
id: '' export {
} groupDialogSortingOptions,
groupDialogFilterOptions,
FILTER_EVERYONE,
FILTER_NO_ROLE
}; };
export { groupDialogSortingOptions, groupDialogFilterOptions };
+4 -1
View File
@@ -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' }
); );
+17
View File
@@ -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>
+121
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>');
});
});
+3 -3
View File
@@ -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, '&amp;') .replaceAll(/&/g, '&amp;')
.replaceAll(/</g, '&lt;') .replaceAll(/</g, '&lt;')
.replaceAll(/>/g, '&gt;') .replaceAll(/>/g, '&gt;')