diff --git a/apps/cyberstorm-remix/app/p/components/PackageListing/ManagementTools.tsx b/apps/cyberstorm-remix/app/p/components/PackageListing/ManagementTools.tsx new file mode 100644 index 000000000..ce0fa153f --- /dev/null +++ b/apps/cyberstorm-remix/app/p/components/PackageListing/ManagementTools.tsx @@ -0,0 +1,103 @@ +import { faCog, faList, faBoxOpen } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faArrowUpRight } from "@fortawesome/pro-solid-svg-icons"; +import { ReviewPackageForm } from "./ReviewPackageForm"; +import { type DapperTsInterface } from "@thunderstore/dapper-ts"; +import { NewButton, NewIcon, useToast } from "@thunderstore/cyberstorm"; +import { + fetchPackagePermissions, + type RequestConfig, +} from "@thunderstore/thunderstore-api"; +import { type PackageListingStatus } from "@thunderstore/dapper/types"; + +export interface ManagementToolsProps { + packagePermissions: Awaited>; + listing: Awaited>; + listingStatus: PackageListingStatus; + toast: ReturnType; + requestConfig: () => RequestConfig; +} + +export function ManagementTools({ + packagePermissions, + listing, + listingStatus, + toast, + requestConfig, +}: ManagementToolsProps) { + const perms = packagePermissions.permissions; + const pkg = packagePermissions.package; + + return ( +
+ {/* Review Package */} + {perms.can_moderate && ( +
+ + + {/* Package Listing admin link */} + {perms.can_view_listing_admin_page && ( + + + + + Listing admin + + + + + )} + + {/* Package admin link */} + {perms.can_view_package_admin_page && ( + + + + + Package admin + + + + + )} +
+ )} + + {/* Manage package */} + {perms.can_manage && ( +
+ + + + + Manage Package + +
+ )} +
+ ); +} diff --git a/apps/cyberstorm-remix/app/p/components/PackageListing/ReviewPackageForm.tsx b/apps/cyberstorm-remix/app/p/components/PackageListing/ReviewPackageForm.tsx new file mode 100644 index 000000000..3bb873b07 --- /dev/null +++ b/apps/cyberstorm-remix/app/p/components/PackageListing/ReviewPackageForm.tsx @@ -0,0 +1,203 @@ +import { useEffect, useState } from "react"; +import { useRevalidator } from "react-router"; +import { faScaleBalanced } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { ApiAction } from "@thunderstore/ts-api-react-actions"; +import { + Modal, + NewAlert, + NewButton, + NewIcon, + NewTag, + NewTextInput, + useToast, +} from "@thunderstore/cyberstorm"; +import { + packageListingApprove, + packageListingReject, + type RequestConfig, +} from "@thunderstore/thunderstore-api"; + +import { type PackageListingStatus } from "@thunderstore/dapper/types"; + +export interface ReviewPackageFormProps { + communityId: string; + namespaceId: string; + packageId: string; + packageListingStatus: PackageListingStatus; + config: () => RequestConfig; + toast: ReturnType; +} + +const reviewStatusColorMap = { + approved: "green", + rejected: "red", + unreviewed: "orange", +} as const; + +export function ReviewPackageForm({ + communityId, + namespaceId, + packageId, + packageListingStatus, + config, + toast, +}: ReviewPackageFormProps) { + const [rejectionReason, setRejectionReason] = useState( + packageListingStatus?.rejection_reason ?? "" + ); + + const [internalNotes, setInternalNotes] = useState( + packageListingStatus?.internal_notes ?? "" + ); + + const reviewStatus = packageListingStatus?.review_status ?? "unreviewed"; + const reviewStatusColor = + reviewStatusColorMap[reviewStatus as keyof typeof reviewStatusColorMap] ?? + "orange"; + + const { revalidate } = useRevalidator(); + + useEffect(() => { + setRejectionReason(packageListingStatus?.rejection_reason ?? ""); + setInternalNotes(packageListingStatus?.internal_notes ?? ""); + }, [packageListingStatus]); + + const rejectPackageAction = ApiAction({ + endpoint: packageListingReject, + onSubmitSuccess: () => { + toast.addToast({ + csVariant: "success", + children: `Package rejected`, + duration: 4000, + }); + revalidate(); + }, + onSubmitError: (error) => { + toast.addToast({ + csVariant: "danger", + children: `Error occurred: ${error.message || "Unknown error"}`, + duration: 8000, + }); + }, + }); + + const approvePackageAction = ApiAction({ + endpoint: packageListingApprove, + onSubmitSuccess: () => { + toast.addToast({ + csVariant: "success", + children: `Package approved`, + duration: 4000, + }); + revalidate(); + }, + onSubmitError: (error) => { + toast.addToast({ + csVariant: "danger", + children: `Error occurred: ${error.message || "Unknown error"}`, + duration: 8000, + }); + }, + }); + + return ( + + + + + Review Package + + } + titleContent="Review Package" + > + + + Changes might take several minutes to show publicly! Info shown below + is always up to date. + + +
+

Review status

+ + {reviewStatus} + +
+ +
+

+ Reject reason (saved on reject) +

+ setRejectionReason(e.target.value)} + placeholder="Invalid submission" + csSize="textarea" + rootClasses="review-package__textarea" + /> +
+ +
+

