-
Notifications
You must be signed in to change notification settings - Fork 192
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1601 from rockingrohit9639/feature/import-csv-users
Feature/import csv users
- Loading branch information
Showing
12 changed files
with
692 additions
and
12 deletions.
There are no files selected for viewing
200 changes: 200 additions & 0 deletions
200
app/components/settings/import-users-dialog/import-users-dialog.tsx
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,200 @@ | ||
import { cloneElement, useState } from "react"; | ||
import { useNavigate } from "@remix-run/react"; | ||
import { UploadIcon } from "lucide-react"; | ||
import type { z } from "zod"; | ||
import useFetcherWithReset from "~/hooks/use-fetcher-with-reset"; | ||
import { isFormProcessing } from "~/utils/form"; | ||
import { tw } from "~/utils/tw"; | ||
import Input from "../../forms/input"; | ||
import { Dialog, DialogPortal } from "../../layout/dialog"; | ||
import { Button } from "../../shared/button"; | ||
import { WarningBox } from "../../shared/warning-box"; | ||
import When from "../../when/when"; | ||
import type { InviteUserFormSchema } from "../invite-user-dialog"; | ||
import ImportUsersSuccessContent from "./import-users-success-content"; | ||
|
||
type ImportUsersDialogProps = { | ||
className?: string; | ||
trigger?: React.ReactElement<{ onClick: () => void }>; | ||
}; | ||
|
||
type ImportUser = z.infer<typeof InviteUserFormSchema>; | ||
|
||
export type FetcherData = { | ||
error?: { message?: string }; | ||
success?: boolean; | ||
inviteSentUsers?: ImportUser[]; | ||
skippedUsers?: ImportUser[]; | ||
extraMessage?: string; | ||
}; | ||
|
||
export default function ImportUsersDialog({ | ||
className, | ||
trigger, | ||
}: ImportUsersDialogProps) { | ||
const [isDialogOpen, setIsDialogOpen] = useState(false); | ||
const [selectedFile, setSelectedFile] = useState<File>(); | ||
const [error, setError] = useState<string>(""); | ||
|
||
const navigate = useNavigate(); | ||
|
||
const fetcher = useFetcherWithReset<FetcherData>(); | ||
const disabled = isFormProcessing(fetcher.state); | ||
|
||
function openDialog() { | ||
setIsDialogOpen(true); | ||
} | ||
|
||
function closeDialog() { | ||
fetcher.reset(); | ||
setIsDialogOpen(false); | ||
} | ||
|
||
function handleSelectFile(event: React.ChangeEvent<HTMLInputElement>) { | ||
setError(""); | ||
|
||
const file = event.target.files?.[0]; | ||
if (file?.type !== "text/csv") { | ||
setError("Invalid file type. Please select a CSV file."); | ||
return; | ||
} | ||
|
||
setSelectedFile(file); | ||
} | ||
|
||
function goToInvites() { | ||
navigate("/settings/team/invites"); | ||
closeDialog(); | ||
} | ||
|
||
return ( | ||
<> | ||
{trigger ? ( | ||
cloneElement(trigger, { onClick: openDialog }) | ||
) : ( | ||
<Button | ||
variant="secondary" | ||
className="mt-2 w-full md:mt-0 md:w-max" | ||
onClick={openDialog} | ||
> | ||
<span className="whitespace-nowrap">Import Users</span> | ||
</Button> | ||
)} | ||
|
||
<DialogPortal> | ||
<Dialog | ||
className={tw( | ||
"h-[calc(100vh_-_50px)] overflow-auto", | ||
!fetcher.data?.success && "md:w-[calc(100vw_-_200px)]", | ||
className | ||
)} | ||
open={isDialogOpen} | ||
onClose={closeDialog} | ||
title={ | ||
<div className="mt-4 inline-flex items-center justify-center rounded-full border-4 border-solid border-primary-50 bg-primary-100 p-1.5 text-primary"> | ||
<UploadIcon /> | ||
</div> | ||
} | ||
> | ||
{fetcher.data?.success ? ( | ||
<ImportUsersSuccessContent | ||
data={fetcher.data} | ||
onClose={closeDialog} | ||
onViewInvites={goToInvites} | ||
/> | ||
) : ( | ||
<div className="px-6 pb-4 pt-2"> | ||
<h3>Invite Users via CSV Upload</h3> | ||
<p> | ||
Invite multiple users to your organization by uploading a CSV | ||
file. To get started,{" "} | ||
<Button | ||
variant="link" | ||
to="/static/shelf.nu-example-import-users-from-content.csv" | ||
target="_blank" | ||
download | ||
> | ||
download our CSV template. | ||
</Button> | ||
</p> | ||
<WarningBox className="my-4"> | ||
<> | ||
<strong>IMPORTANT</strong>: Use the provided template to | ||
ensure proper formatting. Uploading incorrectly formatted | ||
files may cause errors. | ||
</> | ||
</WarningBox> | ||
<h4>Base Rules and Limitations</h4> | ||
<ul className="list-inside list-disc"> | ||
<li> | ||
You must use <b>, (comma)</b> as a delimiter in your CSV file. | ||
</li> | ||
<li> | ||
Only valid roles are <b>ADMIN</b>, <b>BASE</b> and{" "} | ||
<b>SELF_SERVICE</b>. Role column is case-sensitive. | ||
</li> | ||
<li> | ||
Each row represents a new user to be invited. Ensure the email | ||
column is valid. | ||
</li> | ||
<li> | ||
Invited users will receive an email with a link to join the | ||
organization. | ||
</li> | ||
</ul> | ||
|
||
<h4 className="mt-2">Extra Considerations</h4> | ||
<ul className="mb-4 list-inside list-disc"> | ||
<li> | ||
The first row of the sheet will be ignored. Use it for column | ||
headers as in the provided template. | ||
</li> | ||
</ul> | ||
|
||
<p className="mb-4"> | ||
Once you've uploaded your file, a summary of the processed | ||
invitations will be displayed, along with any errors | ||
encountered. | ||
</p> | ||
|
||
<fetcher.Form | ||
action="/api/settings/import-users" | ||
method="POST" | ||
encType="multipart/form-data" | ||
> | ||
<Input | ||
inputType="textarea" | ||
label="Enter your message to user" | ||
name="message" | ||
className="mb-2" | ||
disabled={disabled} | ||
rows={5} | ||
/> | ||
|
||
<Input | ||
type="file" | ||
name="file" | ||
label="Select a csv file" | ||
required | ||
accept=".csv" | ||
className="mb-2" | ||
error={error} | ||
onChange={handleSelectFile} | ||
disabled={disabled} | ||
/> | ||
|
||
<When truthy={!!fetcher?.data?.error}> | ||
<p className="mb-2 text-sm text-error-500"> | ||
{fetcher.data?.error?.message} | ||
</p> | ||
</When> | ||
|
||
<Button disabled={!selectedFile || disabled}>Import now</Button> | ||
</fetcher.Form> | ||
</div> | ||
)} | ||
</Dialog> | ||
</DialogPortal> | ||
</> | ||
); | ||
} |
69 changes: 69 additions & 0 deletions
69
app/components/settings/import-users-dialog/import-users-success-content.tsx
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,69 @@ | ||
import { ClientOnly } from "remix-utils/client-only"; | ||
import { Button } from "~/components/shared/button"; | ||
import { WarningBox } from "~/components/shared/warning-box"; | ||
import When from "~/components/when/when"; | ||
import SuccessAnimation from "~/components/zxing-scanner/success-animation"; | ||
import { tw } from "~/utils/tw"; | ||
import type { FetcherData } from "./import-users-dialog"; | ||
import ImportUsersTable from "./import-users-table"; | ||
|
||
type ImportUsersSuccessContentProps = { | ||
className?: string; | ||
data: FetcherData; | ||
onClose: () => void; | ||
onViewInvites: () => void; | ||
}; | ||
|
||
export default function ImportUsersSuccessContent({ | ||
className, | ||
data, | ||
onClose, | ||
onViewInvites, | ||
}: ImportUsersSuccessContentProps) { | ||
return ( | ||
<div | ||
className={tw( | ||
"flex flex-col items-center justify-center px-6 pb-4 text-center", | ||
className | ||
)} | ||
> | ||
<ClientOnly fallback={null}>{() => <SuccessAnimation />}</ClientOnly> | ||
|
||
<h4>Import completed</h4> | ||
<p className="mb-4"> | ||
Users from the csv file has been invited. Below you can find a summary | ||
of the invited users. | ||
</p> | ||
|
||
<When truthy={!!data.extraMessage}> | ||
<WarningBox className="mb-4 w-full"> | ||
{data.extraMessage ?? ""} | ||
</WarningBox> | ||
</When> | ||
|
||
<When truthy={!!data?.inviteSentUsers?.length}> | ||
<ImportUsersTable | ||
className="mb-4" | ||
title="Invited users" | ||
users={data?.inviteSentUsers ?? []} | ||
/> | ||
</When> | ||
<When truthy={!!data?.skippedUsers?.length}> | ||
<ImportUsersTable | ||
className="mb-4" | ||
title="Skipped users" | ||
users={data?.skippedUsers ?? []} | ||
/> | ||
</When> | ||
|
||
<div className="flex w-full items-center gap-2"> | ||
<Button variant="secondary" onClick={onClose} className="flex-1"> | ||
Close | ||
</Button> | ||
<Button onClick={onViewInvites} className="flex-1"> | ||
View Invites | ||
</Button> | ||
</div> | ||
</div> | ||
); | ||
} |
51 changes: 51 additions & 0 deletions
51
app/components/settings/import-users-dialog/import-users-table.tsx
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,51 @@ | ||
import type { z } from "zod"; | ||
import { organizationRolesMap } from "~/routes/_layout+/settings.team"; | ||
import { tw } from "~/utils/tw"; | ||
import type { InviteUserFormSchema } from "../invite-user-dialog"; | ||
|
||
type ImportUsersTableProps = { | ||
className?: string; | ||
style?: React.CSSProperties; | ||
title: string; | ||
users: z.infer<typeof InviteUserFormSchema>[]; | ||
}; | ||
|
||
export default function ImportUsersTable({ | ||
className, | ||
style, | ||
title, | ||
users, | ||
}: ImportUsersTableProps) { | ||
return ( | ||
<div | ||
className={tw( | ||
"relative w-full overflow-x-auto rounded-md border", | ||
className | ||
)} | ||
style={style} | ||
> | ||
<h4 className="px-6 py-3 text-left">{title}</h4> | ||
|
||
<table className="w-full text-left text-sm"> | ||
<thead className="bg-gray-50 text-xs uppercase"> | ||
<tr> | ||
<th scope="col" className="px-6 py-3"> | ||
</th> | ||
<th scope="col" className="px-6 py-3"> | ||
Role | ||
</th> | ||
</tr> | ||
</thead> | ||
<tbody> | ||
{users.map((user) => ( | ||
<tr key={user.email}> | ||
<td className="px-6 py-4">{user.email}</td> | ||
<td className="px-6 py-4">{organizationRolesMap[user.role]}</td> | ||
</tr> | ||
))} | ||
</tbody> | ||
</table> | ||
</div> | ||
); | ||
} |
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
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
Oops, something went wrong.