diff --git a/astroplant-frontend/src/scenes/kit/access/components/Members.tsx b/astroplant-frontend/src/scenes/kit/access/components/Members.tsx
index 67ebb4f..0846c2c 100644
--- a/astroplant-frontend/src/scenes/kit/access/components/Members.tsx
+++ b/astroplant-frontend/src/scenes/kit/access/components/Members.tsx
@@ -8,10 +8,11 @@ import { DropdownDetails } from "~/Components/DropdownDetails";
import style from "./Members.module.css";
import clsx from "clsx";
-import { IconCheck } from "@tabler/icons-react";
+import { IconCheck, IconPlus, IconX } from "@tabler/icons-react";
import { ChangeEvent, useEffect, useMemo, useRef, useState } from "react";
import { ModalDialog } from "~/Components/ModalDialog";
import { Input } from "~/Components/Input";
+import { useDebounce } from "~/hooks";
export type Props = {
kit: schemas["Kit"];
@@ -22,6 +23,8 @@ export default function Members({ kit }: Props) {
const [deleteKitMembership, { error: deleteKitMembershipError }] =
rtkApi.useDeleteKitMembershipMutation();
+ const [addingMember, setAddingMember] = useState(false);
+
useEffect(() => {
if (deleteKitMembershipError !== undefined) {
// TODO: use something other than an `alert`
@@ -42,6 +45,16 @@ export default function Members({ kit }: Props) {
return (
<>
+
+ setAddingMember(false)}>
+ setAddingMember(false)} />
+
{data?.map((membership) => (
-
@@ -251,3 +264,135 @@ function RoleSelector({
);
}
+
+function AddMember({ kit, close }: { kit: schemas["Kit"]; close: () => void }) {
+ const [query, setQuery] = useState("");
+ const debouncedQuery = useDebounce(query, 250);
+
+ const [selectedUser, setSelectedUser] = useState(
+ null,
+ );
+ const [success, setSuccess] = useState(false);
+
+ const { data: suggestions } = rtkApi.useGetKitMemberSuggestionsQuery(
+ {
+ kitSerial: kit.serial,
+ query: { username: debouncedQuery },
+ },
+ { skip: debouncedQuery === "" },
+ );
+ const [addKitMember] = rtkApi.useAddKitMemberMutation();
+
+ return (
+ <>
+
+ Find people to add to {kit.serial}
+
+ {selectedUser ? (
+ success ? (
+
+
+ User {selectedUser.displayName} has successfully
+ been added as member.
+
+ }
+ onClick={async () => {
+ setSelectedUser(null);
+ setSuccess(false);
+ setQuery("");
+ close();
+ }}
+ >
+ Close
+
+
+ ) : (
+
+
+
+
+ {selectedUser.displayName}
+
+ {selectedUser.username}
+
+
+
+
+
+
+ Do you want to add {selectedUser.displayName} to
+ this kit?
+
+
+ }
+ variant="primary"
+ onClick={async () => {
+ const result = await addKitMember({
+ kitSerial: kit.serial,
+ member: {
+ username: selectedUser.username,
+ accessConfigure: false,
+ accessSuper: false,
+ },
+ });
+ if ("data" in result) {
+ setSuccess(true);
+ } else {
+ alert("An error occurred");
+ }
+ }}
+ >
+ Add {selectedUser.displayName}
+
+
+
+ )
+ ) : (
+
+ setQuery(e.currentTarget.value)}
+ placeholder="Username"
+ />
+ {suggestions !== undefined && (
+
+ {suggestions.length === 0 ? (
+
+ Could not find anyone with that username.
+
+ ) : (
+
+ {suggestions.map((suggestion) => (
+ - setSelectedUser(suggestion)}
+ >
+ {" "}
+ {suggestion.username}
+
+ ))}
+
+ )}
+
+ )}
+
+ )}
+ >
+ );
+}
diff --git a/astroplant-frontend/src/services/astroplant.ts b/astroplant-frontend/src/services/astroplant.ts
index 6fe2cc1..b27bf8c 100644
--- a/astroplant-frontend/src/services/astroplant.ts
+++ b/astroplant-frontend/src/services/astroplant.ts
@@ -74,7 +74,7 @@ const baseQueryWithRetry = retry(baseQueryFn);
export const rtkApi = createApi({
reducerPath: "api",
baseQuery: baseQueryWithRetry,
- tagTypes: ["Users", "KitMemberships"],
+ tagTypes: ["Users", "KitMemberships", "KitMemberSuggestions"],
endpoints: (build) => ({
listKits: build.query({
query: () => ({ path: "/kits", method: "GET" }),
@@ -105,6 +105,37 @@ export const rtkApi = createApi({
{ type: "KitMemberships", id: result?.[0]?.kit.id },
],
}),
+ addKitMember: build.mutation<
+ schemas["KitMembership"],
+ {
+ kitSerial: string;
+ member: {
+ username: string;
+ accessConfigure: boolean;
+ accessSuper: boolean;
+ };
+ }
+ >({
+ query: ({ kitSerial, member }) => ({
+ path: `/kits/${encodeUri(kitSerial)}/members`,
+ method: "POST",
+ body: member,
+ }),
+ invalidatesTags: (result) => [
+ { type: "KitMemberships", id: result?.kit.id },
+ ],
+ }),
+ getKitMemberSuggestions: build.query<
+ Array,
+ { kitSerial: string; query: { username: string } }
+ >({
+ query: ({ kitSerial, query }) => ({
+ path: `/kits/${encodeUri(kitSerial)}/member-suggestions`,
+ method: "GET",
+ query,
+ }),
+ providesTags: [{ type: "KitMemberSuggestions" }],
+ }),
patchKitMembership: build.mutation<
schemas["KitMembership"],
{ kitMembershipId: number; patch: schemas["PatchKitMembership"] }
@@ -114,6 +145,9 @@ export const rtkApi = createApi({
method: "PATCH",
body: patch,
}),
+ invalidatesTags: (result) => [
+ { type: "KitMemberships", id: result?.kit.id },
+ ],
}),
/// `kitId` is required to be able to target the cache invalidation
deleteKitMembership: build.mutation<
@@ -126,6 +160,7 @@ export const rtkApi = createApi({
}),
invalidatesTags: (_result, _err, { kitId }) => [
{ type: "KitMemberships", id: kitId },
+ { type: "KitMemberSuggestions" },
],
}),
getArchiveDownloadToken: build.query<