Files
outline/app/scenes/Settings/Members.tsx
Tom Moor d63326066f feat: Improve settings layout (#3234)
* Setup, and security settings

* Settings -> Details

* Settings -> Notifications

* Profile

* lint

* fix: Flash of loading on members screen

* align language input

* feat: Move share links management to sortable table

* Add account menu to sidebar on settings page

* Aesthetic tweaks, light borders between settings and slight column offset
2022-03-14 17:44:56 -07:00

209 lines
6.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { sortBy } from "lodash";
import { observer } from "mobx-react";
import { PlusIcon, UserIcon } from "outline-icons";
import * as React from "react";
import { Trans, useTranslation } from "react-i18next";
import { useHistory, useLocation } from "react-router-dom";
import styled from "styled-components";
import { PAGINATION_SYMBOL } from "~/stores/BaseStore";
import User from "~/models/User";
import Invite from "~/scenes/Invite";
import { Action } from "~/components/Actions";
import Button from "~/components/Button";
import Flex from "~/components/Flex";
import Heading from "~/components/Heading";
import InputSearch from "~/components/InputSearch";
import Modal from "~/components/Modal";
import Scene from "~/components/Scene";
import Text from "~/components/Text";
import useBoolean from "~/hooks/useBoolean";
import useCurrentTeam from "~/hooks/useCurrentTeam";
import usePolicy from "~/hooks/usePolicy";
import useQuery from "~/hooks/useQuery";
import useStores from "~/hooks/useStores";
import PeopleTable from "./components/PeopleTable";
import UserStatusFilter from "./components/UserStatusFilter";
function Members() {
const location = useLocation();
const history = useHistory();
const [
inviteModalOpen,
handleInviteModalOpen,
handleInviteModalClose,
] = useBoolean();
const team = useCurrentTeam();
const { users } = useStores();
const { t } = useTranslation();
const params = useQuery();
const [isLoading, setIsLoading] = React.useState(false);
const [data, setData] = React.useState<User[]>([]);
const [totalPages, setTotalPages] = React.useState(0);
const [userIds, setUserIds] = React.useState<string[]>([]);
const can = usePolicy(team.id);
const query = params.get("query") || "";
const filter = params.get("filter") || "";
const sort = params.get("sort") || "name";
const direction = (params.get("direction") || "asc").toUpperCase() as
| "ASC"
| "DESC";
const page = parseInt(params.get("page") || "0", 10);
const limit = 25;
React.useEffect(() => {
const fetchData = async () => {
setIsLoading(true);
try {
const response = await users.fetchPage({
offset: page * limit,
limit,
sort,
direction,
query,
filter,
});
setTotalPages(Math.ceil(response[PAGINATION_SYMBOL].total / limit));
setUserIds(response.map((u: User) => u.id));
} finally {
setIsLoading(false);
}
};
fetchData();
}, [query, sort, filter, page, direction, users]);
React.useEffect(() => {
let filtered = users.orderedData;
if (!filter) {
filtered = users.active.filter((u) => userIds.includes(u.id));
} else if (filter === "all") {
filtered = users.orderedData.filter((u) => userIds.includes(u.id));
} else if (filter === "admins") {
filtered = users.admins.filter((u) => userIds.includes(u.id));
} else if (filter === "suspended") {
filtered = users.suspended.filter((u) => userIds.includes(u.id));
} else if (filter === "invited") {
filtered = users.invited.filter((u) => userIds.includes(u.id));
} else if (filter === "viewers") {
filtered = users.viewers.filter((u) => userIds.includes(u.id));
}
// sort the resulting data by the original order from the server
setData(sortBy(filtered, (item) => userIds.indexOf(item.id)));
}, [
filter,
users.active,
users.admins,
users.orderedData,
users.suspended,
users.invited,
users.viewers,
userIds,
]);
const handleFilter = React.useCallback(
(filter) => {
if (filter) {
params.set("filter", filter);
params.delete("page");
} else {
params.delete("filter");
}
history.replace({
pathname: location.pathname,
search: params.toString(),
});
},
[params, history, location.pathname]
);
const handleSearch = React.useCallback(
(event) => {
const { value } = event.target;
if (value) {
params.set("query", event.target.value);
params.delete("page");
} else {
params.delete("query");
}
history.replace({
pathname: location.pathname,
search: params.toString(),
});
},
[params, history, location.pathname]
);
return (
<Scene
title={t("Members")}
icon={<UserIcon color="currentColor" />}
actions={
<>
{can.inviteUser && (
<Action>
<Button
type="button"
data-on="click"
data-event-category="invite"
data-event-action="peoplePage"
onClick={handleInviteModalOpen}
icon={<PlusIcon />}
>
{t("Invite people")}
</Button>
</Action>
)}
</>
}
>
<Heading>{t("Members")}</Heading>
<Text type="secondary">
<Trans>
Everyone that has signed into Outline appears here. Its possible that
there are other users who have access through {team.signinMethods} but
havent signed in yet.
</Trans>
</Text>
<Flex gap={8}>
<InputSearch
short
value={query}
placeholder={`${t("Filter")}`}
onChange={handleSearch}
/>
<LargeUserStatusFilter activeKey={filter} onSelect={handleFilter} />
</Flex>
<PeopleTable
data={data}
canManage={can.manage}
isLoading={isLoading}
page={page}
pageSize={limit}
totalPages={totalPages}
defaultSortDirection="ASC"
/>
{can.inviteUser && (
<Modal
title={t("Invite people")}
onRequestClose={handleInviteModalClose}
isOpen={inviteModalOpen}
>
<Invite onSubmit={handleInviteModalClose} />
</Modal>
)}
</Scene>
);
}
const LargeUserStatusFilter = styled(UserStatusFilter)`
height: 32px;
`;
export default observer(Members);