Skip to content

Commit

Permalink
Merge branch 'main' into openhands-fix-issue-389
Browse files Browse the repository at this point in the history
  • Loading branch information
MathiasGruber authored Feb 21, 2025
2 parents 6a3a5dc + a234f76 commit 35bfda3
Show file tree
Hide file tree
Showing 5 changed files with 196 additions and 100 deletions.
47 changes: 47 additions & 0 deletions app/src/app/profile/edit/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ import UserRequestSystem from "@/layout/UserRequestSystem";
import type { Gender } from "@/validators/register";
import type { BaseServerResponse } from "@/server/api/trpc";
import type { Bloodline, Village } from "@/drizzle/schema";
import {
AiRule,
ConditionDistanceHigherThan,
ActionMoveTowardsOpponent,
} from "@/validators/ai";

export default function EditProfile() {
// State
Expand Down Expand Up @@ -293,6 +298,17 @@ const BattleSettingsEdit: React.FC<{ userId: string }> = ({ userId }) => {
},
});

const { mutate: updateAiProfile, isPending } =
api.ai.updateAiProfile.useMutation({
onSuccess: async (data) => {
showMutationToast(data);
if (data.success) {
await utils.profile.getAi.invalidate();
await utils.profile.getPublicUser.invalidate();
}
},
});

