diff --git a/web/src/app/mapfixes/[mapfixId]/_reviewButtons.tsx b/web/src/app/mapfixes/[mapfixId]/_reviewButtons.tsx index 24d75c4..55480b6 100644 --- a/web/src/app/mapfixes/[mapfixId]/_reviewButtons.tsx +++ b/web/src/app/mapfixes/[mapfixId]/_reviewButtons.tsx @@ -1,8 +1,11 @@ +import { Roles, RolesConstants } from "@/app/ts/Roles"; +import { MapfixStatus } from "@/app/ts/Mapfix"; import { Button, ButtonOwnProps } from "@mui/material"; +import { useState, useEffect } from "react"; type Actions = "Completed" | "Submit" | "Reject" | "Revoke" -type ApiActions = Lowercase<Actions> | "trigger-validate" | "retry-validate" | "trigger-upload" | "reset-uploading" | "reset-validating" -type Review = Actions | "Accept" | "Validate" | "Upload" | "Reset Uploading (fix softlocked status)" | "Reset Validating (fix softlocked status)" | "Request Changes" +type ApiActions = Lowercase<Actions> | "request-changes" | "trigger-validate" | "retry-validate" | "trigger-upload" | "reset-uploading" | "reset-validating" +type Review = Actions | "Request Changes" | "Accept" | "Validate" | "Upload" | "Reset Uploading (fix softlocked status)" | "Reset Validating (fix softlocked status)" | "Request Changes" interface ReviewButton { name: Review, @@ -12,7 +15,9 @@ interface ReviewButton { } interface ReviewId { - mapfixId: string + mapfixId: string, + mapfixStatus: number, + mapfixSubmitter: number, } async function ReviewButtonClicked(action: ApiActions, mapfixId: string) { @@ -45,7 +50,6 @@ function ReviewButton(props: ReviewButton) { } export default function ReviewButtons(props: ReviewId) { - const mapfixId = props.mapfixId // When is each button visible? // Multiple buttons can be visible at once. // Action | Role | When Current Status is One of: @@ -59,16 +63,86 @@ export default function ReviewButtons(props: ReviewId) { // RequestChanges | Reviewer | Validated, Accepted, Submitted // Upload | MapAdmin | Validated // ResetUploading | MapAdmin | Uploading + const { mapfixId, mapfixStatus } = props; + const [user, setUser] = useState<number|null>(null); + const [roles, setRoles] = useState<Roles>(RolesConstants.Empty); + const [loading, setLoading] = useState(true); + + useEffect(() => { + async function fetchData() { + try { + const [rolesData, userData] = await Promise.all([ + fetch("/api/session/roles").then(rolesResponse => rolesResponse.json()), + fetch("/api/session/user").then(userResponse => userResponse.json()) + ]); + + setRoles(rolesData.Roles); + setUser(userData.UserID); + } catch (error) { + console.error("Error fetching data:", error); + } finally { + setLoading(false); + } + } + + fetchData(); + }, [mapfixId]); + + if (loading) return <p>Loading...</p>; + + const visibleButtons: ReviewButton[] = []; + + const is_submitter = user === props.mapfixSubmitter; + if (is_submitter) { + if ([MapfixStatus.UnderConstruction, MapfixStatus.ChangesRequested].includes(mapfixStatus!)) { + visibleButtons.push({ name: "Submit", action: "submit", color: "info", mapfixId }); + } + if ([MapfixStatus.Submitted, MapfixStatus.ChangesRequested].includes(mapfixStatus!)) { + visibleButtons.push({ name: "Revoke", action: "revoke", color: "info", mapfixId }); + } + } + + if (roles&RolesConstants.MapfixReview) { + // you can't review your own mapfix! + // note that this means there needs to be more than one person with MapfixReview + if (!is_submitter && mapfixStatus === MapfixStatus.Submitted) { + visibleButtons.push({ name: "Accept", action: "trigger-validate", color: "info", mapfixId }); + visibleButtons.push({ name: "Reject", action: "reject", color: "error", mapfixId }); + } + if (mapfixStatus === MapfixStatus.Accepted) { + visibleButtons.push({ name: "Validate", action: "retry-validate", color: "info", mapfixId }); + } + if (mapfixStatus === MapfixStatus.Validating) { + visibleButtons.push({ name: "Reset Validating (fix softlocked status)", action: "reset-validating", color: "error", mapfixId }); + } + // this button serves the same purpose as Revoke if you are both + // the map submitter and have MapfixReview when status is Submitted + if ( + [MapfixStatus.Validated, MapfixStatus.Accepted].includes(mapfixStatus!) + || !is_submitter && mapfixStatus == MapfixStatus.Submitted + ) { + visibleButtons.push({ name: "Request Changes", action: "request-changes", color: "error", mapfixId }); + } + } + + if (roles&RolesConstants.MapfixUpload) { + if (mapfixStatus === MapfixStatus.Validated) { + visibleButtons.push({ name: "Upload", action: "trigger-upload", color: "info", mapfixId }); + } + if (mapfixStatus === MapfixStatus.Uploading) { + visibleButtons.push({ name: "Reset Uploading (fix softlocked status)", action: "reset-uploading", color: "error", mapfixId }); + } + } + return ( <section className="review-set"> - <ReviewButton color="info" name="Submit" action="submit" mapfixId={mapfixId}/> - <ReviewButton color="info" name="Revoke" action="revoke" mapfixId={mapfixId}/> - <ReviewButton color="info" name="Accept" action="trigger-validate" mapfixId={mapfixId}/> - <ReviewButton color="info" name="Validate" action="retry-validate" mapfixId={mapfixId}/> - <ReviewButton color="error" name="Reject" action="reject" mapfixId={mapfixId}/> - <ReviewButton color="info" name="Upload" action="trigger-upload" mapfixId={mapfixId}/> - <ReviewButton color="error" name="Reset Uploading (fix softlocked status)" action="reset-uploading" mapfixId={mapfixId}/> - <ReviewButton color="error" name="Reset Validating (fix softlocked status)" action="reset-validating" mapfixId={mapfixId}/> + {visibleButtons.length === 0 ? ( + <p>No available actions</p> + ) : ( + visibleButtons.map((btn) => ( + <ReviewButton key={btn.action} {...btn} /> + )) + )} </section> - ) + ); } diff --git a/web/src/app/mapfixes/[mapfixId]/page.tsx b/web/src/app/mapfixes/[mapfixId]/page.tsx index cc38cb3..43d3f2e 100644 --- a/web/src/app/mapfixes/[mapfixId]/page.tsx +++ b/web/src/app/mapfixes/[mapfixId]/page.tsx @@ -15,7 +15,9 @@ import { useState, useEffect } from "react"; import "./(styles)/page.scss"; interface ReviewId { - mapfixId: string + mapfixId: string, + mapfixStatus: number; + mapfixSubmitter: number, } function Ratings() { @@ -46,7 +48,7 @@ function RatingArea(mapfix: ReviewId) { <MapImage/> </section> <Ratings/> - <ReviewButtons mapfixId={mapfix.mapfixId}/> + <ReviewButtons mapfixId={mapfix.mapfixId} mapfixStatus={mapfix.mapfixStatus} mapfixSubmitter={mapfix.mapfixSubmitter}/> </aside> ) } @@ -96,7 +98,7 @@ export default function MapfixInfoPage() { <Webpage> <main className="map-page-main"> <section className="review-section"> - <RatingArea mapfixId={dynamicId.mapfixId}/> + <RatingArea mapfixId={dynamicId.mapfixId} mapfixStatus={mapfix.StatusID} mapfixSubmitter={mapfix.Submitter}/> <TitleAndComments name={mapfix.DisplayName} creator={mapfix.Creator} review={mapfix.StatusID} status_message={mapfix.StatusMessage} asset_id={mapfix.AssetID} comments={[]}/> </section> </main> diff --git a/web/src/app/submissions/[submissionId]/_reviewButtons.tsx b/web/src/app/submissions/[submissionId]/_reviewButtons.tsx index 822327c..069cfd6 100644 --- a/web/src/app/submissions/[submissionId]/_reviewButtons.tsx +++ b/web/src/app/submissions/[submissionId]/_reviewButtons.tsx @@ -1,8 +1,11 @@ +import { Roles, RolesConstants } from "@/app/ts/Roles"; +import { SubmissionStatus } from "@/app/ts/Submission"; import { Button, ButtonOwnProps } from "@mui/material"; +import { useState, useEffect } from "react"; type Actions = "Completed" | "Submit" | "Reject" | "Revoke" -type ApiActions = Lowercase<Actions> | "trigger-validate" | "retry-validate" | "trigger-upload" | "reset-uploading" | "reset-validating" -type Review = Actions | "Accept" | "Validate" | "Upload" | "Reset Uploading (fix softlocked status)" | "Reset Validating (fix softlocked status)" | "Request Changes" +type ApiActions = Lowercase<Actions> | "request-changes" | "trigger-validate" | "retry-validate" | "trigger-upload" | "reset-uploading" | "reset-validating" +type Review = Actions | "Request Changes" | "Accept" | "Validate" | "Upload" | "Reset Uploading (fix softlocked status)" | "Reset Validating (fix softlocked status)" | "Request Changes" interface ReviewButton { name: Review, @@ -12,7 +15,9 @@ interface ReviewButton { } interface ReviewId { - submissionId: string + submissionId: string, + submissionStatus: number, + submissionSubmitter: number, } async function ReviewButtonClicked(action: ApiActions, submissionId: string) { @@ -45,7 +50,6 @@ function ReviewButton(props: ReviewButton) { } export default function ReviewButtons(props: ReviewId) { - const submissionId = props.submissionId // When is each button visible? // Multiple buttons can be visible at once. // Action | Role | When Current Status is One of: @@ -59,16 +63,86 @@ export default function ReviewButtons(props: ReviewId) { // RequestChanges | Reviewer | Validated, Accepted, Submitted // Upload | MapAdmin | Validated // ResetUploading | MapAdmin | Uploading + const { submissionId, submissionStatus } = props; + const [user, setUser] = useState<number|null>(null); + const [roles, setRoles] = useState<Roles>(RolesConstants.Empty); + const [loading, setLoading] = useState(true); + + useEffect(() => { + async function fetchData() { + try { + const [rolesData, userData] = await Promise.all([ + fetch("/api/session/roles").then(rolesResponse => rolesResponse.json()), + fetch("/api/session/user").then(userResponse => userResponse.json()) + ]); + + setRoles(rolesData.Roles); + setUser(userData.UserID); + } catch (error) { + console.error("Error fetching data:", error); + } finally { + setLoading(false); + } + } + + fetchData(); + }, [submissionId]); + + if (loading) return <p>Loading...</p>; + + const visibleButtons: ReviewButton[] = []; + + const is_submitter = user === props.submissionSubmitter; + if (is_submitter) { + if ([SubmissionStatus.UnderConstruction, SubmissionStatus.ChangesRequested].includes(submissionStatus!)) { + visibleButtons.push({ name: "Submit", action: "submit", color: "info", submissionId }); + } + if ([SubmissionStatus.Submitted, SubmissionStatus.ChangesRequested].includes(submissionStatus!)) { + visibleButtons.push({ name: "Revoke", action: "revoke", color: "info", submissionId }); + } + } + + if (roles&RolesConstants.SubmissionReview) { + // you can't review your own submission! + // note that this means there needs to be more than one person with SubmissionReview + if (!is_submitter && submissionStatus === SubmissionStatus.Submitted) { + visibleButtons.push({ name: "Accept", action: "trigger-validate", color: "info", submissionId }); + visibleButtons.push({ name: "Reject", action: "reject", color: "error", submissionId }); + } + if (submissionStatus === SubmissionStatus.Accepted) { + visibleButtons.push({ name: "Validate", action: "retry-validate", color: "info", submissionId }); + } + if (submissionStatus === SubmissionStatus.Validating) { + visibleButtons.push({ name: "Reset Validating (fix softlocked status)", action: "reset-validating", color: "error", submissionId }); + } + // this button serves the same purpose as Revoke if you are both + // the map submitter and have SubmissionReview when status is Submitted + if ( + [SubmissionStatus.Validated, SubmissionStatus.Accepted].includes(submissionStatus!) + || !is_submitter && submissionStatus == SubmissionStatus.Submitted + ) { + visibleButtons.push({ name: "Request Changes", action: "request-changes", color: "error", submissionId }); + } + } + + if (roles&RolesConstants.SubmissionUpload) { + if (submissionStatus === SubmissionStatus.Validated) { + visibleButtons.push({ name: "Upload", action: "trigger-upload", color: "info", submissionId }); + } + if (submissionStatus === SubmissionStatus.Uploading) { + visibleButtons.push({ name: "Reset Uploading (fix softlocked status)", action: "reset-uploading", color: "error", submissionId }); + } + } + return ( <section className="review-set"> - <ReviewButton color="info" name="Submit" action="submit" submissionId={submissionId}/> - <ReviewButton color="info" name="Revoke" action="revoke" submissionId={submissionId}/> - <ReviewButton color="info" name="Accept" action="trigger-validate" submissionId={submissionId}/> - <ReviewButton color="info" name="Validate" action="retry-validate" submissionId={submissionId}/> - <ReviewButton color="error" name="Reject" action="reject" submissionId={submissionId}/> - <ReviewButton color="info" name="Upload" action="trigger-upload" submissionId={submissionId}/> - <ReviewButton color="error" name="Reset Uploading (fix softlocked status)" action="reset-uploading" submissionId={submissionId}/> - <ReviewButton color="error" name="Reset Validating (fix softlocked status)" action="reset-validating" submissionId={submissionId}/> + {visibleButtons.length === 0 ? ( + <p>No available actions</p> + ) : ( + visibleButtons.map((btn) => ( + <ReviewButton key={btn.action} {...btn} /> + )) + )} </section> - ) + ); } diff --git a/web/src/app/submissions/[submissionId]/page.tsx b/web/src/app/submissions/[submissionId]/page.tsx index c78ba8b..9b8d4d1 100644 --- a/web/src/app/submissions/[submissionId]/page.tsx +++ b/web/src/app/submissions/[submissionId]/page.tsx @@ -17,6 +17,8 @@ import "./(styles)/page.scss"; interface ReviewId { submissionId: string; assetId: number; + submissionStatus: number; + submissionSubmitter: number, } function Ratings() { @@ -47,7 +49,7 @@ function RatingArea(submission: ReviewId) { <MapImage id={submission.assetId}/> </section> <Ratings/> - <ReviewButtons submissionId={submission.submissionId}/> + <ReviewButtons submissionId={submission.submissionId} submissionStatus={submission.submissionStatus} submissionSubmitter={submission.submissionSubmitter}/> </aside> ) } @@ -97,7 +99,7 @@ export default function SubmissionInfoPage() { <Webpage> <main className="map-page-main"> <section className="review-section"> - <RatingArea assetId={submission.AssetID} submissionId={dynamicId.submissionId}/> + <RatingArea assetId={submission.AssetID} submissionId={dynamicId.submissionId} submissionStatus={submission.StatusID} submissionSubmitter={submission.Submitter}/> <TitleAndComments name={submission.DisplayName} creator={submission.Creator} review={submission.StatusID} status_message={submission.StatusMessage} asset_id={submission.AssetID} comments={[]}/> </section> </main> diff --git a/web/src/app/ts/Mapfix.ts b/web/src/app/ts/Mapfix.ts index 0a5bd25..0570db0 100644 --- a/web/src/app/ts/Mapfix.ts +++ b/web/src/app/ts/Mapfix.ts @@ -8,6 +8,7 @@ const enum MapfixStatus { Uploading = 6, Uploaded = 7, Rejected = 8, + // MapfixStatus does not have a Released state } interface MapfixInfo { diff --git a/web/src/app/ts/Roles.ts b/web/src/app/ts/Roles.ts new file mode 100644 index 0000000..57a7d9d --- /dev/null +++ b/web/src/app/ts/Roles.ts @@ -0,0 +1,25 @@ +type Roles = number; + +// Constants +const RolesConstants = { + All: -1 as Roles, + SubmissionUpload: 1 << 6 as Roles, + SubmissionReview: 1 << 5 as Roles, + SubmissionRelease: 1 << 4 as Roles, + ScriptWrite: 1 << 3 as Roles, + MapfixUpload: 1 << 2 as Roles, + MapfixReview: 1 << 1 as Roles, + MapDownload: 1 << 0 as Roles, + Empty: 0 as Roles, +}; + +// Operations +function hasRole(flags: Roles, role: Roles): boolean { + return (flags & role) === role; +} + +export { + type Roles, + RolesConstants, + hasRole, +};