-
Notifications
You must be signed in to change notification settings - Fork 115
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[front/components/vaults] - feature: add UI for creating vaults with …
…member selection - Implement a new modal component to create vaults and manage vault members - Provide form inputs for vault name and search-filterable member list with add/remove actions
- Loading branch information
Jules
authored and
Jules
committed
Aug 12, 2024
1 parent
7a401ad
commit 52e3566
Showing
1 changed file
with
201 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,201 @@ | ||
import { | ||
Button, | ||
DataTable, | ||
Icon, | ||
Input, | ||
Modal, | ||
Page, | ||
PlusIcon, | ||
Searchbar, | ||
} from "@dust-tt/sparkle"; | ||
import type { UserType, WorkspaceType } from "@dust-tt/types"; | ||
import { InformationCircleIcon } from "@heroicons/react/20/solid"; | ||
import type { CellContext, ColumnDef } from "@tanstack/react-table"; | ||
import { MinusIcon } from "lucide-react"; | ||
import React, { useCallback, useContext, useMemo, useState } from "react"; | ||
|
||
import { SendNotificationsContext } from "@app/components/sparkle/Notification"; | ||
|
||
type RowData = { | ||
icon: string; | ||
name: string; | ||
userId: string; | ||
onClick?: () => void; | ||
onMoreClick?: () => void; | ||
}; | ||
|
||
type Info = CellContext<RowData, unknown>; | ||
|
||
interface CreateVaultModalProps { | ||
owner: WorkspaceType; | ||
isOpen: boolean; | ||
onClose: () => void; | ||
onSave: () => void; | ||
allUsers: UserType[]; | ||
} | ||
|
||
function getTableRows(allUsers: UserType[]): RowData[] { | ||
return allUsers.map((user) => { | ||
return { | ||
icon: user.image ?? "", | ||
name: user.fullName, | ||
userId: user.sId, | ||
}; | ||
}); | ||
} | ||
|
||
function getTableColumns( | ||
selectedMembers: string[], | ||
handleMemberToggle: (member: string) => void | ||
): ColumnDef<RowData, unknown>[] { | ||
return [ | ||
{ | ||
header: "Name", | ||
accessorKey: "name", | ||
id: "name", | ||
cell: (info: Info) => ( | ||
<DataTable.Cell avatarUrl={info.row.original.icon}> | ||
{info.row.original.name} | ||
</DataTable.Cell> | ||
), | ||
}, | ||
{ | ||
id: "action", | ||
cell: (info: Info) => { | ||
const isSelected = selectedMembers.includes(info.row.original.userId); | ||
return ( | ||
<Button | ||
label={isSelected ? "Remove" : "Add"} | ||
onClick={() => handleMemberToggle(info.row.original.userId)} | ||
variant={isSelected ? "tertiary" : "secondary"} | ||
size="sm" | ||
icon={isSelected ? MinusIcon : PlusIcon} | ||
/> | ||
); | ||
}, | ||
}, | ||
]; | ||
} | ||
|
||
export function CreateVaultModal({ | ||
owner, | ||
isOpen, | ||
onClose, | ||
onSave, | ||
allUsers, | ||
}: CreateVaultModalProps) { | ||
const [vaultName, setVaultName] = useState<string | null>(null); | ||
const [selectedMembers, setSelectedMembers] = useState<string[]>([]); | ||
const [searchTerm, setSearchTerm] = useState<string>(""); | ||
const [isSaving, setIsSaving] = useState(false); | ||
|
||
const sendNotification = useContext(SendNotificationsContext); | ||
|
||
const handleMemberToggle = useCallback((member: string) => { | ||
setSelectedMembers((prevSelectedMembers) => { | ||
const isCurrentlySelected = prevSelectedMembers.includes(member); | ||
if (isCurrentlySelected) { | ||
return prevSelectedMembers.filter((m) => m !== member); | ||
} else { | ||
return [...prevSelectedMembers, member]; | ||
} | ||
}); | ||
}, []); | ||
|
||
const createVault = async () => { | ||
setIsSaving(true); | ||
try { | ||
const res = await fetch(`/api/w/${owner.sId}/vaults`, { | ||
method: "POST", | ||
headers: { | ||
"Content-Type": "application/json", | ||
}, | ||
body: JSON.stringify({ | ||
name: vaultName, | ||
memberIds: selectedMembers, | ||
}), | ||
}); | ||
|
||
if (!res.ok) { | ||
const errorData = await res.json(); | ||
throw new Error(errorData.error?.message || "Failed to create vault"); | ||
} | ||
onSave(); | ||
onClose(); | ||
return await res.json(); | ||
} finally { | ||
setIsSaving(false); | ||
} | ||
}; | ||
|
||
const rows = useMemo( | ||
() => getTableRows(allUsers), | ||
[allUsers] | ||
); | ||
|
||
const columns = useMemo( | ||
() => getTableColumns(selectedMembers, handleMemberToggle), | ||
[selectedMembers, handleMemberToggle] | ||
); | ||
|
||
return ( | ||
<Modal | ||
isOpen={isOpen} | ||
onClose={onClose} | ||
title="Create a Vault" | ||
saveLabel="Create" | ||
variant="side-md" | ||
hasChanged={!!vaultName && selectedMembers.length > 0} | ||
isSaving={isSaving} | ||
onSave={async () => { | ||
try { | ||
await createVault(); | ||
sendNotification({ | ||
type: "success", | ||
title: "Successfully created vault", | ||
description: "Vault was successfully created.", | ||
}); | ||
} catch (e) { | ||
sendNotification({ | ||
type: "error", | ||
title: "Failed to create vault", | ||
description: | ||
"An unexpected error occurred while creating the vault.", | ||
}); | ||
} | ||
}} | ||
> | ||
<Page.Vertical gap="md"> | ||
<div className="mb-4 flex w-full max-w-xl flex-col gap-y-2 p-4"> | ||
<Page.SectionHeader title="Name" /> | ||
<Input | ||
placeholder="Vault's name" | ||
value={vaultName} | ||
name="vaultName" | ||
size="sm" | ||
onChange={(value) => setVaultName(value)} | ||
/> | ||
<div className="flex gap-1 text-xs text-element-700"> | ||
<Icon visual={InformationCircleIcon} size="xs" /> | ||
<span>Vault name must be unique</span> | ||
</div> | ||
</div> | ||
<div className="flex w-full flex-col gap-y-4 border-t p-4"> | ||
<Page.SectionHeader title="Vault members" /> | ||
<Searchbar | ||
name="search" | ||
placeholder="Search members" | ||
value={searchTerm} | ||
onChange={setSearchTerm} | ||
/> | ||
<DataTable | ||
data={rows} | ||
columns={columns} | ||
filterColumn="name" | ||
filter={searchTerm} | ||
/> | ||
</div> | ||
</Page.Vertical> | ||
</Modal> | ||
); | ||
} |