|
|
@@ -1,4 +1,10 @@
|
|
|
-import React, { Component } from "react";
|
|
|
+import React, {
|
|
|
+ Component,
|
|
|
+ useState,
|
|
|
+ useEffect,
|
|
|
+ useContext,
|
|
|
+ useMemo,
|
|
|
+} from "react";
|
|
|
import styled from "styled-components";
|
|
|
|
|
|
import { InviteType } from "shared/types";
|
|
|
@@ -10,36 +16,26 @@ import InputRow from "components/values-form/InputRow";
|
|
|
import Helper from "components/values-form/Helper";
|
|
|
import Heading from "components/values-form/Heading";
|
|
|
import CopyToClipboard from "components/CopyToClipboard";
|
|
|
+import { Column } from "react-table";
|
|
|
+import Table from "components/Table";
|
|
|
|
|
|
-type PropsType = {};
|
|
|
+type Props = {};
|
|
|
|
|
|
-type StateType = {
|
|
|
- loading: boolean;
|
|
|
- invites: InviteType[];
|
|
|
- email: string;
|
|
|
- invalidEmail: boolean;
|
|
|
- isHTTPS: boolean;
|
|
|
-};
|
|
|
-
|
|
|
-const dummyInvites = [];
|
|
|
+const InvitePage: React.FunctionComponent<Props> = ({}) => {
|
|
|
+ const { currentProject } = useContext(Context);
|
|
|
+ const [isLoading, setIsLoading] = useState(true);
|
|
|
+ const [invites, setInvites] = useState<Array<InviteType>>([]);
|
|
|
+ const [email, setEmail] = useState("");
|
|
|
+ const [isInvalidEmail, setIsInvalidEmail] = useState(false);
|
|
|
+ const [isHTTPS] = useState(() => window.location.protocol === "https:");
|
|
|
|
|
|
-export default class InviteList extends Component<PropsType, StateType> {
|
|
|
- state = {
|
|
|
- loading: true,
|
|
|
- invites: [] as InviteType[],
|
|
|
- email: "",
|
|
|
- invalidEmail: false,
|
|
|
- isHTTPS: window.location.protocol === "https:",
|
|
|
- };
|
|
|
-
|
|
|
- componentDidMount() {
|
|
|
- this.getInviteData();
|
|
|
- }
|
|
|
+ useEffect(() => {
|
|
|
+ getInviteData();
|
|
|
+ }, []);
|
|
|
|
|
|
- getInviteData = () => {
|
|
|
- let { currentProject } = this.context;
|
|
|
+ const getInviteData = () => {
|
|
|
+ setIsLoading(true);
|
|
|
|
|
|
- this.setState({ loading: true });
|
|
|
api
|
|
|
.getInvites(
|
|
|
"<token>",
|
|
|
@@ -48,206 +44,220 @@ export default class InviteList extends Component<PropsType, StateType> {
|
|
|
id: currentProject.id,
|
|
|
}
|
|
|
)
|
|
|
- .then((res) => this.setState({ invites: res.data, loading: false }))
|
|
|
+ .then((res) => {
|
|
|
+ setInvites(res.data);
|
|
|
+ setIsLoading(false);
|
|
|
+ })
|
|
|
.catch((err) => console.log(err));
|
|
|
};
|
|
|
|
|
|
- validateEmail = () => {
|
|
|
- var regex = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
|
|
- if (regex.test(this.state.email.toLowerCase())) {
|
|
|
- this.setState({ invalidEmail: false });
|
|
|
- this.createInvite();
|
|
|
- } else {
|
|
|
- this.setState({ invalidEmail: true });
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- createInvite = () => {
|
|
|
- let { currentProject } = this.context;
|
|
|
+ const createInvite = () => {
|
|
|
api
|
|
|
- .createInvite(
|
|
|
- "<token>",
|
|
|
- { email: this.state.email },
|
|
|
- { id: currentProject.id }
|
|
|
- )
|
|
|
- .then((_) => {
|
|
|
- this.getInviteData();
|
|
|
- this.setState({ email: "" });
|
|
|
+ .createInvite("<token>", { email }, { id: currentProject.id })
|
|
|
+ .then(() => {
|
|
|
+ getInviteData();
|
|
|
+ setEmail("");
|
|
|
})
|
|
|
.catch((err) => console.log(err));
|
|
|
};
|
|
|
|
|
|
- deleteInvite = (index: number) => {
|
|
|
- let { currentProject } = this.context;
|
|
|
+ const deleteInvite = (inviteId: number) => {
|
|
|
api
|
|
|
.deleteInvite(
|
|
|
"<token>",
|
|
|
{},
|
|
|
{
|
|
|
id: currentProject.id,
|
|
|
- invId: this.state.invites[index].id,
|
|
|
+ invId: inviteId,
|
|
|
}
|
|
|
)
|
|
|
- .then(this.getInviteData)
|
|
|
+ .then(getInviteData)
|
|
|
.catch((err) => console.log(err));
|
|
|
};
|
|
|
|
|
|
- replaceInvite = (index: number) => {
|
|
|
- let { currentProject } = this.context;
|
|
|
+ const replaceInvite = (inviteEmail: string, inviteId: number) => {
|
|
|
api
|
|
|
.createInvite(
|
|
|
"<token>",
|
|
|
- { email: this.state.invites[index].email },
|
|
|
+ { email: inviteEmail },
|
|
|
{ id: currentProject.id }
|
|
|
)
|
|
|
- .then((_) =>
|
|
|
+ .then(() =>
|
|
|
api.deleteInvite(
|
|
|
"<token>",
|
|
|
{},
|
|
|
{
|
|
|
id: currentProject.id,
|
|
|
- invId: this.state.invites[index].id,
|
|
|
+ invId: inviteId,
|
|
|
}
|
|
|
)
|
|
|
)
|
|
|
- .then(this.getInviteData)
|
|
|
+ .then(getInviteData)
|
|
|
.catch((err) => console.log(err));
|
|
|
};
|
|
|
|
|
|
- getInviteUrl = (index: number) => {
|
|
|
- let { currentProject } = this.context;
|
|
|
- return `${this.state.isHTTPS ? "https://" : ""}${
|
|
|
- window.location.host
|
|
|
- }/api/projects/${currentProject.id}/invites/${
|
|
|
- this.state.invites[index].token
|
|
|
- }`;
|
|
|
+ const validateEmail = () => {
|
|
|
+ const regex = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
|
|
+ if (!regex.test(email.toLowerCase())) {
|
|
|
+ setIsInvalidEmail(true);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ setIsInvalidEmail(false);
|
|
|
+ createInvite();
|
|
|
};
|
|
|
|
|
|
- renderInvitations = () => {
|
|
|
- let { currentProject } = this.context;
|
|
|
- if (this.state.loading) {
|
|
|
- return <Loading />;
|
|
|
- } else {
|
|
|
- var invContent: any[] = [];
|
|
|
- var collabList: any[] = [];
|
|
|
- this.state.invites.sort((a: any, b: any) => (a.email > b.email ? 1 : -1));
|
|
|
- this.state.invites.sort((a: any, b: any) =>
|
|
|
- a.accepted > b.accepted ? 1 : -1
|
|
|
- );
|
|
|
- for (let i = 0; i < this.state.invites.length; i++) {
|
|
|
- if (this.state.invites[i].accepted) {
|
|
|
- collabList.push(
|
|
|
- <Tr key={i}>
|
|
|
- <MailTd isTop={i === 0}>{this.state.invites[i].email}</MailTd>
|
|
|
- <LinkTd isTop={i === 0}></LinkTd>
|
|
|
- <Td isTop={i === 0}>
|
|
|
- <CopyButton invis={true}>Remove</CopyButton>
|
|
|
- </Td>
|
|
|
- </Tr>
|
|
|
+ const columns = useMemo<
|
|
|
+ Column<{
|
|
|
+ email: string;
|
|
|
+ id: number;
|
|
|
+ status: string;
|
|
|
+ invite_link: string;
|
|
|
+ }>[]
|
|
|
+ >(
|
|
|
+ () => [
|
|
|
+ {
|
|
|
+ Header: "Mail address",
|
|
|
+ accessor: "email",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ Header: "Status",
|
|
|
+ accessor: "status",
|
|
|
+ Cell: ({ row }) => {
|
|
|
+ return (
|
|
|
+ <Status status={row.values.status}>{row.values.status}</Status>
|
|
|
);
|
|
|
- } else if (this.state.invites[i].expired) {
|
|
|
- invContent.push(
|
|
|
- <Tr key={i}>
|
|
|
- <MailTd isTop={i === 0}>{this.state.invites[i].email}</MailTd>
|
|
|
- <LinkTd isTop={i === 0}>
|
|
|
- <Rower>
|
|
|
- Link Expired.
|
|
|
- <NewLinkButton onClick={() => this.replaceInvite(i)}>
|
|
|
- <u>Generate a new link</u>
|
|
|
- </NewLinkButton>
|
|
|
- </Rower>
|
|
|
- </LinkTd>
|
|
|
- <Td isTop={i === 0}>
|
|
|
- <CopyButton onClick={() => this.deleteInvite(i)}>
|
|
|
- Delete Invite
|
|
|
- </CopyButton>
|
|
|
- </Td>
|
|
|
- </Tr>
|
|
|
+ },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ Header: "Invite link",
|
|
|
+ accessor: "invite_link",
|
|
|
+ Cell: ({ row }) => {
|
|
|
+ if (row.values.status === "expired") {
|
|
|
+ return (
|
|
|
+ <NewLinkButton
|
|
|
+ onClick={() => replaceInvite(row.values.email, row.values.id)}
|
|
|
+ >
|
|
|
+ <u>Generate a new link</u>
|
|
|
+ </NewLinkButton>
|
|
|
+ );
|
|
|
+ }
|
|
|
+ if (row.values.status === "accepted") {
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+
|
|
|
+ return (
|
|
|
+ <>
|
|
|
+ <CopyToClipboard as={Url} text={row.values.invite_link}>
|
|
|
+ <span>{row.values.invite_link}</span>
|
|
|
+ <i className="material-icons-outlined">content_copy</i>
|
|
|
+ </CopyToClipboard>
|
|
|
+ </>
|
|
|
);
|
|
|
- } else {
|
|
|
- invContent.push(
|
|
|
- <Tr key={i}>
|
|
|
- <MailTd isTop={i === 0}>{this.state.invites[i].email}</MailTd>
|
|
|
- <LinkTd isTop={i === 0}>
|
|
|
- <Rower>
|
|
|
- <ShareLink
|
|
|
- disabled={true}
|
|
|
- type="string"
|
|
|
- value={`${this.state.isHTTPS ? "https://" : ""}${
|
|
|
- window.location.host
|
|
|
- }/api/projects/${currentProject.id}/invites/${
|
|
|
- this.state.invites[i].token
|
|
|
- }`}
|
|
|
- placeholder="Unable to retrieve link"
|
|
|
- />
|
|
|
- <CopyToClipboard
|
|
|
- as={CopyButton}
|
|
|
- text={this.getInviteUrl(i)}
|
|
|
- onError={() => console.log("Couldn't copy to clipboard")}
|
|
|
- >
|
|
|
- Copy Link
|
|
|
- </CopyToClipboard>
|
|
|
- </Rower>
|
|
|
- </LinkTd>
|
|
|
- <Td isTop={i === 0}>
|
|
|
- <CopyButton onClick={() => this.deleteInvite(i)}>
|
|
|
- Delete Invite
|
|
|
- </CopyButton>
|
|
|
- </Td>
|
|
|
- </Tr>
|
|
|
+ },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ accessor: "id",
|
|
|
+ Cell: ({ row }) => {
|
|
|
+ if (row.values.status === "accepted") {
|
|
|
+ return <CopyButton invis={true}>Remove</CopyButton>;
|
|
|
+ }
|
|
|
+ return (
|
|
|
+ <>
|
|
|
+ <CopyButton onClick={() => deleteInvite(row.values.id)}>
|
|
|
+ Delete Invite
|
|
|
+ </CopyButton>
|
|
|
+ </>
|
|
|
);
|
|
|
+ },
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ []
|
|
|
+ );
|
|
|
+
|
|
|
+ const data = useMemo(() => {
|
|
|
+ const inviteList = [...invites];
|
|
|
+ inviteList.sort((a: any, b: any) => (a.email > b.email ? 1 : -1));
|
|
|
+ inviteList.sort((a: any, b: any) => (a.accepted > b.accepted ? 1 : -1));
|
|
|
+ const buildInviteLink = (token: string) => `
|
|
|
+ ${isHTTPS ? "https://" : ""}${window.location.host}/api/projects/${
|
|
|
+ currentProject.id
|
|
|
+ }/invites/${token}
|
|
|
+ `;
|
|
|
+
|
|
|
+ const mappedInviteList = inviteList.map(
|
|
|
+ ({ accepted, expired, token, ...rest }) => {
|
|
|
+ if (accepted) {
|
|
|
+ return {
|
|
|
+ status: "accepted",
|
|
|
+ invite_link: buildInviteLink(token),
|
|
|
+ ...rest,
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!accepted && expired) {
|
|
|
+ return {
|
|
|
+ status: "expired",
|
|
|
+ invite_link: buildInviteLink(token),
|
|
|
+ ...rest,
|
|
|
+ };
|
|
|
}
|
|
|
+
|
|
|
+ return {
|
|
|
+ status: "pending",
|
|
|
+ invite_link: buildInviteLink(token),
|
|
|
+ ...rest,
|
|
|
+ };
|
|
|
}
|
|
|
+ );
|
|
|
|
|
|
- return (
|
|
|
- <>
|
|
|
- <Heading>Invites & Collaborators</Heading>
|
|
|
- <Helper>Manage pending invites and view collaborators.</Helper>
|
|
|
- {invContent.length > 0 || collabList.length > 0 ? (
|
|
|
- <Table>
|
|
|
- <tbody>
|
|
|
- {invContent}
|
|
|
- {collabList}
|
|
|
- </tbody>
|
|
|
- </Table>
|
|
|
- ) : (
|
|
|
- <Placeholder>
|
|
|
- This project currently has no invites or collaborators.
|
|
|
- </Placeholder>
|
|
|
- )}
|
|
|
- </>
|
|
|
- );
|
|
|
- }
|
|
|
- };
|
|
|
+ return mappedInviteList || [];
|
|
|
+ }, [invites, currentProject?.id, window?.location?.host, isHTTPS]);
|
|
|
|
|
|
- render() {
|
|
|
- return (
|
|
|
- <>
|
|
|
- <Heading isAtTop={true}>Share Project</Heading>
|
|
|
- <Helper>Generate a project invite for another admin user.</Helper>
|
|
|
- <DarkMatter />
|
|
|
+ return (
|
|
|
+ <>
|
|
|
+ <Heading isAtTop={true}>Share Project</Heading>
|
|
|
+ <Helper>Generate a project invite for another admin user.</Helper>
|
|
|
+ <InputRowWrapper>
|
|
|
<InputRow
|
|
|
- value={this.state.email}
|
|
|
+ value={email}
|
|
|
type="text"
|
|
|
- setValue={(x: string) => this.setState({ email: x })}
|
|
|
- width="calc(100%)"
|
|
|
+ setValue={(newEmail: string) => setEmail(newEmail)}
|
|
|
+ width="100%"
|
|
|
placeholder="ex: mrp@getporter.dev"
|
|
|
/>
|
|
|
- <ButtonWrapper>
|
|
|
- <InviteButton disabled={false} onClick={() => this.validateEmail()}>
|
|
|
- Create Invite
|
|
|
- </InviteButton>
|
|
|
- {this.state.invalidEmail && (
|
|
|
- <Invalid>Invalid email address. Please try again.</Invalid>
|
|
|
- )}
|
|
|
- </ButtonWrapper>
|
|
|
- {this.renderInvitations()}
|
|
|
- </>
|
|
|
- );
|
|
|
- }
|
|
|
-}
|
|
|
+ </InputRowWrapper>
|
|
|
+ <ButtonWrapper>
|
|
|
+ <InviteButton disabled={false} onClick={() => validateEmail()}>
|
|
|
+ Create Invite
|
|
|
+ </InviteButton>
|
|
|
+ {isInvalidEmail && (
|
|
|
+ <Invalid>Invalid email address. Please try again.</Invalid>
|
|
|
+ )}
|
|
|
+ </ButtonWrapper>
|
|
|
+
|
|
|
+ <Heading>Invites & Collaborators</Heading>
|
|
|
+ <Helper>Manage pending invites and view collaborators.</Helper>
|
|
|
+ {isLoading && <Loading height={"30%"} />}
|
|
|
+ {data?.length && !isLoading ? (
|
|
|
+ <Table
|
|
|
+ columns={columns}
|
|
|
+ data={data}
|
|
|
+ isLoading={false}
|
|
|
+ disableGlobalFilter={true}
|
|
|
+ />
|
|
|
+ ) : (
|
|
|
+ !isLoading && (
|
|
|
+ <Placeholder>
|
|
|
+ This project currently has no invites or collaborators.
|
|
|
+ </Placeholder>
|
|
|
+ )
|
|
|
+ )}
|
|
|
+ </>
|
|
|
+ );
|
|
|
+};
|
|
|
|
|
|
-InviteList.contextType = Context;
|
|
|
+export default InvitePage;
|
|
|
|
|
|
const Placeholder = styled.div`
|
|
|
width: 100%;
|
|
|
@@ -267,9 +277,8 @@ const ButtonWrapper = styled.div`
|
|
|
align-items: center;
|
|
|
`;
|
|
|
|
|
|
-const DarkMatter = styled.div`
|
|
|
- width: 100%;
|
|
|
- margin-top: -10px;
|
|
|
+const InputRowWrapper = styled.div`
|
|
|
+ width: 40%;
|
|
|
`;
|
|
|
|
|
|
const CopyButton = styled.div`
|
|
|
@@ -278,16 +287,17 @@ const CopyButton = styled.div`
|
|
|
color: #ffffff;
|
|
|
font-weight: 400;
|
|
|
font-size: 13px;
|
|
|
- margin-left: 12px;
|
|
|
+ margin: 8px 0 8px 12px;
|
|
|
float: right;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
width: 120px;
|
|
|
- padding-top: 7px;
|
|
|
- padding-bottom: 6px;
|
|
|
cursor: pointer;
|
|
|
+ height: 30px;
|
|
|
border-radius: 5px;
|
|
|
border: 1px solid #ffffff20;
|
|
|
background-color: #ffffff10;
|
|
|
- text-align: center;
|
|
|
overflow: hidden;
|
|
|
transition: all 0.1s ease-out;
|
|
|
:hover {
|
|
|
@@ -299,6 +309,9 @@ const CopyButton = styled.div`
|
|
|
const NewLinkButton = styled(CopyButton)`
|
|
|
border: none;
|
|
|
width: auto;
|
|
|
+ float: none;
|
|
|
+ display: block;
|
|
|
+ margin: unset;
|
|
|
background-color: transparent;
|
|
|
:hover {
|
|
|
border: none;
|
|
|
@@ -336,67 +349,28 @@ const InviteButton = styled.div<{ disabled: boolean }>`
|
|
|
margin-bottom: 10px;
|
|
|
`;
|
|
|
|
|
|
-const Rower = styled.div`
|
|
|
+const Url = styled.a`
|
|
|
+ max-width: 300px;
|
|
|
+ font-size: 13px;
|
|
|
+ user-select: text;
|
|
|
+ font-weight: 400;
|
|
|
display: flex;
|
|
|
- flex-direction: row;
|
|
|
align-items: center;
|
|
|
-`;
|
|
|
-
|
|
|
-const ShareLink = styled.input`
|
|
|
- outline: none;
|
|
|
- border: none;
|
|
|
- font-size: 13px;
|
|
|
- background: none;
|
|
|
- width: 60%;
|
|
|
- color: #74a5f7;
|
|
|
- margin-left: -10px;
|
|
|
- padding: 5px 10px;
|
|
|
- height: 30px;
|
|
|
- text-overflow: ellipsis;
|
|
|
- border-radius: 3px;
|
|
|
- ::placeholder,
|
|
|
- ::-webkit-input-placeholder {
|
|
|
- color: #fa0a26;
|
|
|
- font-weight: 600;
|
|
|
+ justify-content: center;
|
|
|
+ > i {
|
|
|
+ margin-left: 10px;
|
|
|
+ font-size: 15px;
|
|
|
}
|
|
|
-`;
|
|
|
-
|
|
|
-const Table = styled.table`
|
|
|
- width: 100%;
|
|
|
- border-spacing: 0px;
|
|
|
- border: 1px solid #ffffff55;
|
|
|
- margin-top: 22px;
|
|
|
- border-radius: 5px;
|
|
|
- background: #ffffff11;
|
|
|
- color: #ffffff;
|
|
|
- font-weight: 400;
|
|
|
- font-size: 13px;
|
|
|
-`;
|
|
|
|
|
|
-const Td = styled.td`
|
|
|
- white-space: nowrap;
|
|
|
- padding: 6px 0px;
|
|
|
- border-top: ${(props: { isTop: boolean }) =>
|
|
|
- props.isTop ? "none" : "1px solid #ffffff55"};
|
|
|
- &:last-child {
|
|
|
- padding-right: 16px;
|
|
|
+ > span {
|
|
|
+ overflow: hidden;
|
|
|
+ white-space: nowrap;
|
|
|
+ text-overflow: ellipsis;
|
|
|
}
|
|
|
-`;
|
|
|
-
|
|
|
-const Tr = styled.tr``;
|
|
|
-
|
|
|
-const MailTd = styled(Td)`
|
|
|
- padding: 0 12px;
|
|
|
- max-width: 186px;
|
|
|
- min-width: 186px;
|
|
|
- overflow: hidden;
|
|
|
- color: #aaaabb;
|
|
|
- text-overflow: ellipsis;
|
|
|
-`;
|
|
|
|
|
|
-const LinkTd = styled(Td)`
|
|
|
- width: calc(100% - 40px);
|
|
|
- padding-left: 40px;
|
|
|
+ :hover {
|
|
|
+ cursor: pointer;
|
|
|
+ }
|
|
|
`;
|
|
|
|
|
|
const Invalid = styled.div`
|
|
|
@@ -405,3 +379,23 @@ const Invalid = styled.div`
|
|
|
font-size: 13px;
|
|
|
font-family: "Work Sans", sans-serif;
|
|
|
`;
|
|
|
+
|
|
|
+const Status = styled.div<{ status: "accepted" | "expired" | "pending" }>`
|
|
|
+ padding: 5px 10px;
|
|
|
+ margin-right: 12px;
|
|
|
+ background: ${(props) => {
|
|
|
+ if (props.status === "accepted") return "#38a88a";
|
|
|
+ if (props.status === "expired") return "#cc3d42";
|
|
|
+ if (props.status === "pending") return "#ffffff11";
|
|
|
+ }};
|
|
|
+ font-size: 13px;
|
|
|
+ border-radius: 3px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ max-height: 25px;
|
|
|
+ max-width: 80px;
|
|
|
+ text-transform: capitalize;
|
|
|
+ font-weight: 400;
|
|
|
+ user-select: none;
|
|
|
+`;
|