mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-08-27 21:55:15 -05:00
Team SendouForm finalized, custom roles (#3156)
This commit is contained in:
@@ -4,7 +4,6 @@ import type {
|
||||
Insertable,
|
||||
JSONColumnType,
|
||||
Selectable,
|
||||
Updateable,
|
||||
} from "kysely";
|
||||
import type { AssociationVisibility } from "~/features/associations/associations-types";
|
||||
import type { tags } from "~/features/calendar/calendar-constants";
|
||||
@@ -34,6 +33,8 @@ type Generated<T> =
|
||||
|
||||
export type MemberRole = (typeof TEAM_MEMBER_ROLES)[number];
|
||||
|
||||
export type MemberRoleType = "PLAYER" | "OTHER";
|
||||
|
||||
/** In SQLite booleans are presented as 0 (false) and 1 (true) */
|
||||
export type DBBoolean = number;
|
||||
|
||||
@@ -98,6 +99,9 @@ export interface TeamMember {
|
||||
isManager: Generated<number>;
|
||||
leftAt: number | null;
|
||||
role: MemberRole | null;
|
||||
customRole: string | null;
|
||||
/** If customRole is defined, this classifies how the role should be treated */
|
||||
roleType: MemberRoleType | null;
|
||||
teamId: number;
|
||||
userId: number;
|
||||
isMainTeam: DBBoolean;
|
||||
@@ -1439,7 +1443,6 @@ export interface SplatoonRotation {
|
||||
|
||||
export type Tables = { [P in keyof DB]: Selectable<DB[P]> };
|
||||
export type TablesInsertable = { [P in keyof DB]: Insertable<DB[P]> };
|
||||
export type TablesUpdatable = { [P in keyof DB]: Updateable<DB[P]> };
|
||||
|
||||
export interface DB {
|
||||
AllTeam: Team;
|
||||
|
||||
@@ -12,7 +12,7 @@ import { assertUnreachable } from "~/utils/types";
|
||||
import { scrimsPage } from "~/utils/urls";
|
||||
import * as SQGroupRepository from "../../sendouq/SQGroupRepository.server";
|
||||
import * as TeamRepository from "../../team/TeamRepository.server";
|
||||
import { NON_PLAYER_TEAM_ROLES } from "../../team/team-constants";
|
||||
import { getMemberRoleType } from "../../team/team-utils";
|
||||
import * as ScrimPostRepository from "../ScrimPostRepository.server";
|
||||
import { LUTI_DIVS, SCRIM } from "../scrims-constants";
|
||||
import {
|
||||
@@ -120,7 +120,7 @@ export const usersListForPost = async ({
|
||||
errorToastIfFalsy(team, "User is not a member of this team");
|
||||
|
||||
const filteredMembers = team.members.filter(
|
||||
(member) => !member.role || !NON_PLAYER_TEAM_ROLES.includes(member.role),
|
||||
(member) => getMemberRoleType(member) !== "OTHER",
|
||||
);
|
||||
|
||||
// handle case when all users are from excluded roles
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import { db } from "~/db/sql";
|
||||
import type { TablesUpdatable } from "~/db/tables";
|
||||
|
||||
export function update(
|
||||
where: { teamId: number; userId: number },
|
||||
values: TablesUpdatable["TeamMember"],
|
||||
) {
|
||||
return db
|
||||
.updateTable("AllTeamMember")
|
||||
.set(values)
|
||||
.where("AllTeamMember.teamId", "=", where.teamId)
|
||||
.where("AllTeamMember.userId", "=", where.userId)
|
||||
.execute();
|
||||
}
|
||||
@@ -64,13 +64,19 @@ export function searchByName({
|
||||
.select(["User.id", "User.username"])
|
||||
.whereRef("TeamMemberWithSecondary.teamId", "=", "Team.id")
|
||||
.where((eb2) =>
|
||||
eb2.or([
|
||||
eb2("TeamMemberWithSecondary.role", "is", null),
|
||||
eb2(
|
||||
"TeamMemberWithSecondary.role",
|
||||
"not in",
|
||||
NON_PLAYER_TEAM_ROLES,
|
||||
),
|
||||
eb2.and([
|
||||
eb2.or([
|
||||
eb2("TeamMemberWithSecondary.role", "is", null),
|
||||
eb2(
|
||||
"TeamMemberWithSecondary.role",
|
||||
"not in",
|
||||
NON_PLAYER_TEAM_ROLES,
|
||||
),
|
||||
]),
|
||||
eb2.or([
|
||||
eb2("TeamMemberWithSecondary.roleType", "is", null),
|
||||
eb2("TeamMemberWithSecondary.roleType", "!=", "OTHER"),
|
||||
]),
|
||||
]),
|
||||
)
|
||||
.orderBy("TeamMemberWithSecondary.isOwner", "desc"),
|
||||
@@ -171,6 +177,8 @@ export function findByCustomUrl(
|
||||
.select(({ eb: innerEb }) => [
|
||||
...COMMON_USER_FIELDS,
|
||||
"TeamMemberWithSecondary.role",
|
||||
"TeamMemberWithSecondary.customRole",
|
||||
"TeamMemberWithSecondary.roleType",
|
||||
"TeamMemberWithSecondary.isOwner",
|
||||
"TeamMemberWithSecondary.isManager",
|
||||
"TeamMemberWithSecondary.isMainTeam",
|
||||
@@ -312,7 +320,7 @@ export async function teamsByMemberUserId(
|
||||
eb
|
||||
.selectFrom("TeamMemberWithSecondary as m2")
|
||||
.innerJoin("User", "User.id", "m2.userId")
|
||||
.select([...COMMON_USER_FIELDS, "m2.role"])
|
||||
.select([...COMMON_USER_FIELDS, "m2.role", "m2.roleType"])
|
||||
.whereRef("TeamMemberWithSecondary.teamId", "=", "m2.teamId"),
|
||||
).as("members"),
|
||||
])
|
||||
@@ -545,51 +553,102 @@ export function handleMemberLeaving({
|
||||
userId: number;
|
||||
teamId: number;
|
||||
newOwnerUserId?: number;
|
||||
}) {
|
||||
return db
|
||||
.transaction()
|
||||
.execute((trx) => memberLeave(trx, { userId, teamId, newOwnerUserId }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a roster edit in a single transaction: updates each kept member's role
|
||||
* & editor status and removes (kicks) the members in `kickedUserIds`.
|
||||
*/
|
||||
export function updateRoster({
|
||||
teamId,
|
||||
members,
|
||||
kickedUserIds,
|
||||
}: {
|
||||
teamId: number;
|
||||
members: Array<{
|
||||
userId: number;
|
||||
role: Tables["TeamMember"]["role"];
|
||||
customRole: Tables["TeamMember"]["customRole"];
|
||||
roleType: Tables["TeamMember"]["roleType"];
|
||||
isManager: boolean;
|
||||
}>;
|
||||
kickedUserIds: number[];
|
||||
}) {
|
||||
return db.transaction().execute(async (trx) => {
|
||||
const currentTeams = await teamsByMemberUserId(userId, trx);
|
||||
|
||||
const teamToLeave = currentTeams.find((team) => team.id === teamId);
|
||||
invariant(teamToLeave, "User is not a member of this team");
|
||||
invariant(
|
||||
!teamToLeave.isOwner || newOwnerUserId,
|
||||
"New owner id must be provided when old is leaving",
|
||||
);
|
||||
|
||||
const wasMainTeam = teamToLeave.isMainTeam;
|
||||
const newMainTeam = currentTeams.find((team) => team.id !== teamId);
|
||||
if (wasMainTeam && newMainTeam) {
|
||||
await trx
|
||||
.updateTable("AllTeamMember")
|
||||
.set({
|
||||
isMainTeam: 1,
|
||||
})
|
||||
.where("userId", "=", userId)
|
||||
.where("teamId", "=", newMainTeam.id)
|
||||
.execute();
|
||||
for (const userId of kickedUserIds) {
|
||||
await memberLeave(trx, { userId, teamId });
|
||||
}
|
||||
|
||||
await trx
|
||||
.updateTable("AllTeamMember")
|
||||
.set({
|
||||
leftAt: databaseTimestampNow(),
|
||||
isMainTeam: 0,
|
||||
isOwner: 0,
|
||||
isManager: 0,
|
||||
})
|
||||
.where("userId", "=", userId)
|
||||
.where("teamId", "=", teamId)
|
||||
.execute();
|
||||
if (newOwnerUserId) {
|
||||
for (const member of members) {
|
||||
await trx
|
||||
.updateTable("AllTeamMember")
|
||||
.set({
|
||||
isOwner: 1,
|
||||
isManager: 0,
|
||||
role: member.role,
|
||||
customRole: member.customRole,
|
||||
roleType: member.roleType,
|
||||
isManager: member.isManager ? 1 : 0,
|
||||
})
|
||||
.where("userId", "=", newOwnerUserId)
|
||||
.where("teamId", "=", teamId)
|
||||
.where("userId", "=", member.userId)
|
||||
.execute();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function memberLeave(
|
||||
trx: Transaction<DB>,
|
||||
{
|
||||
userId,
|
||||
teamId,
|
||||
newOwnerUserId,
|
||||
}: { userId: number; teamId: number; newOwnerUserId?: number },
|
||||
) {
|
||||
const currentTeams = await teamsByMemberUserId(userId, trx);
|
||||
|
||||
const teamToLeave = currentTeams.find((team) => team.id === teamId);
|
||||
invariant(teamToLeave, "User is not a member of this team");
|
||||
invariant(
|
||||
!teamToLeave.isOwner || newOwnerUserId,
|
||||
"New owner id must be provided when old is leaving",
|
||||
);
|
||||
|
||||
const wasMainTeam = teamToLeave.isMainTeam;
|
||||
const newMainTeam = currentTeams.find((team) => team.id !== teamId);
|
||||
if (wasMainTeam && newMainTeam) {
|
||||
await trx
|
||||
.updateTable("AllTeamMember")
|
||||
.set({
|
||||
isMainTeam: 1,
|
||||
})
|
||||
.where("userId", "=", userId)
|
||||
.where("teamId", "=", newMainTeam.id)
|
||||
.execute();
|
||||
}
|
||||
|
||||
await trx
|
||||
.updateTable("AllTeamMember")
|
||||
.set({
|
||||
leftAt: databaseTimestampNow(),
|
||||
isMainTeam: 0,
|
||||
isOwner: 0,
|
||||
isManager: 0,
|
||||
})
|
||||
.where("userId", "=", userId)
|
||||
.where("teamId", "=", teamId)
|
||||
.execute();
|
||||
if (newOwnerUserId) {
|
||||
await trx
|
||||
.updateTable("AllTeamMember")
|
||||
.set({
|
||||
isOwner: 1,
|
||||
isManager: 0,
|
||||
})
|
||||
.where("userId", "=", newOwnerUserId)
|
||||
.where("teamId", "=", teamId)
|
||||
.execute();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import type { ActionFunction } from "react-router";
|
||||
import { redirect } from "react-router";
|
||||
import type { MemberRole, MemberRoleType } from "~/db/tables";
|
||||
import { requireUser } from "~/features/auth/core/user.server";
|
||||
import {
|
||||
errorToastIfFalsy,
|
||||
notFoundIfFalsy,
|
||||
parseRequestPayload,
|
||||
} from "~/utils/remix.server";
|
||||
import { parseFormData } from "~/form/parse.server";
|
||||
import { errorToastIfFalsy, notFoundIfFalsy } from "~/utils/remix.server";
|
||||
import { assertUnreachable } from "~/utils/types";
|
||||
import * as TeamMemberRepository from "../TeamMemberRepository.server";
|
||||
import { teamPage } from "~/utils/urls";
|
||||
import * as TeamRepository from "../TeamRepository.server";
|
||||
import { CUSTOM_ROLE_VALUE } from "../team-schemas";
|
||||
import { manageRosterSchema, teamParamsSchema } from "../team-schemas.server";
|
||||
import { isTeamManager } from "../team-utils";
|
||||
|
||||
@@ -21,66 +21,69 @@ export const action: ActionFunction = async ({ request, params }) => {
|
||||
"Only team manager or owner can manage roster",
|
||||
);
|
||||
|
||||
const data = await parseRequestPayload({
|
||||
const result = await parseFormData({
|
||||
request,
|
||||
schema: manageRosterSchema,
|
||||
});
|
||||
if (!result.success) {
|
||||
return { fieldErrors: result.fieldErrors };
|
||||
}
|
||||
const data = result.data;
|
||||
|
||||
switch (data._action) {
|
||||
case "DELETE_MEMBER": {
|
||||
const member = team.members.find((m) => m.id === data.userId);
|
||||
|
||||
errorToastIfFalsy(member, "Member not found");
|
||||
errorToastIfFalsy(member.id !== user.id, "Can't delete yourself");
|
||||
errorToastIfFalsy(!member.isOwner, "Can't delete owner");
|
||||
|
||||
await TeamRepository.handleMemberLeaving({
|
||||
teamId: team.id,
|
||||
userId: data.userId,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "RESET_INVITE_LINK": {
|
||||
await TeamRepository.resetInviteCode(team.id);
|
||||
|
||||
break;
|
||||
}
|
||||
case "ADD_MANAGER": {
|
||||
await TeamMemberRepository.update(
|
||||
{ teamId: team.id, userId: data.userId },
|
||||
{
|
||||
isManager: 1,
|
||||
},
|
||||
case "UPDATE_ROSTER": {
|
||||
const submittedIds = new Set(data.members.map((m) => m.userId));
|
||||
const existingMembersById = new Map(
|
||||
team.members.map((member) => [member.id, member]),
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
case "REMOVE_MANAGER": {
|
||||
const member = team.members.find((m) => m.id === data.userId);
|
||||
errorToastIfFalsy(member, "Member not found");
|
||||
errorToastIfFalsy(
|
||||
member.id !== user.id,
|
||||
"Can't remove yourself as manager",
|
||||
);
|
||||
for (const member of data.members) {
|
||||
errorToastIfFalsy(
|
||||
existingMembersById.has(member.userId),
|
||||
"Member not found",
|
||||
);
|
||||
}
|
||||
|
||||
await TeamMemberRepository.update(
|
||||
{ teamId: team.id, userId: data.userId },
|
||||
{
|
||||
isManager: 0,
|
||||
},
|
||||
);
|
||||
const kickedUserIds = team.members
|
||||
.filter((member) => !submittedIds.has(member.id))
|
||||
.map((member) => {
|
||||
errorToastIfFalsy(!member.isOwner, "Can't kick the owner");
|
||||
errorToastIfFalsy(member.id !== user.id, "Can't kick yourself");
|
||||
return member.id;
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
case "UPDATE_MEMBER_ROLE": {
|
||||
await TeamMemberRepository.update(
|
||||
{ teamId: team.id, userId: data.userId },
|
||||
{
|
||||
role: data.role || null,
|
||||
},
|
||||
);
|
||||
await TeamRepository.updateRoster({
|
||||
teamId: team.id,
|
||||
members: data.members.map((member) => {
|
||||
const isCustom = member.role === CUSTOM_ROLE_VALUE;
|
||||
const existing = existingMembersById.get(member.userId);
|
||||
const isProtectedMember = Boolean(
|
||||
existing?.isOwner || existing?.id === user.id,
|
||||
);
|
||||
|
||||
break;
|
||||
return {
|
||||
userId: member.userId,
|
||||
role: isCustom
|
||||
? null
|
||||
: ((member.role || null) as MemberRole | null),
|
||||
customRole: isCustom ? member.customRole || null : null,
|
||||
roleType: isCustom
|
||||
? ((member.roleType || null) as MemberRoleType | null)
|
||||
: null,
|
||||
isManager: isProtectedMember
|
||||
? Boolean(existing?.isManager)
|
||||
: member.isManager,
|
||||
};
|
||||
}),
|
||||
kickedUserIds,
|
||||
});
|
||||
|
||||
return redirect(teamPage(customUrl));
|
||||
}
|
||||
default: {
|
||||
assertUnreachable(data);
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
import { LogOut, Menu, SquarePen, Star, Trash2, Users } from "lucide-react";
|
||||
import {
|
||||
LogOut,
|
||||
Menu,
|
||||
SquarePen,
|
||||
Star,
|
||||
Swords,
|
||||
Trash2,
|
||||
Users,
|
||||
Wrench,
|
||||
} from "lucide-react";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link, useFetcher, useMatches } from "react-router";
|
||||
@@ -6,6 +15,12 @@ import { Avatar } from "~/components/Avatar";
|
||||
import { LinkButton, SendouButton } from "~/components/elements/Button";
|
||||
import { SendouDialog } from "~/components/elements/Dialog";
|
||||
import { SendouMenu, SendouMenuItem } from "~/components/elements/Menu";
|
||||
import {
|
||||
SendouTab,
|
||||
SendouTabList,
|
||||
SendouTabPanel,
|
||||
SendouTabs,
|
||||
} from "~/components/elements/Tabs";
|
||||
import { WeaponImage } from "~/components/Image";
|
||||
import { Placement } from "~/components/Placement";
|
||||
import { useUser } from "~/features/auth/core/user";
|
||||
@@ -17,6 +32,7 @@ import { action } from "../actions/t.$customUrl.index.server";
|
||||
import type * as TeamRepository from "../TeamRepository.server";
|
||||
import styles from "../team.module.css";
|
||||
import {
|
||||
getMemberRoleType,
|
||||
isTeamManager,
|
||||
isTeamMember,
|
||||
isTeamOwner,
|
||||
@@ -26,10 +42,27 @@ import {
|
||||
export { action };
|
||||
|
||||
export default function TeamIndexPage() {
|
||||
const { t } = useTranslation(["team"]);
|
||||
const [, parentRoute] = useMatches();
|
||||
invariant(parentRoute);
|
||||
const layoutData = parentRoute.data as TeamLoaderData;
|
||||
|
||||
const members = layoutData.team.members;
|
||||
const playerMembers = members.filter(
|
||||
(member) => getMemberRoleType(member) !== "OTHER",
|
||||
);
|
||||
const otherMembers = members.filter(
|
||||
(member) => getMemberRoleType(member) === "OTHER",
|
||||
);
|
||||
const showSections = playerMembers.length > 0 && otherMembers.length > 0;
|
||||
|
||||
const renderMember = (member: (typeof members)[number]) => (
|
||||
<React.Fragment key={member.discordId}>
|
||||
<MemberRow member={member} number={members.indexOf(member)} />
|
||||
<MobileMemberCard member={member} />
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="stack lg">
|
||||
<ActionButtons />
|
||||
@@ -39,14 +72,34 @@ export default function TeamIndexPage() {
|
||||
{layoutData.team.bio ? (
|
||||
<article data-testid="team-bio">{layoutData.team.bio}</article>
|
||||
) : null}
|
||||
<div className="stack lg">
|
||||
{layoutData.team.members.map((member, i) => (
|
||||
<React.Fragment key={member.discordId}>
|
||||
<MemberRow member={member} number={i} />
|
||||
<MobileMemberCard member={member} />
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
{showSections ? (
|
||||
<SendouTabs>
|
||||
<SendouTabList>
|
||||
<SendouTab
|
||||
id="players"
|
||||
icon={<Swords />}
|
||||
number={playerMembers.length}
|
||||
>
|
||||
{t("team:roster.sections.players")}
|
||||
</SendouTab>
|
||||
<SendouTab
|
||||
id="other"
|
||||
icon={<Wrench />}
|
||||
number={otherMembers.length}
|
||||
>
|
||||
{t("team:roster.sections.other")}
|
||||
</SendouTab>
|
||||
</SendouTabList>
|
||||
<SendouTabPanel id="players">
|
||||
<div className="stack lg">{playerMembers.map(renderMember)}</div>
|
||||
</SendouTabPanel>
|
||||
<SendouTabPanel id="other">
|
||||
<div className="stack lg">{otherMembers.map(renderMember)}</div>
|
||||
</SendouTabPanel>
|
||||
</SendouTabs>
|
||||
) : (
|
||||
<div className="stack lg">{members.map(renderMember)}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -274,7 +327,14 @@ function MemberRow({
|
||||
className={styles.member}
|
||||
data-testid={member.isOwner ? `member-owner-${member.id}` : undefined}
|
||||
>
|
||||
{member.role ? (
|
||||
{member.customRole ? (
|
||||
<span
|
||||
className={styles.memberRole}
|
||||
data-testid={`member-row-role-${number}`}
|
||||
>
|
||||
{member.customRole}
|
||||
</span>
|
||||
) : member.role ? (
|
||||
<span
|
||||
className={styles.memberRole}
|
||||
data-testid={`member-row-role-${number}`}
|
||||
@@ -334,7 +394,9 @@ function MobileMemberCard({
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
{member.role ? (
|
||||
{member.customRole ? (
|
||||
<span className={styles.memberRoleMobile}>{member.customRole}</span>
|
||||
) : member.role ? (
|
||||
<span className={styles.memberRoleMobile}>
|
||||
{t(`team:roles.${member.role}`)}
|
||||
</span>
|
||||
|
||||
@@ -1,26 +1,23 @@
|
||||
import clsx from "clsx";
|
||||
import { Check, Clipboard, Trash } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { Check, Clipboard } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { MetaFunction } from "react-router";
|
||||
import { Form, useFetcher, useLoaderData } from "react-router";
|
||||
import { Form, useLoaderData } from "react-router";
|
||||
import { Alert } from "~/components/Alert";
|
||||
import { Avatar } from "~/components/Avatar";
|
||||
import { SendouButton } from "~/components/elements/Button";
|
||||
import { SendouPopover } from "~/components/elements/Popover";
|
||||
import { SendouSwitch } from "~/components/elements/Switch";
|
||||
import { FormWithConfirm } from "~/components/FormWithConfirm";
|
||||
import { Main } from "~/components/Main";
|
||||
import { SubmitButton } from "~/components/SubmitButton";
|
||||
import { useUser } from "~/features/auth/core/user";
|
||||
import { TeamGoBackButton } from "~/features/team/components/TeamGoBackButton";
|
||||
import { SendouForm } from "~/form/SendouForm";
|
||||
import type { ArrayItemRenderContext } from "~/form/types";
|
||||
import { useCopyToClipboard } from "~/hooks/useCopyToClipboard";
|
||||
import { metaTags } from "~/utils/remix";
|
||||
import { joinTeamPage } from "~/utils/urls";
|
||||
import { action } from "../actions/t.$customUrl.roster.server";
|
||||
import { loader } from "../loaders/t.$customUrl.roster.server";
|
||||
import type * as TeamRepository from "../TeamRepository.server";
|
||||
import styles from "../team.module.css";
|
||||
import { TEAM_MEMBER_ROLES } from "../team-constants";
|
||||
import { CUSTOM_ROLE_VALUE, updateRosterSchema } from "../team-schemas";
|
||||
import { isTeamFull } from "../team-utils";
|
||||
|
||||
export { action, loader };
|
||||
@@ -36,7 +33,7 @@ export default function ManageTeamRosterPage() {
|
||||
const { t } = useTranslation(["team"]);
|
||||
|
||||
return (
|
||||
<Main className="stack lg">
|
||||
<Main halfWidth className="stack lg">
|
||||
<TeamGoBackButton />
|
||||
<InviteCodeSection />
|
||||
<MemberActions />
|
||||
@@ -105,129 +102,72 @@ function InviteCodeSection() {
|
||||
}
|
||||
|
||||
function MemberActions() {
|
||||
const { t } = useTranslation(["team"]);
|
||||
const { t } = useTranslation(["common", "team"]);
|
||||
const { team } = useLoaderData<typeof loader>();
|
||||
const user = useUser();
|
||||
|
||||
const isProtectedMember = (member: { id: number; isOwner: number }) =>
|
||||
Boolean(member.isOwner) || member.id === user!.id;
|
||||
|
||||
return (
|
||||
<div className="stack md">
|
||||
<h2 className="text-lg">{t("team:roster.members.header")}</h2>
|
||||
|
||||
<div className={styles.rosterMembers}>
|
||||
{team.members.map((member, i) => (
|
||||
<MemberRow key={member.id} member={member} number={i} />
|
||||
))}
|
||||
</div>
|
||||
<SendouForm
|
||||
fullWidth
|
||||
schema={updateRosterSchema}
|
||||
submitButtonText={t("common:actions.save")}
|
||||
defaultValues={{
|
||||
members: team.members.map((member) => ({
|
||||
userId: member.id,
|
||||
role: member.customRole ? CUSTOM_ROLE_VALUE : (member.role ?? null),
|
||||
customRole: member.customRole ?? null,
|
||||
roleType: member.roleType ?? null,
|
||||
isManager: Boolean(member.isManager),
|
||||
})),
|
||||
}}
|
||||
>
|
||||
{({ FormField }) => (
|
||||
<FormField
|
||||
name="members"
|
||||
canRemoveItem={(item) => {
|
||||
const member = team.members.find(
|
||||
(m) => m.id === (item as { userId: number }).userId,
|
||||
);
|
||||
return Boolean(member) && !isProtectedMember(member!);
|
||||
}}
|
||||
>
|
||||
{({ index, itemName, values }: ArrayItemRenderContext) => {
|
||||
const member = team.members.find(
|
||||
(m) => m.id === (values as { userId: number }).userId,
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="stack md-plus"
|
||||
data-testid={`member-row-${index}`}
|
||||
>
|
||||
<div className="stack horizontal sm items-center text-sm font-bold mb-2">
|
||||
{member ? <Avatar size="xs" user={member} /> : null}
|
||||
{member?.username}
|
||||
</div>
|
||||
<FormField name={`${itemName}.role`} />
|
||||
{(values as { role: string | null }).role ===
|
||||
CUSTOM_ROLE_VALUE ? (
|
||||
<>
|
||||
<FormField name={`${itemName}.customRole`} />
|
||||
<FormField name={`${itemName}.roleType`} />
|
||||
</>
|
||||
) : null}
|
||||
{member && !isProtectedMember(member) ? (
|
||||
<FormField name={`${itemName}.isManager`} />
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</FormField>
|
||||
)}
|
||||
</SendouForm>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const NO_ROLE = "NO_ROLE";
|
||||
function MemberRow({
|
||||
member,
|
||||
number,
|
||||
}: {
|
||||
member: TeamRepository.findByCustomUrl["members"][number];
|
||||
number: number;
|
||||
}) {
|
||||
const { team } = useLoaderData<typeof loader>();
|
||||
const { t } = useTranslation(["team"]);
|
||||
const user = useUser();
|
||||
|
||||
const roleFetcher = useFetcher();
|
||||
const editorFetcher = useFetcher();
|
||||
|
||||
const isSelf = user!.id === member.id;
|
||||
const role = team.members.find((m) => m.id === member.id)?.role ?? NO_ROLE;
|
||||
|
||||
const isThisMemberOwner = Boolean(
|
||||
team.members.find((m) => m.id === member.id)?.isOwner,
|
||||
);
|
||||
const isThisMemberManager = Boolean(
|
||||
team.members.find((m) => m.id === member.id)?.isManager,
|
||||
);
|
||||
|
||||
const editorIsBeingAdded =
|
||||
editorFetcher.formData?.get("_action") === "ADD_MANAGER";
|
||||
const editorIsBeingRemoved =
|
||||
editorFetcher.formData?.get("_action") === "REMOVE_MANAGER";
|
||||
|
||||
return (
|
||||
<React.Fragment key={member.id}>
|
||||
<div className={styles.rosterMember} data-testid={`member-row-${number}`}>
|
||||
{member.username}
|
||||
</div>
|
||||
<div>
|
||||
<select
|
||||
defaultValue={role}
|
||||
onChange={(e) =>
|
||||
roleFetcher.submit(
|
||||
{
|
||||
_action: "UPDATE_MEMBER_ROLE",
|
||||
userId: String(member.id),
|
||||
role: e.target.value === NO_ROLE ? "" : e.target.value,
|
||||
},
|
||||
{ method: "post" },
|
||||
)
|
||||
}
|
||||
disabled={roleFetcher.state !== "idle"}
|
||||
data-testid={`role-select-${number}`}
|
||||
>
|
||||
<option value={NO_ROLE}>No role</option>
|
||||
{TEAM_MEMBER_ROLES.map((role) => {
|
||||
return (
|
||||
<option key={role} value={role}>
|
||||
{t(`team:roles.${role}`)}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
<div className={clsx({ invisible: isThisMemberOwner || isSelf })}>
|
||||
<SendouSwitch
|
||||
onChange={(isSelected) =>
|
||||
editorFetcher.submit(
|
||||
{
|
||||
_action: isSelected ? "ADD_MANAGER" : "REMOVE_MANAGER",
|
||||
userId: String(member.id),
|
||||
},
|
||||
{ method: "post" },
|
||||
)
|
||||
}
|
||||
isSelected={
|
||||
editorIsBeingAdded
|
||||
? true
|
||||
: editorIsBeingRemoved
|
||||
? false
|
||||
: isThisMemberManager
|
||||
}
|
||||
data-testid="editor-switch"
|
||||
>
|
||||
{t("team:editor.label")}
|
||||
</SendouSwitch>
|
||||
</div>
|
||||
<div className={clsx({ invisible: isThisMemberOwner || isSelf })}>
|
||||
<FormWithConfirm
|
||||
dialogHeading={t("team:kick.header", {
|
||||
teamName: team.name,
|
||||
user: member.username,
|
||||
})}
|
||||
submitButtonText={t("team:actionButtons.kick")}
|
||||
fields={[
|
||||
["_action", "DELETE_MEMBER"],
|
||||
["userId", member.id],
|
||||
]}
|
||||
>
|
||||
<SendouButton
|
||||
size="small"
|
||||
variant="destructive"
|
||||
icon={<Trash />}
|
||||
data-testid={!isSelf ? "kick-button" : undefined}
|
||||
>
|
||||
{t("team:actionButtons.kick")}
|
||||
</SendouButton>
|
||||
</FormWithConfirm>
|
||||
</div>
|
||||
<hr className={styles.rosterSeparator} />
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -27,3 +27,5 @@ export const TEAM_MEMBER_ROLES = [
|
||||
/** Roles that are not part of a team's active competitive lineup. Excluded when sourcing a roster (e.g. prefilling tournament registration or a scrim post). */
|
||||
export const NON_PLAYER_TEAM_ROLES: readonly (typeof TEAM_MEMBER_ROLES)[number][] =
|
||||
["CHEERLEADER", "COACH", "SUB"];
|
||||
|
||||
export const CUSTOM_ROLE_MAX_LENGTH = 32;
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { z } from "zod";
|
||||
import { mySlugify } from "~/utils/urls";
|
||||
import { _action, id, themeInputSchema } from "~/utils/zod";
|
||||
import { _action, themeInputSchema } from "~/utils/zod";
|
||||
import * as TeamRepository from "./TeamRepository.server";
|
||||
import { TEAM_MEMBER_ROLES } from "./team-constants";
|
||||
import { createTeamSchema, editTeamFormSchema } from "./team-schemas";
|
||||
import {
|
||||
createTeamSchema,
|
||||
editTeamFormSchema,
|
||||
updateRosterSchema,
|
||||
} from "./team-schemas";
|
||||
|
||||
export const createTeamSchemaServer = z.object({
|
||||
...createTeamSchema.shape,
|
||||
@@ -47,24 +50,8 @@ export const editTeamActionSchema = z.union([
|
||||
]);
|
||||
|
||||
export const manageRosterSchema = z.union([
|
||||
updateRosterSchema,
|
||||
z.object({
|
||||
_action: _action("RESET_INVITE_LINK"),
|
||||
}),
|
||||
z.object({
|
||||
_action: _action("DELETE_MEMBER"),
|
||||
userId: id,
|
||||
}),
|
||||
z.object({
|
||||
_action: _action("ADD_MANAGER"),
|
||||
userId: id,
|
||||
}),
|
||||
z.object({
|
||||
_action: _action("REMOVE_MANAGER"),
|
||||
userId: id,
|
||||
}),
|
||||
z.object({
|
||||
_action: _action("UPDATE_MEMBER_ROLE"),
|
||||
userId: id,
|
||||
role: z.union([z.enum(TEAM_MEMBER_ROLES), z.literal("")]),
|
||||
}),
|
||||
]);
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
import { z } from "zod";
|
||||
import {
|
||||
array,
|
||||
fieldset,
|
||||
idConstant,
|
||||
image,
|
||||
selectOptional,
|
||||
stringConstant,
|
||||
textAreaOptional,
|
||||
textFieldOptional,
|
||||
textFieldRequired,
|
||||
toggle,
|
||||
} from "~/form/fields";
|
||||
import { mySlugify } from "~/utils/urls";
|
||||
import { TEAM } from "./team-constants";
|
||||
import {
|
||||
CUSTOM_ROLE_MAX_LENGTH,
|
||||
TEAM,
|
||||
TEAM_MEMBER_ROLES,
|
||||
} from "./team-constants";
|
||||
|
||||
const teamNameValidate = {
|
||||
func: (teamName: string) =>
|
||||
@@ -50,3 +59,74 @@ export const editTeamFormSchema = z.object({
|
||||
logo: image({ label: "labels.logo" }),
|
||||
banner: image({ label: "labels.banner", dimensions: "thick-banner" }),
|
||||
});
|
||||
|
||||
/** Sentinel `role` value selected to switch a member to a free-text custom role. Never stored. */
|
||||
export const CUSTOM_ROLE_VALUE = "CUSTOM";
|
||||
|
||||
export const updateRosterSchema = z
|
||||
.object({
|
||||
_action: stringConstant("UPDATE_ROSTER"),
|
||||
members: array({
|
||||
max: TEAM.MAX_MEMBER_COUNT,
|
||||
addable: false,
|
||||
field: fieldset({
|
||||
fields: z.object({
|
||||
userId: idConstant(),
|
||||
role: selectOptional({
|
||||
label: "labels.teamMemberRole",
|
||||
items: [
|
||||
...TEAM_MEMBER_ROLES.map((role) => ({
|
||||
value: role,
|
||||
label: `options.teamMemberRole.${role}` as const,
|
||||
})),
|
||||
{
|
||||
value: CUSTOM_ROLE_VALUE,
|
||||
label: "options.teamMemberRole.CUSTOM" as const,
|
||||
},
|
||||
],
|
||||
}),
|
||||
customRole: textFieldOptional({
|
||||
label: "labels.teamMemberCustomRole",
|
||||
maxLength: CUSTOM_ROLE_MAX_LENGTH,
|
||||
}),
|
||||
roleType: selectOptional({
|
||||
label: "labels.teamMemberRoleType",
|
||||
items: [
|
||||
{ value: "PLAYER", label: "options.teamMemberRoleType.PLAYER" },
|
||||
{ value: "OTHER", label: "options.teamMemberRoleType.OTHER" },
|
||||
],
|
||||
}),
|
||||
isManager: toggle({ label: "labels.teamEditor" }),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
for (const [index, member] of data.members.entries()) {
|
||||
const isCustom = member.role === CUSTOM_ROLE_VALUE;
|
||||
|
||||
if (isCustom && !member.customRole) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["members", index, "customRole"],
|
||||
message: "forms:errors.customRoleRequired",
|
||||
});
|
||||
}
|
||||
|
||||
if (isCustom && !member.roleType) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["members", index, "roleType"],
|
||||
message: "forms:errors.customRoleTypeRequired",
|
||||
});
|
||||
}
|
||||
|
||||
if (!isCustom && member.customRole) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["members", index, "customRole"],
|
||||
message: "forms:errors.customRoleOnlyWhenCustom",
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Tables } from "~/db/tables";
|
||||
import type { MemberRole, MemberRoleType, Tables } from "~/db/tables";
|
||||
import type * as TeamRepository from "./TeamRepository.server";
|
||||
import { TEAM } from "./team-constants";
|
||||
import { NON_PLAYER_TEAM_ROLES, TEAM } from "./team-constants";
|
||||
|
||||
export function isTeamOwner({
|
||||
team,
|
||||
@@ -74,6 +74,20 @@ export function resolveNewOwner(
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves how a team member's role should be classified. For custom roles the explicit
|
||||
* `roleType` is authoritative; for predefined roles it is derived from {@link NON_PLAYER_TEAM_ROLES}.
|
||||
* Returns `null` when the member has no role at all (treated as a player by callers).
|
||||
*/
|
||||
export function getMemberRoleType(member: {
|
||||
role: MemberRole | null;
|
||||
roleType: MemberRoleType | null;
|
||||
}): MemberRoleType | null {
|
||||
if (member.roleType) return member.roleType;
|
||||
if (!member.role) return null;
|
||||
return NON_PLAYER_TEAM_ROLES.includes(member.role) ? "OTHER" : "PLAYER";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of participant IDs who are considered "substitutes" for a given tournament result,
|
||||
* based on the team's member history and the result's participants.
|
||||
|
||||
@@ -228,25 +228,6 @@
|
||||
margin-block-start: var(--s-2);
|
||||
}
|
||||
|
||||
.rosterMembers {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: var(--s-4);
|
||||
place-items: center;
|
||||
max-width: max-content;
|
||||
}
|
||||
|
||||
.rosterMember {
|
||||
justify-self: flex-start;
|
||||
font-weight: var(--weight-bold);
|
||||
font-size: var(--font-sm);
|
||||
}
|
||||
|
||||
.rosterSeparator {
|
||||
grid-column: 1 / 3;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.inviteContainer {
|
||||
margin-block-start: var(--s-16);
|
||||
display: flex;
|
||||
@@ -298,14 +279,6 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
.rosterMembers {
|
||||
grid-template-columns: 1fr 1fr max-content max-content;
|
||||
}
|
||||
|
||||
.rosterSeparator {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.bannerPlaceholder {
|
||||
height: 12rem;
|
||||
}
|
||||
|
||||
@@ -155,6 +155,7 @@ function TeamRow({
|
||||
<tr
|
||||
className={clsx({ [styles.droppedOut]: team.droppedOut })}
|
||||
data-testid="team-row"
|
||||
data-team-id={team.id}
|
||||
>
|
||||
<td>
|
||||
<div className="stack horizontal sm items-center">
|
||||
|
||||
@@ -198,6 +198,7 @@ export async function findProfileByIdentifier(
|
||||
"Team.id",
|
||||
"TeamMemberWithSecondary.isMainTeam",
|
||||
"TeamMemberWithSecondary.role as userTeamRole",
|
||||
"TeamMemberWithSecondary.customRole as userTeamCustomRole",
|
||||
concatUserSubmittedImagePrefix(eb.ref("UserSubmittedImage.url")).as(
|
||||
"avatarUrl",
|
||||
),
|
||||
|
||||
@@ -249,7 +249,11 @@ function TeamInfo() {
|
||||
) : null}
|
||||
<div>
|
||||
{data.user.team.name}
|
||||
{data.user.team.userTeamRole ? (
|
||||
{data.user.team.userTeamCustomRole ? (
|
||||
<div className="text-xxs text-lighter font-bold">
|
||||
{data.user.team.userTeamCustomRole}
|
||||
</div>
|
||||
) : data.user.team.userTeamRole ? (
|
||||
<div className="text-xxs text-lighter font-bold">
|
||||
{t(`team:roles.${data.user.team.userTeamRole}`)}
|
||||
</div>
|
||||
@@ -310,7 +314,11 @@ function SecondaryTeamsPopover() {
|
||||
) : null}
|
||||
{team.name}
|
||||
</Link>
|
||||
{team.userTeamRole ? (
|
||||
{team.userTeamCustomRole ? (
|
||||
<div className="text-xxs text-lighter font-bold">
|
||||
{team.userTeamCustomRole}
|
||||
</div>
|
||||
) : team.userTeamRole ? (
|
||||
<div className="text-xxs text-lighter font-bold">
|
||||
{t(`team:roles.${team.userTeamRole}`)}
|
||||
</div>
|
||||
|
||||
@@ -61,6 +61,8 @@ interface FormFieldProps {
|
||||
| ((props: ArrayItemRenderContext) => React.ReactNode);
|
||||
/** Field-specific options */
|
||||
options?: unknown;
|
||||
/** For `array` fields: hide the remove button for items where this returns false. */
|
||||
canRemoveItem?: (itemValue: unknown, index: number) => boolean;
|
||||
}
|
||||
|
||||
export function FormField({
|
||||
@@ -71,6 +73,7 @@ export function FormField({
|
||||
field,
|
||||
children,
|
||||
options,
|
||||
canRemoveItem,
|
||||
}: FormFieldProps) {
|
||||
const context = useOptionalFormFieldContext();
|
||||
|
||||
@@ -379,6 +382,7 @@ export function FormField({
|
||||
onChange={handleChange as (v: unknown[]) => void}
|
||||
isObjectArray={isObjectArray}
|
||||
itemInitialValue={itemInitialValue}
|
||||
canRemoveItem={canRemoveItem}
|
||||
renderItem={(idx, itemName) => {
|
||||
if (hasCustomRender && isObjectArray) {
|
||||
const arrayValue = value as Record<string, unknown>[];
|
||||
|
||||
@@ -16,6 +16,8 @@ type ArrayFormFieldProps = Omit<FormFieldProps<"array">, "field"> & {
|
||||
isObjectArray?: boolean;
|
||||
sortable?: boolean;
|
||||
itemInitialValue?: unknown;
|
||||
addable?: boolean;
|
||||
canRemoveItem?: (itemValue: unknown, index: number) => boolean;
|
||||
};
|
||||
|
||||
export function ArrayFormField({
|
||||
@@ -31,6 +33,8 @@ export function ArrayFormField({
|
||||
isObjectArray,
|
||||
sortable,
|
||||
itemInitialValue,
|
||||
addable = true,
|
||||
canRemoveItem,
|
||||
}: ArrayFormFieldProps) {
|
||||
const { t } = useTranslation(["common"]);
|
||||
const { translatedLabel, translatedBottomText, translatedError } =
|
||||
@@ -89,7 +93,9 @@ export function ArrayFormField({
|
||||
// so it shouldn't offer a remove button (you can't go below one visible row
|
||||
// anyway). A lone edited row stays removable so the only item can be cleared.
|
||||
const canRemoveAt = (index: number) =>
|
||||
count > min && (count > minVisible || !isPristineItem(value[index]));
|
||||
(canRemoveItem ? canRemoveItem(value[index], index) : true) &&
|
||||
count > min &&
|
||||
(count > minVisible || !isPristineItem(value[index]));
|
||||
|
||||
const handleRemoveAt = (index: number) => {
|
||||
const next = value.filter((_, i) => i !== index);
|
||||
@@ -147,16 +153,18 @@ export function ArrayFormField({
|
||||
{translatedBottomText && !translatedError ? (
|
||||
<FormMessage type="info">{translatedBottomText}</FormMessage>
|
||||
) : null}
|
||||
<SendouButton
|
||||
size="small"
|
||||
variant="outlined"
|
||||
icon={<Plus />}
|
||||
onPress={handleAdd}
|
||||
isDisabled={count >= max}
|
||||
className="m-0-auto"
|
||||
>
|
||||
{t("common:actions.add")}
|
||||
</SendouButton>
|
||||
{addable ? (
|
||||
<SendouButton
|
||||
size="small"
|
||||
variant="outlined"
|
||||
icon={<Plus />}
|
||||
onPress={handleAdd}
|
||||
isDisabled={count >= max}
|
||||
className="m-0-auto"
|
||||
>
|
||||
{t("common:actions.add")}
|
||||
</SendouButton>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -129,6 +129,8 @@ export interface FormFieldArray<T extends string, S extends z.ZodType>
|
||||
min?: number;
|
||||
max: number;
|
||||
field: S;
|
||||
/** When false, the "Add" button is hidden (the array can only be edited/shrunk, not grown). Defaults to true. */
|
||||
addable?: boolean;
|
||||
}
|
||||
|
||||
interface FormFieldTimeRange<T extends string> extends FormFieldBase<T> {
|
||||
@@ -269,6 +271,7 @@ export type TypedFormFieldProps<
|
||||
label?: string;
|
||||
disabled?: boolean;
|
||||
maxCount?: number;
|
||||
canRemoveItem?: (itemValue: unknown, index: number) => boolean;
|
||||
children?:
|
||||
| ((props: FormFieldChildrenProps) => React.ReactNode)
|
||||
| ((props: ArrayItemRenderContext) => React.ReactNode);
|
||||
@@ -285,6 +288,7 @@ export type FlexibleFormFieldProps = {
|
||||
label?: string;
|
||||
disabled?: boolean;
|
||||
maxCount?: number;
|
||||
canRemoveItem?: (itemValue: unknown, index: number) => boolean;
|
||||
children?:
|
||||
| ((props: FormFieldChildrenProps) => React.ReactNode)
|
||||
| ((props: ArrayItemRenderContext) => React.ReactNode);
|
||||
|
||||
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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -73,18 +73,46 @@ test.describe("Team page", () => {
|
||||
|
||||
await page.getByTestId("manage-roster-button").click();
|
||||
|
||||
await page.getByTestId("role-select-0").selectOption("SUPPORT");
|
||||
await page
|
||||
.getByTestId("member-row-0")
|
||||
.locator("select")
|
||||
.selectOption("SUPPORT");
|
||||
|
||||
await page.getByTestId("member-row-3").isVisible();
|
||||
await page.getByTestId("kick-button").last().click();
|
||||
await modalClickConfirmButton(page);
|
||||
await expect(page.getByTestId("member-row-3")).toBeVisible();
|
||||
await page
|
||||
.locator("fieldset:has([data-testid='member-row-3'])")
|
||||
.getByRole("button", { name: "Remove item" })
|
||||
.click();
|
||||
await isNotVisible(page.getByTestId("member-row-3"));
|
||||
|
||||
await submit(page);
|
||||
|
||||
await navigate({ page, url: teamPage("alliance-rogue") });
|
||||
|
||||
await expect(page.getByTestId("member-row-role-0")).toHaveText("Support");
|
||||
});
|
||||
|
||||
test("sets a custom role for a member", async ({ page }) => {
|
||||
await seed(page);
|
||||
await impersonate(page, ADMIN_ID);
|
||||
await navigate({ page, url: teamPage("alliance-rogue") });
|
||||
|
||||
await page.getByTestId("manage-roster-button").click();
|
||||
|
||||
const memberRow = page.getByTestId("member-row-1");
|
||||
await memberRow.locator("select").first().selectOption("CUSTOM");
|
||||
await memberRow.getByRole("textbox").fill("Strategist");
|
||||
await memberRow.locator("select").nth(1).selectOption("OTHER");
|
||||
|
||||
await submit(page);
|
||||
|
||||
await navigate({ page, url: teamPage("alliance-rogue") });
|
||||
|
||||
// custom role is classified as "OTHER" so it lives under the "Other" tab
|
||||
await page.getByRole("tab", { name: /Other/ }).click();
|
||||
await expect(page.getByText("Strategist").first()).toBeVisible();
|
||||
});
|
||||
|
||||
test("deletes team", async ({ page }) => {
|
||||
await seed(page);
|
||||
await impersonate(page, ADMIN_ID);
|
||||
@@ -172,9 +200,8 @@ test.describe("Team page", () => {
|
||||
|
||||
await page.getByTestId("manage-roster-button").click();
|
||||
|
||||
await waitForPOSTResponse(page, async () => {
|
||||
await page.getByLabel("Editor").first().click({ force: true });
|
||||
});
|
||||
await page.getByLabel("Editor").first().click({ force: true });
|
||||
await submit(page);
|
||||
|
||||
await impersonate(page, NZAP_TEST_ID);
|
||||
await navigate({ page, url: editTeamPage("alliance-rogue") });
|
||||
|
||||
@@ -303,11 +303,12 @@ test.describe("Tournament bracket", () => {
|
||||
url: tournamentAdminPage(tournamentId),
|
||||
});
|
||||
|
||||
// check team 216 in to the underground bracket (after the start only
|
||||
// checked in teams are listed so the row index is not stable)
|
||||
// check team 216 in to the underground bracket — match on the stable team
|
||||
// id rather than the seeded (faker-generated) team name or the row index,
|
||||
// neither of which is stable after the start
|
||||
await adminTeamRowAction(
|
||||
page,
|
||||
page.getByTestId("team-row").filter({ hasText: "Protective Wet Floor" }),
|
||||
page.locator('[data-testid="team-row"][data-team-id="216"]'),
|
||||
"Check in (Underground bracket)",
|
||||
);
|
||||
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
"labels.banner": "",
|
||||
"labels.tag": "",
|
||||
"labels.teamBsky": "",
|
||||
"labels.teamEditor": "",
|
||||
"labels.teamMemberRole": "",
|
||||
"labels.teamMemberCustomRole": "",
|
||||
"labels.teamMemberRoleType": "",
|
||||
"labels.clockFormat": "",
|
||||
"labels.disableBuildAbilitySorting": "",
|
||||
"labels.disallowScrimPickupsFromUntrusted": "",
|
||||
@@ -23,6 +27,21 @@
|
||||
"options.clockFormat.auto": "",
|
||||
"options.clockFormat.24h": "",
|
||||
"options.clockFormat.12h": "",
|
||||
"options.teamMemberRole.CAPTAIN": "Kaptajn",
|
||||
"options.teamMemberRole.CO_CAPTAIN": "",
|
||||
"options.teamMemberRole.FRONTLINE": "Frontlinje",
|
||||
"options.teamMemberRole.SLAYER": "",
|
||||
"options.teamMemberRole.SKIRMISHER": "",
|
||||
"options.teamMemberRole.SUPPORT": "Støtte",
|
||||
"options.teamMemberRole.MIDLINE": "Midlinje",
|
||||
"options.teamMemberRole.BACKLINE": "Backline",
|
||||
"options.teamMemberRole.FLEX": "Fleksibel",
|
||||
"options.teamMemberRole.SUB": "Vikar",
|
||||
"options.teamMemberRole.COACH": "Træner",
|
||||
"options.teamMemberRole.CHEERLEADER": "",
|
||||
"options.teamMemberRole.CUSTOM": "",
|
||||
"options.teamMemberRoleType.PLAYER": "",
|
||||
"options.teamMemberRoleType.OTHER": "",
|
||||
"errors.required": "",
|
||||
"errors.minLength": "",
|
||||
"errors.maxLength": "",
|
||||
@@ -32,6 +51,9 @@
|
||||
"errors.duplicateName": "Holdnavnet er taget af et andet hold",
|
||||
"errors.duplicateOrgName": "",
|
||||
"errors.noOnlySpecialCharacters": "",
|
||||
"errors.customRoleRequired": "",
|
||||
"errors.customRoleTypeRequired": "",
|
||||
"errors.customRoleOnlyWhenCustom": "",
|
||||
"labels.weaponPool": "Våbenpulje",
|
||||
"placeholders.weaponPoolFull": "",
|
||||
"labels.voiceChat": "",
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"leaveTeam.header.newOwner": "",
|
||||
"actionButtons.leaveTeam.confirm": "Forlad hold",
|
||||
"actionButtons.editTeam": "Rediger hold",
|
||||
"actionButtons.manageRoster": "Administrer hold",
|
||||
"actionButtons.manageRoster": "",
|
||||
"actionButtons.deleteTeam": "Slet hold",
|
||||
"actionButtons.deleteTeam.profilePicture": "",
|
||||
"actionButtons.deleteTeam.banner": "",
|
||||
@@ -49,6 +49,8 @@
|
||||
"roster.teamFull": "Holdet kan ikke få flere medlemmer",
|
||||
"roster.inviteLink.header": "Del invitationslinket for at tilføje medlemmer",
|
||||
"roster.members.header": "Medlemmer",
|
||||
"roster.sections.players": "",
|
||||
"roster.sections.other": "",
|
||||
"validation.SHORT_CODE": "Invitationskoden har ikke den korrekte længde. Har du kopieret hele URL-adressen? ",
|
||||
"validation.TEAM_FULL": "Holdet, som du prøver at blive medlem af, er fuldt",
|
||||
"validation.INVITE_CODE_WRONG": "Ukorrekt invitationskode",
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
"labels.banner": "",
|
||||
"labels.tag": "",
|
||||
"labels.teamBsky": "",
|
||||
"labels.teamEditor": "",
|
||||
"labels.teamMemberRole": "",
|
||||
"labels.teamMemberCustomRole": "",
|
||||
"labels.teamMemberRoleType": "",
|
||||
"labels.clockFormat": "",
|
||||
"labels.disableBuildAbilitySorting": "",
|
||||
"labels.disallowScrimPickupsFromUntrusted": "",
|
||||
@@ -23,6 +27,21 @@
|
||||
"options.clockFormat.auto": "",
|
||||
"options.clockFormat.24h": "",
|
||||
"options.clockFormat.12h": "",
|
||||
"options.teamMemberRole.CAPTAIN": "Kapitän",
|
||||
"options.teamMemberRole.CO_CAPTAIN": "",
|
||||
"options.teamMemberRole.FRONTLINE": "Frontline",
|
||||
"options.teamMemberRole.SLAYER": "",
|
||||
"options.teamMemberRole.SKIRMISHER": "",
|
||||
"options.teamMemberRole.SUPPORT": "Support",
|
||||
"options.teamMemberRole.MIDLINE": "Midline",
|
||||
"options.teamMemberRole.BACKLINE": "Backline",
|
||||
"options.teamMemberRole.FLEX": "",
|
||||
"options.teamMemberRole.SUB": "",
|
||||
"options.teamMemberRole.COACH": "Coach",
|
||||
"options.teamMemberRole.CHEERLEADER": "",
|
||||
"options.teamMemberRole.CUSTOM": "",
|
||||
"options.teamMemberRoleType.PLAYER": "",
|
||||
"options.teamMemberRoleType.OTHER": "",
|
||||
"errors.required": "",
|
||||
"errors.minLength": "",
|
||||
"errors.maxLength": "",
|
||||
@@ -32,6 +51,9 @@
|
||||
"errors.duplicateName": "Es gibt bereits ein Team mit diesem Namen",
|
||||
"errors.duplicateOrgName": "",
|
||||
"errors.noOnlySpecialCharacters": "",
|
||||
"errors.customRoleRequired": "",
|
||||
"errors.customRoleTypeRequired": "",
|
||||
"errors.customRoleOnlyWhenCustom": "",
|
||||
"labels.weaponPool": "Waffenpool",
|
||||
"placeholders.weaponPoolFull": "",
|
||||
"labels.voiceChat": "",
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"leaveTeam.header.newOwner": "",
|
||||
"actionButtons.leaveTeam.confirm": "Verlassen",
|
||||
"actionButtons.editTeam": "Team bearbeiten",
|
||||
"actionButtons.manageRoster": "Aufstellung bearbeiten",
|
||||
"actionButtons.manageRoster": "",
|
||||
"actionButtons.deleteTeam": "Team löschen",
|
||||
"actionButtons.deleteTeam.profilePicture": "",
|
||||
"actionButtons.deleteTeam.banner": "",
|
||||
@@ -49,6 +49,8 @@
|
||||
"roster.teamFull": "Team ist voll",
|
||||
"roster.inviteLink.header": "Teile den Einladungslink, um Mitglieder hinzuzufügen",
|
||||
"roster.members.header": "Mitglieder",
|
||||
"roster.sections.players": "",
|
||||
"roster.sections.other": "",
|
||||
"validation.SHORT_CODE": "Einladungscode hat nicht die richtige Länge. Hast du die vollständige URL kopiert?",
|
||||
"validation.TEAM_FULL": "Das Team, dem du beitreten willst, ist bereits voll.",
|
||||
"validation.INVITE_CODE_WRONG": "Einladungscode ist falsch.",
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
"labels.banner": "Banner",
|
||||
"labels.tag": "Tag",
|
||||
"labels.teamBsky": "Team Bluesky",
|
||||
"labels.teamEditor": "Editor",
|
||||
"labels.teamMemberRole": "Role",
|
||||
"labels.teamMemberCustomRole": "Custom role",
|
||||
"labels.teamMemberRoleType": "Role type",
|
||||
"labels.clockFormat": "Clock format",
|
||||
"labels.disableBuildAbilitySorting": "Builds: Disable automatic ability sorting",
|
||||
"labels.disallowScrimPickupsFromUntrusted": "Disallow scrim pickups from non-friends",
|
||||
@@ -23,6 +27,21 @@
|
||||
"options.clockFormat.auto": "Automatic",
|
||||
"options.clockFormat.24h": "24-hour",
|
||||
"options.clockFormat.12h": "12-hour",
|
||||
"options.teamMemberRole.CAPTAIN": "Captain",
|
||||
"options.teamMemberRole.CO_CAPTAIN": "Co-Captain",
|
||||
"options.teamMemberRole.FRONTLINE": "Frontline",
|
||||
"options.teamMemberRole.SLAYER": "Slayer",
|
||||
"options.teamMemberRole.SKIRMISHER": "Skirmisher",
|
||||
"options.teamMemberRole.SUPPORT": "Support",
|
||||
"options.teamMemberRole.MIDLINE": "Midline",
|
||||
"options.teamMemberRole.BACKLINE": "Backline",
|
||||
"options.teamMemberRole.FLEX": "Flex",
|
||||
"options.teamMemberRole.SUB": "Sub",
|
||||
"options.teamMemberRole.COACH": "Coach",
|
||||
"options.teamMemberRole.CHEERLEADER": "Cheerleader",
|
||||
"options.teamMemberRole.CUSTOM": "Custom...",
|
||||
"options.teamMemberRoleType.PLAYER": "Player",
|
||||
"options.teamMemberRoleType.OTHER": "Other",
|
||||
"errors.required": "This field is required",
|
||||
"errors.minLength": "Must be at least {{min}} characters",
|
||||
"errors.maxLength": "Must be at most {{max}} characters",
|
||||
@@ -32,6 +51,9 @@
|
||||
"errors.duplicateName": "There is already a team with this name",
|
||||
"errors.duplicateOrgName": "There is already an organization with this name",
|
||||
"errors.noOnlySpecialCharacters": "Name can't be only special characters",
|
||||
"errors.customRoleRequired": "Enter a name for the custom role",
|
||||
"errors.customRoleTypeRequired": "Select a role type for the custom role",
|
||||
"errors.customRoleOnlyWhenCustom": "Custom role is only allowed when the role is set to Custom",
|
||||
"labels.weaponPool": "Weapon pool",
|
||||
"placeholders.weaponPoolFull": "Pool full - remove a weapon to add more",
|
||||
"labels.voiceChat": "Can voice chat",
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"leaveTeam.header.newOwner": "Are you sure you want to leave {{teamName}}? New owner will be {{newOwner}}",
|
||||
"actionButtons.leaveTeam.confirm": "Leave",
|
||||
"actionButtons.editTeam": "Edit Team",
|
||||
"actionButtons.manageRoster": "Manage Roster",
|
||||
"actionButtons.manageRoster": "Manage Members",
|
||||
"actionButtons.deleteTeam": "Delete Team",
|
||||
"actionButtons.deleteTeam.profilePicture": "Remove Profile Picture",
|
||||
"actionButtons.deleteTeam.banner": "Remove Banner",
|
||||
@@ -49,6 +49,8 @@
|
||||
"roster.teamFull": "Team is full",
|
||||
"roster.inviteLink.header": "Share invite link to add members",
|
||||
"roster.members.header": "Members",
|
||||
"roster.sections.players": "Roster",
|
||||
"roster.sections.other": "Other",
|
||||
"validation.SHORT_CODE": "Invite code is not the right length. Did you copy the full URL?",
|
||||
"validation.TEAM_FULL": "Team you are trying to join is full.",
|
||||
"validation.INVITE_CODE_WRONG": "Invite code is wrong.",
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
"labels.banner": "",
|
||||
"labels.tag": "Etiqueta",
|
||||
"labels.teamBsky": "Bluesky del equipo",
|
||||
"labels.teamEditor": "Editor",
|
||||
"labels.teamMemberRole": "",
|
||||
"labels.teamMemberCustomRole": "",
|
||||
"labels.teamMemberRoleType": "",
|
||||
"labels.clockFormat": "Formato de hora",
|
||||
"labels.disableBuildAbilitySorting": "Builds: Desactivar orden automático de potenciadores",
|
||||
"labels.disallowScrimPickupsFromUntrusted": "No permitir invitaciones de usuarios no verificados",
|
||||
@@ -23,6 +27,21 @@
|
||||
"options.clockFormat.auto": "Automático",
|
||||
"options.clockFormat.24h": "24 horas",
|
||||
"options.clockFormat.12h": "12 horas",
|
||||
"options.teamMemberRole.CAPTAIN": "Capitán",
|
||||
"options.teamMemberRole.CO_CAPTAIN": "Co-Capitán",
|
||||
"options.teamMemberRole.FRONTLINE": "Delantero",
|
||||
"options.teamMemberRole.SLAYER": "Slayer",
|
||||
"options.teamMemberRole.SKIRMISHER": "Skirmisher",
|
||||
"options.teamMemberRole.SUPPORT": "Soporte",
|
||||
"options.teamMemberRole.MIDLINE": "Centro",
|
||||
"options.teamMemberRole.BACKLINE": "Defensor",
|
||||
"options.teamMemberRole.FLEX": "Flex",
|
||||
"options.teamMemberRole.SUB": "Suplente",
|
||||
"options.teamMemberRole.COACH": "Entrenador",
|
||||
"options.teamMemberRole.CHEERLEADER": "Animador",
|
||||
"options.teamMemberRole.CUSTOM": "",
|
||||
"options.teamMemberRoleType.PLAYER": "",
|
||||
"options.teamMemberRoleType.OTHER": "",
|
||||
"errors.required": "Este campo es obligatorio",
|
||||
"errors.minLength": "Debe tener al menos {{min}} caracteres",
|
||||
"errors.maxLength": "Debe tener como máximo {{max}} caracteres",
|
||||
@@ -32,6 +51,9 @@
|
||||
"errors.duplicateName": "Ya existe un equipo con ese nombre",
|
||||
"errors.duplicateOrgName": "",
|
||||
"errors.noOnlySpecialCharacters": "El nombre no puede ser solo caracteres especiales",
|
||||
"errors.customRoleRequired": "",
|
||||
"errors.customRoleTypeRequired": "",
|
||||
"errors.customRoleOnlyWhenCustom": "",
|
||||
"labels.weaponPool": "Selección de armas",
|
||||
"placeholders.weaponPoolFull": "Selección llena - elimina un arma para añadir más",
|
||||
"labels.voiceChat": "Puede usar chat de voz",
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"leaveTeam.header.newOwner": "¿Seguro que quieres abandonar {{teamName}}? El nuevo propietario será {{newOwner}}",
|
||||
"actionButtons.leaveTeam.confirm": "Abandonar",
|
||||
"actionButtons.editTeam": "Editar Equipo",
|
||||
"actionButtons.manageRoster": "Manejar Miembros",
|
||||
"actionButtons.manageRoster": "",
|
||||
"actionButtons.deleteTeam": "Eliminar Equipo",
|
||||
"actionButtons.deleteTeam.profilePicture": "Eliminar foto de perfil",
|
||||
"actionButtons.deleteTeam.banner": "Eliminar banner",
|
||||
@@ -49,6 +49,8 @@
|
||||
"roster.teamFull": "El equipo está lleno",
|
||||
"roster.inviteLink.header": "Comparte el link de invitación para agregar más miembros",
|
||||
"roster.members.header": "Miembros",
|
||||
"roster.sections.players": "",
|
||||
"roster.sections.other": "",
|
||||
"validation.SHORT_CODE": "El código de invitación no tiene la longitud correcta. ¿Copiaste la URL completa?",
|
||||
"validation.TEAM_FULL": "El equipo al que te intentas unir ya está lleno.",
|
||||
"validation.INVITE_CODE_WRONG": "El código de invitación es incorrecto.",
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
"labels.banner": "",
|
||||
"labels.tag": "",
|
||||
"labels.teamBsky": "",
|
||||
"labels.teamEditor": "",
|
||||
"labels.teamMemberRole": "",
|
||||
"labels.teamMemberCustomRole": "",
|
||||
"labels.teamMemberRoleType": "",
|
||||
"labels.clockFormat": "",
|
||||
"labels.disableBuildAbilitySorting": "",
|
||||
"labels.disallowScrimPickupsFromUntrusted": "",
|
||||
@@ -23,6 +27,21 @@
|
||||
"options.clockFormat.auto": "",
|
||||
"options.clockFormat.24h": "",
|
||||
"options.clockFormat.12h": "",
|
||||
"options.teamMemberRole.CAPTAIN": "Capitán",
|
||||
"options.teamMemberRole.CO_CAPTAIN": "",
|
||||
"options.teamMemberRole.FRONTLINE": "Delantero",
|
||||
"options.teamMemberRole.SLAYER": "",
|
||||
"options.teamMemberRole.SKIRMISHER": "",
|
||||
"options.teamMemberRole.SUPPORT": "Soporte",
|
||||
"options.teamMemberRole.MIDLINE": "Centro",
|
||||
"options.teamMemberRole.BACKLINE": "Defensor",
|
||||
"options.teamMemberRole.FLEX": "Flex",
|
||||
"options.teamMemberRole.SUB": "Suplente",
|
||||
"options.teamMemberRole.COACH": "Entrenador",
|
||||
"options.teamMemberRole.CHEERLEADER": "",
|
||||
"options.teamMemberRole.CUSTOM": "",
|
||||
"options.teamMemberRoleType.PLAYER": "",
|
||||
"options.teamMemberRoleType.OTHER": "",
|
||||
"errors.required": "",
|
||||
"errors.minLength": "",
|
||||
"errors.maxLength": "",
|
||||
@@ -32,6 +51,9 @@
|
||||
"errors.duplicateName": "Ya existe un equipo con ese nombre",
|
||||
"errors.duplicateOrgName": "",
|
||||
"errors.noOnlySpecialCharacters": "",
|
||||
"errors.customRoleRequired": "",
|
||||
"errors.customRoleTypeRequired": "",
|
||||
"errors.customRoleOnlyWhenCustom": "",
|
||||
"labels.weaponPool": "Grupo de armas",
|
||||
"placeholders.weaponPoolFull": "",
|
||||
"labels.voiceChat": "",
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"leaveTeam.header.newOwner": "",
|
||||
"actionButtons.leaveTeam.confirm": "Abandonar",
|
||||
"actionButtons.editTeam": "Editar Equipo",
|
||||
"actionButtons.manageRoster": "Manejar Miembros",
|
||||
"actionButtons.manageRoster": "",
|
||||
"actionButtons.deleteTeam": "Eliminar Equipo",
|
||||
"actionButtons.deleteTeam.profilePicture": "",
|
||||
"actionButtons.deleteTeam.banner": "",
|
||||
@@ -49,6 +49,8 @@
|
||||
"roster.teamFull": "El equipo esta lleno",
|
||||
"roster.inviteLink.header": "Comparte el link de invitación para agregar más miembros",
|
||||
"roster.members.header": "Miembros",
|
||||
"roster.sections.players": "",
|
||||
"roster.sections.other": "",
|
||||
"validation.SHORT_CODE": "El código de invitación no tiene la longitud correcta. ¿Copiaste la URL completa?",
|
||||
"validation.TEAM_FULL": "El equipo al que te intentas unir ya esta lleno.",
|
||||
"validation.INVITE_CODE_WRONG": "El código de invitación es incorrecto.",
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
"labels.banner": "",
|
||||
"labels.tag": "",
|
||||
"labels.teamBsky": "",
|
||||
"labels.teamEditor": "",
|
||||
"labels.teamMemberRole": "",
|
||||
"labels.teamMemberCustomRole": "",
|
||||
"labels.teamMemberRoleType": "",
|
||||
"labels.clockFormat": "",
|
||||
"labels.disableBuildAbilitySorting": "",
|
||||
"labels.disallowScrimPickupsFromUntrusted": "",
|
||||
@@ -23,6 +27,21 @@
|
||||
"options.clockFormat.auto": "",
|
||||
"options.clockFormat.24h": "",
|
||||
"options.clockFormat.12h": "",
|
||||
"options.teamMemberRole.CAPTAIN": "Capitaine",
|
||||
"options.teamMemberRole.CO_CAPTAIN": "",
|
||||
"options.teamMemberRole.FRONTLINE": "Frontline",
|
||||
"options.teamMemberRole.SLAYER": "",
|
||||
"options.teamMemberRole.SKIRMISHER": "",
|
||||
"options.teamMemberRole.SUPPORT": "Support",
|
||||
"options.teamMemberRole.MIDLINE": "Midline",
|
||||
"options.teamMemberRole.BACKLINE": "Backline",
|
||||
"options.teamMemberRole.FLEX": "Flex",
|
||||
"options.teamMemberRole.SUB": "",
|
||||
"options.teamMemberRole.COACH": "Coach",
|
||||
"options.teamMemberRole.CHEERLEADER": "",
|
||||
"options.teamMemberRole.CUSTOM": "",
|
||||
"options.teamMemberRoleType.PLAYER": "",
|
||||
"options.teamMemberRoleType.OTHER": "",
|
||||
"errors.required": "",
|
||||
"errors.minLength": "",
|
||||
"errors.maxLength": "",
|
||||
@@ -32,6 +51,9 @@
|
||||
"errors.duplicateName": "Il y a déjà une équipe avec ce nom",
|
||||
"errors.duplicateOrgName": "",
|
||||
"errors.noOnlySpecialCharacters": "",
|
||||
"errors.customRoleRequired": "",
|
||||
"errors.customRoleTypeRequired": "",
|
||||
"errors.customRoleOnlyWhenCustom": "",
|
||||
"labels.weaponPool": "Armes jouées",
|
||||
"placeholders.weaponPoolFull": "",
|
||||
"labels.voiceChat": "",
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"leaveTeam.header.newOwner": "",
|
||||
"actionButtons.leaveTeam.confirm": "Quitter",
|
||||
"actionButtons.editTeam": "Modifier l'équipe",
|
||||
"actionButtons.manageRoster": "Gerer la composition de l'équipe",
|
||||
"actionButtons.manageRoster": "",
|
||||
"actionButtons.deleteTeam": "Supprimer l'équipe",
|
||||
"actionButtons.deleteTeam.profilePicture": "",
|
||||
"actionButtons.deleteTeam.banner": "",
|
||||
@@ -49,6 +49,8 @@
|
||||
"roster.teamFull": "L'équipe est complète",
|
||||
"roster.inviteLink.header": "Partager le lien d'invitation pour ajouter des membres",
|
||||
"roster.members.header": "Membres",
|
||||
"roster.sections.players": "",
|
||||
"roster.sections.other": "",
|
||||
"validation.SHORT_CODE": "Le code d'invitation n'a pas la bonne longueur. Avez-vous bien copier l'URL au complet ?",
|
||||
"validation.TEAM_FULL": "L'équipe que vous essayez de rejoindre est complète.",
|
||||
"validation.INVITE_CODE_WRONG": "Le code d'invitation est incorrect.",
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
"labels.banner": "",
|
||||
"labels.tag": "",
|
||||
"labels.teamBsky": "Team Bluesky",
|
||||
"labels.teamEditor": "Editer",
|
||||
"labels.teamMemberRole": "",
|
||||
"labels.teamMemberCustomRole": "",
|
||||
"labels.teamMemberRoleType": "",
|
||||
"labels.clockFormat": "",
|
||||
"labels.disableBuildAbilitySorting": "",
|
||||
"labels.disallowScrimPickupsFromUntrusted": "",
|
||||
@@ -23,6 +27,21 @@
|
||||
"options.clockFormat.auto": "",
|
||||
"options.clockFormat.24h": "",
|
||||
"options.clockFormat.12h": "",
|
||||
"options.teamMemberRole.CAPTAIN": "Capitaine",
|
||||
"options.teamMemberRole.CO_CAPTAIN": "Co-capitaine",
|
||||
"options.teamMemberRole.FRONTLINE": "Frontline",
|
||||
"options.teamMemberRole.SLAYER": "Slayer",
|
||||
"options.teamMemberRole.SKIRMISHER": "Skirmisher",
|
||||
"options.teamMemberRole.SUPPORT": "Support",
|
||||
"options.teamMemberRole.MIDLINE": "Midline",
|
||||
"options.teamMemberRole.BACKLINE": "Backline",
|
||||
"options.teamMemberRole.FLEX": "Flex",
|
||||
"options.teamMemberRole.SUB": "Sub",
|
||||
"options.teamMemberRole.COACH": "Coach",
|
||||
"options.teamMemberRole.CHEERLEADER": "Cheerleader",
|
||||
"options.teamMemberRole.CUSTOM": "",
|
||||
"options.teamMemberRoleType.PLAYER": "",
|
||||
"options.teamMemberRoleType.OTHER": "",
|
||||
"errors.required": "",
|
||||
"errors.minLength": "",
|
||||
"errors.maxLength": "",
|
||||
@@ -32,6 +51,9 @@
|
||||
"errors.duplicateName": "Il y a déjà une équipe avec ce nom",
|
||||
"errors.duplicateOrgName": "",
|
||||
"errors.noOnlySpecialCharacters": "",
|
||||
"errors.customRoleRequired": "",
|
||||
"errors.customRoleTypeRequired": "",
|
||||
"errors.customRoleOnlyWhenCustom": "",
|
||||
"labels.weaponPool": "Armes jouées",
|
||||
"placeholders.weaponPoolFull": "",
|
||||
"labels.voiceChat": "",
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"leaveTeam.header.newOwner": "Êtes vous sur de vouloir quitté {{teamName}}? Le nouveau capitaine sera {{newOwner}}",
|
||||
"actionButtons.leaveTeam.confirm": "Quitter",
|
||||
"actionButtons.editTeam": "Modifier l'équipe",
|
||||
"actionButtons.manageRoster": "Gerer la composition de l'équipe",
|
||||
"actionButtons.manageRoster": "",
|
||||
"actionButtons.deleteTeam": "Supprimer l'équipe",
|
||||
"actionButtons.deleteTeam.profilePicture": "",
|
||||
"actionButtons.deleteTeam.banner": "",
|
||||
@@ -49,6 +49,8 @@
|
||||
"roster.teamFull": "L'équipe est complète",
|
||||
"roster.inviteLink.header": "Partager le lien d'invitation pour ajouter des membres",
|
||||
"roster.members.header": "Membres",
|
||||
"roster.sections.players": "",
|
||||
"roster.sections.other": "",
|
||||
"validation.SHORT_CODE": "Le code d'invitation n'a pas la bonne longueur. Avez-vous bien copier l'URL au complet ?",
|
||||
"validation.TEAM_FULL": "L'équipe que vous essayez de rejoindre est complète.",
|
||||
"validation.INVITE_CODE_WRONG": "Le code d'invitation est incorrect.",
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
"labels.banner": "",
|
||||
"labels.tag": "",
|
||||
"labels.teamBsky": "",
|
||||
"labels.teamEditor": "",
|
||||
"labels.teamMemberRole": "",
|
||||
"labels.teamMemberCustomRole": "",
|
||||
"labels.teamMemberRoleType": "",
|
||||
"labels.clockFormat": "",
|
||||
"labels.disableBuildAbilitySorting": "",
|
||||
"labels.disallowScrimPickupsFromUntrusted": "",
|
||||
@@ -23,6 +27,21 @@
|
||||
"options.clockFormat.auto": "",
|
||||
"options.clockFormat.24h": "",
|
||||
"options.clockFormat.12h": "",
|
||||
"options.teamMemberRole.CAPTAIN": "קפטן",
|
||||
"options.teamMemberRole.CO_CAPTAIN": "",
|
||||
"options.teamMemberRole.FRONTLINE": "קו החזית",
|
||||
"options.teamMemberRole.SLAYER": "",
|
||||
"options.teamMemberRole.SKIRMISHER": "",
|
||||
"options.teamMemberRole.SUPPORT": "תמיכה",
|
||||
"options.teamMemberRole.MIDLINE": "קו אמצע",
|
||||
"options.teamMemberRole.BACKLINE": "קו אחורי",
|
||||
"options.teamMemberRole.FLEX": "מעורב",
|
||||
"options.teamMemberRole.SUB": "",
|
||||
"options.teamMemberRole.COACH": "מאמן",
|
||||
"options.teamMemberRole.CHEERLEADER": "",
|
||||
"options.teamMemberRole.CUSTOM": "",
|
||||
"options.teamMemberRoleType.PLAYER": "",
|
||||
"options.teamMemberRoleType.OTHER": "",
|
||||
"errors.required": "",
|
||||
"errors.minLength": "",
|
||||
"errors.maxLength": "",
|
||||
@@ -32,6 +51,9 @@
|
||||
"errors.duplicateName": "יש כבר צוות בשם הזה",
|
||||
"errors.duplicateOrgName": "",
|
||||
"errors.noOnlySpecialCharacters": "",
|
||||
"errors.customRoleRequired": "",
|
||||
"errors.customRoleTypeRequired": "",
|
||||
"errors.customRoleOnlyWhenCustom": "",
|
||||
"labels.weaponPool": "מאגר נשקים",
|
||||
"placeholders.weaponPoolFull": "",
|
||||
"labels.voiceChat": "",
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"leaveTeam.header.newOwner": "",
|
||||
"actionButtons.leaveTeam.confirm": "לעזוב",
|
||||
"actionButtons.editTeam": "עריכת צוות",
|
||||
"actionButtons.manageRoster": "ניהול חברי צוות",
|
||||
"actionButtons.manageRoster": "",
|
||||
"actionButtons.deleteTeam": "מחיקת צוות",
|
||||
"actionButtons.deleteTeam.profilePicture": "",
|
||||
"actionButtons.deleteTeam.banner": "",
|
||||
@@ -49,6 +49,8 @@
|
||||
"roster.teamFull": "הצוות מלא",
|
||||
"roster.inviteLink.header": "שיתוף קישור הזמנה להוספת חברי צוות",
|
||||
"roster.members.header": "חברי צוות",
|
||||
"roster.sections.players": "",
|
||||
"roster.sections.other": "",
|
||||
"validation.SHORT_CODE": "קוד ההזמנה אינו באורך המתאים. העתקתם את כתובת האתר המלאה?",
|
||||
"validation.TEAM_FULL": "הצוות שאתם מנסים להצטרף אליו מלא.",
|
||||
"validation.INVITE_CODE_WRONG": "קוד ההזמנה שגוי.",
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
"labels.banner": "",
|
||||
"labels.tag": "",
|
||||
"labels.teamBsky": "Bluesky del team",
|
||||
"labels.teamEditor": "Editor",
|
||||
"labels.teamMemberRole": "",
|
||||
"labels.teamMemberCustomRole": "",
|
||||
"labels.teamMemberRoleType": "",
|
||||
"labels.clockFormat": "",
|
||||
"labels.disableBuildAbilitySorting": "",
|
||||
"labels.disallowScrimPickupsFromUntrusted": "",
|
||||
@@ -23,6 +27,21 @@
|
||||
"options.clockFormat.auto": "",
|
||||
"options.clockFormat.24h": "",
|
||||
"options.clockFormat.12h": "",
|
||||
"options.teamMemberRole.CAPTAIN": "Capitano",
|
||||
"options.teamMemberRole.CO_CAPTAIN": "Co-Capitano",
|
||||
"options.teamMemberRole.FRONTLINE": "Frontline",
|
||||
"options.teamMemberRole.SLAYER": "Slayer",
|
||||
"options.teamMemberRole.SKIRMISHER": "Skirmisher",
|
||||
"options.teamMemberRole.SUPPORT": "Supporto",
|
||||
"options.teamMemberRole.MIDLINE": "Midline",
|
||||
"options.teamMemberRole.BACKLINE": "Backline",
|
||||
"options.teamMemberRole.FLEX": "Flex",
|
||||
"options.teamMemberRole.SUB": "Sostituto",
|
||||
"options.teamMemberRole.COACH": "Coach",
|
||||
"options.teamMemberRole.CHEERLEADER": "Cheerleader",
|
||||
"options.teamMemberRole.CUSTOM": "",
|
||||
"options.teamMemberRoleType.PLAYER": "",
|
||||
"options.teamMemberRoleType.OTHER": "",
|
||||
"errors.required": "",
|
||||
"errors.minLength": "",
|
||||
"errors.maxLength": "",
|
||||
@@ -32,6 +51,9 @@
|
||||
"errors.duplicateName": "Esiste già un team con questo nome",
|
||||
"errors.duplicateOrgName": "",
|
||||
"errors.noOnlySpecialCharacters": "",
|
||||
"errors.customRoleRequired": "",
|
||||
"errors.customRoleTypeRequired": "",
|
||||
"errors.customRoleOnlyWhenCustom": "",
|
||||
"labels.weaponPool": "Pool armi",
|
||||
"placeholders.weaponPoolFull": "",
|
||||
"labels.voiceChat": "",
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"leaveTeam.header.newOwner": "Sei sicur/o di voler lasciare {{teamName}}? Il nuovo proprietario sarà {{newOwner}}",
|
||||
"actionButtons.leaveTeam.confirm": "Lascia",
|
||||
"actionButtons.editTeam": "Modifica team",
|
||||
"actionButtons.manageRoster": "Gestisci roster",
|
||||
"actionButtons.manageRoster": "",
|
||||
"actionButtons.deleteTeam": "Delete team",
|
||||
"actionButtons.deleteTeam.profilePicture": "",
|
||||
"actionButtons.deleteTeam.banner": "",
|
||||
@@ -49,6 +49,8 @@
|
||||
"roster.teamFull": "Il team è completo",
|
||||
"roster.inviteLink.header": "Condividi link di invito per aggiungere membri",
|
||||
"roster.members.header": "Membri",
|
||||
"roster.sections.players": "",
|
||||
"roster.sections.other": "",
|
||||
"validation.SHORT_CODE": "Il codice di invito non è della lunghezza giusta. Hai copiato l'intero URL?",
|
||||
"validation.TEAM_FULL": "Il team alla quale hai provato ad unirti è al completo.",
|
||||
"validation.INVITE_CODE_WRONG": "Il codice invito è errato.",
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
"labels.banner": "",
|
||||
"labels.tag": "",
|
||||
"labels.teamBsky": "チームの Bluesky",
|
||||
"labels.teamEditor": "",
|
||||
"labels.teamMemberRole": "",
|
||||
"labels.teamMemberCustomRole": "",
|
||||
"labels.teamMemberRoleType": "",
|
||||
"labels.clockFormat": "",
|
||||
"labels.disableBuildAbilitySorting": "",
|
||||
"labels.disallowScrimPickupsFromUntrusted": "",
|
||||
@@ -23,6 +27,21 @@
|
||||
"options.clockFormat.auto": "",
|
||||
"options.clockFormat.24h": "",
|
||||
"options.clockFormat.12h": "",
|
||||
"options.teamMemberRole.CAPTAIN": "キャプテン",
|
||||
"options.teamMemberRole.CO_CAPTAIN": "キャプテン補佐",
|
||||
"options.teamMemberRole.FRONTLINE": "前衛",
|
||||
"options.teamMemberRole.SLAYER": "",
|
||||
"options.teamMemberRole.SKIRMISHER": "前衛(突撃担当)",
|
||||
"options.teamMemberRole.SUPPORT": "サポート",
|
||||
"options.teamMemberRole.MIDLINE": "中衛",
|
||||
"options.teamMemberRole.BACKLINE": "後衛",
|
||||
"options.teamMemberRole.FLEX": "マルチ",
|
||||
"options.teamMemberRole.SUB": "補欠",
|
||||
"options.teamMemberRole.COACH": "コーチ",
|
||||
"options.teamMemberRole.CHEERLEADER": "引き立て役(チアリーダー)",
|
||||
"options.teamMemberRole.CUSTOM": "",
|
||||
"options.teamMemberRoleType.PLAYER": "",
|
||||
"options.teamMemberRoleType.OTHER": "",
|
||||
"errors.required": "",
|
||||
"errors.minLength": "",
|
||||
"errors.maxLength": "",
|
||||
@@ -32,6 +51,9 @@
|
||||
"errors.duplicateName": "そのチーム名はすでに使用されています",
|
||||
"errors.duplicateOrgName": "",
|
||||
"errors.noOnlySpecialCharacters": "",
|
||||
"errors.customRoleRequired": "",
|
||||
"errors.customRoleTypeRequired": "",
|
||||
"errors.customRoleOnlyWhenCustom": "",
|
||||
"labels.weaponPool": "使用ブキ",
|
||||
"placeholders.weaponPoolFull": "",
|
||||
"labels.voiceChat": "",
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"leaveTeam.header.newOwner": "",
|
||||
"actionButtons.leaveTeam.confirm": "チームを抜ける",
|
||||
"actionButtons.editTeam": "チームを編集",
|
||||
"actionButtons.manageRoster": "名簿管理",
|
||||
"actionButtons.manageRoster": "",
|
||||
"actionButtons.deleteTeam": "チームを削除",
|
||||
"actionButtons.deleteTeam.profilePicture": "",
|
||||
"actionButtons.deleteTeam.banner": "",
|
||||
@@ -49,6 +49,8 @@
|
||||
"roster.teamFull": "チームは満員です",
|
||||
"roster.inviteLink.header": "招待リンクを共有する",
|
||||
"roster.members.header": "メンバー",
|
||||
"roster.sections.players": "",
|
||||
"roster.sections.other": "",
|
||||
"validation.SHORT_CODE": "招待コードの長さが正しくありません。URL をすべてコピーしましたか?",
|
||||
"validation.TEAM_FULL": "参加しようとしているチームはすでに満員です。",
|
||||
"validation.INVITE_CODE_WRONG": "招待コードが正しくありません。",
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
"labels.banner": "",
|
||||
"labels.tag": "",
|
||||
"labels.teamBsky": "",
|
||||
"labels.teamEditor": "",
|
||||
"labels.teamMemberRole": "",
|
||||
"labels.teamMemberCustomRole": "",
|
||||
"labels.teamMemberRoleType": "",
|
||||
"labels.clockFormat": "",
|
||||
"labels.disableBuildAbilitySorting": "",
|
||||
"labels.disallowScrimPickupsFromUntrusted": "",
|
||||
@@ -23,6 +27,21 @@
|
||||
"options.clockFormat.auto": "",
|
||||
"options.clockFormat.24h": "",
|
||||
"options.clockFormat.12h": "",
|
||||
"options.teamMemberRole.CAPTAIN": "",
|
||||
"options.teamMemberRole.CO_CAPTAIN": "",
|
||||
"options.teamMemberRole.FRONTLINE": "",
|
||||
"options.teamMemberRole.SLAYER": "",
|
||||
"options.teamMemberRole.SKIRMISHER": "",
|
||||
"options.teamMemberRole.SUPPORT": "",
|
||||
"options.teamMemberRole.MIDLINE": "",
|
||||
"options.teamMemberRole.BACKLINE": "",
|
||||
"options.teamMemberRole.FLEX": "",
|
||||
"options.teamMemberRole.SUB": "",
|
||||
"options.teamMemberRole.COACH": "",
|
||||
"options.teamMemberRole.CHEERLEADER": "",
|
||||
"options.teamMemberRole.CUSTOM": "",
|
||||
"options.teamMemberRoleType.PLAYER": "",
|
||||
"options.teamMemberRoleType.OTHER": "",
|
||||
"errors.required": "",
|
||||
"errors.minLength": "",
|
||||
"errors.maxLength": "",
|
||||
@@ -32,6 +51,9 @@
|
||||
"errors.duplicateName": "",
|
||||
"errors.duplicateOrgName": "",
|
||||
"errors.noOnlySpecialCharacters": "",
|
||||
"errors.customRoleRequired": "",
|
||||
"errors.customRoleTypeRequired": "",
|
||||
"errors.customRoleOnlyWhenCustom": "",
|
||||
"labels.weaponPool": "",
|
||||
"placeholders.weaponPoolFull": "",
|
||||
"labels.voiceChat": "",
|
||||
|
||||
@@ -49,6 +49,8 @@
|
||||
"roster.teamFull": "",
|
||||
"roster.inviteLink.header": "",
|
||||
"roster.members.header": "",
|
||||
"roster.sections.players": "",
|
||||
"roster.sections.other": "",
|
||||
"validation.SHORT_CODE": "",
|
||||
"validation.TEAM_FULL": "",
|
||||
"validation.INVITE_CODE_WRONG": "",
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
"labels.banner": "",
|
||||
"labels.tag": "",
|
||||
"labels.teamBsky": "",
|
||||
"labels.teamEditor": "",
|
||||
"labels.teamMemberRole": "",
|
||||
"labels.teamMemberCustomRole": "",
|
||||
"labels.teamMemberRoleType": "",
|
||||
"labels.clockFormat": "",
|
||||
"labels.disableBuildAbilitySorting": "",
|
||||
"labels.disallowScrimPickupsFromUntrusted": "",
|
||||
@@ -23,6 +27,21 @@
|
||||
"options.clockFormat.auto": "",
|
||||
"options.clockFormat.24h": "",
|
||||
"options.clockFormat.12h": "",
|
||||
"options.teamMemberRole.CAPTAIN": "",
|
||||
"options.teamMemberRole.CO_CAPTAIN": "",
|
||||
"options.teamMemberRole.FRONTLINE": "",
|
||||
"options.teamMemberRole.SLAYER": "",
|
||||
"options.teamMemberRole.SKIRMISHER": "",
|
||||
"options.teamMemberRole.SUPPORT": "",
|
||||
"options.teamMemberRole.MIDLINE": "",
|
||||
"options.teamMemberRole.BACKLINE": "",
|
||||
"options.teamMemberRole.FLEX": "",
|
||||
"options.teamMemberRole.SUB": "",
|
||||
"options.teamMemberRole.COACH": "",
|
||||
"options.teamMemberRole.CHEERLEADER": "",
|
||||
"options.teamMemberRole.CUSTOM": "",
|
||||
"options.teamMemberRoleType.PLAYER": "",
|
||||
"options.teamMemberRoleType.OTHER": "",
|
||||
"errors.required": "",
|
||||
"errors.minLength": "",
|
||||
"errors.maxLength": "",
|
||||
@@ -32,6 +51,9 @@
|
||||
"errors.duplicateName": "",
|
||||
"errors.duplicateOrgName": "",
|
||||
"errors.noOnlySpecialCharacters": "",
|
||||
"errors.customRoleRequired": "",
|
||||
"errors.customRoleTypeRequired": "",
|
||||
"errors.customRoleOnlyWhenCustom": "",
|
||||
"labels.weaponPool": "",
|
||||
"placeholders.weaponPoolFull": "",
|
||||
"labels.voiceChat": "",
|
||||
|
||||
@@ -49,6 +49,8 @@
|
||||
"roster.teamFull": "",
|
||||
"roster.inviteLink.header": "",
|
||||
"roster.members.header": "",
|
||||
"roster.sections.players": "",
|
||||
"roster.sections.other": "",
|
||||
"validation.SHORT_CODE": "",
|
||||
"validation.TEAM_FULL": "",
|
||||
"validation.INVITE_CODE_WRONG": "",
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
"labels.banner": "",
|
||||
"labels.tag": "",
|
||||
"labels.teamBsky": "",
|
||||
"labels.teamEditor": "",
|
||||
"labels.teamMemberRole": "",
|
||||
"labels.teamMemberCustomRole": "",
|
||||
"labels.teamMemberRoleType": "",
|
||||
"labels.clockFormat": "",
|
||||
"labels.disableBuildAbilitySorting": "",
|
||||
"labels.disallowScrimPickupsFromUntrusted": "",
|
||||
@@ -23,6 +27,21 @@
|
||||
"options.clockFormat.auto": "",
|
||||
"options.clockFormat.24h": "",
|
||||
"options.clockFormat.12h": "",
|
||||
"options.teamMemberRole.CAPTAIN": "Kapitan",
|
||||
"options.teamMemberRole.CO_CAPTAIN": "",
|
||||
"options.teamMemberRole.FRONTLINE": "Frontline",
|
||||
"options.teamMemberRole.SLAYER": "",
|
||||
"options.teamMemberRole.SKIRMISHER": "",
|
||||
"options.teamMemberRole.SUPPORT": "Support",
|
||||
"options.teamMemberRole.MIDLINE": "",
|
||||
"options.teamMemberRole.BACKLINE": "Backline",
|
||||
"options.teamMemberRole.FLEX": "",
|
||||
"options.teamMemberRole.SUB": "",
|
||||
"options.teamMemberRole.COACH": "Trener",
|
||||
"options.teamMemberRole.CHEERLEADER": "",
|
||||
"options.teamMemberRole.CUSTOM": "",
|
||||
"options.teamMemberRoleType.PLAYER": "",
|
||||
"options.teamMemberRoleType.OTHER": "",
|
||||
"errors.required": "",
|
||||
"errors.minLength": "",
|
||||
"errors.maxLength": "",
|
||||
@@ -32,6 +51,9 @@
|
||||
"errors.duplicateName": "Istnieje już drużyna o tym imieniu",
|
||||
"errors.duplicateOrgName": "",
|
||||
"errors.noOnlySpecialCharacters": "",
|
||||
"errors.customRoleRequired": "",
|
||||
"errors.customRoleTypeRequired": "",
|
||||
"errors.customRoleOnlyWhenCustom": "",
|
||||
"labels.weaponPool": "Pula broni",
|
||||
"placeholders.weaponPoolFull": "",
|
||||
"labels.voiceChat": "",
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"leaveTeam.header.newOwner": "",
|
||||
"actionButtons.leaveTeam.confirm": "Opuść",
|
||||
"actionButtons.editTeam": "Edytuj Drużynę",
|
||||
"actionButtons.manageRoster": "Zarządzaj Członkami",
|
||||
"actionButtons.manageRoster": "",
|
||||
"actionButtons.deleteTeam": "Usuń drużynę",
|
||||
"actionButtons.deleteTeam.profilePicture": "",
|
||||
"actionButtons.deleteTeam.banner": "",
|
||||
@@ -49,6 +49,8 @@
|
||||
"roster.teamFull": "Drużyna jest pełna",
|
||||
"roster.inviteLink.header": "Udostępnij link by dodać członków",
|
||||
"roster.members.header": "Członkowie",
|
||||
"roster.sections.players": "",
|
||||
"roster.sections.other": "",
|
||||
"validation.SHORT_CODE": "Zaproszenie jest złej długości. Czy napewno skopiowałeś/aś pełne URL?",
|
||||
"validation.TEAM_FULL": "Drużyna, do której próbujesz dołączyć, jest pełna.",
|
||||
"validation.INVITE_CODE_WRONG": "Zaproszenie niepoprawne.",
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
"labels.banner": "",
|
||||
"labels.tag": "",
|
||||
"labels.teamBsky": "",
|
||||
"labels.teamEditor": "",
|
||||
"labels.teamMemberRole": "",
|
||||
"labels.teamMemberCustomRole": "",
|
||||
"labels.teamMemberRoleType": "",
|
||||
"labels.clockFormat": "",
|
||||
"labels.disableBuildAbilitySorting": "",
|
||||
"labels.disallowScrimPickupsFromUntrusted": "",
|
||||
@@ -23,6 +27,21 @@
|
||||
"options.clockFormat.auto": "",
|
||||
"options.clockFormat.24h": "",
|
||||
"options.clockFormat.12h": "",
|
||||
"options.teamMemberRole.CAPTAIN": "Capitão(ã)",
|
||||
"options.teamMemberRole.CO_CAPTAIN": "",
|
||||
"options.teamMemberRole.FRONTLINE": "Linha de frente (frontline)",
|
||||
"options.teamMemberRole.SLAYER": "",
|
||||
"options.teamMemberRole.SKIRMISHER": "",
|
||||
"options.teamMemberRole.SUPPORT": "Suporte",
|
||||
"options.teamMemberRole.MIDLINE": "Linha média (midline)",
|
||||
"options.teamMemberRole.BACKLINE": "Linha de trás (backline)",
|
||||
"options.teamMemberRole.FLEX": "Flex",
|
||||
"options.teamMemberRole.SUB": "",
|
||||
"options.teamMemberRole.COACH": "Treinador",
|
||||
"options.teamMemberRole.CHEERLEADER": "",
|
||||
"options.teamMemberRole.CUSTOM": "",
|
||||
"options.teamMemberRoleType.PLAYER": "",
|
||||
"options.teamMemberRoleType.OTHER": "",
|
||||
"errors.required": "",
|
||||
"errors.minLength": "",
|
||||
"errors.maxLength": "",
|
||||
@@ -32,6 +51,9 @@
|
||||
"errors.duplicateName": "Já existe um time com esse nome",
|
||||
"errors.duplicateOrgName": "",
|
||||
"errors.noOnlySpecialCharacters": "",
|
||||
"errors.customRoleRequired": "",
|
||||
"errors.customRoleTypeRequired": "",
|
||||
"errors.customRoleOnlyWhenCustom": "",
|
||||
"labels.weaponPool": "Seleção de armas",
|
||||
"placeholders.weaponPoolFull": "",
|
||||
"labels.voiceChat": "",
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"leaveTeam.header.newOwner": "",
|
||||
"actionButtons.leaveTeam.confirm": "Sair",
|
||||
"actionButtons.editTeam": "Editar Time",
|
||||
"actionButtons.manageRoster": "Gerenciar a lista",
|
||||
"actionButtons.manageRoster": "",
|
||||
"actionButtons.deleteTeam": "Apagar Time",
|
||||
"actionButtons.deleteTeam.profilePicture": "",
|
||||
"actionButtons.deleteTeam.banner": "",
|
||||
@@ -49,6 +49,8 @@
|
||||
"roster.teamFull": "O time está cheio",
|
||||
"roster.inviteLink.header": "Compartilhe o link de convite para adicionar membros",
|
||||
"roster.members.header": "Membros",
|
||||
"roster.sections.players": "",
|
||||
"roster.sections.other": "",
|
||||
"validation.SHORT_CODE": "O código de convite não está no comprimento certo. Você copiou o URL completo?",
|
||||
"validation.TEAM_FULL": "O time que você está tentando se juntar está cheio.",
|
||||
"validation.INVITE_CODE_WRONG": "O código de convite está incorreto.",
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
"labels.banner": "",
|
||||
"labels.tag": "",
|
||||
"labels.teamBsky": "Bluesky команды",
|
||||
"labels.teamEditor": "Редактор",
|
||||
"labels.teamMemberRole": "",
|
||||
"labels.teamMemberCustomRole": "",
|
||||
"labels.teamMemberRoleType": "",
|
||||
"labels.clockFormat": "",
|
||||
"labels.disableBuildAbilitySorting": "",
|
||||
"labels.disallowScrimPickupsFromUntrusted": "",
|
||||
@@ -23,6 +27,21 @@
|
||||
"options.clockFormat.auto": "",
|
||||
"options.clockFormat.24h": "",
|
||||
"options.clockFormat.12h": "",
|
||||
"options.teamMemberRole.CAPTAIN": "Капитан",
|
||||
"options.teamMemberRole.CO_CAPTAIN": "Со-капитан",
|
||||
"options.teamMemberRole.FRONTLINE": "Фронтлайн",
|
||||
"options.teamMemberRole.SLAYER": "Слеер",
|
||||
"options.teamMemberRole.SKIRMISHER": "Скёрмишер",
|
||||
"options.teamMemberRole.SUPPORT": "Поддержка",
|
||||
"options.teamMemberRole.MIDLINE": "Мидлайн",
|
||||
"options.teamMemberRole.BACKLINE": "Бэклайн",
|
||||
"options.teamMemberRole.FLEX": "Флекс",
|
||||
"options.teamMemberRole.SUB": "Запасной",
|
||||
"options.teamMemberRole.COACH": "Тренер",
|
||||
"options.teamMemberRole.CHEERLEADER": "Чирлидер",
|
||||
"options.teamMemberRole.CUSTOM": "",
|
||||
"options.teamMemberRoleType.PLAYER": "",
|
||||
"options.teamMemberRoleType.OTHER": "",
|
||||
"errors.required": "",
|
||||
"errors.minLength": "",
|
||||
"errors.maxLength": "",
|
||||
@@ -32,6 +51,9 @@
|
||||
"errors.duplicateName": "Уже существует команда с таким названием",
|
||||
"errors.duplicateOrgName": "",
|
||||
"errors.noOnlySpecialCharacters": "",
|
||||
"errors.customRoleRequired": "",
|
||||
"errors.customRoleTypeRequired": "",
|
||||
"errors.customRoleOnlyWhenCustom": "",
|
||||
"labels.weaponPool": "Используемое оружие",
|
||||
"placeholders.weaponPoolFull": "",
|
||||
"labels.voiceChat": "",
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"leaveTeam.header.newOwner": "Вы точно хотите покинуть {{teamName}}? Новым владельцем будет {{newOwner}}",
|
||||
"actionButtons.leaveTeam.confirm": "Покинуть",
|
||||
"actionButtons.editTeam": "Редактировать команду",
|
||||
"actionButtons.manageRoster": "Управлять составом",
|
||||
"actionButtons.manageRoster": "",
|
||||
"actionButtons.deleteTeam": "Удалить команду",
|
||||
"actionButtons.deleteTeam.profilePicture": "",
|
||||
"actionButtons.deleteTeam.banner": "",
|
||||
@@ -49,6 +49,8 @@
|
||||
"roster.teamFull": "Команда заполнена",
|
||||
"roster.inviteLink.header": "Поделитесь ссылкой на приглашение, чтобы добавить участников",
|
||||
"roster.members.header": "Участники",
|
||||
"roster.sections.players": "",
|
||||
"roster.sections.other": "",
|
||||
"validation.SHORT_CODE": "Код приглашения неверной длинны. Вы скопировали URL целиком?",
|
||||
"validation.TEAM_FULL": "Команда, в которую вы пытаетесь попасть, уже заполнена.",
|
||||
"validation.INVITE_CODE_WRONG": "Неверный код приглашения.",
|
||||
|
||||
@@ -93,8 +93,6 @@
|
||||
"damage.header.baseDamage": "基础伤害",
|
||||
"damage.header.baseDamage.short": "基础",
|
||||
"damage.header.distance": "距离",
|
||||
"damage.toSplat_one": "{{count}} 确击倒",
|
||||
"damage.toSplat_other": "{{count}} 确击倒",
|
||||
"damage.NORMAL_MIN": "最小值",
|
||||
"damage.NORMAL_MAX": "最大值",
|
||||
"damage.SECONDARY_MODE_MIN": "第二射击模式 (最小值)",
|
||||
@@ -164,8 +162,6 @@
|
||||
"labels.weapon": "武器",
|
||||
"dmgHtdExplanation": "DMG = 伤害 | HTD = 破坏所需伤害",
|
||||
"noDmgData": "目前没有该武器的伤害数据。请期待后续更新!",
|
||||
"perInkTankGrid.header_one": "使用 {{count}} 次次要武器后的 {{weapon}} 剩余攻击次数",
|
||||
"perInkTankGrid.header_other": "使用 {{count}} 次次要武器后的 {{weapon}} 剩余攻击次数",
|
||||
"bigBubblerExplanation": "{{weapon}} 的持续时间也会一起增加。",
|
||||
"button.showChart": "显示图表",
|
||||
"button.showConsumptionGrid": "显示耗墨量网格",
|
||||
@@ -185,8 +181,6 @@
|
||||
"comp.pickWeapon": "选择一个武器",
|
||||
"comp.showWeaponGrid": "显示武器选择器",
|
||||
"comp.hideWeaponGrid": "隐藏武器选择器",
|
||||
"comp.hits_one": "命中 {{count}} 次",
|
||||
"comp.hits_other": "命中 {{count}} 次",
|
||||
"comp.enemyRes": "对手的 减轻对手墨汁影响",
|
||||
"comp.enemySubDef": "对手的 减轻次要武器影响",
|
||||
"comp.weaponRanges": "武器射程",
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
"labels.banner": "",
|
||||
"labels.tag": "",
|
||||
"labels.teamBsky": "",
|
||||
"labels.teamEditor": "",
|
||||
"labels.teamMemberRole": "",
|
||||
"labels.teamMemberCustomRole": "",
|
||||
"labels.teamMemberRoleType": "",
|
||||
"labels.clockFormat": "",
|
||||
"labels.disableBuildAbilitySorting": "",
|
||||
"labels.disallowScrimPickupsFromUntrusted": "",
|
||||
@@ -23,6 +27,21 @@
|
||||
"options.clockFormat.auto": "",
|
||||
"options.clockFormat.24h": "",
|
||||
"options.clockFormat.12h": "",
|
||||
"options.teamMemberRole.CAPTAIN": "队长",
|
||||
"options.teamMemberRole.CO_CAPTAIN": "",
|
||||
"options.teamMemberRole.FRONTLINE": "前排",
|
||||
"options.teamMemberRole.SLAYER": "",
|
||||
"options.teamMemberRole.SKIRMISHER": "",
|
||||
"options.teamMemberRole.SUPPORT": "辅助",
|
||||
"options.teamMemberRole.MIDLINE": "中排",
|
||||
"options.teamMemberRole.BACKLINE": "后排",
|
||||
"options.teamMemberRole.FLEX": "自由人",
|
||||
"options.teamMemberRole.SUB": "",
|
||||
"options.teamMemberRole.COACH": "教练",
|
||||
"options.teamMemberRole.CHEERLEADER": "",
|
||||
"options.teamMemberRole.CUSTOM": "",
|
||||
"options.teamMemberRoleType.PLAYER": "",
|
||||
"options.teamMemberRoleType.OTHER": "",
|
||||
"errors.required": "",
|
||||
"errors.minLength": "",
|
||||
"errors.maxLength": "",
|
||||
@@ -32,6 +51,9 @@
|
||||
"errors.duplicateName": "该队名已被使用",
|
||||
"errors.duplicateOrgName": "",
|
||||
"errors.noOnlySpecialCharacters": "",
|
||||
"errors.customRoleRequired": "",
|
||||
"errors.customRoleTypeRequired": "",
|
||||
"errors.customRoleOnlyWhenCustom": "",
|
||||
"labels.weaponPool": "武器池",
|
||||
"placeholders.weaponPoolFull": "",
|
||||
"labels.voiceChat": "",
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"leaveTeam.header.newOwner": "",
|
||||
"actionButtons.leaveTeam.confirm": "退出",
|
||||
"actionButtons.editTeam": "编辑队伍",
|
||||
"actionButtons.manageRoster": "管理阵容",
|
||||
"actionButtons.manageRoster": "",
|
||||
"actionButtons.deleteTeam": "删除队伍",
|
||||
"actionButtons.deleteTeam.profilePicture": "",
|
||||
"actionButtons.deleteTeam.banner": "",
|
||||
@@ -49,6 +49,8 @@
|
||||
"roster.teamFull": "成员已满",
|
||||
"roster.inviteLink.header": "分享邀请链接来添加成员",
|
||||
"roster.members.header": "成员",
|
||||
"roster.sections.players": "",
|
||||
"roster.sections.other": "",
|
||||
"validation.SHORT_CODE": "邀请码长度不符,请确保您复制了完整的URL。",
|
||||
"validation.TEAM_FULL": "您想要加入的队伍成员已满。",
|
||||
"validation.INVITE_CODE_WRONG": "邀请码有误。",
|
||||
|
||||
10
migrations/150-team-member-custom-roles.js
Normal file
10
migrations/150-team-member-custom-roles.js
Normal file
@@ -0,0 +1,10 @@
|
||||
export function up(db) {
|
||||
db.transaction(() => {
|
||||
db.prepare(
|
||||
/* sql */ `alter table "AllTeamMember" add "customRole" text`,
|
||||
).run();
|
||||
db.prepare(
|
||||
/* sql */ `alter table "AllTeamMember" add "roleType" text`,
|
||||
).run();
|
||||
})();
|
||||
}
|
||||
Reference in New Issue
Block a user