Internal notes

+ setInternalNotes(e.target.value)} + placeholder=".exe requires manual review" + csSize="textarea" + rootClasses="review-package__textarea" + /> +
+
+ + + + rejectPackageAction({ + config, + params: { + community: communityId, + namespace: namespaceId, + package: packageId, + }, + queryParams: {}, + data: { + rejection_reason: rejectionReason, + internal_notes: internalNotes ? internalNotes : null, + }, + }) + } + > + Reject + + + + approvePackageAction({ + config, + params: { + community: communityId, + namespace: namespaceId, + package: packageId, + }, + queryParams: {}, + data: { + internal_notes: internalNotes ? internalNotes : null, + }, + }) + } + > + Approve + + +
+ ); +} diff --git a/apps/cyberstorm-remix/app/p/listingUtils.ts b/apps/cyberstorm-remix/app/p/listingUtils.ts new file mode 100644 index 000000000..da17906c2 --- /dev/null +++ b/apps/cyberstorm-remix/app/p/listingUtils.ts @@ -0,0 +1,72 @@ +import { DapperTs } from "@thunderstore/dapper-ts"; +import { isApiError } from "@thunderstore/thunderstore-api"; + +export interface ListingIdentifiers { + communityId: string; + namespaceId: string; + packageId: string; +} + +/** + * Server-side listing fetcher: + * 1. Try public listing + * 2. If 404, return undefined + */ +export async function getPublicListing( + dapper: DapperTs, + ids: ListingIdentifiers +) { + const { communityId, namespaceId, packageId } = ids; + try { + return await dapper.getPackageListingDetails( + communityId, + namespaceId, + packageId + ); + } catch (e) { + if (isApiError(e) && e.response.status === 404) { + return undefined; + } else { + throw e; + } + } +} + +/** + * Client-side listing fetcher: + * 1. Try public listing + * 2. If 404, try private listing + * 3. If still missing, throw 404 + */ +export async function getPrivateListing( + dapper: DapperTs, + ids: ListingIdentifiers +) { + const { communityId, namespaceId, packageId } = ids; + + try { + return await dapper.getPackageListingDetails( + communityId, + namespaceId, + packageId + ); + } catch (e) { + const is404 = isApiError(e) && e.response?.status === 404; + if (!is404) { + throw e; + } + } + + const privateListing = await dapper.getPackageListingDetails( + communityId, + namespaceId, + packageId, + true + ); + + if (!privateListing) { + throw new Response("Package not found", { status: 404 }); + } + + return privateListing; +} diff --git a/apps/cyberstorm-remix/app/p/packageListing.tsx b/apps/cyberstorm-remix/app/p/packageListing.tsx index 6f0724c69..a28757665 100644 --- a/apps/cyberstorm-remix/app/p/packageListing.tsx +++ b/apps/cyberstorm-remix/app/p/packageListing.tsx @@ -3,7 +3,6 @@ import { type ReactElement, Suspense, useEffect, - useMemo, useRef, useState, } from "react"; @@ -14,7 +13,6 @@ import { useLocation, useOutletContext, type LoaderFunctionArgs, - type ShouldRevalidateFunctionArgs, } from "react-router"; import { useHydrated } from "remix-utils/use-hydrated"; import { @@ -24,8 +22,6 @@ import { faThumbsUp, faWarning, faCaretRight, - faScaleBalanced, - faCog, } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faArrowUpRight, faLips } from "@fortawesome/pro-solid-svg-icons"; @@ -44,15 +40,11 @@ import { import { Drawer, Heading, - Modal, - NewAlert, NewButton, NewIcon, NewLink, NewTag, - NewTextInput, RelativeTime, - SkeletonBox, Tabs, ThunderstoreLogo, formatFileSize, @@ -61,16 +53,10 @@ import { useToast, } from "@thunderstore/cyberstorm"; import { PackageLikeAction } from "@thunderstore/cyberstorm-forms"; -import type { TagVariants } from "@thunderstore/cyberstorm-theme/src/components"; import type { CurrentUser } from "@thunderstore/dapper/types"; import { DapperTs, type DapperTsInterface } from "@thunderstore/dapper-ts"; -import { - fetchPackagePermissions, - packageListingApprove, - packageListingReject, - type RequestConfig, -} from "@thunderstore/thunderstore-api"; -import { ApiAction } from "@thunderstore/ts-api-react-actions"; +import { getPublicListing, getPrivateListing } from "./listingUtils"; +import { ManagementTools } from "./components/PackageListing/ManagementTools"; import "./packageListing.css"; @@ -79,28 +65,35 @@ type PackageListingOutletContext = OutletContextShape & { }; export async function loader({ params }: LoaderFunctionArgs) { - if (params.communityId && params.namespaceId && params.packageId) { - const publicEnvVariables = getPublicEnvVariables(["VITE_API_URL"]); - const dapper = new DapperTs(() => { - return { - apiHost: publicEnvVariables.VITE_API_URL, - sessionId: undefined, - }; - }); + const { communityId, namespaceId, packageId } = params; - return { - community: await dapper.getCommunity(params.communityId), - communityFilters: await dapper.getCommunityFilters(params.communityId), - listing: await dapper.getPackageListingDetails( - params.communityId, - params.namespaceId, - params.packageId - ), - team: await dapper.getTeamDetails(params.namespaceId), - permissions: undefined, - }; + if (!communityId || !namespaceId || !packageId) { + throw new Response("Package not found", { status: 404 }); } - throw new Response("Package not found", { status: 404 }); + + const publicEnvVariables = getPublicEnvVariables(["VITE_API_URL"]); + const dapper = new DapperTs(() => ({ + apiHost: publicEnvVariables.VITE_API_URL, + sessionId: undefined, + })); + + const listing = await getPublicListing(dapper, { + communityId, + namespaceId, + packageId, + }); + + return { + community: await dapper.getCommunity(communityId), + communityFilters: await dapper.getCommunityFilters(communityId), + listing: listing, + listingStatus: undefined, + team: await dapper.getTeamDetails(namespaceId), + permissions: undefined, + community_identifier: communityId, + namespace_id: namespaceId, + package_id: packageId, + }; } async function getUserPermissions( @@ -112,63 +105,91 @@ async function getUserPermissions( ) { const cu = await tools.getSessionCurrentUser(); if (cu.username) { - return dapper.getPackagePermissions(communityId, namespaceId, packageId); + return await dapper.getPackagePermissions( + communityId, + namespaceId, + packageId + ); } return undefined; } -// TODO: Needs to check if package is available for the logged in user -export async function clientLoader({ params }: LoaderFunctionArgs) { - if (params.communityId && params.namespaceId && params.packageId) { - const tools = getSessionTools(); - const dapper = new DapperTs(() => { - return { - apiHost: tools.getConfig().apiHost, - sessionId: tools.getConfig().sessionId, - }; - }); - - return { - community: dapper.getCommunity(params.communityId), - communityFilters: dapper.getCommunityFilters(params.communityId), - listing: dapper.getPackageListingDetails( - params.communityId, - params.namespaceId, - params.packageId - ), - team: dapper.getTeamDetails(params.namespaceId), - permissions: getUserPermissions( - tools, - dapper, - params.communityId, - params.namespaceId, - params.packageId - ), - }; +async function getPackageListingStatus( + tools: ReturnType, + dapper: DapperTs, + communityId: string, + namespaceId: string, + packageId: string +) { + const cu = await tools.getSessionCurrentUser(); + if (cu.username) { + return await dapper.getPackageListingStatus( + communityId, + namespaceId, + packageId + ); } - throw new Response("Package not found", { status: 404 }); + return undefined; } -clientLoader.hydrate = true; +export async function clientLoader({ params }: LoaderFunctionArgs) { + const { communityId, namespaceId, packageId } = params; -export function shouldRevalidate(arg: ShouldRevalidateFunctionArgs) { - const oldPath = arg.currentUrl.pathname.split("/"); - const newPath = arg.nextUrl.pathname.split("/"); - // If we're staying on the same package page, don't revalidate - if ( - oldPath[2] === newPath[2] && - oldPath[3] === newPath[3] && - oldPath[5] === newPath[5] - ) { - return false; + if (!communityId || !namespaceId || !packageId) { + throw new Response("Package not found", { status: 404 }); } - return arg.defaultShouldRevalidate; + + const tools = getSessionTools(); + const config = tools.getConfig(); + const dapper = new DapperTs(() => ({ + apiHost: config.apiHost, + sessionId: config.sessionId, + })); + + const listing = await getPrivateListing(dapper, { + communityId, + namespaceId, + packageId, + }); + + return { + community: dapper.getCommunity(communityId), + communityFilters: dapper.getCommunityFilters(communityId), + listing: listing, + listingStatus: getPackageListingStatus( + tools, + dapper, + communityId, + namespaceId, + packageId + ), + team: dapper.getTeamDetails(namespaceId), + permissions: getUserPermissions( + tools, + dapper, + communityId, + namespaceId, + packageId + ), + community_identifier: communityId, + namespace_id: namespaceId, + package_id: packageId, + }; } +clientLoader.hydrate = true; + export default function PackageListing() { - const { community, listing, team, permissions } = useLoaderData< - typeof loader | typeof clientLoader - >(); + const { + community, + listing, + listingStatus, + team, + permissions, + community_identifier, + namespace_id, + package_id, + } = useLoaderData(); const location = useLocation(); @@ -182,14 +203,16 @@ export default function PackageListing() { const toast = useToast(); const { ReportPackageButton, ReportPackageModal } = useReportPackage({ - formPropsPromise: Promise.resolve(listing).then((listingData) => ({ - community: listingData.community_identifier, - namespace: listingData.namespace, - package: listingData.name, - })), + formPropsPromise: Promise.resolve({ + community: community_identifier, + namespace: namespace_id, + package: package_id, + }), config: config, }); + // TODO: This needs to be fixes - listing is not a promise here + // TODO: no need to get namespace and name from listing if we have them in params const fetchAndSetRatedPackages = async () => { const rp = await dapper.getRatedPackages(); if (isPromise(listing)) { @@ -226,7 +249,10 @@ export default function PackageListing() { // https://react.dev/reference/react/StrictMode // If strict mode is removed from the entry.client.tsx, this should only run once useEffect(() => { - if (!startsHydrated.current && isHydrated) return; + if (!listing || (!startsHydrated.current && isHydrated)) { + return; + } + if (isPromise(listing)) { listing.then((listingData) => { setLastUpdated( @@ -258,36 +284,30 @@ export default function PackageListing() { const currentTab = location.pathname.split("/")[6] || "details"; - const listingAndCommunityPromise = useMemo( - () => Promise.all([listing, community]), - [] - ); - - const listingAndPermissionsPromise = useMemo( - () => Promise.all([listing, permissions]), - [] - ); - - const listingAndTeamPromise = useMemo(() => Promise.all([listing, team]), []); - const packageLikeAction = PackageLikeAction({ isLoggedIn: Boolean(currentUser?.username), dataUpdateTrigger: fetchAndSetRatedPackages, config: config, }); + // TODO: Add proper loading element + if (!listing) { + return
Loading package...
; + } + + // TODO: some variables are available in props (communityId, namespaceId, packageId) return ( <> - - {(resolvedValue) => ( + + {(resolvedCommunity) => ( <> - + - + )} +
- - {(resolvedValue) => - resolvedValue && resolvedValue[1] ? ( -
- {managementTools( - resolvedValue[1], - resolvedValue[0], - toast, - config - )} -
- ) : null - } + + {(resolvedStatus) => ( + + {(resolvedPermissions) => + resolvedPermissions ? ( + + ) : null + } + + )}
+
- + + + + + + {listing.namespace} + + + {listing.website_url ? ( + + {listing.website_url} + + + + + ) : null} + } > - - {(resolvedValue) => ( - - - - - - {resolvedValue.namespace} - - {resolvedValue.website_url ? ( - - {resolvedValue.website_url} - - - - - ) : null} - - } - > - {formatToDisplayName(resolvedValue.name)} - - )} - - + {formatToDisplayName(listing.name)} + +
+ + {packageMeta(lastUpdated, firstUploaded, listing)} + Loading...

}> - - {(resolvedValue) => ( - <> - {packageMeta( - lastUpdated, - firstUploaded, - resolvedValue - )} - - )} - -
- Loading...

}> - - {(resolvedValue) => ( - <> - {packageBoxes( - resolvedValue[0], - resolvedValue[1], - domain - )} - - )} + + {(resolvedCommunity) => + packageBoxes(listing, resolvedCommunity, domain) + }
+ Loading...

}> - - {(resolvedValue) => ( + + {(resolvedTeam) => ( - - } - > - - {(resolvedValue) => ( - <> - - - Details - - - Required ({resolvedValue.dependency_count}) - - - Wiki - - - Changelog - - - Versions - - - Analysis - - -
- -
- - )} -
-
+ + <> + + + Details + + + + Required ({listing.dependency_count}) + + + + Wiki + + + + Changelog + + + + Versions + + + + Analysis + + + +
+ +
+
+ @@ -649,160 +608,6 @@ export default function PackageListing() { ); } -function ReviewPackageForm(props: { - communityId: string; - namespaceId: string; - packageId: string; - reviewStatus: string; - reviewStatusColor: TagVariants; - config: () => RequestConfig; - toast: ReturnType; -}) { - const { - communityId, - namespaceId, - packageId, - reviewStatus, - reviewStatusColor, - toast, - config, - } = props; - const [rejectionReason, setRejectionReason] = useState(""); - const [internalNotes, setInternalNotes] = useState(""); - const rejectPackageAction = ApiAction({ - endpoint: packageListingReject, - onSubmitSuccess: () => { - toast.addToast({ - csVariant: "success", - children: `Package rejected`, - duration: 4000, - }); - }, - onSubmitError: (error) => { - toast.addToast({ - csVariant: "danger", - children: `Error occurred: ${error.message || "Unknown error"}`, - duration: 8000, - }); - }, - }); - - const approvePackageAction = ApiAction({ - endpoint: packageListingApprove, - onSubmitSuccess: () => { - toast.addToast({ - csVariant: "success", - children: `Package approved`, - duration: 4000, - }); - }, - onSubmitError: (error) => { - toast.addToast({ - csVariant: "danger", - children: `Error occurred: ${error.message || "Unknown error"}`, - duration: 8000, - }); - }, - }); - - return ( - - - - - Review Package - - } - titleContent="Review Package" - > - - - Changes might take several minutes to show publicly! Info shown below - is always up to date. - -
-

Review status

- - {reviewStatus} - -
-
-

- Reject reason (saved on reject) -

- setRejectionReason(e.target.value)} - placeholder="Invalid submission" - csSize="textarea" - rootClasses="review-package__textarea" - /> -
-
-

Internal notes

- setInternalNotes(e.target.value)} - placeholder=".exe requires manual review" - csSize="textarea" - rootClasses="review-package__textarea" - /> -
-
- - - rejectPackageAction({ - config: config, - params: { - community: communityId, - namespace: namespaceId, - package: packageId, - }, - queryParams: {}, - data: { - rejection_reason: rejectionReason, - internal_notes: internalNotes ? internalNotes : null, - }, - }) - } - > - Reject - - - approvePackageAction({ - config: config, - params: { - community: communityId, - namespace: namespaceId, - package: packageId, - }, - queryParams: {}, - data: { - internal_notes: internalNotes ? internalNotes : null, - }, - }) - } - > - Approve - - -
- ); -} - -ReviewPackageForm.displayName = "ReviewPackageForm"; - function packageTags( listing: Awaited>, community: Awaited> @@ -876,87 +681,6 @@ function packageBoxes( ); } -// TODO: Enable when APIs are available -function managementTools( - packagePermissions: Awaited>, - listing: Awaited>, - toast: ReturnType, - requestConfig: () => RequestConfig -) { - return ( -
- {packagePermissions.permissions.can_moderate ? ( -
- {packagePermissions.permissions.can_moderate ? ( - - ) : null} - {/* {packagePermissions.permissions.can_view_listing_admin_page ? ( - - - - - Listing admin - - - - - ) : null} - {packagePermissions.permissions.can_view_package_admin_page ? ( - - - - - Package admin - - - - - ) : null} */} -
- ) : null} - {packagePermissions.permissions.can_manage ? ( -
- - - - - Manage Package - -
- ) : null} -
- ); -} - const Actions = memo(function Actions(props: { team: Awaited>; listing: Awaited>; diff --git a/packages/dapper-ts/src/index.ts b/packages/dapper-ts/src/index.ts index 7a7960eab..4e38aacc2 100644 --- a/packages/dapper-ts/src/index.ts +++ b/packages/dapper-ts/src/index.ts @@ -23,6 +23,7 @@ import { } from "./methods/package"; import { getPackageListingDetails, + getPackageListingStatus, getPackageListings, } from "./methods/packageListings"; import { @@ -56,6 +57,7 @@ export class DapperTs implements DapperTsInterface { this.getPackageChangelog = this.getPackageChangelog.bind(this); this.getPackageListings = this.getPackageListings.bind(this); this.getPackageListingDetails = this.getPackageListingDetails.bind(this); + this.getPackageListingStatus = this.getPackageListingStatus.bind(this); this.getPackageReadme = this.getPackageReadme.bind(this); this.getPackageVersionDetails = this.getPackageVersionDetails.bind(this); this.getPackageVersions = this.getPackageVersions.bind(this); @@ -85,6 +87,7 @@ export class DapperTs implements DapperTsInterface { public getPackageChangelog = getPackageChangelog; public getPackageListings = getPackageListings; public getPackageListingDetails = getPackageListingDetails; + public getPackageListingStatus = getPackageListingStatus; public getPackageReadme = getPackageReadme; public getPackageVersions = getPackageVersions; public getPackageVersionDependencies = getPackageVersionDependencies; diff --git a/packages/dapper-ts/src/methods/packageListings.ts b/packages/dapper-ts/src/methods/packageListings.ts index 95a608986..3894a91f6 100644 --- a/packages/dapper-ts/src/methods/packageListings.ts +++ b/packages/dapper-ts/src/methods/packageListings.ts @@ -4,6 +4,7 @@ import { fetchNamespacePackageListings, fetchPackageDependantsListings, fetchPackageListingDetails, + fetchPackageListingStatus, PackageListingsOrderingEnum, PackageListingsRequestQueryParams, } from "@thunderstore/thunderstore-api"; @@ -121,7 +122,8 @@ export async function getPackageListingDetails( this: DapperTsInterface, communityId: string, namespaceId: string, - packageName: string + packageName: string, + useSession = false ) { const data = await fetchPackageListingDetails({ config: this.config, @@ -132,6 +134,27 @@ export async function getPackageListingDetails( }, data: {}, queryParams: {}, + useSession: useSession, + }); + + return data; +} + +export async function getPackageListingStatus( + this: DapperTsInterface, + communityId: string, + namespaceId: string, + packageName: string +) { + const data = await fetchPackageListingStatus({ + config: this.config, + params: { + community_id: communityId, + namespace_id: namespaceId, + package_name: packageName, + }, + data: {}, + queryParams: {}, }); return data; diff --git a/packages/dapper/src/types/package.ts b/packages/dapper/src/types/package.ts index a4d800978..91397474a 100644 --- a/packages/dapper/src/types/package.ts +++ b/packages/dapper/src/types/package.ts @@ -19,6 +19,12 @@ export interface PackageListing { export type PackageListings = PaginatedList; +export interface PackageListingStatus { + review_status: string; + rejection_reason: string | null; + internal_notes: string | null; +} + export interface PackageListingDetails extends PackageListing { community_name: string; datetime_created: string; diff --git a/packages/thunderstore-api/src/get/packageListingDetails.ts b/packages/thunderstore-api/src/get/packageListingDetails.ts index 9c98c3dc8..189900891 100644 --- a/packages/thunderstore-api/src/get/packageListingDetails.ts +++ b/packages/thunderstore-api/src/get/packageListingDetails.ts @@ -1,22 +1,49 @@ import { ApiEndpointProps } from "../index"; import { apiFetch } from "../apiFetch"; -import { packageListingDetailsSchema } from "../schemas/objectSchemas"; +import { + packageListingDetailsSchema, + packageListingStatusSchema, +} from "../schemas/objectSchemas"; import { PackageListingDetailsRequestParams } from "../schemas/requestSchemas"; -import { PackageListingDetailsResponseData } from "../schemas/responseSchemas"; +import { + PackageListingDetailsResponseData, + PackageListingStatusResponseData, +} from "../schemas/responseSchemas"; + +import { BASE_LISTING_PATH } from "../index"; export async function fetchPackageListingDetails( props: ApiEndpointProps ): Promise { - const { config, params } = props; - const path = `api/cyberstorm/listing/${params.community_id}/${params.namespace_id}/${params.package_name}/`; + const { config, params, useSession } = props; + const path = `${BASE_LISTING_PATH}${params.community_id}/${params.namespace_id}/${params.package_name}/`; return await apiFetch({ args: { config: config, path: path, + useSession: useSession, }, requestSchema: undefined, queryParamsSchema: undefined, responseSchema: packageListingDetailsSchema, }); } + +export async function fetchPackageListingStatus( + props: ApiEndpointProps +): Promise { + const { config, params } = props; + const path = `${BASE_LISTING_PATH}${params.community_id}/${params.namespace_id}/${params.package_name}/status/`; + + return await apiFetch({ + args: { + config: config, + path: path, + useSession: true, + }, + requestSchema: undefined, + queryParamsSchema: undefined, + responseSchema: packageListingStatusSchema, + }); +} diff --git a/packages/thunderstore-api/src/index.ts b/packages/thunderstore-api/src/index.ts index 9e4b0ffae..18f03ace2 100644 --- a/packages/thunderstore-api/src/index.ts +++ b/packages/thunderstore-api/src/index.ts @@ -14,6 +14,8 @@ export interface ApiEndpointProps { queryParams: QueryParams; } +export const BASE_LISTING_PATH = "api/cyberstorm/listing/"; + export * from "./delete/packageWiki"; export * from "./delete/teamDisband"; export * from "./delete/teamRemoveMember"; diff --git a/packages/thunderstore-api/src/schemas/objectSchemas.ts b/packages/thunderstore-api/src/schemas/objectSchemas.ts index caeaecc68..be49cc5f1 100644 --- a/packages/thunderstore-api/src/schemas/objectSchemas.ts +++ b/packages/thunderstore-api/src/schemas/objectSchemas.ts @@ -133,8 +133,19 @@ export const packageListingSchema = z.object({ size: z.number().int(), }); +export const packageListingStatusSchema = z.object({ + review_status: z + .enum(["unreviewed", "approved", "rejected"]) + .nullable() + .optional(), + rejection_reason: z.string().nullable().optional(), + internal_notes: z.string().nullable().optional(), +}); + export type PackageListing = z.infer; +export type PackageListingStatus = z.infer; + export const packageTeamSchema = z.object({ name: z.string().min(1), members: teamMemberSchema.array(), diff --git a/packages/thunderstore-api/src/schemas/responseSchemas.ts b/packages/thunderstore-api/src/schemas/responseSchemas.ts index 5d67c5939..104b89112 100644 --- a/packages/thunderstore-api/src/schemas/responseSchemas.ts +++ b/packages/thunderstore-api/src/schemas/responseSchemas.ts @@ -120,6 +120,17 @@ export type PackageListingDetailsResponseData = z.infer< typeof packageListingDetailsResponseDataSchema >; +// PackageListingStatusResponse +export const packageListingStatusResponseDataSchema = z.object({ + review_status: z.enum(["unreviewed", "approved", "rejected"]).nullable(), + rejection_reason: z.string().nullable(), + internal_notes: z.string().nullable(), +}); + +export type PackageListingStatusResponseData = z.infer< + typeof packageListingStatusResponseDataSchema +>; + // PackageVersionDetailsResponse export const packageVersionDetailsResponseDataSchema = z.object({ description: z.string(),