mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-10-01 15:48:23 -05:00
Team map list (#2935)
This commit is contained in:
@@ -178,7 +178,7 @@ const basicSeeds = (variation?: SeedVariation | null) => [
|
||||
adminUserWeaponPool,
|
||||
adminUserWidgets,
|
||||
userProfiles,
|
||||
userMapModePreferences,
|
||||
variation === "TEAM_MAP_PREFS" ? undefined : userMapModePreferences,
|
||||
userQWeaponPool,
|
||||
seedingSkills,
|
||||
lastMonthsVoting,
|
||||
@@ -230,7 +230,7 @@ const basicSeeds = (variation?: SeedVariation | null) => [
|
||||
arts,
|
||||
commissionsOpen,
|
||||
playedMatches,
|
||||
variation === "NO_SQ_GROUPS" ? undefined : groups,
|
||||
variation === "NO_SQ_GROUPS" ? undefined : () => groups(variation),
|
||||
friendCodes,
|
||||
lfgPosts,
|
||||
variation === "NO_SCRIMS" ? undefined : scrimPosts,
|
||||
@@ -2099,6 +2099,20 @@ const detailedTeam = (seedVariation?: SeedVariation | null) => () => {
|
||||
)
|
||||
.run();
|
||||
}
|
||||
|
||||
const teamPreferences: UserMapModePreferences = {
|
||||
modes: modesShort.map((mode) => ({ mode, preference: "PREFER" as const })),
|
||||
pool: modesShort.map((mode) => ({
|
||||
mode,
|
||||
stages: [...SENDOUQ_DEFAULT_MAPS[mode]],
|
||||
})),
|
||||
};
|
||||
|
||||
sql
|
||||
.prepare(
|
||||
/*sql*/ `update "AllTeam" set "mapModePreferences" = ? where "id" = 1`,
|
||||
)
|
||||
.run(JSON.stringify(teamPreferences));
|
||||
};
|
||||
|
||||
function otherTeams() {
|
||||
@@ -2456,7 +2470,11 @@ function commissionsOpen() {
|
||||
}
|
||||
|
||||
const SENDOU_IN_FULL_GROUP = true;
|
||||
async function groups() {
|
||||
async function groups(variation?: SeedVariation | null) {
|
||||
if (variation === "TEAM_MAP_PREFS") {
|
||||
return teamMapPrefsGroups();
|
||||
}
|
||||
|
||||
const users = userIdsInAscendingOrderById()
|
||||
.slice(0, 100)
|
||||
.filter((id) => id !== ADMIN_ID && id !== NZAP_TEST_ID);
|
||||
@@ -2498,6 +2516,65 @@ async function groups() {
|
||||
}
|
||||
}
|
||||
|
||||
async function teamMapPrefsGroups() {
|
||||
const arMemberIds = (
|
||||
sql
|
||||
.prepare(
|
||||
/*sql*/ `select "userId" from "AllTeamMember" where "teamId" = 1 and "leftAt" is null and "userId" != ?`,
|
||||
)
|
||||
.all(ADMIN_ID) as any[]
|
||||
).map((row: any) => row.userId as number);
|
||||
|
||||
const arMemberSet = new Set(arMemberIds);
|
||||
const users = userIdsInAscendingOrderById()
|
||||
.slice(0, 100)
|
||||
.filter(
|
||||
(id) => id !== ADMIN_ID && id !== NZAP_TEST_ID && !arMemberSet.has(id),
|
||||
);
|
||||
|
||||
const nzapGroup = await SQGroupRepository.createGroup({
|
||||
status: "ACTIVE",
|
||||
userId: NZAP_TEST_ID,
|
||||
});
|
||||
for (let j = 0; j < 3; j++) {
|
||||
sql
|
||||
.prepare(
|
||||
/* sql */ `
|
||||
insert into "GroupMember" ("groupId", "userId", "role")
|
||||
values (@groupId, @userId, @role)
|
||||
`,
|
||||
)
|
||||
.run({
|
||||
groupId: nzapGroup.id,
|
||||
userId: users.pop()!,
|
||||
role: "REGULAR",
|
||||
});
|
||||
}
|
||||
|
||||
const adminGroup = await SQGroupRepository.createGroup({
|
||||
status: "ACTIVE",
|
||||
userId: ADMIN_ID,
|
||||
});
|
||||
for (const memberId of arMemberIds) {
|
||||
sql
|
||||
.prepare(
|
||||
/* sql */ `
|
||||
insert into "GroupMember" ("groupId", "userId", "role")
|
||||
values (@groupId, @userId, @role)
|
||||
`,
|
||||
)
|
||||
.run({
|
||||
groupId: adminGroup.id,
|
||||
userId: memberId,
|
||||
role: "REGULAR",
|
||||
});
|
||||
}
|
||||
|
||||
sql
|
||||
.prepare(/*sql*/ `update "Group" set "teamId" = 1 where "id" = ?`)
|
||||
.run(adminGroup.id);
|
||||
}
|
||||
|
||||
const randomMapList = (
|
||||
groupAlpha: number,
|
||||
groupBravo: number,
|
||||
@@ -3111,7 +3188,7 @@ async function friendships(variation?: SeedVariation | null) {
|
||||
.run({ userOneId, userTwoId });
|
||||
}
|
||||
|
||||
if (variation === "NO_SQ_GROUPS") return;
|
||||
if (variation === "NO_SQ_GROUPS" || variation === "TEAM_MAP_PREFS") return;
|
||||
|
||||
for (const friendId of SENDOU_FRIEND_IDS_IN_LOOKING_GROUPS) {
|
||||
const group = await SQGroupRepository.createGroup({
|
||||
|
||||
@@ -86,6 +86,7 @@ export interface Team {
|
||||
inviteCode: string;
|
||||
name: string;
|
||||
bsky: string | null;
|
||||
mapModePreferences: JSONColumnTypeNullable<UserMapModePreferences>;
|
||||
/** Team's tag, typically used in-game in front of users' names to indicate they are a member of the team. */
|
||||
tag: string | null;
|
||||
}
|
||||
@@ -289,7 +290,11 @@ export type ParsedMemento = {
|
||||
>;
|
||||
/** mapPreferences of season 2 */
|
||||
mapPreferences?: Array<{ userId: number; preference?: Preference }[]>;
|
||||
pools: Array<{ userId: number; pool: UserMapModePreferences["pool"] }>;
|
||||
pools: Array<{
|
||||
userId: number;
|
||||
pool: UserMapModePreferences["pool"];
|
||||
teamName?: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
export interface GroupMatch {
|
||||
|
||||
@@ -6,5 +6,6 @@ export const SEED_VARIATIONS = [
|
||||
"NZAP_IN_TEAM",
|
||||
"NO_SCRIMS",
|
||||
"NO_SQ_GROUPS",
|
||||
"TEAM_MAP_PREFS",
|
||||
"FINALIZED_BRACKET",
|
||||
] as const;
|
||||
|
||||
@@ -347,14 +347,19 @@ export function compareMatchToReportedScores({
|
||||
return "SAME";
|
||||
}
|
||||
|
||||
type GroupPreference = {
|
||||
userId: number;
|
||||
preferences: UserMapModePreferences;
|
||||
teamName?: string | null;
|
||||
};
|
||||
type CreateMatchMementoArgs = {
|
||||
own: {
|
||||
group: SQUncensoredGroup;
|
||||
preferences: { userId: number; preferences: UserMapModePreferences }[];
|
||||
preferences: GroupPreference[];
|
||||
};
|
||||
their: {
|
||||
group: SQUncensoredGroup;
|
||||
preferences: { userId: number; preferences: UserMapModePreferences }[];
|
||||
preferences: GroupPreference[];
|
||||
};
|
||||
mapList: TournamentMapListMap[];
|
||||
};
|
||||
@@ -440,6 +445,7 @@ function poolsMemento(args: CreateMatchMementoArgs): ParsedMemento["pools"] {
|
||||
return {
|
||||
userId: p.userId,
|
||||
pool,
|
||||
...(p.teamName ? { teamName: p.teamName } : {}),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1293,17 +1293,49 @@ function MapListMapPickInfo({
|
||||
return result;
|
||||
};
|
||||
|
||||
const mapPreferences = data.match.memento?.mapPreferences?.[i];
|
||||
const showPopover = () => {
|
||||
// legacy preference system (season 2)
|
||||
if (mapPreferences && mapPreferences.length > 0) return true;
|
||||
const sourceTeams = () => {
|
||||
if (!data.match.memento?.pools) return [];
|
||||
|
||||
if (map.source === "DEFAULT") return true;
|
||||
const pickerGroups = [data.match.groupAlpha, data.match.groupBravo].filter(
|
||||
(g) => map.source === "BOTH" || String(g.id) === map.source,
|
||||
);
|
||||
|
||||
return sourcePoolMemberIds().length > 0;
|
||||
const teams: Array<{ name: string; avatarUrl: string | null }> = [];
|
||||
for (const pickerGroup of pickerGroups) {
|
||||
for (const poolEntry of data.match.memento.pools) {
|
||||
if (!poolEntry.teamName) continue;
|
||||
if (!pickerGroup.members.some((m) => m.id === poolEntry.userId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const modePool = poolEntry.pool.find((p) => p.mode === map.mode);
|
||||
if (
|
||||
modePool?.stages.includes(map.stageId) &&
|
||||
!teams.some((t) => t.name === poolEntry.teamName)
|
||||
) {
|
||||
teams.push({
|
||||
name: poolEntry.teamName,
|
||||
avatarUrl:
|
||||
pickerGroup.team?.name === poolEntry.teamName
|
||||
? pickerGroup.team.avatarUrl
|
||||
: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return teams;
|
||||
};
|
||||
|
||||
if (showPopover()) {
|
||||
const mapPreferences = data.match.memento?.mapPreferences?.[i];
|
||||
const teams = sourceTeams();
|
||||
const poolMemberIds = sourcePoolMemberIds();
|
||||
const showPopover =
|
||||
(mapPreferences && mapPreferences.length > 0) ||
|
||||
map.source === "DEFAULT" ||
|
||||
poolMemberIds.length > 0;
|
||||
|
||||
if (showPopover) {
|
||||
return (
|
||||
<SendouPopover
|
||||
popoverClassName="text-main-forced"
|
||||
@@ -1321,9 +1353,25 @@ function MapListMapPickInfo({
|
||||
<div className="text-sm text-center text-lighter">
|
||||
{t("tournament:pickInfo.default.explanation")}
|
||||
</div>
|
||||
) : sourcePoolMemberIds().length > 0 ? (
|
||||
) : teams.length > 0 ? (
|
||||
<div className="stack sm">
|
||||
{sourcePoolMemberIds().map((userId) => {
|
||||
{teams.map((team) => (
|
||||
<div
|
||||
key={team.name}
|
||||
className="stack sm horizontal items-center xs"
|
||||
>
|
||||
<Avatar
|
||||
size="xxs"
|
||||
url={team.avatarUrl}
|
||||
identiconInput={team.name}
|
||||
/>
|
||||
{team.name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : poolMemberIds.length > 0 ? (
|
||||
<div className="stack sm">
|
||||
{poolMemberIds.map((userId) => {
|
||||
const user = userIdToUser(userId);
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -32,10 +32,6 @@ export async function updateUserMapModePreferences({
|
||||
userId: number;
|
||||
mapModePreferences: UserMapModePreferences;
|
||||
}) {
|
||||
const modesExcluded = modesShort.filter(
|
||||
(mode) => !mapModePreferences.pool.some((mp) => mp.mode === mode),
|
||||
);
|
||||
|
||||
const currentPreferences = (
|
||||
await db
|
||||
.selectFrom("User")
|
||||
@@ -44,22 +40,55 @@ export async function updateUserMapModePreferences({
|
||||
.executeTakeFirstOrThrow()
|
||||
).mapModePreferences;
|
||||
|
||||
for (const mode of modesExcluded) {
|
||||
const previousModePreference = currentPreferences?.pool.filter(
|
||||
(mp) => mp.mode === mode,
|
||||
);
|
||||
if (previousModePreference && previousModePreference.length > 0) {
|
||||
mapModePreferences.pool.push(...previousModePreference);
|
||||
}
|
||||
}
|
||||
const mergedPool = mergeExcludedModePreferences(
|
||||
mapModePreferences.pool,
|
||||
currentPreferences?.pool,
|
||||
);
|
||||
|
||||
return db
|
||||
.updateTable("User")
|
||||
.set({ mapModePreferences: JSON.stringify(mapModePreferences) })
|
||||
.set({
|
||||
mapModePreferences: JSON.stringify({
|
||||
...mapModePreferences,
|
||||
pool: mergedPool,
|
||||
}),
|
||||
})
|
||||
.where("id", "=", userId)
|
||||
.execute();
|
||||
}
|
||||
|
||||
export async function updateTeamMapModePreferences({
|
||||
teamId,
|
||||
mapModePreferences,
|
||||
}: {
|
||||
teamId: number;
|
||||
mapModePreferences: UserMapModePreferences;
|
||||
}) {
|
||||
const currentPreferences = (
|
||||
await db
|
||||
.selectFrom("AllTeam")
|
||||
.select("mapModePreferences")
|
||||
.where("id", "=", teamId)
|
||||
.executeTakeFirstOrThrow()
|
||||
).mapModePreferences;
|
||||
|
||||
const mergedPool = mergeExcludedModePreferences(
|
||||
mapModePreferences.pool,
|
||||
currentPreferences?.pool,
|
||||
);
|
||||
|
||||
return db
|
||||
.updateTable("AllTeam")
|
||||
.set({
|
||||
mapModePreferences: JSON.stringify({
|
||||
...mapModePreferences,
|
||||
pool: mergedPool,
|
||||
}),
|
||||
})
|
||||
.where("id", "=", teamId)
|
||||
.execute();
|
||||
}
|
||||
|
||||
export function updateVoiceChat(args: {
|
||||
userId: number;
|
||||
vc: Tables["User"]["vc"];
|
||||
@@ -112,11 +141,21 @@ export function updateNoScreen({
|
||||
.execute();
|
||||
}
|
||||
|
||||
export function currentTeamByUserId(userId: number) {
|
||||
return db
|
||||
.selectFrom("TeamMember")
|
||||
.innerJoin("Team", "Team.id", "TeamMember.teamId")
|
||||
.select(["Team.name"])
|
||||
.where("TeamMember.userId", "=", userId)
|
||||
.executeTakeFirst();
|
||||
/**
|
||||
* Preserves existing preferences for modes not included in the new submission.
|
||||
* So if they later want to play this mode again, the system remembers their maps.
|
||||
*/
|
||||
function mergeExcludedModePreferences(
|
||||
newPool: UserMapModePreferences["pool"],
|
||||
currentPool: UserMapModePreferences["pool"] | undefined,
|
||||
) {
|
||||
const modesExcluded = modesShort.filter(
|
||||
(mode) => !newPool.some((mp) => mp.mode === mode),
|
||||
);
|
||||
|
||||
const preservedPreferences = modesExcluded.flatMap(
|
||||
(mode) => currentPool?.filter((mp) => mp.mode === mode) ?? [],
|
||||
);
|
||||
|
||||
return [...newPool, ...preservedPreferences];
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { requireUser } from "~/features/auth/core/user.server";
|
||||
import * as QSettingsRepository from "~/features/sendouq-settings/QSettingsRepository.server";
|
||||
import * as TeamRepository from "~/features/team/TeamRepository.server";
|
||||
import { parseRequestPayload } from "~/utils/remix.server";
|
||||
import { assertUnreachable } from "~/utils/types";
|
||||
import { settingsActionSchema } from "../q-settings-schemas.server";
|
||||
@@ -13,10 +14,25 @@ export const action = async ({ request }: { request: Request }) => {
|
||||
|
||||
switch (data._action) {
|
||||
case "UPDATE_MAP_MODE_PREFERENCES": {
|
||||
await QSettingsRepository.updateUserMapModePreferences({
|
||||
mapModePreferences: data.mapModePreferences,
|
||||
userId: user.id,
|
||||
});
|
||||
if (typeof data.teamId === "number") {
|
||||
const allTeams = await TeamRepository.findAllMemberOfByUserId(user.id);
|
||||
const canManage = allTeams.some(
|
||||
(t) => t.id === data.teamId && (t.isOwner || t.isManager),
|
||||
);
|
||||
if (!canManage) {
|
||||
throw new Response(null, { status: 403 });
|
||||
}
|
||||
|
||||
await QSettingsRepository.updateTeamMapModePreferences({
|
||||
mapModePreferences: data.mapModePreferences,
|
||||
teamId: data.teamId,
|
||||
});
|
||||
} else {
|
||||
await QSettingsRepository.updateUserMapModePreferences({
|
||||
mapModePreferences: data.mapModePreferences,
|
||||
userId: user.id,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "UPDATE_VC": {
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { requireUser } from "~/features/auth/core/user.server";
|
||||
import * as QSettingsRepository from "~/features/sendouq-settings/QSettingsRepository.server";
|
||||
import * as TeamRepository from "~/features/team/TeamRepository.server";
|
||||
|
||||
export const loader = async () => {
|
||||
const user = requireUser();
|
||||
|
||||
const allTeams = await TeamRepository.findAllMemberOfByUserId(user.id);
|
||||
const manageableTeams = allTeams.filter((t) => t.isOwner || t.isManager);
|
||||
|
||||
return {
|
||||
settings: await QSettingsRepository.settingsByUserId(user.id),
|
||||
team: await QSettingsRepository.currentTeamByUserId(user.id),
|
||||
manageableTeams,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { z } from "zod";
|
||||
import { _action, modeShort, safeJSONParse, stageId } from "~/utils/zod";
|
||||
import { _action, id, modeShort, safeJSONParse, stageId } from "~/utils/zod";
|
||||
import { AMOUNT_OF_MAPS_IN_POOL_PER_MODE } from "./q-settings-constants";
|
||||
import {
|
||||
updateVoiceChatSchema,
|
||||
@@ -10,6 +10,10 @@ const preference = z.enum(["AVOID", "PREFER"]).optional();
|
||||
export const settingsActionSchema = z.union([
|
||||
z.object({
|
||||
_action: _action("UPDATE_MAP_MODE_PREFERENCES"),
|
||||
teamId: z.preprocess(
|
||||
(val) => (val === "" || val === undefined ? undefined : Number(val)),
|
||||
id.optional(),
|
||||
),
|
||||
mapModePreferences: z.preprocess(
|
||||
safeJSONParse,
|
||||
z
|
||||
|
||||
@@ -5,10 +5,13 @@ import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { MetaFunction } from "react-router";
|
||||
import { useFetcher, useLoaderData } from "react-router";
|
||||
import { Avatar } from "~/components/Avatar";
|
||||
import { SendouSelect, SendouSelectItem } from "~/components/elements/Select";
|
||||
import { ModeImage } from "~/components/Image";
|
||||
import { Main } from "~/components/Main";
|
||||
import { SubmitButton } from "~/components/SubmitButton";
|
||||
import type { Preference, UserMapModePreferences } from "~/db/tables";
|
||||
import { useUser } from "~/features/auth/core/user";
|
||||
import {
|
||||
soundCodeToLocalStorageKey,
|
||||
soundVolume,
|
||||
@@ -18,6 +21,7 @@ import { SendouForm } from "~/form/SendouForm";
|
||||
import { useHydrated } from "~/hooks/useHydrated";
|
||||
import { modesShort } from "~/modules/in-game-lists/modes";
|
||||
import type { ModeShort } from "~/modules/in-game-lists/types";
|
||||
import type { SerializeFrom } from "~/utils/remix";
|
||||
import { metaTags } from "~/utils/remix";
|
||||
import type { SendouRouteHandle } from "~/utils/remix.server";
|
||||
import {
|
||||
@@ -79,29 +83,52 @@ export default function SendouQSettingsPage() {
|
||||
);
|
||||
}
|
||||
|
||||
const PERSONAL_KEY = "personal";
|
||||
|
||||
type ManageableTeam = SerializeFrom<typeof loader>["manageableTeams"][number];
|
||||
|
||||
function preferencesFromRaw(
|
||||
raw: UserMapModePreferences | null,
|
||||
): UserMapModePreferences {
|
||||
if (!raw) return { pool: [], modes: [] };
|
||||
|
||||
return {
|
||||
modes: raw.modes,
|
||||
pool: raw.pool.map((p) => ({
|
||||
mode: p.mode,
|
||||
stages: p.stages.filter((s) => !BANNED_MAPS[p.mode].includes(s)),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function MapPicker() {
|
||||
const { t } = useTranslation(["q", "common"]);
|
||||
const data = useLoaderData<typeof loader>();
|
||||
const fetcher = useFetcher();
|
||||
const [preferences, setPreferences] = React.useState<UserMapModePreferences>(
|
||||
() => {
|
||||
if (!data.settings.mapModePreferences) {
|
||||
return {
|
||||
pool: [],
|
||||
modes: [],
|
||||
};
|
||||
}
|
||||
const hasTeams = data.manageableTeams.length > 0;
|
||||
|
||||
return {
|
||||
modes: data.settings.mapModePreferences.modes,
|
||||
pool: data.settings.mapModePreferences.pool.map((p) => ({
|
||||
mode: p.mode,
|
||||
stages: p.stages.filter((s) => !BANNED_MAPS[p.mode].includes(s)),
|
||||
})),
|
||||
};
|
||||
},
|
||||
const [selectedKey, setSelectedKey] = useState<string>(PERSONAL_KEY);
|
||||
|
||||
const selectedTeamId =
|
||||
selectedKey === PERSONAL_KEY ? undefined : Number(selectedKey);
|
||||
|
||||
const preferencesForSelection = (key: string) => {
|
||||
if (key === PERSONAL_KEY) {
|
||||
return preferencesFromRaw(data.settings.mapModePreferences);
|
||||
}
|
||||
const team = data.manageableTeams.find((t) => String(t.id) === key);
|
||||
return preferencesFromRaw(team?.mapModePreferences ?? null);
|
||||
};
|
||||
|
||||
const [preferences, setPreferences] = useState<UserMapModePreferences>(() =>
|
||||
preferencesForSelection(PERSONAL_KEY),
|
||||
);
|
||||
|
||||
const handleSelectionChange = (key: string) => {
|
||||
setSelectedKey(key);
|
||||
setPreferences(preferencesForSelection(key));
|
||||
};
|
||||
|
||||
const handleModePreferenceChange = ({
|
||||
mode,
|
||||
preference,
|
||||
@@ -142,6 +169,14 @@ function MapPicker() {
|
||||
return true;
|
||||
};
|
||||
|
||||
const selectItems = [
|
||||
{ id: PERSONAL_KEY, name: t("q:settings.maps.personal") },
|
||||
...data.manageableTeams.map((team) => ({
|
||||
id: String(team.id),
|
||||
name: team.name,
|
||||
})),
|
||||
];
|
||||
|
||||
return (
|
||||
<details>
|
||||
<summary className={clsx(styles.summary, "half-width")}>
|
||||
@@ -164,7 +199,34 @@ function MapPicker() {
|
||||
}),
|
||||
})}
|
||||
/>
|
||||
{selectedTeamId ? (
|
||||
<input type="hidden" name="teamId" value={selectedTeamId} />
|
||||
) : null}
|
||||
<div className="stack lg">
|
||||
{hasTeams ? (
|
||||
<div className="half-width">
|
||||
<SendouSelect
|
||||
value={selectedKey}
|
||||
onChange={(key) => handleSelectionChange(String(key))}
|
||||
aria-label={t("q:settings.maps.preferencesFor")}
|
||||
items={selectItems}
|
||||
bottomText={t("q:settings.maps.teamExplanation")}
|
||||
>
|
||||
{(item) => (
|
||||
<SendouSelectItem
|
||||
key={item.id}
|
||||
id={item.id}
|
||||
textValue={item.name}
|
||||
>
|
||||
<MapPickerSelectOption
|
||||
item={item}
|
||||
teams={data.manageableTeams}
|
||||
/>
|
||||
</SendouSelectItem>
|
||||
)}
|
||||
</SendouSelect>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="stack items-center">
|
||||
{modesShort.map((modeShort) => {
|
||||
const preference = preferences.modes.find(
|
||||
@@ -241,6 +303,35 @@ function MapPicker() {
|
||||
);
|
||||
}
|
||||
|
||||
function MapPickerSelectOption({
|
||||
item,
|
||||
teams,
|
||||
}: {
|
||||
item: { id: string; name: string };
|
||||
teams: ManageableTeam[];
|
||||
}) {
|
||||
const user = useUser();
|
||||
|
||||
if (item.id === PERSONAL_KEY) {
|
||||
return (
|
||||
<div className="stack horizontal xs items-center">
|
||||
<Avatar user={user} size="xxxs" />
|
||||
{item.name}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const team = teams.find((t) => String(t.id) === item.id);
|
||||
if (!team) return item.name;
|
||||
|
||||
return (
|
||||
<div className="stack horizontal xs items-center">
|
||||
<Avatar size="xxxs" url={team.logoUrl} identiconInput={team.name} />
|
||||
{team.name}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function VoiceChat() {
|
||||
const { t } = useTranslation(["q"]);
|
||||
const data = useLoaderData<typeof loader>();
|
||||
|
||||
@@ -12,7 +12,31 @@ import { userIsBanned } from "../ban/core/banned.server";
|
||||
import { FULL_GROUP_SIZE } from "./q-constants";
|
||||
import { SendouQError } from "./q-utils.server";
|
||||
|
||||
export function mapModePreferencesByGroupId(groupId: number) {
|
||||
export async function mapModePreferencesByGroupId(groupId: number) {
|
||||
const group = await db
|
||||
.selectFrom("Group")
|
||||
.leftJoin("AllTeam", "AllTeam.id", "Group.teamId")
|
||||
.select([
|
||||
"AllTeam.mapModePreferences as teamMapModePreferences",
|
||||
"AllTeam.name as teamName",
|
||||
])
|
||||
.where("Group.id", "=", groupId)
|
||||
.executeTakeFirst();
|
||||
|
||||
if (group?.teamMapModePreferences) {
|
||||
const members = await db
|
||||
.selectFrom("GroupMember")
|
||||
.select("GroupMember.userId")
|
||||
.where("GroupMember.groupId", "=", groupId)
|
||||
.execute();
|
||||
|
||||
return members.map((m) => ({
|
||||
userId: m.userId,
|
||||
preferences: group.teamMapModePreferences as UserMapModePreferences,
|
||||
teamName: group.teamName,
|
||||
}));
|
||||
}
|
||||
|
||||
return db
|
||||
.selectFrom("GroupMember")
|
||||
.innerJoin("User", "User.id", "GroupMember.userId")
|
||||
|
||||
@@ -69,7 +69,10 @@ export function findAllMemberOfByUserId(userId: number) {
|
||||
"Team.id",
|
||||
"Team.customUrl",
|
||||
"Team.name",
|
||||
"Team.mapModePreferences",
|
||||
"TeamMemberWithSecondary.role",
|
||||
"TeamMemberWithSecondary.isOwner",
|
||||
"TeamMemberWithSecondary.isManager",
|
||||
concatUserSubmittedImagePrefix(eb.ref("UserSubmittedImage.url")).as(
|
||||
"logoUrl",
|
||||
),
|
||||
|
||||
BIN
db-test.sqlite3
BIN
db-test.sqlite3
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
e2e/seeds/db-seed-TEAM_MAP_PREFS.sqlite3
Normal file
BIN
e2e/seeds/db-seed-TEAM_MAP_PREFS.sqlite3
Normal file
Binary file not shown.
@@ -249,4 +249,39 @@ test.describe("SendouQ", () => {
|
||||
combinedGroup.getByTestId("sendouq-group-card-member"),
|
||||
).toHaveCount(2);
|
||||
});
|
||||
|
||||
test("Team map preferences shown on match page", async ({ page }) => {
|
||||
await seed(page, "TEAM_MAP_PREFS");
|
||||
await impersonate(page, ADMIN_ID);
|
||||
|
||||
await navigate({ page, url: SENDOUQ_LOOKING_PAGE });
|
||||
|
||||
const groupCards = page.getByTestId("sendouq-group-card");
|
||||
const count = await groupCards.count();
|
||||
for (let i = 1; i < count; i++) {
|
||||
const groupCard = groupCards.nth(i);
|
||||
const challengeButton = groupCard
|
||||
.locator('button[type="submit"]')
|
||||
.first();
|
||||
await challengeButton.click();
|
||||
}
|
||||
|
||||
await impersonate(page, NZAP_TEST_ID);
|
||||
await navigate({ page, url: SENDOUQ_LOOKING_PAGE });
|
||||
|
||||
const acceptButton = page
|
||||
.getByRole("button", { name: "Start match" })
|
||||
.first();
|
||||
await expect(acceptButton).toBeVisible();
|
||||
await acceptButton.click();
|
||||
|
||||
await expect(page).toHaveURL(/\/q\/match\/\d+/);
|
||||
|
||||
const popoverButton = page.getByRole("button", { name: /votes/ }).first();
|
||||
await expect(popoverButton).toBeVisible();
|
||||
await popoverButton.click();
|
||||
|
||||
const popover = page.getByRole("dialog");
|
||||
await expect(popover.getByText("Alliance Rogue")).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -51,6 +51,9 @@
|
||||
"front.preview": "",
|
||||
"front.preview.explanation": "",
|
||||
"settings.maps.header": "",
|
||||
"settings.maps.personal": "",
|
||||
"settings.maps.preferencesFor": "",
|
||||
"settings.maps.teamExplanation": "",
|
||||
"settings.maps.avoid": "",
|
||||
"settings.maps.prefer": "",
|
||||
"settings.maps.neutral": "",
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
"pickInfo.default.explanation": "",
|
||||
"pickInfo.votes_one": "",
|
||||
"pickInfo.votes_other": "",
|
||||
"pickInfo.teamMapList": "",
|
||||
"pickInfo.counterpick": "Modvalg",
|
||||
"pickInfo.roll": "",
|
||||
"generator.error": "Ændringer, som du har lavet, blev ikke gemt, da turneringen nu er begyndt. ",
|
||||
|
||||
@@ -51,6 +51,9 @@
|
||||
"front.preview": "",
|
||||
"front.preview.explanation": "",
|
||||
"settings.maps.header": "",
|
||||
"settings.maps.personal": "",
|
||||
"settings.maps.preferencesFor": "",
|
||||
"settings.maps.teamExplanation": "",
|
||||
"settings.maps.avoid": "",
|
||||
"settings.maps.prefer": "",
|
||||
"settings.maps.neutral": "",
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
"pickInfo.default.explanation": "",
|
||||
"pickInfo.votes_one": "",
|
||||
"pickInfo.votes_other": "",
|
||||
"pickInfo.teamMapList": "",
|
||||
"pickInfo.counterpick": "Counterpick",
|
||||
"pickInfo.roll": "",
|
||||
"generator.error": "Die Änderungen wurden nicht gespeichert, weil das Turnier bereits begonnen hat",
|
||||
|
||||
@@ -51,6 +51,9 @@
|
||||
"front.preview": "Preview groups in the queue without joining",
|
||||
"front.preview.explanation": "This feature is only available to Supporter tier (or above) patrons of sendou.ink",
|
||||
"settings.maps.header": "Stages and modes",
|
||||
"settings.maps.personal": "Personal",
|
||||
"settings.maps.preferencesFor": "Preferences for",
|
||||
"settings.maps.teamExplanation": "Team map list is used for all members when queueing as a team",
|
||||
"settings.maps.avoid": "Avoid",
|
||||
"settings.maps.prefer": "Prefer",
|
||||
"settings.maps.neutral": "Neutral",
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
"pickInfo.default.explanation": "A suitable map was not available from the participants' pools. This map was selected from all popular maps instead.",
|
||||
"pickInfo.votes_one": "{{count}} vote",
|
||||
"pickInfo.votes_other": "{{count}} votes",
|
||||
"pickInfo.teamMapList": "{{teamName}} map list",
|
||||
"pickInfo.counterpick": "Counterpick",
|
||||
"pickInfo.roll": "Random",
|
||||
"generator.error": "Changes you made weren't saved since the tournament started",
|
||||
|
||||
@@ -51,6 +51,9 @@
|
||||
"front.preview": "Ver avance de grupos en fila sin unirte",
|
||||
"front.preview.explanation": "Esta función solo es disponible para Patrons a nivel Supporter (o más alto) de sendou.ink",
|
||||
"settings.maps.header": "Mapas y modos",
|
||||
"settings.maps.personal": "",
|
||||
"settings.maps.preferencesFor": "",
|
||||
"settings.maps.teamExplanation": "",
|
||||
"settings.maps.avoid": "Evitar",
|
||||
"settings.maps.prefer": "Preferir",
|
||||
"settings.maps.neutral": "Neutral",
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
"pickInfo.votes_one": "{{count}} voto",
|
||||
"pickInfo.votes_many": "{{count}} votos",
|
||||
"pickInfo.votes_other": "{{count}} votos",
|
||||
"pickInfo.teamMapList": "",
|
||||
"pickInfo.counterpick": "Contraselección",
|
||||
"pickInfo.roll": "",
|
||||
"generator.error": "Los cambios no fueron guardados ya que el torneo comenzó",
|
||||
|
||||
@@ -51,6 +51,9 @@
|
||||
"front.preview": "Ver avance de grupos en fila sin unirte",
|
||||
"front.preview.explanation": "Esta función solo es disponible para Patrons a nivel Supporter (o más alto) de sendou.ink",
|
||||
"settings.maps.header": "Escenarios y Estilos",
|
||||
"settings.maps.personal": "",
|
||||
"settings.maps.preferencesFor": "",
|
||||
"settings.maps.teamExplanation": "",
|
||||
"settings.maps.avoid": "Evitar",
|
||||
"settings.maps.prefer": "Preferir",
|
||||
"settings.maps.neutral": "Neutral",
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
"pickInfo.votes_one": "",
|
||||
"pickInfo.votes_many": "",
|
||||
"pickInfo.votes_other": "",
|
||||
"pickInfo.teamMapList": "",
|
||||
"pickInfo.counterpick": "Contraselección",
|
||||
"pickInfo.roll": "",
|
||||
"generator.error": "Cambios no fueron guardados ya que el torneo comenzó",
|
||||
|
||||
@@ -51,6 +51,9 @@
|
||||
"front.preview": "",
|
||||
"front.preview.explanation": "",
|
||||
"settings.maps.header": "",
|
||||
"settings.maps.personal": "",
|
||||
"settings.maps.preferencesFor": "",
|
||||
"settings.maps.teamExplanation": "",
|
||||
"settings.maps.avoid": "",
|
||||
"settings.maps.prefer": "",
|
||||
"settings.maps.neutral": "",
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
"pickInfo.votes_one": "",
|
||||
"pickInfo.votes_many": "",
|
||||
"pickInfo.votes_other": "",
|
||||
"pickInfo.teamMapList": "",
|
||||
"pickInfo.counterpick": "",
|
||||
"pickInfo.roll": "",
|
||||
"generator.error": "Les modifications apportées n'ont pas été enregistrées depuis le début du tournoi",
|
||||
|
||||
@@ -51,6 +51,9 @@
|
||||
"front.preview": "Regarder qui est dans la queue sans la rejoindre",
|
||||
"front.preview.explanation": "Cette fonctionnalité n'est disponible que pour les utilisateurs de niveau Supporter (ou supérieur) sur le patreon de sendou.ink.",
|
||||
"settings.maps.header": "Stages et modes",
|
||||
"settings.maps.personal": "",
|
||||
"settings.maps.preferencesFor": "",
|
||||
"settings.maps.teamExplanation": "",
|
||||
"settings.maps.avoid": "Détester",
|
||||
"settings.maps.prefer": "Préférer",
|
||||
"settings.maps.neutral": "Neutre",
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
"pickInfo.votes_one": "",
|
||||
"pickInfo.votes_many": "",
|
||||
"pickInfo.votes_other": "",
|
||||
"pickInfo.teamMapList": "",
|
||||
"pickInfo.counterpick": "Counterpick",
|
||||
"pickInfo.roll": "",
|
||||
"generator.error": "Les modifications apportées n'ont pas été enregistrées depuis le début du tournoi",
|
||||
|
||||
@@ -51,6 +51,9 @@
|
||||
"front.preview": "",
|
||||
"front.preview.explanation": "",
|
||||
"settings.maps.header": "",
|
||||
"settings.maps.personal": "",
|
||||
"settings.maps.preferencesFor": "",
|
||||
"settings.maps.teamExplanation": "",
|
||||
"settings.maps.avoid": "",
|
||||
"settings.maps.prefer": "",
|
||||
"settings.maps.neutral": "",
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
"pickInfo.votes_one": "",
|
||||
"pickInfo.votes_two": "",
|
||||
"pickInfo.votes_other": "",
|
||||
"pickInfo.teamMapList": "",
|
||||
"pickInfo.counterpick": "",
|
||||
"pickInfo.roll": "",
|
||||
"generator.error": "שינויים שביצעת לא נשמרו מאז תחילת הטורניר",
|
||||
|
||||
@@ -51,6 +51,9 @@
|
||||
"front.preview": "Visualizza gruppi nella coda prima di unirti",
|
||||
"front.preview.explanation": "Questa funzione è disponibile solo per iscritti al Patreon di sendou.ink di tier Supporter o più",
|
||||
"settings.maps.header": "Mappe e modalità",
|
||||
"settings.maps.personal": "",
|
||||
"settings.maps.preferencesFor": "",
|
||||
"settings.maps.teamExplanation": "",
|
||||
"settings.maps.avoid": "Evita",
|
||||
"settings.maps.prefer": "Preferisci",
|
||||
"settings.maps.neutral": "Neutrale",
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
"pickInfo.votes_one": "",
|
||||
"pickInfo.votes_many": "",
|
||||
"pickInfo.votes_other": "",
|
||||
"pickInfo.teamMapList": "",
|
||||
"pickInfo.counterpick": "Counterpick",
|
||||
"pickInfo.roll": "",
|
||||
"generator.error": "I cambiamenti che hai fatto non sono stati salvati visto che il torneo è già cominciato.",
|
||||
|
||||
@@ -51,6 +51,9 @@
|
||||
"front.preview": "ジョインせず列にいるグループを見る",
|
||||
"front.preview.explanation": "この機能はサポーター(あるいはそれ以上)の支援者のみ使えます。",
|
||||
"settings.maps.header": "ステージ、モード",
|
||||
"settings.maps.personal": "",
|
||||
"settings.maps.preferencesFor": "",
|
||||
"settings.maps.teamExplanation": "",
|
||||
"settings.maps.avoid": "避ける",
|
||||
"settings.maps.prefer": "好み",
|
||||
"settings.maps.neutral": "普通",
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
"pickInfo.both": "両者選択",
|
||||
"pickInfo.default": "",
|
||||
"pickInfo.default.explanation": "",
|
||||
"pickInfo.teamMapList": "",
|
||||
"pickInfo.counterpick": "順番に選ぶ",
|
||||
"pickInfo.roll": "",
|
||||
"generator.error": "トーナメントが開始済みなので変更は保存されませんでした。",
|
||||
|
||||
@@ -51,6 +51,9 @@
|
||||
"front.preview": "",
|
||||
"front.preview.explanation": "",
|
||||
"settings.maps.header": "",
|
||||
"settings.maps.personal": "",
|
||||
"settings.maps.preferencesFor": "",
|
||||
"settings.maps.teamExplanation": "",
|
||||
"settings.maps.avoid": "",
|
||||
"settings.maps.prefer": "",
|
||||
"settings.maps.neutral": "",
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
"pickInfo.both": "",
|
||||
"pickInfo.default": "",
|
||||
"pickInfo.default.explanation": "",
|
||||
"pickInfo.teamMapList": "",
|
||||
"pickInfo.counterpick": "",
|
||||
"pickInfo.roll": "",
|
||||
"generator.error": "",
|
||||
|
||||
@@ -51,6 +51,9 @@
|
||||
"front.preview": "",
|
||||
"front.preview.explanation": "",
|
||||
"settings.maps.header": "",
|
||||
"settings.maps.personal": "",
|
||||
"settings.maps.preferencesFor": "",
|
||||
"settings.maps.teamExplanation": "",
|
||||
"settings.maps.avoid": "",
|
||||
"settings.maps.prefer": "",
|
||||
"settings.maps.neutral": "",
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
"pickInfo.default.explanation": "",
|
||||
"pickInfo.votes_one": "",
|
||||
"pickInfo.votes_other": "",
|
||||
"pickInfo.teamMapList": "",
|
||||
"pickInfo.counterpick": "",
|
||||
"pickInfo.roll": "",
|
||||
"generator.error": "",
|
||||
|
||||
@@ -51,6 +51,9 @@
|
||||
"front.preview": "",
|
||||
"front.preview.explanation": "",
|
||||
"settings.maps.header": "",
|
||||
"settings.maps.personal": "",
|
||||
"settings.maps.preferencesFor": "",
|
||||
"settings.maps.teamExplanation": "",
|
||||
"settings.maps.avoid": "",
|
||||
"settings.maps.prefer": "",
|
||||
"settings.maps.neutral": "",
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
"pickInfo.votes_few": "",
|
||||
"pickInfo.votes_many": "",
|
||||
"pickInfo.votes_other": "",
|
||||
"pickInfo.teamMapList": "",
|
||||
"pickInfo.counterpick": "",
|
||||
"pickInfo.roll": "",
|
||||
"generator.error": "Zmiany nie zostały zapisane z powodu rozpoczęcia turnieju",
|
||||
|
||||
@@ -51,6 +51,9 @@
|
||||
"front.preview": "Pré-visualizar grupos na fila sem entrar",
|
||||
"front.preview.explanation": "Essa função está disponível apenas para patronos (ou patronesses) da tier Supporter (ou acima) do sendou.ink",
|
||||
"settings.maps.header": "Mapas e modos",
|
||||
"settings.maps.personal": "",
|
||||
"settings.maps.preferencesFor": "",
|
||||
"settings.maps.teamExplanation": "",
|
||||
"settings.maps.avoid": "Evitar",
|
||||
"settings.maps.prefer": "Prefere",
|
||||
"settings.maps.neutral": "Neutro",
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
"pickInfo.votes_one": "",
|
||||
"pickInfo.votes_many": "",
|
||||
"pickInfo.votes_other": "",
|
||||
"pickInfo.teamMapList": "",
|
||||
"pickInfo.counterpick": "Counterpick",
|
||||
"pickInfo.roll": "",
|
||||
"generator.error": "Mudanças que você fez não foram salvas uma vez que o torneio já começou",
|
||||
|
||||
@@ -51,6 +51,9 @@
|
||||
"front.preview": "Предпросмотр груп в очереди без присоединения",
|
||||
"front.preview.explanation": "Данная функция доступна только меценатам sendou.imk уровня Supporter (или выше)",
|
||||
"settings.maps.header": "Арены и режимы",
|
||||
"settings.maps.personal": "",
|
||||
"settings.maps.preferencesFor": "",
|
||||
"settings.maps.teamExplanation": "",
|
||||
"settings.maps.avoid": "Избегаю",
|
||||
"settings.maps.prefer": "Предпочитаю",
|
||||
"settings.maps.neutral": "Нейтрально",
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
"pickInfo.votes_few": "",
|
||||
"pickInfo.votes_many": "",
|
||||
"pickInfo.votes_other": "",
|
||||
"pickInfo.teamMapList": "",
|
||||
"pickInfo.counterpick": "Контрвыбор",
|
||||
"pickInfo.roll": "",
|
||||
"generator.error": "Ваши изменения не были сохранены так как турнир начался",
|
||||
|
||||
@@ -51,6 +51,9 @@
|
||||
"front.preview": "预览当前匹配中的队伍",
|
||||
"front.preview.explanation": "该功能仅对sendou.ink的patron支持者开放",
|
||||
"settings.maps.header": "地图和模式",
|
||||
"settings.maps.personal": "",
|
||||
"settings.maps.preferencesFor": "",
|
||||
"settings.maps.teamExplanation": "",
|
||||
"settings.maps.avoid": "避开",
|
||||
"settings.maps.prefer": "偏好",
|
||||
"settings.maps.neutral": "中立",
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
"pickInfo.both": "双方都选择",
|
||||
"pickInfo.default": "",
|
||||
"pickInfo.default.explanation": "",
|
||||
"pickInfo.teamMapList": "",
|
||||
"pickInfo.counterpick": "反选",
|
||||
"pickInfo.roll": "",
|
||||
"generator.error": "由于比赛已经开始,您所更改的内容无法保存",
|
||||
|
||||
5
migrations/128-team-map-mode-preferences.js
Normal file
5
migrations/128-team-map-mode-preferences.js
Normal file
@@ -0,0 +1,5 @@
|
||||
export function up(db) {
|
||||
db.prepare(
|
||||
/* sql */ `alter table "AllTeam" add column "mapModePreferences" text`,
|
||||
).run();
|
||||
}
|
||||
Reference in New Issue
Block a user