Refactor player handling: replace player arrays with IDs, implement PlayerSelector component
This commit is contained in:
@@ -20,7 +20,7 @@
|
||||
<script lang="ts">
|
||||
import {Input, Label, Select} from "flowbite-svelte";
|
||||
import TypeAheadSearch from "./TypeAheadSearch.svelte";
|
||||
import {gamemodes, groups, maps, players} from "@stores/stores.ts";
|
||||
import {gamemodes, groups, maps} from "@stores/stores.ts";
|
||||
import type {Team} from "@type/team.ts";
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -22,17 +22,33 @@
|
||||
|
||||
import {Button, Card, Checkbox, Input, Label, Navbar, NavBrand, Radio, Spinner} from "flowbite-svelte";
|
||||
import {ArrowLeftOutline} from "flowbite-svelte-icons";
|
||||
import {players} from "@stores/stores.ts";
|
||||
import {capitalize} from "../util.ts";
|
||||
import {permsRepo} from "@repo/perms.ts";
|
||||
import {me} from "@stores/me.ts";
|
||||
import SWButton from "@components/styled/SWButton.svelte";
|
||||
import SWModal from "@components/styled/SWModal.svelte";
|
||||
import {userRepo} from "@repo/user.ts";
|
||||
import {dataRepo} from "@repo/data.ts";
|
||||
import type {Player} from "@type/data";
|
||||
|
||||
let search = $state("");
|
||||
let playersList: Player[] = $state([]);
|
||||
let debounceTimer: NodeJS.Timeout;
|
||||
|
||||
function fetchPlayers(searchTerm: string) {
|
||||
clearTimeout(debounceTimer);
|
||||
debounceTimer = setTimeout(async () => {
|
||||
const res = await $dataRepo.queryPlayers(searchTerm || undefined, undefined, undefined, 100, 0, undefined, undefined);
|
||||
playersList = res.players;
|
||||
}, 300);
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
fetchPlayers(search);
|
||||
});
|
||||
|
||||
let selectedPlayer: string | null = $state(null);
|
||||
let selectedPlayerName: string = $state("");
|
||||
let playerPerms = $state(loadPlayer(selectedPlayer));
|
||||
|
||||
let prefixEdit = $state("PREFIX_NONE");
|
||||
@@ -99,9 +115,7 @@
|
||||
resetPasswordRepeat = "";
|
||||
resetPasswordModal = false;
|
||||
}
|
||||
let lowerCaseSearch = $derived(search.toLowerCase());
|
||||
let filteredPlayers = $derived($players.filter(value => value.name.toLowerCase().includes(lowerCaseSearch)));
|
||||
let player = $derived($players.find(value => value.uuid === selectedPlayer));
|
||||
|
||||
run(() => {
|
||||
playerPerms = loadPlayer(selectedPlayer);
|
||||
});
|
||||
@@ -126,12 +140,15 @@
|
||||
<Label for="user_search" class="mb-2">Search Users...</Label>
|
||||
<Input type="text" id="user_search" placeholder="Name..." bind:value={search}/>
|
||||
</div>
|
||||
{#if filteredPlayers.length < 100}
|
||||
{#if playersList.length < 100}
|
||||
<ul class="flex-1 overflow-scroll">
|
||||
{#each filteredPlayers as player (player.uuid)}
|
||||
{#each playersList as player (player.uuid)}
|
||||
<li class="p-4 transition-colors hover:bg-gray-700 cursor-pointer"
|
||||
class:text-orange-500={player.uuid === selectedPlayer}
|
||||
onclick={preventDefault(() => selectedPlayer = player.uuid)}>
|
||||
onclick={preventDefault(() => {
|
||||
selectedPlayer = player.uuid;
|
||||
selectedPlayerName = player.name;
|
||||
})}>
|
||||
{player.name}
|
||||
</li>
|
||||
{/each}
|
||||
@@ -140,7 +157,7 @@
|
||||
</Card>
|
||||
<Card class="!max-w-full" style="grid-column: 2/4">
|
||||
{#if selectedPlayer}
|
||||
<h1 class="text-3xl">{player.name}</h1>
|
||||
<h1 class="text-3xl">{selectedPlayerName}</h1>
|
||||
{#await permsFuture}
|
||||
<Spinner></Spinner>
|
||||
{:then perms}
|
||||
|
||||
@@ -36,8 +36,9 @@
|
||||
} from "flowbite-svelte-icons";
|
||||
import FightCard from "./FightCard.svelte";
|
||||
import CreateFightModal from "./modals/CreateFightModal.svelte";
|
||||
import {groups, players} from "@stores/stores.ts";
|
||||
import {groups} from "@stores/stores.ts";
|
||||
import TypeAheadSearch from "../../components/TypeAheadSearch.svelte";
|
||||
import PlayerSelector from "@components/ui/PlayerSelector.svelte";
|
||||
import {fightRepo, type UpdateFight} from "@repo/fight.ts";
|
||||
import dayjs from "dayjs";
|
||||
import duration from "dayjs/plugin/duration";
|
||||
@@ -97,12 +98,6 @@
|
||||
}
|
||||
|
||||
let spectatePortOpen = $state(false);
|
||||
let selectPlayers = $derived($players.map(player => {
|
||||
return {
|
||||
name: player.name,
|
||||
value: player.uuid
|
||||
};
|
||||
}).sort((a, b) => a.name.localeCompare(b.name)));
|
||||
let spectatePort = $state("");
|
||||
|
||||
async function updateSpectatePort() {
|
||||
@@ -262,12 +257,12 @@
|
||||
<TypeAheadSearch items={selectPlayers} bind:selected={spectatePort}></TypeAheadSearch>
|
||||
</div>
|
||||
{#snippet footer()}
|
||||
|
||||
<Button class="ml-auto" onclick={updateSpectatePort}>Change</Button>
|
||||
<Button onclick={() => spectatePortOpen = false} color="alternative">Cancel</Button>
|
||||
|
||||
{/snippet}
|
||||
</Modal>
|
||||
<Modal bind:open={spectatePortOpen} title="Change Kampfleiter" size="sm">
|
||||
<div class="m-2">
|
||||
<Label for="fight-kampf">Kampfleiter</Label>
|
||||
<PlayerSelector bind:value={spectatePort} placeholder="Search player..." />
|
||||
</div>
|
||||
{#snippet footer()}
|
||||
|
||||
<Modal bind:open={groupChangeOpen} title="Change Group" size="sm">
|
||||
<div class="m-2">
|
||||
|
||||
@@ -23,8 +23,7 @@
|
||||
import {PlusOutline} from "flowbite-svelte-icons";
|
||||
import SWModal from "@components/styled/SWModal.svelte";
|
||||
import SWButton from "@components/styled/SWButton.svelte";
|
||||
import TypeAheadSearch from "@components/admin/components/TypeAheadSearch.svelte";
|
||||
import {players} from "@stores/stores.ts";
|
||||
import PlayerSelector from "@components/ui/PlayerSelector.svelte";
|
||||
import {eventRepo} from "@repo/event.ts";
|
||||
|
||||
interface Props {
|
||||
@@ -102,8 +101,7 @@
|
||||
<SWModal title="Schiedsrichter hinzufügen" bind:open={showAdd}>
|
||||
<div class="flex flex-grow justify-center h-80">
|
||||
<div>
|
||||
<TypeAheadSearch bind:searchValue bind:selected={selectedPlayer}
|
||||
items={$players.map(v => ({ name: v.name, value: v.uuid }))}/>
|
||||
<PlayerSelector bind:value={selectedPlayer} placeholder="Search player..." />
|
||||
</div>
|
||||
</div>
|
||||
{#snippet footer()}
|
||||
|
||||
@@ -19,12 +19,10 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { Table, TableBody, TableCell, TableCaption, TableHead, TableHeader, TableRow } from "@components/ui/table";
|
||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@components/ui/command/index.js";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@components/ui/popover/index.js";
|
||||
import { Button } from "@components/ui/button/index.js";
|
||||
import type { ExtendedEvent } from "@type/event.ts";
|
||||
import { eventRepo } from "@repo/event";
|
||||
import { players } from "@stores/stores";
|
||||
import PlayerSelector from "@components/ui/PlayerSelector.svelte";
|
||||
|
||||
const { event }: { event: ExtendedEvent } = $props();
|
||||
|
||||
@@ -39,8 +37,6 @@
|
||||
await $eventRepo.deleteReferees(event.event.id.toString(), [value]);
|
||||
referees = await $eventRepo.listReferees(event.event.id.toString());
|
||||
}
|
||||
|
||||
let playerSearch = $state("");
|
||||
</script>
|
||||
|
||||
<Table>
|
||||
@@ -60,27 +56,7 @@
|
||||
</TableRow>
|
||||
{/each}
|
||||
</TableBody>
|
||||
<Popover>
|
||||
<TableCaption>
|
||||
<PopoverTrigger>
|
||||
<Button>Hinzufügen</Button>
|
||||
</PopoverTrigger>
|
||||
<PlayerSelector placeholder="Hinzufügen" onSelect={(player) => addReferee(player.uuid)} />
|
||||
</TableCaption>
|
||||
<PopoverContent class="p-0">
|
||||
<Command shouldFilter={false}>
|
||||
<CommandInput bind:value={playerSearch} placeholder="Search players..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No Players found :(</CommandEmpty>
|
||||
<CommandGroup heading="Players">
|
||||
{#each $players
|
||||
.filter((v) => v.name.toLowerCase().includes(playerSearch.toLowerCase()))
|
||||
.filter((v, i) => i < 50)
|
||||
.filter((v) => !referees.some((k) => k.uuid === v.uuid)) as player (player.uuid)}
|
||||
<CommandItem value={player.name} onSelect={() => addReferee(player.uuid)} keywords={[player.uuid]}>{player.name}</CommandItem>
|
||||
{/each}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</Table>
|
||||
|
||||
@@ -11,10 +11,10 @@
|
||||
import { Input } from "@components/ui/input";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@components/ui/popover";
|
||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@components/ui/command";
|
||||
import { players } from "@components/stores/stores";
|
||||
import { Check } from "lucide-svelte";
|
||||
import { cn } from "@components/utils";
|
||||
import DateTimePicker from "@components/ui/datetime-picker/DateTimePicker.svelte";
|
||||
import PlayerSelector from "@components/ui/PlayerSelector.svelte";
|
||||
|
||||
let debounceTimer: NodeJS.Timeout;
|
||||
const debounce = <T,>(value: T, func: (value: T) => void) => {
|
||||
@@ -27,11 +27,11 @@
|
||||
let actionText = $state("");
|
||||
let serverText = $state("");
|
||||
let fullText = $state("");
|
||||
let actors = $state<string[]>([]);
|
||||
let actors = $state<number[]>([]);
|
||||
let actionTypes = $state<string[]>([]);
|
||||
let timeGreater = $state<ZonedDateTime>(now("Europe/Berlin").subtract({ months: 1 }));
|
||||
let timeLess = $state<ZonedDateTime>(now("Europe/Berlin"));
|
||||
let serverOwner = $state<string[]>([]);
|
||||
let serverOwner = $state<number[]>([]);
|
||||
let velocity = $state(false);
|
||||
let sorting = $state("DESC");
|
||||
|
||||
@@ -118,56 +118,12 @@
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Popover>
|
||||
<PopoverTrigger>
|
||||
<Button variant="outline" class="mr-2 {actors && 'text-muted-foreground'}">Spieler Filter ({actors.length})</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="p-0">
|
||||
<Command shouldFilter={false}>
|
||||
<CommandInput bind:value={playerSearch} placeholder="Search players..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No Players found :(</CommandEmpty>
|
||||
<CommandGroup heading="Players">
|
||||
{#each $players.filter((v) => v.name.toLowerCase().includes(playerSearch.toLowerCase())).filter((v, i) => i < 50) as player (player.uuid)}
|
||||
<CommandItem
|
||||
value={player.name}
|
||||
onSelect={() => (actors = actors.includes(player.uuid) ? actors.filter((v) => v !== player.uuid) : [...actors, player.uuid])}
|
||||
keywords={[player.uuid]}
|
||||
>
|
||||
<Check class={cn("mr-2 size-4", !actors.includes(player.uuid) && "text-transparent")} />
|
||||
{player.name}
|
||||
</CommandItem>
|
||||
{/each}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<Popover>
|
||||
<PopoverTrigger>
|
||||
<Button variant="outline" class="mr-2 {serverOwner && 'text-muted-foreground'}">Server Owner ({serverOwner.length})</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="p-0">
|
||||
<Command shouldFilter={false}>
|
||||
<CommandInput bind:value={ownerSearch} placeholder="Search players..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No Players found :(</CommandEmpty>
|
||||
<CommandGroup heading="Players">
|
||||
{#each $players.filter((v) => v.name.toLowerCase().includes(ownerSearch.toLowerCase())).filter((v, i) => i < 50) as player (player.uuid)}
|
||||
<CommandItem
|
||||
value={player.name}
|
||||
onSelect={() => (serverOwner = serverOwner.includes(player.uuid) ? serverOwner.filter((v) => v !== player.uuid) : [...serverOwner, player.uuid])}
|
||||
keywords={[player.uuid]}
|
||||
>
|
||||
<Check class={cn("mr-2 size-4", !serverOwner.includes(player.uuid) && "text-transparent")} />
|
||||
{player.name}
|
||||
</CommandItem>
|
||||
{/each}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<div class="mr-2">
|
||||
<PlayerSelector bind:value={actors} multiple placeholder="Spieler Filter" />
|
||||
</div>
|
||||
<div class="mr-2">
|
||||
<PlayerSelector bind:value={serverOwner} multiple placeholder="Server Owner" />
|
||||
</div>
|
||||
<div class="mr-2">
|
||||
<DateTimePicker bind:value={timeGreater} />
|
||||
</div>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import {permissions, players} from "@stores/stores.ts";
|
||||
import {permissions} from "@stores/stores.ts";
|
||||
import {Select, SelectContent, SelectItem} from "@components/ui/select";
|
||||
import {SelectTrigger} from "@components/ui/select/index.js";
|
||||
import {permsRepo} from "@repo/perms.ts";
|
||||
|
||||
@@ -17,16 +17,132 @@
|
||||
- along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<script>
|
||||
import Table from "@components/moderator/pages/players/Table.svelte";
|
||||
<script lang="ts">
|
||||
import { createSvelteTable, FlexRender } from "@components/ui/data-table";
|
||||
import { columns } from "./columns";
|
||||
import { getCoreRowModel, getPaginationRowModel, type PaginationState } from "@tanstack/table-core";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@components/ui/table";
|
||||
import { dataRepo } from "@repo/data";
|
||||
import type { Player } from "@type/data";
|
||||
import { Button } from "@components/ui/button";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger } from "@components/ui/select";
|
||||
import { Input } from "@components/ui/input";
|
||||
|
||||
import {dataRepo} from "@repo/data";
|
||||
let debounceTimer: NodeJS.Timeout;
|
||||
const debounce = <T,>(value: T, func: (value: T) => void) => {
|
||||
clearTimeout(debounceTimer);
|
||||
debounceTimer = setTimeout(() => {
|
||||
func(value);
|
||||
}, 300);
|
||||
};
|
||||
|
||||
let playersFuture = $state($dataRepo.getPlayers())
|
||||
let search = $state("");
|
||||
|
||||
let pagination = $state<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 25,
|
||||
});
|
||||
|
||||
let data = $state<Player[]>([]);
|
||||
let rows = $state(0);
|
||||
|
||||
$effect(() => {
|
||||
$dataRepo.queryPlayers(search || undefined, undefined, undefined, pagination.pageSize, pagination.pageIndex, true, true).then((res) => {
|
||||
data = res.entries;
|
||||
rows = res.rows;
|
||||
});
|
||||
});
|
||||
|
||||
const table = createSvelteTable({
|
||||
get data() {
|
||||
return data;
|
||||
},
|
||||
columns,
|
||||
state: {
|
||||
get pagination() {
|
||||
return pagination;
|
||||
},
|
||||
},
|
||||
onPaginationChange: (updater) => {
|
||||
if (typeof updater === "function") {
|
||||
pagination = updater(pagination);
|
||||
} else {
|
||||
pagination = updater;
|
||||
}
|
||||
},
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
manualPagination: true,
|
||||
get rowCount() {
|
||||
return rows;
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
{#await playersFuture}
|
||||
<p>Loading...</p>
|
||||
{:then players}
|
||||
<Table data={players} />
|
||||
{/await}
|
||||
<div class="p-4">
|
||||
<div class="rounded border mb-4 p-2 flex lg:flex-row flex-col">
|
||||
<Input
|
||||
class="w-48 mr-2"
|
||||
placeholder="Search players..."
|
||||
value={search}
|
||||
onchange={(e) =>
|
||||
debounce(e.currentTarget.value, (v) => {
|
||||
search = v;
|
||||
})}
|
||||
oninput={(e) =>
|
||||
debounce(e.currentTarget.value, (v) => {
|
||||
search = v;
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div class="rounded border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
|
||||
<TableRow>
|
||||
{#each headerGroup.headers as header (header.id)}
|
||||
<TableHead colspan={header.colSpan}>
|
||||
{#if !header.isPlaceholder}
|
||||
<FlexRender content={header.column.columnDef.header} context={header.getContext()} />
|
||||
{/if}
|
||||
</TableHead>
|
||||
{/each}
|
||||
</TableRow>
|
||||
{/each}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{#each table.getRowModel().rows as row (row.id)}
|
||||
<TableRow>
|
||||
{#each row.getVisibleCells() as cell (cell.id)}
|
||||
<TableCell class="p-2 align-top">
|
||||
<FlexRender content={cell.column.columnDef.cell} context={cell.getContext()} />
|
||||
</TableCell>
|
||||
{/each}
|
||||
</TableRow>
|
||||
{:else}
|
||||
<TableRow>
|
||||
<TableCell colspan={columns.length} class="h-24 text-center">No players found.</TableCell>
|
||||
</TableRow>
|
||||
{/each}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end space-x-2 py-4">
|
||||
<div>
|
||||
<Select type="single" value={pagination.pageSize.toString()} onValueChange={(e) => (pagination = { pageSize: +e, pageIndex: 0 })}>
|
||||
<SelectTrigger>{pagination.pageSize}</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="5">5</SelectItem>
|
||||
<SelectItem value="10">10</SelectItem>
|
||||
<SelectItem value="25">25</SelectItem>
|
||||
<SelectItem value="50">50</SelectItem>
|
||||
<SelectItem value="100">100</SelectItem>
|
||||
<SelectItem value="200">200</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onclick={() => table.previousPage()} disabled={!table.getCanPreviousPage()}>Previous</Button>
|
||||
<Button variant="outline" size="sm" onclick={() => table.nextPage()} disabled={!table.getCanNextPage()}>Next</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,174 +0,0 @@
|
||||
<!--
|
||||
- This file is a part of the SteamWar software.
|
||||
-
|
||||
- Copyright (C) 2025 SteamWar.de-Serverteam
|
||||
-
|
||||
- This program is free software: you can redistribute it and/or modify
|
||||
- it under the terms of the GNU Affero General Public License as published by
|
||||
- the Free Software Foundation, either version 3 of the License, or
|
||||
- (at your option) any later version.
|
||||
-
|
||||
- This program is distributed in the hope that it will be useful,
|
||||
- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
- GNU Affero General Public License for more details.
|
||||
-
|
||||
- You should have received a copy of the GNU Affero General Public License
|
||||
- along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
type ColumnFiltersState,
|
||||
getCoreRowModel, getFilteredRowModel,
|
||||
getPaginationRowModel, getSortedRowModel,
|
||||
type PaginationState,
|
||||
type SortingState,
|
||||
} from "@tanstack/table-core";
|
||||
import {
|
||||
createSvelteTable,
|
||||
FlexRender,
|
||||
} from "@components/ui/data-table/index";
|
||||
import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from "@components/ui/table";
|
||||
import {Button} from "@components/ui/button";
|
||||
import {Input} from "@components/ui/input";
|
||||
import {Select} from "@components/ui/select";
|
||||
import {SelectContent, SelectItem, SelectTrigger} from "@components/ui/select/index.js";
|
||||
import type {Player} from "@type/data";
|
||||
import { columns } from "./columns";
|
||||
|
||||
let { data }: { data: Player[] } = $props();
|
||||
|
||||
let pagination = $state<PaginationState>({ pageIndex: 0, pageSize: 25 });
|
||||
let sorting = $state<SortingState>([]);
|
||||
let columnFilters = $state<ColumnFiltersState>([]);
|
||||
|
||||
const table = createSvelteTable({
|
||||
get data() {
|
||||
return data;
|
||||
},
|
||||
state: {
|
||||
get pagination() {
|
||||
return pagination;
|
||||
},
|
||||
get sorting() {
|
||||
return sorting;
|
||||
},
|
||||
get columnFilters() {
|
||||
return columnFilters;
|
||||
},
|
||||
},
|
||||
onPaginationChange: (updater) => {
|
||||
if (typeof updater === "function") {
|
||||
pagination = updater(pagination);
|
||||
} else {
|
||||
pagination = updater;
|
||||
}
|
||||
},
|
||||
onSortingChange: (updater) => {
|
||||
if (typeof updater === "function") {
|
||||
sorting = updater(sorting);
|
||||
} else {
|
||||
sorting = updater;
|
||||
}
|
||||
},
|
||||
onColumnFiltersChange: (updater) => {
|
||||
if (typeof updater === "function") {
|
||||
columnFilters = updater(columnFilters);
|
||||
} else {
|
||||
columnFilters = updater;
|
||||
}
|
||||
},
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="rounded-md border m-4">
|
||||
<div class="flex items-center p-4 border-b">
|
||||
<Input
|
||||
placeholder="Filter Players..."
|
||||
value={(table.getColumn("name")?.getFilterValue() as string) ?? ""}
|
||||
onchange={(e) => {
|
||||
table.getColumn("name")?.setFilterValue(e.currentTarget.value);
|
||||
}}
|
||||
oninput={(e) => {
|
||||
table.getColumn("name")?.setFilterValue(e.currentTarget.value);
|
||||
}}
|
||||
class="max-w-sm"
|
||||
/>
|
||||
<div class="flex items-center px-4">
|
||||
<Select type="single" value={pagination.pageSize.toString()} onValueChange={(e) => pagination = { pageSize: +e, pageIndex: 0 }}>
|
||||
<SelectTrigger>{pagination.pageSize}</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="5">5</SelectItem>
|
||||
<SelectItem value="10">10</SelectItem>
|
||||
<SelectItem value="25">25</SelectItem>
|
||||
<SelectItem value="50">50</SelectItem>
|
||||
<SelectItem value="100">100</SelectItem>
|
||||
<SelectItem value="200">200</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
|
||||
<TableRow>
|
||||
{#each headerGroup.headers as header (header.id)}
|
||||
<TableHead>
|
||||
{#if !header.isPlaceholder}
|
||||
<FlexRender
|
||||
content={header.column.columnDef.header}
|
||||
context={header.getContext()}
|
||||
/>
|
||||
{/if}
|
||||
</TableHead>
|
||||
{/each}
|
||||
</TableRow>
|
||||
{/each}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{#each table.getRowModel().rows as row (row.id)}
|
||||
<TableRow data-state={row.getIsSelected() && "selected"}>
|
||||
{#each row.getVisibleCells() as cell (cell.id)}
|
||||
<TableCell>
|
||||
<FlexRender
|
||||
content={cell.column.columnDef.cell}
|
||||
context={cell.getContext()}
|
||||
/>
|
||||
</TableCell>
|
||||
{/each}
|
||||
</TableRow>
|
||||
{:else}
|
||||
<TableRow>
|
||||
<TableCell colspan={columns.length} class="h-24 text-center">
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/each}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div class="flex items-center justify-end space-x-2 p-4 border-t">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() => table.previousPage()}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
Previous
|
||||
</Button>
|
||||
<span>{pagination.pageIndex + 1}/{table.getPageCount()}</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() => table.nextPage()}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -7,11 +7,11 @@ export class AuditLogRepo {
|
||||
actionText: string | undefined,
|
||||
serverText: string | undefined,
|
||||
fullText: string | undefined,
|
||||
actor: string[] | undefined,
|
||||
actor: number[] | undefined,
|
||||
actionType: string[] | undefined,
|
||||
timeFrom: number | undefined,
|
||||
timeTo: number | undefined,
|
||||
serverOwner: string[] | undefined,
|
||||
serverOwner: number[] | undefined,
|
||||
velocity: boolean | undefined,
|
||||
page: number,
|
||||
pageSize: number,
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type { Player, Server } from "@type/data.ts";
|
||||
import { PlayerSchema, ServerSchema } from "@type/data.ts";
|
||||
import type { Player, PlayerList, Server } from "@type/data.ts";
|
||||
import { PlayerListSchema, PlayerSchema, ServerSchema } from "@type/data.ts";
|
||||
import { fetchWithToken, tokenStore } from "./repo.ts";
|
||||
import { derived, get } from "svelte/store";
|
||||
import { TeamSchema, type Team } from "@components/types/team.ts";
|
||||
@@ -38,10 +38,28 @@ export class DataRepo {
|
||||
.then(PlayerSchema.parse);
|
||||
}
|
||||
|
||||
public async getPlayers(): Promise<Player[]> {
|
||||
return await fetchWithToken(get(tokenStore), "/data/admin/users")
|
||||
public async queryPlayers(
|
||||
name: string | undefined,
|
||||
uuid: string | undefined,
|
||||
team: number[] | undefined,
|
||||
limit: number | undefined,
|
||||
page: number | undefined,
|
||||
includePerms: boolean | undefined,
|
||||
includeId: boolean | undefined
|
||||
): Promise<PlayerList> {
|
||||
let query = new URLSearchParams();
|
||||
|
||||
if (name) query.append("name", name);
|
||||
if (uuid) query.append("uuid", uuid);
|
||||
if (team) team.forEach((t) => query.append("team", t.toString()));
|
||||
if (limit) query.append("limit", limit.toString());
|
||||
if (page) query.append("page", page.toString());
|
||||
if (includePerms !== undefined) query.append("includePerms", includePerms.toString());
|
||||
if (includeId !== undefined) query.append("includeId", includeId.toString());
|
||||
|
||||
return await fetchWithToken(this.token, "/data/admin/users?" + query.toString())
|
||||
.then((value) => value.json())
|
||||
.then(PlayerSchema.array().parse);
|
||||
.then(PlayerListSchema.parse);
|
||||
}
|
||||
|
||||
public async getTeams(): Promise<Team[]> {
|
||||
|
||||
@@ -31,10 +31,6 @@ import { permsRepo } from "@repo/perms.ts";
|
||||
|
||||
export const schemTypes = cached<SchematicType[]>([], () => fetchWithToken(get(tokenStore), "/data/admin/schematicTypes").then((res) => res.json()));
|
||||
|
||||
export const players = cached<Player[]>([], async () => {
|
||||
return get(dataRepo).getPlayers();
|
||||
});
|
||||
|
||||
export const teams = cached<Team[]>([], async () => {
|
||||
return get(dataRepo).getTeams();
|
||||
});
|
||||
|
||||
@@ -29,12 +29,20 @@ export type SchematicType = z.infer<typeof SchematicTypeSchema>;
|
||||
export const PlayerSchema = z.object({
|
||||
name: z.string(),
|
||||
uuid: z.string(),
|
||||
prefix: z.string(),
|
||||
perms: z.array(z.string()),
|
||||
prefix: z.string().nullable(),
|
||||
perms: z.array(z.string()).nullable(),
|
||||
id: z.number().nullable(),
|
||||
});
|
||||
|
||||
export type Player = z.infer<typeof PlayerSchema>;
|
||||
|
||||
export const PlayerListSchema = z.object({
|
||||
entries: z.array(PlayerSchema),
|
||||
rows: z.number(),
|
||||
});
|
||||
|
||||
export type PlayerList = z.infer<typeof PlayerListSchema>;
|
||||
|
||||
export const ServerSchema = z.object({
|
||||
description: z.any(),
|
||||
players: z.object({
|
||||
|
||||
122
src/components/ui/PlayerSelector.svelte
Normal file
122
src/components/ui/PlayerSelector.svelte
Normal file
@@ -0,0 +1,122 @@
|
||||
<script lang="ts">
|
||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@components/ui/command";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@components/ui/popover";
|
||||
import { Button } from "@components/ui/button";
|
||||
import { Check, ChevronsUpDown } from "lucide-svelte";
|
||||
import { cn } from "@components/utils";
|
||||
import { dataRepo } from "@repo/data";
|
||||
import type { Player } from "@type/data";
|
||||
|
||||
let {
|
||||
value = $bindable(null),
|
||||
multiple = false,
|
||||
placeholder = "Select player...",
|
||||
onSelect,
|
||||
}: {
|
||||
value?: number | number[] | null;
|
||||
multiple?: boolean;
|
||||
placeholder?: string;
|
||||
onSelect?: (player: Player) => void;
|
||||
} = $props();
|
||||
|
||||
let open = $state(false);
|
||||
let search = $state("");
|
||||
let players: Player[] = $state([]);
|
||||
let loading = $state(false);
|
||||
|
||||
let debounceTimer: NodeJS.Timeout;
|
||||
|
||||
function fetchPlayers(searchTerm: string) {
|
||||
clearTimeout(debounceTimer);
|
||||
debounceTimer = setTimeout(async () => {
|
||||
loading = true;
|
||||
try {
|
||||
const res = await $dataRepo.queryPlayers(searchTerm || undefined, undefined, undefined, 50, 0, false, true);
|
||||
players = res.entries;
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
fetchPlayers(search);
|
||||
});
|
||||
|
||||
function handleSelect(player: Player) {
|
||||
if (onSelect) {
|
||||
onSelect(player);
|
||||
}
|
||||
|
||||
if (multiple) {
|
||||
if (Array.isArray(value)) {
|
||||
if (value.includes(player.id!)) {
|
||||
value = value.filter((v) => v !== player.id);
|
||||
} else {
|
||||
value = [...value, player.id!];
|
||||
}
|
||||
} else {
|
||||
value = [player.id!];
|
||||
}
|
||||
} else {
|
||||
if (value === player.id) {
|
||||
value = null; // Deselect
|
||||
} else {
|
||||
value = player.id;
|
||||
open = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isSelected(id: number) {
|
||||
if (multiple) {
|
||||
return Array.isArray(value) && value.includes(id);
|
||||
}
|
||||
return value === id;
|
||||
}
|
||||
|
||||
let triggerLabel = $derived.by(() => {
|
||||
if (multiple) {
|
||||
if (Array.isArray(value) && value.length > 0) {
|
||||
return `${placeholder} (${value.length})`;
|
||||
}
|
||||
return placeholder;
|
||||
} else {
|
||||
// We might need to fetch the selected player's name if it's not in the current list
|
||||
// For now, let's just show the placeholder or "Selected"
|
||||
// Ideally we would have a way to resolve the name from the UUID if it's not in `players`
|
||||
// But `players` only contains search results.
|
||||
// If we want to show the name, we might need to fetch it or pass it in.
|
||||
// Given the context of AuditLog, it shows "Spieler Filter (count)".
|
||||
// Given RefereesList, it's a button "Hinzufügen".
|
||||
return placeholder;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<Popover bind:open>
|
||||
<PopoverTrigger>
|
||||
{#snippet child({ props })}
|
||||
<Button variant="outline" class={cn("justify-between", Array.isArray(value) && !value?.length && "text-muted-foreground")} {...props} role="combobox" aria-expanded={open}>
|
||||
{triggerLabel}
|
||||
<ChevronsUpDown class="ml-2 size-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
{/snippet}
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="p-0">
|
||||
<Command shouldFilter={false}>
|
||||
<CommandInput bind:value={search} placeholder="Search players..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No players found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{#each players as player (player.uuid)}
|
||||
<CommandItem value={player.id?.toString()} onSelect={() => handleSelect(player)}>
|
||||
<Check class={cn("mr-2 size-4", isSelected(player.id!) ? "opacity-100" : "opacity-0")} />
|
||||
{player.name}
|
||||
</CommandItem>
|
||||
{/each}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
Reference in New Issue
Block a user