Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement insights team feature #1080

Merged
merged 27 commits into from
Apr 18, 2023
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
d03766d
intial commit for this PR
OgDev-01 Apr 5, 2023
8e70e61
chore: update db types
OgDev-01 Apr 6, 2023
e381c0f
feat: create insight members hook
OgDev-01 Apr 6, 2023
2c2b907
chore: copy text updates and fix insights data error
OgDev-01 Apr 6, 2023
9ba88c6
chore: validate email
OgDev-01 Apr 10, 2023
0a28784
Merge branch 'beta' of https://github.com/open-sauced/insights into 9…
OgDev-01 Apr 13, 2023
2d3489d
feat: setup team invite
OgDev-01 Apr 14, 2023
a2008bf
feat: add teams hook
OgDev-01 Apr 14, 2023
a5548b0
Merge branch 'beta' of https://github.com/open-sauced/insights into 9…
OgDev-01 Apr 14, 2023
14e12ce
feat: update member role implementation
OgDev-01 Apr 15, 2023
b59bab4
chore: minor changes
OgDev-01 Apr 15, 2023
377b3c2
refactor: improve search component
OgDev-01 Apr 17, 2023
99960a8
refactor: improve select filter for teams role
OgDev-01 Apr 17, 2023
8b92d73
feat: add toast for visual feedback
OgDev-01 Apr 17, 2023
35eb88a
chore: update teams crud
OgDev-01 Apr 17, 2023
b4849d2
refactor: minor adjustments
OgDev-01 Apr 17, 2023
d5f923e
fix: linting errors
OgDev-01 Apr 17, 2023
e3d97ee
fix: select tick visibility
OgDev-01 Apr 17, 2023
ffc079c
Merge branch 'beta' of https://github.com/open-sauced/insights into 9…
OgDev-01 Apr 17, 2023
634c310
refactor: minor adjustments
OgDev-01 Apr 17, 2023
7f4b33d
fix: build error
OgDev-01 Apr 17, 2023
1f49082
fix: update teams story
OgDev-01 Apr 17, 2023
c9c0e94
refactor: update toast message
OgDev-01 Apr 17, 2023
df18bcc
chore: review changes
OgDev-01 Apr 17, 2023
571e273
chore: review changes
OgDev-01 Apr 18, 2023
8226832
fix: update story props
OgDev-01 Apr 18, 2023
9afe063
chore: review changes
OgDev-01 Apr 18, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
66 changes: 47 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,46 @@ 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={
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
OgDev-01 marked this conversation as resolved.
Show resolved Hide resolved
}
/>
<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
61 changes: 47 additions & 14 deletions components/molecules/TeamMemberRow/team-member-row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,39 +7,72 @@ import { TeamMemberData } from "../TeamMembersConfig/team-members-config";

interface TeamMemberRowProps extends TeamMemberData {
className?: string;
onDelete: Function;
onUpdate: Function;
OgDev-01 marked this conversation as resolved.
Show resolved Hide resolved
}

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") {
OgDev-01 marked this conversation as resolved.
Show resolved Hide resolved
onDelete(id);
} else {
onUpdate(id, role);
}
};

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;
77 changes: 68 additions & 9 deletions components/molecules/TeamMembersConfig/team-members-config.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,89 @@
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: Function;
onDeleteMember: Function;
onUpdateMember: Function;
OgDev-01 marked this conversation as resolved.
Show resolved Hide resolved
}

export interface TeamMemberData {
id: number;
insight_id: number;
user_id?: number;
name: string;
access: "owner" | "pending" | "admin" | "edit" | "view";
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