mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-10 21:26:08 -05:00
Remove SendouQ group roles and kicking
This commit is contained in:
@@ -7,19 +7,19 @@ type InsertArgs = Omit<
|
||||
Parameters<typeof SQGroupRepository.insert>[0],
|
||||
"userId"
|
||||
> & {
|
||||
/** The group's members, the first of them its owner. */
|
||||
/** The group's members, the first of them its creator. */
|
||||
memberUserIds: number[];
|
||||
};
|
||||
|
||||
type Options = {
|
||||
/** Was the group made in the matchmaking UI? */
|
||||
isMatchmade?: boolean;
|
||||
/** Groups that have liked this one, each of them as its own owner. */
|
||||
/** Groups that have liked this one. */
|
||||
likedByGroupIds?: number[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates SendouQ groups. The first of `memberUserIds` is the owner, whose
|
||||
* Creates SendouQ groups. The first of `memberUserIds` is the creator, whose
|
||||
* membership the repository creates with the group; the rest join it the way they do
|
||||
* in production. Invite and chat codes are the repository's own.
|
||||
*/
|
||||
@@ -28,19 +28,19 @@ export const { create } = defineFactory({
|
||||
status: "ACTIVE" as const,
|
||||
}),
|
||||
insert: async ({ memberUserIds, ...args }: InsertArgs) => {
|
||||
const [ownerUserId, ...otherMemberUserIds] = memberUserIds;
|
||||
invariant(ownerUserId, "A group needs at least an owner");
|
||||
const [creatorUserId, ...otherMemberUserIds] = memberUserIds;
|
||||
invariant(creatorUserId, "A group needs at least one member");
|
||||
|
||||
const group = await SQGroupRepository.insert({
|
||||
...args,
|
||||
userId: ownerUserId,
|
||||
userId: creatorUserId,
|
||||
});
|
||||
|
||||
for (const userId of otherMemberUserIds) {
|
||||
await SQGroupRepository.insertMember(group.id, { userId });
|
||||
}
|
||||
|
||||
return { id: group.id, memberUserIds, ownerUserId };
|
||||
return { id: group.id, memberUserIds };
|
||||
},
|
||||
applyOptions: async (group, { isMatchmade, likedByGroupIds }: Options) => {
|
||||
for (const likerGroupId of likedByGroupIds ?? []) {
|
||||
|
||||
@@ -139,7 +139,7 @@ async function cancelMatch(
|
||||
) {
|
||||
const request = await SQMatchRepository.requestCancelMatch({
|
||||
matchId: match.id,
|
||||
requestedByUserId: match.alphaGroup.ownerUserId,
|
||||
requestedByUserId: match.alphaGroup.memberUserIds[0],
|
||||
...requested,
|
||||
});
|
||||
|
||||
@@ -150,7 +150,7 @@ async function cancelMatch(
|
||||
|
||||
const acceptance = await SQMatchRepository.acceptCancelMatch({
|
||||
matchId: match.id,
|
||||
acceptedByUserId: match.bravoGroup.ownerUserId,
|
||||
acceptedByUserId: match.bravoGroup.memberUserIds[0],
|
||||
...accepted,
|
||||
});
|
||||
|
||||
|
||||
@@ -368,7 +368,6 @@ export interface GroupMember {
|
||||
createdAt: Generated<number>;
|
||||
groupId: number;
|
||||
note: string | null;
|
||||
role: "OWNER" | "MANAGER" | "REGULAR";
|
||||
userId: number;
|
||||
}
|
||||
|
||||
|
||||
@@ -154,7 +154,6 @@ function groupWithTeamAndMembers(
|
||||
)
|
||||
.select((arrayEb) => [
|
||||
...commonUserSelect(arrayEb),
|
||||
"GroupMember.role",
|
||||
"GroupMember.note",
|
||||
"User.inGameName",
|
||||
"User.vc",
|
||||
|
||||
@@ -134,10 +134,9 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
|
||||
"This group must use the continue vote",
|
||||
);
|
||||
|
||||
const requester = previousGroup.members.find((m) => m.id === user.id);
|
||||
errorToastIfFalsy(
|
||||
requester?.role === "OWNER",
|
||||
"You are not the owner of the group",
|
||||
previousGroup.members.some((m) => m.id === user.id),
|
||||
"Not a member of the group",
|
||||
);
|
||||
|
||||
for (const member of previousGroup.members) {
|
||||
@@ -147,10 +146,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
|
||||
|
||||
await SQGroupRepository.insertFromPrevious({
|
||||
previousGroupId: data.previousGroupId,
|
||||
members: previousGroup.members.map((m) => ({
|
||||
id: m.id,
|
||||
role: m.role,
|
||||
})),
|
||||
memberUserIds: previousGroup.members.map((m) => m.id),
|
||||
status: "ACTIVE",
|
||||
});
|
||||
|
||||
@@ -226,14 +222,14 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
|
||||
});
|
||||
|
||||
if (votingResult?.type === "RESOLVED") {
|
||||
const survivors = viewerGroup.members
|
||||
.filter((m) => votingResult.continuingUserIds.includes(m.id))
|
||||
.map((m) => ({ id: m.id, role: m.role }));
|
||||
const survivors = viewerGroup.members.filter((m) =>
|
||||
votingResult.continuingUserIds.includes(m.id),
|
||||
);
|
||||
|
||||
try {
|
||||
await SQGroupRepository.insertFromPrevious({
|
||||
previousGroupId: viewerGroup.id,
|
||||
members: survivors,
|
||||
memberUserIds: survivors.map((m) => m.id),
|
||||
status: "ACTIVE",
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
@@ -56,39 +56,26 @@ export function MatchmadeRejoinSection({
|
||||
|
||||
export function TrustedRejoinSection({
|
||||
viewerGroup,
|
||||
viewerUserId,
|
||||
}: {
|
||||
viewerGroup: NonNullable<SendouQMatchLoaderData["match"]["groupAlpha"]>;
|
||||
viewerUserId: number;
|
||||
}) {
|
||||
const { t } = useTranslation(["q"]);
|
||||
const viewerRole = viewerGroup.members.find(
|
||||
(m) => m.id === viewerUserId,
|
||||
)?.role;
|
||||
const lookAgain = useActionSubmit(matchSchema);
|
||||
|
||||
if (viewerRole === "OWNER") {
|
||||
return (
|
||||
<div className="stack md items-center">
|
||||
<SendouButton
|
||||
variant="primary"
|
||||
isPending={lookAgain.state !== "idle"}
|
||||
onPress={() => {
|
||||
lookAgain.submit("LOOK_AGAIN", {
|
||||
previousGroupId: viewerGroup.id,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("q:match.actions.lookAgain")}
|
||||
</SendouButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<p className="text-lighter text-sm text-center">
|
||||
{t("q:match.rematch.waitingCaptain")}
|
||||
</p>
|
||||
<div className="stack md items-center">
|
||||
<SendouButton
|
||||
variant="primary"
|
||||
isPending={lookAgain.state !== "idle"}
|
||||
onPress={() => {
|
||||
lookAgain.submit("LOOK_AGAIN", {
|
||||
previousGroupId: viewerGroup.id,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("q:match.actions.lookAgain")}
|
||||
</SendouButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -274,10 +274,7 @@ function RequeueTab({
|
||||
{!data.isOffSeason &&
|
||||
!viewerGroup.matchmade &&
|
||||
(!awaitingConfirmation || isOnReporterTeam) ? (
|
||||
<TrustedRejoinSection
|
||||
viewerGroup={viewerGroup}
|
||||
viewerUserId={user.id}
|
||||
/>
|
||||
<TrustedRejoinSection viewerGroup={viewerGroup} />
|
||||
) : null}
|
||||
{isOnReporterTeam ? <hr className={styles.divider} /> : null}
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import type { UserMapModePreferences } from "~/db/tables-json";
|
||||
import { actorId } from "~/features/auth/core/user.server";
|
||||
import { databaseTimestampNow, dateToDatabaseTimestamp } from "~/utils/dates";
|
||||
import { shortNanoid } from "~/utils/id";
|
||||
import invariant from "~/utils/invariant";
|
||||
import {
|
||||
commonUserMembersAgg,
|
||||
commonUserSelect,
|
||||
@@ -77,7 +76,6 @@ export async function findCurrentGroups() {
|
||||
commonUserMembersAgg(eb, {
|
||||
mapModePreferences: eb.ref("User.mapModePreferences"),
|
||||
noScreen: eb.ref("User.noScreen"),
|
||||
role: eb.ref("GroupMember.role"),
|
||||
note: eb.ref("GroupMember.note"),
|
||||
weapons: matchProfileWeapons(eb),
|
||||
languages: eb.ref("User.languages"),
|
||||
@@ -122,7 +120,6 @@ export async function insert(args: CreateGroupArgs) {
|
||||
.values({
|
||||
groupId: createdGroup.id,
|
||||
userId: args.userId,
|
||||
role: "OWNER",
|
||||
})
|
||||
.execute();
|
||||
|
||||
@@ -141,17 +138,13 @@ export async function insert(args: CreateGroupArgs) {
|
||||
|
||||
type CreateGroupFromPreviousGroupArgs = {
|
||||
previousGroupId: number;
|
||||
members: {
|
||||
id: number;
|
||||
role: Tables["GroupMember"]["role"];
|
||||
}[];
|
||||
memberUserIds: number[];
|
||||
status?: Exclude<Tables["Group"]["status"], "INACTIVE">;
|
||||
};
|
||||
export async function insertFromPrevious(
|
||||
args: CreateGroupFromPreviousGroupArgs,
|
||||
) {
|
||||
const status = args.status ?? "PREPARING";
|
||||
const membersWithEnsuredOwner = ensureOwnerRole(args.members);
|
||||
|
||||
return db.transaction().execute(async (trx) => {
|
||||
const createdGroup = await trx
|
||||
@@ -175,10 +168,9 @@ export async function insertFromPrevious(
|
||||
await trx
|
||||
.insertInto("GroupMember")
|
||||
.values(
|
||||
membersWithEnsuredOwner.map((member) => ({
|
||||
args.memberUserIds.map((userId) => ({
|
||||
groupId: createdGroup.id,
|
||||
userId: member.id,
|
||||
role: member.role,
|
||||
userId,
|
||||
})),
|
||||
)
|
||||
.execute();
|
||||
@@ -193,19 +185,6 @@ export async function insertFromPrevious(
|
||||
});
|
||||
}
|
||||
|
||||
function ensureOwnerRole(
|
||||
members: CreateGroupFromPreviousGroupArgs["members"],
|
||||
): CreateGroupFromPreviousGroupArgs["members"] {
|
||||
if (members.some((m) => m.role === "OWNER")) return members;
|
||||
|
||||
const promoteeIndex = members.findIndex((m) => m.role === "MANAGER");
|
||||
const targetIndex = promoteeIndex !== -1 ? promoteeIndex : 0;
|
||||
|
||||
return members.map((m, i) =>
|
||||
i === targetIndex ? { ...m, role: "OWNER" as const } : m,
|
||||
);
|
||||
}
|
||||
|
||||
function deleteLikesByGroupId(groupId: number, trx: Transaction<DB>) {
|
||||
return trx
|
||||
.deleteFrom("GroupLike")
|
||||
@@ -233,34 +212,12 @@ export function morphGroups({
|
||||
.where("Group.id", "=", survivingGroupId)
|
||||
.execute();
|
||||
|
||||
const otherGroupMembers = await trx
|
||||
.selectFrom("GroupMember")
|
||||
.select(["GroupMember.userId", "GroupMember.role"])
|
||||
await trx
|
||||
.updateTable("GroupMember")
|
||||
.set({ groupId: survivingGroupId })
|
||||
.where("GroupMember.groupId", "=", otherGroupId)
|
||||
.execute();
|
||||
|
||||
for (const member of otherGroupMembers) {
|
||||
const oldRole = otherGroupMembers.find(
|
||||
(m) => m.userId === member.userId,
|
||||
)?.role;
|
||||
invariant(oldRole, "Member lacking a role");
|
||||
|
||||
await trx
|
||||
.updateTable("GroupMember")
|
||||
.set({
|
||||
role:
|
||||
oldRole === "OWNER"
|
||||
? "MANAGER"
|
||||
: oldRole === "MANAGER"
|
||||
? "MANAGER"
|
||||
: "REGULAR",
|
||||
groupId: survivingGroupId,
|
||||
})
|
||||
.where("GroupMember.groupId", "=", otherGroupId)
|
||||
.where("GroupMember.userId", "=", member.userId)
|
||||
.execute();
|
||||
}
|
||||
|
||||
await deleteLikesByGroupId(survivingGroupId, trx);
|
||||
await refreshGroup(survivingGroupId, trx);
|
||||
|
||||
@@ -312,13 +269,7 @@ async function isGroupCorrect(
|
||||
|
||||
export async function insertMember(
|
||||
groupId: number,
|
||||
{
|
||||
userId,
|
||||
role = "REGULAR",
|
||||
}: {
|
||||
userId: number;
|
||||
role?: Tables["GroupMember"]["role"];
|
||||
},
|
||||
{ userId }: { userId: number },
|
||||
) {
|
||||
const chatCodeToRevalidate = await db.transaction().execute(async (trx) => {
|
||||
await trx
|
||||
@@ -326,7 +277,6 @@ export async function insertMember(
|
||||
.values({
|
||||
groupId,
|
||||
userId,
|
||||
role,
|
||||
})
|
||||
.execute();
|
||||
|
||||
@@ -656,7 +606,7 @@ export function leaveGroup(userId: number) {
|
||||
const userGroup = await trx
|
||||
.selectFrom("GroupMember")
|
||||
.innerJoin("Group", "Group.id", "GroupMember.groupId")
|
||||
.select(["Group.id", "GroupMember.role"])
|
||||
.select(["Group.id"])
|
||||
.where("userId", "=", userId)
|
||||
.where("Group.status", "!=", "INACTIVE")
|
||||
.executeTakeFirstOrThrow();
|
||||
@@ -667,30 +617,17 @@ export function leaveGroup(userId: number) {
|
||||
.where("GroupMember.groupId", "=", userGroup.id)
|
||||
.execute();
|
||||
|
||||
const remainingMembers = await trx
|
||||
const remainingMember = await trx
|
||||
.selectFrom("GroupMember")
|
||||
.select(["userId", "role"])
|
||||
.select(["userId"])
|
||||
.where("groupId", "=", userGroup.id)
|
||||
.execute();
|
||||
.executeTakeFirst();
|
||||
|
||||
if (remainingMembers.length === 0) {
|
||||
if (!remainingMember) {
|
||||
await trx.deleteFrom("Group").where("id", "=", userGroup.id).execute();
|
||||
return;
|
||||
}
|
||||
|
||||
if (userGroup.role === "OWNER") {
|
||||
const newOwner =
|
||||
remainingMembers.find((m) => m.role === "MANAGER") ??
|
||||
remainingMembers[0];
|
||||
|
||||
await trx
|
||||
.updateTable("GroupMember")
|
||||
.set({ role: "OWNER" })
|
||||
.where("userId", "=", newOwner.userId)
|
||||
.where("groupId", "=", userGroup.id)
|
||||
.execute();
|
||||
}
|
||||
|
||||
const match = await trx
|
||||
.selectFrom("GroupMatch")
|
||||
.select(["GroupMatch.id"])
|
||||
@@ -735,31 +672,6 @@ export function updateOwnMemberNote({
|
||||
});
|
||||
}
|
||||
|
||||
export function updateMemberRole({
|
||||
userId,
|
||||
groupId,
|
||||
role,
|
||||
}: {
|
||||
userId: number;
|
||||
groupId: number;
|
||||
role: Tables["GroupMember"]["role"];
|
||||
}) {
|
||||
if (role === "OWNER") {
|
||||
throw new Error("Can't set role to OWNER with this function");
|
||||
}
|
||||
|
||||
return db.transaction().execute(async (trx) => {
|
||||
await trx
|
||||
.updateTable("GroupMember")
|
||||
.set({ role })
|
||||
.where("userId", "=", userId)
|
||||
.where("groupId", "=", groupId)
|
||||
.execute();
|
||||
|
||||
await refreshGroup(groupId, trx);
|
||||
});
|
||||
}
|
||||
|
||||
export function setPreparingGroupAsActive(groupId: number) {
|
||||
return db
|
||||
.updateTable("Group")
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
import * as SQMatchRepository from "~/features/sendouq-match/SQMatchRepository.server";
|
||||
import { refreshStreamsCache } from "~/features/sendouq-streams/core/streams.server";
|
||||
import { parseFormData } from "~/form/parse.server";
|
||||
import { errorToastIfFalsy } from "~/utils/remix.server";
|
||||
import { assertUnreachable } from "~/utils/types";
|
||||
import { SENDOUQ_PAGE, sendouQMatchPage } from "~/utils/urls";
|
||||
import { groupAfterMorph } from "../core/groups";
|
||||
@@ -64,17 +63,8 @@ export const action: ActionFunction = async ({ request }) => {
|
||||
});
|
||||
|
||||
try {
|
||||
// this throws because there should normally be no way user loses ownership by the action of some other user
|
||||
const validateIsGroupOwner = () =>
|
||||
errorToastIfFalsy(currentGroup.usersRole === "OWNER", "Not owner");
|
||||
const isGroupManager = () =>
|
||||
currentGroup.usersRole === "MANAGER" ||
|
||||
currentGroup.usersRole === "OWNER";
|
||||
|
||||
switch (data._action) {
|
||||
case "LIKE": {
|
||||
if (!isGroupManager()) return null;
|
||||
|
||||
await SQGroupRepository.insertLike({
|
||||
likerGroupId: currentGroup.id,
|
||||
targetGroupId: data.targetGroupId,
|
||||
@@ -86,8 +76,6 @@ export const action: ActionFunction = async ({ request }) => {
|
||||
break;
|
||||
}
|
||||
case "RECHALLENGE": {
|
||||
if (!isGroupManager()) return null;
|
||||
|
||||
await SQGroupRepository.rechallenge({
|
||||
likerGroupId: currentGroup.id,
|
||||
targetGroupId: data.targetGroupId,
|
||||
@@ -98,8 +86,6 @@ export const action: ActionFunction = async ({ request }) => {
|
||||
break;
|
||||
}
|
||||
case "UNLIKE": {
|
||||
if (!isGroupManager()) return null;
|
||||
|
||||
await SQGroupRepository.deleteLike({
|
||||
likerGroupId: currentGroup.id,
|
||||
targetGroupId: data.targetGroupId,
|
||||
@@ -111,8 +97,6 @@ export const action: ActionFunction = async ({ request }) => {
|
||||
break;
|
||||
}
|
||||
case "GROUP_UP": {
|
||||
if (!isGroupManager()) return null;
|
||||
|
||||
const allLikes = await SQGroupRepository.findAllLikesByGroupId(
|
||||
data.targetGroupId,
|
||||
);
|
||||
@@ -161,8 +145,6 @@ export const action: ActionFunction = async ({ request }) => {
|
||||
break;
|
||||
}
|
||||
case "MATCH_UP": {
|
||||
if (!isGroupManager()) return null;
|
||||
|
||||
const ownGroup = SendouQ.findOwnGroup(user.id);
|
||||
const theirGroup = SendouQ.findUncensoredGroupById(data.targetGroupId);
|
||||
if (!ownGroup || !theirGroup) return null;
|
||||
@@ -264,36 +246,6 @@ export const action: ActionFunction = async ({ request }) => {
|
||||
|
||||
throw redirect(sendouQMatchPage(createdMatch.id));
|
||||
}
|
||||
case "GIVE_MANAGER": {
|
||||
validateIsGroupOwner();
|
||||
|
||||
await SQGroupRepository.updateMemberRole({
|
||||
groupId: currentGroup.id,
|
||||
userId: data.userId,
|
||||
role: "MANAGER",
|
||||
});
|
||||
|
||||
await refreshSendouQInstance();
|
||||
|
||||
revalidateGroupTopic(currentGroup.id);
|
||||
|
||||
break;
|
||||
}
|
||||
case "REMOVE_MANAGER": {
|
||||
validateIsGroupOwner();
|
||||
|
||||
await SQGroupRepository.updateMemberRole({
|
||||
groupId: currentGroup.id,
|
||||
userId: data.userId,
|
||||
role: "REGULAR",
|
||||
});
|
||||
|
||||
await refreshSendouQInstance();
|
||||
|
||||
revalidateGroupTopic(currentGroup.id);
|
||||
|
||||
break;
|
||||
}
|
||||
case "LEAVE_GROUP": {
|
||||
await SQGroupRepository.leaveGroup(user.id);
|
||||
|
||||
@@ -316,26 +268,6 @@ export const action: ActionFunction = async ({ request }) => {
|
||||
|
||||
throw redirect(SENDOUQ_PAGE);
|
||||
}
|
||||
case "KICK_FROM_GROUP": {
|
||||
validateIsGroupOwner();
|
||||
errorToastIfFalsy(data.userId !== user.id, "Can't kick yourself");
|
||||
|
||||
await SQGroupRepository.leaveGroup(data.userId);
|
||||
|
||||
await refreshSendouQInstance();
|
||||
|
||||
const remainingGroup = SendouQ.findUncensoredGroupById(currentGroup.id);
|
||||
if (remainingGroup?.chatCode) {
|
||||
setGroupChatMetadata({
|
||||
chatCode: remainingGroup.chatCode,
|
||||
members: remainingGroup.members,
|
||||
});
|
||||
}
|
||||
|
||||
broadcastLookingUpdate();
|
||||
|
||||
break;
|
||||
}
|
||||
case "REFRESH_GROUP": {
|
||||
await SQGroupRepository.refreshGroup(currentGroup.id);
|
||||
|
||||
|
||||
@@ -26,11 +26,6 @@ export const action = async ({ request }: ActionFunctionArgs) => {
|
||||
const ownGroup = SendouQ.findOwnGroup(user.id);
|
||||
errorToastIfFalsy(ownGroup, "No group found");
|
||||
|
||||
// no perms, possibly just lost them so no more graceful degradation
|
||||
if (ownGroup.usersRole === "REGULAR") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const season = Seasons.current();
|
||||
errorToastIfFalsy(season, "Season is not active");
|
||||
|
||||
@@ -67,10 +62,7 @@ export const action = async ({ request }: ActionFunctionArgs) => {
|
||||
|
||||
const { chatCodeToRevalidate } = await SQGroupRepository.insertMember(
|
||||
ownGroup.id,
|
||||
{
|
||||
userId: data.id,
|
||||
role: "MANAGER",
|
||||
},
|
||||
{ userId: data.id },
|
||||
);
|
||||
|
||||
if (chatCodeToRevalidate) {
|
||||
|
||||
@@ -94,10 +94,7 @@ export const action: ActionFunction = async ({ request, url }) => {
|
||||
|
||||
const { chatCodeToRevalidate } = await SQGroupRepository.insertMember(
|
||||
groupInvitedTo.id,
|
||||
{
|
||||
userId: user.id,
|
||||
role: "MANAGER",
|
||||
},
|
||||
{ userId: user.id },
|
||||
);
|
||||
|
||||
if (chatCodeToRevalidate) {
|
||||
|
||||
@@ -24,7 +24,6 @@ function createMember(overrides: Partial<SQGroupMember> = {}): SQGroupMember {
|
||||
discordAvatar: null,
|
||||
customAvatarUrl: null,
|
||||
customUrl: null,
|
||||
role: "OWNER",
|
||||
vc: "NO",
|
||||
languages: [],
|
||||
skill: "CALCULATING",
|
||||
@@ -52,7 +51,6 @@ function createGroup(
|
||||
tierRange: null,
|
||||
skillDifference: undefined,
|
||||
isReplay: false,
|
||||
usersRole: null,
|
||||
noScreen: false,
|
||||
modePreferences: [],
|
||||
status: "ACTIVE",
|
||||
@@ -75,7 +73,6 @@ function createOwnGroupMember(
|
||||
discordAvatar: null,
|
||||
customAvatarUrl: null,
|
||||
customUrl: null,
|
||||
role: "OWNER",
|
||||
vc: "NO",
|
||||
languages: [],
|
||||
skill: "CALCULATING",
|
||||
@@ -103,7 +100,6 @@ function createOwnGroup(
|
||||
tierRange: null,
|
||||
skillDifference: undefined,
|
||||
isReplay: false,
|
||||
usersRole: "OWNER",
|
||||
noScreen: false,
|
||||
modePreferences: [],
|
||||
chatCode: null,
|
||||
@@ -159,8 +155,8 @@ describe("GroupCard", () => {
|
||||
group: createGroup({
|
||||
members: [
|
||||
createMember({ id: 1, username: "Player1" }),
|
||||
createMember({ id: 2, username: "Player2", role: "MANAGER" }),
|
||||
createMember({ id: 3, username: "Player3", role: "REGULAR" }),
|
||||
createMember({ id: 2, username: "Player2" }),
|
||||
createMember({ id: 3, username: "Player3" }),
|
||||
],
|
||||
}),
|
||||
});
|
||||
@@ -260,9 +256,9 @@ describe("GroupCard", () => {
|
||||
tier,
|
||||
members: [
|
||||
createMember({ id: 1 }),
|
||||
createMember({ id: 2, role: "REGULAR" }),
|
||||
createMember({ id: 3, role: "REGULAR" }),
|
||||
createMember({ id: 4, role: "REGULAR" }),
|
||||
createMember({ id: 2 }),
|
||||
createMember({ id: 3 }),
|
||||
createMember({ id: 4 }),
|
||||
],
|
||||
}),
|
||||
});
|
||||
@@ -301,9 +297,9 @@ describe("GroupCard", () => {
|
||||
isReplay: true,
|
||||
members: [
|
||||
createMember({ id: 1 }),
|
||||
createMember({ id: 2, role: "REGULAR" }),
|
||||
createMember({ id: 3, role: "REGULAR" }),
|
||||
createMember({ id: 4, role: "REGULAR" }),
|
||||
createMember({ id: 2 }),
|
||||
createMember({ id: 3 }),
|
||||
createMember({ id: 4 }),
|
||||
],
|
||||
}),
|
||||
});
|
||||
@@ -377,24 +373,6 @@ describe("GroupCard", () => {
|
||||
// Actual translated text is "Start match"
|
||||
await expect.element(screen.getByText("Start match")).toBeVisible();
|
||||
});
|
||||
|
||||
test("hides actions when user is not owner or manager", async () => {
|
||||
// ownGroup with REGULAR role shouldn't show action buttons
|
||||
const ownGroup = createOwnGroup({ id: 2, usersRole: "REGULAR" });
|
||||
|
||||
const screen = await renderGroupCard({
|
||||
group: createGroup({ members: [createMember()] }),
|
||||
action: "LIKE",
|
||||
ownGroup,
|
||||
displayOnly: false,
|
||||
});
|
||||
|
||||
// Action button should not be rendered when user is not OWNER or MANAGER
|
||||
const actionButton = screen.container.querySelector(
|
||||
'[data-testid="group-card-action-button"]',
|
||||
);
|
||||
expect(actionButton).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("props", () => {
|
||||
|
||||
@@ -99,20 +99,6 @@
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
.star {
|
||||
min-width: 18px;
|
||||
max-width: 18px;
|
||||
color: var(--color-text-accent);
|
||||
}
|
||||
|
||||
.starFilled {
|
||||
fill: var(--color-text-accent);
|
||||
}
|
||||
|
||||
.starInactive {
|
||||
color: var(--color-text-high);
|
||||
}
|
||||
|
||||
.displayTier {
|
||||
display: flex;
|
||||
gap: var(--s-1);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import clsx from "clsx";
|
||||
import type { SqlBool } from "kysely";
|
||||
import { Mic, Star, Volume2, VolumeX } from "lucide-react";
|
||||
import { Mic, Volume2, VolumeX } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { Flipped } from "react-flip-toolkit";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -74,8 +74,6 @@ export function GroupCard({
|
||||
? resolveFutureMatchModes(ownGroup, group)
|
||||
: null;
|
||||
|
||||
const enableKicking = group.usersRole === "OWNER" && !displayOnly;
|
||||
|
||||
return (
|
||||
<GroupCardContainer
|
||||
groupId={group.id}
|
||||
@@ -89,13 +87,10 @@ export function GroupCard({
|
||||
return (
|
||||
<GroupMember
|
||||
member={member}
|
||||
showActions={group.usersRole === "OWNER"}
|
||||
key={member.discordId}
|
||||
displayOnly={displayOnly}
|
||||
hideVc={hideVc}
|
||||
hideWeapons={hideWeapons}
|
||||
hideNote={hideNote}
|
||||
enableKicking={enableKicking}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
@@ -199,9 +194,7 @@ export function GroupCard({
|
||||
{group.skillDifference ? (
|
||||
<GroupSkillDifference skillDifference={group.skillDifference} />
|
||||
) : null}
|
||||
{action &&
|
||||
(ownGroup?.usersRole === "OWNER" ||
|
||||
ownGroup?.usersRole === "MANAGER") ? (
|
||||
{action ? (
|
||||
<ActionButton
|
||||
schema={lookingSchema}
|
||||
action={action === "MATCH_UP_RECHALLENGE" ? "MATCH_UP" : action}
|
||||
@@ -246,20 +239,14 @@ function GroupCardContainer({
|
||||
|
||||
function GroupMember({
|
||||
member,
|
||||
showActions,
|
||||
displayOnly,
|
||||
hideVc,
|
||||
hideWeapons,
|
||||
hideNote,
|
||||
enableKicking,
|
||||
}: {
|
||||
member: SQGroupMember;
|
||||
showActions: boolean;
|
||||
displayOnly?: boolean;
|
||||
hideVc?: SqlBool;
|
||||
hideWeapons?: SqlBool;
|
||||
hideNote?: boolean;
|
||||
enableKicking?: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation(["q", "user"]);
|
||||
const user = useUser();
|
||||
@@ -298,13 +285,6 @@ function GroupMember({
|
||||
styles.memberActions,
|
||||
)}
|
||||
>
|
||||
{showActions || displayOnly ? (
|
||||
<MemberRoleManager
|
||||
member={member}
|
||||
displayOnly={displayOnly}
|
||||
enableKicking={enableKicking}
|
||||
/>
|
||||
) : null}
|
||||
{member.skill ? <TierInfo skill={member.skill} /> : null}
|
||||
</div>
|
||||
</div>
|
||||
@@ -509,83 +489,6 @@ function MemberSkillDifference({
|
||||
);
|
||||
}
|
||||
|
||||
function MemberRoleManager({
|
||||
member,
|
||||
displayOnly,
|
||||
enableKicking,
|
||||
}: {
|
||||
member: Pick<SQGroupMember, "id" | "role">;
|
||||
displayOnly?: boolean;
|
||||
enableKicking?: boolean;
|
||||
}) {
|
||||
const loggedInUser = useUser();
|
||||
const { t } = useTranslation(["q"]);
|
||||
|
||||
if (displayOnly && member.role !== "OWNER") return null;
|
||||
|
||||
return (
|
||||
<SendouPopover
|
||||
trigger={
|
||||
<SendouButton
|
||||
variant="minimal"
|
||||
icon={
|
||||
<Star
|
||||
className={clsx(styles.star, {
|
||||
[styles.starFilled]: member.role === "OWNER",
|
||||
[styles.starInactive]: member.role === "REGULAR",
|
||||
})}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className="stack sm items-center">
|
||||
<div>{t(`q:roles.${member.role}`)}</div>
|
||||
{member.role !== "OWNER" && !displayOnly ? (
|
||||
<div className="stack md items-center">
|
||||
{member.role === "REGULAR" ? (
|
||||
<ActionButton
|
||||
schema={lookingSchema}
|
||||
action="GIVE_MANAGER"
|
||||
fields={{ userId: member.id }}
|
||||
formAction={SENDOUQ_LOOKING_PAGE}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
>
|
||||
{t("q:looking.groups.actions.giveManager")}
|
||||
</ActionButton>
|
||||
) : null}
|
||||
{member.role === "MANAGER" ? (
|
||||
<ActionButton
|
||||
schema={lookingSchema}
|
||||
action="REMOVE_MANAGER"
|
||||
fields={{ userId: member.id }}
|
||||
formAction={SENDOUQ_LOOKING_PAGE}
|
||||
variant="destructive"
|
||||
size="small"
|
||||
>
|
||||
{t("q:looking.groups.actions.removeManager")}
|
||||
</ActionButton>
|
||||
) : null}
|
||||
{enableKicking && member.id !== loggedInUser?.id ? (
|
||||
<ActionButton
|
||||
schema={lookingSchema}
|
||||
action="KICK_FROM_GROUP"
|
||||
fields={{ userId: member.id }}
|
||||
formAction={SENDOUQ_LOOKING_PAGE}
|
||||
variant="destructive"
|
||||
size="small"
|
||||
>
|
||||
{t("q:looking.groups.actions.kick")}
|
||||
</ActionButton>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</SendouPopover>
|
||||
);
|
||||
}
|
||||
|
||||
function TierInfo({ skill }: { skill: TieredSkill | "CALCULATING" }) {
|
||||
const { t } = useTranslation(["q"]);
|
||||
|
||||
|
||||
@@ -138,28 +138,6 @@ describe("SendouQ", () => {
|
||||
expect(group).toBeUndefined();
|
||||
});
|
||||
|
||||
test("returns group with correct role when user is OWNER", async () => {
|
||||
await createGroup([1, 2]);
|
||||
await refreshSendouQInstance();
|
||||
|
||||
const group = SendouQ.findOwnGroup(users.id(1));
|
||||
|
||||
expect(group).toBeDefined();
|
||||
const member = group?.members.find((m) => m.id === users.id(1));
|
||||
expect(member?.role).toBe("OWNER");
|
||||
});
|
||||
|
||||
test("returns group with correct role when user is REGULAR member", async () => {
|
||||
await createGroup([1, 2]);
|
||||
await refreshSendouQInstance();
|
||||
|
||||
const group = SendouQ.findOwnGroup(users.id(2));
|
||||
|
||||
expect(group).toBeDefined();
|
||||
const member = group?.members.find((m) => m.id === users.id(2));
|
||||
expect(member?.role).toBe("REGULAR");
|
||||
});
|
||||
|
||||
test("returns correct group when multiple groups exist", async () => {
|
||||
await createGroup([1, 2]);
|
||||
await createGroup([3, 4]);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { isWithinInterval, sub } from "date-fns";
|
||||
import * as R from "remeda";
|
||||
import type { DBBoolean, Tables } from "~/db/tables";
|
||||
import type { DBBoolean } from "~/db/tables";
|
||||
import type { ParsedMemento } from "~/db/tables-json";
|
||||
import type { AuthenticatedUser } from "~/features/auth/core/user.server";
|
||||
import * as Seasons from "~/features/mmr/core/Seasons";
|
||||
@@ -79,7 +79,6 @@ class SendouQClass {
|
||||
skillDifference:
|
||||
undefined as ParsedMemento["groups"][number]["skillDifference"],
|
||||
isReplay: false,
|
||||
usersRole: null as Tables["GroupMember"]["role"] | null,
|
||||
members: group.members.map((member) => {
|
||||
const skill = calculatedUserSkills[String(member.id)];
|
||||
|
||||
@@ -116,20 +115,12 @@ class SendouQClass {
|
||||
|
||||
/**
|
||||
* Finds the group that a user belongs to.
|
||||
* @returns The user's group with their role, or undefined if not in a group
|
||||
* @returns The user's group, or undefined if not in a group
|
||||
*/
|
||||
findOwnGroup(userId: number) {
|
||||
const result = this.groups.find((group) =>
|
||||
return this.groups.find((group) =>
|
||||
group.members.some((member) => member.id === userId),
|
||||
);
|
||||
if (!result) return;
|
||||
|
||||
const member = result.members.find((m) => m.id === userId)!;
|
||||
|
||||
return {
|
||||
...result,
|
||||
usersRole: member.role,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -44,21 +44,9 @@ export const lookingSchema = z.union([
|
||||
_action: _action("MATCH_UP"),
|
||||
targetGroupId: id,
|
||||
}),
|
||||
z.object({
|
||||
_action: _action("GIVE_MANAGER"),
|
||||
userId: id,
|
||||
}),
|
||||
z.object({
|
||||
_action: _action("REMOVE_MANAGER"),
|
||||
userId: id,
|
||||
}),
|
||||
z.object({
|
||||
_action: _action("LEAVE_GROUP"),
|
||||
}),
|
||||
z.object({
|
||||
_action: _action("KICK_FROM_GROUP"),
|
||||
userId: id,
|
||||
}),
|
||||
z.object({
|
||||
_action: _action("REFRESH_GROUP"),
|
||||
}),
|
||||
|
||||
@@ -46,9 +46,7 @@ export default function QPreparingPage() {
|
||||
<div className={styles.cardContainer}>
|
||||
<GroupCard group={data.group} hideNote ownGroup={data.group} />
|
||||
</div>
|
||||
{data.group.members.length < FULL_GROUP_SIZE &&
|
||||
(data.group.usersRole === "OWNER" ||
|
||||
data.group.usersRole === "MANAGER") ? (
|
||||
{data.group.members.length < FULL_GROUP_SIZE ? (
|
||||
<MemberAdder
|
||||
inviteCode={data.group.inviteCode}
|
||||
groupMemberIds={data.group.members.map((m) => m.id)}
|
||||
|
||||
@@ -14,7 +14,6 @@ import { Image } from "~/components/Image";
|
||||
import { LocaleTime } from "~/components/LocaleTime";
|
||||
import { LocaleTimeRange } from "~/components/LocaleTimeRange";
|
||||
import { Main } from "~/components/Main";
|
||||
import type { Tables } from "~/db/tables";
|
||||
import { useUser } from "~/features/auth/core/user";
|
||||
import type * as Seasons from "~/features/mmr/core/Seasons";
|
||||
import { useDateTimeFormat } from "~/hooks/intl/useDateTimeFormat";
|
||||
@@ -228,7 +227,6 @@ function JoinTeamDialog({
|
||||
close: () => void;
|
||||
members: {
|
||||
username: string;
|
||||
role: Tables["GroupMember"]["role"];
|
||||
}[];
|
||||
}) {
|
||||
const { t, i18n } = useTranslation(["q"]);
|
||||
|
||||
@@ -191,7 +191,8 @@ test.describe("SendouQ match page", () => {
|
||||
isConcluded: true,
|
||||
});
|
||||
|
||||
await impersonate(page, bravo[0].id);
|
||||
// any member can re-queue the group, not only the member who created it
|
||||
await impersonate(page, bravo[1].id);
|
||||
const match = new SendouQMatchPage(page);
|
||||
await match.goto(matchId);
|
||||
await match.lookAgain();
|
||||
|
||||
@@ -66,7 +66,6 @@
|
||||
"looking.groups.actions.undo": "",
|
||||
"looking.groups.actions.giveManager": "",
|
||||
"looking.groups.actions.removeManager": "",
|
||||
"looking.groups.actions.kick": "",
|
||||
"looking.groups.actions.leaveGroup": "",
|
||||
"looking.groups.actions.leaveGroup.confirm": "",
|
||||
"looking.groups.actions.stopLooking": "",
|
||||
@@ -188,7 +187,6 @@
|
||||
"match.rematch.vote.noConfirm": "",
|
||||
"match.rematch.declined": "",
|
||||
"match.rematch.fizzled": "",
|
||||
"match.rematch.waitingCaptain": "",
|
||||
"match.rematch.rejoinQueue": "",
|
||||
"match.rematch.backToQueue": "",
|
||||
"match.rematch.offSeason": "",
|
||||
|
||||
@@ -66,7 +66,6 @@
|
||||
"looking.groups.actions.undo": "",
|
||||
"looking.groups.actions.giveManager": "",
|
||||
"looking.groups.actions.removeManager": "",
|
||||
"looking.groups.actions.kick": "",
|
||||
"looking.groups.actions.leaveGroup": "",
|
||||
"looking.groups.actions.leaveGroup.confirm": "",
|
||||
"looking.groups.actions.stopLooking": "",
|
||||
@@ -188,7 +187,6 @@
|
||||
"match.rematch.vote.noConfirm": "",
|
||||
"match.rematch.declined": "",
|
||||
"match.rematch.fizzled": "",
|
||||
"match.rematch.waitingCaptain": "",
|
||||
"match.rematch.rejoinQueue": "",
|
||||
"match.rematch.backToQueue": "",
|
||||
"match.rematch.offSeason": "",
|
||||
|
||||
@@ -66,7 +66,6 @@
|
||||
"looking.groups.actions.undo": "Undo",
|
||||
"looking.groups.actions.giveManager": "Give manager",
|
||||
"looking.groups.actions.removeManager": "Remove manager",
|
||||
"looking.groups.actions.kick": "Kick",
|
||||
"looking.groups.actions.leaveGroup": "Leave group",
|
||||
"looking.groups.actions.leaveGroup.confirm": "Leave this group?",
|
||||
"looking.groups.actions.stopLooking": "Stop looking",
|
||||
@@ -188,7 +187,6 @@
|
||||
"match.rematch.vote.noConfirm": "Vote no? You can't change your vote afterwards.",
|
||||
"match.rematch.declined": "You declined to continue",
|
||||
"match.rematch.fizzled": "Nobody wanted to continue",
|
||||
"match.rematch.waitingCaptain": "Waiting for the captain to choose whether to re-queue",
|
||||
"match.rematch.rejoinQueue": "Rejoin queue",
|
||||
"match.rematch.backToQueue": "Back to queue",
|
||||
"match.rematch.offSeason": "Season has ended. The queue will reopen when the next season starts.",
|
||||
|
||||
@@ -66,7 +66,6 @@
|
||||
"looking.groups.actions.undo": "Deshacer",
|
||||
"looking.groups.actions.giveManager": "Hacer mánager",
|
||||
"looking.groups.actions.removeManager": "Quitar mánager",
|
||||
"looking.groups.actions.kick": "Expulsar",
|
||||
"looking.groups.actions.leaveGroup": "Dejar grupo",
|
||||
"looking.groups.actions.leaveGroup.confirm": "¿Abandonar este grupo?",
|
||||
"looking.groups.actions.stopLooking": "Dejar de buscar",
|
||||
@@ -188,7 +187,6 @@
|
||||
"match.rematch.vote.noConfirm": "¿Votar no? No podrás cambiar tu voto después.",
|
||||
"match.rematch.declined": "Rechazaste continuar",
|
||||
"match.rematch.fizzled": "Nadie quiso continuar",
|
||||
"match.rematch.waitingCaptain": "Esperando a que el capitán decida si volver a la cola",
|
||||
"match.rematch.rejoinQueue": "Volver a la cola",
|
||||
"match.rematch.backToQueue": "Volver a la cola",
|
||||
"match.rematch.offSeason": "La temporada ha terminado. La cola volverá a abrir cuando empiece la siguiente temporada.",
|
||||
|
||||
@@ -66,7 +66,6 @@
|
||||
"looking.groups.actions.undo": "Deshacer",
|
||||
"looking.groups.actions.giveManager": "Hacer mánager",
|
||||
"looking.groups.actions.removeManager": "Quitar mánager",
|
||||
"looking.groups.actions.kick": "Expulsar",
|
||||
"looking.groups.actions.leaveGroup": "Dejar grupo",
|
||||
"looking.groups.actions.leaveGroup.confirm": "",
|
||||
"looking.groups.actions.stopLooking": "",
|
||||
@@ -188,7 +187,6 @@
|
||||
"match.rematch.vote.noConfirm": "",
|
||||
"match.rematch.declined": "",
|
||||
"match.rematch.fizzled": "",
|
||||
"match.rematch.waitingCaptain": "",
|
||||
"match.rematch.rejoinQueue": "",
|
||||
"match.rematch.backToQueue": "",
|
||||
"match.rematch.offSeason": "",
|
||||
|
||||
@@ -66,7 +66,6 @@
|
||||
"looking.groups.actions.undo": "",
|
||||
"looking.groups.actions.giveManager": "",
|
||||
"looking.groups.actions.removeManager": "",
|
||||
"looking.groups.actions.kick": "",
|
||||
"looking.groups.actions.leaveGroup": "",
|
||||
"looking.groups.actions.leaveGroup.confirm": "",
|
||||
"looking.groups.actions.stopLooking": "",
|
||||
@@ -188,7 +187,6 @@
|
||||
"match.rematch.vote.noConfirm": "",
|
||||
"match.rematch.declined": "",
|
||||
"match.rematch.fizzled": "",
|
||||
"match.rematch.waitingCaptain": "",
|
||||
"match.rematch.rejoinQueue": "",
|
||||
"match.rematch.backToQueue": "",
|
||||
"match.rematch.offSeason": "",
|
||||
|
||||
@@ -66,7 +66,6 @@
|
||||
"looking.groups.actions.undo": "Annulé",
|
||||
"looking.groups.actions.giveManager": "Promouvoir",
|
||||
"looking.groups.actions.removeManager": "Rétrograder",
|
||||
"looking.groups.actions.kick": "Kick",
|
||||
"looking.groups.actions.leaveGroup": "Quitter le groupe",
|
||||
"looking.groups.actions.leaveGroup.confirm": "",
|
||||
"looking.groups.actions.stopLooking": "",
|
||||
@@ -188,7 +187,6 @@
|
||||
"match.rematch.vote.noConfirm": "",
|
||||
"match.rematch.declined": "",
|
||||
"match.rematch.fizzled": "",
|
||||
"match.rematch.waitingCaptain": "",
|
||||
"match.rematch.rejoinQueue": "",
|
||||
"match.rematch.backToQueue": "",
|
||||
"match.rematch.offSeason": "",
|
||||
|
||||
@@ -66,7 +66,6 @@
|
||||
"looking.groups.actions.undo": "",
|
||||
"looking.groups.actions.giveManager": "",
|
||||
"looking.groups.actions.removeManager": "",
|
||||
"looking.groups.actions.kick": "",
|
||||
"looking.groups.actions.leaveGroup": "",
|
||||
"looking.groups.actions.leaveGroup.confirm": "",
|
||||
"looking.groups.actions.stopLooking": "",
|
||||
@@ -188,7 +187,6 @@
|
||||
"match.rematch.vote.noConfirm": "",
|
||||
"match.rematch.declined": "",
|
||||
"match.rematch.fizzled": "",
|
||||
"match.rematch.waitingCaptain": "",
|
||||
"match.rematch.rejoinQueue": "",
|
||||
"match.rematch.backToQueue": "",
|
||||
"match.rematch.offSeason": "",
|
||||
|
||||
@@ -66,7 +66,6 @@
|
||||
"looking.groups.actions.undo": "Annulla",
|
||||
"looking.groups.actions.giveManager": "Dai manager",
|
||||
"looking.groups.actions.removeManager": "Rimuovi manager",
|
||||
"looking.groups.actions.kick": "Caccia",
|
||||
"looking.groups.actions.leaveGroup": "Lascia gruppo",
|
||||
"looking.groups.actions.leaveGroup.confirm": "",
|
||||
"looking.groups.actions.stopLooking": "",
|
||||
@@ -188,7 +187,6 @@
|
||||
"match.rematch.vote.noConfirm": "",
|
||||
"match.rematch.declined": "",
|
||||
"match.rematch.fizzled": "",
|
||||
"match.rematch.waitingCaptain": "",
|
||||
"match.rematch.rejoinQueue": "",
|
||||
"match.rematch.backToQueue": "",
|
||||
"match.rematch.offSeason": "",
|
||||
|
||||
@@ -66,7 +66,6 @@
|
||||
"looking.groups.actions.undo": "戻す",
|
||||
"looking.groups.actions.giveManager": "マネージャーにあげる",
|
||||
"looking.groups.actions.removeManager": "マネージャーを外す",
|
||||
"looking.groups.actions.kick": "キックする",
|
||||
"looking.groups.actions.leaveGroup": "グループを出る",
|
||||
"looking.groups.actions.leaveGroup.confirm": "",
|
||||
"looking.groups.actions.stopLooking": "",
|
||||
@@ -188,7 +187,6 @@
|
||||
"match.rematch.vote.noConfirm": "",
|
||||
"match.rematch.declined": "",
|
||||
"match.rematch.fizzled": "",
|
||||
"match.rematch.waitingCaptain": "",
|
||||
"match.rematch.rejoinQueue": "",
|
||||
"match.rematch.backToQueue": "",
|
||||
"match.rematch.offSeason": "",
|
||||
|
||||
@@ -66,7 +66,6 @@
|
||||
"looking.groups.actions.undo": "",
|
||||
"looking.groups.actions.giveManager": "",
|
||||
"looking.groups.actions.removeManager": "",
|
||||
"looking.groups.actions.kick": "",
|
||||
"looking.groups.actions.leaveGroup": "",
|
||||
"looking.groups.actions.leaveGroup.confirm": "",
|
||||
"looking.groups.actions.stopLooking": "",
|
||||
@@ -188,7 +187,6 @@
|
||||
"match.rematch.vote.noConfirm": "",
|
||||
"match.rematch.declined": "",
|
||||
"match.rematch.fizzled": "",
|
||||
"match.rematch.waitingCaptain": "",
|
||||
"match.rematch.rejoinQueue": "",
|
||||
"match.rematch.backToQueue": "",
|
||||
"match.rematch.offSeason": "",
|
||||
|
||||
@@ -66,7 +66,6 @@
|
||||
"looking.groups.actions.undo": "",
|
||||
"looking.groups.actions.giveManager": "",
|
||||
"looking.groups.actions.removeManager": "",
|
||||
"looking.groups.actions.kick": "",
|
||||
"looking.groups.actions.leaveGroup": "",
|
||||
"looking.groups.actions.leaveGroup.confirm": "",
|
||||
"looking.groups.actions.stopLooking": "",
|
||||
@@ -188,7 +187,6 @@
|
||||
"match.rematch.vote.noConfirm": "",
|
||||
"match.rematch.declined": "",
|
||||
"match.rematch.fizzled": "",
|
||||
"match.rematch.waitingCaptain": "",
|
||||
"match.rematch.rejoinQueue": "",
|
||||
"match.rematch.backToQueue": "",
|
||||
"match.rematch.offSeason": "",
|
||||
|
||||
@@ -66,7 +66,6 @@
|
||||
"looking.groups.actions.undo": "",
|
||||
"looking.groups.actions.giveManager": "",
|
||||
"looking.groups.actions.removeManager": "",
|
||||
"looking.groups.actions.kick": "",
|
||||
"looking.groups.actions.leaveGroup": "",
|
||||
"looking.groups.actions.leaveGroup.confirm": "",
|
||||
"looking.groups.actions.stopLooking": "",
|
||||
@@ -188,7 +187,6 @@
|
||||
"match.rematch.vote.noConfirm": "",
|
||||
"match.rematch.declined": "",
|
||||
"match.rematch.fizzled": "",
|
||||
"match.rematch.waitingCaptain": "",
|
||||
"match.rematch.rejoinQueue": "",
|
||||
"match.rematch.backToQueue": "",
|
||||
"match.rematch.offSeason": "",
|
||||
|
||||
@@ -66,7 +66,6 @@
|
||||
"looking.groups.actions.undo": "Desfazer",
|
||||
"looking.groups.actions.giveManager": "Dar gerência",
|
||||
"looking.groups.actions.removeManager": "Remover gerência",
|
||||
"looking.groups.actions.kick": "Chutar (kick)",
|
||||
"looking.groups.actions.leaveGroup": "Sair do grupo",
|
||||
"looking.groups.actions.leaveGroup.confirm": "",
|
||||
"looking.groups.actions.stopLooking": "",
|
||||
@@ -188,7 +187,6 @@
|
||||
"match.rematch.vote.noConfirm": "",
|
||||
"match.rematch.declined": "",
|
||||
"match.rematch.fizzled": "",
|
||||
"match.rematch.waitingCaptain": "",
|
||||
"match.rematch.rejoinQueue": "",
|
||||
"match.rematch.backToQueue": "",
|
||||
"match.rematch.offSeason": "",
|
||||
|
||||
@@ -66,7 +66,6 @@
|
||||
"looking.groups.actions.undo": "Отменить",
|
||||
"looking.groups.actions.giveManager": "Дать роль менеджера",
|
||||
"looking.groups.actions.removeManager": "Удалить роль менеджера",
|
||||
"looking.groups.actions.kick": "Выгнать",
|
||||
"looking.groups.actions.leaveGroup": "Покинуть группу",
|
||||
"looking.groups.actions.leaveGroup.confirm": "",
|
||||
"looking.groups.actions.stopLooking": "",
|
||||
@@ -188,7 +187,6 @@
|
||||
"match.rematch.vote.noConfirm": "",
|
||||
"match.rematch.declined": "",
|
||||
"match.rematch.fizzled": "",
|
||||
"match.rematch.waitingCaptain": "",
|
||||
"match.rematch.rejoinQueue": "",
|
||||
"match.rematch.backToQueue": "",
|
||||
"match.rematch.offSeason": "",
|
||||
|
||||
@@ -66,7 +66,6 @@
|
||||
"looking.groups.actions.undo": "撤销",
|
||||
"looking.groups.actions.giveManager": "授予管理者权限",
|
||||
"looking.groups.actions.removeManager": "移除管理者权限",
|
||||
"looking.groups.actions.kick": "踢出",
|
||||
"looking.groups.actions.leaveGroup": "离开小组",
|
||||
"looking.groups.actions.leaveGroup.confirm": "要离开此小组吗?",
|
||||
"looking.groups.actions.stopLooking": "停止匹配",
|
||||
@@ -188,7 +187,6 @@
|
||||
"match.rematch.vote.noConfirm": "确认投反对票吗?投票后将无法更改。",
|
||||
"match.rematch.declined": "您已拒绝继续排队",
|
||||
"match.rematch.fizzled": "无人想要继续排队",
|
||||
"match.rematch.waitingCaptain": "正在等待队长选择是否重新排队",
|
||||
"match.rematch.rejoinQueue": "重新加入队列",
|
||||
"match.rematch.backToQueue": "返回队列",
|
||||
"match.rematch.offSeason": "赛季已结束。队列将在下个赛季开始时重新开放。",
|
||||
|
||||
7
migrations/20260807120658-remove-group-member-role.ts
Normal file
7
migrations/20260807120658-remove-group-member-role.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { Kysely } from "kysely";
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await db.transaction().execute(async (trx) => {
|
||||
await trx.schema.alterTable("GroupMember").dropColumn("role").execute();
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user