// Update highest preferences
const { mutate: updatePreferences } = api.profile.updatePreferences.useMutation({
onSuccess: async (data) => {
Expand Down Expand Up @@ -445,6 +461,37 @@ const BattleSettingsEdit: React.FC<{ userId: string }> = ({ userId }) => {
}
/>
<Label htmlFor="battle-description">Show battle descriptions</Label>
< br/>
< br/>
<Confirm
title="Reset AI Profile"
button={
<Button
variant="destructive"
size="sm"
disabled={!profile?.aiProfileId || isPending}
>
{isPending ? <Loader size={5} /> : "Reset AI Profile"}
</Button>
}
onAccept={() => {
if (!profile?.aiProfileId) return;
const defaultAiProfilePayload = {
id: profile.aiProfileId,
rules: [
AiRule.parse({
conditions: [ConditionDistanceHigherThan.parse({ value: 2 })],
action: ActionMoveTowardsOpponent.parse({}),
}),
],
includeDefaultRules: true,
};
updateAiProfile(defaultAiProfilePayload);
}}
>
This will reset your AI profile to default settings. This action cannot be undone. Are you sure you want to continue?
</Confirm>

</TabsContent>
<TabsContent value="aiprofile">
<AiProfileEdit userData={profile} hideTitle />
Expand Down
137 changes: 72 additions & 65 deletions app/src/layout/Clan.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ import type { MutateContentSchema } from "@/validators/comments";
import type { UserNindo } from "@/drizzle/schema";
import type { ArrayElement } from "@/utils/typeutils";
import type { ClanRouter } from "@/routers/clan";
import { canEditClans } from "@/utils/permissions";

export const ClansOverview: React.FC = () => {
// Must be in allied village
Expand Down Expand Up @@ -1209,73 +1210,79 @@ export const ClanMembers: React.FC<ClanMembersProps> = (props) => {
// Derived
const isColeader = checkCoLeader(userId, clanData);
const isLeader = userId === clanData.leaderId;
const canEdit = userData ? canEditClans(userData.role) : false;

// Adjust members for table
const members = clanData.members
.map((member) => {
const memberIsLeader = member.userId === clanData.leaderId;
const memberIsColeader = checkCoLeader(member.userId, clanData);
const memberLeaderLike = memberIsLeader || memberIsColeader;
return {
...member,
rank: memberIsLeader
? "Leader"
: memberIsColeader
? "Coleader"
: showUserRank(member),
actions: (
<div className="flex flex-row gap-1">
{member.userId !== userId && (
<>
{(isLeader || isColeader) && !memberLeaderLike && (
<Confirm
title="Kick Member"
proceed_label="Submit"
button={
<Button id={`kick-${member.userId}`}>
<DoorOpen className="mr-2 h-5 w-5" />
Kick
</Button>
}
onAccept={() => kick({ clanId, memberId: member.userId })}
>
Confirm that you want to kick this member from the clan.
</Confirm>
)}
{isLeader && memberLeaderLike && (
<Confirm
title="Demote Member"
button={
<Button id={`demote-${member.userId}`}>
<ArrowBigDownDash className="mr-2 h-5 w-5" />
Demote
</Button>
}
onAccept={() => demote({ clanId, memberId: member.userId })}
>
Confirm that you want to demote this member.
</Confirm>
)}
{(isLeader || (isColeader && !memberLeaderLike)) && (
<Confirm
title="Promote Member"
button={
<Button id={`promote-${member.userId}`}>
<ArrowBigUpDash className="mr-2 h-5 w-5" />
Promote
</Button>
}
onAccept={() => promote({ clanId, memberId: member.userId })}
>
Confirm that you want to promote this member to leader of the clan.
</Confirm>
)}
</>
)}
</div>
),
};
})
const members = clanData.members.map((member) => {
const memberIsLeader = member.userId === clanData.leaderId;
const memberIsColeader = checkCoLeader(member.userId, clanData);
const canKick =
canEdit || // canEdit role can kick anyone
(isLeader && !memberIsLeader) || // Leader can kick anyone except other leaders
(isColeader && !memberIsLeader && !memberIsColeader); // Co-leaders can kick normal members only
return {
...member,
rank: memberIsLeader ? "Leader" : memberIsColeader ? "Coleader" : showUserRank(member),
actions: (
<div className="flex flex-row gap-1">
{member.userId !== userId && (
<>
{/* KICK BUTTON (Now allows kicking leaders if canEdit is true) */}
{canKick && (
<Confirm
title="Kick Member"
proceed_label="Submit"
button={
<Button id={`kick-${member.userId}`}>
<DoorOpen className="mr-2 h-5 w-5" />
Kick
</Button>
}
onAccept={() => kick({ clanId, memberId: member.userId })}
>
{memberIsLeader
? "You are about to kick the leader. Ensure leadership transition is planned."
: "Confirm that you want to kick this member from the clan."}
</Confirm>
)}

{/* DEMOTE BUTTON */}
{(isLeader || canEdit) && (
<Confirm
title="Demote Member"
button={
<Button id={`demote-${member.userId}`}>
<ArrowBigDownDash className="mr-2 h-5 w-5" />
Demote
</Button>
}
onAccept={() => demote({ clanId, memberId: member.userId })}
>
Confirm that you want to demote this member.
</Confirm>
)}

{/* PROMOTE BUTTON */}
{(isLeader || (isColeader && !memberIsLeader && !memberIsColeader) || canEdit) && (
<Confirm
title="Promote Member"
button={
<Button id={`promote-${member.userId}`}>
<ArrowBigUpDash className="mr-2 h-5 w-5" />
Promote
</Button>
}
onAccept={() => promote({ clanId, memberId: member.userId })}
>
Confirm that you want to promote this member.
</Confirm>
)}
</>
)}
</div>
),
};
})
.sort((a, b) => {
if (a.rank === "Leader") return -1;
if (b.rank === "Leader") return 1;
Expand Down
98 changes: 63 additions & 35 deletions app/src/server/api/routers/clan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { fetchUser, updateNindo } from "@/routers/profile";
import { getServerPusher } from "@/libs/pusher";
import { clanCreateSchema, checkCoLeader } from "@/validators/clan";
import { hasRequiredRank } from "@/libs/train";
import { canEditClans } from "@/utils/permissions";
import { checkIfSectorIsAvailable } from "@/libs/clan";
import {
fetchRequest,
Expand Down Expand Up @@ -553,39 +554,58 @@ export const clanRouter = createTRPCRouter({
const isLeader = user.userId === fetchedClan?.leaderId;
const isColeader = checkCoLeader(user.userId, fetchedClan);
const isMemberColeader = checkCoLeader(input.memberId, fetchedClan);
const updateData = (() => {
if (isMemberColeader && isLeader)
return {
leaderId: input.memberId,
coLeader1:
fetchedClan.coLeader1 === input.memberId ? null : fetchedClan.coLeader1,
coLeader2:
fetchedClan.coLeader2 === input.memberId ? null : fetchedClan.coLeader2,
coLeader3:
fetchedClan.coLeader3 === input.memberId ? null : fetchedClan.coLeader3,
coLeader4:
fetchedClan.coLeader4 === input.memberId ? null : fetchedClan.coLeader4,
};
if (!fetchedClan?.coLeader1) return { coLeader1: input.memberId };
if (!fetchedClan?.coLeader2) return { coLeader2: input.memberId };
if (!fetchedClan?.coLeader3) return { coLeader3: input.memberId };
if (!fetchedClan?.coLeader4) return { coLeader4: input.memberId };
return null;
})();
const canEdit = canEditClans(user.role);
const isMemberLeader = input.memberId === fetchedClan?.leaderId;
// Guards
if (!user) return errorResponse("User not found");
if (!member) return errorResponse("Member not found");
if (!isLeader && !isColeader)
if (member.userId === user.userId) return errorResponse("Not yourself");
if (isMemberLeader)
return errorResponse(`${groupLabel} leader cannot be promoted`);
if (!isLeader && !isColeader && !canEdit)
return errorResponse(`Only ${groupLabel} leaders can promote`);
if (isMemberColeader && !isLeader)
if (isMemberColeader && !isLeader && !canEdit)
return errorResponse(`Only for ${groupLabel} leader`);
if (member.userId === user.userId) return errorResponse("Not yourself");
if (member.clanId !== fetchedClan.id)
return errorResponse(`Not in ${groupLabel}`);
if (!updateData)
return errorResponse(`No more co-leaders can be added to ${groupLabel}`);
if (!hasRequiredRank(member.rank, CLAN_RANK_REQUIREMENT)) {
if (!hasRequiredRank(member.rank, CLAN_RANK_REQUIREMENT))
return errorResponse("Leader rank too low");
let updateData = null;
// If member is a co-leader and the user is either a leader or has canEdit, promote them to leader
if (isMemberColeader && (isLeader || canEdit)) {
updateData = {
leaderId: input.memberId, // Promote co-leader to leader
coLeader1:
fetchedClan.coLeader1 === input.memberId
? fetchedClan.leaderId
: fetchedClan.coLeader1, // Demote leader to first available co-leader slot
coLeader2:
fetchedClan.coLeader2 === input.memberId
? fetchedClan.coLeader1
: fetchedClan.coLeader2,
coLeader3:
fetchedClan.coLeader3 === input.memberId
? fetchedClan.coLeader2
: fetchedClan.coLeader3,
coLeader4:
fetchedClan.coLeader4 === input.memberId
? fetchedClan.coLeader3
: fetchedClan.coLeader4,
};
}
// If member is NOT a co-leader, allow promotion to co-leader instead
else if (!isMemberColeader) {
if (!fetchedClan.coLeader1) updateData = { coLeader1: input.memberId };
else if (!fetchedClan.coLeader2) updateData = { coLeader2: input.memberId };
else if (!fetchedClan.coLeader3) updateData = { coLeader3: input.memberId };
else if (!fetchedClan.coLeader4) updateData = { coLeader4: input.memberId };
else return errorResponse(`No more co-leader slots available in ${groupLabel}`);
}
// If the member is already a co-leader but the user isn't a leader or can't edit, deny the promotion attempt
else {
return errorResponse(
`Only ${groupLabel} leader or canEdit users can promote to leader`,
);
}
// Mutate
await ctx.drizzle.update(clan).set(updateData).where(eq(clan.id, fetchedClan.id));
Expand All @@ -607,22 +627,19 @@ export const clanRouter = createTRPCRouter({
const isColeader = checkCoLeader(user.userId, clanData);
const isMemberLeader = input.memberId === clanData?.leaderId;
const isMemberColeader = checkCoLeader(input.memberId, clanData);
const canEdit = canEditClans(user.role);
const isYourself = ctx.userId === input.memberId;
const groupLabel = user?.isOutlaw ? "faction" : "clan";
// Guards
if (!clanData) return errorResponse(`${groupLabel} not found`);
if (!user) return errorResponse("User not found");
if (!member) return errorResponse("Member not found");
if (!isLeader && !isColeader)
return errorResponse(`Only ${groupLabel} leaders can demote`);
if (!isLeader && !isColeader && !canEdit) return errorResponse(`Not allowed`);
if (isMemberLeader)
return errorResponse(`New ${groupLabel} leader must be promoted first`);
if (member.clanId !== clanData.id) return errorResponse(`Not in ${groupLabel}`);
if (!hasRequiredRank(member.rank, CLAN_RANK_REQUIREMENT)) {
return errorResponse("Leader rank too low");
}
if (isMemberColeader && !isLeader && !isYourself) {
return errorResponse(`Only for ${groupLabel} leader`);
if (isMemberColeader && !isLeader && !canEdit && !isYourself) {
return errorResponse(`Only ${groupLabel} leader can demote`);
}
// Mutate
await ctx.drizzle
Expand Down Expand Up @@ -652,17 +669,28 @@ export const clanRouter = createTRPCRouter({
const isColeader = checkCoLeader(user.userId, fetchedClan);
const isMemberColeader = checkCoLeader(input.memberId, fetchedClan);
const isMemberLeader = input.memberId === fetchedClan?.leaderId;
const canEdit = canEditClans(user.role);
const groupLabel = user?.isOutlaw ? "faction" : "clan";
// Guards
if (!fetchedClan) return errorResponse(`${groupLabel} not found`);
if (!user) return errorResponse("User not found");
if (!member) return errorResponse("Member not found");
if (isMemberLeader) return errorResponse(`Cannot kick ${groupLabel} leader`);
if (isMemberLeader && !canEdit)
return errorResponse(`Cannot kick ${groupLabel} leader`);
if (fetchedClan.villageId !== user.villageId)
return errorResponse(user.isOutlaw ? "!= syndicate" : "!= village");
if (!isLeader && !isColeader) return errorResponse("Not allowed");
if (!isLeader && isMemberColeader)
if (!isLeader && !isColeader && !canEdit) return errorResponse("Not allowed");
if (!isLeader && isMemberColeader && !canEdit)
return errorResponse(`Only ${groupLabel} leader can kick`);
// If the leader is being kicked, promote the kicker to leader
if (isMemberLeader && canEdit) {
await ctx.drizzle
.update(clan)
.set({
leaderId: user.userId, // Promote the kicker to leader
})
.where(eq(clan.id, fetchedClan.id));
}
// Mutate
await removeFromClan(ctx.drizzle, fetchedClan, member, [
`Kicked by ${user.username}`,
Expand Down
9 changes: 9 additions & 0 deletions app/src/server/api/routers/profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,12 @@ export const profileRouter = createTRPCRouter({
where: and(eq(userData.userId, input.userId), eq(userData.isAi, true)),
with: { jutsus: { with: { jutsu: true } }, items: { with: { item: true } } },
});
// Filter off entries that do not exist
if (user) {
user.jutsus = user.jutsus.filter((j) => j.jutsu);
user.items = user.items.filter((i) => i.item);
}
// Return user
return user ?? null;
}),
// Create new AI
Expand Down Expand Up @@ -970,6 +976,9 @@ export const profileRouter = createTRPCRouter({
if (!requester || !canSeeIps(requester.role)) {
user.lastIp = "hidden";
}
// Filter off entries that do not exist
user.jutsus = user.jutsus.filter((j) => j.jutsu);
user.items = user.items.filter((i) => i.item);
// If no avatarLight version, create one
if (!user.avatarLight && user.avatar) {
const thumbnail = await createThumbnail(user.avatar);
Expand Down
Loading

0 comments on commit 35bfda3

Please sign in to comment.