chore(site): refactor groups to use react-query (#9701)

This commit is contained in:
Bruno Quaresma
2023-09-19 13:37:17 -03:00
committed by GitHub
parent 161a3cfa26
commit 87d50f17a2
10 changed files with 386 additions and 723 deletions

View File

@ -969,6 +969,24 @@ export const patchGroup = async (
return response.data; return response.data;
}; };
export const addMember = async (groupId: string, userId: string) => {
return patchGroup(groupId, {
name: "",
display_name: "",
add_users: [userId],
remove_users: [],
});
};
export const removeMember = async (groupId: string, userId: string) => {
return patchGroup(groupId, {
name: "",
display_name: "",
add_users: [],
remove_users: [userId],
});
};
export const deleteGroup = async (groupId: string): Promise<void> => { export const deleteGroup = async (groupId: string): Promise<void> => {
await axios.delete(`/api/v2/groups/${groupId}`); await axios.delete(`/api/v2/groups/${groupId}`);
}; };

View File

@ -0,0 +1,101 @@
import { QueryClient } from "@tanstack/react-query";
import * as API from "api/api";
import { checkAuthorization } from "api/api";
import {
CreateGroupRequest,
Group,
PatchGroupRequest,
} from "api/typesGenerated";
const GROUPS_QUERY_KEY = ["groups"];
const getGroupQueryKey = (groupId: string) => ["group", groupId];
export const groups = (organizationId: string) => {
return {
queryKey: GROUPS_QUERY_KEY,
queryFn: () => API.getGroups(organizationId),
};
};
export const group = (groupId: string) => {
return {
queryKey: getGroupQueryKey(groupId),
queryFn: () => API.getGroup(groupId),
};
};
export const groupPermissions = (groupId: string) => {
return {
queryKey: [...getGroupQueryKey(groupId), "permissions"],
queryFn: () =>
checkAuthorization({
checks: {
canUpdateGroup: {
object: {
resource_type: "group",
resource_id: groupId,
},
action: "update",
},
},
}),
};
};
export const createGroup = (queryClient: QueryClient) => {
return {
mutationFn: ({
organizationId,
...request
}: CreateGroupRequest & { organizationId: string }) =>
API.createGroup(organizationId, request),
onSuccess: async () => {
await queryClient.invalidateQueries(GROUPS_QUERY_KEY);
},
};
};
export const patchGroup = (queryClient: QueryClient) => {
return {
mutationFn: ({
groupId,
...request
}: PatchGroupRequest & { groupId: string }) =>
API.patchGroup(groupId, request),
onSuccess: async (updatedGroup: Group) =>
invalidateGroup(queryClient, updatedGroup.id),
};
};
export const deleteGroup = (queryClient: QueryClient) => {
return {
mutationFn: API.deleteGroup,
onSuccess: async (_: void, groupId: string) =>
invalidateGroup(queryClient, groupId),
};
};
export const addMember = (queryClient: QueryClient) => {
return {
mutationFn: ({ groupId, userId }: { groupId: string; userId: string }) =>
API.addMember(groupId, userId),
onSuccess: async (updatedGroup: Group) =>
invalidateGroup(queryClient, updatedGroup.id),
};
};
export const removeMember = (queryClient: QueryClient) => {
return {
mutationFn: ({ groupId, userId }: { groupId: string; userId: string }) =>
API.removeMember(groupId, userId),
onSuccess: async (updatedGroup: Group) =>
invalidateGroup(queryClient, updatedGroup.id),
};
};
export const invalidateGroup = (queryClient: QueryClient, groupId: string) =>
Promise.all([
queryClient.invalidateQueries(GROUPS_QUERY_KEY),
queryClient.invalidateQueries(getGroupQueryKey(groupId)),
]);

View File

