chore: Normalize "new" actions in settings (#2226)

* fix: Unauthorized request to views.list from shared documents

* Bump dep styled-components

* chore: Normalize 'new' actions in settings area to top right
chore: Add translation hooks to API tokens screen
chore: Move API tokens loading to paginated list
This commit is contained in:
Tom Moor 2021-06-15 19:10:50 -07:00 committed by GitHub
parent d85592b5f3
commit 2c39cd6496
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 169 additions and 91 deletions

65
app/scenes/APITokenNew.js Normal file
View File

@ -0,0 +1,65 @@
// @flow
import * as React from "react";
import { useTranslation, Trans } from "react-i18next";
import Button from "components/Button";
import Flex from "components/Flex";
import HelpText from "components/HelpText";
import Input from "components/Input";
import useStores from "hooks/useStores";
type Props = {|
onSubmit: () => void,
|};
function APITokenNew({ onSubmit }: Props) {
const [name, setName] = React.useState("");
const [isSaving, setIsSaving] = React.useState(false);
const { apiKeys, ui } = useStores();
const { t } = useTranslation();
const handleSubmit = React.useCallback(async () => {
setIsSaving(true);
try {
await apiKeys.create({ name });
ui.showToast(t("API token created", { type: "success" }));
onSubmit();
} catch (err) {
ui.showToast(err.message, { type: "error" });
} finally {
setIsSaving(false);
}
}, [t, ui, name, onSubmit, apiKeys]);
const handleNameChange = React.useCallback((event) => {
setName(event.target.value);
}, []);
return (
<form onSubmit={handleSubmit}>
<HelpText>
<Trans>
Name your token something that will help you to remember it's use in
the future, for example "local development", "production", or
"continuous integration".
</Trans>
</HelpText>
<Flex>
<Input
type="text"
label="Name"
onChange={handleNameChange}
value={name}
required
autoFocus
flex
/>
</Flex>
<Button type="submit" disabled={isSaving || !name}>
{isSaving ? "Creating…" : "Create"}
</Button>
</form>
);
}
export default APITokenNew;

View File

@ -4,6 +4,7 @@ import { PlusIcon, GroupIcon } from "outline-icons";
import * as React from "react"; import * as React from "react";
import { useTranslation, Trans } from "react-i18next"; import { useTranslation, Trans } from "react-i18next";
import GroupNew from "scenes/GroupNew"; import GroupNew from "scenes/GroupNew";
import { Action } from "components/Actions";
import Button from "components/Button"; import Button from "components/Button";
import Empty from "components/Empty"; import Empty from "components/Empty";
import GroupListItem from "components/GroupListItem"; import GroupListItem from "components/GroupListItem";
@ -33,25 +34,31 @@ function Groups() {
}, []); }, []);
return ( return (
<Scene title={t("Groups")} icon={<GroupIcon color="currentColor" />}> <Scene
title={t("Groups")}
icon={<GroupIcon color="currentColor" />}
actions={
<>
{can.createGroup && (
<Action>
<Button
type="button"
onClick={handleNewGroupModalOpen}
icon={<PlusIcon />}
>
{`${t("New group")}`}
</Button>
</Action>
)}
</>
}
>
<Heading>{t("Groups")}</Heading> <Heading>{t("Groups")}</Heading>
<HelpText> <HelpText>
<Trans> <Trans>
Groups can be used to organize and manage the people on your team. Groups can be used to organize and manage the people on your team.
</Trans> </Trans>
</HelpText> </HelpText>
{can.createGroup && (
<Button
type="button"
onClick={handleNewGroupModalOpen}
icon={<PlusIcon />}
neutral
>
{`${t("New group")}`}
</Button>
)}
<Subheading>{t("All groups")}</Subheading> <Subheading>{t("All groups")}</Subheading>
<PaginatedList <PaginatedList
items={groups.orderedData} items={groups.orderedData}

View File

@ -1,89 +1,86 @@
// @flow // @flow
import { observable } from "mobx"; import { observer } from "mobx-react";
import { observer, inject } from "mobx-react";
import { CodeIcon } from "outline-icons"; import { CodeIcon } from "outline-icons";
import * as React from "react"; import * as React from "react";
import ApiKeysStore from "stores/ApiKeysStore"; import { useTranslation, Trans } from "react-i18next";
import UiStore from "stores/UiStore"; import APITokenNew from "scenes/APITokenNew";
import { Action } from "components/Actions";
import Button from "components/Button"; import Button from "components/Button";
import Heading from "components/Heading"; import Heading from "components/Heading";
import HelpText from "components/HelpText"; import HelpText from "components/HelpText";
import Input from "components/Input"; import Modal from "components/Modal";
import List from "components/List"; import PaginatedList from "components/PaginatedList";
import Scene from "components/Scene"; import Scene from "components/Scene";
import Subheading from "components/Subheading";
import TokenListItem from "./components/TokenListItem"; import TokenListItem from "./components/TokenListItem";
import useCurrentTeam from "hooks/useCurrentTeam";
import useStores from "hooks/useStores";
type Props = { function Tokens() {
apiKeys: ApiKeysStore, const team = useCurrentTeam();
ui: UiStore, const { t } = useTranslation();
}; const { apiKeys, policies } = useStores();
const [newModalOpen, setNewModalOpen] = React.useState(false);
const can = policies.abilities(team.id);
@observer const handleNewModalOpen = React.useCallback(() => {
class Tokens extends React.Component<Props> { setNewModalOpen(true);
@observable name: string = ""; }, []);
componentDidMount() { const handleNewModalClose = React.useCallback(() => {
this.props.apiKeys.fetchPage({ limit: 100 }); setNewModalOpen(false);
} }, []);
handleUpdate = (ev: SyntheticInputEvent<*>) => { return (
this.name = ev.target.value; <Scene
}; title={t("API Tokens")}
icon={<CodeIcon color="currentColor" />}
handleSubmit = async (ev: SyntheticEvent<>) => { actions={
try { <>
ev.preventDefault(); {can.createApiKey && (
await this.props.apiKeys.create({ name: this.name }); <Action>
this.name = ""; <Button
} catch (error) { type="submit"
this.props.ui.showToast(error.message, { type: "error" }); value={`${t("New token")}`}
} onClick={handleNewModalOpen}
};
render() {
const { apiKeys } = this.props;
const hasApiKeys = apiKeys.orderedData.length > 0;
return (
<Scene title="API Tokens" icon={<CodeIcon color="currentColor" />}>
<Heading>API Tokens</Heading>
<HelpText>
You can create an unlimited amount of personal tokens to authenticate
with the API. For more details about the API take a look at the{" "}
<a href="https://www.getoutline.com/developers">
developer documentation
</a>
.
</HelpText>
{hasApiKeys && (
<List>
{apiKeys.orderedData.map((token) => (
<TokenListItem
key={token.id}
token={token}
onDelete={token.delete}
/> />
))} </Action>
</List> )}
)} </>
}
>
<Heading>{t("API Tokens")}</Heading>
<HelpText>
<Trans
defaults="You can create an unlimited amount of personal tokens to authenticate
with the API. Tokens have the same permissions as your user account.
For more details see the <em>developer documentation</em>."
components={{
em: (
<a href="https://www.getoutline.com/developers" target="_blank" />
),
}}
/>
</HelpText>
<form onSubmit={this.handleSubmit}> <PaginatedList
<Input fetch={apiKeys.fetchPage}
onChange={this.handleUpdate} items={apiKeys.orderedData}
placeholder="Token label (eg. development)" heading={<Subheading sticky>{t("Tokens")}</Subheading>}
value={this.name} renderItem={(token) => (
required <TokenListItem key={token.id} token={token} onDelete={token.delete} />
/> )}
<Button />
type="submit"
value="Create Token" <Modal
disabled={apiKeys.isSaving} title={t("Create a token")}
/> onRequestClose={handleNewModalClose}
</form> isOpen={newModalOpen}
</Scene> >
); <APITokenNew onSubmit={handleNewModalClose} />
} </Modal>
</Scene>
);
} }
export default inject("apiKeys", "ui")(Tokens); export default observer(Tokens);

View File

@ -4,17 +4,20 @@ import ApiKey from "models/ApiKey";
import Button from "components/Button"; import Button from "components/Button";
import ListItem from "components/List/Item"; import ListItem from "components/List/Item";
type Props = { type Props = {|
token: ApiKey, token: ApiKey,
onDelete: (tokenId: string) => Promise<void>, onDelete: (tokenId: string) => Promise<void>,
}; |};
const TokenListItem = ({ token, onDelete }: Props) => { const TokenListItem = ({ token, onDelete }: Props) => {
return ( return (
<ListItem <ListItem
key={token.id} key={token.id}
title={token.name} title={
subtitle={<code>{token.secret}</code>} <>
{token.name} <code>{token.secret}</code>
</>
}
actions={ actions={
<Button onClick={() => onDelete(token.id)} neutral> <Button onClick={() => onDelete(token.id)} neutral>
Revoke Revoke

View File

@ -216,6 +216,8 @@
"Revoke invite": "Revoke invite", "Revoke invite": "Revoke invite",
"Activate account": "Activate account", "Activate account": "Activate account",
"Suspend account": "Suspend account", "Suspend account": "Suspend account",
"API token created": "API token created",
"Name your token something that will help you to remember it's use in the future, for example \"local development\", \"production\", or \"continuous integration\".": "Name your token something that will help you to remember it's use in the future, for example \"local development\", \"production\", or \"continuous integration\".",
"Documents": "Documents", "Documents": "Documents",
"The document archive is empty at the moment.": "The document archive is empty at the moment.", "The document archive is empty at the moment.": "The document archive is empty at the moment.",
"Search in collection": "Search in collection", "Search in collection": "Search in collection",
@ -386,8 +388,8 @@
"Active": "Active", "Active": "Active",
"Everyone": "Everyone", "Everyone": "Everyone",
"Admins": "Admins", "Admins": "Admins",
"Groups can be used to organize and manage the people on your team.": "Groups can be used to organize and manage the people on your team.",
"New group": "New group", "New group": "New group",
"Groups can be used to organize and manage the people on your team.": "Groups can be used to organize and manage the people on your team.",
"All groups": "All groups", "All groups": "All groups",
"No groups have been created yet": "No groups have been created yet", "No groups have been created yet": "No groups have been created yet",
"Import started": "Import started", "Import started": "Import started",
@ -428,6 +430,10 @@
"Connect Outline collections to Slack channels and messages will be automatically posted to Slack when documents are published or updated.": "Connect Outline collections to Slack channels and messages will be automatically posted to Slack when documents are published or updated.", "Connect Outline collections to Slack channels and messages will be automatically posted to Slack when documents are published or updated.": "Connect Outline collections to Slack channels and messages will be automatically posted to Slack when documents are published or updated.",
"Connected to the <em>{{ channelName }}</em> channel": "Connected to the <em>{{ channelName }}</em> channel", "Connected to the <em>{{ channelName }}</em> channel": "Connected to the <em>{{ channelName }}</em> channel",
"Connect": "Connect", "Connect": "Connect",
"New token": "New token",
"You can create an unlimited amount of personal tokens to authenticate\n with the API. Tokens have the same permissions as your user account.\n For more details see the <em>developer documentation</em>.": "You can create an unlimited amount of personal tokens to authenticate\n with the API. Tokens have the same permissions as your user account.\n For more details see the <em>developer documentation</em>.",
"Tokens": "Tokens",
"Create a token": "Create a token",
"Youve not starred any documents yet.": "Youve not starred any documents yet.", "Youve not starred any documents yet.": "Youve not starred any documents yet.",
"There are no templates just yet.": "There are no templates just yet.", "There are no templates just yet.": "There are no templates just yet.",
"You can create templates to help your team create consistent and accurate documentation.": "You can create templates to help your team create consistent and accurate documentation.", "You can create templates to help your team create consistent and accurate documentation.": "You can create templates to help your team create consistent and accurate documentation.",