fix: Add translation hooks on settings screen (#2298)

Co-authored-by: Tom Moor <tom.moor@gmail.com>
This commit is contained in:
Saumya Pandey
2021-07-15 14:50:36 +05:30
committed by GitHub
parent 7ae3addea0
commit 3f030540b3
5 changed files with 340 additions and 316 deletions

View File

@@ -1,11 +1,10 @@
// @flow
import { observable } from "mobx";
import { observer, inject } from "mobx-react";
import { observer } from "mobx-react";
import { TeamIcon } from "outline-icons";
import * as React from "react";
import { useRef, useState } from "react";
import { useTranslation, Trans } from "react-i18next";
import styled from "styled-components";
import AuthStore from "stores/AuthStore";
import UiStore from "stores/UiStore";
import Button from "components/Button";
import Flex from "components/Flex";
import Heading from "components/Heading";
@@ -14,137 +13,128 @@ import Input, { LabelText } from "components/Input";
import Scene from "components/Scene";
import ImageUpload from "./components/ImageUpload";
import env from "env";
import useCurrentTeam from "hooks/useCurrentTeam";
import useStores from "hooks/useStores";
type Props = {
auth: AuthStore,
ui: UiStore,
};
function Details() {
const { auth, ui } = useStores();
const team = useCurrentTeam();
const { t } = useTranslation();
const form = useRef<?HTMLFormElement>();
const [name, setName] = useState(team.name);
const [subdomain, setSubdomain] = useState(team.subdomain);
const [avatarUrl, setAvatarUrl] = useState();
@observer
class Details extends React.Component<Props> {
timeout: TimeoutID;
form: ?HTMLFormElement;
const handleSubmit = React.useCallback(
async (event: ?SyntheticEvent<>) => {
if (event) {
event.preventDefault();
}
@observable name: string;
@observable subdomain: ?string;
@observable avatarUrl: ?string;
try {
await auth.updateTeam({
name,
avatarUrl,
subdomain,
});
ui.showToast(t("Settings saved"), { type: "success" });
} catch (err) {
ui.showToast(err.message, { type: "error" });
}
},
[auth, ui, name, avatarUrl, subdomain, t]
);
componentDidMount() {
const { team } = this.props.auth;
if (team) {
this.name = team.name;
this.subdomain = team.subdomain;
}
}
const handleNameChange = React.useCallback((ev: SyntheticInputEvent<*>) => {
setName(ev.target.value);
}, []);
componentWillUnmount() {
clearTimeout(this.timeout);
}
const handleSubdomainChange = React.useCallback(
(ev: SyntheticInputEvent<*>) => {
setSubdomain(ev.target.value.toLowerCase());
},
[]
);
handleSubmit = async (event: ?SyntheticEvent<>) => {
if (event) {
event.preventDefault();
}
const handleAvatarUpload = React.useCallback(
(avatarUrl: string) => {
setAvatarUrl(avatarUrl);
handleSubmit();
},
[handleSubmit]
);
try {
await this.props.auth.updateTeam({
name: this.name,
avatarUrl: this.avatarUrl,
subdomain: this.subdomain,
});
this.props.ui.showToast("Settings saved", { type: "success" });
} catch (err) {
this.props.ui.showToast(err.message, { type: "error" });
}
};
const handleAvatarError = React.useCallback(
(error: ?string) => {
ui.showToast(error || t("Unable to upload new logo"));
},
[ui, t]
);
handleNameChange = (ev: SyntheticInputEvent<*>) => {
this.name = ev.target.value;
};
const isValid = form.current && form.current.checkValidity();
handleSubdomainChange = (ev: SyntheticInputEvent<*>) => {
this.subdomain = ev.target.value.toLowerCase();
};
handleAvatarUpload = (avatarUrl: string) => {
this.avatarUrl = avatarUrl;
this.handleSubmit();
};
handleAvatarError = (error: ?string) => {
this.props.ui.showToast(error || "Unable to upload new logo");
};
get isValid() {
return this.form && this.form.checkValidity();
}
render() {
const { team, isSaving } = this.props.auth;
if (!team) return null;
const avatarUrl = this.avatarUrl || team.avatarUrl;
return (
<Scene title="Details" icon={<TeamIcon color="currentColor" />}>
<Heading>Details</Heading>
<HelpText>
return (
<Scene title={t("Details")} icon={<TeamIcon color="currentColor" />}>
<Heading>{t("Details")}</Heading>
<HelpText>
<Trans>
These details affect the way that your Outline appears to everyone on
the team.
</HelpText>
</Trans>
</HelpText>
<ProfilePicture column>
<LabelText>Logo</LabelText>
<AvatarContainer>
<ImageUpload
onSuccess={this.handleAvatarUpload}
onError={this.handleAvatarError}
submitText="Crop logo"
borderRadius={0}
>
<Avatar src={avatarUrl} />
<Flex auto align="center" justify="center">
Upload
</Flex>
</ImageUpload>
</AvatarContainer>
</ProfilePicture>
<form onSubmit={this.handleSubmit} ref={(ref) => (this.form = ref)}>
<Input
label="Name"
name="name"
autoComplete="organization"
value={this.name}
onChange={this.handleNameChange}
required
short
/>
{env.SUBDOMAINS_ENABLED && (
<>
<Input
label="Subdomain"
name="subdomain"
value={this.subdomain || ""}
onChange={this.handleSubdomainChange}
autoComplete="off"
minLength={4}
maxLength={32}
short
/>
{this.subdomain && (
<HelpText small>
Your knowledge base will be accessible at{" "}
<strong>{this.subdomain}.getoutline.com</strong>
</HelpText>
)}
</>
)}
<Button type="submit" disabled={isSaving || !this.isValid}>
{isSaving ? "Saving…" : "Save"}
</Button>
</form>
</Scene>
);
}
<ProfilePicture column>
<LabelText>{t("Logo")}</LabelText>
<AvatarContainer>
<ImageUpload
onSuccess={handleAvatarUpload}
onError={handleAvatarError}
submitText={t("Crop logo")}
borderRadius={0}
>
<Avatar src={avatarUrl} />
<Flex auto align="center" justify="center">
<Trans>Upload</Trans>
</Flex>
</ImageUpload>
</AvatarContainer>
</ProfilePicture>
<form onSubmit={handleSubmit} ref={form}>
<Input
label={t("Name")}
name="name"
autoComplete="organization"
value={name}
onChange={handleNameChange}
required
short
/>
{env.SUBDOMAINS_ENABLED && (
<>
<Input
label={t("Subdomain")}
name="subdomain"
value={subdomain || ""}
onChange={handleSubdomainChange}
autoComplete="off"
minLength={4}
maxLength={32}
short
/>
{subdomain && (
<HelpText small>
<Trans>Your knowledge base will be accessible at</Trans>{" "}
<strong>{subdomain}.getoutline.com</strong>
</HelpText>
)}
</>
)}
<Button type="submit" disabled={auth.isSaving || !isValid}>
{auth.isSaving ? `${t("Saving")}` : t("Save")}
</Button>
</form>
</Scene>
);
}
const ProfilePicture = styled(Flex)`
@@ -186,4 +176,4 @@ const Avatar = styled.img`
${avatarStyles};
`;
export default inject("auth", "ui")(Details);
export default observer(Details);

View File

@@ -1,137 +1,140 @@
// @flow
import { debounce } from "lodash";
import { observer, inject } from "mobx-react";
import { observer } from "mobx-react";
import { EmailIcon } from "outline-icons";
import * as React from "react";
import { useTranslation, Trans } from "react-i18next";
import styled from "styled-components";
import AuthStore from "stores/AuthStore";
import NotificationSettingsStore from "stores/NotificationSettingsStore";
import UiStore from "stores/UiStore";
import Heading from "components/Heading";
import HelpText from "components/HelpText";
import Input from "components/Input";
import Notice from "components/Notice";
import Scene from "components/Scene";
import Subheading from "components/Subheading";
import NotificationListItem from "./components/NotificationListItem";
import useCurrentUser from "hooks/useCurrentUser";
import useStores from "hooks/useStores";
type Props = {
ui: UiStore,
auth: AuthStore,
notificationSettings: NotificationSettingsStore,
};
function Notifications() {
const { notificationSettings, ui } = useStores();
const user = useCurrentUser();
const { t } = useTranslation();
const options = [
{
event: "documents.publish",
title: "Document published",
description: "Receive a notification whenever a new document is published",
},
{
event: "documents.update",
title: "Document updated",
description: "Receive a notification when a document you created is edited",
},
{
event: "collections.create",
title: "Collection created",
description: "Receive a notification whenever a new collection is created",
},
{
separator: true,
},
{
event: "emails.onboarding",
title: "Getting started",
description:
"Tips on getting started with Outline`s features and functionality",
},
{
event: "emails.features",
title: "New features",
description: "Receive an email when new features of note are added",
},
];
const options = [
{
event: "documents.publish",
title: t("Document published"),
description: t(
"Receive a notification whenever a new document is published"
),
},
{
event: "documents.update",
title: t("Document updated"),
description: t(
"Receive a notification when a document you created is edited"
),
},
{
event: "collections.create",
title: t("Collection created"),
description: t(
"Receive a notification whenever a new collection is created"
),
},
{
separator: true,
},
{
event: "emails.onboarding",
title: t("Getting started"),
description: t(
"Tips on getting started with Outline`s features and functionality"
),
},
{
event: "emails.features",
title: t("New features"),
description: t("Receive an email when new features of note are added"),
},
];
@observer
class Notifications extends React.Component<Props> {
componentDidMount() {
this.props.notificationSettings.fetchPage();
}
React.useEffect(() => {
notificationSettings.fetchPage();
}, [notificationSettings]);
handleChange = async (ev: SyntheticInputEvent<>) => {
const { notificationSettings } = this.props;
const setting = notificationSettings.getByEvent(ev.target.name);
if (ev.target.checked) {
await notificationSettings.save({
event: ev.target.name,
});
} else if (setting) {
await notificationSettings.delete(setting);
}
this.showSuccessMessage();
};
showSuccessMessage = debounce(() => {
this.props.ui.showToast("Notifications saved", { type: "success" });
const showSuccessMessage = debounce(() => {
ui.showToast(t("Notifications saved"), { type: "success" });
}, 500);
render() {
const { notificationSettings, auth } = this.props;
const showSuccessNotice = window.location.search === "?success";
const { user, team } = auth;
if (!team || !user) return null;
const handleChange = React.useCallback(
async (ev: SyntheticInputEvent<>) => {
const setting = notificationSettings.getByEvent(ev.target.name);
return (
<Scene title="Notifications" icon={<EmailIcon color="currentColor" />}>
{showSuccessNotice && (
<Notice>
if (ev.target.checked) {
await notificationSettings.save({
event: ev.target.name,
});
} else if (setting) {
await notificationSettings.delete(setting);
}
showSuccessMessage();
},
[notificationSettings, showSuccessMessage]
);
const showSuccessNotice = window.location.search === "?success";
return (
<Scene title={t("Notifications")} icon={<EmailIcon color="currentColor" />}>
{showSuccessNotice && (
<Notice>
<Trans>
Unsubscription successful. Your notification settings were updated
</Notice>
)}
<Heading>Notifications</Heading>
<HelpText>
</Trans>
</Notice>
)}
<Heading>{t("Notifications")}</Heading>
<HelpText>
<Trans>
Manage when and where you receive email notifications from Outline.
Your email address can be updated in your SSO provider.
</HelpText>
</Trans>
</HelpText>
<Input
type="email"
value={user.email}
label={t("Email address")}
readOnly
short
/>
<Input
type="email"
value={user.email}
label="Email address"
readOnly
short
/>
<Subheading>{t("Notifications")}</Subheading>
<Subheading>Notifications</Subheading>
{options.map((option, index) => {
if (option.separator) return <Separator key={`separator-${index}`} />;
{options.map((option, index) => {
if (option.separator) return <Separator key={`separator-${index}`} />;
const setting = notificationSettings.getByEvent(option.event);
const setting = notificationSettings.getByEvent(option.event);
return (
<NotificationListItem
key={option.event}
onChange={this.handleChange}
setting={setting}
disabled={
(setting && setting.isSaving) || notificationSettings.isFetching
}
{...option}
/>
);
})}
</Scene>
);
}
return (
<NotificationListItem
key={option.event}
onChange={handleChange}
setting={setting}
disabled={
(setting && setting.isSaving) || notificationSettings.isFetching
}
{...option}
/>
);
})}
</Scene>
);
}
const Separator = styled.hr`
padding-bottom: 12px;
`;
export default inject("notificationSettings", "auth", "ui")(Notifications);
export default observer(Notifications);

View File

@@ -1,97 +1,94 @@
// @flow
import { debounce } from "lodash";
import { observable } from "mobx";
import { observer, inject } from "mobx-react";
import { observer } from "mobx-react";
import { PadlockIcon } from "outline-icons";
import * as React from "react";
import AuthStore from "stores/AuthStore";
import UiStore from "stores/UiStore";
import { useState } from "react";
import { useTranslation, Trans } from "react-i18next";
import Checkbox from "components/Checkbox";
import Heading from "components/Heading";
import HelpText from "components/HelpText";
import Scene from "components/Scene";
import useCurrentTeam from "hooks/useCurrentTeam";
import useStores from "hooks/useStores";
type Props = {
auth: AuthStore,
ui: UiStore,
};
function Security() {
const { auth, ui } = useStores();
const team = useCurrentTeam();
const { t } = useTranslation();
const [sharing, setSharing] = useState(team.documentEmbeds);
const [documentEmbeds, setDocumentEmbeds] = useState(team.guestSignin);
const [guestSignin, setGuestSignin] = useState(team.sharing);
@observer
class Security extends React.Component<Props> {
form: ?HTMLFormElement;
@observable sharing: boolean;
@observable documentEmbeds: boolean;
@observable guestSignin: boolean;
componentDidMount() {
const { auth } = this.props;
if (auth.team) {
this.documentEmbeds = auth.team.documentEmbeds;
this.guestSignin = auth.team.guestSignin;
this.sharing = auth.team.sharing;
}
}
handleChange = async (ev: SyntheticInputEvent<*>) => {
switch (ev.target.name) {
case "sharing":
this.sharing = ev.target.checked;
break;
case "documentEmbeds":
this.documentEmbeds = ev.target.checked;
break;
case "guestSignin":
this.guestSignin = ev.target.checked;
break;
default:
}
await this.props.auth.updateTeam({
sharing: this.sharing,
documentEmbeds: this.documentEmbeds,
guestSignin: this.guestSignin,
});
this.showSuccessMessage();
};
showSuccessMessage = debounce(() => {
this.props.ui.showToast("Settings saved", { type: "success" });
const showSuccessMessage = debounce(() => {
ui.showToast(t("Settings saved"), { type: "success" });
}, 500);
render() {
return (
<Scene title="Security" icon={<PadlockIcon color="currentColor" />}>
<Heading>Security</Heading>
<HelpText>
const handleChange = React.useCallback(
async (ev: SyntheticInputEvent<*>) => {
switch (ev.target.name) {
case "sharing":
setSharing(ev.target.checked);
break;
case "documentEmbeds":
setDocumentEmbeds(ev.target.checked);
break;
case "guestSignin":
setGuestSignin(ev.target.checked);
break;
default:
}
await auth.updateTeam({
sharing,
documentEmbeds,
guestSignin,
});
showSuccessMessage();
},
[auth, sharing, documentEmbeds, guestSignin, showSuccessMessage]
);
return (
<Scene title={t("Security")} icon={<PadlockIcon color="currentColor" />}>
<Heading>
<Trans>Security</Trans>
</Heading>
<HelpText>
<Trans>
Settings that impact the access, security, and content of your
knowledge base.
</HelpText>
</Trans>
</HelpText>
<Checkbox
label="Allow email authentication"
name="guestSignin"
checked={this.guestSignin}
onChange={this.handleChange}
note="When enabled, users can sign-in using their email address"
/>
<Checkbox
label="Public document sharing"
name="sharing"
checked={this.sharing}
onChange={this.handleChange}
note="When enabled, documents can be shared publicly on the internet by any team member"
/>
<Checkbox
label="Rich service embeds"
name="documentEmbeds"
checked={this.documentEmbeds}
onChange={this.handleChange}
note="Links to supported services are shown as rich embeds within your documents"
/>
</Scene>
);
}
<Checkbox
label={t("Allow email authentication")}
name="guestSignin"
checked={guestSignin}
onChange={handleChange}
note={t("When enabled, users can sign-in using their email address")}
/>
<Checkbox
label={t("Public document sharing")}
name="sharing"
checked={sharing}
onChange={handleChange}
note={t(
"When enabled, documents can be shared publicly on the internet by any team member"
)}
/>
<Checkbox
label={t("Rich service embeds")}
name="documentEmbeds"
checked={documentEmbeds}
onChange={handleChange}
note={t(
"Links to supported services are shown as rich embeds within your documents"
)}
/>
</Scene>
);
}
export default inject("auth", "ui")(Security);
export default observer(Security);

View File

@@ -1,5 +1,6 @@
// @flow
import * as React from "react";
import { useTranslation, Trans } from "react-i18next";
import Button from "components/Button";
import Heading from "components/Heading";
import HelpText from "components/HelpText";
@@ -7,13 +8,16 @@ import Scene from "components/Scene";
import ZapierIcon from "components/ZapierIcon";
function Zapier() {
const { t } = useTranslation();
return (
<Scene title="Zapier" icon={<ZapierIcon color="currentColor" />}>
<Heading>Zapier</Heading>
<Scene title={t("Zapier")} icon={<ZapierIcon color="currentColor" />}>
<Heading>{t("Zapier")}</Heading>
<HelpText>
Zapier is a platform that allows Outline to easily integrate with
thousands of other business tools. Head over to Zapier to setup a "Zap"
and start programmatically interacting with Outline.
<Trans>
Zapier is a platform that allows Outline to easily integrate with
thousands of other business tools. Head over to Zapier to setup a
"Zap" and start programmatically interacting with Outline.'
</Trans>
</HelpText>
<p>
<Button
@@ -21,7 +25,7 @@ function Zapier() {
(window.location.href = "https://zapier.com/apps/outline")
}
>
Open Zapier
{t("Open Zapier")}
</Button>
</p>
</Scene>