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:
parent
d85592b5f3
commit
2c39cd6496
|
@ -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;
|
|
@ -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}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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",
|
||||||
"You’ve not starred any documents yet.": "You’ve not starred any documents yet.",
|
"You’ve not starred any documents yet.": "You’ve 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.",
|
||||||
|
|
Reference in New Issue