mirror of
https://github.com/LemmyNet/lemmy-ui.git
synced 2024-12-22 19:01:26 +00:00
Settings Import/export (#2223)
* Add UI for import/export of settings * Make settings update after import without requiring manual browser refresh * Address PR feedback * Add translations
This commit is contained in:
parent
9fcd5ef54f
commit
4be7310441
2 changed files with 191 additions and 2 deletions
|
@ -1 +1 @@
|
|||
Subproject commit b0d80808047eb547aa995e923e9c742e87cd8be7
|
||||
Subproject commit 277e3c335bbb9c00c4a600bd4d655b273393f84a
|
|
@ -17,7 +17,7 @@ import { capitalizeFirstLetter, debounce } from "@utils/helpers";
|
|||
import { Choice, RouteDataResponse } from "@utils/types";
|
||||
import classNames from "classnames";
|
||||
import { NoOptionI18nKeys } from "i18next";
|
||||
import { Component, linkEvent } from "inferno";
|
||||
import { Component, createRef, linkEvent } from "inferno";
|
||||
import {
|
||||
BlockCommunityResponse,
|
||||
BlockInstanceResponse,
|
||||
|
@ -60,6 +60,7 @@ import { CommunityLink } from "../community/community-link";
|
|||
import { PersonListing } from "./person-listing";
|
||||
import { InitialFetchRequest } from "../../interfaces";
|
||||
import TotpModal from "../common/totp-modal";
|
||||
import { LoadingEllipses } from "../common/loading-ellipses";
|
||||
|
||||
type SettingsData = RouteDataResponse<{
|
||||
instancesRes: GetFederatedInstancesResponse;
|
||||
|
@ -119,6 +120,9 @@ interface SettingsState {
|
|||
searchInstanceOptions: Choice[];
|
||||
isIsomorphic: boolean;
|
||||
show2faModal: boolean;
|
||||
importSettingsRes: RequestState<any>;
|
||||
exportSettingsRes: RequestState<any>;
|
||||
settingsFile?: File;
|
||||
}
|
||||
|
||||
type FilterType = "user" | "community" | "instance";
|
||||
|
@ -183,6 +187,8 @@ function handleClose2faModal(i: Settings) {
|
|||
|
||||
export class Settings extends Component<any, SettingsState> {
|
||||
private isoData = setIsoData<SettingsData>(this.context);
|
||||
exportSettingsLink = createRef<HTMLAnchorElement>();
|
||||
|
||||
state: SettingsState = {
|
||||
saveRes: EMPTY_REQUEST,
|
||||
deleteAccountRes: EMPTY_REQUEST,
|
||||
|
@ -207,6 +213,8 @@ export class Settings extends Component<any, SettingsState> {
|
|||
generateTotpRes: EMPTY_REQUEST,
|
||||
updateTotpRes: EMPTY_REQUEST,
|
||||
show2faModal: false,
|
||||
importSettingsRes: EMPTY_REQUEST,
|
||||
exportSettingsRes: EMPTY_REQUEST,
|
||||
};
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
|
@ -251,6 +259,7 @@ export class Settings extends Component<any, SettingsState> {
|
|||
show_read_posts,
|
||||
send_notifications_to_email,
|
||||
email,
|
||||
open_links_in_new_tab,
|
||||
},
|
||||
person: {
|
||||
avatar,
|
||||
|
@ -288,6 +297,7 @@ export class Settings extends Component<any, SettingsState> {
|
|||
bio,
|
||||
send_notifications_to_email,
|
||||
matrix_user_id,
|
||||
open_links_in_new_tab,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -332,8 +342,18 @@ export class Settings extends Component<any, SettingsState> {
|
|||
}
|
||||
|
||||
render() {
|
||||
/* eslint-disable jsx-a11y/anchor-has-content, jsx-a11y/anchor-is-valid */
|
||||
return (
|
||||
<div className="person-settings container-lg">
|
||||
<a
|
||||
ref={this.exportSettingsLink}
|
||||
download={`${I18NextService.i18n.t("export_file_name")}_${new Date()
|
||||
.toISOString()
|
||||
.replace(/:|-/g, "")}.json`}
|
||||
className="d-none"
|
||||
href="javascript:void(0)"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<HtmlTags
|
||||
title={this.documentTitle}
|
||||
path={this.context.router.route.match.url}
|
||||
|
@ -356,6 +376,7 @@ export class Settings extends Component<any, SettingsState> {
|
|||
/>
|
||||
</div>
|
||||
);
|
||||
/* eslint-enable jsx-a11y/anchor-has-content, jsx-a11y/anchor-is-valid */
|
||||
}
|
||||
|
||||
userSettings(isSelected: boolean) {
|
||||
|
@ -377,6 +398,9 @@ export class Settings extends Component<any, SettingsState> {
|
|||
<div className="card border-secondary mb-3">
|
||||
<div className="card-body">{this.changePasswordHtmlForm()}</div>
|
||||
</div>
|
||||
<div className="card border-secondary mb-3">
|
||||
<div className="card-body">{this.importExport()}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -595,6 +619,58 @@ export class Settings extends Component<any, SettingsState> {
|
|||
);
|
||||
}
|
||||
|
||||
importExport() {
|
||||
return (
|
||||
<>
|
||||
<h2 className="h5">
|
||||
{I18NextService.i18n.t("import_export_section_title")}
|
||||
</h2>
|
||||
<p>{I18NextService.i18n.t("import_export_section_description")}</p>
|
||||
{!(
|
||||
this.state.importSettingsRes.state === "loading" ||
|
||||
this.state.exportSettingsRes.state === "loading"
|
||||
) ? (
|
||||
<>
|
||||
<button
|
||||
className="btn btn-secondary w-100 mb-4"
|
||||
onClick={linkEvent(this, this.handleExportSettings)}
|
||||
type="button"
|
||||
>
|
||||
{I18NextService.i18n.t("export")}
|
||||
</button>
|
||||
<fieldset className="border border-secondary rounded p-3 bg-dark bg-opacity-25">
|
||||
<input
|
||||
type="file"
|
||||
accept="application/json"
|
||||
className="form-control"
|
||||
aria-label="Import settings file input"
|
||||
onChange={linkEvent(this, this.handleImportFileChange)}
|
||||
/>
|
||||
<button
|
||||
className="btn btn-secondary w-100 mt-3"
|
||||
onClick={linkEvent(this, this.handleImportSettings)}
|
||||
type="button"
|
||||
disabled={!this.state.settingsFile}
|
||||
>
|
||||
{I18NextService.i18n.t("import")}
|
||||
</button>
|
||||
</fieldset>
|
||||
</>
|
||||
) : (
|
||||
<div>
|
||||
<div className="text-center">
|
||||
{this.state.exportSettingsRes.state === "loading"
|
||||
? I18NextService.i18n.t("exporting")
|
||||
: I18NextService.i18n.t("importing")}
|
||||
<LoadingEllipses />
|
||||
</div>
|
||||
<Spinner large />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
saveUserSettingsHtmlForm() {
|
||||
const selectedLangs = this.state.saveUserSettingsForm.discussion_languages;
|
||||
|
||||
|
@ -1096,6 +1172,7 @@ export class Settings extends Component<any, SettingsState> {
|
|||
this.setState({ show2faModal: false });
|
||||
|
||||
const siteRes = await HttpService.client.getSite();
|
||||
|
||||
UserService.Instance.myUserInfo!.local_user_view.local_user.totp_2fa_enabled =
|
||||
enabled;
|
||||
|
||||
|
@ -1474,6 +1551,118 @@ export class Settings extends Component<any, SettingsState> {
|
|||
}
|
||||
}
|
||||
|
||||
handleImportFileChange(i: Settings, event) {
|
||||
i.setState({ settingsFile: event.target.files?.item(0) });
|
||||
}
|
||||
|
||||
async handleExportSettings(i: Settings) {
|
||||
i.setState({ exportSettingsRes: LOADING_REQUEST });
|
||||
const res = await HttpService.client.exportSettings();
|
||||
|
||||
if (res.state === "success") {
|
||||
i.exportSettingsLink.current!.href = encodeURI(
|
||||
`data:application/json,${JSON.stringify(res.data)}`,
|
||||
);
|
||||
i.exportSettingsLink.current?.click();
|
||||
} else if (res.state === "failed") {
|
||||
toast(
|
||||
res.err.message === "rate_limit_error"
|
||||
? I18NextService.i18n.t("import_export_rate_limit_error")
|
||||
: I18NextService.i18n.t("export_error"),
|
||||
"danger",
|
||||
);
|
||||
}
|
||||
|
||||
i.setState({ exportSettingsRes: EMPTY_REQUEST });
|
||||
}
|
||||
|
||||
async handleImportSettings(i: Settings) {
|
||||
i.setState({ importSettingsRes: LOADING_REQUEST });
|
||||
|
||||
const res = await HttpService.client.importSettings(
|
||||
JSON.parse(await i.state.settingsFile!.text()),
|
||||
);
|
||||
|
||||
if (res.state === "success") {
|
||||
toast(I18NextService.i18n.t("import_success"), "success");
|
||||
|
||||
const saveRes = i.state.saveRes;
|
||||
i.setState({ saveRes: LOADING_REQUEST });
|
||||
|
||||
const siteRes = await HttpService.client.getSite();
|
||||
i.setState({ saveRes });
|
||||
|
||||
if (siteRes.state === "success") {
|
||||
const {
|
||||
local_user: {
|
||||
show_nsfw,
|
||||
blur_nsfw,
|
||||
auto_expand,
|
||||
theme,
|
||||
default_sort_type,
|
||||
default_listing_type,
|
||||
interface_language,
|
||||
show_avatars,
|
||||
show_bot_accounts,
|
||||
show_scores,
|
||||
show_read_posts,
|
||||
send_notifications_to_email,
|
||||
email,
|
||||
open_links_in_new_tab,
|
||||
},
|
||||
person: {
|
||||
avatar,
|
||||
banner,
|
||||
display_name,
|
||||
bot_account,
|
||||
bio,
|
||||
matrix_user_id,
|
||||
},
|
||||
} = siteRes.data.my_user!.local_user_view;
|
||||
|
||||
UserService.Instance.myUserInfo = siteRes.data.my_user;
|
||||
|
||||
i.setState(prev => ({
|
||||
...prev,
|
||||
siteRes: siteRes.data,
|
||||
saveUserSettingsForm: {
|
||||
...prev.saveUserSettingsForm,
|
||||
show_avatars,
|
||||
show_bot_accounts,
|
||||
show_nsfw,
|
||||
teme: theme ?? "browser",
|
||||
avatar,
|
||||
banner,
|
||||
display_name,
|
||||
bio,
|
||||
matrix_user_id,
|
||||
auto_expand,
|
||||
blur_nsfw,
|
||||
bot_account,
|
||||
default_listing_type,
|
||||
default_sort_type,
|
||||
discussion_languages: siteRes.data.my_user?.discussion_languages,
|
||||
email,
|
||||
interface_language,
|
||||
open_links_in_new_tab,
|
||||
send_notifications_to_email,
|
||||
show_read_posts,
|
||||
show_scores,
|
||||
},
|
||||
}));
|
||||
}
|
||||
} else if (res.state === "failed") {
|
||||
toast(
|
||||
res.err.message === "rate_limit_error"
|
||||
? I18NextService.i18n.t("import_export_rate_limit_error")
|
||||
: I18NextService.i18n.t("import_error"),
|
||||
"danger",
|
||||
);
|
||||
}
|
||||
|
||||
i.setState({ importSettingsRes: EMPTY_REQUEST, settingsFile: undefined });
|
||||
}
|
||||
|
||||
handleDeleteAccountShowConfirmToggle(i: Settings) {
|
||||
i.setState({ deleteAccountShowConfirm: !i.state.deleteAccountShowConfirm });
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue