add search for userdialog tabs (#476)

This commit is contained in:
pa
2026-03-16 13:18:06 +09:00
parent ed1db05d94
commit 03bb1b5410
5 changed files with 108 additions and 9 deletions

View File

@@ -27,8 +27,8 @@
}}</span>
</div>
<div class="flex items-center">
<Input v-model="avatarSearchQuery" class="h-8 w-40 mr-2" placeholder="Search avatars" @click.stop />
<template v-if="userDialog.ref.id === currentUser.id">
<Input v-model="avatarSearchQuery" class="h-8 w-40 mr-2" placeholder="Search avatars" @click.stop />
<span class="mr-1">{{ t('dialog.user.avatars.sort_by') }}</span>
<Select
:model-value="userDialog.avatarSorting"
@@ -136,9 +136,6 @@
const avatarSearchQuery = ref('');
const filteredUserDialogAvatars = computed(() => {
const avatars = userDialogAvatars.value;
if (userDialog.value.ref?.id !== currentUser.value.id) {
return avatars;
}
const query = avatarSearchQuery.value.trim().toLowerCase();
if (!query) {
return avatars;

View File

@@ -14,7 +14,33 @@
<DeprecationAlert
v-if="userDialog.ref.id === currentUser.id"
:feature-name="t('nav_tooltip.favorite_worlds')" />
<Input v-model="searchQuery" class="h-8 w-40 mt-2" placeholder="Search worlds" @click.stop />
<template v-if="searchActive">
<div
class="flex flex-wrap items-start"
style="margin-top: 8px; min-height: 60px; max-height: 50vh; overflow: auto">
<div
v-for="world in allFilteredFavoriteWorlds"
:key="world.favoriteId"
class="box-border flex items-center p-1.5 text-[13px] cursor-pointer w-[167px] hover:rounded-[25px_5px_5px_25px]"
@click="showWorldDialog(world.id)">
<div class="relative inline-block flex-none size-9 mr-2.5">
<Avatar class="size-9">
<AvatarImage :src="world.thumbnailImageUrl" class="object-cover" />
<AvatarFallback>
<Image class="size-4 text-muted-foreground" />
</AvatarFallback>
</Avatar>
</div>
<div class="flex-1 overflow-hidden">
<span class="block truncate font-medium leading-[18px]" v-text="world.name"></span>
<span v-if="world.occupants" class="block truncate text-xs">({{ world.occupants }})</span>
</div>
</div>
</div>
</template>
<TabsUnderline
v-else
v-model="favoriteWorldsTab"
:items="favoriteWorldTabs"
:unmount-on-hide="false"
@@ -69,8 +95,9 @@
</template>
<script setup>
import { computed, ref } from 'vue';
import { computed, ref, watch } from 'vue';
import { Image } from 'lucide-vue-next';
import { Input } from '@/components/ui/input';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { DataTableEmpty } from '@/components/ui/data-table';
import { TabsUnderline } from '@/components/ui/tabs';
@@ -99,6 +126,17 @@
}))
);
const searchQuery = ref('');
const searchActive = computed(() => searchQuery.value.trim().length > 0);
const allFilteredFavoriteWorlds = computed(() => {
const query = searchQuery.value.trim().toLowerCase();
if (!query) return [];
const lists = userDialog.value.userFavoriteWorlds || [];
const all = lists.flatMap((list) => list[2] || []);
return all.filter((w) => (w.name || '').toLowerCase().includes(query));
});
watch(() => userDialog.value.id, () => { searchQuery.value = ''; });
/**
*
* @param visibility

View File

@@ -19,6 +19,7 @@
</div>
<div style="display: flex; align-items: center">
<template v-if="!userDialogGroupEditMode">
<Input v-model="groupSearchQuery" class="h-8 w-40 mr-2" placeholder="Search groups" @click.stop />
<span style="margin-right: 6px">{{ t('dialog.user.groups.sort_by') }}</span>
<Select
:model-value="userDialogGroupSortingKey"
@@ -236,6 +237,36 @@
</div>
</div>
</template>
<template v-else-if="groupSearchActive">
<div class="flex flex-wrap items-start" style="margin-top: 8px; min-height: 60px">
<div
v-for="group in allFilteredGroups"
:key="group.id"
class="box-border flex items-center p-1.5 text-[13px] cursor-pointer w-[167px] hover:rounded-[25px_5px_5px_25px]"
@click="showGroupDialog(group.id)">
<div class="relative inline-block flex-none size-9 mr-2.5">
<Avatar class="size-9">
<AvatarImage :src="group.iconUrl" class="object-cover" />
<AvatarFallback>
<Users class="size-4 text-muted-foreground" />
</AvatarFallback>
</Avatar>
</div>
<div class="flex-1 overflow-hidden">
<span class="block truncate font-medium leading-[18px]" v-text="group.name"></span>
<div class="block truncate text-xs inline-flex! items-center">
<TooltipWrapper
v-if="group.isRepresenting"
side="top"
:content="t('dialog.group.members.representing')">
<Tag style="margin-right: 6px" />
</TooltipWrapper>
<span>({{ group.memberCount }})</span>
</div>
</div>
</div>
</div>
</template>
<template v-else>
<template v-if="userDialog.userGroups.ownGroups.length > 0">
<span class="text-base font-bold">{{ t('dialog.user.groups.own_groups') }}</span>
@@ -384,9 +415,10 @@
import { ArrowDown, ArrowUp, DownloadIcon, Eye, LogOut, RefreshCw, Tag, Users } from 'lucide-vue-next';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { nextTick, ref } from 'vue';
import { computed, nextTick, ref, watch } from 'vue';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import { Input } from '@/components/ui/input';
import { Spinner } from '@/components/ui/spinner';
import { storeToRefs } from 'pinia';
import { toast } from 'vue-sonner';
@@ -426,6 +458,15 @@
setUserDialogGroupSorting
);
const groupSearchQuery = ref('');
const groupSearchActive = computed(() => groupSearchQuery.value.trim().length > 0);
const allFilteredGroups = computed(() => {
const query = groupSearchQuery.value.trim().toLowerCase();
if (!query) return [];
return userDialog.value.userGroups.groups.filter((g) => (g.name || '').toLowerCase().includes(query));
});
watch(() => userDialog.value.id, () => { groupSearchQuery.value = ''; });
/**
*
* @param sortOrder

View File

@@ -15,6 +15,7 @@
}}</span>
</div>
<div style="display: flex; align-items: center">
<Input v-model="searchQuery" class="h-8 w-40 mr-2" placeholder="Search friends" @click.stop />
<span style="margin-right: 6px">{{ t('dialog.user.groups.sort_by') }}</span>
<Select
:model-value="userDialogMutualFriendSortingKey"
@@ -36,7 +37,7 @@
</div>
<ul class="flex flex-wrap items-start" style="margin-top: 8px; overflow: auto; max-height: 250px; min-width: 130px">
<li
v-for="user in userDialog.mutualFriends"
v-for="user in filteredMutualFriends"
:key="user.id"
class="box-border flex items-center p-1.5 text-[13px] cursor-pointer w-[167px] hover:rounded-[25px_5px_5px_25px]"
@click="showUserDialog(user.id)">
@@ -64,6 +65,8 @@
import { Button } from '@/components/ui/button';
import { RefreshCw, User } from 'lucide-vue-next';
import { Spinner } from '@/components/ui/spinner';
import { Input } from '@/components/ui/input';
import { computed, ref, watch } from 'vue';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
@@ -89,6 +92,15 @@
setUserDialogMutualFriendSorting
);
const searchQuery = ref('');
const filteredMutualFriends = computed(() => {
const friends = userDialog.value.mutualFriends;
const query = searchQuery.value.trim().toLowerCase();
if (!query) return friends;
return friends.filter((u) => (u.displayName || '').toLowerCase().includes(query));
});
watch(() => userDialog.value.id, () => { searchQuery.value = ''; });
/**
*
* @param sortOrder

View File

@@ -15,6 +15,7 @@
}}</span>
</div>
<div style="display: flex; align-items: center">
<Input v-model="searchQuery" class="h-8 w-40 mr-2" placeholder="Search worlds" @click.stop />
<span class="mr-1">{{ t('dialog.user.worlds.sort_by') }}</span>
<Select
:model-value="userDialogWorldSortingKey"
@@ -54,7 +55,7 @@
<div class="flex flex-wrap items-start" style="margin-top: 8px; min-height: 60px">
<template v-if="userDialog.worlds.length">
<div
v-for="world in userDialog.worlds"
v-for="world in filteredWorlds"
:key="world.id"
class="box-border flex items-center p-1.5 text-[13px] cursor-pointer w-[167px] hover:rounded-[25px_5px_5px_25px]"
@click="showWorldDialog(world.id)">
@@ -87,7 +88,8 @@
import { Image, RefreshCw } from 'lucide-vue-next';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Spinner } from '@/components/ui/spinner';
import { ref } from 'vue';
import { Input } from '@/components/ui/input';
import { computed, ref, watch } from 'vue';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
@@ -105,6 +107,15 @@
const userDialogWorldsRequestId = ref(0);
const searchQuery = ref('');
const filteredWorlds = computed(() => {
const worlds = userDialog.value.worlds;
const query = searchQuery.value.trim().toLowerCase();
if (!query) return worlds;
return worlds.filter((w) => (w.name || '').toLowerCase().includes(query));
});
watch(() => userDialog.value.id, () => { searchQuery.value = ''; });
/**
*
* @param userId