@ -1,26 +1,17 @@
import { useMachine } from "@xstate/react";
import { useOrganizationId } from "hooks/useOrganizationId"; import { useOrganizationId } from "hooks/useOrganizationId";
import { FC } from "react"; import { FC } from "react";
import { Helmet } from "react-helmet-async"; import { Helmet } from "react-helmet-async";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { pageTitle } from "utils/page"; import { pageTitle } from "utils/page";
import { createGroupMachine } from "xServices/groups/createGroupXService";
import CreateGroupPageView from "./CreateGroupPageView"; import CreateGroupPageView from "./CreateGroupPageView";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { createGroup } from "api/queries/groups";
export const CreateGroupPage: FC = () => { export const CreateGroupPage: FC = () => {
const queryClient = useQueryClient();
const navigate = useNavigate(); const navigate = useNavigate();
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const [createState, sendCreateEvent] = useMachine(createGroupMachine, { const createGroupMutation = useMutation(createGroup(queryClient));
context: {
organizationId,
},
actions: {
onCreate: (_, { data }) => {
navigate(`/groups/${data.id}`);
},
},
});
const { error } = createState.context;
return ( return (
<> <>
@ -28,14 +19,15 @@ export const CreateGroupPage: FC = () => {
<title>{pageTitle("Create Group")}</title> <title>{pageTitle("Create Group")}</title>
</Helmet> </Helmet>
<CreateGroupPageView <CreateGroupPageView
onSubmit={(data) => { onSubmit={async (data) => {
sendCreateEvent({ const newGroup = await createGroupMutation.mutateAsync({
type: "CREATE", organizationId,
data, ...data,
}); });
navigate(`/groups/${newGroup.id}`);
}} }}
formErrors={error} formErrors={createGroupMutation.error}
isLoading={createState.matches("creatingGroup")} isLoading={createGroupMutation.isLoading}
/> />
</> </>
); );

View File

@ -9,8 +9,7 @@ import TableRow from "@mui/material/TableRow";
import DeleteOutline from "@mui/icons-material/DeleteOutline"; import DeleteOutline from "@mui/icons-material/DeleteOutline";
import PersonAdd from "@mui/icons-material/PersonAdd"; import PersonAdd from "@mui/icons-material/PersonAdd";
import SettingsOutlined from "@mui/icons-material/SettingsOutlined"; import SettingsOutlined from "@mui/icons-material/SettingsOutlined";
import { useMachine } from "@xstate/react"; import { Group, User } from "api/typesGenerated";
import { User } from "api/typesGenerated";
import { AvatarData } from "components/AvatarData/AvatarData"; import { AvatarData } from "components/AvatarData/AvatarData";
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"; import { ChooseOne, Cond } from "components/Conditionals/ChooseOne";
import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog"; import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog";
@ -30,7 +29,6 @@ import { useState } from "react";
import { Helmet } from "react-helmet-async"; import { Helmet } from "react-helmet-async";
import { Link as RouterLink, useNavigate, useParams } from "react-router-dom"; import { Link as RouterLink, useNavigate, useParams } from "react-router-dom";
import { pageTitle } from "utils/page"; import { pageTitle } from "utils/page";
import { groupMachine } from "xServices/groups/groupXService";
import { Maybe } from "components/Conditionals/Maybe"; import { Maybe } from "components/Conditionals/Maybe";
import { makeStyles } from "@mui/styles"; import { makeStyles } from "@mui/styles";
import { import {
@ -39,6 +37,175 @@ import {
} from "components/TableToolbar/TableToolbar"; } from "components/TableToolbar/TableToolbar";
import { UserAvatar } from "components/UserAvatar/UserAvatar"; import { UserAvatar } from "components/UserAvatar/UserAvatar";
import { isEveryoneGroup } from "utils/groups"; import { isEveryoneGroup } from "utils/groups";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
addMember,
deleteGroup,
group,
groupPermissions,
removeMember,
} from "api/queries/groups";
import { displayError, displaySuccess } from "components/GlobalSnackbar/utils";
import { getErrorMessage } from "api/errors";
export const GroupPage: React.FC = () => {
const { groupId } = useParams() as { groupId: string };
const queryClient = useQueryClient();
const navigate = useNavigate();
const groupQuery = useQuery(group(groupId));
const groupData = groupQuery.data;
const { data: permissions } = useQuery(groupPermissions(groupId));
const addMemberMutation = useMutation(addMember(queryClient));
const deleteGroupMutation = useMutation(deleteGroup(queryClient));
const [isDeletingGroup, setIsDeletingGroup] = useState(false);
const isLoading = !groupData || !permissions;
const canUpdateGroup = permissions ? permissions.canUpdateGroup : false;
return (
<>
<Helmet>
<title>
{pageTitle(
(groupData?.display_name || groupData?.name) ?? "Loading...",
)}
</title>
</Helmet>
<ChooseOne>
<Cond condition={isLoading}>
<Loader />
</Cond>
<Cond>
<Margins>
<PageHeader
actions={
<Maybe condition={canUpdateGroup}>
<Link to="settings" component={RouterLink}>
<Button startIcon={<SettingsOutlined />}>Settings</Button>
</Link>
<Button
disabled={groupData?.id === groupData?.organization_id}
onClick={() => {
setIsDeletingGroup(true);
}}
startIcon={<DeleteOutline />}
>
Delete
</Button>
</Maybe>
}
>
<PageHeaderTitle>
{groupData?.display_name || groupData?.name}
</PageHeaderTitle>
<PageHeaderSubtitle>
{/* Show the name if it differs from the display name. */}
{groupData?.display_name &&
groupData?.display_name !== groupData?.name
? groupData?.name
: ""}{" "}
</PageHeaderSubtitle>
</PageHeader>
<Stack spacing={1}>
<Maybe
condition={
canUpdateGroup &&
groupData !== undefined &&
!isEveryoneGroup(groupData)
}
>
<AddGroupMember
isLoading={addMemberMutation.isLoading}
onSubmit={async (user, reset) => {
try {
await addMemberMutation.mutateAsync({
groupId,
userId: user.id,
});
reset();
} catch (error) {
displayError(
getErrorMessage(error, "Failed to add member."),
);
}
}}
/>
</Maybe>
<TableToolbar>
<PaginationStatus
isLoading={Boolean(isLoading)}
showing={groupData?.members.length ?? 0}
total={groupData?.members.length ?? 0}
label="members"
/>
</TableToolbar>
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell width="99%">User</TableCell>
<TableCell width="1%"></TableCell>
</TableRow>
</TableHead>
<TableBody>
<ChooseOne>
<Cond
condition={Boolean(groupData?.members.length === 0)}
>
<TableRow>
<TableCell colSpan={999}>
<EmptyState
message="No members yet"
description="Add a member using the controls above"
/>
</TableCell>
</TableRow>
</Cond>
<Cond>
{groupData?.members.map((member) => (
<GroupMemberRow
member={member}
group={groupData}
key={member.id}
canUpdate={canUpdateGroup}
/>
))}
</Cond>
</ChooseOne>
</TableBody>
</Table>
</TableContainer>
</Stack>
</Margins>
</Cond>
</ChooseOne>
{group && (
<DeleteDialog
isOpen={isDeletingGroup}
confirmLoading={deleteGroupMutation.isLoading}
name={group.name}
entity="group"
onConfirm={async () => {
try {
await deleteGroupMutation.mutateAsync(groupId);
navigate("/groups");
} catch (error) {
displayError(getErrorMessage(error, "Failed to delete group."));
}
}}
onCancel={() => {
setIsDeletingGroup(false);
}}
/>
)}
</>
);
};
const AddGroupMember: React.FC<{ const AddGroupMember: React.FC<{
isLoading: boolean; isLoading: boolean;
@ -83,122 +250,16 @@ const AddGroupMember: React.FC<{
); );
}; };
export const GroupPage: React.FC = () => { const GroupMemberRow = (props: {
const { groupId } = useParams(); member: User;
if (!groupId) { group: Group;
throw new Error("groupId is not defined."); canUpdate: boolean;
} }) => {
const { member, group, canUpdate } = props;
const navigate = useNavigate(); const queryClient = useQueryClient();
const [state, send] = useMachine(groupMachine, { const removeMemberMutation = useMutation(removeMember(queryClient));
context: {
groupId,
},
actions: {
redirectToGroups: () => {
navigate("/groups");
},
},
});
const { group, permissions } = state.context;
const isLoading = group === undefined || permissions === undefined;
const canUpdateGroup = permissions ? permissions.canUpdateGroup : false;
return ( return (
<>
<Helmet>
<title>
{pageTitle((group?.display_name || group?.name) ?? "Loading...")}
</title>
</Helmet>
<ChooseOne>
<Cond condition={isLoading}>
<Loader />
</Cond>
<Cond>
<Margins>
<PageHeader
actions={
<Maybe condition={canUpdateGroup}>
<Link to="settings" component={RouterLink}>
<Button startIcon={<SettingsOutlined />}>Settings</Button>
</Link>
<Button
disabled={group?.id === group?.organization_id}
onClick={() => {
send("DELETE");
}}
startIcon={<DeleteOutline />}
>
Delete
</Button>
</Maybe>
}
>
<PageHeaderTitle>
{group?.display_name || group?.name}
</PageHeaderTitle>
<PageHeaderSubtitle>
{/* Show the name if it differs from the display name. */}
{group?.display_name && group?.display_name !== group?.name
? group?.name
: ""}{" "}
</PageHeaderSubtitle>
</PageHeader>
<Stack spacing={1}>
<Maybe
condition={
canUpdateGroup &&
group !== undefined &&
!isEveryoneGroup(group)
}
>
<AddGroupMember
isLoading={state.matches("addingMember")}
onSubmit={(user, reset) => {
send({
type: "ADD_MEMBER",
userId: user.id,
callback: reset,
});
}}
/>
</Maybe>
<TableToolbar>
<PaginationStatus
isLoading={Boolean(isLoading)}
showing={group?.members.length ?? 0}
total={group?.members.length ?? 0}
label="members"
/>
</TableToolbar>
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell width="99%">User</TableCell>
<TableCell width="1%"></TableCell>
</TableRow>
</TableHead>
<TableBody>
<ChooseOne>
<Cond condition={Boolean(group?.members.length === 0)}>
<TableRow>
<TableCell colSpan={999}>
<EmptyState
message="No members yet"
description="Add a member using the controls above"
/>
</TableCell>
</TableRow>
</Cond>
<Cond>
{group?.members.map((member) => (
<TableRow key={member.id}> <TableRow key={member.id}>
<TableCell width="99%"> <TableCell width="99%">
<AvatarData <AvatarData
@ -213,52 +274,32 @@ export const GroupPage: React.FC = () => {
/> />
</TableCell> </TableCell>
<TableCell width="1%"> <TableCell width="1%">
<Maybe condition={canUpdateGroup}> <Maybe condition={canUpdate}>
<TableRowMenu <TableRowMenu
data={member} data={member}
menuItems={[ menuItems={[
{ {
label: "Remove", label: "Remove",
onClick: () => { onClick: async () => {
send({ try {
type: "REMOVE_MEMBER", await removeMemberMutation.mutateAsync({
groupId: group.id,
userId: member.id, userId: member.id,
}); });
displaySuccess("Member removed successfully.");
} catch (error) {
displayError(
getErrorMessage(error, "Failed to remove member."),
);
}
}, },
disabled: disabled: group.id === group.organization_id,
group.id === group.organization_id,
}, },
]} ]}
/> />
</Maybe> </Maybe>
</TableCell> </TableCell>
</TableRow> </TableRow>
))}
</Cond>
</ChooseOne>
</TableBody>
</Table>
</TableContainer>
</Stack>
</Margins>
</Cond>
</ChooseOne>
{group && (
<DeleteDialog
isOpen={state.matches("confirmingDelete")}
confirmLoading={state.matches("deleting")}
name={group.name}
entity="group"
onConfirm={() => {
send("CONFIRM_DELETE");
}}
onCancel={() => {
send("CANCEL_DELETE");
}}
/>
)}
</>
); );
}; };

View File

@ -1,24 +1,28 @@
import { useMachine } from "@xstate/react";
import { useFeatureVisibility } from "hooks/useFeatureVisibility"; import { useFeatureVisibility } from "hooks/useFeatureVisibility";
import { useOrganizationId } from "hooks/useOrganizationId"; import { useOrganizationId } from "hooks/useOrganizationId";
import { usePermissions } from "hooks/usePermissions"; import { usePermissions } from "hooks/usePermissions";
import { FC } from "react"; import { FC, useEffect } from "react";
import { Helmet } from "react-helmet-async"; import { Helmet } from "react-helmet-async";
import { pageTitle } from "utils/page"; import { pageTitle } from "utils/page";
import { groupsMachine } from "xServices/groups/groupsXService";
import GroupsPageView from "./GroupsPageView"; import GroupsPageView from "./GroupsPageView";
import { useQuery } from "@tanstack/react-query";
import { groups } from "api/queries/groups";
import { displayError } from "components/GlobalSnackbar/utils";
import { getErrorMessage } from "api/errors";
export const GroupsPage: FC = () => { export const GroupsPage: FC = () => {
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const { createGroup: canCreateGroup } = usePermissions(); const { createGroup: canCreateGroup } = usePermissions();
const { template_rbac: isTemplateRBACEnabled } = useFeatureVisibility(); const { template_rbac: isTemplateRBACEnabled } = useFeatureVisibility();
const [state] = useMachine(groupsMachine, { const groupsQuery = useQuery(groups(organizationId));
context: {
organizationId, useEffect(() => {
shouldFetchGroups: isTemplateRBACEnabled, if (groupsQuery.error) {
}, displayError(
}); getErrorMessage(groupsQuery.error, "Error on loading groups."),
const { groups } = state.context; );
}
}, [groupsQuery.error]);
return ( return (
<> <>
@ -27,7 +31,7 @@ export const GroupsPage: FC = () => {
</Helmet> </Helmet>
<GroupsPageView <GroupsPageView
groups={groups} groups={groupsQuery.data}
canCreateGroup={canCreateGroup} canCreateGroup={canCreateGroup}
isTemplateRBACEnabled={isTemplateRBACEnabled} isTemplateRBACEnabled={isTemplateRBACEnabled}
/> />

View File

@ -1,33 +1,24 @@
import { useMachine } from "@xstate/react";
import { FC } from "react"; import { FC } from "react";
import { Helmet } from "react-helmet-async"; import { Helmet } from "react-helmet-async";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import { pageTitle } from "utils/page"; import { pageTitle } from "utils/page";
import { editGroupMachine } from "xServices/groups/editGroupXService";
import SettingsGroupPageView from "./SettingsGroupPageView"; import SettingsGroupPageView from "./SettingsGroupPageView";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { group, patchGroup } from "api/queries/groups";
import { displayError } from "components/GlobalSnackbar/utils";
import { getErrorMessage } from "api/errors";
export const SettingsGroupPage: FC = () => { export const SettingsGroupPage: FC = () => {
const { groupId } = useParams(); const { groupId } = useParams() as { groupId: string };
if (!groupId) { const queryClient = useQueryClient();
throw new Error("Group ID not defined."); const groupQuery = useQuery(group(groupId));
} const patchGroupMutation = useMutation(patchGroup(queryClient));
const navigate = useNavigate(); const navigate = useNavigate();
const navigateToGroup = () => { const navigateToGroup = () => {
navigate(`/groups/${groupId}`); navigate(`/groups/${groupId}`);
}; };
const [editState, sendEditEvent] = useMachine(editGroupMachine, {
context: {
groupId,
},
actions: {
onUpdate: navigateToGroup,
},
});
const { error, group } = editState.context;
return ( return (
<> <>
<Helmet> <Helmet>
@ -36,13 +27,23 @@ export const SettingsGroupPage: FC = () => {
<SettingsGroupPageView <SettingsGroupPageView
onCancel={navigateToGroup} onCancel={navigateToGroup}
onSubmit={(data) => { onSubmit={async (data) => {
sendEditEvent({ type: "UPDATE", data }); try {
await patchGroupMutation.mutateAsync({
groupId,
...data,
add_users: [],
remove_users: [],
});
navigateToGroup();
} catch (error) {
displayError(getErrorMessage(error, "Failed to update group"));
}
}} }}
group={group} group={groupQuery.data}
formErrors={error} formErrors={groupQuery.error}
isLoading={editState.matches("loading")} isLoading={groupQuery.isLoading}
isUpdating={editState.matches("updating")} isUpdating={patchGroupMutation.isLoading}
/> />
</> </>
); );

View File

@ -1,59 +0,0 @@
import { createGroup } from "api/api";
import { CreateGroupRequest, Group } from "api/typesGenerated";
import { createMachine, assign } from "xstate";
export const createGroupMachine = createMachine(
{
id: "createGroupMachine",
schema: {
context: {} as {
organizationId: string;
error?: unknown;
},
services: {} as {
createGroup: {
data: Group;
};
},
events: {} as {
type: "CREATE";
data: CreateGroupRequest;
},
},
tsTypes: {} as import("./createGroupXService.typegen").Typegen0,
initial: "idle",
states: {
idle: {
on: {
CREATE: {
target: "creatingGroup",
},
},
},
creatingGroup: {
invoke: {
src: "createGroup",
onDone: {
target: "idle",
actions: ["onCreate"],
},
onError: {
target: "idle",
actions: ["assignError"],
},
},
},
},
},
{
services: {
createGroup: ({ organizationId }, { data }) =>
createGroup(organizationId, data),
},
actions: {
assignError: assign({
error: (_, event) => event.data,
}),
},
},
);

View File

@ -1,100 +0,0 @@
import { getGroup, patchGroup } from "api/api";
import { getErrorMessage } from "api/errors";
import { Group } from "api/typesGenerated";
import { displayError } from "components/GlobalSnackbar/utils";
import { assign, createMachine } from "xstate";
export const editGroupMachine = createMachine(
{
id: "editGroup",
schema: {
context: {} as {
groupId: string;
group?: Group;
error?: unknown;
},
services: {} as {
loadGroup: {
data: Group;
};
updateGroup: {
data: Group;
};
},
events: {} as {
type: "UPDATE";
data: {
display_name: string;
name: string;
avatar_url: string;
quota_allowance: number;
};
},
},
tsTypes: {} as import("./editGroupXService.typegen").Typegen0,
initial: "loading",
states: {
loading: {
invoke: {
src: "loadGroup",
onDone: {
actions: ["assignGroup"],
target: "idle",
},
onError: {
actions: ["displayLoadGroupError"],
target: "idle",
},
},
},
idle: {
on: {
UPDATE: {
target: "updating",
},
},
},
updating: {
invoke: {
src: "updateGroup",
onDone: {
actions: ["onUpdate"],
},
onError: {
target: "idle",
actions: ["assignError"],
},
},
},
},
},
{
services: {
loadGroup: ({ groupId }) => getGroup(groupId),
updateGroup: ({ group }, { data }) => {
if (!group) {
throw new Error("Group not defined.");
}
return patchGroup(group.id, {
...data,
add_users: [],
remove_users: [],
});
},
},
actions: {
assignGroup: assign({
group: (_, { data }) => data,
}),
displayLoadGroupError: (_, { data }) => {
const message = getErrorMessage(data, "Failed to the group.");
displayError(message);
},
assignError: assign({
error: (_, event) => event.data,
}),
},
},
);

View File

@ -1,275 +0,0 @@
import { deleteGroup, getGroup, patchGroup, checkAuthorization } from "api/api";
import { getErrorMessage } from "api/errors";
import { AuthorizationResponse, Group } from "api/typesGenerated";
import { displayError, displaySuccess } from "components/GlobalSnackbar/utils";
import { assign, createMachine } from "xstate";
export const groupMachine = createMachine(
{
id: "group",
schema: {
context: {} as {
groupId: string;
group?: Group;
permissions?: AuthorizationResponse;
addMemberCallback?: () => void;
removingMember?: string;
},
services: {} as {
loadGroup: {
data: Group;
};
loadPermissions: {
data: AuthorizationResponse;
};
addMember: {
data: Group;
};
removeMember: {
data: Group;
};
deleteGroup: {
data: unknown;
};
},
events: {} as
| {
type: "ADD_MEMBER";
userId: string;
callback: () => void;
}
| {
type: "REMOVE_MEMBER";
userId: string;
}
| {
type: "DELETE";
}
| {
type: "CONFIRM_DELETE";
}
| {
type: "CANCEL_DELETE";
},
},
tsTypes: {} as import("./groupXService.typegen").Typegen0,
initial: "loading",
states: {
loading: {
type: "parallel",
states: {
data: {
initial: "loading",
states: {
loading: {
invoke: {
src: "loadGroup",
onDone: {
actions: ["assignGroup"],
target: "success",
},
onError: {
actions: ["displayLoadGroupError"],
},
},
},
success: {
type: "final",
},
},
},
permissions: {
initial: "loading",
states: {
loading: {
invoke: {
src: "loadPermissions",
onDone: {
actions: ["assignPermissions"],
target: "success",
},
onError: {
actions: ["displayLoadPermissionsError"],
},
},
},
success: {
type: "final",
},
},
},
},
onDone: "idle",
},
idle: {
on: {
ADD_MEMBER: {
target: "addingMember",
actions: ["assignAddMemberCallback"],
},
REMOVE_MEMBER: {
target: "removingMember",
actions: ["removeUserFromMembers"],
},
DELETE: {
target: "confirmingDelete",
},
},
},
addingMember: {
invoke: {
src: "addMember",
onDone: {
actions: ["assignGroup", "callAddMemberCallback"],
target: "idle",
},
onError: {
target: "idle",
actions: ["displayAddMemberError"],
},
},
},
removingMember: {
invoke: {
src: "removeMember",
onDone: {
actions: ["assignGroup", "displayRemoveMemberSuccess"],
target: "idle",
},
onError: {
target: "idle",
actions: ["displayRemoveMemberError"],
},
},
},
confirmingDelete: {
on: {
CONFIRM_DELETE: "deleting",
CANCEL_DELETE: "idle",
},
},
deleting: {
invoke: {
src: "deleteGroup",
onDone: {
actions: ["redirectToGroups"],
},
onError: {
actions: ["displayDeleteGroupError"],
},
},
},
},
},
{
services: {
loadGroup: ({ groupId }) => getGroup(groupId),
loadPermissions: ({ groupId }) =>
checkAuthorization({
checks: {
canUpdateGroup: {
object: {
resource_type: "group",
resource_id: groupId,
},
action: "update",
},
},
}),
addMember: ({ group }, { userId }) => {
if (!group) {
throw new Error("Group not defined.");
}
return patchGroup(group.id, {
name: "",
display_name: "",
add_users: [userId],
remove_users: [],
});
},
removeMember: ({ group }, { userId }) => {
if (!group) {
throw new Error("Group not defined.");
}
return patchGroup(group.id, {
name: "",
display_name: "",
add_users: [],
remove_users: [userId],
});
},
deleteGroup: ({ group }) => {
if (!group) {
throw new Error("Group not defined.");
}
return deleteGroup(group.id);
},
},
actions: {
assignGroup: assign({
group: (_, { data }) => data,
}),
assignAddMemberCallback: assign({
addMemberCallback: (_, { callback }) => callback,
}),
displayLoadGroupError: (_, { data }) => {
const message = getErrorMessage(data, "Failed to load the group.");
displayError(message);
},
displayAddMemberError: (_, { data }) => {
const message = getErrorMessage(
data,
"Failed to add member to the group.",
);
displayError(message);
},
callAddMemberCallback: ({ addMemberCallback }) => {
if (addMemberCallback) {
addMemberCallback();
}
},
// Optimistically remove the user from members
removeUserFromMembers: assign({
group: ({ group }, { userId }) => {
if (!group) {
throw new Error("Group is not defined.");
}
return {
...group,
members: group.members.filter(
(currentMember) => currentMember.id !== userId,
),
};
},
}),
displayRemoveMemberError: (_, { data }) => {
const message = getErrorMessage(
data,
"Failed to remove member from the group.",
);
displayError(message);
},
displayRemoveMemberSuccess: () => {
displaySuccess("Member removed successfully.");
},
displayDeleteGroupError: (_, { data }) => {
const message = getErrorMessage(data, "Failed to delete group.");
displayError(message);
},
assignPermissions: assign({
permissions: (_, { data }) => data,
}),
displayLoadPermissionsError: (_, { data }) => {
const message = getErrorMessage(
data,
"Failed to load the permissions.",
);
displayError(message);
},
},
},
);

View File

@ -1,60 +0,0 @@
import { getGroups } from "api/api";
import { getErrorMessage } from "api/errors";
import { Group } from "api/typesGenerated";
import { displayError } from "components/GlobalSnackbar/utils";
import { assign, createMachine } from "xstate";
export const groupsMachine = createMachine(
{
id: "groupsMachine",
predictableActionArguments: true,
schema: {
context: {} as {
organizationId: string;
shouldFetchGroups: boolean;
groups?: Group[];
},
services: {} as {
loadGroups: {
data: Group[];
};
},
},
tsTypes: {} as import("./groupsXService.typegen").Typegen0,
initial: "loading",
states: {
loading: {
always: [{ target: "idle", cond: "cantFetchGroups" }],
invoke: {
src: "loadGroups",
onDone: {
actions: ["assignGroups"],
target: "idle",
},
onError: {
target: "idle",
actions: ["displayLoadingGroupsError"],
},
},
},
idle: {},
},
},
{
guards: {
cantFetchGroups: ({ shouldFetchGroups }) => !shouldFetchGroups,
},
services: {
loadGroups: ({ organizationId }) => getGroups(organizationId),
},
actions: {
assignGroups: assign({
groups: (_, { data }) => data,
}),
displayLoadingGroupsError: (_, { data }) => {
const message = getErrorMessage(data, "Error on loading groups.");
displayError(message);
},
},
},
);