feat: Phase 2 — Mission rotation management + multi-file upload

- Backend: add terrain field to Arma3MissionManager.list_missions()
- Backend: add missions field to ServerConfig Pydantic model
- Backend: add GET /missions/rotation and PUT /missions/rotation endpoints
- Frontend: Mission type gains terrain field; new MissionRotationEntry type
- Frontend: useServerMissionRotation and useUpdateMissionRotation hooks
- Frontend: useUploadMission updated to accept File[] with sequential upload
- Frontend: MissionList redesigned with Available Missions + Mission Rotation sections
- Frontend: per-file upload progress tracking, terrain badges, difficulty select
- Tests: 5 new tests; fixed existing useUploadMission test for File[] API; 141 pass
This commit is contained in:
Tran G. (Revernomad) Khoa
2026-04-17 20:33:04 +07:00
parent dedf082491
commit 4aae08420b
9 changed files with 459 additions and 82 deletions

View File

@@ -102,6 +102,12 @@ export interface Mission {
name: string;
filename: string;
size_bytes: number;
terrain: string;
}
export interface MissionRotationEntry {
name: string;
difficulty: string;
}
export interface MissionsResponse {
@@ -311,15 +317,43 @@ export function useRevokeBan(serverId: number) {
});
}
export function useServerMissionRotation(serverId: number) {
return useQuery({
queryKey: ["missions", serverId, "rotation"],
queryFn: async () => {
const res = await apiClient.get<{
success: boolean;
data: { missions: MissionRotationEntry[] };
}>(`/api/servers/${serverId}/missions/rotation`);
return res.data.data.missions;
},
enabled: serverId > 0,
});
}
export function useUpdateMissionRotation(serverId: number) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: { missions: MissionRotationEntry[]; config_version: number }) =>
apiClient.put(`/api/servers/${serverId}/missions/rotation`, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["missions", serverId, "rotation"] });
queryClient.invalidateQueries({ queryKey: ["servers", serverId, "config", "server"] });
},
});
}
export function useUploadMission(serverId: number) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (file: File) => {
const formData = new FormData();
formData.append("file", file);
return apiClient.post(`/api/servers/${serverId}/missions`, formData, {
headers: { "Content-Type": "multipart/form-data" },
});
mutationFn: async (files: File[]) => {
for (const file of files) {
const formData = new FormData();
formData.append("file", file);
await apiClient.post(`/api/servers/${serverId}/missions`, formData, {
headers: { "Content-Type": "multipart/form-data" },
});
}
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["missions", serverId] });