mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-14 07:24:52 +09:00
Search mods clientside
This commit is contained in:
1 parent
3bf0d48641
commit
600a5d5ce1
5 files changed
+326
-207
No files matched your search
@@ -98,7 +98,7 @@ export default class ControlConnection extends BaseConnection {
|
||||
this.handle(lib.ModGetRequest, this.handleModGetRequest.bind(this));
|
||||
this.handle(lib.ModListRequest, this.handleModListRequest.bind(this));
|
||||
this.handle(lib.ModSearchRequest, this.handleModSearchRequest.bind(this));
|
||||
this.handle(lib.ModPortalSearchRequest, this.handleModPortalSearchRequest.bind(this));
|
||||
this.handle(lib.ModPortalGetAllRequest, this.handleModPortalGetAllRequest.bind(this));
|
||||
this.handle(lib.ModDeleteRequest, this.handleModDeleteRequest.bind(this));
|
||||
this.handle(lib.LogSetSubscriptionsRequest, this.handleLogSetSubscriptionsRequest.bind(this));
|
||||
this.handle(lib.LogQueryRequest, this.handleLogQueryRequest.bind(this));
|
||||
@@ -515,19 +515,23 @@ export default class ControlConnection extends BaseConnection {
|
||||
};
|
||||
}
|
||||
|
||||
async handleModPortalSearchRequest(request: lib.ModPortalSearchRequest) {
|
||||
|
||||
/**
|
||||
* Handle request to fetch all mods from the Factorio Mod Portal for a given version.
|
||||
* @param request - The request object containing factorioVersion.
|
||||
*/
|
||||
async handleModPortalGetAllRequest(request: lib.ModPortalGetAllRequest) {
|
||||
try {
|
||||
return lib.ModStore.searchModPortal(
|
||||
request.query,
|
||||
const mods = await lib.ModStore.fetchAllModsFromPortal(
|
||||
request.factorioVersion,
|
||||
request.page,
|
||||
request.pageSize || 10,
|
||||
request.sort,
|
||||
request.sortOrder || "asc"
|
||||
request.hide_deprecated
|
||||
);
|
||||
// The Response class is defined inline within the Request class
|
||||
return new lib.ModPortalGetAllRequest.Response(mods);
|
||||
} catch (error: any) {
|
||||
logger.error(`Error searching mod portal: ${error}`);
|
||||
throw new lib.RequestError(`Failed to search mod portal: ${error.message}`);
|
||||
logger.error(`Error fetching all mods from portal (${request.factorioVersion}): ${error.message}`);
|
||||
// Propagate a user-friendly error back to the client
|
||||
throw new lib.RequestError(`Portal mod fetch failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,28 @@ interface ModsInfoResponse {
|
||||
results: Array<ModDetails>,
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface describing the structure of the pagination info from the portal API
|
||||
*/
|
||||
interface ModPortalPagination {
|
||||
count: number;
|
||||
links: { next?: string; prev?: string; };
|
||||
page: number;
|
||||
page_count: number;
|
||||
page_size: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface describing the structure of the portal API response for a single page
|
||||
*/
|
||||
interface ModPortalResponse {
|
||||
pagination: ModPortalPagination;
|
||||
results: ModDetails[];
|
||||
}
|
||||
|
||||
const MOD_PORTAL_API_URL = "https://mods.factorio.com/api/mods";
|
||||
const MOD_PORTAL_MAX_PAGE_SIZE = 10000; // Or largest allowed by API
|
||||
|
||||
export default class ModStore extends TypedEventEmitter<keyof ModStoreEvents, ModStoreEvents> {
|
||||
constructor(
|
||||
public modsDirectory: string,
|
||||
@@ -434,95 +456,60 @@ export default class ModStore extends TypedEventEmitter<keyof ModStoreEvents, Mo
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for mods on the Factorio mod portal
|
||||
* Fetch all mods from the Factorio Mod Portal for a specific Factorio version.
|
||||
*
|
||||
* @param query Search query string
|
||||
* @param factorioVersion Factorio version to filter for
|
||||
* @param page Page number (1-based)
|
||||
* @param pageSize Number of results per page
|
||||
* @param sort Field to sort by (name, title, downloads, etc.)
|
||||
* @param sortOrder Sort order (asc or desc)
|
||||
* @returns Search results from the mod portal
|
||||
* Handles pagination automatically to retrieve the complete list.
|
||||
*
|
||||
* @param factorioVersion - The Factorio version to fetch mods for (e.g., "1.1").
|
||||
* @param hide_deprecated - Whether to exclude deprecated mods.
|
||||
* @returns An array of mod details.
|
||||
*/
|
||||
static async searchModPortal(
|
||||
query: string,
|
||||
static async fetchAllModsFromPortal(
|
||||
factorioVersion: string,
|
||||
page: number,
|
||||
pageSize: number = 10,
|
||||
sort?: string,
|
||||
sortOrder: string = "asc"
|
||||
) {
|
||||
// eslint-disable-next-line
|
||||
logger.info(`Searching mod portal for ${query} with factorio version ${factorioVersion} on page ${page} with page size ${pageSize} and sort ${sort} and sort order ${sortOrder}`);
|
||||
hide_deprecated: boolean = false
|
||||
): Promise<ModDetails[]> {
|
||||
logger.info(`Fetching all mods for Factorio version ${factorioVersion}...`);
|
||||
let currentPage = 1;
|
||||
let allMods: ModDetails[] = [];
|
||||
let hasMorePages = true;
|
||||
|
||||
// Construct the URL for the Factorio mod portal API
|
||||
const url = new URL("https://mods.factorio.com/api/mods");
|
||||
try {
|
||||
while (hasMorePages) {
|
||||
const url = new URL(MOD_PORTAL_API_URL);
|
||||
url.searchParams.set("page_size", String(MOD_PORTAL_MAX_PAGE_SIZE));
|
||||
url.searchParams.set("version", factorioVersion);
|
||||
url.searchParams.set("page", String(currentPage));
|
||||
url.searchParams.set("hide_deprecated", String(hide_deprecated));
|
||||
|
||||
// Add query parameters
|
||||
url.searchParams.set("page", page.toString());
|
||||
url.searchParams.set("page_size", pageSize.toString());
|
||||
url.searchParams.set("version", factorioVersion);
|
||||
logger.verbose(`Fetching mod portal page ${currentPage}: ${url.toString()}`);
|
||||
const response = await fetch(url.toString());
|
||||
|
||||
if (query) {
|
||||
url.searchParams.set("q", query);
|
||||
}
|
||||
if (!response.ok) {
|
||||
let errorDetail = "";
|
||||
try {
|
||||
errorDetail = await response.text();
|
||||
} catch (bodyError) {
|
||||
logger.warn("Failed to read response body for failed portal request");
|
||||
}
|
||||
let errorMessage = `Mod portal fetch page ${currentPage} failed: `;
|
||||
errorMessage += `${response.status} ${response.statusText}`;
|
||||
if (errorDetail) {
|
||||
errorMessage += ` - ${errorDetail}`;
|
||||
}
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
// Handle sorting
|
||||
if (sort) {
|
||||
let sortField = sort;
|
||||
// Map frontend sort fields to API sort fields
|
||||
if (sort === "name") {
|
||||
sortField = "name";
|
||||
} else if (sort === "title") {
|
||||
sortField = "title";
|
||||
} else if (sort === "downloads") {
|
||||
sortField = "downloads_count";
|
||||
} else if (sort === "updated") {
|
||||
sortField = "updated_at";
|
||||
const data = await response.json() as ModPortalResponse;
|
||||
allMods = allMods.concat(data.results);
|
||||
|
||||
hasMorePages = currentPage < data.pagination.page_count;
|
||||
currentPage += 1;
|
||||
}
|
||||
|
||||
url.searchParams.set("sort", sortField);
|
||||
url.searchParams.set("sort_order", sortOrder);
|
||||
logger.info(`Successfully fetched ${allMods.length} mods for Factorio version ${factorioVersion}.`);
|
||||
return allMods;
|
||||
} catch (err: any) {
|
||||
logger.error(err);
|
||||
throw new Error(err);
|
||||
}
|
||||
|
||||
// Make the request to the Factorio mod portal
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch from mod portal: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json() as ModsInfoResponse;
|
||||
logger.info(`Mod portal search results: ${JSON.stringify(data)}`);
|
||||
|
||||
// Transform the response to match our expected format
|
||||
const results = data.results.map(mod => {
|
||||
// Find the latest release for the specified Factorio version
|
||||
const latestRelease = mod.latest_release;
|
||||
|
||||
const transformedMod = {
|
||||
name: mod.name,
|
||||
title: mod.title,
|
||||
summary: mod.summary,
|
||||
owner: mod.owner,
|
||||
downloads_count: mod.downloads_count,
|
||||
latest_release: latestRelease ? {
|
||||
version: latestRelease.version,
|
||||
factorio_version: latestRelease.info_json.factorio_version,
|
||||
released_at: latestRelease.released_at,
|
||||
download_url: latestRelease.download_url,
|
||||
file_name: latestRelease.file_name,
|
||||
sha1: latestRelease.sha1,
|
||||
} : undefined,
|
||||
};
|
||||
return transformedMod;
|
||||
}).filter(mod => mod.latest_release); // Only include mods with a release for the specified version
|
||||
|
||||
return {
|
||||
queryIssues: [],
|
||||
pageCount: Math.ceil(data.pagination.count / pageSize),
|
||||
resultCount: data.pagination.count,
|
||||
results,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -197,87 +197,73 @@ export class ModSearchRequest {
|
||||
};
|
||||
}
|
||||
|
||||
export class ModPortalSearchRequest {
|
||||
declare ["constructor"]: typeof ModPortalSearchRequest;
|
||||
// Define the structure for the latest release info from the portal
|
||||
const ModPortalReleaseSchema = Type.Object({
|
||||
version: Type.String(),
|
||||
// Match the structure from ModStore's ModRelease/ModDetails
|
||||
info_json: Type.Object({ factorio_version: Type.String() }),
|
||||
released_at: Type.String(), // ISO 8601 date string
|
||||
download_url: Type.String(),
|
||||
file_name: Type.String(),
|
||||
sha1: Type.String(),
|
||||
});
|
||||
|
||||
// Define the structure for mod details returned by the portal API
|
||||
// This should align with the ModDetails interface in ModStore.ts
|
||||
const ModPortalDetailsSchema = Type.Object({
|
||||
name: Type.String(),
|
||||
title: Type.String(),
|
||||
summary: Type.String(),
|
||||
owner: Type.String(),
|
||||
downloads_count: Type.Integer(),
|
||||
category: Type.Optional(Type.String()), // Add optional category
|
||||
score: Type.Optional(Type.Number()), // Add optional score
|
||||
latest_release: Type.Optional(ModPortalReleaseSchema),
|
||||
// Add releases array if needed: releases: Type.Optional(Type.Array(ModPortalReleaseSchema)),
|
||||
});
|
||||
|
||||
export class ModPortalGetAllRequest {
|
||||
declare ["constructor"]: typeof ModPortalGetAllRequest;
|
||||
static type = "request" as const;
|
||||
static src = "control" as const;
|
||||
static dst = "controller" as const;
|
||||
static permission = "core.mod.search_portal" as const;
|
||||
static permission = "core.mod.search_portal" as const; // Reuse search permission for now
|
||||
|
||||
// Define allowed Factorio versions
|
||||
static allowedVersions = [
|
||||
Type.Literal("0.13"), Type.Literal("0.14"), Type.Literal("0.15"),
|
||||
Type.Literal("0.16"), Type.Literal("0.17"), Type.Literal("0.18"),
|
||||
Type.Literal("1.0"), Type.Literal("1.1"), Type.Literal("2.0"),
|
||||
] as const;
|
||||
|
||||
constructor(
|
||||
public query: string,
|
||||
public factorioVersion: string,
|
||||
public page: number,
|
||||
public pageSize?: number,
|
||||
public sort?: string,
|
||||
public sortOrder?: string,
|
||||
public factorioVersion: Static<typeof ModPortalGetAllRequest.allowedVersions[number]>,
|
||||
public hide_deprecated?: boolean,
|
||||
) { }
|
||||
|
||||
static jsonSchema = Type.Object({
|
||||
"query": Type.String(),
|
||||
"factorioVersion": Type.String(),
|
||||
"page": Type.Integer(),
|
||||
"pageSize": Type.Optional(Type.Integer()),
|
||||
"sort": Type.Optional(Type.String()),
|
||||
"sortOrder": Type.Optional(Type.String()),
|
||||
"factorioVersion": Type.Union([...this.allowedVersions]), // Use Union for validation
|
||||
"hide_deprecated": Type.Optional(Type.Boolean()),
|
||||
});
|
||||
|
||||
static fromJSON(json: Static<typeof this.jsonSchema>) {
|
||||
return new this(json.query, json.factorioVersion, json.page, json.pageSize, json.sort, json.sortOrder);
|
||||
return new this(json.factorioVersion, json.hide_deprecated);
|
||||
}
|
||||
|
||||
static Response = class Response {
|
||||
// Define the Response class inline
|
||||
static Response = class ModPortalGetAllResponse {
|
||||
declare ["constructor"]: typeof ModPortalGetAllResponse;
|
||||
constructor(
|
||||
public queryIssues: string[],
|
||||
public pageCount: number,
|
||||
public resultCount: number,
|
||||
public results: {
|
||||
name: string,
|
||||
title: string,
|
||||
summary: string,
|
||||
owner: string,
|
||||
downloads_count: number,
|
||||
latest_release: {
|
||||
version: string,
|
||||
factorio_version: string,
|
||||
released_at: string,
|
||||
download_url: string,
|
||||
file_name: string,
|
||||
sha1: string,
|
||||
},
|
||||
}[],
|
||||
public mods: Static<typeof ModPortalDetailsSchema>[],
|
||||
) { }
|
||||
|
||||
static jsonSchema = Type.Object({
|
||||
"queryIssues": Type.Array(Type.String()),
|
||||
"pageCount": Type.Integer(),
|
||||
"resultCount": Type.Integer(),
|
||||
"results": Type.Array(
|
||||
Type.Object({
|
||||
"name": Type.String(),
|
||||
"title": Type.String(),
|
||||
"summary": Type.String(),
|
||||
"owner": Type.String(),
|
||||
"downloads_count": Type.Number(),
|
||||
"latest_release": Type.Object({
|
||||
"version": Type.String(),
|
||||
"factorio_version": Type.String(),
|
||||
"released_at": Type.String(),
|
||||
"download_url": Type.String(),
|
||||
"file_name": Type.String(),
|
||||
"sha1": Type.String(),
|
||||
}),
|
||||
}),
|
||||
),
|
||||
mods: Type.Array(ModPortalDetailsSchema),
|
||||
});
|
||||
|
||||
static fromJSON(json: Static<typeof this.jsonSchema>) {
|
||||
return new this(
|
||||
json.queryIssues,
|
||||
json.pageCount,
|
||||
json.resultCount,
|
||||
json.results
|
||||
);
|
||||
// No transformation needed if schema matches API structure
|
||||
return new this(json.mods);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ export const dataClasses: (RequestClass<unknown, unknown> | EventClass<unknown>)
|
||||
mod.ModGetRequest,
|
||||
mod.ModListRequest,
|
||||
mod.ModSearchRequest,
|
||||
mod.ModPortalSearchRequest,
|
||||
mod.ModPortalGetAllRequest,
|
||||
mod.ModDownloadRequest,
|
||||
mod.ModDeleteRequest,
|
||||
mod.ModPackUpdatesEvent,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React, { Fragment, useState, useContext, useEffect } from "react";
|
||||
import React, { Fragment, useState, useContext, useEffect, useMemo } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Button, Form, Input, Modal, Popconfirm, Space, Table, Typography, Upload } from "antd";
|
||||
import { Button, Form, Input, Modal, Popconfirm, Space, Table, Typography, Upload, Alert, Select } from "antd";
|
||||
import ImportOutlined from "@ant-design/icons/ImportOutlined";
|
||||
import PlusOutlined from "@ant-design/icons/PlusOutlined";
|
||||
import SearchOutlined from "@ant-design/icons/SearchOutlined";
|
||||
import { Static } from "@sinclair/typebox";
|
||||
|
||||
import * as lib from "@clusterio/lib";
|
||||
|
||||
@@ -22,6 +23,9 @@ import UploadButton from "./UploadButton";
|
||||
|
||||
const strcmp = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" }).compare;
|
||||
|
||||
// Define the allowed Factorio versions based on the lib schema
|
||||
type FactorioVersion = Static<(typeof lib.ModPortalGetAllRequest)["allowedVersions"][number]>;
|
||||
|
||||
function ImportModPackButton() {
|
||||
let control = useContext(ControlContext);
|
||||
let navigate = useNavigate();
|
||||
@@ -130,45 +134,71 @@ function CreateModPackButton() {
|
||||
</>;
|
||||
}
|
||||
|
||||
// Define a basic type for the mod data we expect back from the new backend request
|
||||
// This should ideally match the structure returned by the backend.
|
||||
// We reuse the ModPortalMod structure name for convenience, but it's fetched via backend.
|
||||
interface ModPortalMod {
|
||||
name: string;
|
||||
title: string;
|
||||
owner: string;
|
||||
summary: string;
|
||||
downloads_count: number;
|
||||
category: string;
|
||||
score?: number;
|
||||
latest_release?: {
|
||||
download_url: string;
|
||||
file_name: string;
|
||||
info_json: { factorio_version: string; };
|
||||
released_at: string;
|
||||
version: string;
|
||||
sha1: string;
|
||||
};
|
||||
}
|
||||
|
||||
function SearchModsButton() {
|
||||
const control = useContext(ControlContext);
|
||||
const account = useAccount();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
const [searchText, setSearchText] = useState("");
|
||||
const [factorioVersion, setFactorioVersion] = useState("1.1");
|
||||
const [modResults, setModResults] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [factorioVersion, setFactorioVersion] = useState<FactorioVersion>("2.0");
|
||||
|
||||
// State for all mods fetched from backend
|
||||
const [allMods, setAllMods] = useState<ModPortalMod[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
// State for client-side pagination and sorting
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [totalResults, setTotalResults] = useState(0);
|
||||
const [totalPages, setTotalPages] = useState(0);
|
||||
const [sort, setSort] = useState<string>("name");
|
||||
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc");
|
||||
|
||||
// Search mods when search parameters change
|
||||
// Fetch all mods from backend when modal opens or version changes
|
||||
useEffect(() => {
|
||||
if (!open) { return; }
|
||||
if (!open || !factorioVersion) {
|
||||
setAllMods([]); // Clear mods if modal closed or no version
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
let canceled = false;
|
||||
|
||||
control.send(new lib.ModPortalSearchRequest(
|
||||
searchText,
|
||||
factorioVersion,
|
||||
page,
|
||||
pageSize,
|
||||
sort,
|
||||
sortOrder
|
||||
)).then(response => {
|
||||
// Use the hypothetical new backend request
|
||||
control.send(
|
||||
// Factorio version state now matches the required type
|
||||
new lib.ModPortalGetAllRequest(factorioVersion)
|
||||
).then((response: any) => {
|
||||
if (canceled) { return; }
|
||||
|
||||
setModResults(response.results);
|
||||
setTotalResults(response.resultCount);
|
||||
setTotalPages(response.pageCount);
|
||||
// *** Backend Change Needed: Ensure the response has a 'mods' array ***
|
||||
setAllMods(response?.mods || []); // Added optional chaining
|
||||
setLoading(false);
|
||||
}).catch(error => {
|
||||
}).catch(err => {
|
||||
if (canceled) { return; }
|
||||
notifyErrorHandler("Error fetching mods from portal")(err);
|
||||
setError(err);
|
||||
setAllMods([]);
|
||||
setLoading(false);
|
||||
});
|
||||
|
||||
@@ -176,15 +206,80 @@ function SearchModsButton() {
|
||||
return () => {
|
||||
canceled = true;
|
||||
};
|
||||
}, [open, searchText, factorioVersion, page, pageSize, sort, sortOrder, control]);
|
||||
// Re-fetch when modal opens or factorio version changes
|
||||
}, [open, factorioVersion, control]);
|
||||
|
||||
const handleSearch = () => {
|
||||
const values = form.getFieldsValue();
|
||||
setSearchText(values.name || "");
|
||||
setFactorioVersion(values.factorioVersion || "1.1");
|
||||
setPage(1); // Reset to first page on new search
|
||||
|
||||
// Memoize the filtered, sorted, and paginated mods (logic remains the same)
|
||||
const displayedMods = useMemo(() => {
|
||||
let filtered = allMods;
|
||||
|
||||
// Apply search filter (case-insensitive)
|
||||
if (searchText) {
|
||||
const lowerSearchText = searchText.toLowerCase();
|
||||
filtered = filtered.filter(mod => mod.name.toLowerCase().includes(lowerSearchText)
|
||||
|| mod.title.toLowerCase().includes(lowerSearchText)
|
||||
);
|
||||
}
|
||||
|
||||
// Apply sorting
|
||||
if (sort && sortOrder) {
|
||||
filtered = [...filtered].sort((a, b) => {
|
||||
let aValue: any = a[sort as keyof ModPortalMod] ?? "";
|
||||
let bValue: any = b[sort as keyof ModPortalMod] ?? "";
|
||||
|
||||
// Handle specific types if necessary (e.g., numbers, dates)
|
||||
if (sort === "downloads_count") {
|
||||
aValue = a.downloads_count ?? 0;
|
||||
bValue = b.downloads_count ?? 0;
|
||||
return sortOrder === "asc" ? aValue - bValue : bValue - aValue;
|
||||
}
|
||||
|
||||
// Default string comparison
|
||||
const comparison = strcmp(String(aValue), String(bValue));
|
||||
return sortOrder === "asc" ? comparison : -comparison;
|
||||
});
|
||||
}
|
||||
|
||||
// Apply pagination
|
||||
const startIndex = (page - 1) * pageSize;
|
||||
return filtered.slice(startIndex, startIndex + pageSize);
|
||||
|
||||
}, [allMods, searchText, sort, sortOrder, page, pageSize]);
|
||||
|
||||
// Calculate total results based on the filtered list *before* pagination (logic remains the same)
|
||||
const totalResults = useMemo(() => {
|
||||
let filtered = allMods;
|
||||
if (searchText) {
|
||||
const lowerSearchText = searchText.toLowerCase();
|
||||
filtered = filtered.filter(mod => mod.name.toLowerCase().includes(lowerSearchText)
|
||||
|| mod.title.toLowerCase().includes(lowerSearchText)
|
||||
);
|
||||
}
|
||||
return filtered.length;
|
||||
}, [allMods, searchText]);
|
||||
|
||||
// Update search text and factorio version from form
|
||||
const handleSearch = (changedValues: any, allValues: any) => {
|
||||
const nameValue = allValues.name;
|
||||
const versionValue = allValues.factorioVersion as FactorioVersion | undefined;
|
||||
|
||||
// Only trigger state updates if values actually changed
|
||||
if (nameValue !== searchText) {
|
||||
setSearchText(nameValue || "");
|
||||
}
|
||||
if (versionValue && versionValue !== factorioVersion) {
|
||||
setFactorioVersion(versionValue);
|
||||
setAllMods([]); // Clear mods when version changes, useEffect will fetch new ones
|
||||
setLoading(true); // Show loading indicator immediately
|
||||
}
|
||||
// Reset page only if search text or version changed
|
||||
if (nameValue !== searchText || (versionValue && versionValue !== factorioVersion)) {
|
||||
setPage(1);
|
||||
}
|
||||
};
|
||||
|
||||
// Update pagination and sorting state when table changes (logic remains the same)
|
||||
const handleTableChange = (
|
||||
pagination: any,
|
||||
filters: any,
|
||||
@@ -195,14 +290,23 @@ function SearchModsButton() {
|
||||
}
|
||||
if (pagination.pageSize !== pageSize) {
|
||||
setPageSize(pagination.pageSize);
|
||||
setPage(1); // Reset to first page if page size changes
|
||||
}
|
||||
|
||||
if (sorter.field && sorter.order) {
|
||||
// Update sort state if sorter exists and has changed
|
||||
if (sorter.field && (sorter.field !== sort || sorter.order !== (sortOrder === "asc" ? "ascend" : "descend"))) {
|
||||
setSort(sorter.field);
|
||||
setSortOrder(sorter.order === "ascend" ? "asc" : "desc");
|
||||
setPage(1); // Reset to first page on sort change
|
||||
} else if (!sorter.field && sort !== "name") { // Reset only if not already default
|
||||
// Clear sort if column header clicked without order - default to name ascending
|
||||
setSort("name");
|
||||
setSortOrder("asc");
|
||||
setPage(1);
|
||||
}
|
||||
};
|
||||
|
||||
// Helper to get antd sort order format (logic remains the same)
|
||||
const getColumnSortOrder = (columnKey: string): "ascend" | "descend" | undefined => {
|
||||
if (sort === columnKey) {
|
||||
return sortOrder === "asc" ? "ascend" : "descend";
|
||||
@@ -213,7 +317,7 @@ function SearchModsButton() {
|
||||
return <>
|
||||
<Button icon={<SearchOutlined />} onClick={() => { setOpen(true); }}>Search</Button>
|
||||
<Modal
|
||||
title="Search Mods"
|
||||
title="Search Mod Portal"
|
||||
open={open}
|
||||
onCancel={() => { setOpen(false); }}
|
||||
width={800}
|
||||
@@ -221,27 +325,48 @@ function SearchModsButton() {
|
||||
<Button key="close" onClick={() => { setOpen(false); }}>
|
||||
Close
|
||||
</Button>,
|
||||
<Button key="search" type="primary" onClick={handleSearch}>
|
||||
Search
|
||||
</Button>,
|
||||
// Remove explicit search button as form updates trigger search/filter
|
||||
]}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={handleSearch}
|
||||
initialValues={{ factorioVersion: "2.0" }}
|
||||
onValuesChange={handleSearch} // Trigger update on any form change
|
||||
initialValues={{ factorioVersion: factorioVersion, name: searchText }}
|
||||
>
|
||||
<Form.Item name="name" label="Name or Title">
|
||||
<Input placeholder="Enter mod name or title" />
|
||||
<Form.Item name="name" label="Filter by Name or Title">
|
||||
<Input placeholder="Start typing to filter..." />
|
||||
</Form.Item>
|
||||
<Form.Item name="factorioVersion" label="Factorio Version">
|
||||
<Input placeholder="e.g. 1.1" />
|
||||
<Form.Item
|
||||
name="factorioVersion"
|
||||
label="Factorio Version"
|
||||
>
|
||||
<Select>
|
||||
{/* Map allowed versions to Select Options */}
|
||||
<Select.Option value="2.0">2.0</Select.Option>
|
||||
<Select.Option value="1.1">1.1</Select.Option>
|
||||
<Select.Option value="1.0">1.0</Select.Option>
|
||||
<Select.Option value="0.18">0.18</Select.Option>
|
||||
<Select.Option value="0.17">0.17</Select.Option>
|
||||
<Select.Option value="0.16">0.16</Select.Option>
|
||||
<Select.Option value="0.15">0.15</Select.Option>
|
||||
<Select.Option value="0.14">0.14</Select.Option>
|
||||
<Select.Option value="0.13">0.13</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
{/* Display error message if fetching failed */}
|
||||
{error && <Alert
|
||||
message="Error Fetching Mods"
|
||||
description={error.message}
|
||||
type="error"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
/>}
|
||||
|
||||
<Table
|
||||
dataSource={modResults}
|
||||
dataSource={displayedMods}
|
||||
rowKey={record => record.name}
|
||||
loading={loading}
|
||||
onChange={handleTableChange}
|
||||
@@ -250,20 +375,20 @@ function SearchModsButton() {
|
||||
pageSize: pageSize,
|
||||
total: totalResults,
|
||||
showSizeChanger: true,
|
||||
pageSizeOptions: ["10", "20", "50"],
|
||||
pageSizeOptions: ["10", "20", "50", "100"],
|
||||
}}
|
||||
expandable={{
|
||||
expandedRowRender: record => (
|
||||
<div>
|
||||
<p><strong>Summary:</strong> {record.summary ?? "N/A"}</p>
|
||||
<p><strong>Downloads:</strong> {record.downloads_count ?? "N/A"}</p>
|
||||
<p><strong>Downloads:</strong> {record.downloads_count?.toLocaleString() ?? "N/A"}</p>
|
||||
<p><strong>Latest Release:</strong></p>
|
||||
{record.latest_release ? (
|
||||
<ul>
|
||||
<li>Version: {record.latest_release.version ?? "N/A"}</li>
|
||||
<li>
|
||||
Factorio Version: {
|
||||
record.latest_release.factorio_version ?? "N/A"
|
||||
record.latest_release.info_json?.factorio_version ?? "N/A"
|
||||
}
|
||||
</li>
|
||||
<li>
|
||||
@@ -275,21 +400,28 @@ function SearchModsButton() {
|
||||
</li>
|
||||
</ul>
|
||||
) : <p>No release information available.</p>}
|
||||
{/* Download button logic remains the same, assuming backend handles downloads */}
|
||||
{account.hasPermission("core.mod.download") && record.latest_release && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
control.send(
|
||||
new lib.ModDownloadRequest(record.name, record.latest_release.version)
|
||||
).then((streamId: string) => {
|
||||
let url = new URL(webRoot, document.location.origin);
|
||||
url.pathname += `api/stream/${streamId}`;
|
||||
document.location = url.href;
|
||||
}).catch(
|
||||
notifyErrorHandler("Error downloading mod")
|
||||
);
|
||||
if (record.latest_release?.version) {
|
||||
control.send(
|
||||
new lib.ModDownloadRequest(
|
||||
record.name,
|
||||
record.latest_release.version
|
||||
)
|
||||
)
|
||||
.then((streamId: string) => {
|
||||
let url = new URL(webRoot, document.location.origin);
|
||||
url.pathname += `api/stream/${streamId}`;
|
||||
document.location = url.href;
|
||||
})
|
||||
.catch(
|
||||
notifyErrorHandler("Error downloading mod")
|
||||
);
|
||||
}
|
||||
}}
|
||||
// Disable button if version is missing
|
||||
disabled={!record.latest_release.version}
|
||||
disabled={!record.latest_release?.version}
|
||||
>
|
||||
Download Latest Version
|
||||
</Button>
|
||||
@@ -298,6 +430,7 @@ function SearchModsButton() {
|
||||
),
|
||||
}}
|
||||
columns={[
|
||||
// Columns remain largely the same
|
||||
{
|
||||
title: "Name",
|
||||
dataIndex: "name",
|
||||
@@ -319,6 +452,15 @@ function SearchModsButton() {
|
||||
sorter: true,
|
||||
sortOrder: getColumnSortOrder("owner"),
|
||||
},
|
||||
{
|
||||
title: "Downloads",
|
||||
dataIndex: "downloads_count",
|
||||
key: "downloads_count",
|
||||
sorter: true,
|
||||
sortOrder: getColumnSortOrder("downloads_count"),
|
||||
align: "right",
|
||||
render: (count) => count?.toLocaleString() ?? "N/A",
|
||||
},
|
||||
{
|
||||
title: "Latest Version",
|
||||
key: "version",
|
||||
|
||||
Reference in new issue
Block a user