Skip to content

Commit

Permalink
fix: members tab
Browse files Browse the repository at this point in the history
  • Loading branch information
Security2431 committed Apr 12, 2024
1 parent ff4cfeb commit abe60ff
Show file tree
Hide file tree
Showing 5 changed files with 320 additions and 6 deletions.
129 changes: 123 additions & 6 deletions apps/nextjs/src/_components/settings/members-tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { useState } from "react";
import { useParams } from "next/navigation";
import * as z from "zod";

import { Role } from "@acme/db";
import { Avatar, AvatarFallback, AvatarImage } from "@acme/ui/avatar";
Expand All @@ -21,9 +22,29 @@ import {
CommandItem,
CommandList,
} from "@acme/ui/command";
import {
Dialog,
DialogClose,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@acme/ui/dialog";
import {
Form,
FormField,
FormItem,
FormLabel,
FormMessage,
useForm,
} from "@acme/ui/form";
import { Icons } from "@acme/ui/icons";
import { MultiSelect } from "@acme/ui/multiselect";
import { Popover, PopoverContent, PopoverTrigger } from "@acme/ui/popover";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@acme/ui/tabs";
import { toast } from "@acme/ui/toast";
import { CreateMembersSchema } from "@acme/validators";

import { getAvatarFallback } from "~/_utils/common";
import { PERMISSION_LIST } from "~/_utils/constants";
Expand All @@ -32,10 +53,16 @@ import { api } from "~/trpc/react";

export function MembersTab() {
const utils = api.useUtils();
const [role, setRole] = useState<(typeof PERMISSION_LIST)[number]>();
const params = useParams<{ id: string }>();
const { data: permissions } = api.workspacesMembers.all.useQuery(params.id);

const form = useForm({
schema: CreateMembersSchema,
defaultValues: {
emails: [],
},
});

const updatePermission = api.workspacesMembers.update.useMutation({
async onSuccess() {
toast.success("Your workspace permissions updated successfully!");
Expand All @@ -51,13 +78,103 @@ export function MembersTab() {
},
});

const onSubmit = async (data: z.infer<typeof CreateMembersSchema>) => {};

return (
<Card>
<CardHeader>
<CardTitle>Team Members</CardTitle>
<CardDescription>
Invite your team members to collaborate.
</CardDescription>
<CardHeader className="flex flex-row items-center justify-between">
<div>
<CardTitle>Team Members</CardTitle>
<CardDescription>
Invite your team members to collaborate.
</CardDescription>
</div>
<Dialog>
<DialogTrigger asChild>
<Button size="sm" className="ml-auto h-7 gap-1">
<Icons.PlusCircle className="h-3.5 w-3.5" />
<span className="sr-only sm:not-sr-only sm:whitespace-nowrap">
Invite Members
</span>
</Button>
</DialogTrigger>
<DialogContent className="max-w-5xl">
<DialogHeader>
<DialogTitle className="text-center">Invite members</DialogTitle>
</DialogHeader>
<Tabs defaultValue="publicLink">
<TabsList>
<TabsTrigger value="publicLink">Public link</TabsTrigger>
<TabsTrigger value="personalEmail">Personal email</TabsTrigger>
</TabsList>

<TabsContent value="publicLink">
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<FormField
control={form.control}
name="emails"
render={({ field }) => (
<FormItem>
<FormLabel>Select Frameworks</FormLabel>
<MultiSelect
selected={field.value}
options={[
{
value: "next.js",
label: "Next.js",
},
{
value: "sveltekit",
label: "SvelteKit",
},
{
value: "nuxt.js",
label: "Nuxt.js",
},
{
value: "remix",
label: "Remix",
},
{
value: "astro",
label: "Astro",
},
{
value: "wordpress",
label: "WordPress",
},
{
value: "express.js",
label: "Express.js",
},
]}
{...field}
className="sm:w-[510px]"
/>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter className="mt-6 sm:justify-start">
<Button
type="submit"
// disabled={
// updateSprint.isPending ||
// createSprint.isPending ||
// deleteReports.isPending
// }
>
Save
</Button>
</DialogFooter>
</form>
</Form>
</TabsContent>
<TabsContent value="personalEmail"></TabsContent>
</Tabs>
</DialogContent>
</Dialog>
</CardHeader>
<CardContent className="grid gap-6">
{permissions?.map(({ id, user, permission }) => (
Expand Down
2 changes: 2 additions & 0 deletions packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
".": "./src/index.ts",
"./alert-dialog": "./src/alert-dialog.tsx",
"./avatar": "./src/avatar.tsx",
"./badge": "./src/badge.tsx",
"./button": "./src/button.tsx",
"./card": "./src/card.tsx",
"./command": "./src/command.tsx",
Expand All @@ -16,6 +17,7 @@
"./icons": "./src/icons.tsx",
"./input": "./src/input.tsx",
"./label": "./src/label.tsx",
"./multiselect": "./src/multiselect.tsx",
"./popover": "./src/popover.tsx",
"./radio-group": "./src/radio-group.tsx",
"./scroll-area": "./src/scroll-area.tsx",
Expand Down
37 changes: 37 additions & 0 deletions packages/ui/src/badge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { VariantProps } from "class-variance-authority";
import * as React from "react";
import { cva } from "class-variance-authority";

import { cn } from ".";

const badgeVariants = cva(
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
},
);

export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}

function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
);
}

export { Badge, badgeVariants };
154 changes: 154 additions & 0 deletions packages/ui/src/multiselect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
"use client";

import * as React from "react";

import { cn } from ".";
import { Badge } from "./badge";
import { Button } from "./button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandSeparator,
} from "./command";
import { Icons } from "./icons";
import { Input } from "./input";
import { Popover, PopoverContent, PopoverTrigger } from "./popover";

export type OptionType = {
label: string;
value: string;
};

interface MultiSelectProps {
options: OptionType[];
selected: string[];
onChange: React.Dispatch<React.SetStateAction<string[]>>;
className?: string;
}

function MultiSelect({
options,
selected,
onChange,
className,
...props
}: MultiSelectProps) {
const [open, setOpen] = React.useState(false);

const handleUnselect = (item: string) => {
onChange(selected.filter((i) => i !== item));
};

const [newOption, setNewOption] = React.useState("");

const handleNewOptionEntry = (e: React.ChangeEvent<HTMLInputElement>) => {
setNewOption(e.target.value);
};

const handleNewOptionSubmit = () => {
if (newOption) {
options.push({ label: newOption, value: newOption });
onChange(
selected.includes(newOption)
? selected.filter((item) => item !== newOption)
: [...selected, newOption],
);
setNewOption("");
setOpen(true);
}
};

return (
<Popover open={open} onOpenChange={setOpen} {...props}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className={`w-full justify-between ${
selected.length > 1 ? "h-full" : "h-10"
}`}
onClick={() => setOpen(!open)}
>
<div className="flex flex-wrap gap-1">
{selected.map((item) => (
<Badge
variant="secondary"
key={item}
className="mb-1 mr-1"
onClick={() => handleUnselect(item)}
>
{item}
<button
className="ml-1 rounded-full outline-none ring-offset-background focus:ring-2 focus:ring-ring focus:ring-offset-2"
onKeyDown={(e) => {
if (e.key === "Enter") {
handleUnselect(item);
}
}}
onMouseDown={(e) => {
e.preventDefault();
e.stopPropagation();
}}
onClick={() => handleUnselect(item)}
>
<Icons.X className="h-3 w-3 text-muted-foreground hover:text-foreground" />
</button>
</Badge>
))}
</div>
<Icons.ChevronsUpDown className="h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-full p-0">
<Command className={className}>
<CommandInput placeholder="Search ..." />
<CommandEmpty>No item found.</CommandEmpty>
<CommandGroup className="max-h-64 overflow-auto">
{options.map((option) => (
<CommandItem
key={option.value}
onSelect={() => {
onChange(
selected.includes(option.value)
? selected.filter((item) => item !== option.value)
: [...selected, option.value],
);
setOpen(true);
}}
>
<Icons.Check
className={cn(
"mr-2 h-4 w-4",
selected.includes(option.value)
? "opacity-100"
: "opacity-0",
)}
/>
{option.label}
</CommandItem>
))}
</CommandGroup>
<CommandSeparator />
<CommandGroup>
<div className="flex gap-1">
<Input
placeholder="other tags"
value={newOption}
onChange={handleNewOptionEntry}
/>
<Button variant="ghost" onClick={handleNewOptionSubmit}>
<Icons.PlusCircleIcon />
</Button>
</div>
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
);
}

export { MultiSelect };
4 changes: 4 additions & 0 deletions packages/validators/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,7 @@ export const CreateProjectSchema = z.object({
export const AuthLoginSchema = z.object({
email: z.string().min(1),
});

export const CreateMembersSchema = z.object({
emails: z.array(z.record(z.string().trim())),
});

0 comments on commit abe60ff

Please sign in to comment.