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

View File

@ -9,8 +9,7 @@ import TableRow from "@mui/material/TableRow";
import DeleteOutline from "@mui/icons-material/DeleteOutline";
import PersonAdd from "@mui/icons-material/PersonAdd";
import SettingsOutlined from "@mui/icons-material/SettingsOutlined";
import { useMachine } from "@xstate/react";
import { User } from "api/typesGenerated";
import { Group, User } from "api/typesGenerated";
import { AvatarData } from "components/AvatarData/AvatarData";
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne";
import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog";
@ -30,7 +29,6 @@ import { useState } from "react";
import { Helmet } from "react-helmet-async";
import { Link as RouterLink, useNavigate, useParams } from "react-router-dom";
import { pageTitle } from "utils/page";
import { groupMachine } from "xServices/groups/groupXService";
import { Maybe } from "components/Conditionals/Maybe";
import { makeStyles } from "@mui/styles";
import {
@ -39,6 +37,175 @@ import {
} from "components/TableToolbar/TableToolbar";
import { UserAvatar } from "components/UserAvatar/UserAvatar";
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<{
isLoading: boolean;
@ -83,182 +250,56 @@ const AddGroupMember: React.FC<{
);
};
export const GroupPage: React.FC = () => {
const { groupId } = useParams();
if (!groupId) {
throw new Error("groupId is not defined.");
}
const navigate = useNavigate();
const [state, send] = useMachine(groupMachine, {
context: {
groupId,
},
actions: {
redirectToGroups: () => {
navigate("/groups");
},
},
});
const { group, permissions } = state.context;
const isLoading = group === undefined || permissions === undefined;
const canUpdateGroup = permissions ? permissions.canUpdateGroup : false;
const GroupMemberRow = (props: {
member: User;
group: Group;
canUpdate: boolean;
}) => {
const { member, group, canUpdate } = props;
const queryClient = useQueryClient();
const removeMemberMutation = useMutation(removeMember(queryClient));
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}>
<TableCell width="99%">
<AvatarData
avatar={
<UserAvatar
username={member.username}
avatarURL={member.avatar_url}
/>
}
title={member.username}
subtitle={member.email}
/>
</TableCell>
<TableCell width="1%">
<Maybe condition={canUpdateGroup}>
<TableRowMenu
data={member}
menuItems={[
{
label: "Remove",
onClick: () => {
send({
type: "REMOVE_MEMBER",
userId: member.id,
});
},
disabled:
group.id === group.organization_id,
},
]}
/>
</Maybe>
</TableCell>
</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");
}}
<TableRow key={member.id}>
<TableCell width="99%">
<AvatarData
avatar={
<UserAvatar
username={member.username}
avatarURL={member.avatar_url}
/>
}
title={member.username}
subtitle={member.email}
/>
)}
</>
</TableCell>
<TableCell width="1%">
<Maybe condition={canUpdate}>
<TableRowMenu
data={member}
menuItems={[
{
label: "Remove",
onClick: async () => {
try {
await removeMemberMutation.mutateAsync({
groupId: group.id,
userId: member.id,
});
displaySuccess("Member removed successfully.");
} catch (error) {
displayError(
getErrorMessage(error, "Failed to remove member."),
);
}
},
disabled: group.id === group.organization_id,
},
]}
/>
</Maybe>
</TableCell>
</TableRow>
);
};

View File

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

View File

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