diff --git a/apps/cyberstorm-nextjs/app/c/[community]/(package)/@tspackage/p/[namespace]/(packageRoute)/[package]/(pkg)/@packageCard/@packageManagement/page.tsx b/apps/cyberstorm-nextjs/app/c/[community]/(package)/@tspackage/p/[namespace]/(packageRoute)/[package]/(pkg)/@packageCard/@packageManagement/page.tsx index cabb209d1..03765c9e6 100644 --- a/apps/cyberstorm-nextjs/app/c/[community]/(package)/@tspackage/p/[namespace]/(packageRoute)/[package]/(pkg)/@packageCard/@packageManagement/page.tsx +++ b/apps/cyberstorm-nextjs/app/c/[community]/(package)/@tspackage/p/[namespace]/(packageRoute)/[package]/(pkg)/@packageCard/@packageManagement/page.tsx @@ -1,10 +1,12 @@ "use client"; -import { Alert, Button, Tag, TextInput } from "@thunderstore/cyberstorm"; -import styles from "./PackageManagementForm.module.css"; +import { Button } from "@thunderstore/cyberstorm"; import { useDapper } from "@thunderstore/dapper"; import { usePromise } from "@thunderstore/use-promise"; -import { faCircleExclamation } from "@fortawesome/pro-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { + PackageDeprecateAction, + PackageEditForm, +} from "@thunderstore/cyberstorm-forms"; +import { useState } from "react"; export default function Page({ params, @@ -12,49 +14,59 @@ export default function Page({ params: { community: string; namespace: string; package: string }; }) { const dapper = useDapper(); - const packageData = usePromise(dapper.getPackageListingDetails, [ + const packageDataInitial = usePromise(dapper.getPackageListingDetails, [ params.community, params.namespace, params.package, ]); + const communityData = usePromise(dapper.getCommunityFilters, [ + params.community, + ]); + const options = communityData.package_categories.map((cat) => { + return { label: cat.name, value: cat.slug }; + }); + + const [packageData, setPackageData] = useState(packageDataInitial); + + // TODO: Convert to using usePromise's cache when it can handle manual busts + // Or React Query stuff + async function useUpdatePackageData() { + const dapper = useDapper(); + const packageDataUpdate = await dapper.getPackageListingDetails( + params.community, + params.namespace, + params.package + ); + setPackageData(packageDataUpdate); + } return ( -
-
- } - content={ - "Changes might take several minutes to show publicly! Info shown below is always up to date." - } - variant="info" - /> -
Package status
-
- -
-
-
-
Edit categories
- -
-
- {packageData.is_deprecated ? ( - + cat.slug)} + isDeprecated={packageData.is_deprecated} + deprecationButton={ + + {packageData.is_deprecated ? ( Undeprecate - - ) : ( - + ) : ( Deprecate - - )} - - Save changes + )} -
-
+ } + /> ); } diff --git a/apps/cyberstorm-nextjs/app/c/[community]/(package)/@tspackage/p/[namespace]/(packageRoute)/[package]/(pkg)/@packageDetail/layout.tsx b/apps/cyberstorm-nextjs/app/c/[community]/(package)/@tspackage/p/[namespace]/(packageRoute)/[package]/(pkg)/@packageDetail/layout.tsx index c8a70c75a..5bfa2e087 100644 --- a/apps/cyberstorm-nextjs/app/c/[community]/(package)/@tspackage/p/[namespace]/(packageRoute)/[package]/(pkg)/@packageDetail/layout.tsx +++ b/apps/cyberstorm-nextjs/app/c/[community]/(package)/@tspackage/p/[namespace]/(packageRoute)/[package]/(pkg)/@packageDetail/layout.tsx @@ -41,6 +41,8 @@ export default function PackageDetailsLayout({ currentUser.rated_packages.includes(packageData.uuid4) ); + // TODO: Convert to using usePromise's cache when it can handle manual busts + // Or React Query stuff async function useUpdateLikeStatus() { const dapper = useDapper(); const currentUser = await dapper.getCurrentUser(); @@ -73,6 +75,7 @@ export default function PackageDetailsLayout({ ) : null} Promise; +}) { + const { onSubmitSuccess, onSubmitError } = useFormToaster({ + successMessage: `${ + props.isDeprecated ? "Undeprecated" : "Deprecated" + } package ${props.packageName}`, + }); + + function onActionSuccess() { + props.packageDataUpdateTrigger(); + onSubmitSuccess(); + } + + function onActionError() { + onSubmitError(); + } + + const onSubmit = ApiAction({ + schema: PackageDeprecateActionSchema, + meta: { packageName: props.packageName, namespace: props.namespace }, + endpoint: packageDeprecate, + onSubmitSuccess: onActionSuccess, + onSubmitError: onActionError, + }); + + return function () { + onSubmit({ is_deprecated: !props.isDeprecated }); + }; +} + +PackageDeprecateAction.displayName = "PackageDeprecateAction"; diff --git a/packages/cyberstorm-forms/src/actions/PackageLikeAction.tsx b/packages/cyberstorm-forms/src/actions/PackageLikeAction.tsx index 8f6e218cf..40ebb7f05 100644 --- a/packages/cyberstorm-forms/src/actions/PackageLikeAction.tsx +++ b/packages/cyberstorm-forms/src/actions/PackageLikeAction.tsx @@ -8,6 +8,7 @@ import { import { packageLike } from "@thunderstore/thunderstore-api"; export function PackageLikeAction(props: { + isLoggedIn: boolean; packageName: string; uuid4: string; isLiked: boolean; @@ -17,6 +18,9 @@ export function PackageLikeAction(props: { successMessage: `${props.isLiked ? "Unliked" : "Liked"} package ${ props.packageName }`, + errorMessage: props.isLoggedIn + ? "Unknown error occurred. The error has been logged" + : "You must be logged in to like a package!", }); function onActionSuccess() { diff --git a/apps/cyberstorm-nextjs/app/c/[community]/(package)/@tspackage/p/[namespace]/(packageRoute)/[package]/(pkg)/@packageCard/@packageManagement/PackageManagementForm.module.css b/packages/cyberstorm-forms/src/forms/PackageEditForm.module.css similarity index 50% rename from apps/cyberstorm-nextjs/app/c/[community]/(package)/@tspackage/p/[namespace]/(packageRoute)/[package]/(pkg)/@packageCard/@packageManagement/PackageManagementForm.module.css rename to packages/cyberstorm-forms/src/forms/PackageEditForm.module.css index ad8dfc16c..99dd89f37 100644 --- a/apps/cyberstorm-nextjs/app/c/[community]/(package)/@tspackage/p/[namespace]/(packageRoute)/[package]/(pkg)/@packageCard/@packageManagement/PackageManagementForm.module.css +++ b/packages/cyberstorm-forms/src/forms/PackageEditForm.module.css @@ -2,10 +2,20 @@ display: flex; flex-direction: column; gap: var(--gap--32); - max-height: 70%; } -.section { +.main { + display: flex; + flex-direction: column; + gap: var(--space--32); + padding: var(--space--32); +} + +.dialog { + border-top: var(--border-width--px) solid var(--color-surface--5); +} + +.statusSection { display: flex; flex-direction: column; gap: var(--gap--16); @@ -23,6 +33,9 @@ .footer { display: flex; - flex-direction: row; + flex-direction: row-reverse; + gap: 1rem; justify-content: space-between; + padding: var(--space--16) var(--space--24); + border-top: var(--border-width--px) solid var(--color-surface--5); } diff --git a/packages/cyberstorm-forms/src/forms/PackageEditForm.tsx b/packages/cyberstorm-forms/src/forms/PackageEditForm.tsx new file mode 100644 index 000000000..bcbc02a53 --- /dev/null +++ b/packages/cyberstorm-forms/src/forms/PackageEditForm.tsx @@ -0,0 +1,99 @@ +"use client"; + +import styles from "./PackageEditForm.module.css"; +import { packageEditCategories } from "@thunderstore/thunderstore-api"; +import { + ApiForm, + packageEditFormSchema, +} from "@thunderstore/ts-api-react-forms"; +import { + FormMultiSelectSearch, + FormSubmitButton, + useFormToaster, +} from "@thunderstore/cyberstorm-forms"; +import { Alert, MultiSelectSearchOption, Tag } from "@thunderstore/cyberstorm"; +import { ReactNode } from "react"; +import { faCircleExclamation } from "@fortawesome/pro-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +// TODO: Separate deprecation action from this form +// when we move away from the modal +// Package deprecation is done for the package, while other edits +// are for the package listing +export function PackageEditForm(props: { + dialogOnChange?: (v: boolean) => void; + options: MultiSelectSearchOption[]; + community: string; + namespace: string; + package: string; + current_categories: string[]; + isDeprecated: boolean; + deprecationButton: ReactNode; +}) { + const { onSubmitSuccess, onSubmitError } = useFormToaster({ + successMessage: "Changes saved!", + }); + + return ( + +
+
+
+ } + content={ + "Changes might take several minutes to show publicly! Info shown below is always up to date." + } + variant="info" + /> +
+
+
+
Package status
+
+ +
+
+
+
+
Edit categories
+
Current categories
+ {props.current_categories} +
New categories
+ + v.map((x) => x.value) + } + /> +
+
+
+ + {props.deprecationButton} +
+
+
+ ); +} + +PackageEditForm.displayName = "PackageEditForm"; diff --git a/packages/cyberstorm-forms/src/index.ts b/packages/cyberstorm-forms/src/index.ts index f75356cff..3938927fe 100644 --- a/packages/cyberstorm-forms/src/index.ts +++ b/packages/cyberstorm-forms/src/index.ts @@ -1,6 +1,7 @@ export { useFormToaster } from "./useFormToaster"; export { TeamMemberChangeRoleAction } from "./actions/TeamMemberChangeRoleAction"; export { PackageLikeAction } from "./actions/PackageLikeAction"; +export { PackageDeprecateAction } from "./actions/PackageDeprecateAction"; export { FormSubmitButton } from "./components/FormSubmitButton"; export { FormSelectSearch } from "./components/FormSelectSearch"; export { FormMultiSelectSearch } from "./components/FormMultiSelectSearch"; @@ -15,3 +16,4 @@ export { RemoveServiceAccountForm } from "./forms/RemoveServiceAccountForm"; export { DeleteAccountForm } from "./forms/DeleteAccountForm"; export { DisbandTeamForm } from "./forms/DisbandTeamForm"; export { LeaveTeamForm } from "./forms/LeaveTeamForm"; +export { PackageEditForm } from "./forms/PackageEditForm"; diff --git a/packages/cyberstorm-forms/src/useFormToaster.ts b/packages/cyberstorm-forms/src/useFormToaster.ts index ec89fe582..7e0e53254 100644 --- a/packages/cyberstorm-forms/src/useFormToaster.ts +++ b/packages/cyberstorm-forms/src/useFormToaster.ts @@ -4,6 +4,7 @@ import { useToast } from "@thunderstore/cyberstorm/src/components/Toast/Provider export type UseFormToasterArgs = { successMessage: string; + errorMessage?: string; }; export type UseFormToasterReturn = { onSubmitSuccess: () => void; @@ -11,6 +12,7 @@ export type UseFormToasterReturn = { }; export function useFormToaster({ successMessage, + errorMessage, }: UseFormToasterArgs): UseFormToasterReturn { const toast = useToast(); @@ -25,7 +27,9 @@ export function useFormToaster({ onSubmitError: () => { toast.addToast({ variant: "danger", - message: "Unknown error occurred. The error has been logged", + message: errorMessage + ? errorMessage + : "Unknown error occurred. The error has been logged", }); }, }; diff --git a/packages/cyberstorm/src/components/CollapsibleText/CollapsibleText.module.css b/packages/cyberstorm/src/components/CollapsibleText/CollapsibleText.module.css index 3dc709fe1..84a8240af 100644 --- a/packages/cyberstorm/src/components/CollapsibleText/CollapsibleText.module.css +++ b/packages/cyberstorm/src/components/CollapsibleText/CollapsibleText.module.css @@ -3,6 +3,7 @@ } .text { + max-width: 45rem; overflow: hidden; color: var(--color-text--secondary); font-size: var(--font-size--l); diff --git a/packages/thunderstore-api/src/fetch/packageDeprecate.ts b/packages/thunderstore-api/src/fetch/packageDeprecate.ts new file mode 100644 index 000000000..8003adfd8 --- /dev/null +++ b/packages/thunderstore-api/src/fetch/packageDeprecate.ts @@ -0,0 +1,29 @@ +import { RequestConfig } from "../index"; +import { apiFetch2 } from "../apiFetch"; + +export type packageDeprecateMetaArgs = { + packageName: string; + namespace: string; +}; + +export type packageDeprecateApiArgs = { + is_deprecated: boolean; +}; + +export function packageDeprecate( + config: RequestConfig, + data: packageDeprecateApiArgs, + meta: packageDeprecateMetaArgs +) { + const path = `/api/cyberstorm/package/${meta.namespace}/${meta.packageName}/deprecate/`; + + return apiFetch2({ + config, + path, + request: { + method: "POST", + cache: "no-store", + body: JSON.stringify(data), + }, + }); +} diff --git a/packages/thunderstore-api/src/fetch/packageEditCategories.ts b/packages/thunderstore-api/src/fetch/packageEditCategories.ts new file mode 100644 index 000000000..8d9487529 --- /dev/null +++ b/packages/thunderstore-api/src/fetch/packageEditCategories.ts @@ -0,0 +1,34 @@ +import { RequestConfig } from "../index"; +import { apiFetch2 } from "../apiFetch"; + +export type PackageEditMetaArgs = { + community: string; + namespace: string; + package: string; + current_categories: string[]; +}; + +export type PackageEditApiArgs = { + new_categories: string[]; +}; + +export function packageEditCategories( + config: RequestConfig, + data: PackageEditApiArgs, + meta: PackageEditMetaArgs +) { + console.log("asdasdasd"); + const path = `/api/cyberstorm/listing/${meta.community}/${meta.namespace}/${meta.package}/edit/categories/`; + + return apiFetch2({ + config, + path, + request: { + method: "POST", + body: JSON.stringify({ + ...data, + current_categories: meta.current_categories, + }), + }, + }); +} diff --git a/packages/thunderstore-api/src/index.ts b/packages/thunderstore-api/src/index.ts index 12cf16ecb..8c0fc8bad 100644 --- a/packages/thunderstore-api/src/index.ts +++ b/packages/thunderstore-api/src/index.ts @@ -30,5 +30,7 @@ export * from "./fetch/userDelete"; export * from "./fetch/teamDisbandTeam"; export * from "./fetch/teamEditMember"; export * from "./fetch/packageLike"; +export * from "./fetch/packageEditCategories"; +export * from "./fetch/packageDeprecate"; export * from "./fetch/teamRemoveMember"; export * from "./errors"; diff --git a/packages/ts-api-react-actions/src/index.ts b/packages/ts-api-react-actions/src/index.ts index 49f3b6a0e..a032ccc7c 100644 --- a/packages/ts-api-react-actions/src/index.ts +++ b/packages/ts-api-react-actions/src/index.ts @@ -1,2 +1,3 @@ export { ApiAction } from "./ApiAction"; export { packageLikeActionSchema } from "./schema"; +export { PackageDeprecateActionSchema } from "./schema"; diff --git a/packages/ts-api-react-actions/src/schema.ts b/packages/ts-api-react-actions/src/schema.ts index 82abb89e1..2b276660f 100644 --- a/packages/ts-api-react-actions/src/schema.ts +++ b/packages/ts-api-react-actions/src/schema.ts @@ -3,3 +3,7 @@ import { z } from "zod"; export const packageLikeActionSchema = z.object({ target_state: z.union([z.literal("rated"), z.literal("unrated")]), }); + +export const PackageDeprecateActionSchema = z.object({ + is_deprecated: z.boolean(), +}); diff --git a/packages/ts-api-react-forms/src/index.ts b/packages/ts-api-react-forms/src/index.ts index c0f24f532..0160cc214 100644 --- a/packages/ts-api-react-forms/src/index.ts +++ b/packages/ts-api-react-forms/src/index.ts @@ -9,3 +9,4 @@ export { teamAddServiceAccountFormSchema } from "./schema"; export { userDeleteFormSchema } from "./schema"; export { teamDisbandFormSchema } from "./schema"; export { teamEditMemberFormSchema } from "./schema"; +export { packageEditFormSchema } from "./schema"; diff --git a/packages/ts-api-react-forms/src/schema.ts b/packages/ts-api-react-forms/src/schema.ts index 94c31e5c0..825e2d184 100644 --- a/packages/ts-api-react-forms/src/schema.ts +++ b/packages/ts-api-react-forms/src/schema.ts @@ -47,3 +47,7 @@ export const teamDisbandFormSchema = z.object({ .string({ required_error: "Verification is required" }) .min(1, { message: "Verification is required" }), }); + +export const packageEditFormSchema = z.object({ + new_categories: z.array(z.string()), +});