fix: Add ability to choose user permission level when inviting (#2473)
* Select user role while sending invite * Add tests to check for role * Update app/scenes/Invite.js Co-authored-by: Tom Moor <tom.moor@gmail.com> * Use select * Use inviteUser policy * Remove unnecessary code * Normalize rank/role Fix text sizing of select input, fix alignment on users invite form * Move component to root * cleanup Co-authored-by: Tom Moor <tom.moor@gmail.com>
This commit is contained in:
parent
00ba65f3ef
commit
e4b7aa6761
|
@ -4,6 +4,7 @@ import { observer } from "mobx-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { VisuallyHidden } from "reakit/VisuallyHidden";
|
import { VisuallyHidden } from "reakit/VisuallyHidden";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
import breakpoint from "styled-components-breakpoint";
|
||||||
import { Outline, LabelText } from "./Input";
|
import { Outline, LabelText } from "./Input";
|
||||||
|
|
||||||
const Select = styled.select`
|
const Select = styled.select`
|
||||||
|
@ -15,6 +16,7 @@ const Select = styled.select`
|
||||||
background: none;
|
background: none;
|
||||||
color: ${(props) => props.theme.text};
|
color: ${(props) => props.theme.text};
|
||||||
height: 30px;
|
height: 30px;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
option {
|
option {
|
||||||
background: ${(props) => props.theme.buttonNeutralBackground};
|
background: ${(props) => props.theme.buttonNeutralBackground};
|
||||||
|
@ -24,6 +26,10 @@ const Select = styled.select`
|
||||||
&::placeholder {
|
&::placeholder {
|
||||||
color: ${(props) => props.theme.placeholder};
|
color: ${(props) => props.theme.placeholder};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
${breakpoint("mobile", "tablet")`
|
||||||
|
font-size: 16px;
|
||||||
|
`};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Wrapper = styled.label`
|
const Wrapper = styled.label`
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
// @flow
|
||||||
|
import * as React from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import InputSelect, { type Props, type Option } from "components/InputSelect";
|
||||||
|
|
||||||
|
const InputSelectRole = (props: $Rest<Props, { options: Array<Option> }>) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InputSelect
|
||||||
|
label={t("Role")}
|
||||||
|
options={[
|
||||||
|
{ label: t("Member"), value: "member" },
|
||||||
|
{ label: t("Viewer"), value: "viewer" },
|
||||||
|
{ label: t("Admin"), value: "admin" },
|
||||||
|
]}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InputSelectRole;
|
|
@ -49,7 +49,7 @@ function UserMenu({ user }: Props) {
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
users.demote(user, "Member");
|
users.demote(user, "member");
|
||||||
},
|
},
|
||||||
[users, user, t]
|
[users, user, t]
|
||||||
);
|
);
|
||||||
|
@ -69,7 +69,7 @@ function UserMenu({ user }: Props) {
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
users.demote(user, "Viewer");
|
users.demote(user, "viewer");
|
||||||
},
|
},
|
||||||
[users, user, t]
|
[users, user, t]
|
||||||
);
|
);
|
||||||
|
@ -119,21 +119,21 @@ function UserMenu({ user }: Props) {
|
||||||
userName: user.name,
|
userName: user.name,
|
||||||
}),
|
}),
|
||||||
onClick: handleMember,
|
onClick: handleMember,
|
||||||
visible: can.demote && user.rank !== "Member",
|
visible: can.demote && user.role !== "member",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("Make {{ userName }} a viewer", {
|
title: t("Make {{ userName }} a viewer", {
|
||||||
userName: user.name,
|
userName: user.name,
|
||||||
}),
|
}),
|
||||||
onClick: handleViewer,
|
onClick: handleViewer,
|
||||||
visible: can.demote && user.rank !== "Viewer",
|
visible: can.demote && user.role !== "viewer",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("Make {{ userName }} an admin…", {
|
title: t("Make {{ userName }} an admin…", {
|
||||||
userName: user.name,
|
userName: user.name,
|
||||||
}),
|
}),
|
||||||
onClick: handlePromote,
|
onClick: handlePromote,
|
||||||
visible: can.promote && user.rank !== "Admin",
|
visible: can.promote && user.role !== "admin",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "separator",
|
type: "separator",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { computed } from "mobx";
|
import { computed } from "mobx";
|
||||||
import type { Rank } from "shared/types";
|
import type { Role } from "shared/types";
|
||||||
import BaseModel from "./BaseModel";
|
import BaseModel from "./BaseModel";
|
||||||
|
|
||||||
class User extends BaseModel {
|
class User extends BaseModel {
|
||||||
|
@ -21,13 +21,13 @@ class User extends BaseModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed
|
@computed
|
||||||
get rank(): Rank {
|
get role(): Role {
|
||||||
if (this.isAdmin) {
|
if (this.isAdmin) {
|
||||||
return "Admin";
|
return "admin";
|
||||||
} else if (this.isViewer) {
|
} else if (this.isViewer) {
|
||||||
return "Viewer";
|
return "viewer";
|
||||||
} else {
|
} else {
|
||||||
return "Member";
|
return "member";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,11 +5,13 @@ import * as React from "react";
|
||||||
import { useTranslation, Trans } from "react-i18next";
|
import { useTranslation, Trans } from "react-i18next";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
import type { Role } from "shared/types";
|
||||||
import Button from "components/Button";
|
import Button from "components/Button";
|
||||||
import CopyToClipboard from "components/CopyToClipboard";
|
import CopyToClipboard from "components/CopyToClipboard";
|
||||||
import Flex from "components/Flex";
|
import Flex from "components/Flex";
|
||||||
import HelpText from "components/HelpText";
|
import HelpText from "components/HelpText";
|
||||||
import Input from "components/Input";
|
import Input from "components/Input";
|
||||||
|
import InputSelectRole from "components/InputSelectRole";
|
||||||
import NudeButton from "components/NudeButton";
|
import NudeButton from "components/NudeButton";
|
||||||
import Tooltip from "components/Tooltip";
|
import Tooltip from "components/Tooltip";
|
||||||
import useCurrentTeam from "hooks/useCurrentTeam";
|
import useCurrentTeam from "hooks/useCurrentTeam";
|
||||||
|
@ -26,15 +28,16 @@ type Props = {|
|
||||||
type InviteRequest = {
|
type InviteRequest = {
|
||||||
email: string,
|
email: string,
|
||||||
name: string,
|
name: string,
|
||||||
|
role: Role,
|
||||||
};
|
};
|
||||||
|
|
||||||
function Invite({ onSubmit }: Props) {
|
function Invite({ onSubmit }: Props) {
|
||||||
const [isSaving, setIsSaving] = React.useState();
|
const [isSaving, setIsSaving] = React.useState();
|
||||||
const [linkCopied, setLinkCopied] = React.useState<boolean>(false);
|
const [linkCopied, setLinkCopied] = React.useState<boolean>(false);
|
||||||
const [invites, setInvites] = React.useState<InviteRequest[]>([
|
const [invites, setInvites] = React.useState<InviteRequest[]>([
|
||||||
{ email: "", name: "" },
|
{ email: "", name: "", role: "member" },
|
||||||
{ email: "", name: "" },
|
{ email: "", name: "", role: "member" },
|
||||||
{ email: "", name: "" },
|
{ email: "", name: "", role: "member" },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const { users, policies } = useStores();
|
const { users, policies } = useStores();
|
||||||
|
@ -84,7 +87,7 @@ function Invite({ onSubmit }: Props) {
|
||||||
|
|
||||||
setInvites((prevInvites) => {
|
setInvites((prevInvites) => {
|
||||||
const newInvites = [...prevInvites];
|
const newInvites = [...prevInvites];
|
||||||
newInvites.push({ email: "", name: "" });
|
newInvites.push({ email: "", name: "", role: "member" });
|
||||||
return newInvites;
|
return newInvites;
|
||||||
});
|
});
|
||||||
}, [showToast, invites, t]);
|
}, [showToast, invites, t]);
|
||||||
|
@ -109,6 +112,14 @@ function Invite({ onSubmit }: Props) {
|
||||||
});
|
});
|
||||||
}, [showToast, t]);
|
}, [showToast, t]);
|
||||||
|
|
||||||
|
const handleRoleChange = React.useCallback((ev, index) => {
|
||||||
|
setInvites((prevInvites) => {
|
||||||
|
const newInvites = [...prevInvites];
|
||||||
|
newInvites[index]["role"] = ev.target.value;
|
||||||
|
return newInvites;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
{team.guestSignin ? (
|
{team.guestSignin ? (
|
||||||
|
@ -160,7 +171,7 @@ function Invite({ onSubmit }: Props) {
|
||||||
</CopyBlock>
|
</CopyBlock>
|
||||||
)}
|
)}
|
||||||
{invites.map((invite, index) => (
|
{invites.map((invite, index) => (
|
||||||
<Flex key={index}>
|
<Flex key={index} gap={8}>
|
||||||
<Input
|
<Input
|
||||||
type="email"
|
type="email"
|
||||||
name="email"
|
name="email"
|
||||||
|
@ -173,7 +184,6 @@ function Invite({ onSubmit }: Props) {
|
||||||
autoFocus={index === 0}
|
autoFocus={index === 0}
|
||||||
flex
|
flex
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
name="name"
|
name="name"
|
||||||
|
@ -182,7 +192,12 @@ function Invite({ onSubmit }: Props) {
|
||||||
onChange={(ev) => handleChange(ev, index)}
|
onChange={(ev) => handleChange(ev, index)}
|
||||||
value={invite.name}
|
value={invite.name}
|
||||||
required={!!invite.email}
|
required={!!invite.email}
|
||||||
flex
|
/>
|
||||||
|
<InputSelectRole
|
||||||
|
onChange={(ev) => handleRoleChange(ev, index)}
|
||||||
|
value={invite.role}
|
||||||
|
labelHidden={index !== 0}
|
||||||
|
short
|
||||||
/>
|
/>
|
||||||
{index !== 0 && (
|
{index !== 0 && (
|
||||||
<Remove>
|
<Remove>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import invariant from "invariant";
|
import invariant from "invariant";
|
||||||
import { filter, orderBy } from "lodash";
|
import { filter, orderBy } from "lodash";
|
||||||
import { observable, computed, action, runInAction } from "mobx";
|
import { observable, computed, action, runInAction } from "mobx";
|
||||||
import type { Rank } from "shared/types";
|
import type { Role } from "shared/types";
|
||||||
import User from "models/User";
|
import User from "models/User";
|
||||||
import BaseStore from "./BaseStore";
|
import BaseStore from "./BaseStore";
|
||||||
import RootStore from "./RootStore";
|
import RootStore from "./RootStore";
|
||||||
|
@ -68,20 +68,20 @@ export default class UsersStore extends BaseStore<User> {
|
||||||
@action
|
@action
|
||||||
promote = async (user: User) => {
|
promote = async (user: User) => {
|
||||||
try {
|
try {
|
||||||
this.updateCounts("Admin", user.rank);
|
this.updateCounts("admin", user.role);
|
||||||
await this.actionOnUser("promote", user);
|
await this.actionOnUser("promote", user);
|
||||||
} catch {
|
} catch {
|
||||||
this.updateCounts(user.rank, "Admin");
|
this.updateCounts(user.role, "admin");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@action
|
@action
|
||||||
demote = async (user: User, to: Rank) => {
|
demote = async (user: User, to: Role) => {
|
||||||
try {
|
try {
|
||||||
this.updateCounts(to, user.rank);
|
this.updateCounts(to, user.role);
|
||||||
await this.actionOnUser("demote", user, to);
|
await this.actionOnUser("demote", user, to);
|
||||||
} catch {
|
} catch {
|
||||||
this.updateCounts(user.rank, to);
|
this.updateCounts(user.role, to);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -110,7 +110,7 @@ export default class UsersStore extends BaseStore<User> {
|
||||||
};
|
};
|
||||||
|
|
||||||
@action
|
@action
|
||||||
invite = async (invites: { email: string, name: string }[]) => {
|
invite = async (invites: { email: string, name: string, role: Role }[]) => {
|
||||||
const res = await client.post(`/users.invite`, { invites });
|
const res = await client.post(`/users.invite`, { invites });
|
||||||
invariant(res && res.data, "Data should be available");
|
invariant(res && res.data, "Data should be available");
|
||||||
runInAction(`invite`, () => {
|
runInAction(`invite`, () => {
|
||||||
|
@ -152,24 +152,24 @@ export default class UsersStore extends BaseStore<User> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
updateCounts = (to: Rank, from: Rank) => {
|
updateCounts = (to: Role, from: Role) => {
|
||||||
if (to === "Admin") {
|
if (to === "admin") {
|
||||||
this.counts.admins += 1;
|
this.counts.admins += 1;
|
||||||
if (from === "Viewer") {
|
if (from === "viewer") {
|
||||||
this.counts.viewers -= 1;
|
this.counts.viewers -= 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (to === "Viewer") {
|
if (to === "viewer") {
|
||||||
this.counts.viewers += 1;
|
this.counts.viewers += 1;
|
||||||
if (from === "Admin") {
|
if (from === "admin") {
|
||||||
this.counts.admins -= 1;
|
this.counts.admins -= 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (to === "Member") {
|
if (to === "member") {
|
||||||
if (from === "Viewer") {
|
if (from === "viewer") {
|
||||||
this.counts.viewers -= 1;
|
this.counts.viewers -= 1;
|
||||||
}
|
}
|
||||||
if (from === "Admin") {
|
if (from === "admin") {
|
||||||
this.counts.admins -= 1;
|
this.counts.admins -= 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -233,7 +233,7 @@ export default class UsersStore extends BaseStore<User> {
|
||||||
return queriedUsers(users, query);
|
return queriedUsers(users, query);
|
||||||
};
|
};
|
||||||
|
|
||||||
actionOnUser = async (action: string, user: User, to?: Rank) => {
|
actionOnUser = async (action: string, user: User, to?: Role) => {
|
||||||
const res = await client.post(`/users.${action}`, {
|
const res = await client.post(`/users.${action}`, {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
to,
|
to,
|
||||||
|
|
|
@ -181,7 +181,7 @@ router.post("users.demote", auth(), async (ctx) => {
|
||||||
const actor = ctx.state.user;
|
const actor = ctx.state.user;
|
||||||
ctx.assertPresent(userId, "id is required");
|
ctx.assertPresent(userId, "id is required");
|
||||||
|
|
||||||
to = to === "Viewer" ? "Viewer" : "Member";
|
to = to === "viewer" ? "viewer" : "member";
|
||||||
|
|
||||||
const user = await User.findByPk(userId);
|
const user = await User.findByPk(userId);
|
||||||
|
|
||||||
|
@ -262,7 +262,7 @@ router.post("users.invite", auth(), async (ctx) => {
|
||||||
|
|
||||||
const { user } = ctx.state;
|
const { user } = ctx.state;
|
||||||
const team = await Team.findByPk(user.teamId);
|
const team = await Team.findByPk(user.teamId);
|
||||||
authorize(user, "invite", team);
|
authorize(user, "inviteUser", team);
|
||||||
|
|
||||||
const response = await userInviter({ user, invites, ip: ctx.request.ip });
|
const response = await userInviter({ user, invites, ip: ctx.request.ip });
|
||||||
|
|
||||||
|
|
|
@ -159,7 +159,7 @@ describe("#users.invite", () => {
|
||||||
const res = await server.post("/api/users.invite", {
|
const res = await server.post("/api/users.invite", {
|
||||||
body: {
|
body: {
|
||||||
token: user.getJwtToken(),
|
token: user.getJwtToken(),
|
||||||
invites: [{ email: "test@example.com", name: "Test", guest: false }],
|
invites: [{ email: "test@example.com", name: "Test", role: "member" }],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const body = await res.json();
|
const body = await res.json();
|
||||||
|
@ -168,27 +168,74 @@ describe("#users.invite", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should require invites to be an array", async () => {
|
it("should require invites to be an array", async () => {
|
||||||
const user = await buildUser();
|
const admin = await buildAdmin();
|
||||||
const res = await server.post("/api/users.invite", {
|
const res = await server.post("/api/users.invite", {
|
||||||
body: {
|
body: {
|
||||||
token: user.getJwtToken(),
|
token: admin.getJwtToken(),
|
||||||
invites: { email: "test@example.com", name: "Test", guest: false },
|
invites: { email: "test@example.com", name: "Test", role: "member" },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(res.status).toEqual(400);
|
expect(res.status).toEqual(400);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should require admin", async () => {
|
it("should require admin", async () => {
|
||||||
const user = await buildUser();
|
const admin = await buildUser();
|
||||||
const res = await server.post("/api/users.invite", {
|
const res = await server.post("/api/users.invite", {
|
||||||
body: {
|
body: {
|
||||||
token: user.getJwtToken(),
|
token: admin.getJwtToken(),
|
||||||
invites: [{ email: "test@example.com", name: "Test", guest: false }],
|
invites: [{ email: "test@example.com", name: "Test", role: "member" }],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(res.status).toEqual(403);
|
expect(res.status).toEqual(403);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should invite user as an admin", async () => {
|
||||||
|
const admin = await buildAdmin();
|
||||||
|
const res = await server.post("/api/users.invite", {
|
||||||
|
body: {
|
||||||
|
token: admin.getJwtToken(),
|
||||||
|
invites: [{ email: "test@example.com", name: "Test", role: "admin" }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const body = await res.json();
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
expect(body.data.sent.length).toEqual(1);
|
||||||
|
expect(body.data.users[0].isAdmin).toBeTruthy();
|
||||||
|
expect(body.data.users[0].isViewer).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should invite user as a viewer", async () => {
|
||||||
|
const admin = await buildAdmin();
|
||||||
|
const res = await server.post("/api/users.invite", {
|
||||||
|
body: {
|
||||||
|
token: admin.getJwtToken(),
|
||||||
|
invites: [{ email: "test@example.com", name: "Test", role: "viewer" }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const body = await res.json();
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
expect(body.data.sent.length).toEqual(1);
|
||||||
|
expect(body.data.users[0].isViewer).toBeTruthy();
|
||||||
|
expect(body.data.users[0].isAdmin).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should invite user as a member if role is any arbitary value", async () => {
|
||||||
|
const admin = await buildAdmin();
|
||||||
|
const res = await server.post("/api/users.invite", {
|
||||||
|
body: {
|
||||||
|
token: admin.getJwtToken(),
|
||||||
|
invites: [
|
||||||
|
{ email: "test@example.com", name: "Test", role: "arbitary" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const body = await res.json();
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
expect(body.data.sent.length).toEqual(1);
|
||||||
|
expect(body.data.users[0].isViewer).toBeFalsy();
|
||||||
|
expect(body.data.users[0].isAdmin).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
it("should require authentication", async () => {
|
it("should require authentication", async () => {
|
||||||
const res = await server.post("/api/users.invite");
|
const res = await server.post("/api/users.invite");
|
||||||
expect(res.status).toEqual(401);
|
expect(res.status).toEqual(401);
|
||||||
|
@ -325,7 +372,7 @@ describe("#users.demote", () => {
|
||||||
body: {
|
body: {
|
||||||
token: admin.getJwtToken(),
|
token: admin.getJwtToken(),
|
||||||
id: user.id,
|
id: user.id,
|
||||||
to: "Viewer",
|
to: "viewer",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const body = await res.json();
|
const body = await res.json();
|
||||||
|
@ -342,7 +389,7 @@ describe("#users.demote", () => {
|
||||||
body: {
|
body: {
|
||||||
token: admin.getJwtToken(),
|
token: admin.getJwtToken(),
|
||||||
id: user.id,
|
id: user.id,
|
||||||
to: "Member",
|
to: "member",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const body = await res.json();
|
const body = await res.json();
|
||||||
|
|
|
@ -1,9 +1,14 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { uniqBy } from "lodash";
|
import { uniqBy } from "lodash";
|
||||||
|
import type { Role } from "shared/types";
|
||||||
import mailer from "../mailer";
|
import mailer from "../mailer";
|
||||||
import { User, Event, Team } from "../models";
|
import { User, Event, Team } from "../models";
|
||||||
|
|
||||||
type Invite = { name: string, email: string };
|
type Invite = {
|
||||||
|
name: string,
|
||||||
|
email: string,
|
||||||
|
role: Role,
|
||||||
|
};
|
||||||
|
|
||||||
export default async function userInviter({
|
export default async function userInviter({
|
||||||
user,
|
user,
|
||||||
|
@ -52,6 +57,8 @@ export default async function userInviter({
|
||||||
name: invite.name,
|
name: invite.name,
|
||||||
email: invite.email,
|
email: invite.email,
|
||||||
service: null,
|
service: null,
|
||||||
|
isAdmin: invite.role === "admin",
|
||||||
|
isViewer: invite.role === "viewer",
|
||||||
});
|
});
|
||||||
users.push(newUser);
|
users.push(newUser);
|
||||||
|
|
||||||
|
@ -62,6 +69,7 @@ export default async function userInviter({
|
||||||
data: {
|
data: {
|
||||||
email: invite.email,
|
email: invite.email,
|
||||||
name: invite.name,
|
name: invite.name,
|
||||||
|
role: invite.role,
|
||||||
},
|
},
|
||||||
ip,
|
ip,
|
||||||
});
|
});
|
||||||
|
|
|
@ -298,7 +298,7 @@ User.getCounts = async function (teamId: string) {
|
||||||
|
|
||||||
User.prototype.demote = async function (
|
User.prototype.demote = async function (
|
||||||
teamId: string,
|
teamId: string,
|
||||||
to: "Member" | "Viewer"
|
to: "member" | "viewer"
|
||||||
) {
|
) {
|
||||||
const res = await User.findAndCountAll({
|
const res = await User.findAndCountAll({
|
||||||
where: {
|
where: {
|
||||||
|
@ -312,9 +312,9 @@ User.prototype.demote = async function (
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.count >= 1) {
|
if (res.count >= 1) {
|
||||||
if (to === "Member") {
|
if (to === "member") {
|
||||||
return this.update({ isAdmin: false, isViewer: false });
|
return this.update({ isAdmin: false, isViewer: false });
|
||||||
} else if (to === "Viewer") {
|
} else if (to === "viewer") {
|
||||||
return this.update({ isAdmin: false, isViewer: true });
|
return this.update({ isAdmin: false, isViewer: true });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -130,6 +130,10 @@
|
||||||
"View and edit": "View and edit",
|
"View and edit": "View and edit",
|
||||||
"View only": "View only",
|
"View only": "View only",
|
||||||
"No access": "No access",
|
"No access": "No access",
|
||||||
|
"Role": "Role",
|
||||||
|
"Member": "Member",
|
||||||
|
"Viewer": "Viewer",
|
||||||
|
"Admin": "Admin",
|
||||||
"Outline is available in your language {{optionLabel}}, would you like to change?": "Outline is available in your language {{optionLabel}}, would you like to change?",
|
"Outline is available in your language {{optionLabel}}, would you like to change?": "Outline is available in your language {{optionLabel}}, would you like to change?",
|
||||||
"Change Language": "Change Language",
|
"Change Language": "Change Language",
|
||||||
"Dismiss": "Dismiss",
|
"Dismiss": "Dismiss",
|
||||||
|
@ -306,7 +310,6 @@
|
||||||
"Active <1></1> ago": "Active <1></1> ago",
|
"Active <1></1> ago": "Active <1></1> ago",
|
||||||
"Never signed in": "Never signed in",
|
"Never signed in": "Never signed in",
|
||||||
"Invited": "Invited",
|
"Invited": "Invited",
|
||||||
"Admin": "Admin",
|
|
||||||
"{{ userName }} was removed from the collection": "{{ userName }} was removed from the collection",
|
"{{ userName }} was removed from the collection": "{{ userName }} was removed from the collection",
|
||||||
"Could not remove user": "Could not remove user",
|
"Could not remove user": "Could not remove user",
|
||||||
"{{ userName }} permissions were updated": "{{ userName }} permissions were updated",
|
"{{ userName }} permissions were updated": "{{ userName }} permissions were updated",
|
||||||
|
@ -476,8 +479,6 @@
|
||||||
"All collections": "All collections",
|
"All collections": "All collections",
|
||||||
"{{userName}} requested": "{{userName}} requested",
|
"{{userName}} requested": "{{userName}} requested",
|
||||||
"Last active": "Last active",
|
"Last active": "Last active",
|
||||||
"Role": "Role",
|
|
||||||
"Viewer": "Viewer",
|
|
||||||
"Suspended": "Suspended",
|
"Suspended": "Suspended",
|
||||||
"Shared": "Shared",
|
"Shared": "Shared",
|
||||||
"by {{ name }}": "by {{ name }}",
|
"by {{ name }}": "by {{ name }}",
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
// @flow
|
// @flow
|
||||||
export type Rank = "Admin" | "Viewer" | "Member";
|
export type Role = "admin" | "viewer" | "member";
|
||||||
|
|
Reference in New Issue