diff --git a/apps/cyberstorm-remix/app/settings/teams/team/tabs/Members/MemberAddForm.tsx b/apps/cyberstorm-remix/app/settings/teams/team/tabs/Members/MemberAddForm.tsx new file mode 100644 index 000000000..aa4cd3383 --- /dev/null +++ b/apps/cyberstorm-remix/app/settings/teams/team/tabs/Members/MemberAddForm.tsx @@ -0,0 +1,162 @@ +import { faPlus } from "@fortawesome/pro-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useState, useReducer } from "react"; + +import { + useToast, + Modal, + NewButton, + NewIcon, + NewTextInput, + NewSelect, + type SelectOption, +} from "@thunderstore/cyberstorm"; +import { + teamAddMember, + type RequestConfig, + type TeamAddMemberRequestData, +} from "@thunderstore/thunderstore-api"; + +import { useStrongForm } from "cyberstorm/utils/StrongForm/useStrongForm"; + +const roleOptions: SelectOption<"owner" | "member">[] = [ + { value: "member", label: "Member" }, + { value: "owner", label: "Owner" }, +]; + +export function MemberAddForm(props: { + teamName: string; + updateTrigger: () => Promise; + config: () => RequestConfig; +}) { + const toast = useToast(); + const [open, setOpen] = useState(false); + + function formFieldUpdateAction( + state: TeamAddMemberRequestData, + action: { + field: keyof TeamAddMemberRequestData; + value: TeamAddMemberRequestData[keyof TeamAddMemberRequestData]; + } + ) { + return { + ...state, + [action.field]: action.value, + }; + } + + const [formInputs, updateFormFieldState] = useReducer(formFieldUpdateAction, { + username: "", + role: "member", + }); + + type SubmitorOutput = Awaited>; + + async function submitor(data: typeof formInputs): Promise { + return await teamAddMember({ + config: props.config, + params: { team_name: props.teamName }, + queryParams: {}, + data: { username: data.username, role: data.role }, + }); + } + + type InputErrors = { + [key in keyof typeof formInputs]?: string | string[]; + }; + + const strongForm = useStrongForm< + typeof formInputs, + TeamAddMemberRequestData, + Error, + SubmitorOutput, + Error, + InputErrors + >({ + inputs: formInputs, + submitor, + onSubmitSuccess: () => { + props.updateTrigger(); + toast.addToast({ + csVariant: "success", + children: `Team member added`, + duration: 4000, + }); + setOpen(false); + }, + onSubmitError: (error) => { + toast.addToast({ + csVariant: "danger", + children: `Error occurred: ${error.message || "Unknown error"}`, + duration: 8000, + }); + }, + }); + + return ( + + Add Member + + + + + } + > + +
+ Enter the username of the user you wish to add to the team{" "} + {props.teamName}. +
+
+
+ + { + updateFormFieldState({ + field: "username", + value: e.target.value, + }); + }} + rootClasses="add-member-form__username-input" + id="username" + /> +
+
+ + { + updateFormFieldState({ field: "role", value: value }); + }} + id="role" + /> +
+
+
+ + + Add member + + +
+ ); +} + +MemberAddForm.displayName = "MemberAddForm"; diff --git a/apps/cyberstorm-remix/app/settings/teams/team/tabs/Members/Members.css b/apps/cyberstorm-remix/app/settings/teams/team/tabs/Members/Members.css index 19a50be02..c1547d56a 100644 --- a/apps/cyberstorm-remix/app/settings/teams/team/tabs/Members/Members.css +++ b/apps/cyberstorm-remix/app/settings/teams/team/tabs/Members/Members.css @@ -92,4 +92,8 @@ font-weight: var(--font-weight-bold); line-height: var(--line-height-bold); } + + .members_table__span { + font-size: var(--font-size-body-md); + } } diff --git a/apps/cyberstorm-remix/app/settings/teams/team/tabs/Members/Members.tsx b/apps/cyberstorm-remix/app/settings/teams/team/tabs/Members/Members.tsx index 32f52ad6c..b35d9f478 100644 --- a/apps/cyberstorm-remix/app/settings/teams/team/tabs/Members/Members.tsx +++ b/apps/cyberstorm-remix/app/settings/teams/team/tabs/Members/Members.tsx @@ -1,390 +1,59 @@ -import { faPlus, faTrashCan } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { useReducer, useState } from "react"; -import { useLoaderData, useOutletContext, useRevalidator } from "react-router"; - -import { - Modal, - NewAvatar, - NewButton, - NewIcon, - NewLink, - NewSelect, - NewTable, - NewTextInput, - type SelectOption, - useToast, -} from "@thunderstore/cyberstorm"; -import { TableSort } from "@thunderstore/cyberstorm/src/newComponents/Table/Table"; +import { Suspense } from "react"; import { - type RequestConfig, - teamAddMember, - type TeamAddMemberRequestData, - teamEditMember, - teamRemoveMember, -} from "@thunderstore/thunderstore-api"; -import { ApiAction } from "@thunderstore/ts-api-react-actions"; + Await, + useLoaderData, + useOutletContext, + useRevalidator, +} from "react-router"; import { type OutletContextShape } from "app/root"; import { makeTeamSettingsTabLoader } from "cyberstorm/utils/dapperClientLoaders"; -import { useStrongForm } from "cyberstorm/utils/StrongForm/useStrongForm"; +import { MemberAddForm } from "./MemberAddForm"; +import { MembersTable } from "./MembersTable"; import "./Members.css"; export const clientLoader = makeTeamSettingsTabLoader( async (dapper, teamName) => ({ - members: await dapper.getTeamMembers(teamName), + members: dapper.getTeamMembers(teamName), }) ); -export function HydrateFallback() { - return
Loading...
; -} - -const teamMemberColumns = [ - { value: "User", disableSort: false }, - { value: "Role", disableSort: false }, - { value: "Actions", disableSort: true }, -]; - -const roleOptions: SelectOption<"owner" | "member">[] = [ - { value: "member", label: "Member" }, - { value: "owner", label: "Owner" }, -]; - export default function Members() { const { teamName, members } = useLoaderData(); const outletContext = useOutletContext() as OutletContextShape; - const revalidator = useRevalidator(); async function teamMemberRevalidate() { revalidator.revalidate(); } - const currentUser = outletContext.currentUser; - const currentUserTeam = currentUser?.teams_full?.find( - (team) => team.name === teamName - ); - const isOwner = currentUserTeam?.role === "owner"; - - const toast = useToast(); - - const changeMemberRoleAction = ApiAction({ - endpoint: teamEditMember, - onSubmitSuccess: () => { - toast.addToast({ - csVariant: "success", - children: `Member role updated`, - duration: 4000, - }); - teamMemberRevalidate(); - }, - onSubmitError: (error) => { - toast.addToast({ - csVariant: "danger", - children: `Error occurred: ${error.message || "Unknown error"}`, - duration: 8000, - }); - }, - }); - - function changeMemberRole(username: string, value: "owner" | "member") { - changeMemberRoleAction({ - params: { teamIdentifier: teamName, username }, - data: { role: value }, - queryParams: {}, - config: outletContext.requestConfig, - }); - } - - const tableData = members.map((member, index) => { - return [ - { - value: ( - - - {member.username} - - ), - sortValue: member.username, - }, - { - value: ( -
- - changeMemberRole(member.username, val) - } - disabled={!isOwner || currentUser?.username === member.username} - /> -
- ), - sortValue: member.role, - }, - { - value: - !isOwner || currentUser?.username === member.username ? null : ( - - ), - sortValue: 0, - }, - ]; - }); - return ( -
-
-
-

Teams

-

Manage your teams

- -
-
- -
-
-
- ); -} - -function AddTeamMemberForm(props: { - teamName: string; - updateTrigger: () => Promise; - config: () => RequestConfig; -}) { - const toast = useToast(); - const [open, setOpen] = useState(false); - - function formFieldUpdateAction( - state: TeamAddMemberRequestData, - action: { - field: keyof TeamAddMemberRequestData; - value: TeamAddMemberRequestData[keyof TeamAddMemberRequestData]; - } - ) { - return { - ...state, - [action.field]: action.value, - }; - } - - const [formInputs, updateFormFieldState] = useReducer(formFieldUpdateAction, { - username: "", - role: "member", - }); - - type SubmitorOutput = Awaited>; - - async function submitor(data: typeof formInputs): Promise { - return await teamAddMember({ - config: props.config, - params: { team_name: props.teamName }, - queryParams: {}, - data: { username: data.username, role: data.role }, - }); - } - - type InputErrors = { - [key in keyof typeof formInputs]?: string | string[]; - }; - - const strongForm = useStrongForm< - typeof formInputs, - TeamAddMemberRequestData, - Error, - SubmitorOutput, - Error, - InputErrors - >({ - inputs: formInputs, - submitor, - onSubmitSuccess: () => { - props.updateTrigger(); - toast.addToast({ - csVariant: "success", - children: `Team member added`, - duration: 4000, - }); - setOpen(false); - }, - onSubmitError: (error) => { - toast.addToast({ - csVariant: "danger", - children: `Error occurred: ${error.message || "Unknown error"}`, - duration: 8000, - }); - }, - }); - - return ( - - Add Member - - - - - } - > - -
- Enter the username of the user you wish to add to the team{" "} - {props.teamName}. -
-
-
- - { - updateFormFieldState({ - field: "username", - value: e.target.value, - }); - }} - rootClasses="add-member-form__username-input" - id="username" - /> + Loading...
}> + + {(resolvedMembers) => ( +
+
+
+

Teams

+

Manage your teams

+ +
+
+ +
+
-
- - { - updateFormFieldState({ field: "role", value: value }); - }} - id="role" - /> -
-
-
- - - Add member - - -
+ )} + + ); } - -AddTeamMemberForm.displayName = "AddTeamMemberForm"; - -function RemoveTeamMemberForm(props: { - indexKey?: string; - userName: string; - teamName: string; - updateTrigger: () => Promise; - config: () => RequestConfig; -}) { - const toast = useToast(); - const [open, setOpen] = useState(false); - - const kickMemberAction = ApiAction({ - endpoint: teamRemoveMember, - onSubmitSuccess: () => { - props.updateTrigger(); - toast.addToast({ - csVariant: "success", - children: `Team member removed`, - duration: 4000, - }); - }, - onSubmitError: (error) => { - toast.addToast({ - csVariant: "danger", - children: `Error occurred: ${error.message || "Unknown error"}`, - duration: 8000, - }); - }, - }); - - return ( - - - - - Kick - - } - > - -
- You are about to kick member{" "} - - {props.userName} - - . -
-
- - - Cancel - - - kickMemberAction({ - config: props.config, - params: { team_name: props.teamName, username: props.userName }, - queryParams: {}, - data: {}, - }).then(() => { - setOpen(false); - }) - } - > - Kick member - - -
- ); -} - -RemoveTeamMemberForm.displayName = "RemoveTeamMemberForm"; diff --git a/apps/cyberstorm-remix/app/settings/teams/team/tabs/Members/MembersTable.tsx b/apps/cyberstorm-remix/app/settings/teams/team/tabs/Members/MembersTable.tsx new file mode 100644 index 000000000..bee87f952 --- /dev/null +++ b/apps/cyberstorm-remix/app/settings/teams/team/tabs/Members/MembersTable.tsx @@ -0,0 +1,241 @@ +import { faTrashCan } from "@fortawesome/pro-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useState } from "react"; +import { useOutletContext } from "react-router"; + +import { + Modal, + NewAvatar, + NewButton, + NewIcon, + NewLink, + NewSelect, + NewTable, + useToast, + type SelectOption, +} from "@thunderstore/cyberstorm"; +import { TableSort } from "@thunderstore/cyberstorm/src/newComponents/Table/Table"; +import { + teamEditMember, + teamRemoveMember, + type RequestConfig, + type TeamMember, +} from "@thunderstore/thunderstore-api"; +import { ApiAction } from "@thunderstore/ts-api-react-actions"; + +import { type OutletContextShape } from "app/root"; + +const teamMemberColumns = [ + { value: "User", disableSort: false }, + { value: "Role", disableSort: false }, + { value: "Actions", disableSort: true }, +]; + +const roleOptions: SelectOption<"owner" | "member">[] = [ + { value: "member", label: "Member" }, + { value: "owner", label: "Owner" }, +]; + +export function MembersTable(props: { + config: () => RequestConfig; + members: TeamMember[]; + teamName: string; + updateTrigger: () => Promise; +}) { + const { config, members, teamName, updateTrigger } = props; + const outletContext = useOutletContext() as OutletContextShape; + const toast = useToast(); + + const currentUser = outletContext.currentUser; + const membership = currentUser?.teams_full.find((t) => t.name === teamName); + const isOwner = membership?.role === "owner"; + + const canManageMember = (username: string) => + isOwner && currentUser?.username !== username; + + const changeMemberRoleAction = ApiAction({ + endpoint: teamEditMember, + onSubmitSuccess: () => { + toast.addToast({ + csVariant: "success", + children: `Member role updated`, + duration: 4000, + }); + updateTrigger(); + }, + onSubmitError: (error) => { + toast.addToast({ + csVariant: "danger", + children: `Error occurred: ${error.message || "Unknown error"}`, + duration: 8000, + }); + }, + }); + + function changeMemberRole(username: string, value: "owner" | "member") { + changeMemberRoleAction({ + params: { teamIdentifier: teamName, username }, + data: { role: value }, + queryParams: {}, + config, + }); + } + + const tableData = members.map((member, index) => { + return [ + { + value: ( + + + {member.username} + + ), + sortValue: member.username, + }, + { + value: ( +
+ + changeMemberRole(member.username, val) + } + disabled={!canManageMember(member.username)} + /> +
+ ), + sortValue: member.role, + }, + { + value: canManageMember(member.username) ? ( + + ) : currentUser?.username === member.username ? ( + + Use the{" "} + + Settings tab + {" "} + to leave the team. + + ) : null, + sortValue: 0, + }, + ]; + }); + + return ( + + ); +} + +MembersTable.displayName = "MembersTable"; + +function RemoveTeamMemberForm(props: { + indexKey?: string; + userName: string; + teamName: string; + updateTrigger: () => Promise; + config: () => RequestConfig; +}) { + const toast = useToast(); + const [open, setOpen] = useState(false); + + const kickMemberAction = ApiAction({ + endpoint: teamRemoveMember, + onSubmitSuccess: () => { + props.updateTrigger(); + toast.addToast({ + csVariant: "success", + children: `Team member removed`, + duration: 4000, + }); + }, + onSubmitError: (error) => { + toast.addToast({ + csVariant: "danger", + children: `Error occurred: ${error.message || "Unknown error"}`, + duration: 8000, + }); + }, + }); + + return ( + + + + + Kick + + } + > + +
+ You are about to kick member{" "} + + {props.userName} + + . +
+
+ + + Cancel + + + kickMemberAction({ + config: props.config, + params: { team_name: props.teamName, username: props.userName }, + queryParams: {}, + data: {}, + }).then(() => { + setOpen(false); + }) + } + > + Kick member + + +
+ ); +} + +RemoveTeamMemberForm.displayName = "RemoveTeamMemberForm"; 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 ef26cd41e..d76552e27 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 @@ -1,9 +1,15 @@ -import { useReducer } from "react"; -import { useLoaderData, useOutletContext, useRevalidator } from "react-router"; +import { Suspense, useReducer } from "react"; +import { + Await, + useLoaderData, + useOutletContext, + useRevalidator, +} from "react-router"; import { NewButton, NewTextInput, useToast } from "@thunderstore/cyberstorm"; import { teamDetailsEdit, + type TeamDetails, type TeamDetailsEditRequestData, } from "@thunderstore/thunderstore-api"; @@ -17,20 +23,30 @@ export const clientLoader = makeTeamSettingsTabLoader( // TODO: for hygienie we shouldn't use this public endpoint but // have an endpoint that confirms user permissions and returns // possibly sensitive information. - team: await dapper.getTeamDetails(teamName), + team: dapper.getTeamDetails(teamName), }) ); -export function HydrateFallback() { - return
Loading...
; -} - export default function Profile() { const { team } = useLoaderData(); - const outletContext = useOutletContext() as OutletContextShape; - const revalidator = useRevalidator(); + return ( + Loading...}> + + {(resolvedTeam) => ( +
+ +
+ )} +
+
+ ); +} +function ProfileForm(props: { team: TeamDetails }) { + const { team } = props; + const outletContext = useOutletContext() as OutletContextShape; + const revalidator = useRevalidator(); const toast = useToast(); function formFieldUpdateAction( @@ -93,37 +109,34 @@ export default function Profile() { }); return ( -
-
-
-

Donation Link

-
-
-
-
- URL - - updateFormFieldState({ - field: "donation_link", - value: e.target.value, - }) - } - rootClasses="team-profile__input" - /> -
+
+
+

Donation Link

+
+
+
+
+ URL + + updateFormFieldState({ + field: "donation_link", + value: e.target.value, + }) + } + rootClasses="team-profile__input" + />
- - Save changes -
+ + Save changes +
); } + +ProfileForm.displayName = "ProfileForm"; diff --git a/apps/cyberstorm-remix/app/settings/teams/team/tabs/ServiceAccounts/ServiceAccounts.tsx b/apps/cyberstorm-remix/app/settings/teams/team/tabs/ServiceAccounts/ServiceAccounts.tsx index 81c822ab4..ad1fca880 100644 --- a/apps/cyberstorm-remix/app/settings/teams/team/tabs/ServiceAccounts/ServiceAccounts.tsx +++ b/apps/cyberstorm-remix/app/settings/teams/team/tabs/ServiceAccounts/ServiceAccounts.tsx @@ -1,21 +1,22 @@ import { faPlus } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { useReducer, useState } from "react"; -import { useLoaderData, useOutletContext, useRevalidator } from "react-router"; +import { Suspense, useReducer, useState } from "react"; +import { + Await, + useLoaderData, + useOutletContext, + useRevalidator, +} from "react-router"; import { NewAlert, NewButton, Modal, - NewTable, NewIcon, NewTextInput, - Heading, CodeBox, } from "@thunderstore/cyberstorm"; -import { TableSort } from "@thunderstore/cyberstorm/src/newComponents/Table/Table"; import { - type RequestConfig, teamAddServiceAccount, type TeamServiceAccountAddRequestData, } from "@thunderstore/thunderstore-api"; @@ -23,106 +24,59 @@ import { import { type OutletContextShape } from "app/root"; import { makeTeamSettingsTabLoader } from "cyberstorm/utils/dapperClientLoaders"; import { useStrongForm } from "cyberstorm/utils/StrongForm/useStrongForm"; -import { ServiceAccountRemoveModal } from "./ServiceAccountRemoveModal"; +import { ServiceAccountsTable } from "./ServiceAccountsTable"; import "./ServiceAccounts.css"; export const clientLoader = makeTeamSettingsTabLoader( async (dapper, teamName) => ({ - serviceAccounts: await dapper.getTeamServiceAccounts(teamName), + serviceAccounts: dapper.getTeamServiceAccounts(teamName), }) ); -export function HydrateFallback() { - return
Loading...
; -} - -const serviceAccountColumns = [ - { value: "Nickname", disableSort: false }, - { value: "Last Used", disableSort: false }, - { value: "Actions", disableSort: true }, -]; - export default function ServiceAccounts() { const { teamName, serviceAccounts } = useLoaderData(); - const outletContext = useOutletContext() as OutletContextShape; - const revalidator = useRevalidator(); - const currentUserTeam = outletContext.currentUser?.teams_full?.find( - (team) => team.name === teamName - ); - const isOwner = currentUserTeam?.role === "owner"; - async function serviceAccountRevalidate() { revalidator.revalidate(); } - const tableData = serviceAccounts.map((serviceAccount) => { - return [ - { - value: ( -

- {serviceAccount.name} -

- ), - sortValue: serviceAccount.name, - }, - { - value: ( -

- {serviceAccount.last_used ?? "Never"} -

- ), - sortValue: serviceAccount.last_used ?? "0", - }, - { - value: ( - - ), - sortValue: 0, - }, - ]; - }); - return ( -
-
-
-

Service accounts

-

Your loyal servants

- {isOwner && ( - - )} -
-
- Service Accounts} - headers={serviceAccountColumns} - rows={tableData} - sortByHeader={1} - sortDirection={TableSort.ASC} - /> -
-
-
+ Loading...
}> + + {(resolvedServiceAccounts) => ( +
+
+
+

Service accounts

+

+ Your loyal servants +

+ +
+
+ +
+
+
+ )} +
+ ); } function AddServiceAccountForm(props: { teamName: string; - config: () => RequestConfig; - serviceAccountRevalidate?: () => void; + serviceAccountRevalidate: () => Promise; }) { + const outletContext = useOutletContext() as OutletContextShape; const [open, setOpen] = useState(false); const [serviceAccountAdded, setServiceAccountAdded] = useState(false); const [addedServiceAccountToken, setAddedServiceAccountToken] = useState(""); @@ -130,6 +84,14 @@ function AddServiceAccountForm(props: { useState(""); const [error, setError] = useState(null); + const currentUserTeam = outletContext.currentUser?.teams_full?.find( + (team) => team.name === props.teamName + ); + + if (currentUserTeam?.role !== "owner") { + return null; + } + function onSuccess( result: Awaited> ) { @@ -161,7 +123,7 @@ function AddServiceAccountForm(props: { async function submitor(data: typeof formInputs): Promise { return await teamAddServiceAccount({ - config: props.config, + config: outletContext.requestConfig, params: { team_name: props.teamName }, queryParams: {}, data: { nickname: data.nickname.trim() }, @@ -188,7 +150,7 @@ function AddServiceAccountForm(props: { // Refresh the service accounts list to show the newly created account // TODO: When API returns identifier in response, we can append the new // service account to the list instead of refreshing from backend - props.serviceAccountRevalidate?.(); + props.serviceAccountRevalidate(); }, onSubmitError: (error) => { const message = `Error occurred: ${error.message || "Unknown error"}`; diff --git a/apps/cyberstorm-remix/app/settings/teams/team/tabs/ServiceAccounts/ServiceAccountsTable.tsx b/apps/cyberstorm-remix/app/settings/teams/team/tabs/ServiceAccounts/ServiceAccountsTable.tsx new file mode 100644 index 000000000..61a9481df --- /dev/null +++ b/apps/cyberstorm-remix/app/settings/teams/team/tabs/ServiceAccounts/ServiceAccountsTable.tsx @@ -0,0 +1,69 @@ +import { useOutletContext } from "react-router"; + +import { NewTable, Heading } from "@thunderstore/cyberstorm"; +import { TableSort } from "@thunderstore/cyberstorm/src/newComponents/Table/Table"; +import { type TeamServiceAccount } from "@thunderstore/thunderstore-api"; + +import { type OutletContextShape } from "app/root"; +import { ServiceAccountRemoveModal } from "./ServiceAccountRemoveModal"; +import "./ServiceAccounts.css"; + +const serviceAccountColumns = [ + { value: "Nickname", disableSort: false }, + { value: "Last Used", disableSort: false }, + { value: "Actions", disableSort: true }, +]; + +export function ServiceAccountsTable(props: { + serviceAccounts: TeamServiceAccount[]; + teamName: string; + serviceAccountRevalidate: () => Promise; +}) { + const { serviceAccounts, serviceAccountRevalidate, teamName } = props; + const outletContext = useOutletContext() as OutletContextShape; + + const tableData = serviceAccounts.map((serviceAccount) => { + return [ + { + value: ( +

+ {serviceAccount.name} +

+ ), + sortValue: serviceAccount.name, + }, + { + value: ( +

+ {serviceAccount.last_used ?? "Never"} +

+ ), + sortValue: serviceAccount.last_used ?? "0", + }, + { + value: ( + + ), + sortValue: 0, + }, + ]; + }); + + return ( + Service Accounts} + headers={serviceAccountColumns} + rows={tableData} + sortByHeader={1} + sortDirection={TableSort.ASC} + /> + ); +} + +ServiceAccountsTable.displayName = "ServiceAccountsTable"; diff --git a/apps/cyberstorm-remix/app/settings/teams/team/tabs/Settings/Settings.tsx b/apps/cyberstorm-remix/app/settings/teams/team/tabs/Settings/Settings.tsx index c43c1ec15..588b0e25b 100644 --- a/apps/cyberstorm-remix/app/settings/teams/team/tabs/Settings/Settings.tsx +++ b/apps/cyberstorm-remix/app/settings/teams/team/tabs/Settings/Settings.tsx @@ -1,7 +1,12 @@ import { faTrashCan } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { useReducer, useState } from "react"; -import { useLoaderData, useNavigate, useOutletContext } from "react-router"; +import { Suspense, useReducer, useState } from "react"; +import { + Await, + useLoaderData, + useNavigate, + useOutletContext, +} from "react-router"; import "./Settings.css"; import { @@ -28,20 +33,18 @@ import { useStrongForm } from "cyberstorm/utils/StrongForm/useStrongForm"; export const clientLoader = makeTeamSettingsTabLoader( // TODO: add end point for checking can leave/disband status. - async (dapper, teamName) => ({}) + async (dapper, teamName) => ({ teamName }) ); export default function Settings() { const { teamName } = useLoaderData(); const outletContext = useOutletContext() as OutletContextShape; - - if (!outletContext.currentUser || !outletContext.currentUser.username) - return ; - const toast = useToast(); - const navigate = useNavigate(); + const currentUser = outletContext.currentUser?.username; + if (!currentUser) return ; + async function moveToTeams() { toast.addToast({ csVariant: "info", @@ -52,57 +55,63 @@ export default function Settings() { } return ( -
-
-
-

Leave team

-

Leave your team

-
-
- - You cannot currently leave this team as you are it's last - owner. - -

- If you are the owner of the team, you can only leave if the team has - another owner assigned. -

- -
-
-
-
-
-

Disband team

-

- Disband your team completely -

-
-
- - You cannot currently disband this team as it has packages. - -

You are about to disband the team {teamName}.

-

- Be aware you can currently only disband teams with no packages. If - you need to archive a team with existing pages, contact Mythic#0001 - on the Thunderstore Discord. -

- -
-
-
+ Loading...
}> + + {(resolvedTeamName) => ( +
+
+
+

Leave team

+

Leave your team

+
+
+ + You cannot currently leave this team as you are it's last + owner. + +

+ If you are the owner of the team, you can only leave if the + team has another owner assigned. +

+ +
+
+
+
+
+

Disband team

+

+ Disband your team completely +

+
+
+ + You cannot currently disband this team as it has packages. + +

You are about to disband the team {resolvedTeamName}.

+

+ Be aware you can currently only disband teams with no + packages. If you need to archive a team with existing pages, + contact Mythic#0001 on the Thunderstore Discord. +

+ +
+
+
+ )} + + ); } diff --git a/apps/cyberstorm-remix/cyberstorm/utils/dapperClientLoaders.ts b/apps/cyberstorm-remix/cyberstorm/utils/dapperClientLoaders.ts index 63fba0ab8..807439792 100644 --- a/apps/cyberstorm-remix/cyberstorm/utils/dapperClientLoaders.ts +++ b/apps/cyberstorm-remix/cyberstorm/utils/dapperClientLoaders.ts @@ -5,6 +5,21 @@ import { ApiError, type GenericApiError } from "@thunderstore/thunderstore-api"; import { getSessionTools } from "cyberstorm/security/publicEnvVariables"; +/** + * TODO + * 1) This approach no longer handles different ApiErrors properly + * when the data isn't awaited in the clientLoader but returned as + * promises for the Suspense/Await elements to handle. Instead, any + * HTTP error codes are shown as 500 errors. This isn't fixed yet + * as it will be easier to do once upcoming project wide error + * handling changes are merged. + * 2) The purpose of this helper was to reduce boilerplate in different + * tab components of the team settings page. Half of that boilerplate + * is Dapper setup, the other is handling ApiErrors. As the latter is + * supposed to be handled elsewhere after the changes mentioned above, + * this helper might no longer have a valid reason to exist after the + * changes. + */ export function makeTeamSettingsTabLoader( dataFetcher: (dapper: DapperTs, teamName: string) => Promise ) {