Skip to content

Commit

Permalink
feat: implement insights team feature (#1080)
Browse files Browse the repository at this point in the history
* intial commit for this PR

* chore: update db types

* feat: create insight members hook

* chore: copy text updates and fix insights data error

* chore: validate email

* feat: setup team invite

* feat: add teams hook

* feat: update member role implementation

* chore: minor changes

* refactor: improve search component

* refactor: improve select filter for teams role

* feat: add toast for visual feedback

* chore: update teams crud

* refactor: minor adjustments

* fix: linting errors

* fix: select tick visibility

* refactor: minor adjustments

* fix: build error

* fix: update teams story

* refactor: update toast message

* chore: review changes

* chore: review changes

* fix: update story props

* chore: review changes
  • Loading branch information
OgDev-01 committed Apr 18, 2023
1 parent 08e652c commit 5186a60
Show file tree
Hide file tree
Showing 12 changed files with 328 additions and 97 deletions.
6 changes: 5 additions & 1 deletion components/atoms/Search/search.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState } from "react";
import React, { useEffect, useState } from "react";
import { GrClose } from "react-icons/gr";
import { FaSearch } from "react-icons/fa";
import { Spinner } from "../SpinLoader/spin-loader";
Expand Down Expand Up @@ -44,6 +44,10 @@ const Search = ({
onChange?.("");
};

useEffect(() => {
setSearch(value);
}, [value]);

const handleOnSelect = (suggestion: string) => {
setSearch(suggestion);
onSearch?.(suggestion);
Expand Down
61 changes: 27 additions & 34 deletions components/atoms/Selector/selector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,46 +2,39 @@ import Radio from "components/atoms/Radio/radio";
import RadioCheck from "../RadioCheck/radio-check";

interface SelectorProps {
filterOptions: string[];
filterOptions: { name: string; value: string }[];
handleFilterClick: (filter: string) => void;
selected?: string;
variation?: "circle" | "check"
variation?: "circle" | "check";
}

const Selector = ({
filterOptions,
handleFilterClick,
selected,
variation = "circle"
}: SelectorProps) => {
const Selector = ({ filterOptions, handleFilterClick, selected, variation = "circle" }: SelectorProps) => {
return (
<div className="mt-2 absolute transform md:translate-x-0 space-y-1 mt-1 shadow-superlative z-10 w-72 bg-white rounded-lg px-1.5 py-2">
<div className="mt-2 absolute transform md:translate-x-0 space-y-1 shadow-superlative z-10 w-72 bg-white rounded-lg px-1.5 py-2">
{filterOptions.length > 0 &&
filterOptions.map((option, index) => {
return (
variation === "circle" ? (
<Radio
key={index}
onClick={() => {
handleFilterClick(option);
}}
className="!w-full"
checked={selected === option ? true : false}
>
{option}
</Radio>
) : (
<RadioCheck
key={index}
onClick={() => {
handleFilterClick(option);
}}
className="!w-full"
checked={selected === option ? true : false}
>
{option}
</RadioCheck>
)
filterOptions.map(({ name, value }, index) => {
return variation === "circle" ? (
<Radio
key={index}
onClick={() => {
handleFilterClick(value);
}}
className="!w-full"
checked={selected === value ? true : false}
>
{name}
</Radio>
) : (
<RadioCheck
key={index}
onClick={() => {
handleFilterClick(value);
}}
className="!w-full"
checked={selected === value ? true : false}
>
{name}
</RadioCheck>
);
})}
</div>
Expand Down
46 changes: 27 additions & 19 deletions components/molecules/FilterCardSelect/filter-card-select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ import repoIcon from "../../../img/icons/repo.svg";
import Selector from "../../atoms/Selector/selector";

interface FilterCardSelectProps {
selected: string;
icon?: "topic" | "repo" | "org" | "contributor";
options: string[];
handleFilterClick: (filter: string) => void;
selected: string;
icon?: "topic" | "repo" | "org" | "contributor";
options: string[];
handleFilterClick: (filter: string) => void;
}

const icons = {
Expand All @@ -33,8 +33,12 @@ const icons = {
}
};

const FilterCardSelect: React.FC<FilterCardSelectProps> = ({ selected: filterName, icon = "topic", options, handleFilterClick }) => {

const FilterCardSelect: React.FC<FilterCardSelectProps> = ({
selected: filterName,
icon = "topic",
options,
handleFilterClick
}) => {
const ref = useRef<HTMLDivElement>(null);
const [isOpen, setIsOpen] = useState(false);
const toggleFilter = () => {
Expand Down Expand Up @@ -62,22 +66,26 @@ const FilterCardSelect: React.FC<FilterCardSelectProps> = ({ selected: filterNam
<div
onClick={toggleFilter}
ref={ref}
className={"inline-block py-1 border border-slate-300 outline-none hover:bg-slate-50 focus:ring-2 bg-slate-100 focus:ring-slate-300 rounded-lg cursor-pointer"}>
className={
"inline-block py-1 border border-slate-300 outline-none hover:bg-slate-50 focus:ring-2 bg-slate-100 focus:ring-slate-300 rounded-lg cursor-pointer"
}
>
<button className="flex items-center gap-1 mx-2">
<Image
width={14} height={14}
alt={icon === "topic" ? icons.topic.alt : icon === "org" ? icons.org.alt : icon === "contributor" ? icons.contributor.alt : icon === "repo" ? icons.repo.alt : "Icon"}
src={icon === "topic" ? icons.topic.src : icon === "org" ? icons.org.src : icon === "contributor" ? icons.contributor.src : icon === "repo" ? icons.repo.src : icons.topic.src} />
<Text className="!text-sm font-semibold tracking-tight !text-slate-900">
{filterName}
</Text>
width={14}
height={14}
alt={icons[icon] ? icons[icon].alt : "Icons"}
src={icons[icon] ? icons[icon].src : icons.topic.src}
/>
<Text className="!text-sm font-semibold tracking-tight !text-slate-900">{filterName}</Text>
</button>
{ isOpen && <Selector
filterOptions={options}
handleFilterClick={handleFilterClick}
selected={filterName}
/>
}
{isOpen && (
<Selector
filterOptions={options.map((option) => ({ name: option, value: option }))}
handleFilterClick={handleFilterClick}
selected={filterName}
/>
)}
</div>
</>
);
Expand Down
63 changes: 48 additions & 15 deletions components/molecules/TeamMemberRow/team-member-row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,43 +3,76 @@ import { AiOutlineCaretDown } from "react-icons/ai";
import pendingImg from "img/icons/fallback-image-disabled-square.svg";
import { useState } from "react";
import Selector from "components/atoms/Selector/selector";
import { TeamMemberData } from "../TeamMembersConfig/team-members-config";
import { MemberAccess, TeamMemberData } from "../TeamMembersConfig/team-members-config";

interface TeamMemberRowProps extends TeamMemberData {
className?: string;
onDelete: (memberId: string) => void;
onUpdate: (memberId: string, access: MemberAccess) => Promise<any> | undefined;
}

const mapRoleToText: Record<TeamMemberRowProps["role"], string> = {
const mapRoleToText: Record<TeamMemberRowProps["access"], string> = {
owner: "Owner",
admin: "Admin",
editor: "can edit",
viewer: "can view",
edit: "can edit",
view: "can view",
pending: "Pending"
};

const TeamMemberRow = ({ className, name, avatarUrl, role }: TeamMemberRowProps) => {

const TeamMemberRow = ({ className, name, avatarUrl, access, email, onDelete, onUpdate, id }: TeamMemberRowProps) => {
const [isMenuOpen, setIsMenuOpen] = useState(false);

const pending = role == "pending";
const pending = access == "pending";
const isOwner = access == "owner";

const handleRoleChange = (role: string) => {};
const handleRoleChange = async (role: string) => {
setIsMenuOpen(false);
if (role === "remove") {
onDelete(id);
} else {
onUpdate(id, role as MemberAccess);
}
};

return(
return (
<div className={`flex justify-between items-center ${className && className} ${pending && "text-light-slate-10"}`}>
<div className="flex items-center">
<div className="flex items-center gap-3">
<Avatar size={40} isCircle avatarURL={pending ? pendingImg : avatarUrl} />
<p className="ml-3">{name}</p>
<p>{name || email}</p>
</div>
<div>
<div className="flex items-center gap-3">
{mapRoleToText[role]} {!pending && <AiOutlineCaretDown onClick={() => {setIsMenuOpen(!isMenuOpen);}} />}
{mapRoleToText[access]}
{!pending && (
<>
{isOwner ? (
<AiOutlineCaretDown />
) : (
<AiOutlineCaretDown
className="cursor-pointer"
onClick={() => {
setIsMenuOpen(!isMenuOpen);
}}
/>
)}
</>
)}
</div>
{ !pending && isMenuOpen && (
<Selector filterOptions={["Admin", "can edit", "can view"]} selected={mapRoleToText[role]} variation="check" handleFilterClick={handleRoleChange} />
{!pending && isMenuOpen && (
<Selector
filterOptions={[
{ name: "Admin", value: "admin" },
{ name: "can edit", value: "edit" },
{ name: "can view", value: "view" },
{ name: "remove", value: "remove" }
]}
selected={access}
variation="check"
handleFilterClick={handleRoleChange}
/>
)}
</div>
</div>
);

};
export default TeamMemberRow;
78 changes: 69 additions & 9 deletions components/molecules/TeamMembersConfig/team-members-config.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,90 @@
import Button from "components/atoms/Button/button";
import Search from "components/atoms/Search/search";
import TeamMemberRow from "../TeamMemberRow/team-member-row";
import { useState } from "react";
import { validateEmail } from "lib/utils/validate-email";
import { useToast } from "lib/hooks/useToast";

interface TeamMembersConfigProps {
className?: string;
members: TeamMemberData[];
onAddMember: (email: string) => Promise<any> | undefined;
onDeleteMember: (memberId: string) => void;
onUpdateMember: (memberId: string, access: MemberAccess) => Promise<any>;
}

export type MemberAccess = "owner" | "pending" | "admin" | "edit" | "view";
export interface TeamMemberData {
id: string;
insight_id: number;
user_id?: number;
name: string;
access: MemberAccess;
avatarUrl: string;
role: "admin" | "editor" | "viewer" | "pending";
email?: string;
}

const TeamMembersConfig = ({ className, members }: TeamMembersConfigProps) => {
const TeamMembersConfig = ({
className,
members,
onAddMember,
onDeleteMember,
onUpdateMember
}: TeamMembersConfigProps) => {
const [validInput, setValidInput] = useState(false);
const [email, setEmail] = useState("");
const [loading, setLoading] = useState(false);
const { toast } = useToast();

const handleChange = async (value: string) => {
setEmail(value);

setValidInput(!!validateEmail(value));
};

const handleAddMember = async () => {
const memberExists = members.find((member) => member.email?.toLowerCase() === email.toLowerCase());

if (memberExists) {
toast({ description: "Member already exists", variant: "danger" });
return;
}
setLoading(true);
const res = await onAddMember(email);
setLoading(false);
setEmail("");
if (res) {
toast({ description: "Member invite sent successfully", variant: "success" });
} else {
toast({ description: "An error occurred!", variant: "danger" });
}
};

return (
<div className={`max-w-xl ${className && className}`}>
<h2 className="font-medium text-base">Add Team Members</h2>
<div className="flex justify-between items-center">
<Search name="search" className="w-4/5" />
<Button variant="primary" className="h-7 flex items-center">Send Invite</Button>
<div className={` ${className && className}`}>
<h2 className="text-lg font-medium tracking-wide">Add Team Members</h2>
<div className="flex items-center gap-5 mt-3">
<Search
isLoading={loading}
value={email}
onChange={(value) => handleChange(value)}
placeholder="Enter email address"
name="search"
className="flex-1 text-base"
/>
<Button onClick={handleAddMember} disabled={!validInput} variant="primary" className="flex items-center h-7">
Send Invite
</Button>
</div>
<div className="mt-7">
{members.map(member => (
<TeamMemberRow key={member.name + member.avatarUrl} className="mb-4" {...member} />
{members.map((member) => (
<TeamMemberRow
onUpdate={onUpdateMember}
onDelete={onDeleteMember}
key={member.id}
className="mb-4"
{...member}
/>
))}
</div>
</div>
Expand Down
Loading

0 comments on commit 5186a60

Please sign in to comment.