-
Notifications
You must be signed in to change notification settings - Fork 27
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add report button to package details page
Adds a report button to the package details page Adds the react / tsx necessary to make it work Modifies APIError to take an extractedMessage Renames BaseApiError to BaseValidationError Adds the "required" optional property to FormSelectFieldProps
- Loading branch information
Showing
13 changed files
with
366 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import React, { useState } from "react"; | ||
import { ReportModal } from "./ReportModal"; | ||
import { | ||
ReportModalContextProps, | ||
ReportModalContextProvider, | ||
} from "./ReportModalContext"; | ||
import { CsrfTokenProvider } from "../CsrfTokenContext"; | ||
|
||
export const ReportButton: React.FC<ReportModalContextProps> = (props) => { | ||
const [isVisible, setIsVisible] = useState<boolean>(false); | ||
const closeModal = () => setIsVisible(false); | ||
|
||
return ( | ||
<CsrfTokenProvider token={props.csrfToken}> | ||
<ReportModalContextProvider initial={props} closeModal={closeModal}> | ||
{isVisible && <ReportModal />} | ||
<button | ||
type={"button"} | ||
className="btn btn-danger" | ||
aria-label="Report" | ||
onClick={() => setIsVisible(true)} | ||
> | ||
<span className="fa fa-exclamation-circle" /> | ||
Report | ||
</button> | ||
</ReportModalContextProvider> | ||
</CsrfTokenProvider> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
import React, { CSSProperties } from "react"; | ||
import { useReportModalContext } from "./ReportModalContext"; | ||
import { useOnEscape } from "../common/useOnEscape"; | ||
import { ReportForm, useReportForm } from "./hooks"; | ||
import { FormSelectField } from "../FormSelectField"; | ||
import { FieldError } from "react-hook-form/dist/types"; | ||
|
||
const Header: React.FC = () => { | ||
const context = useReportModalContext(); | ||
|
||
return ( | ||
<div className="modal-header"> | ||
<div className="modal-title">Submit Report</div> | ||
<button | ||
type="button" | ||
className="close" | ||
aria-label="Close" | ||
onClick={context.closeModal} | ||
ref={(element) => element?.focus()} | ||
> | ||
<span aria-hidden="true">×</span> | ||
</button> | ||
</div> | ||
); | ||
}; | ||
|
||
function getErrorMessage(error: FieldError): string { | ||
if (error.message) return error.message; | ||
switch (error.type) { | ||
case "required": | ||
return "This field is required"; | ||
case "maxLength": | ||
return "Max length exceeded"; | ||
default: | ||
return "Unknown error"; | ||
} | ||
} | ||
|
||
function getFormErrorMessage(errorMessage: string): string { | ||
if (errorMessage === "Authentication credentials were not provided.") { | ||
return "You must be logged in to do this."; | ||
} | ||
return errorMessage; | ||
} | ||
|
||
const FieldError: React.FC<{ error?: FieldError }> = ({ error }) => { | ||
if (!error) return null; | ||
return <span className={"mt-1 text-danger"}>{getErrorMessage(error)}</span>; | ||
}; | ||
|
||
interface BodyProps { | ||
form: ReportForm; | ||
} | ||
|
||
const Body: React.FC<BodyProps> = ({ form }) => { | ||
const context = useReportModalContext().props; | ||
|
||
return ( | ||
<div className="modal-body gap-2 d-flex flex-column"> | ||
<div className={"d-flex flex-column gap-1"}> | ||
<label className={"mb-0"}>Report reason</label> | ||
<FormSelectField | ||
className={"select-category"} | ||
control={form.control} | ||
name={"reason"} | ||
data={context.reasonChoices} | ||
getOption={(x) => x} | ||
required={true} | ||
/> | ||
<FieldError error={form.fieldErrors?.reason} /> | ||
</div> | ||
<div className={"d-flex flex-column gap-1"}> | ||
<label className={"mb-0"}>Description (optional)</label> | ||
<textarea | ||
{...form.control.register("description", { | ||
maxLength: context.descriptionMaxLength, | ||
})} | ||
className={"code-input"} | ||
style={{ minHeight: "100px", fontFamily: "inherit" }} | ||
/> | ||
<FieldError error={form.fieldErrors?.description} /> | ||
</div> | ||
|
||
{form.error && ( | ||
<div className={"alert alert-danger mt-2 mb-0"}> | ||
<p className={"mb-0"}>{getFormErrorMessage(form.error)}</p> | ||
</div> | ||
)} | ||
{form.status === "SUBMITTING" && ( | ||
<div className={"alert alert-warning mt-2 mb-0"}> | ||
<p className={"mb-0"}>Submitting...</p> | ||
</div> | ||
)} | ||
{form.status === "SUCCESS" && ( | ||
<div className={"alert alert-success mt-2 mb-0"}> | ||
<p className={"mb-0"}>Report submitted successfully!</p> | ||
</div> | ||
)} | ||
</div> | ||
); | ||
}; | ||
|
||
interface FooterProps { | ||
form: ReportForm; | ||
} | ||
|
||
const Footer: React.FC<FooterProps> = ({ form }) => { | ||
return ( | ||
<div className="modal-footer d-flex justify-content-end"> | ||
<button | ||
type="button" | ||
className="btn btn-danger" | ||
disabled={ | ||
form.status === "SUBMITTING" || form.status === "SUCCESS" | ||
} | ||
onClick={form.onSubmit} | ||
> | ||
Submit | ||
</button> | ||
</div> | ||
); | ||
}; | ||
export const ReportModal: React.FC = () => { | ||
const context = useReportModalContext(); | ||
useOnEscape(context.closeModal); | ||
|
||
const form = useReportForm({ | ||
packageListingId: context.props.packageListingId, | ||
packageVersionId: context.props.packageVersionId, | ||
}); | ||
|
||
const style = { | ||
backgroundColor: "rgba(0, 0, 0, 0.4)", | ||
display: "block", | ||
} as CSSProperties; | ||
return ( | ||
<div | ||
className="modal" | ||
role="dialog" | ||
style={style} | ||
onClick={context.closeModal} | ||
> | ||
<div | ||
className="modal-dialog modal-dialog-centered" | ||
role="document" | ||
onClick={(e) => e.stopPropagation()} | ||
> | ||
<div className="modal-content"> | ||
<Header /> | ||
<Body form={form} /> | ||
<Footer form={form} /> | ||
</div> | ||
</div> | ||
</div> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import React, { PropsWithChildren, useContext } from "react"; | ||
|
||
export type ReportModalContextProps = { | ||
csrfToken: string; | ||
packageListingId: string; | ||
packageVersionId: string; | ||
reasonChoices: { value: string; label: string }[]; | ||
descriptionMaxLength: number; | ||
}; | ||
|
||
export interface IReportModalContext { | ||
props: ReportModalContextProps; | ||
closeModal: () => void; | ||
} | ||
|
||
export interface ReportModalContextProviderProps { | ||
initial: ReportModalContextProps; | ||
closeModal: () => void; | ||
} | ||
|
||
export const ReportModalContextProvider: React.FC< | ||
PropsWithChildren<ReportModalContextProviderProps> | ||
> = ({ children, initial, closeModal }) => { | ||
return ( | ||
<ReportModalContext.Provider value={{ props: initial, closeModal }}> | ||
{children} | ||
</ReportModalContext.Provider> | ||
); | ||
}; | ||
export const ReportModalContext = React.createContext< | ||
IReportModalContext | undefined | ||
>(undefined); | ||
|
||
export const useReportModalContext = (): IReportModalContext => { | ||
return useContext(ReportModalContext)!; | ||
}; |
Oops, something went wrong.