feat: I18n (#1653)
* feat: i18n * Changing language single source of truth from TEAM to USER * Changes according to @tommoor comments on PR * Changed package.json for build:i18n and translation label * Finished 1st MVP of i18n for outline * new translation labels & Portuguese from Portugal translation * Fixes from PR request * Described language dropdown as an experimental feature * Set keySeparator to false in order to cowork with html keys * Added useTranslation to Breadcrumb * Repositioned <strong> element * Removed extra space from TemplatesMenu * Fortified the test suite for i18n * Fixed trans component problematic * Check if selected language is available * Update yarn.lock * Removed unused Trans * Removing debug variable from i18n init * Removed debug variable * test: update snapshots * flow: Remove decorator usage to get proper flow typing It's a shame, but hopefully we'll move to Typescript in the next 6 months and we can forget this whole Flow mistake ever happened * translate: Drafts * More translatable strings * Mo translation strings * translation: Search * async translations loading * cache translations in client * Revert "cache translations in client" This reverts commit 08fb61ce36384ff90a704faffe4761eccfb76da1. * Revert localStorage cache for cache headers * Update Crowdin configuration file * Moved translation files to locales folder and fixed english text * Added CONTRIBUTING File for CrowdIn * chore: Move translations again to please CrowdIn * fix: loading paths chore: Add strings for editor * fix: Improve validation on documents.import endpoint * test: mock bull * fix: Unknown mimetype should fallback to Markdown parsing if markdown extension (#1678) * closes #1675 * Update CONTRIBUTING * chore: Add link to translation portal from app UI * refactor: Centralize language config * fix: Ensure creation of i18n directory in build * feat: Add language prompt * chore: Improve contributing guidelines, add link from README * chore: Normalize tab header casing * chore: More string externalization * fix: Language prompt in dark mode Co-authored-by: André Glatzl <andreglatzl@gmail.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
// @flow
|
||||
import { observer, inject } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import DocumentsStore from "stores/DocumentsStore";
|
||||
import CenteredContent from "components/CenteredContent";
|
||||
@@ -14,26 +15,26 @@ type Props = {
|
||||
documents: DocumentsStore,
|
||||
};
|
||||
|
||||
@observer
|
||||
class Archive extends React.Component<Props> {
|
||||
render() {
|
||||
const { documents } = this.props;
|
||||
function Archive(props: Props) {
|
||||
const { t } = useTranslation();
|
||||
const { documents } = props;
|
||||
|
||||
return (
|
||||
<CenteredContent column auto>
|
||||
<PageTitle title="Archive" />
|
||||
<Heading>Archive</Heading>
|
||||
<PaginatedDocumentList
|
||||
documents={documents.archived}
|
||||
fetch={documents.fetchArchived}
|
||||
heading={<Subheading>Documents</Subheading>}
|
||||
empty={<Empty>The document archive is empty at the moment.</Empty>}
|
||||
showCollection
|
||||
showTemplate
|
||||
/>
|
||||
</CenteredContent>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<CenteredContent column auto>
|
||||
<PageTitle title={t("Archive")} />
|
||||
<Heading>{t("Archive")}</Heading>
|
||||
<PaginatedDocumentList
|
||||
documents={documents.archived}
|
||||
fetch={documents.fetchArchived}
|
||||
heading={<Subheading>{t("Documents")}</Subheading>}
|
||||
empty={
|
||||
<Empty>{t("The document archive is empty at the moment.")}</Empty>
|
||||
}
|
||||
showCollection
|
||||
showTemplate
|
||||
/>
|
||||
</CenteredContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default inject("documents")(Archive);
|
||||
export default inject("documents")(observer(Archive));
|
||||
|
||||
@@ -4,6 +4,7 @@ import { observer, inject } from "mobx-react";
|
||||
|
||||
import { NewDocumentIcon, PlusIcon, PinIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { withTranslation, Trans, type TFunction } from "react-i18next";
|
||||
import { Redirect, Link, Switch, Route, type Match } from "react-router-dom";
|
||||
import styled, { withTheme } from "styled-components";
|
||||
|
||||
@@ -47,6 +48,7 @@ type Props = {
|
||||
policies: PoliciesStore,
|
||||
match: Match,
|
||||
theme: Theme,
|
||||
t: TFunction,
|
||||
};
|
||||
|
||||
@observer
|
||||
@@ -64,7 +66,7 @@ class CollectionScene extends React.Component<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
const { id } = this.props.match.params;
|
||||
|
||||
if (this.collection) {
|
||||
@@ -132,7 +134,7 @@ class CollectionScene extends React.Component<Props> {
|
||||
};
|
||||
|
||||
renderActions() {
|
||||
const { match, policies } = this.props;
|
||||
const { match, policies, t } = this.props;
|
||||
const can = policies.abilities(match.params.id || "");
|
||||
|
||||
return (
|
||||
@@ -142,19 +144,19 @@ class CollectionScene extends React.Component<Props> {
|
||||
<Action>
|
||||
<InputSearch
|
||||
source="collection"
|
||||
placeholder="Search in collection…"
|
||||
placeholder={t("Search in collection…")}
|
||||
collectionId={match.params.id}
|
||||
/>
|
||||
</Action>
|
||||
<Action>
|
||||
<Tooltip
|
||||
tooltip="New document"
|
||||
tooltip={t("New document")}
|
||||
shortcut="n"
|
||||
delay={500}
|
||||
placement="bottom"
|
||||
>
|
||||
<Button onClick={this.onNewDocument} icon={<PlusIcon />}>
|
||||
New doc
|
||||
{t("New doc")}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Action>
|
||||
@@ -169,7 +171,7 @@ class CollectionScene extends React.Component<Props> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { documents, theme } = this.props;
|
||||
const { documents, theme, t } = this.props;
|
||||
|
||||
if (this.redirectTo) return <Redirect to={this.redirectTo} push />;
|
||||
if (!this.isFetching && !this.collection) return <Search notFound />;
|
||||
@@ -179,6 +181,7 @@ class CollectionScene extends React.Component<Props> {
|
||||
: [];
|
||||
const hasPinnedDocuments = !!pinnedDocuments.length;
|
||||
const collection = this.collection;
|
||||
const collectionName = collection ? collection.name : "";
|
||||
|
||||
return (
|
||||
<CenteredContent>
|
||||
@@ -188,26 +191,28 @@ class CollectionScene extends React.Component<Props> {
|
||||
{collection.isEmpty ? (
|
||||
<Centered column>
|
||||
<HelpText>
|
||||
<strong>{collection.name}</strong> doesn’t contain any
|
||||
documents yet.
|
||||
<Trans>
|
||||
<strong>{{ collectionName }}</strong> doesn’t contain any
|
||||
documents yet.
|
||||
</Trans>
|
||||
<br />
|
||||
Get started by creating a new one!
|
||||
<Trans>Get started by creating a new one!</Trans>
|
||||
</HelpText>
|
||||
<Wrapper>
|
||||
<Link to={newDocumentUrl(collection.id)}>
|
||||
<Button icon={<NewDocumentIcon color={theme.buttonText} />}>
|
||||
Create a document
|
||||
{t("Create a document")}
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
{collection.private && (
|
||||
<Button onClick={this.onPermissions} neutral>
|
||||
Manage members…
|
||||
{t("Manage members…")}
|
||||
</Button>
|
||||
)}
|
||||
</Wrapper>
|
||||
<Modal
|
||||
title="Collection permissions"
|
||||
title={t("Collection permissions")}
|
||||
onRequestClose={this.handlePermissionsModalClose}
|
||||
isOpen={this.permissionsModalOpen}
|
||||
>
|
||||
@@ -218,7 +223,7 @@ class CollectionScene extends React.Component<Props> {
|
||||
/>
|
||||
</Modal>
|
||||
<Modal
|
||||
title="Edit collection"
|
||||
title={t("Edit collection")}
|
||||
onRequestClose={this.handleEditModalClose}
|
||||
isOpen={this.editModalOpen}
|
||||
>
|
||||
@@ -249,7 +254,7 @@ class CollectionScene extends React.Component<Props> {
|
||||
{hasPinnedDocuments && (
|
||||
<>
|
||||
<Subheading>
|
||||
<TinyPinIcon size={18} /> Pinned
|
||||
<TinyPinIcon size={18} /> {t("Pinned")}
|
||||
</Subheading>
|
||||
<DocumentList documents={pinnedDocuments} showPin />
|
||||
</>
|
||||
@@ -257,16 +262,16 @@ class CollectionScene extends React.Component<Props> {
|
||||
|
||||
<Tabs>
|
||||
<Tab to={collectionUrl(collection.id)} exact>
|
||||
Recently updated
|
||||
{t("Recently updated")}
|
||||
</Tab>
|
||||
<Tab to={collectionUrl(collection.id, "recent")} exact>
|
||||
Recently published
|
||||
{t("Recently published")}
|
||||
</Tab>
|
||||
<Tab to={collectionUrl(collection.id, "old")} exact>
|
||||
Least recently updated
|
||||
{t("Least recently updated")}
|
||||
</Tab>
|
||||
<Tab to={collectionUrl(collection.id, "alphabetical")} exact>
|
||||
A–Z
|
||||
{t("A–Z")}
|
||||
</Tab>
|
||||
</Tabs>
|
||||
<Switch>
|
||||
@@ -351,9 +356,11 @@ const Wrapper = styled(Flex)`
|
||||
margin: 10px 0;
|
||||
`;
|
||||
|
||||
export default inject(
|
||||
"collections",
|
||||
"policies",
|
||||
"documents",
|
||||
"ui"
|
||||
)(withTheme(CollectionScene));
|
||||
export default withTranslation()<CollectionScene>(
|
||||
inject(
|
||||
"collections",
|
||||
"policies",
|
||||
"documents",
|
||||
"ui"
|
||||
)(withTheme(CollectionScene))
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { observable } from "mobx";
|
||||
import { inject, observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { withTranslation, type TFunction } from "react-i18next";
|
||||
import UiStore from "stores/UiStore";
|
||||
import Collection from "models/Collection";
|
||||
import Button from "components/Button";
|
||||
@@ -16,6 +17,7 @@ type Props = {
|
||||
collection: Collection,
|
||||
ui: UiStore,
|
||||
onSubmit: () => void,
|
||||
t: TFunction,
|
||||
};
|
||||
|
||||
@observer
|
||||
@@ -30,6 +32,7 @@ class CollectionEdit extends React.Component<Props> {
|
||||
handleSubmit = async (ev: SyntheticEvent<*>) => {
|
||||
ev.preventDefault();
|
||||
this.isSaving = true;
|
||||
const { t } = this.props;
|
||||
|
||||
try {
|
||||
await this.props.collection.save({
|
||||
@@ -40,7 +43,7 @@ class CollectionEdit extends React.Component<Props> {
|
||||
private: this.private,
|
||||
});
|
||||
this.props.onSubmit();
|
||||
this.props.ui.showToast("The collection was updated");
|
||||
this.props.ui.showToast(t("The collection was updated"));
|
||||
} catch (err) {
|
||||
this.props.ui.showToast(err.message);
|
||||
} finally {
|
||||
@@ -48,7 +51,7 @@ class CollectionEdit extends React.Component<Props> {
|
||||
}
|
||||
};
|
||||
|
||||
handleDescriptionChange = (getValue) => {
|
||||
handleDescriptionChange = (getValue: () => string) => {
|
||||
this.description = getValue();
|
||||
};
|
||||
|
||||
@@ -66,17 +69,20 @@ class CollectionEdit extends React.Component<Props> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
|
||||
return (
|
||||
<Flex column>
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<HelpText>
|
||||
You can edit the name and other details at any time, however doing
|
||||
so often might confuse your team mates.
|
||||
{t(
|
||||
"You can edit the name and other details at any time, however doing so often might confuse your team mates."
|
||||
)}
|
||||
</HelpText>
|
||||
<Flex>
|
||||
<Input
|
||||
type="text"
|
||||
label="Name"
|
||||
label={t("Name")}
|
||||
onChange={this.handleNameChange}
|
||||
value={this.name}
|
||||
required
|
||||
@@ -92,27 +98,29 @@ class CollectionEdit extends React.Component<Props> {
|
||||
</Flex>
|
||||
<InputRich
|
||||
id={this.props.collection.id}
|
||||
label="Description"
|
||||
label={t("Description")}
|
||||
onChange={this.handleDescriptionChange}
|
||||
defaultValue={this.description || ""}
|
||||
placeholder="More details about this collection…"
|
||||
placeholder={t("More details about this collection…")}
|
||||
minHeight={68}
|
||||
maxHeight={200}
|
||||
/>
|
||||
<Switch
|
||||
id="private"
|
||||
label="Private collection"
|
||||
label={t("Private collection")}
|
||||
onChange={this.handlePrivateChange}
|
||||
checked={this.private}
|
||||
/>
|
||||
<HelpText>
|
||||
A private collection will only be visible to invited team members.
|
||||
{t(
|
||||
"A private collection will only be visible to invited team members."
|
||||
)}
|
||||
</HelpText>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={this.isSaving || !this.props.collection.name}
|
||||
>
|
||||
{this.isSaving ? "Saving…" : "Save"}
|
||||
{this.isSaving ? t("Saving…") : t("Save")}
|
||||
</Button>
|
||||
</form>
|
||||
</Flex>
|
||||
@@ -120,4 +128,4 @@ class CollectionEdit extends React.Component<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
export default inject("ui")(CollectionEdit);
|
||||
export default withTranslation()<CollectionEdit>(inject("ui")(CollectionEdit));
|
||||
|
||||
@@ -3,12 +3,14 @@ import { debounce } from "lodash";
|
||||
import { observable } from "mobx";
|
||||
import { inject, observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { withTranslation, type TFunction } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
import AuthStore from "stores/AuthStore";
|
||||
import CollectionGroupMembershipsStore from "stores/CollectionGroupMembershipsStore";
|
||||
import GroupsStore from "stores/GroupsStore";
|
||||
import UiStore from "stores/UiStore";
|
||||
import Collection from "models/Collection";
|
||||
import Group from "models/Group";
|
||||
import GroupNew from "scenes/GroupNew";
|
||||
import Button from "components/Button";
|
||||
import Empty from "components/Empty";
|
||||
@@ -26,6 +28,7 @@ type Props = {
|
||||
collectionGroupMemberships: CollectionGroupMembershipsStore,
|
||||
groups: GroupsStore,
|
||||
onSubmit: () => void,
|
||||
t: TFunction,
|
||||
};
|
||||
|
||||
@observer
|
||||
@@ -52,50 +55,56 @@ class AddGroupsToCollection extends React.Component<Props> {
|
||||
});
|
||||
}, 250);
|
||||
|
||||
handleAddGroup = (group) => {
|
||||
handleAddGroup = (group: Group) => {
|
||||
const { t } = this.props;
|
||||
|
||||
try {
|
||||
this.props.collectionGroupMemberships.create({
|
||||
collectionId: this.props.collection.id,
|
||||
groupId: group.id,
|
||||
permission: "read_write",
|
||||
});
|
||||
this.props.ui.showToast(`${group.name} was added to the collection`);
|
||||
this.props.ui.showToast(
|
||||
t("{{ groupName }} was added to the collection", {
|
||||
groupName: group.name,
|
||||
})
|
||||
);
|
||||
} catch (err) {
|
||||
this.props.ui.showToast("Could not add user");
|
||||
this.props.ui.showToast(t("Could not add user"));
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { groups, collection, auth } = this.props;
|
||||
const { groups, collection, auth, t } = this.props;
|
||||
const { user, team } = auth;
|
||||
if (!user || !team) return null;
|
||||
|
||||
return (
|
||||
<Flex column>
|
||||
<HelpText>
|
||||
Can’t find the group you’re looking for?{" "}
|
||||
{t("Can’t find the group you’re looking for?")}{" "}
|
||||
<a role="button" onClick={this.handleNewGroupModalOpen}>
|
||||
Create a group
|
||||
{t("Create a group")}
|
||||
</a>
|
||||
.
|
||||
</HelpText>
|
||||
|
||||
<Input
|
||||
type="search"
|
||||
placeholder="Search by group name…"
|
||||
placeholder={t("Search by group name…")}
|
||||
value={this.query}
|
||||
onChange={this.handleFilter}
|
||||
label="Search groups"
|
||||
label={t("Search groups")}
|
||||
labelHidden
|
||||
flex
|
||||
/>
|
||||
<PaginatedList
|
||||
empty={
|
||||
this.query ? (
|
||||
<Empty>No groups matching your search</Empty>
|
||||
<Empty>{t("No groups matching your search")}</Empty>
|
||||
) : (
|
||||
<Empty>No groups left to add</Empty>
|
||||
<Empty>{t("No groups left to add")}</Empty>
|
||||
)
|
||||
}
|
||||
items={groups.notInCollection(collection.id, this.query)}
|
||||
@@ -108,7 +117,7 @@ class AddGroupsToCollection extends React.Component<Props> {
|
||||
renderActions={() => (
|
||||
<ButtonWrap>
|
||||
<Button onClick={() => this.handleAddGroup(item)} neutral>
|
||||
Add
|
||||
{t("Add")}
|
||||
</Button>
|
||||
</ButtonWrap>
|
||||
)}
|
||||
@@ -116,7 +125,7 @@ class AddGroupsToCollection extends React.Component<Props> {
|
||||
)}
|
||||
/>
|
||||
<Modal
|
||||
title="Create a group"
|
||||
title={t("Create a group")}
|
||||
onRequestClose={this.handleNewGroupModalClose}
|
||||
isOpen={this.newGroupModalOpen}
|
||||
>
|
||||
@@ -131,9 +140,11 @@ const ButtonWrap = styled.div`
|
||||
margin-left: 6px;
|
||||
`;
|
||||
|
||||
export default inject(
|
||||
"auth",
|
||||
"groups",
|
||||
"collectionGroupMemberships",
|
||||
"ui"
|
||||
)(AddGroupsToCollection);
|
||||
export default withTranslation()<AddGroupsToCollection>(
|
||||
inject(
|
||||
"auth",
|
||||
"groups",
|
||||
"collectionGroupMemberships",
|
||||
"ui"
|
||||
)(AddGroupsToCollection)
|
||||
);
|
||||
|
||||
@@ -3,11 +3,13 @@ import { debounce } from "lodash";
|
||||
import { observable } from "mobx";
|
||||
import { inject, observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { withTranslation, type TFunction } from "react-i18next";
|
||||
import AuthStore from "stores/AuthStore";
|
||||
import MembershipsStore from "stores/MembershipsStore";
|
||||
import UiStore from "stores/UiStore";
|
||||
import UsersStore from "stores/UsersStore";
|
||||
import Collection from "models/Collection";
|
||||
import User from "models/User";
|
||||
import Invite from "scenes/Invite";
|
||||
import Empty from "components/Empty";
|
||||
import Flex from "components/Flex";
|
||||
@@ -24,6 +26,7 @@ type Props = {
|
||||
memberships: MembershipsStore,
|
||||
users: UsersStore,
|
||||
onSubmit: () => void,
|
||||
t: TFunction,
|
||||
};
|
||||
|
||||
@observer
|
||||
@@ -50,40 +53,43 @@ class AddPeopleToCollection extends React.Component<Props> {
|
||||
});
|
||||
}, 250);
|
||||
|
||||
handleAddUser = (user) => {
|
||||
handleAddUser = (user: User) => {
|
||||
const { t } = this.props;
|
||||
try {
|
||||
this.props.memberships.create({
|
||||
collectionId: this.props.collection.id,
|
||||
userId: user.id,
|
||||
permission: "read_write",
|
||||
});
|
||||
this.props.ui.showToast(`${user.name} was added to the collection`);
|
||||
this.props.ui.showToast(
|
||||
t("{{ userName }} was added to the collection", { userName: user.name })
|
||||
);
|
||||
} catch (err) {
|
||||
this.props.ui.showToast("Could not add user");
|
||||
this.props.ui.showToast(t("Could not add user"));
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { users, collection, auth } = this.props;
|
||||
const { users, collection, auth, t } = this.props;
|
||||
const { user, team } = auth;
|
||||
if (!user || !team) return null;
|
||||
|
||||
return (
|
||||
<Flex column>
|
||||
<HelpText>
|
||||
Need to add someone who’s not yet on the team yet?{" "}
|
||||
{t("Need to add someone who’s not yet on the team yet?")}{" "}
|
||||
<a role="button" onClick={this.handleInviteModalOpen}>
|
||||
Invite people to {team.name}
|
||||
{t("Invite people to {{ teamName }}", { teamName: team.name })}
|
||||
</a>
|
||||
.
|
||||
</HelpText>
|
||||
|
||||
<Input
|
||||
type="search"
|
||||
placeholder="Search by name…"
|
||||
placeholder={t("Search by name…")}
|
||||
value={this.query}
|
||||
onChange={this.handleFilter}
|
||||
label="Search people"
|
||||
label={t("Search people")}
|
||||
autoFocus
|
||||
labelHidden
|
||||
flex
|
||||
@@ -91,9 +97,9 @@ class AddPeopleToCollection extends React.Component<Props> {
|
||||
<PaginatedList
|
||||
empty={
|
||||
this.query ? (
|
||||
<Empty>No people matching your search</Empty>
|
||||
<Empty>{t("No people matching your search")}</Empty>
|
||||
) : (
|
||||
<Empty>No people left to add</Empty>
|
||||
<Empty>{t("No people left to add")}</Empty>
|
||||
)
|
||||
}
|
||||
items={users.notInCollection(collection.id, this.query)}
|
||||
@@ -108,7 +114,7 @@ class AddPeopleToCollection extends React.Component<Props> {
|
||||
)}
|
||||
/>
|
||||
<Modal
|
||||
title="Invite people"
|
||||
title={t("Invite people")}
|
||||
onRequestClose={this.handleInviteModalClose}
|
||||
isOpen={this.inviteModalOpen}
|
||||
>
|
||||
@@ -119,9 +125,6 @@ class AddPeopleToCollection extends React.Component<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
export default inject(
|
||||
"auth",
|
||||
"users",
|
||||
"memberships",
|
||||
"ui"
|
||||
)(AddPeopleToCollection);
|
||||
export default withTranslation()<AddPeopleToCollection>(
|
||||
inject("auth", "users", "memberships", "ui")(AddPeopleToCollection)
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
import CollectionGroupMembership from "models/CollectionGroupMembership";
|
||||
import Group from "models/Group";
|
||||
@@ -7,10 +8,6 @@ import { DropdownMenu, DropdownMenuItem } from "components/DropdownMenu";
|
||||
import GroupListItem from "components/GroupListItem";
|
||||
import InputSelect from "components/InputSelect";
|
||||
|
||||
const PERMISSIONS = [
|
||||
{ label: "Read only", value: "read" },
|
||||
{ label: "Read & Edit", value: "read_write" },
|
||||
];
|
||||
type Props = {
|
||||
group: Group,
|
||||
collectionGroupMembership: ?CollectionGroupMembership,
|
||||
@@ -24,6 +21,16 @@ const MemberListItem = ({
|
||||
onUpdate,
|
||||
onRemove,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const PERMISSIONS = React.useMemo(
|
||||
() => [
|
||||
{ label: t("Read only"), value: "read" },
|
||||
{ label: t("Read & Edit"), value: "read_write" },
|
||||
],
|
||||
[t]
|
||||
);
|
||||
|
||||
return (
|
||||
<GroupListItem
|
||||
group={group}
|
||||
@@ -32,7 +39,7 @@ const MemberListItem = ({
|
||||
renderActions={({ openMembersModal }) => (
|
||||
<>
|
||||
<Select
|
||||
label="Permissions"
|
||||
label={t("Permissions")}
|
||||
options={PERMISSIONS}
|
||||
value={
|
||||
collectionGroupMembership
|
||||
@@ -45,10 +52,12 @@ const MemberListItem = ({
|
||||
<ButtonWrap>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuItem onClick={openMembersModal}>
|
||||
Members…
|
||||
{t("Members…")}
|
||||
</DropdownMenuItem>
|
||||
<hr />
|
||||
<DropdownMenuItem onClick={onRemove}>Remove</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={onRemove}>
|
||||
{t("Remove")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenu>
|
||||
</ButtonWrap>
|
||||
</>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
import Membership from "models/Membership";
|
||||
import User from "models/User";
|
||||
@@ -12,10 +13,6 @@ import InputSelect from "components/InputSelect";
|
||||
import ListItem from "components/List/Item";
|
||||
import Time from "components/Time";
|
||||
|
||||
const PERMISSIONS = [
|
||||
{ label: "Read only", value: "read" },
|
||||
{ label: "Read & Edit", value: "read_write" },
|
||||
];
|
||||
type Props = {
|
||||
user: User,
|
||||
membership?: ?Membership,
|
||||
@@ -33,6 +30,16 @@ const MemberListItem = ({
|
||||
onAdd,
|
||||
canEdit,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const PERMISSIONS = React.useMemo(
|
||||
() => [
|
||||
{ label: t("Read only"), value: "read" },
|
||||
{ label: t("Read & Edit"), value: "read_write" },
|
||||
],
|
||||
[t]
|
||||
);
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
title={user.name}
|
||||
@@ -40,13 +47,15 @@ const MemberListItem = ({
|
||||
<>
|
||||
{user.lastActiveAt ? (
|
||||
<>
|
||||
Active <Time dateTime={user.lastActiveAt} /> ago
|
||||
{t("Active {{ lastActiveAt }} ago", {
|
||||
lastActiveAt: <Time dateTime={user.lastActiveAt} />,
|
||||
})}
|
||||
</>
|
||||
) : (
|
||||
"Never signed in"
|
||||
t("Never signed in")
|
||||
)}
|
||||
{user.isInvited && <Badge>Invited</Badge>}
|
||||
{user.isAdmin && <Badge primary={user.isAdmin}>Admin</Badge>}
|
||||
{user.isInvited && <Badge>{t("Invited")}</Badge>}
|
||||
{user.isAdmin && <Badge primary={user.isAdmin}>{t("Admin")}</Badge>}
|
||||
</>
|
||||
}
|
||||
image={<Avatar src={user.avatarUrl} size={40} />}
|
||||
@@ -54,7 +63,7 @@ const MemberListItem = ({
|
||||
<Flex align="center">
|
||||
{canEdit && onUpdate && (
|
||||
<Select
|
||||
label="Permissions"
|
||||
label={t("Permissions")}
|
||||
options={PERMISSIONS}
|
||||
value={membership ? membership.permission : undefined}
|
||||
onChange={(ev) => onUpdate(ev.target.value)}
|
||||
@@ -64,12 +73,14 @@ const MemberListItem = ({
|
||||
|
||||
{canEdit && onRemove && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuItem onClick={onRemove}>Remove</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={onRemove}>
|
||||
{t("Remove")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
{canEdit && onAdd && (
|
||||
<Button onClick={onAdd} neutral>
|
||||
Add
|
||||
{t("Add")}
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// @flow
|
||||
import { PlusIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import User from "models/User";
|
||||
import Avatar from "components/Avatar";
|
||||
import Badge from "components/Badge";
|
||||
@@ -15,6 +16,8 @@ type Props = {
|
||||
};
|
||||
|
||||
const UserListItem = ({ user, onAdd, canEdit }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
title={user.name}
|
||||
@@ -23,19 +26,21 @@ const UserListItem = ({ user, onAdd, canEdit }: Props) => {
|
||||
<>
|
||||
{user.lastActiveAt ? (
|
||||
<>
|
||||
Active <Time dateTime={user.lastActiveAt} /> ago
|
||||
{t("Active {{ lastActiveAt }} ago", {
|
||||
lastActiveAt: <Time dateTime={user.lastActiveAt} />,
|
||||
})}
|
||||
</>
|
||||
) : (
|
||||
"Never signed in"
|
||||
t("Never signed in")
|
||||
)}
|
||||
{user.isInvited && <Badge>Invited</Badge>}
|
||||
{user.isAdmin && <Badge primary={user.isAdmin}>Admin</Badge>}
|
||||
{user.isInvited && <Badge>{t("Invited")}</Badge>}
|
||||
{user.isAdmin && <Badge primary={user.isAdmin}>{t("Admin")}</Badge>}
|
||||
</>
|
||||
}
|
||||
actions={
|
||||
canEdit ? (
|
||||
<Button type="button" onClick={onAdd} icon={<PlusIcon />} neutral>
|
||||
Add
|
||||
{t("Add")}
|
||||
</Button>
|
||||
) : undefined
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { intersection } from "lodash";
|
||||
import { observable } from "mobx";
|
||||
import { inject, observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { withTranslation, type TFunction } from "react-i18next";
|
||||
import { withRouter, type RouterHistory } from "react-router-dom";
|
||||
import CollectionsStore from "stores/CollectionsStore";
|
||||
import UiStore from "stores/UiStore";
|
||||
@@ -20,6 +21,7 @@ type Props = {
|
||||
ui: UiStore,
|
||||
collections: CollectionsStore,
|
||||
onSubmit: () => void,
|
||||
t: TFunction,
|
||||
};
|
||||
|
||||
@observer
|
||||
@@ -84,7 +86,7 @@ class CollectionNew extends React.Component<Props> {
|
||||
this.hasOpenedIconPicker = true;
|
||||
};
|
||||
|
||||
handleDescriptionChange = (getValue) => {
|
||||
handleDescriptionChange = (getValue: () => string) => {
|
||||
this.description = getValue();
|
||||
};
|
||||
|
||||
@@ -98,17 +100,19 @@ class CollectionNew extends React.Component<Props> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
|
||||
return (
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<HelpText>
|
||||
Collections are for grouping your knowledge base. They work best when
|
||||
organized around a topic or internal team — Product or Engineering for
|
||||
example.
|
||||
{t(
|
||||
"Collections are for grouping your knowledge base. They work best when organized around a topic or internal team — Product or Engineering for example."
|
||||
)}
|
||||
</HelpText>
|
||||
<Flex>
|
||||
<Input
|
||||
type="text"
|
||||
label="Name"
|
||||
label={t("Name")}
|
||||
onChange={this.handleNameChange}
|
||||
value={this.name}
|
||||
required
|
||||
@@ -124,29 +128,33 @@ class CollectionNew extends React.Component<Props> {
|
||||
/>
|
||||
</Flex>
|
||||
<InputRich
|
||||
label="Description"
|
||||
label={t("Description")}
|
||||
onChange={this.handleDescriptionChange}
|
||||
defaultValue={this.description || ""}
|
||||
placeholder="More details about this collection…"
|
||||
placeholder={t("More details about this collection…")}
|
||||
minHeight={68}
|
||||
maxHeight={200}
|
||||
/>
|
||||
<Switch
|
||||
id="private"
|
||||
label="Private collection"
|
||||
label={t("Private collection")}
|
||||
onChange={this.handlePrivateChange}
|
||||
checked={this.private}
|
||||
/>
|
||||
<HelpText>
|
||||
A private collection will only be visible to invited team members.
|
||||
{t(
|
||||
"A private collection will only be visible to invited team members."
|
||||
)}
|
||||
</HelpText>
|
||||
|
||||
<Button type="submit" disabled={this.isSaving || !this.name}>
|
||||
{this.isSaving ? "Creating…" : "Create"}
|
||||
{this.isSaving ? t("Creating…") : t("Create")}
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default inject("collections", "ui")(withRouter(CollectionNew));
|
||||
export default withTranslation()<CollectionNew>(
|
||||
inject("collections", "ui")(withRouter(CollectionNew))
|
||||
);
|
||||
|
||||
@@ -1,81 +1,77 @@
|
||||
// @flow
|
||||
import { observer, inject } from "mobx-react";
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Switch, Route } from "react-router-dom";
|
||||
|
||||
import AuthStore from "stores/AuthStore";
|
||||
import DocumentsStore from "stores/DocumentsStore";
|
||||
import Actions, { Action } from "components/Actions";
|
||||
import CenteredContent from "components/CenteredContent";
|
||||
import InputSearch from "components/InputSearch";
|
||||
import LanguagePrompt from "components/LanguagePrompt";
|
||||
import PageTitle from "components/PageTitle";
|
||||
import Tab from "components/Tab";
|
||||
import Tabs from "components/Tabs";
|
||||
import PaginatedDocumentList from "../components/PaginatedDocumentList";
|
||||
import useStores from "../hooks/useStores";
|
||||
import NewDocumentMenu from "menus/NewDocumentMenu";
|
||||
|
||||
type Props = {
|
||||
documents: DocumentsStore,
|
||||
auth: AuthStore,
|
||||
};
|
||||
function Dashboard() {
|
||||
const { documents, ui, auth } = useStores();
|
||||
const { t } = useTranslation();
|
||||
|
||||
@observer
|
||||
class Dashboard extends React.Component<Props> {
|
||||
render() {
|
||||
const { documents, auth } = this.props;
|
||||
if (!auth.user || !auth.team) return null;
|
||||
const user = auth.user.id;
|
||||
if (!auth.user || !auth.team) return null;
|
||||
const user = auth.user.id;
|
||||
|
||||
return (
|
||||
<CenteredContent>
|
||||
<PageTitle title="Home" />
|
||||
<h1>Home</h1>
|
||||
<Tabs>
|
||||
<Tab to="/home" exact>
|
||||
Recently updated
|
||||
</Tab>
|
||||
<Tab to="/home/recent" exact>
|
||||
Recently viewed
|
||||
</Tab>
|
||||
<Tab to="/home/created">Created by me</Tab>
|
||||
</Tabs>
|
||||
<Switch>
|
||||
<Route path="/home/recent">
|
||||
<PaginatedDocumentList
|
||||
key="recent"
|
||||
documents={documents.recentlyViewed}
|
||||
fetch={documents.fetchRecentlyViewed}
|
||||
showCollection
|
||||
/>
|
||||
</Route>
|
||||
<Route path="/home/created">
|
||||
<PaginatedDocumentList
|
||||
key="created"
|
||||
documents={documents.createdByUser(user)}
|
||||
fetch={documents.fetchOwned}
|
||||
options={{ user }}
|
||||
showCollection
|
||||
/>
|
||||
</Route>
|
||||
<Route path="/home">
|
||||
<PaginatedDocumentList
|
||||
documents={documents.recentlyUpdated}
|
||||
fetch={documents.fetchRecentlyUpdated}
|
||||
showCollection
|
||||
/>
|
||||
</Route>
|
||||
</Switch>
|
||||
<Actions align="center" justify="flex-end">
|
||||
<Action>
|
||||
<InputSearch source="dashboard" />
|
||||
</Action>
|
||||
<Action>
|
||||
<NewDocumentMenu />
|
||||
</Action>
|
||||
</Actions>
|
||||
</CenteredContent>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<CenteredContent>
|
||||
<PageTitle title={t("Home")} />
|
||||
{!ui.languagePromptDismissed && <LanguagePrompt />}
|
||||
<h1>{t("Home")}</h1>
|
||||
<Tabs>
|
||||
<Tab to="/home" exact>
|
||||
{t("Recently updated")}
|
||||
</Tab>
|
||||
<Tab to="/home/recent" exact>
|
||||
{t("Recently viewed")}
|
||||
</Tab>
|
||||
<Tab to="/home/created">{t("Created by me")}</Tab>
|
||||
</Tabs>
|
||||
<Switch>
|
||||
<Route path="/home/recent">
|
||||
<PaginatedDocumentList
|
||||
key="recent"
|
||||
documents={documents.recentlyViewed}
|
||||
fetch={documents.fetchRecentlyViewed}
|
||||
showCollection
|
||||
/>
|
||||
</Route>
|
||||
<Route path="/home/created">
|
||||
<PaginatedDocumentList
|
||||
key="created"
|
||||
documents={documents.createdByUser(user)}
|
||||
fetch={documents.fetchOwned}
|
||||
options={{ user }}
|
||||
showCollection
|
||||
/>
|
||||
</Route>
|
||||
<Route path="/home">
|
||||
<PaginatedDocumentList
|
||||
documents={documents.recentlyUpdated}
|
||||
fetch={documents.fetchRecentlyUpdated}
|
||||
showCollection
|
||||
/>
|
||||
</Route>
|
||||
</Switch>
|
||||
<Actions align="center" justify="flex-end">
|
||||
<Action>
|
||||
<InputSearch source="dashboard" />
|
||||
</Action>
|
||||
<Action>
|
||||
<NewDocumentMenu />
|
||||
</Action>
|
||||
</Actions>
|
||||
</CenteredContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default inject("documents", "auth")(Dashboard);
|
||||
export default observer(Dashboard);
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from "outline-icons";
|
||||
import { transparentize, darken } from "polished";
|
||||
import * as React from "react";
|
||||
import { withTranslation, Trans, type TFunction } from "react-i18next";
|
||||
import { Redirect } from "react-router-dom";
|
||||
import styled from "styled-components";
|
||||
import breakpoint from "styled-components-breakpoint";
|
||||
@@ -55,6 +56,7 @@ type Props = {
|
||||
publish?: boolean,
|
||||
autosave?: boolean,
|
||||
}) => void,
|
||||
t: TFunction,
|
||||
};
|
||||
|
||||
@observer
|
||||
@@ -131,6 +133,7 @@ class Header extends React.Component<Props> {
|
||||
publishingIsDisabled,
|
||||
ui,
|
||||
auth,
|
||||
t,
|
||||
} = this.props;
|
||||
|
||||
const share = shares.getByDocumentId(document.id);
|
||||
@@ -153,7 +156,7 @@ class Header extends React.Component<Props> {
|
||||
<Modal
|
||||
isOpen={this.showShareModal}
|
||||
onRequestClose={this.handleCloseShareModal}
|
||||
title="Share document"
|
||||
title={t("Share document")}
|
||||
>
|
||||
<DocumentShare
|
||||
document={document}
|
||||
@@ -166,7 +169,9 @@ class Header extends React.Component<Props> {
|
||||
<>
|
||||
<Slash />
|
||||
<Tooltip
|
||||
tooltip={ui.tocVisible ? "Hide contents" : "Show contents"}
|
||||
tooltip={
|
||||
ui.tocVisible ? t("Hide contents") : t("Show contents")
|
||||
}
|
||||
shortcut={`ctrl+${meta}+h`}
|
||||
delay={250}
|
||||
placement="bottom"
|
||||
@@ -190,14 +195,15 @@ class Header extends React.Component<Props> {
|
||||
{this.isScrolled && (
|
||||
<Title onClick={this.handleClickTitle}>
|
||||
<Fade>
|
||||
{document.title} {document.isArchived && <Badge>Archived</Badge>}
|
||||
{document.title}{" "}
|
||||
{document.isArchived && <Badge>{t("Archived")}</Badge>}
|
||||
</Fade>
|
||||
</Title>
|
||||
)}
|
||||
<Wrapper align="center" justify="flex-end">
|
||||
{isSaving && !isPublishing && (
|
||||
<Action>
|
||||
<Status>Saving…</Status>
|
||||
<Status>{t("Saving…")}</Status>
|
||||
</Action>
|
||||
)}
|
||||
|
||||
@@ -217,10 +223,10 @@ class Header extends React.Component<Props> {
|
||||
<Tooltip
|
||||
tooltip={
|
||||
isPubliclyShared ? (
|
||||
<>
|
||||
<Trans>
|
||||
Anyone with the link <br />
|
||||
can view this document
|
||||
</>
|
||||
</Trans>
|
||||
) : (
|
||||
""
|
||||
)
|
||||
@@ -234,7 +240,7 @@ class Header extends React.Component<Props> {
|
||||
neutral
|
||||
small
|
||||
>
|
||||
Share
|
||||
{t("Share")}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Action>
|
||||
@@ -243,7 +249,7 @@ class Header extends React.Component<Props> {
|
||||
<>
|
||||
<Action>
|
||||
<Tooltip
|
||||
tooltip="Save"
|
||||
tooltip={t("Save")}
|
||||
shortcut={`${meta}+enter`}
|
||||
delay={500}
|
||||
placement="bottom"
|
||||
@@ -255,7 +261,7 @@ class Header extends React.Component<Props> {
|
||||
neutral={isDraft}
|
||||
small
|
||||
>
|
||||
{isDraft ? "Save Draft" : "Done Editing"}
|
||||
{isDraft ? t("Save Draft") : t("Done Editing")}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Action>
|
||||
@@ -264,7 +270,7 @@ class Header extends React.Component<Props> {
|
||||
{canEdit && (
|
||||
<Action>
|
||||
<Tooltip
|
||||
tooltip={`Edit ${document.noun}`}
|
||||
tooltip={t("Edit {{noun}}", { noun: document.noun })}
|
||||
shortcut="e"
|
||||
delay={500}
|
||||
placement="bottom"
|
||||
@@ -275,7 +281,7 @@ class Header extends React.Component<Props> {
|
||||
neutral
|
||||
small
|
||||
>
|
||||
Edit
|
||||
{t("Edit")}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Action>
|
||||
@@ -286,13 +292,13 @@ class Header extends React.Component<Props> {
|
||||
document={document}
|
||||
label={
|
||||
<Tooltip
|
||||
tooltip="New document"
|
||||
tooltip={t("New document")}
|
||||
shortcut="n"
|
||||
delay={500}
|
||||
placement="bottom"
|
||||
>
|
||||
<Button icon={<PlusIcon />} neutral>
|
||||
New doc
|
||||
{t("New doc")}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
}
|
||||
@@ -307,25 +313,25 @@ class Header extends React.Component<Props> {
|
||||
primary
|
||||
small
|
||||
>
|
||||
New from template
|
||||
{t("New from template")}
|
||||
</Button>
|
||||
</Action>
|
||||
)}
|
||||
{can.update && isDraft && !isRevision && (
|
||||
<Action>
|
||||
<Tooltip
|
||||
tooltip="Publish"
|
||||
tooltip={t("Publish")}
|
||||
shortcut={`${meta}+shift+p`}
|
||||
delay={500}
|
||||
placement="bottom"
|
||||
>
|
||||
<Button
|
||||
onClick={this.handlePublish}
|
||||
title="Publish document"
|
||||
title={t("Publish document")}
|
||||
disabled={publishingIsDisabled}
|
||||
small
|
||||
>
|
||||
{isPublishing ? "Publishing…" : "Publish"}
|
||||
{isPublishing ? t("Publishing…") : t("Publish")}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Action>
|
||||
@@ -425,4 +431,6 @@ const Title = styled.div`
|
||||
`};
|
||||
`;
|
||||
|
||||
export default inject("auth", "ui", "policies", "shares")(Header);
|
||||
export default withTranslation()<Header>(
|
||||
inject("auth", "ui", "policies", "shares")(Header)
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import CenteredContent from "components/CenteredContent";
|
||||
import LoadingPlaceholder from "components/LoadingPlaceholder";
|
||||
import PageTitle from "components/PageTitle";
|
||||
@@ -11,9 +12,13 @@ type Props = {|
|
||||
|};
|
||||
|
||||
export default function Loading({ location }: Props) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Container column auto>
|
||||
<PageTitle title={location.state ? location.state.title : "Untitled"} />
|
||||
<PageTitle
|
||||
title={location.state ? location.state.title : t("Untitled")}
|
||||
/>
|
||||
<CenteredContent>
|
||||
<LoadingPlaceholder />
|
||||
</CenteredContent>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { observable } from "mobx";
|
||||
import { inject, observer } from "mobx-react";
|
||||
import queryString from "query-string";
|
||||
import * as React from "react";
|
||||
import { withTranslation, type TFunction } from "react-i18next";
|
||||
import { type RouterHistory } from "react-router-dom";
|
||||
import styled from "styled-components";
|
||||
import DocumentsStore from "stores/DocumentsStore";
|
||||
@@ -25,6 +26,7 @@ type Props = {|
|
||||
documents: DocumentsStore,
|
||||
history: RouterHistory,
|
||||
location: LocationWithState,
|
||||
t: TFunction,
|
||||
|};
|
||||
|
||||
@observer
|
||||
@@ -33,7 +35,7 @@ class Drafts extends React.Component<Props> {
|
||||
this.props.location.search
|
||||
);
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
if (prevProps.location.search !== this.props.location.search) {
|
||||
this.handleQueryChange();
|
||||
}
|
||||
@@ -43,7 +45,10 @@ class Drafts extends React.Component<Props> {
|
||||
this.params = new URLSearchParams(this.props.location.search);
|
||||
};
|
||||
|
||||
handleFilterChange = (search) => {
|
||||
handleFilterChange = (search: {
|
||||
dateFilter?: ?string,
|
||||
collectionId?: ?string,
|
||||
}) => {
|
||||
this.props.history.replace({
|
||||
pathname: this.props.location.pathname,
|
||||
search: queryString.stringify({
|
||||
@@ -64,6 +69,7 @@ class Drafts extends React.Component<Props> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
const { drafts, fetchDrafts } = this.props.documents;
|
||||
const isFiltered = this.collectionId || this.dateFilter;
|
||||
const options = {
|
||||
@@ -73,10 +79,10 @@ class Drafts extends React.Component<Props> {
|
||||
|
||||
return (
|
||||
<CenteredContent column auto>
|
||||
<PageTitle title="Drafts" />
|
||||
<Heading>Drafts</Heading>
|
||||
<PageTitle title={t("Drafts")} />
|
||||
<Heading>{t("Drafts")}</Heading>
|
||||
<Subheading>
|
||||
Documents
|
||||
{t("Documents")}
|
||||
<Filters>
|
||||
<CollectionFilter
|
||||
collectionId={this.collectionId}
|
||||
@@ -95,8 +101,8 @@ class Drafts extends React.Component<Props> {
|
||||
empty={
|
||||
<Empty>
|
||||
{isFiltered
|
||||
? "No documents found for your filters."
|
||||
: "You’ve not got any drafts at the moment."}
|
||||
? t("No documents found for your filters.")
|
||||
: t("You’ve not got any drafts at the moment.")}
|
||||
</Empty>
|
||||
}
|
||||
fetch={fetchDrafts}
|
||||
@@ -131,4 +137,4 @@ const Filters = styled(Flex)`
|
||||
}
|
||||
`;
|
||||
|
||||
export default inject("documents")(Drafts);
|
||||
export default withTranslation()<Drafts>(inject("documents")(Drafts));
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import { useTranslation, Trans } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import CenteredContent from "components/CenteredContent";
|
||||
import Empty from "components/Empty";
|
||||
import PageTitle from "components/PageTitle";
|
||||
|
||||
const Error404 = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<CenteredContent>
|
||||
<PageTitle title="Not Found" />
|
||||
<h1>Not found</h1>
|
||||
<PageTitle title={t("Not found")} />
|
||||
<h1>{t("Not found")}</h1>
|
||||
<Empty>
|
||||
We were unable to find the page you’re looking for. Go to the{" "}
|
||||
<Link to="/home">homepage</Link>?
|
||||
<Trans>
|
||||
We were unable to find the page you’re looking for. Go to the{" "}
|
||||
<Link to="/home">homepage</Link>?
|
||||
</Trans>
|
||||
</Empty>
|
||||
</CenteredContent>
|
||||
);
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import CenteredContent from "components/CenteredContent";
|
||||
import Empty from "components/Empty";
|
||||
import PageTitle from "components/PageTitle";
|
||||
|
||||
const ErrorOffline = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<CenteredContent>
|
||||
<PageTitle title="Offline" />
|
||||
<h1>Offline</h1>
|
||||
<Empty>We were unable to load the document while offline.</Empty>
|
||||
<PageTitle title={t("Offline")} />
|
||||
<h1>{t("Offline")}</h1>
|
||||
<Empty>{t("We were unable to load the document while offline.")}</Empty>
|
||||
</CenteredContent>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,29 +1,37 @@
|
||||
// @flow
|
||||
import { inject, observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { useTranslation, Trans } from "react-i18next";
|
||||
|
||||
import AuthStore from "stores/AuthStore";
|
||||
import CenteredContent from "components/CenteredContent";
|
||||
import PageTitle from "components/PageTitle";
|
||||
|
||||
const ErrorSuspended = observer(({ auth }: { auth: AuthStore }) => {
|
||||
const ErrorSuspended = ({ auth }: { auth: AuthStore }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<CenteredContent>
|
||||
<PageTitle title="Your account has been suspended" />
|
||||
<PageTitle title={t("Your account has been suspended")} />
|
||||
<h1>
|
||||
<span role="img" aria-label="Warning sign">
|
||||
⚠️
|
||||
</span>{" "}
|
||||
Your account has been suspended
|
||||
{t("Your account has been suspended")}
|
||||
</h1>
|
||||
|
||||
<p>
|
||||
A team admin (<strong>{auth.suspendedContactEmail}</strong>) has
|
||||
suspended your account. To re-activate your account, please reach out to
|
||||
them directly.
|
||||
<Trans>
|
||||
A team admin (
|
||||
<strong>
|
||||
{{ suspendedContactEmail: auth.suspendedContactEmail }}
|
||||
</strong>
|
||||
) has suspended your account. To re-activate your account, please
|
||||
reach out to them directly.
|
||||
</Trans>
|
||||
</p>
|
||||
</CenteredContent>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export default inject("auth")(ErrorSuspended);
|
||||
export default inject("auth")(observer(ErrorSuspended));
|
||||
|
||||
@@ -3,11 +3,13 @@ import { debounce } from "lodash";
|
||||
import { observable } from "mobx";
|
||||
import { inject, observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { withTranslation, type TFunction } from "react-i18next";
|
||||
import AuthStore from "stores/AuthStore";
|
||||
import GroupMembershipsStore from "stores/GroupMembershipsStore";
|
||||
import UiStore from "stores/UiStore";
|
||||
import UsersStore from "stores/UsersStore";
|
||||
import Group from "models/Group";
|
||||
import User from "models/User";
|
||||
import Invite from "scenes/Invite";
|
||||
import Empty from "components/Empty";
|
||||
import Flex from "components/Flex";
|
||||
@@ -24,6 +26,7 @@ type Props = {
|
||||
groupMemberships: GroupMembershipsStore,
|
||||
users: UsersStore,
|
||||
onSubmit: () => void,
|
||||
t: TFunction,
|
||||
};
|
||||
|
||||
@observer
|
||||
@@ -50,40 +53,45 @@ class AddPeopleToGroup extends React.Component<Props> {
|
||||
});
|
||||
}, 250);
|
||||
|
||||
handleAddUser = async (user) => {
|
||||
handleAddUser = async (user: User) => {
|
||||
const { t } = this.props;
|
||||
|
||||
try {
|
||||
await this.props.groupMemberships.create({
|
||||
groupId: this.props.group.id,
|
||||
userId: user.id,
|
||||
});
|
||||
this.props.ui.showToast(`${user.name} was added to the group`);
|
||||
this.props.ui.showToast(
|
||||
t(`{{userName}} was added to the group`, { userName: user.name })
|
||||
);
|
||||
} catch (err) {
|
||||
this.props.ui.showToast("Could not add user");
|
||||
this.props.ui.showToast(t("Could not add user"));
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { users, group, auth } = this.props;
|
||||
const { users, group, auth, t } = this.props;
|
||||
const { user, team } = auth;
|
||||
if (!user || !team) return null;
|
||||
|
||||
return (
|
||||
<Flex column>
|
||||
<HelpText>
|
||||
Add team members below to give them access to the group. Need to add
|
||||
someone who’s not yet on the team yet?{" "}
|
||||
{t(
|
||||
"Add team members below to give them access to the group. Need to add someone who’s not yet on the team yet?"
|
||||
)}{" "}
|
||||
<a role="button" onClick={this.handleInviteModalOpen}>
|
||||
Invite them to {team.name}
|
||||
{t("Invite them to {{teamName}}", { teamName: team.name })}
|
||||
</a>
|
||||
.
|
||||
</HelpText>
|
||||
|
||||
<Input
|
||||
type="search"
|
||||
placeholder="Search by name…"
|
||||
placeholder={t("Search by name…")}
|
||||
value={this.query}
|
||||
onChange={this.handleFilter}
|
||||
label="Search people"
|
||||
label={t("Search people")}
|
||||
labelHidden
|
||||
autoFocus
|
||||
flex
|
||||
@@ -91,9 +99,9 @@ class AddPeopleToGroup extends React.Component<Props> {
|
||||
<PaginatedList
|
||||
empty={
|
||||
this.query ? (
|
||||
<Empty>No people matching your search</Empty>
|
||||
<Empty>{t("No people matching your search")}</Empty>
|
||||
) : (
|
||||
<Empty>No people left to add</Empty>
|
||||
<Empty>{t("No people left to add")}</Empty>
|
||||
)
|
||||
}
|
||||
items={users.notInGroup(group.id, this.query)}
|
||||
@@ -108,7 +116,7 @@ class AddPeopleToGroup extends React.Component<Props> {
|
||||
)}
|
||||
/>
|
||||
<Modal
|
||||
title="Invite people"
|
||||
title={t("Invite people")}
|
||||
onRequestClose={this.handleInviteModalClose}
|
||||
isOpen={this.inviteModalOpen}
|
||||
>
|
||||
@@ -119,9 +127,6 @@ class AddPeopleToGroup extends React.Component<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
export default inject(
|
||||
"auth",
|
||||
"users",
|
||||
"groupMemberships",
|
||||
"ui"
|
||||
)(AddPeopleToGroup);
|
||||
export default withTranslation()<AddPeopleToGroup>(
|
||||
inject("auth", "users", "groupMemberships", "ui")(AddPeopleToGroup)
|
||||
);
|
||||
|
||||
@@ -3,12 +3,14 @@ import { observable } from "mobx";
|
||||
import { inject, observer } from "mobx-react";
|
||||
import { PlusIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { withTranslation, type TFunction } from "react-i18next";
|
||||
import AuthStore from "stores/AuthStore";
|
||||
import GroupMembershipsStore from "stores/GroupMembershipsStore";
|
||||
import PoliciesStore from "stores/PoliciesStore";
|
||||
import UiStore from "stores/UiStore";
|
||||
import UsersStore from "stores/UsersStore";
|
||||
import Group from "models/Group";
|
||||
import User from "models/User";
|
||||
import Button from "components/Button";
|
||||
import Empty from "components/Empty";
|
||||
import Flex from "components/Flex";
|
||||
@@ -26,6 +28,7 @@ type Props = {
|
||||
users: UsersStore,
|
||||
policies: PoliciesStore,
|
||||
groupMemberships: GroupMembershipsStore,
|
||||
t: TFunction,
|
||||
};
|
||||
|
||||
@observer
|
||||
@@ -40,20 +43,24 @@ class GroupMembers extends React.Component<Props> {
|
||||
this.addModalOpen = false;
|
||||
};
|
||||
|
||||
handleRemoveUser = async (user) => {
|
||||
handleRemoveUser = async (user: User) => {
|
||||
const { t } = this.props;
|
||||
|
||||
try {
|
||||
await this.props.groupMemberships.delete({
|
||||
groupId: this.props.group.id,
|
||||
userId: user.id,
|
||||
});
|
||||
this.props.ui.showToast(`${user.name} was removed from the group`);
|
||||
this.props.ui.showToast(
|
||||
t(`{{userName}} was removed from the group`, { userName: user.name })
|
||||
);
|
||||
} catch (err) {
|
||||
this.props.ui.showToast("Could not remove user");
|
||||
this.props.ui.showToast(t("Could not remove user"));
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { group, users, groupMemberships, policies, auth } = this.props;
|
||||
const { group, users, groupMemberships, policies, t, auth } = this.props;
|
||||
const { user } = auth;
|
||||
if (!user) return null;
|
||||
|
||||
@@ -75,7 +82,7 @@ class GroupMembers extends React.Component<Props> {
|
||||
icon={<PlusIcon />}
|
||||
neutral
|
||||
>
|
||||
Add people…
|
||||
{t("Add people…")}
|
||||
</Button>
|
||||
</span>
|
||||
</>
|
||||
@@ -90,7 +97,7 @@ class GroupMembers extends React.Component<Props> {
|
||||
items={users.inGroup(group.id)}
|
||||
fetch={groupMemberships.fetchPage}
|
||||
options={{ id: group.id }}
|
||||
empty={<Empty>This group has no members.</Empty>}
|
||||
empty={<Empty>{t("This group has no members.")}</Empty>}
|
||||
renderItem={(item) => (
|
||||
<GroupMemberListItem
|
||||
key={item.id}
|
||||
@@ -119,10 +126,6 @@ class GroupMembers extends React.Component<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
export default inject(
|
||||
"auth",
|
||||
"users",
|
||||
"policies",
|
||||
"groupMemberships",
|
||||
"ui"
|
||||
)(GroupMembers);
|
||||
export default withTranslation()<GroupMembers>(
|
||||
inject("auth", "users", "policies", "groupMemberships", "ui")(GroupMembers)
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
import Flex from "components/Flex";
|
||||
import HelpText from "components/HelpText";
|
||||
@@ -7,153 +8,150 @@ import Key from "components/Key";
|
||||
import { meta } from "utils/keyboard";
|
||||
|
||||
function KeyboardShortcuts() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Flex column>
|
||||
<HelpText>
|
||||
Outline is designed to be fast and easy to use. All of your usual
|
||||
keyboard shortcuts work here, and there’s Markdown too.
|
||||
{t(
|
||||
"Outline is designed to be fast and easy to use. All of your usual keyboard shortcuts work here, and there’s Markdown too."
|
||||
)}
|
||||
</HelpText>
|
||||
|
||||
<h2>Navigation</h2>
|
||||
<h2>{t("Navigation")}</h2>
|
||||
<List>
|
||||
<Keys>
|
||||
<Key>n</Key>
|
||||
</Keys>
|
||||
<Label>New document in current collection</Label>
|
||||
|
||||
<Label>{t("New document in current collection")}</Label>
|
||||
<Keys>
|
||||
<Key>e</Key>
|
||||
</Keys>
|
||||
<Label>Edit current document</Label>
|
||||
|
||||
<Label>{t("Edit current document")}</Label>
|
||||
<Keys>
|
||||
<Key>m</Key>
|
||||
</Keys>
|
||||
<Label>Move current document</Label>
|
||||
|
||||
<Label>{t("Move current document")}</Label>
|
||||
<Keys>
|
||||
<Key>/</Key> or <Key>t</Key>
|
||||
</Keys>
|
||||
<Label>Jump to search</Label>
|
||||
|
||||
<Label>{t("Jump to search")}</Label>
|
||||
<Keys>
|
||||
<Key>d</Key>
|
||||
</Keys>
|
||||
<Label>Jump to dashboard</Label>
|
||||
|
||||
<Label>{t("Jump to dashboard")}</Label>
|
||||
<Keys>
|
||||
<Key>{meta}</Key> + <Key>Ctrl</Key> + <Key>h</Key>
|
||||
</Keys>
|
||||
<Label>Table of contents</Label>
|
||||
|
||||
<Label>{t("Table of contents")}</Label>
|
||||
<Keys>
|
||||
<Key>?</Key>
|
||||
</Keys>
|
||||
<Label>Open this guide</Label>
|
||||
<Label>{t("Open this guide")}</Label>
|
||||
</List>
|
||||
|
||||
<h2>Editor</h2>
|
||||
<h2>{t("Editor")}</h2>
|
||||
<List>
|
||||
<Keys>
|
||||
<Key>{meta}</Key> + <Key>Enter</Key>
|
||||
</Keys>
|
||||
<Label>Save and exit document edit mode</Label>
|
||||
<Label>{t("Save and exit document edit mode")}</Label>
|
||||
<Keys>
|
||||
<Key>{meta}</Key> + <Key>Shift</Key> + <Key>p</Key>
|
||||
</Keys>
|
||||
<Label>Publish and exit document edit mode</Label>
|
||||
<Label>{t("Publish and exit document edit mode")}</Label>
|
||||
<Keys>
|
||||
<Key>{meta}</Key> + <Key>s</Key>
|
||||
</Keys>
|
||||
<Label>Save document and continue editing</Label>
|
||||
<Label>{t("Save document and continue editing")}</Label>
|
||||
<Keys>
|
||||
<Key>{meta}</Key> + <Key>Esc</Key>
|
||||
</Keys>
|
||||
<Label>Cancel editing</Label>
|
||||
<Label>{t("Cancel editing")}</Label>
|
||||
<Keys>
|
||||
<Key>{meta}</Key> + <Key>b</Key>
|
||||
</Keys>
|
||||
<Label>Bold</Label>
|
||||
<Label>{t("Bold")}</Label>
|
||||
<Keys>
|
||||
<Key>{meta}</Key> + <Key>i</Key>
|
||||
</Keys>
|
||||
<Label>Italic</Label>
|
||||
<Label>{t("Italic")}</Label>
|
||||
<Keys>
|
||||
<Key>{meta}</Key> + <Key>u</Key>
|
||||
</Keys>
|
||||
<Label>Underline</Label>
|
||||
<Label>{t("Underline")}</Label>
|
||||
<Keys>
|
||||
<Key>{meta}</Key> + <Key>d</Key>
|
||||
</Keys>
|
||||
<Label>Strikethrough</Label>
|
||||
<Label>{t("Strikethrough")}</Label>
|
||||
<Keys>
|
||||
<Key>{meta}</Key> + <Key>k</Key>
|
||||
</Keys>
|
||||
<Label>Link</Label>
|
||||
<Label>{t("Link")}</Label>
|
||||
<Keys>
|
||||
<Key>{meta}</Key> + <Key>z</Key>
|
||||
</Keys>
|
||||
<Label>Undo</Label>
|
||||
<Label>{t("Undo")}</Label>
|
||||
<Keys>
|
||||
<Key>{meta}</Key> + <Key>Shift</Key> + <Key>z</Key>
|
||||
</Keys>
|
||||
<Label>Redo</Label>
|
||||
<Label>{t("Redo")}</Label>
|
||||
</List>
|
||||
|
||||
<h2>Markdown</h2>
|
||||
<h2>{t("Markdown")}</h2>
|
||||
<List>
|
||||
<Keys>
|
||||
<Key>#</Key> <Key>Space</Key>
|
||||
</Keys>
|
||||
<Label>Large header</Label>
|
||||
<Label>{t("Large header")}</Label>
|
||||
<Keys>
|
||||
<Key>##</Key> <Key>Space</Key>
|
||||
</Keys>
|
||||
<Label>Medium header</Label>
|
||||
<Label>{t("Medium header")}</Label>
|
||||
<Keys>
|
||||
<Key>###</Key> <Key>Space</Key>
|
||||
</Keys>
|
||||
<Label>Small header</Label>
|
||||
<Label>{t("Small header")}</Label>
|
||||
|
||||
<Keys>
|
||||
<Key>1.</Key> <Key>Space</Key>
|
||||
</Keys>
|
||||
<Label>Numbered list</Label>
|
||||
<Label>{t("Numbered list")}</Label>
|
||||
<Keys>
|
||||
<Key>-</Key> <Key>Space</Key>
|
||||
</Keys>
|
||||
<Label>Bulleted list</Label>
|
||||
<Label>{t("Bulleted list")}</Label>
|
||||
<Keys>
|
||||
<Key>[ ]</Key> <Key>Space</Key>
|
||||
</Keys>
|
||||
<Label>Todo list</Label>
|
||||
<Label>{t("Todo list")}</Label>
|
||||
<Keys>
|
||||
<Key>></Key> <Key>Space</Key>
|
||||
</Keys>
|
||||
<Label>Blockquote</Label>
|
||||
<Label>{t("Blockquote")}</Label>
|
||||
<Keys>
|
||||
<Key>---</Key>
|
||||
</Keys>
|
||||
<Label>Horizontal divider</Label>
|
||||
<Label>{t("Horizontal divider")}</Label>
|
||||
<Keys>
|
||||
<Key>{"```"}</Key>
|
||||
</Keys>
|
||||
<Label>Code block</Label>
|
||||
<Label>{t("Code block")}</Label>
|
||||
<Keys>
|
||||
<Key>{":::"}</Key>
|
||||
</Keys>
|
||||
<Label>Info notice</Label>
|
||||
<Label>{t("Info notice")}</Label>
|
||||
|
||||
<Keys>_italic_</Keys>
|
||||
<Label>Italic</Label>
|
||||
<Label>{t("Italic")}</Label>
|
||||
<Keys>**bold**</Keys>
|
||||
<Label>Bold</Label>
|
||||
<Label>{t("Bold")}</Label>
|
||||
<Keys>~~strikethrough~~</Keys>
|
||||
<Label>Strikethrough</Label>
|
||||
<Label>{t("Strikethrough")}</Label>
|
||||
<Keys>{"`code`"}</Keys>
|
||||
<Label>Inline code</Label>
|
||||
<Label>{t("Inline code")}</Label>
|
||||
<Keys>==highlight==</Keys>
|
||||
<Label>highlight</Label>
|
||||
<Label>{t("Highlight")}</Label>
|
||||
</List>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
@@ -7,6 +7,7 @@ import { PlusIcon } from "outline-icons";
|
||||
import queryString from "query-string";
|
||||
import * as React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { withTranslation, Trans, type TFunction } from "react-i18next";
|
||||
import keydown from "react-keydown";
|
||||
import { withRouter, Link } from "react-router-dom";
|
||||
import type { RouterHistory, Match } from "react-router-dom";
|
||||
@@ -44,11 +45,12 @@ type Props = {
|
||||
documents: DocumentsStore,
|
||||
users: UsersStore,
|
||||
notFound: ?boolean,
|
||||
t: TFunction,
|
||||
};
|
||||
|
||||
@observer
|
||||
class Search extends React.Component<Props> {
|
||||
firstDocument: ?React.Component<typeof DocumentPreview>;
|
||||
firstDocument: ?React.Component<any>;
|
||||
lastQuery: string = "";
|
||||
|
||||
@observable
|
||||
@@ -67,7 +69,7 @@ class Search extends React.Component<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
if (prevProps.location.search !== this.props.location.search) {
|
||||
this.handleQueryChange();
|
||||
}
|
||||
@@ -81,7 +83,7 @@ class Search extends React.Component<Props> {
|
||||
this.props.history.goBack();
|
||||
}
|
||||
|
||||
handleKeyDown = (ev) => {
|
||||
handleKeyDown = (ev: SyntheticKeyboardEvent<>) => {
|
||||
if (ev.key === "Enter") {
|
||||
this.fetchResults();
|
||||
return;
|
||||
@@ -124,7 +126,12 @@ class Search extends React.Component<Props> {
|
||||
this.fetchResultsDebounced();
|
||||
};
|
||||
|
||||
handleFilterChange = (search) => {
|
||||
handleFilterChange = (search: {
|
||||
collectionId?: ?string,
|
||||
userId?: ?string,
|
||||
dateFilter?: ?string,
|
||||
includeArchived?: ?string,
|
||||
}) => {
|
||||
this.props.history.replace({
|
||||
pathname: this.props.location.pathname,
|
||||
search: queryString.stringify({
|
||||
@@ -170,7 +177,7 @@ class Search extends React.Component<Props> {
|
||||
|
||||
get title() {
|
||||
const query = this.query;
|
||||
const title = "Search";
|
||||
const title = this.props.t("Search");
|
||||
if (query) return `${query} – ${title}`;
|
||||
return title;
|
||||
}
|
||||
@@ -231,20 +238,19 @@ class Search extends React.Component<Props> {
|
||||
trailing: true,
|
||||
});
|
||||
|
||||
updateLocation = (query) => {
|
||||
updateLocation = (query: string) => {
|
||||
this.props.history.replace({
|
||||
pathname: searchUrl(query),
|
||||
search: this.props.location.search,
|
||||
});
|
||||
};
|
||||
|
||||
setFirstDocumentRef = (ref) => {
|
||||
// $FlowFixMe
|
||||
setFirstDocumentRef = (ref: any) => {
|
||||
this.firstDocument = ref;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { documents, notFound, location } = this.props;
|
||||
const { documents, notFound, location, t } = this.props;
|
||||
const results = documents.searchResults(this.query);
|
||||
const showEmpty = !this.isLoading && this.query && results.length === 0;
|
||||
const showShortcutTip =
|
||||
@@ -256,12 +262,15 @@ class Search extends React.Component<Props> {
|
||||
{this.isLoading && <LoadingIndicator />}
|
||||
{notFound && (
|
||||
<div>
|
||||
<h1>Not Found</h1>
|
||||
<Empty>We were unable to find the page you’re looking for.</Empty>
|
||||
<h1>{t("Not Found")}</h1>
|
||||
<Empty>
|
||||
{t("We were unable to find the page you’re looking for.")}
|
||||
</Empty>
|
||||
</div>
|
||||
)}
|
||||
<ResultsWrapper pinToTop={this.pinToTop} column auto>
|
||||
<SearchField
|
||||
placeholder={t("Search…")}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
onChange={this.updateLocation}
|
||||
defaultValue={this.query}
|
||||
@@ -269,8 +278,10 @@ class Search extends React.Component<Props> {
|
||||
{showShortcutTip && (
|
||||
<Fade>
|
||||
<HelpText small>
|
||||
Use the <strong>{meta}+K</strong> shortcut to search from
|
||||
anywhere in Outline
|
||||
<Trans>
|
||||
Use the <strong>{{ meta }}+K</strong> shortcut to search from
|
||||
anywhere in your knowledge base
|
||||
</Trans>
|
||||
</HelpText>
|
||||
</Fade>
|
||||
)}
|
||||
@@ -304,8 +315,10 @@ class Search extends React.Component<Props> {
|
||||
<Fade>
|
||||
<Centered column>
|
||||
<HelpText>
|
||||
No documents found for your search filters. <br />
|
||||
Create a new document?
|
||||
<Trans>
|
||||
No documents found for your search filters. <br />
|
||||
Create a new document?
|
||||
</Trans>
|
||||
</HelpText>
|
||||
<Wrapper>
|
||||
{this.collectionId ? (
|
||||
@@ -314,14 +327,14 @@ class Search extends React.Component<Props> {
|
||||
icon={<PlusIcon />}
|
||||
primary
|
||||
>
|
||||
New doc
|
||||
{t("New doc")}
|
||||
</Button>
|
||||
) : (
|
||||
<NewDocumentMenu />
|
||||
)}
|
||||
|
||||
<Button as={Link} to="/search" neutral>
|
||||
Clear filters
|
||||
{t("Clear filters")}
|
||||
</Button>
|
||||
</Wrapper>
|
||||
</Centered>
|
||||
@@ -414,4 +427,6 @@ const Filters = styled(Flex)`
|
||||
}
|
||||
`;
|
||||
|
||||
export default withRouter(inject("documents")(Search));
|
||||
export default withTranslation()<Search>(
|
||||
withRouter(inject("documents")(Search))
|
||||
);
|
||||
|
||||
@@ -8,6 +8,7 @@ import { type Theme } from "types";
|
||||
type Props = {
|
||||
onChange: (string) => void,
|
||||
defaultValue?: string,
|
||||
placeholder?: string,
|
||||
theme: Theme,
|
||||
};
|
||||
|
||||
@@ -44,7 +45,7 @@ class SearchField extends React.Component<Props> {
|
||||
ref={(ref) => (this.input = ref)}
|
||||
onChange={this.handleChange}
|
||||
spellCheck="false"
|
||||
placeholder="Search…"
|
||||
placeholder={this.props.placeholder}
|
||||
type="search"
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
import { observable } from "mobx";
|
||||
import { observer, inject } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { Trans, withTranslation, type TFunction } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
import { languageOptions } from "shared/i18n";
|
||||
|
||||
import AuthStore from "stores/AuthStore";
|
||||
import UiStore from "stores/UiStore";
|
||||
@@ -10,13 +12,16 @@ import UserDelete from "scenes/UserDelete";
|
||||
import Button from "components/Button";
|
||||
import CenteredContent from "components/CenteredContent";
|
||||
import Flex from "components/Flex";
|
||||
import HelpText from "components/HelpText";
|
||||
import Input, { LabelText } from "components/Input";
|
||||
import InputSelect from "components/InputSelect";
|
||||
import PageTitle from "components/PageTitle";
|
||||
import ImageUpload from "./components/ImageUpload";
|
||||
|
||||
type Props = {
|
||||
auth: AuthStore,
|
||||
ui: UiStore,
|
||||
t: TFunction,
|
||||
};
|
||||
|
||||
@observer
|
||||
@@ -27,10 +32,12 @@ class Profile extends React.Component<Props> {
|
||||
@observable name: string;
|
||||
@observable avatarUrl: ?string;
|
||||
@observable showDeleteModal: boolean = false;
|
||||
@observable language: string;
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.auth.user) {
|
||||
this.name = this.props.auth.user.name;
|
||||
this.language = this.props.auth.user.language;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,13 +46,16 @@ class Profile extends React.Component<Props> {
|
||||
}
|
||||
|
||||
handleSubmit = async (ev: SyntheticEvent<>) => {
|
||||
const { t } = this.props;
|
||||
ev.preventDefault();
|
||||
|
||||
await this.props.auth.updateUser({
|
||||
name: this.name,
|
||||
avatarUrl: this.avatarUrl,
|
||||
language: this.language,
|
||||
});
|
||||
this.props.ui.showToast("Profile saved");
|
||||
|
||||
this.props.ui.showToast(t("Profile saved"));
|
||||
};
|
||||
|
||||
handleNameChange = (ev: SyntheticInputEvent<*>) => {
|
||||
@@ -53,16 +63,22 @@ class Profile extends React.Component<Props> {
|
||||
};
|
||||
|
||||
handleAvatarUpload = async (avatarUrl: string) => {
|
||||
const { t } = this.props;
|
||||
this.avatarUrl = avatarUrl;
|
||||
|
||||
await this.props.auth.updateUser({
|
||||
avatarUrl: this.avatarUrl,
|
||||
});
|
||||
this.props.ui.showToast("Profile picture updated");
|
||||
this.props.ui.showToast(t("Profile picture updated"));
|
||||
};
|
||||
|
||||
handleAvatarError = (error: ?string) => {
|
||||
this.props.ui.showToast(error || "Unable to upload new avatar");
|
||||
const { t } = this.props;
|
||||
this.props.ui.showToast(error || t("Unable to upload new profile picture"));
|
||||
};
|
||||
|
||||
handleLanguageChange = (ev: SyntheticInputEvent<*>) => {
|
||||
this.language = ev.target.value;
|
||||
};
|
||||
|
||||
toggleDeleteAccount = () => {
|
||||
@@ -74,16 +90,17 @@ class Profile extends React.Component<Props> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
const { user, isSaving } = this.props.auth;
|
||||
if (!user) return null;
|
||||
const avatarUrl = this.avatarUrl || user.avatarUrl;
|
||||
|
||||
return (
|
||||
<CenteredContent>
|
||||
<PageTitle title="Profile" />
|
||||
<h1>Profile</h1>
|
||||
<PageTitle title={t("Profile")} />
|
||||
<h1>{t("Profile")}</h1>
|
||||
<ProfilePicture column>
|
||||
<LabelText>Photo</LabelText>
|
||||
<LabelText>{t("Photo")}</LabelText>
|
||||
<AvatarContainer>
|
||||
<ImageUpload
|
||||
onSuccess={this.handleAvatarUpload}
|
||||
@@ -91,31 +108,55 @@ class Profile extends React.Component<Props> {
|
||||
>
|
||||
<Avatar src={avatarUrl} />
|
||||
<Flex auto align="center" justify="center">
|
||||
Upload
|
||||
{t("Upload")}
|
||||
</Flex>
|
||||
</ImageUpload>
|
||||
</AvatarContainer>
|
||||
</ProfilePicture>
|
||||
<form onSubmit={this.handleSubmit} ref={(ref) => (this.form = ref)}>
|
||||
<Input
|
||||
label="Full name"
|
||||
label={t("Full name")}
|
||||
autoComplete="name"
|
||||
value={this.name}
|
||||
onChange={this.handleNameChange}
|
||||
required
|
||||
short
|
||||
/>
|
||||
<br />
|
||||
<InputSelect
|
||||
label={t("Language")}
|
||||
options={languageOptions}
|
||||
value={this.language}
|
||||
onChange={this.handleLanguageChange}
|
||||
short
|
||||
/>
|
||||
<HelpText small>
|
||||
<Trans>
|
||||
Please note that translations are currently in early access.
|
||||
<br />
|
||||
Community contributions are accepted though our{" "}
|
||||
<a
|
||||
href="https://translate.getoutline.com"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
translation portal
|
||||
</a>
|
||||
</Trans>
|
||||
.
|
||||
</HelpText>
|
||||
<Button type="submit" disabled={isSaving || !this.isValid}>
|
||||
{isSaving ? "Saving…" : "Save"}
|
||||
{isSaving ? t("Saving…") : t("Save")}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<DangerZone>
|
||||
<LabelText>Delete Account</LabelText>
|
||||
<LabelText>{t("Delete Account")}</LabelText>
|
||||
<p>
|
||||
You may delete your account at any time, note that this is
|
||||
unrecoverable.{" "}
|
||||
<a onClick={this.toggleDeleteAccount}>Delete account</a>.
|
||||
{t(
|
||||
"You may delete your account at any time, note that this is unrecoverable"
|
||||
)}
|
||||
. <a onClick={this.toggleDeleteAccount}>{t("Delete account")}</a>.
|
||||
</p>
|
||||
</DangerZone>
|
||||
{this.showDeleteModal && (
|
||||
@@ -170,4 +211,4 @@ const Avatar = styled.img`
|
||||
${avatarStyles};
|
||||
`;
|
||||
|
||||
export default inject("auth", "ui")(Profile);
|
||||
export default withTranslation()<Profile>(inject("auth", "ui")(Profile));
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
// @flow
|
||||
import { observer, inject } from "mobx-react";
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { type Match } from "react-router-dom";
|
||||
|
||||
import DocumentsStore from "stores/DocumentsStore";
|
||||
import Actions, { Action } from "components/Actions";
|
||||
import CenteredContent from "components/CenteredContent";
|
||||
import Empty from "components/Empty";
|
||||
@@ -13,51 +12,50 @@ import PageTitle from "components/PageTitle";
|
||||
import PaginatedDocumentList from "components/PaginatedDocumentList";
|
||||
import Tab from "components/Tab";
|
||||
import Tabs from "components/Tabs";
|
||||
import useStores from "hooks/useStores";
|
||||
import NewDocumentMenu from "menus/NewDocumentMenu";
|
||||
|
||||
type Props = {
|
||||
documents: DocumentsStore,
|
||||
match: Match,
|
||||
};
|
||||
|
||||
@observer
|
||||
class Starred extends React.Component<Props> {
|
||||
render() {
|
||||
const { fetchStarred, starred, starredAlphabetical } = this.props.documents;
|
||||
const { sort } = this.props.match.params;
|
||||
function Starred(props: Props) {
|
||||
const { documents } = useStores();
|
||||
const { t } = useTranslation();
|
||||
const { fetchStarred, starred, starredAlphabetical } = documents;
|
||||
const { sort } = props.match.params;
|
||||
|
||||
return (
|
||||
<CenteredContent column auto>
|
||||
<PageTitle title="Starred" />
|
||||
<Heading>Starred</Heading>
|
||||
<PaginatedDocumentList
|
||||
heading={
|
||||
<Tabs>
|
||||
<Tab to="/starred" exact>
|
||||
Recently Updated
|
||||
</Tab>
|
||||
<Tab to="/starred/alphabetical" exact>
|
||||
Alphabetical
|
||||
</Tab>
|
||||
</Tabs>
|
||||
}
|
||||
empty={<Empty>You’ve not starred any documents yet.</Empty>}
|
||||
fetch={fetchStarred}
|
||||
documents={sort === "alphabetical" ? starredAlphabetical : starred}
|
||||
showCollection
|
||||
/>
|
||||
return (
|
||||
<CenteredContent column auto>
|
||||
<PageTitle title={t("Starred")} />
|
||||
<Heading>{t("Starred")}</Heading>
|
||||
<PaginatedDocumentList
|
||||
heading={
|
||||
<Tabs>
|
||||
<Tab to="/starred" exact>
|
||||
{t("Recently updated")}
|
||||
</Tab>
|
||||
<Tab to="/starred/alphabetical" exact>
|
||||
{t("Alphabetical")}
|
||||
</Tab>
|
||||
</Tabs>
|
||||
}
|
||||
empty={<Empty>{t("You’ve not starred any documents yet.")}</Empty>}
|
||||
fetch={fetchStarred}
|
||||
documents={sort === "alphabetical" ? starredAlphabetical : starred}
|
||||
showCollection
|
||||
/>
|
||||
|
||||
<Actions align="center" justify="flex-end">
|
||||
<Action>
|
||||
<InputSearch source="starred" />
|
||||
</Action>
|
||||
<Action>
|
||||
<NewDocumentMenu />
|
||||
</Action>
|
||||
</Actions>
|
||||
</CenteredContent>
|
||||
);
|
||||
}
|
||||
<Actions align="center" justify="flex-end">
|
||||
<Action>
|
||||
<InputSearch source="starred" />
|
||||
</Action>
|
||||
<Action>
|
||||
<NewDocumentMenu />
|
||||
</Action>
|
||||
</Actions>
|
||||
</CenteredContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default inject("documents")(Starred);
|
||||
export default observer(Starred);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// @flow
|
||||
import { observer, inject } from "mobx-react";
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { type Match } from "react-router-dom";
|
||||
|
||||
import DocumentsStore from "stores/DocumentsStore";
|
||||
import Actions, { Action } from "components/Actions";
|
||||
import CenteredContent from "components/CenteredContent";
|
||||
import Empty from "components/Empty";
|
||||
@@ -12,60 +12,54 @@ import PageTitle from "components/PageTitle";
|
||||
import PaginatedDocumentList from "components/PaginatedDocumentList";
|
||||
import Tab from "components/Tab";
|
||||
import Tabs from "components/Tabs";
|
||||
import useStores from "hooks/useStores";
|
||||
import NewTemplateMenu from "menus/NewTemplateMenu";
|
||||
|
||||
type Props = {
|
||||
documents: DocumentsStore,
|
||||
match: Match,
|
||||
};
|
||||
|
||||
@observer
|
||||
class Templates extends React.Component<Props> {
|
||||
render() {
|
||||
const {
|
||||
fetchTemplates,
|
||||
templates,
|
||||
templatesAlphabetical,
|
||||
} = this.props.documents;
|
||||
const { sort } = this.props.match.params;
|
||||
function Templates(props: Props) {
|
||||
const { documents } = useStores();
|
||||
const { t } = useTranslation();
|
||||
const { fetchTemplates, templates, templatesAlphabetical } = documents;
|
||||
const { sort } = props.match.params;
|
||||
|
||||
return (
|
||||
<CenteredContent column auto>
|
||||
<PageTitle title="Templates" />
|
||||
<Heading>Templates</Heading>
|
||||
<PaginatedDocumentList
|
||||
heading={
|
||||
<Tabs>
|
||||
<Tab to="/templates" exact>
|
||||
Recently Updated
|
||||
</Tab>
|
||||
<Tab to="/templates/alphabetical" exact>
|
||||
Alphabetical
|
||||
</Tab>
|
||||
</Tabs>
|
||||
}
|
||||
empty={
|
||||
<Empty>
|
||||
There are no templates just yet. You can create templates to help
|
||||
your team create consistent and accurate documentation.
|
||||
</Empty>
|
||||
}
|
||||
fetch={fetchTemplates}
|
||||
documents={
|
||||
sort === "alphabetical" ? templatesAlphabetical : templates
|
||||
}
|
||||
showCollection
|
||||
showDraft
|
||||
/>
|
||||
return (
|
||||
<CenteredContent column auto>
|
||||
<PageTitle title={t("Templates")} />
|
||||
<Heading>{t("Templates")}</Heading>
|
||||
<PaginatedDocumentList
|
||||
heading={
|
||||
<Tabs>
|
||||
<Tab to="/templates" exact>
|
||||
{t("Recently updated")}
|
||||
</Tab>
|
||||
<Tab to="/templates/alphabetical" exact>
|
||||
{t("Alphabetical")}
|
||||
</Tab>
|
||||
</Tabs>
|
||||
}
|
||||
empty={
|
||||
<Empty>
|
||||
{t(
|
||||
"There are no templates just yet. You can create templates to help your team create consistent and accurate documentation."
|
||||
)}
|
||||
</Empty>
|
||||
}
|
||||
fetch={fetchTemplates}
|
||||
documents={sort === "alphabetical" ? templatesAlphabetical : templates}
|
||||
showCollection
|
||||
showDraft
|
||||
/>
|
||||
|
||||
<Actions align="center" justify="flex-end">
|
||||
<Action>
|
||||
<NewTemplateMenu />
|
||||
</Action>
|
||||
</Actions>
|
||||
</CenteredContent>
|
||||
);
|
||||
}
|
||||
<Actions align="center" justify="flex-end">
|
||||
<Action>
|
||||
<NewTemplateMenu />
|
||||
</Action>
|
||||
</Actions>
|
||||
</CenteredContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default inject("documents")(Templates);
|
||||
export default observer(Templates);
|
||||
|
||||
@@ -1,39 +1,34 @@
|
||||
// @flow
|
||||
import { observer, inject } from "mobx-react";
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import DocumentsStore from "stores/DocumentsStore";
|
||||
import CenteredContent from "components/CenteredContent";
|
||||
import Empty from "components/Empty";
|
||||
import Heading from "components/Heading";
|
||||
import PageTitle from "components/PageTitle";
|
||||
import PaginatedDocumentList from "components/PaginatedDocumentList";
|
||||
import Subheading from "components/Subheading";
|
||||
import useStores from "hooks/useStores";
|
||||
|
||||
type Props = {
|
||||
documents: DocumentsStore,
|
||||
};
|
||||
function Trash() {
|
||||
const { t } = useTranslation();
|
||||
const { documents } = useStores();
|
||||
|
||||
@observer
|
||||
class Trash extends React.Component<Props> {
|
||||
render() {
|
||||
const { documents } = this.props;
|
||||
|
||||
return (
|
||||
<CenteredContent column auto>
|
||||
<PageTitle title="Trash" />
|
||||
<Heading>Trash</Heading>
|
||||
<PaginatedDocumentList
|
||||
documents={documents.deleted}
|
||||
fetch={documents.fetchDeleted}
|
||||
heading={<Subheading>Documents</Subheading>}
|
||||
empty={<Empty>Trash is empty at the moment.</Empty>}
|
||||
showCollection
|
||||
showTemplate
|
||||
/>
|
||||
</CenteredContent>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<CenteredContent column auto>
|
||||
<PageTitle title={t("Trash")} />
|
||||
<Heading>{t("Trash")}</Heading>
|
||||
<PaginatedDocumentList
|
||||
documents={documents.deleted}
|
||||
fetch={documents.fetchDeleted}
|
||||
heading={<Subheading>{t("Documents")}</Subheading>}
|
||||
empty={<Empty>{t("Trash is empty at the moment.")}</Empty>}
|
||||
showCollection
|
||||
showTemplate
|
||||
/>
|
||||
</CenteredContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default inject("documents")(Trash);
|
||||
export default observer(Trash);
|
||||
|
||||
@@ -3,6 +3,7 @@ import distanceInWordsToNow from "date-fns/distance_in_words_to_now";
|
||||
import { inject, observer } from "mobx-react";
|
||||
import { EditIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { withRouter, type RouterHistory } from "react-router-dom";
|
||||
import styled from "styled-components";
|
||||
import { settings } from "shared/utils/routeHelpers";
|
||||
@@ -26,61 +27,65 @@ type Props = {
|
||||
onRequestClose: () => void,
|
||||
};
|
||||
|
||||
@observer
|
||||
class UserProfile extends React.Component<Props> {
|
||||
render() {
|
||||
const { user, auth, documents, ...rest } = this.props;
|
||||
if (!user) return null;
|
||||
const isCurrentUser = auth.user && auth.user.id === user.id;
|
||||
function UserProfile(props: Props) {
|
||||
const { t } = useTranslation();
|
||||
const { user, auth, documents, ...rest } = props;
|
||||
if (!user) return null;
|
||||
const isCurrentUser = auth.user && auth.user.id === user.id;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={
|
||||
<Flex align="center">
|
||||
<Avatar src={user.avatarUrl} size={38} />
|
||||
<span> {user.name}</span>
|
||||
</Flex>
|
||||
}
|
||||
{...rest}
|
||||
>
|
||||
<Flex column>
|
||||
<Meta>
|
||||
{isCurrentUser
|
||||
? "You joined"
|
||||
: user.lastActiveAt
|
||||
? "Joined"
|
||||
: "Invited"}{" "}
|
||||
{distanceInWordsToNow(new Date(user.createdAt))} ago.
|
||||
{user.isAdmin && (
|
||||
<StyledBadge admin={user.isAdmin}>Admin</StyledBadge>
|
||||
)}
|
||||
{user.isSuspended && <Badge>Suspended</Badge>}
|
||||
{isCurrentUser && (
|
||||
<Edit>
|
||||
<Button
|
||||
onClick={() => this.props.history.push(settings())}
|
||||
icon={<EditIcon />}
|
||||
neutral
|
||||
>
|
||||
Edit Profile
|
||||
</Button>
|
||||
</Edit>
|
||||
)}
|
||||
</Meta>
|
||||
<PaginatedDocumentList
|
||||
documents={documents.createdByUser(user.id)}
|
||||
fetch={documents.fetchOwned}
|
||||
options={{ user: user.id }}
|
||||
heading={<Subheading>Recently updated</Subheading>}
|
||||
empty={
|
||||
<HelpText>{user.name} hasn’t updated any documents yet.</HelpText>
|
||||
}
|
||||
showCollection
|
||||
/>
|
||||
return (
|
||||
<Modal
|
||||
title={
|
||||
<Flex align="center">
|
||||
<Avatar src={user.avatarUrl} size={38} />
|
||||
<span> {user.name}</span>
|
||||
</Flex>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
{...rest}
|
||||
>
|
||||
<Flex column>
|
||||
<Meta>
|
||||
{isCurrentUser
|
||||
? t("You joined")
|
||||
: user.lastActiveAt
|
||||
? t("Joined")
|
||||
: t("Invited")}{" "}
|
||||
{t("{{ time }} ago.", {
|
||||
time: distanceInWordsToNow(new Date(user.createdAt)),
|
||||
})}
|
||||
{user.isAdmin && (
|
||||
<StyledBadge admin={user.isAdmin}>{t("Admin")}</StyledBadge>
|
||||
)}
|
||||
{user.isSuspended && <Badge>{t("Suspended")}</Badge>}
|
||||
{isCurrentUser && (
|
||||
<Edit>
|
||||
<Button
|
||||
onClick={() => this.props.history.push(settings())}
|
||||
icon={<EditIcon />}
|
||||
neutral
|
||||
>
|
||||
{t("Edit Profile")}
|
||||
</Button>
|
||||
</Edit>
|
||||
)}
|
||||
</Meta>
|
||||
<PaginatedDocumentList
|
||||
documents={documents.createdByUser(user.id)}
|
||||
fetch={documents.fetchOwned}
|
||||
options={{ user: user.id }}
|
||||
heading={<Subheading>{t("Recently updated")}</Subheading>}
|
||||
empty={
|
||||
<HelpText>
|
||||
{t("{{ userName }} hasn’t updated any documents yet.", {
|
||||
userName: user.name,
|
||||
})}
|
||||
</HelpText>
|
||||
}
|
||||
showCollection
|
||||
/>
|
||||
</Flex>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
const Edit = styled.span`
|
||||
@@ -98,4 +103,4 @@ const Meta = styled(HelpText)`
|
||||
margin-top: -12px;
|
||||
`;
|
||||
|
||||
export default inject("documents", "auth")(withRouter(UserProfile));
|
||||
export default inject("documents", "auth")(withRouter(observer(UserProfile)));
|
||||
|
||||
Reference in New Issue
Block a user