diff --git a/apps/cyberstorm-remix/app/c/Community.css b/apps/cyberstorm-remix/app/c/Community.css index 2e4cc9be0..5190fb42a 100644 --- a/apps/cyberstorm-remix/app/c/Community.css +++ b/apps/cyberstorm-remix/app/c/Community.css @@ -1,14 +1,41 @@ @layer nimbus-layout { .community { --nimbus-layout-content-max-width: 120rem; - } - .community__heading { + z-index: 1; display: flex; - gap: var(--gap-xl); - align-items: center; + flex-direction: column; + gap: 1rem; + align-items: flex-start; align-self: stretch; - padding: var(--space-8) 0 var(--space-20); + padding: 7.5rem 3rem 2rem; + } + + .community__background { + position: absolute; + display: flex; + flex-direction: column; + align-items: flex-start; + height: 20rem; + border-radius: var(--section-border-radius); + overflow: hidden; + } + + .community__background-image { + display: flex; + flex-direction: column; + align-items: flex-start; + background: var(--color-body-bg-color, #101028); + + opacity: 0.4; + mix-blend-mode: luminosity; + } + + .community__background-tint { + position: absolute; + width: 100%; + height: 20rem; + background: linear-gradient(180deg, rgb(16 16 40 / 0.4) 0%, #101028 85.94%); } .community__small-image { diff --git a/apps/cyberstorm-remix/app/c/community.tsx b/apps/cyberstorm-remix/app/c/community.tsx index fdf1a531d..88b36305e 100644 --- a/apps/cyberstorm-remix/app/c/community.tsx +++ b/apps/cyberstorm-remix/app/c/community.tsx @@ -176,66 +176,74 @@ export default function Community() { const outletContext = useOutletContext() as OutletContextShape; return ( -
- - - Communities - - - {community.name} - - -
-
- - {community.wiki_url ? ( - - - - - Modding Wiki - - - - - ) : null} - {community.discord_url ? ( - - - - - Modding Discord - - - - - ) : null} - - } + <> +
+ {community.hero_image_url ? ( + {community.name} + ) : null} +
+
+
+ + - {community.name} - -
+ Communities + + + {community.name} + + + + {community.wiki_url ? ( + + + + + Modding Wiki + + + + + ) : null} + {community.discord_url ? ( + + + + + Modding Discord + + + + + ) : null} + + } + > + {community.name} + -
-
+ + ); } diff --git a/apps/cyberstorm-remix/app/commonComponents/Navigation/Navigation.css b/apps/cyberstorm-remix/app/commonComponents/Navigation/Navigation.css index be1c0081a..0a6d5acd7 100644 --- a/apps/cyberstorm-remix/app/commonComponents/Navigation/Navigation.css +++ b/apps/cyberstorm-remix/app/commonComponents/Navigation/Navigation.css @@ -65,9 +65,6 @@ align-items: flex-start; } - /* .navigation-header__user-dropdown { - } */ - .navigation-header__user-button { display: inline-flex; flex-shrink: 0; diff --git a/apps/cyberstorm-remix/app/commonComponents/Navigation/Navigation.tsx b/apps/cyberstorm-remix/app/commonComponents/Navigation/Navigation.tsx index b8bee6e33..92b694531 100644 --- a/apps/cyberstorm-remix/app/commonComponents/Navigation/Navigation.tsx +++ b/apps/cyberstorm-remix/app/commonComponents/Navigation/Navigation.tsx @@ -353,7 +353,6 @@ export function DesktopUserDropdown(props: { } - rootClasses="navigation-header__user-dropdown" > @@ -379,8 +380,10 @@ export function DesktopUserDropdown(props: { diff --git a/apps/cyberstorm-remix/app/communities/communities.tsx b/apps/cyberstorm-remix/app/communities/communities.tsx index 646c6bf27..f90bac337 100644 --- a/apps/cyberstorm-remix/app/communities/communities.tsx +++ b/apps/cyberstorm-remix/app/communities/communities.tsx @@ -100,7 +100,6 @@ export async function clientLoader({ context, request }: LoaderFunctionArgs) { export default function CommunitiesPage() { const communitiesData = useLoaderData(); - console.log("Communities data loaded:", communitiesData); const navigationType = useNavigationType(); const [searchParams, setSearchParams] = useSearchParams(); @@ -169,7 +168,7 @@ export default function CommunitiesPage() { /> changeOrder(val as SortOptions)} options={selectOptions} value={searchParams.get("order") ?? SortOptions.Popular} aria-label="Sort communities by" diff --git a/apps/cyberstorm-remix/app/p/packageEdit.css b/apps/cyberstorm-remix/app/p/packageEdit.css new file mode 100644 index 000000000..b82b0ed8a --- /dev/null +++ b/apps/cyberstorm-remix/app/p/packageEdit.css @@ -0,0 +1,92 @@ +@layer nimbus-layout { + .package-edit { + --nimbus-layout-content-max-width: 90rem; + } + + .package-edit__main { + display: flex; + gap: var(--gap-xxxl); + align-items: center; + align-self: stretch; + } + + .package-edit__section { + display: flex; + flex: 1 1 0; + flex-direction: column; + gap: 3rem; + align-items: stretch; + } + + .package-edit__divider { + align-self: stretch; + height: 0.063rem; + background: linear-gradient(0deg, #23234d 0%, #23234d 100%); + } + + .package-edit__row { + display: flex; + flex: 1 1 0; + gap: 5rem; + align-items: flex-start; + justify-content: space-between; + } + + .package-edit__row-content { + display: flex; + flex: 1 1 0; + gap: var(--gap-md); + } + + .package-edit__info { + display: flex; + flex-direction: column; + gap: 0.5rem; + align-items: flex-start; + align-self: stretch; + width: 21rem; + } + + .package-edit__title { + align-self: stretch; + color: var(--color-text-primary); + font-weight: var(--font-weight-bold); + font-size: var(--font-size-body-lg); + line-height: var(--line-height-auto); + } + + .package-edit__description { + align-self: stretch; + color: var(--color-text-tertiary); + font-weight: var(--font-weight-medium); + font-size: var(--font-size-body-md); + line-height: var(--line-height-md); + } + + .package-edit__status { + display: flex; + flex: 1 0 0; + flex-direction: column; + gap: 0.75rem; + align-items: flex-start; + } + + .package-edit__status-description { + color: var(--color-text-tertiary); + font-weight: var(--font-weight-regular); + + font-size: var(--font-size-body-sm); + line-height: var(--line-height-auto); + } + + .package-edit__actions { + display: flex; + flex: 1 0 0; + flex-direction: row; + gap: var(--gap-md); + } + + .package-edit__save-button { + flex: 1 0 0; + } +} diff --git a/apps/cyberstorm-remix/app/p/packageEdit.tsx b/apps/cyberstorm-remix/app/p/packageEdit.tsx new file mode 100644 index 000000000..480372782 --- /dev/null +++ b/apps/cyberstorm-remix/app/p/packageEdit.tsx @@ -0,0 +1,440 @@ +import type { LoaderFunctionArgs, MetaFunction } from "react-router"; +import { useLoaderData, useOutletContext, useRevalidator } from "react-router"; +import { + NewAlert, + NewBreadCrumbs, + NewBreadCrumbsLink, + NewButton, + NewIcon, + NewSelectSearch, + NewTag, +} from "@thunderstore/cyberstorm"; +import "./packageEdit.css"; +import { + ApiError, + packageDeprecate, + packageListingUpdate, + PackageListingUpdateRequestData, + packageUnlist, +} from "@thunderstore/thunderstore-api"; +import { formatToDisplayName } from "@thunderstore/cyberstorm/src/utils/utils"; +import { DapperTs } from "@thunderstore/dapper-ts"; +import { OutletContextShape } from "~/root"; +import { getSessionTools } from "~/middlewares"; +import { PageHeader } from "~/commonComponents/PageHeader/PageHeader"; +import { useStrongForm } from "cyberstorm/utils/StrongForm/useStrongForm"; +import { useReducer } from "react"; +import { useToast } from "@thunderstore/cyberstorm/src/newComponents/Toast/Provider"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faBan, faCheck } from "@fortawesome/pro-solid-svg-icons"; +import { ApiAction } from "@thunderstore/ts-api-react-actions"; + +export const meta: MetaFunction = ({ data }) => { + return [ + { + title: data + ? `${formatToDisplayName(data.listing.name)} - Edit package` + : "Edit package", + }, + ]; +}; + +export async function loader({ params }: LoaderFunctionArgs) { + if (params.communityId && params.namespaceId && params.packageId) { + try { + const dapper = new DapperTs(() => { + return { + apiHost: import.meta.env.VITE_API_URL, + sessionId: undefined, + }; + }); + 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), + filters: await dapper.getCommunityFilters(params.communityId), + permissions: undefined, + }; + } catch (error) { + if (error instanceof ApiError) { + throw new Response("Package not found", { status: 404 }); + } else { + // REMIX TODO: Add sentry + throw error; + } + } + } + throw new Response("Package not found", { status: 404 }); +} + +// TODO: Needs to check if package is available for the logged in user +export async function clientLoader({ params, context }: LoaderFunctionArgs) { + if (params.communityId && params.namespaceId && params.packageId) { + try { + const tools = getSessionTools(context); + const dapper = new DapperTs(() => { + return { + apiHost: tools?.getConfig().apiHost, + sessionId: tools?.getConfig().sessionId, + }; + }); + + const permissions = await dapper.getPackagePermissions( + params.communityId, + params.namespaceId, + params.packageId + ); + + if (!permissions?.permissions.can_manage) { + throw new Response("Unauthorized", { status: 403 }); + } + + 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), + filters: await dapper.getCommunityFilters(params.communityId), + permissions: permissions, + }; + } catch (error) { + if (error instanceof ApiError) { + throw new Response("Package not found", { status: 404 }); + } else { + throw error; + } + } + } + throw new Response("Package not found", { status: 404 }); +} + +clientLoader.hydrate = true; + +export default function PackageListing() { + const { community, listing, filters, permissions } = useLoaderData< + typeof loader | typeof clientLoader + >(); + + const outletContext = useOutletContext() as OutletContextShape; + const config = outletContext.requestConfig; + const toast = useToast(); + const revalidator = useRevalidator(); + + const deprecateToggleAction = ApiAction({ + endpoint: packageDeprecate, + onSubmitSuccess: () => { + toast.addToast({ + csVariant: "success", + children: listing.is_deprecated + ? "Package undeprecated" + : "Package deprecated", + duration: 4000, + }); + revalidator.revalidate(); + }, + onSubmitError: (error) => { + toast.addToast({ + csVariant: "danger", + children: `Error occurred: ${error.message || "Unknown error"}`, + duration: 8000, + }); + }, + }); + + const unlistAction = ApiAction({ + endpoint: packageUnlist, + onSubmitSuccess: () => { + toast.addToast({ + csVariant: "success", + children: "Package unlisted", + duration: 4000, + }); + revalidator.revalidate(); + }, + onSubmitError: (error) => { + toast.addToast({ + csVariant: "danger", + children: `Error occurred: ${error.message || "Unknown error"}`, + duration: 8000, + }); + }, + }); + + function formFieldUpdateAction( + state: PackageListingUpdateRequestData, + action: { + field: keyof PackageListingUpdateRequestData; + value: PackageListingUpdateRequestData[keyof PackageListingUpdateRequestData]; + } + ) { + return { + ...state, + [action.field]: action.value, + }; + } + + const [formInputs, updateFormFieldState] = useReducer(formFieldUpdateAction, { + categories: listing.categories.map((c) => c.slug), + }); + + type SubmitorOutput = Awaited>; + + async function submitor(data: typeof formInputs): Promise { + return await packageListingUpdate({ + config: config, + params: { + community: community.identifier, + namespace: listing.namespace, + package: listing.name, + }, + queryParams: {}, + data: { categories: data.categories }, + }); + } + + type InputErrors = { + [key in keyof typeof formInputs]?: string | string[]; + }; + + const strongForm = useStrongForm< + typeof formInputs, + PackageListingUpdateRequestData, + Error, + SubmitorOutput, + Error, + InputErrors + >({ + inputs: formInputs, + submitor, + onSubmitSuccess: () => { + toast.addToast({ + csVariant: "success", + children: `Changes saved!`, + duration: 4000, + }); + }, + onSubmitError: (error) => { + toast.addToast({ + csVariant: "danger", + children: `Error occurred: ${error.message || "Unknown error"}`, + duration: 8000, + }); + }, + }); + + return ( +
+ + + Communities + + + {community.name} + + + {listing.namespace} + + + {listing.name} + + + Edit package + + + + Edit package + +
+
+ {permissions?.permissions.can_unlist ? ( + <> +
+
+
Listed
+
+ Control if the package is listed on Thunderstore. (in any + community) +
+
+
+
+ + When you unlist the package, this page too will become + unavailable. + +
+ + unlistAction({ + config: config, + params: { + community: community.identifier, + namespace: listing.namespace, + package: listing.name, + }, + queryParams: {}, + data: { unlist: "unlist" }, + useSession: true, + }) + } + csSize="medium" + csVariant="danger" + > + Unlist + +
+
+
+ + ) : null} + {permissions?.permissions.can_manage_deprecation ? ( + <> +
+
+
Status
+
+ Control the status of your package. +
+
+
+
+ + + + + {listing.is_deprecated ? "Deprecated" : "Active"} + + + {listing.is_deprecated + ? "This package is marked as deprecated and is not listed on Thunderstore." + : "This package is marked as active and is listed on Thunderstore."} + +
+ + deprecateToggleAction({ + config: config, + params: { + namespace: listing.namespace, + package: listing.name, + }, + queryParams: {}, + data: { deprecate: !listing.is_deprecated }, + useSession: true, + }) + } + csSize="medium" + csVariant={listing.is_deprecated ? "success" : "warning"} + > + {listing.is_deprecated ? "Undeprecate" : "Deprecate"} + +
+
+
+ + ) : null} +
+
+
Categories
+
+ Select descriptive categories to help people discover your + package. +
+
+
+ ({ + value: category.slug, + label: category.name, + }))} + onChange={(val) => { + updateFormFieldState({ + field: "categories", + value: val ? val.map((v) => v.value) : [], + }); + }} + value={formInputs.categories.map((categoryId) => ({ + value: categoryId, + label: + filters.package_categories.find( + (c) => c.slug === categoryId + )?.name || "", + }))} + /> +
+
+
+
+
+
Save changes
+
+ Your changes will take effect after hitting “Save”. +
+
+
+ + Cancel + + { + strongForm.submit(); + }} + rootClasses="package-edit__save-button" + > + Save changes + +
+
+
+
+
+ ); +} diff --git a/apps/cyberstorm-remix/app/p/packageListing.css b/apps/cyberstorm-remix/app/p/packageListing.css index a0f12303a..3cad99814 100644 --- a/apps/cyberstorm-remix/app/p/packageListing.css +++ b/apps/cyberstorm-remix/app/p/packageListing.css @@ -1,6 +1,35 @@ @layer nimbus-layout { .package-listing { --nimbus-layout-content-max-width: 120rem; + + z-index: 1; + } + + .package-community__background { + position: absolute; + display: flex; + flex-direction: column; + align-items: flex-start; + height: 20rem; + border-radius: var(--section-border-radius); + overflow: hidden; + } + + .package-community__background-image { + display: flex; + flex-direction: column; + align-items: flex-start; + background: var(--color-body-bg-color, #101028); + + opacity: 0.4; + mix-blend-mode: luminosity; + } + + .package-community__background-tint { + position: absolute; + width: 100%; + height: 20rem; + background: linear-gradient(180deg, rgb(16 16 40 / 0.4) 0%, #101028 85.94%); } .package-listing__main { @@ -10,7 +39,108 @@ align-self: stretch; } - .package-listing__section { + .package-listing__package-section { + position: relative; + display: flex; + flex: 1 0 0; + flex-direction: column; + gap: 8px; + align-items: center; + align-items: flex-start; + align-self: stretch; + justify-content: flex-start; + padding: 7.5rem 3rem 2rem; + } + + .package-listing__actions { + position: absolute; + top: 2.5rem; + right: 3rem; + display: flex; + flex-direction: column; + gap: var(--gap-md); + align-items: flex-end; + justify-content: center; + } + + .package-listing-management-tools { + display: flex; + gap: var(--gap-xs); + align-items: center; + } + + .package-listing-management-tools__island { + display: flex; + gap: 6px; + align-items: center; + justify-content: flex-end; + padding: var(--space-8); + border: 2px dashed var(--color-surface-a10); + border-radius: var(--radius-xl); + background: var(--color-surface-1); + } + + .review-package__body { + align-items: stretch; + } + + .review-package__block { + display: flex; + flex-direction: column; + gap: 1rem; + align-items: flex-start; + } + + .review-package__label { + color: var(--color-text-primary); + font-weight: 700; + font-size: 1rem; + font-style: normal; + line-height: normal; + } + + .review-package__textarea { + > textarea { + width: 100%; + height: 10rem; + } + } + + .review-package__footer { + justify-content: space-between; + } + + .report-package__body { + align-items: stretch; + } + + .report-package__block { + display: flex; + flex-direction: column; + gap: 1rem; + align-items: flex-start; + } + + .report-package__label { + color: var(--color-text-primary); + font-weight: var(--font-weight-regular); + font-size: var(--font-size-body-lg); + font-style: normal; + line-height: var(--line-height-md); + } + + .report-package__textarea { + > textarea { + width: 100%; + height: 10rem; + } + } + + .report-package__footer { + justify-content: flex-end; + } + + .package-listing__package-content-section { display: flex; flex: 1 0 0; flex-direction: column; diff --git a/apps/cyberstorm-remix/app/p/packageListing.tsx b/apps/cyberstorm-remix/app/p/packageListing.tsx index 3cad646a1..00c4df8b4 100644 --- a/apps/cyberstorm-remix/app/p/packageListing.tsx +++ b/apps/cyberstorm-remix/app/p/packageListing.tsx @@ -1,5 +1,6 @@ import type { LoaderFunctionArgs, MetaFunction } from "react-router"; import { + Await, Outlet, useLoaderData, useLocation, // useRevalidator, @@ -8,38 +9,59 @@ import { import { Drawer, Heading, - // Modal, + Modal, + NewAlert, NewBreadCrumbs, NewBreadCrumbsLink, NewButton, NewIcon, NewLink, + NewSelect, NewTag, + NewTextInput, Tabs, } from "@thunderstore/cyberstorm"; import "./packageListing.css"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { ApiError } from "@thunderstore/thunderstore-api"; +import { + ApiError, + fetchPackagePermissions, + packageListingApprove, + packageListingReject, + packageListingReport, + PackageListingReportRequestData, + RequestConfig, +} from "@thunderstore/thunderstore-api"; import { ThunderstoreLogo } from "@thunderstore/cyberstorm/src/svg/svg"; import { - // faCog, faUsers, faHandHoldingHeart, faDownload, faThumbsUp, faWarning, faCaretRight, + faScaleBalanced, + // faList, + // faBoxOpen, + faCog, } from "@fortawesome/free-solid-svg-icons"; import TeamMembers from "./components/TeamMembers/TeamMembers"; -import { ReactElement, useEffect, useRef, useState } from "react"; -import { useHydrated } from "remix-utils/use-hydrated"; import { - // PackageDeprecateAction, - // PackageEditForm, - PackageLikeAction, -} from "@thunderstore/cyberstorm-forms"; + ReactElement, + Suspense, + useEffect, + useReducer, + useRef, + useState, +} from "react"; +import { useHydrated } from "remix-utils/use-hydrated"; +import { PackageLikeAction } from "@thunderstore/cyberstorm-forms"; import { PageHeader } from "~/commonComponents/PageHeader/PageHeader"; -import { faArrowUpRight, faLips } from "@fortawesome/pro-solid-svg-icons"; +import { + faArrowUpRight, + // faFlagSwallowtail, + faLips, +} from "@fortawesome/pro-solid-svg-icons"; import { RelativeTime } from "@thunderstore/cyberstorm/src/components/RelativeTime/RelativeTime"; import { formatFileSize, @@ -49,6 +71,13 @@ import { import { DapperTs } from "@thunderstore/dapper-ts"; import { OutletContextShape } from "~/root"; import { CopyButton } from "~/commonComponents/CopyButton/CopyButton"; +import { getSessionTools } from "~/middlewares"; +import { getPackagePermissions } from "@thunderstore/dapper-ts/src/methods/package"; +import { useToast } from "@thunderstore/cyberstorm/src/newComponents/Toast/Provider"; +import { ApiAction } from "@thunderstore/ts-api-react-actions"; +import { TagVariants } from "@thunderstore/cyberstorm-theme/src/components"; +import { SelectOption } from "@thunderstore/cyberstorm/src/newComponents/Select/Select"; +import { useStrongForm } from "cyberstorm/utils/StrongForm/useStrongForm"; export const meta: MetaFunction = ({ data, location }) => { return [ @@ -116,6 +145,7 @@ export async function loader({ params }: LoaderFunctionArgs) { params.packageId ), team: await dapper.getTeamDetails(params.namespaceId), + permissions: undefined, }; } catch (error) { if (error instanceof ApiError) { @@ -130,38 +160,64 @@ export async function loader({ params }: LoaderFunctionArgs) { } // 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) { -// try { -// const dapper = window.Dapper; -// 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), -// currentUser: await dapper.getCurrentUser(), -// }; -// } catch (error) { -// if (error instanceof ApiError) { -// throw new Response("Package not found", { status: 404 }); -// } else { -// // REMIX TODO: Add sentry -// throw error; -// } -// } -// } -// throw new Response("Package not found", { status: 404 }); -// } +export async function clientLoader({ params, context }: LoaderFunctionArgs) { + if (params.communityId && params.namespaceId && params.packageId) { + try { + const tools = getSessionTools(context); + const dapper = new DapperTs(() => { + return { + apiHost: tools?.getConfig().apiHost, + sessionId: tools?.getConfig().sessionId, + }; + }); + + // We do some trickery right here to prevent unnecessary request when the user is not logged in + let permissionsPromise = undefined; + const cu = await tools.getSessionCurrentUser(); + if (cu.username) { + const wrapperPromise = + Promise.withResolvers< + Awaited> + >(); + dapper + .getPackagePermissions( + params.communityId, + params.namespaceId, + params.packageId + ) + .then(wrapperPromise.resolve, wrapperPromise.reject); + permissionsPromise = wrapperPromise.promise; + } + + 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: permissionsPromise, + }; + } catch (error) { + if (error instanceof ApiError) { + throw new Response("Package not found", { status: 404 }); + } else { + // REMIX TODO: Add sentry + throw error; + } + } + } + throw new Response("Package not found", { status: 404 }); +} + +clientLoader.hydrate = true; export default function PackageListing() { - // TODO: Enable when APIs are available - // const { community, communityFilters, listing, team } = - // useLoaderData(); - const { community, listing, team } = useLoaderData(); + const { community, listing, team, permissions } = useLoaderData< + typeof loader | typeof clientLoader + >(); const location = useLocation(); @@ -172,6 +228,7 @@ export default function PackageListing() { const dapper = outletContext.dapper; const [isLiked, setIsLiked] = useState(false); + const toast = useToast(); const fetchAndSetRatedPackages = async () => { const rp = await dapper.getRatedPackages(); @@ -180,14 +237,6 @@ export default function PackageListing() { ); }; - // TODO: Enable when APIs are available - // const revalidator = useRevalidator(); - // const revalidateLoaderData = async () => { - // if (revalidator.state === "idle") { - // revalidator.revalidate(); - // } - // }; - useEffect(() => { if (currentUser?.username) { fetchAndSetRatedPackages(); @@ -239,53 +288,99 @@ export default function PackageListing() { const currentTab = location.pathname.split("/")[6] || "details"; // TODO: Enable when APIs are available - // const managementTools = ( - // - // - // - // - // Manage - // - // } - // > - // { - // return { label: cat.name, value: cat.slug }; - // })} - // community={listing.community_identifier} - // namespace={listing.namespace} - // package={listing.name} - // current_categories={listing.categories} - // isDeprecated={listing.is_deprecated} - // dataUpdateTrigger={revalidateLoaderData} - // deprecationButton={ - // - // {listing.is_deprecated ? "Undeprecate" : "Deprecate"} - // - // } - // config={config} - // /> - // - // ); + function managementTools( + packagePermissions: Awaited> + ) { + return ( +
+ {packagePermissions.permissions.can_moderate ? ( +
+ {packagePermissions.permissions.can_moderate ? ( + + + + + Review Package + + } + > + + + ) : 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 likeAction = PackageLikeAction({ isLoggedIn: Boolean(currentUser?.username), @@ -330,7 +425,7 @@ export default function PackageListing() { ) } tooltipText="Like" - csVariant={isLiked ? "success" : "secondary"} + csVariant={isLiked ? "primary" : "secondary"} csSize="big" csModifiers={["only-icon"]} > @@ -338,7 +433,19 @@ export default function PackageListing() {
- {/* */} + {/* + + + + */} ); @@ -451,261 +558,557 @@ export default function PackageListing() { ); return ( -
- - - Communities - - - {community.name} - - - {listing.namespace} - - - - {formatToDisplayName(listing.name)} - - -
-
- - +
+ {community.hero_image_url ? ( + {community.name} + ) : null} +
+
+
+
+ }> + }> + {(resolvedValue) => + resolvedValue ? ( +
+ {managementTools(resolvedValue)} +
+ ) : null + } +
+
+ + + Communities + + + {community.name} + + + {listing.namespace} + + + + {formatToDisplayName(listing.name)} + + +
+
+ + + + + + {listing.namespace} + + {listing.website_url ? ( + + {listing.website_url} + + + + + ) : null} + + } + > + {formatToDisplayName(listing.name)} + + {/* Report modal is here, so that it can be reused in both desktop on mobile */} + {/* + + */} +
+ - - Details - - } - rootClasses="package-listing__drawer" - > - {packageMeta} + + + Details + + } + rootClasses="package-listing__drawer" + > + {packageMeta} + {packageBoxes} + + {actions} +
+ Description, + } as React.ComponentPropsWithRef, + current: currentTab === "details", + key: "description", + }, + { + itemProps: { + primitiveType: "cyberstormLink", + linkId: "PackageRequired", + community: listing.community_identifier, + namespace: listing.namespace, + package: listing.name, + "aria-current": currentTab === "required", + children: <>Required ({listing.dependency_count}), + } as React.ComponentPropsWithRef, + current: currentTab === "required", + key: "required", + }, + { + itemProps: { + primitiveType: "cyberstormLink", + linkId: "PackageWiki", + community: listing.community_identifier, + namespace: listing.namespace, + package: listing.name, + "aria-current": currentTab === "wiki", + children: <>Wiki, + } as React.ComponentPropsWithRef, + current: currentTab === "wiki", + key: "wiki", + }, + { + itemProps: { + primitiveType: "cyberstormLink", + linkId: "PackageChangelog", + community: listing.community_identifier, + namespace: listing.namespace, + package: listing.name, + "aria-current": currentTab === "changelog", + children: <>Changelog, + disabled: !listing.has_changelog, + } as React.ComponentPropsWithRef, + current: currentTab === "changelog", + key: "changelog", + }, + { + itemProps: { + primitiveType: "cyberstormLink", + linkId: "PackageVersions", + community: listing.community_identifier, + namespace: listing.namespace, + package: listing.name, + "aria-current": currentTab === "versions", + children: <>Versions, + } as React.ComponentPropsWithRef, + current: currentTab === "versions", + key: "versions", + // TODO: Version count field needs to be added to the endpoint + // numberSlateValue: listing.versionCount, + }, + // TODO: Once Analysis page is ready, enable it + // { + // itemProps: { + // key: "source", + // primitiveType: "cyberstormLink", + // linkId: "PackageSource", + // community: listing.community_identifier, + // namespace: listing.namespace, + // package: listing.name, + // "aria-current": currentTab === "source", + // children: <>Analysis, + // }, + // current: currentTab === "source", + // }, + { + itemProps: { + href: `${domain}/c/${listing.community_identifier}/p/${listing.namespace}/${listing.name}/source`, + primitiveType: "link", + "aria-current": currentTab === "source", + children: ( + <> + Analysis{" "} + + + + + ), + } as React.ComponentPropsWithRef, + current: currentTab === "source", + key: "source", + }, + ]} + renderTabItem={(key, itemProps, numberSlate) => { + const { children, ...fItemProps } = + itemProps as React.ComponentPropsWithRef; + return ( + + {children} + {numberSlate} + + ); + }} + /> +
+ +
+
+
- Description, - } as React.ComponentPropsWithRef, - current: currentTab === "details", - key: "description", - }, - { - itemProps: { - primitiveType: "cyberstormLink", - linkId: "PackageRequired", - community: listing.community_identifier, - namespace: listing.namespace, - package: listing.name, - "aria-current": currentTab === "required", - children: <>Required ({listing.dependency_count}), - } as React.ComponentPropsWithRef, - current: currentTab === "required", - key: "required", - }, - { - itemProps: { - primitiveType: "cyberstormLink", - linkId: "PackageWiki", - community: listing.community_identifier, - namespace: listing.namespace, - package: listing.name, - "aria-current": currentTab === "wiki", - children: <>Wiki, - } as React.ComponentPropsWithRef, - current: currentTab === "wiki", - key: "wiki", +
+
+ + ); +} + +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
+
+ + 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, }, - { - itemProps: { - primitiveType: "cyberstormLink", - linkId: "PackageChangelog", - community: listing.community_identifier, - namespace: listing.namespace, - package: listing.name, - "aria-current": currentTab === "changelog", - children: <>Changelog, - disabled: !listing.has_changelog, - } as React.ComponentPropsWithRef, - current: currentTab === "changelog", - key: "changelog", + queryParams: {}, + data: { + rejection_reason: rejectionReason, + internal_notes: internalNotes ? internalNotes : null, }, - { - itemProps: { - primitiveType: "cyberstormLink", - linkId: "PackageVersions", - community: listing.community_identifier, - namespace: listing.namespace, - package: listing.name, - "aria-current": currentTab === "versions", - children: <>Versions, - } as React.ComponentPropsWithRef, - current: currentTab === "versions", - key: "versions", - // TODO: Version count field needs to be added to the endpoint - // numberSlateValue: listing.versionCount, + }) + } + > + Reject + + + approvePackageAction({ + config: config, + params: { + community: communityId, + namespace: namespaceId, + package: packageId, }, - // TODO: Once Analysis page is ready, enable it - // { - // itemProps: { - // key: "source", - // primitiveType: "cyberstormLink", - // linkId: "PackageSource", - // community: listing.community_identifier, - // namespace: listing.namespace, - // package: listing.name, - // "aria-current": currentTab === "source", - // children: <>Analysis, - // }, - // current: currentTab === "source", - // }, - { - itemProps: { - href: `${domain}/c/${listing.community_identifier}/p/${listing.namespace}/${listing.name}/source`, - primitiveType: "link", - "aria-current": currentTab === "source", - children: ( - <> - Analysis{" "} - - - - - ), - } as React.ComponentPropsWithRef, - current: currentTab === "source", - key: "source", + queryParams: {}, + data: { + internal_notes: internalNotes ? internalNotes : null, }, - ]} - renderTabItem={(key, itemProps, numberSlate) => { - const { children, ...fItemProps } = - itemProps as React.ComponentPropsWithRef; - return ( - - {children} - {numberSlate} - - ); + }) + } + > + Approve + +
+
+ ); +} + +ReviewPackageForm.displayName = "ReviewPackageForm"; + +const reportOptions: SelectOption< + | "Spam" + | "Malware" + | "Reupload" + | "CopyrightOrLicense" + | "WrongCommunity" + | "WrongCategories" + | "Other" +>[] = [ + { value: "Spam", label: "Spam" }, + { value: "Malware", label: "Malware" }, + { value: "Reupload", label: "Reupload" }, + { value: "CopyrightOrLicense", label: "Copyright Or License" }, + { value: "WrongCommunity", label: "Wrong Community" }, + { value: "WrongCategories", label: "Wrong Categories" }, + { value: "Other", label: "Other" }, +]; + +function ReportPackageForm(props: { + // communityId: string; + // namespaceId: string; + // packageId: string; + id: string; + config: () => RequestConfig; + toast: ReturnType; +}) { + const { + // communityId, + // namespaceId, + // packageId, + id, + toast, + config, + } = props; + + function formFieldUpdateAction( + state: PackageListingReportRequestData, + action: { + field: keyof PackageListingReportRequestData; + value: PackageListingReportRequestData[keyof PackageListingReportRequestData]; + } + ) { + return { + ...state, + [action.field]: action.value, + }; + } + + const [formInputs, updateFormFieldState] = useReducer(formFieldUpdateAction, { + reason: "Other", + description: "", + }); + + type SubmitorOutput = Awaited>; + + async function submitor(data: typeof formInputs): Promise { + return await packageListingReport({ + config: config, + params: { id: id }, + queryParams: {}, + data: { reason: data.reason, description: data.description }, + }); + } + + type InputErrors = { + [key in keyof typeof formInputs]?: string | string[]; + }; + + const strongForm = useStrongForm< + typeof formInputs, + PackageListingReportRequestData, + Error, + SubmitorOutput, + Error, + InputErrors + >({ + inputs: formInputs, + submitor, + onSubmitSuccess: () => { + toast.addToast({ + csVariant: "success", + children: `Package reported`, + duration: 4000, + }); + }, + onSubmitError: (error) => { + toast.addToast({ + csVariant: "danger", + children: `Error occurred: ${error.message || "Unknown error"}`, + duration: 8000, + }); + }, + }); + + return ( +
+
Report Package
+
+
+

Reason

+ { + updateFormFieldState({ field: "reason", value: value }); }} + id="role" /> -
- -
-
- +
+
+

+ Additional information (optional) +

+ { + updateFormFieldState({ + field: "description", + value: e.target.value, + }); + }} + placeholder="Invalid submission" + csSize="textarea" + rootClasses="report-package__textarea" + /> +
+
+
+ + Submit +
); } -// const ReportButton = (props: Clickable) => ( -// -// -// -// -// -// ); +ReportPackageForm.displayName = "ReportPackageForm"; diff --git a/apps/cyberstorm-remix/app/root.tsx b/apps/cyberstorm-remix/app/root.tsx index 6bb068c1f..ab4715a71 100644 --- a/apps/cyberstorm-remix/app/root.tsx +++ b/apps/cyberstorm-remix/app/root.tsx @@ -84,7 +84,6 @@ export async function clientLoader({ context }: Route.ClientLoaderArgs) { if (!tools) { throw new Error("Session tools not found in context"); } - console.log("root clientloader run"); const currentUser = await tools.getSessionCurrentUser(); const config = tools.getConfig(); return { diff --git a/apps/cyberstorm-remix/app/routes.ts b/apps/cyberstorm-remix/app/routes.ts index 26815996f..9f440a7e9 100644 --- a/apps/cyberstorm-remix/app/routes.ts +++ b/apps/cyberstorm-remix/app/routes.ts @@ -27,6 +27,7 @@ export default [ ]), ]), ]), + route("/c/:communityId/p/:namespaceId/:packageId/edit", "p/packageEdit.tsx"), route( "/package/create/docs", "tools/package-format-docs/packageFormatDocs.tsx" diff --git a/apps/cyberstorm-remix/app/settings/teams/team/tabs/Profile/Profile.tsx b/apps/cyberstorm-remix/app/settings/teams/team/tabs/Profile/Profile.tsx index 9dd743184..b34112fc5 100644 --- a/apps/cyberstorm-remix/app/settings/teams/team/tabs/Profile/Profile.tsx +++ b/apps/cyberstorm-remix/app/settings/teams/team/tabs/Profile/Profile.tsx @@ -65,7 +65,7 @@ export default function Profile() { } const [formInputs, updateFormFieldState] = useReducer(formFieldUpdateAction, { - donation_link: "", + donation_link: team.donation_link || "", }); type SubmitorOutput = Awaited>; diff --git a/apps/cyberstorm-remix/app/styles/layout.css b/apps/cyberstorm-remix/app/styles/layout.css index 76eb5197c..336f99f0c 100644 --- a/apps/cyberstorm-remix/app/styles/layout.css +++ b/apps/cyberstorm-remix/app/styles/layout.css @@ -14,7 +14,6 @@ display: flex; align-items: flex-start; justify-content: center; - padding-bottom: 4.5rem; } .layout__content { diff --git a/apps/cyberstorm-remix/cyberstorm/utils/LinkLibrary.tsx b/apps/cyberstorm-remix/cyberstorm/utils/LinkLibrary.tsx index 3a9da0e92..517ae5120 100644 --- a/apps/cyberstorm-remix/cyberstorm/utils/LinkLibrary.tsx +++ b/apps/cyberstorm-remix/cyberstorm/utils/LinkLibrary.tsx @@ -82,6 +82,13 @@ const library: LinkLibrary = { ref={p.customRef} /> ), + PackageEdit: (p) => ( + + ), PackageRequired: (p) => ( ), + PackageEdit: (p) => ( + + ), PackageRequired: (p) => ( ), + PackageWikiNewPage: (p) => ( + + ), + PackageWikiPage: (p) => ( + + ), + PackageWikiPageEdit: (p) => ( + + ), PackageChangelog: (p) => ( RE | null; + /** Package's edit view */ + PackageEdit: (props: AnyProps & PackageProps) => RE | null; /** Package's required view */ PackageRequired: (props: AnyProps & PackageProps) => RE | null; /** Package's wiki view */ @@ -149,6 +151,7 @@ const library: LinkLibrary = { ManifestValidator: noop, MarkdownPreview: noop, Package: noop, + PackageEdit: noop, PackageRequired: noop, PackageWiki: noop, PackageWikiNewPage: noop, diff --git a/packages/cyberstorm/src/components/Links/Links.tsx b/packages/cyberstorm/src/components/Links/Links.tsx index 2ff606388..3d5788d9f 100644 --- a/packages/cyberstorm/src/components/Links/Links.tsx +++ b/packages/cyberstorm/src/components/Links/Links.tsx @@ -28,6 +28,7 @@ export type CyberstormLinkIds = | "ManifestValidator" | "MarkdownPreview" | "Package" + | "PackageEdit" | "PackageRequired" | "PackageWiki" | "PackageWikiNewPage" diff --git a/packages/cyberstorm/src/newComponents/BreadCrumbs/BreadCrumbs.css b/packages/cyberstorm/src/newComponents/BreadCrumbs/BreadCrumbs.css index 8cfc7a1a2..df2cd9d34 100644 --- a/packages/cyberstorm/src/newComponents/BreadCrumbs/BreadCrumbs.css +++ b/packages/cyberstorm/src/newComponents/BreadCrumbs/BreadCrumbs.css @@ -2,6 +2,7 @@ .breadcrumbs { display: flex; gap: var(--space-4); + width: 100%; padding-left: var(--space-16); overflow: auto; font-weight: var(--font-weight-regular); diff --git a/packages/cyberstorm/src/newComponents/Modal/Modal.tsx b/packages/cyberstorm/src/newComponents/Modal/Modal.tsx index 412f3f1a3..ba376aadb 100644 --- a/packages/cyberstorm/src/newComponents/Modal/Modal.tsx +++ b/packages/cyberstorm/src/newComponents/Modal/Modal.tsx @@ -9,12 +9,13 @@ import { ModalSizes } from "@thunderstore/cyberstorm-theme/src/components/Modal/ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; interface Props extends Omit { - trigger: ReactNode; + trigger?: ReactNode; csVariant?: ModalVariants; csSize?: ModalSizes; } // TODO: Add storybook story +// TODO: Currently the same modal can't be used in 2 different places in the same page. Fix that somehow export function Modal(props: Props) { const { children, csVariant = "default", csSize = "medium", trigger } = props; diff --git a/packages/cyberstorm/src/newComponents/Switch/Switch.css b/packages/cyberstorm/src/newComponents/Switch/Switch.css index 6f5f5694a..9c0da140b 100644 --- a/packages/cyberstorm/src/newComponents/Switch/Switch.css +++ b/packages/cyberstorm/src/newComponents/Switch/Switch.css @@ -26,5 +26,9 @@ box-shadow: var(--switch-knob-box-shadow); transform: var(--switch-knob-transform); transition: var(--switch-knob-transition); + + &:hover { + cursor: pointer; + } } } diff --git a/packages/dapper-fake/src/fakers/package.ts b/packages/dapper-fake/src/fakers/package.ts index 4da93d04a..6e264297b 100644 --- a/packages/dapper-fake/src/fakers/package.ts +++ b/packages/dapper-fake/src/fakers/package.ts @@ -88,6 +88,41 @@ const getFakeDependencies = async ( }); }; +export const getFakePackagePermissions = async ( + community: string, + namespace: string, + name: string +) => { + const seed = `${community}-${namespace}-${name}`; + setSeed(seed); + + // Return shape mirrors API: { package, permissions } + const canManage = faker.datatype.boolean(0.6); + const canModerate = faker.datatype.boolean(0.2); + + return { + package: { + community_id: community, + namespace_id: namespace, + package_name: name, + }, + permissions: { + can_manage: canManage, + can_manage_deprecation: faker.datatype.boolean(0.5), + can_manage_categories: faker.datatype.boolean(0.5), + can_deprecate: faker.datatype.boolean(0.5), + can_undeprecate: faker.datatype.boolean(0.5), + can_unlist: faker.datatype.boolean(0.5), + can_moderate: canModerate, + // Viewing pages is typically allowed if you can manage or moderate + can_view_package_admin_page: + canManage || canModerate || faker.datatype.boolean(0.3), + can_view_listing_admin_page: + canManage || canModerate || faker.datatype.boolean(0.3), + }, + }; +}; + // Content used to render Package's detail view. export const getFakePackageListingDetails = async ( community: string, diff --git a/packages/dapper-fake/src/index.ts b/packages/dapper-fake/src/index.ts index d712c5275..c47046166 100644 --- a/packages/dapper-fake/src/index.ts +++ b/packages/dapper-fake/src/index.ts @@ -9,6 +9,7 @@ import { getFakeChangelog, getFakeReadme } from "./fakers/markup"; import { getFakePackageListingDetails, getFakePackageListings, + getFakePackagePermissions, getFakePackageVersions, } from "./fakers/package"; import { getFakeServiceAccounts } from "./fakers/serviceAccount"; @@ -27,6 +28,7 @@ export class DapperFake implements DapperInterface { public getCommunityFilters = getFakeCommunityFilters; public getCurrentUser = getFakeCurrentUser; public getPackageChangelog = getFakeChangelog; + public getPackagePermissions = getFakePackagePermissions; public getPackageListingDetails = getFakePackageListingDetails; public getPackageListings = getFakePackageListings; public getPackageReadme = getFakeReadme; diff --git a/packages/dapper-ts/src/index.ts b/packages/dapper-ts/src/index.ts index e68160a8b..97ef4e4a7 100644 --- a/packages/dapper-ts/src/index.ts +++ b/packages/dapper-ts/src/index.ts @@ -14,6 +14,7 @@ import { getPackageSubmissionStatus, getPackageWiki, getPackageWikiPage, + getPackagePermissions, } from "./methods/package"; import { getPackageListingDetails, @@ -51,6 +52,7 @@ export class DapperTs implements DapperTsInterface { this.getPackageVersions = this.getPackageVersions.bind(this); this.getPackageWiki = this.getPackageWiki.bind(this); this.getPackageWikiPage = this.getPackageWikiPage.bind(this); + this.getPackagePermissions = this.getPackagePermissions.bind(this); this.getTeamDetails = this.getTeamDetails.bind(this); this.getTeamMembers = this.getTeamMembers.bind(this); this.getTeamServiceAccounts = this.getTeamServiceAccounts.bind(this); @@ -74,6 +76,7 @@ export class DapperTs implements DapperTsInterface { public getPackageVersions = getPackageVersions; public getPackageWiki = getPackageWiki; public getPackageWikiPage = getPackageWikiPage; + public getPackagePermissions = getPackagePermissions; public getTeamDetails = getTeamDetails; public getTeamMembers = getTeamMembers; public getTeamServiceAccounts = getTeamServiceAccounts; diff --git a/packages/dapper-ts/src/methods/package.ts b/packages/dapper-ts/src/methods/package.ts index ac0ec6c12..e6ceeeed1 100644 --- a/packages/dapper-ts/src/methods/package.ts +++ b/packages/dapper-ts/src/methods/package.ts @@ -6,6 +6,8 @@ import { fetchPackageSubmissionStatus, fetchPackageWiki, fetchPackageWikiPage, + fetchPackagePermissions, + ApiError, } from "@thunderstore/thunderstore-api"; import { z } from "zod"; @@ -151,3 +153,31 @@ export async function getPackageSubmissionStatus( return response; } + +export async function getPackagePermissions( + this: DapperTsInterface, + communityId: string, + namespaceId: string, + packageName: string +) { + try { + const response = await fetchPackagePermissions({ + config: this.config, + params: { + community_id: communityId, + namespace_id: namespaceId, + package_name: packageName, + }, + data: {}, + queryParams: {}, + }); + return response; + } catch (error) { + // In case of user not being logged in or stale session + if (error instanceof ApiError && error.response.status === 401) { + return undefined; + } else { + throw error; + } + } +} diff --git a/packages/dapper/src/dapper.ts b/packages/dapper/src/dapper.ts index cfd0bf1b3..e75268f1e 100644 --- a/packages/dapper/src/dapper.ts +++ b/packages/dapper/src/dapper.ts @@ -10,6 +10,7 @@ export interface DapperInterface { getPackageListings: methods.GetPackageListings; getPackageReadme: methods.GetPackageReadme; getPackageVersions: methods.GetPackageVersions; + getPackagePermissions: methods.GetPackagePermissions; getTeamDetails: methods.GetTeamDetails; getTeamMembers: methods.GetTeamMembers; getTeamServiceAccounts: methods.GetTeamServiceAccounts; diff --git a/packages/dapper/src/types/methods.ts b/packages/dapper/src/types/methods.ts index 480c66a25..f07136acd 100644 --- a/packages/dapper/src/types/methods.ts +++ b/packages/dapper/src/types/methods.ts @@ -2,6 +2,7 @@ import { Communities, Community, CommunityFilters } from "./community"; import { PackageListingDetails, PackageListings, + PackagePermissions, PackageSubmissionResponse, PackageVersion, } from "./package"; @@ -59,6 +60,12 @@ export type GetPackageVersions = ( name: string ) => Promise; +export type GetPackagePermissions = ( + namespaceId: string, + communityId: string, + packageName: string +) => Promise; + export type PostPackageSubmissionMetadata = ( author_name: string, communities: string[], diff --git a/packages/dapper/src/types/package.ts b/packages/dapper/src/types/package.ts index efce20577..a34a512f2 100644 --- a/packages/dapper/src/types/package.ts +++ b/packages/dapper/src/types/package.ts @@ -57,6 +57,25 @@ export interface PackageVersion { install_url: string; } +export interface PackagePermissions { + package: { + community_id: string; + namespace_id: string; + package_name: string; + }; + permissions: { + can_manage: boolean; + can_manage_deprecation: boolean; + can_manage_categories: boolean; + can_deprecate: boolean; + can_undeprecate: boolean; + can_unlist: boolean; + can_moderate: boolean; + can_view_package_admin_page: boolean; + can_view_listing_admin_page: boolean; + }; +} + export interface PackageSubmissionError { upload_uuid?: string[] | null; author_name?: string[] | null; diff --git a/packages/thunderstore-api/src/get/package.ts b/packages/thunderstore-api/src/get/package.ts new file mode 100644 index 000000000..1076c033e --- /dev/null +++ b/packages/thunderstore-api/src/get/package.ts @@ -0,0 +1,27 @@ +import { ApiEndpointProps } from "../index"; +import { apiFetch } from "../apiFetch"; +import { PackagePermissionsRequestParams } from "../schemas/requestSchemas"; +import { + PackagePermissionsResponseData, + packagePermissionsResponseDataSchema, +} from "../schemas/responseSchemas"; + +export async function fetchPackagePermissions( + props: ApiEndpointProps +): Promise { + const { config, params } = props; + const path = `api/cyberstorm/package/${params.community_id}/${params.namespace_id}/${params.package_name}/permissions`; + const request = { cache: "no-store" as RequestCache }; + + return await apiFetch({ + args: { + config: config, + path: path, + request: request, + useSession: true, + }, + requestSchema: undefined, + queryParamsSchema: undefined, + responseSchema: packagePermissionsResponseDataSchema, + }); +} diff --git a/packages/thunderstore-api/src/index.ts b/packages/thunderstore-api/src/index.ts index f5c6735d5..2c389a85b 100644 --- a/packages/thunderstore-api/src/index.ts +++ b/packages/thunderstore-api/src/index.ts @@ -22,6 +22,7 @@ export * from "./get/communityPackageListings"; export * from "./get/currentUser"; export * from "./get/ratedPackages"; export * from "./get/namespacePackageListings"; +export * from "./get/package"; export * from "./get/packageChangelog"; export * from "./get/packageDependantsListings"; export * from "./get/packageListingDetails"; diff --git a/packages/thunderstore-api/src/post/package.ts b/packages/thunderstore-api/src/post/package.ts index ab1b6632c..675bea08c 100644 --- a/packages/thunderstore-api/src/post/package.ts +++ b/packages/thunderstore-api/src/post/package.ts @@ -5,6 +5,9 @@ import { packageDeprecateRequestDataSchema, PackageDeprecateRequestParams, packageRateRequestDataSchema, + PackageUnlistRequestData, + packageUnlistRequestDataSchema, + PackageUnlistRequestParams, } from "../schemas/requestSchemas"; import { PackageRateRequestData } from "../schemas/requestSchemas"; import { PackageRateRequestParams } from "../schemas/requestSchemas"; @@ -13,6 +16,8 @@ import { packageDeprecateResponseDataSchema, packageRateResponseDataSchema, PackageRateResponseData, + PackageUnlistResponseData, + packageUnlistResponseDataSchema, } from "../schemas/responseSchemas"; export function packageRate( @@ -68,3 +73,30 @@ export function packageDeprecate( responseSchema: packageDeprecateResponseDataSchema, }); } + +export function packageUnlist( + props: ApiEndpointProps< + PackageUnlistRequestParams, + object, + PackageUnlistRequestData + > +): Promise { + const { config, params, data } = props; + const path = `/c/${params.community}/${params.namespace}/${params.package}/`; + + return apiFetch({ + args: { + config, + path, + request: { + method: "POST", + cache: "no-store", + body: JSON.stringify(data), + }, + useSession: props.useSession, + }, + requestSchema: packageUnlistRequestDataSchema, + queryParamsSchema: undefined, + responseSchema: packageUnlistResponseDataSchema, + }); +} diff --git a/packages/thunderstore-api/src/post/packageListing.ts b/packages/thunderstore-api/src/post/packageListing.ts index 7f2b593a0..079b267d1 100644 --- a/packages/thunderstore-api/src/post/packageListing.ts +++ b/packages/thunderstore-api/src/post/packageListing.ts @@ -10,6 +10,9 @@ import { packageListingUpdateRequestDataSchema, packageListingApproveRequestDataSchema, packageListingRejectRequestDataSchema, + PackageListingReportRequestParams, + PackageListingReportRequestData, + packageListingReportRequestDataSchema, } from "../schemas/requestSchemas"; import { PackageListingUpdateResponseData, @@ -78,7 +81,7 @@ export function packageListingReject( > ) { const { config, params, data } = props; - const path = `/api/cyberstorm/listing/${params.community}/${params.namespace}/${params.package}/approve/`; + const path = `/api/cyberstorm/listing/${params.community}/${params.namespace}/${params.package}/reject/`; return apiFetch({ args: { @@ -96,3 +99,32 @@ export function packageListingReject( responseSchema: undefined, }); } + +export function packageListingReport( + props: ApiEndpointProps< + PackageListingReportRequestParams, + object, + PackageListingReportRequestData + > +) { + const { config, params, data } = props; + // This will most likely change to a dedicated cyberstorm API endpoint, which is commented right now. + // const path = `/api/cyberstorm/listing/${params.community}/${params.namespace}/${params.package}/report/`; + const path = `/api/experimental/package-listing/${params.id}/report/`; + + return apiFetch({ + args: { + config, + path, + request: { + method: "POST", + cache: "no-store", + body: JSON.stringify(data), + }, + useSession: true, + }, + requestSchema: packageListingReportRequestDataSchema, + queryParamsSchema: undefined, + responseSchema: undefined, + }); +} diff --git a/packages/thunderstore-api/src/schemas/objectSchemas.ts b/packages/thunderstore-api/src/schemas/objectSchemas.ts index 5cf8c5fd0..a994d67f3 100644 --- a/packages/thunderstore-api/src/schemas/objectSchemas.ts +++ b/packages/thunderstore-api/src/schemas/objectSchemas.ts @@ -171,6 +171,35 @@ export const packageListingDetailsSchema = packageListingSchema.extend({ export type PackageListingDetails = z.infer; +export const packageInfoSchema = z.object({ + community_id: z.string().min(1), + namespace_id: z.string().min(1), + package_name: z.string().min(1), +}); + +export type PackageInfo = z.infer; + +export const permissionsSchema = z.object({ + can_manage: z.boolean(), + can_manage_deprecation: z.boolean(), + can_manage_categories: z.boolean(), + can_deprecate: z.boolean(), + can_undeprecate: z.boolean(), + can_unlist: z.boolean(), + can_moderate: z.boolean(), + can_view_package_admin_page: z.boolean(), + can_view_listing_admin_page: z.boolean(), +}); + +export type Permissions = z.infer; + +export const packagePermissionsSchema = z.object({ + package: packageInfoSchema, + permissions: permissionsSchema, +}); + +export type PackagePermissions = z.infer; + export const packageVersionSchema = z.object({ version_number: z.string().min(1), datetime_created: z.string().datetime(), diff --git a/packages/thunderstore-api/src/schemas/requestSchemas.ts b/packages/thunderstore-api/src/schemas/requestSchemas.ts index ac3118960..f51a34892 100644 --- a/packages/thunderstore-api/src/schemas/requestSchemas.ts +++ b/packages/thunderstore-api/src/schemas/requestSchemas.ts @@ -305,6 +305,25 @@ export type PackageDeprecateRequestData = z.infer< typeof packageDeprecateRequestDataSchema >; +// PackageUnlistRequest +export const packageUnlistRequestParamsSchema = z.object({ + package: z.string(), + namespace: z.string(), + community: z.string(), +}); + +export type PackageUnlistRequestParams = z.infer< + typeof packageUnlistRequestParamsSchema +>; + +export const packageUnlistRequestDataSchema = z.object({ + unlist: z.literal("unlist"), +}); + +export type PackageUnlistRequestData = z.infer< + typeof packageUnlistRequestDataSchema +>; + // PackageListingUpdateRequest export const packageListingUpdateRequestParamsSchema = z.object({ community: z.string(), @@ -400,6 +419,37 @@ export type PackageListingRejectRequestData = z.infer< typeof packageListingRejectRequestDataSchema >; +// PackageListingReportRequest +export const packageListingReportRequestParamsSchema = z.object({ + // This will most likely change to a dedicated cyberstorm endpoint, so the params will change to the commented ones. + id: z.string(), + // community: z.string(), + // namespace: z.string(), + // package: z.string(), +}); + +export type PackageListingReportRequestParams = z.infer< + typeof packageListingReportRequestParamsSchema +>; + +export const packageListingReportRequestDataSchema = z.object({ + version: z.number().optional(), + reason: z.enum([ + "Spam", + "Malware", + "Reupload", + "CopyrightOrLicense", + "WrongCommunity", + "WrongCategories", + "Other", + ]), + description: z.string().max(12288).optional().nullable(), +}); + +export type PackageListingReportRequestData = z.infer< + typeof packageListingReportRequestDataSchema +>; + // UserLinkedAccountDisconnectRequest export const userLinkedAccountDisconnectProvidersSchema = z.enum([ "discord", @@ -588,3 +638,14 @@ export const teamMemberRemoveRequestParamsSchema = z.object({ export type TeamMemberRemoveRequestParams = z.infer< typeof teamMemberRemoveRequestParamsSchema >; + +// PackagePermissionsRequest +export const packagePermissionsRequestParamsSchema = z.object({ + community_id: z.string(), + namespace_id: z.string(), + package_name: z.string(), +}); + +export type PackagePermissionsRequestParams = z.infer< + typeof packagePermissionsRequestParamsSchema +>; diff --git a/packages/thunderstore-api/src/schemas/responseSchemas.ts b/packages/thunderstore-api/src/schemas/responseSchemas.ts index 740668d5b..bc8816360 100644 --- a/packages/thunderstore-api/src/schemas/responseSchemas.ts +++ b/packages/thunderstore-api/src/schemas/responseSchemas.ts @@ -16,6 +16,7 @@ import { packageSubmissionStatusSchema, markdownRenderSchema, packageWikiPageSchema, + packagePermissionsSchema, } from "../schemas/objectSchemas"; import { paginatedResults } from "../schemas/objectSchemas"; @@ -107,6 +108,13 @@ export type PackageListingDetailsResponseData = z.infer< typeof packageListingDetailsResponseDataSchema >; +// PackagePermissionsResponse +export const packagePermissionsResponseDataSchema = packagePermissionsSchema; + +export type PackagePermissionsResponseData = z.infer< + typeof packagePermissionsResponseDataSchema +>; + // PackageReadmeResponse export const packageReadmeResponseDataSchema = z.object({ html: z.string(), @@ -215,9 +223,18 @@ export type PackageSubmissionResponseData = z.infer< typeof packageSubmissionResponseDataSchema >; +// PackageUnlistResponse +export const packageUnlistResponseDataSchema = z.object({ + message: z.string().min(1), +}); + +export type PackageUnlistResponseData = z.infer< + typeof packageUnlistResponseDataSchema +>; + // PackageListingDeprecateResponse export const packageDeprecateResponseDataSchema = z.object({ - deprecate: z.boolean(), + message: z.string().min(1), }); export type PackageDeprecateResponseData = z.infer< @@ -226,7 +243,12 @@ export type PackageDeprecateResponseData = z.infer< // PackageListingUpdateResponse export const packageListingUpdateResponseDataSchema = z.object({ - categories: z.array(z.string()), + categories: z.array( + z.object({ + slug: z.string().min(1), + name: z.string().min(1), + }) + ), }); export type PackageListingUpdateResponseData = z.infer<