feat(settings): move account json editing to dedicated full editor route

This commit is contained in:
plebeius
2026-02-24 20:33:07 +08:00
parent dc43a8438e
commit fb9443a0f8
47 changed files with 852 additions and 219 deletions
@@ -0,0 +1,87 @@
import * as React from 'react';
import { createElement } from 'react';
import { createRoot, Root } from 'react-dom/client';
import { MemoryRouter } from 'react-router-dom';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import AccountSettings from '../account-settings';
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
const act = (React as { act?: (cb: () => void | Promise<void>) => void | Promise<void> }).act as (cb: () => void | Promise<void>) => void | Promise<void>;
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => key,
i18n: { changeLanguage: vi.fn(), language: 'en' },
}),
}));
vi.mock('@plebbit/plebbit-react-hooks', () => ({
useAccount: () => ({ id: 'test-id', name: 'Account 1', author: { address: '0x123', shortAddress: '0x1...3' } }),
useAccounts: () => ({ accounts: [{ id: 'test-id', name: 'Account 1', author: { shortAddress: '0x1...3' } }] }),
createAccount: vi.fn(),
deleteAccount: vi.fn(),
exportAccount: vi.fn(),
importAccount: vi.fn(),
setActiveAccount: vi.fn(),
}));
vi.mock('@capacitor/core', () => ({
Capacitor: { getPlatform: () => 'web' },
}));
let root: Root;
let container: HTMLDivElement;
const render = (children: React.ReactNode) => {
act(() => {
root.render(createElement(MemoryRouter, { initialEntries: ['/subs/settings'] }, children));
});
};
describe('AccountSettings', () => {
beforeEach(() => {
vi.clearAllMocks();
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
});
afterEach(() => {
act(() => root.unmount());
container.remove();
});
it('does not render an inline textarea', () => {
render(createElement(AccountSettings));
expect(container.querySelector('textarea')).toBeNull();
});
it('does not render save_changes or reset_changes buttons', () => {
render(createElement(AccountSettings));
const buttons = Array.from(container.querySelectorAll('button'));
const buttonTexts = buttons.map((b) => b.textContent ?? '');
expect(buttonTexts.some((t) => t.includes('save_changes'))).toBe(false);
expect(buttonTexts.some((t) => t.includes('reset_changes'))).toBe(false);
});
it('renders an edit button', () => {
render(createElement(AccountSettings));
const buttons = Array.from(container.querySelectorAll('button'));
expect(buttons.some((b) => (b.textContent ?? '').includes('edit'))).toBe(true);
});
it('renders create, import, export buttons', () => {
render(createElement(AccountSettings));
const buttons = Array.from(container.querySelectorAll('button'));
const texts = buttons.map((b) => b.textContent ?? '');
expect(texts.some((t) => t.includes('create'))).toBe(true);
expect(texts.some((t) => t.includes('import'))).toBe(true);
expect(texts.some((t) => t.includes('export'))).toBe(true);
});
it('renders delete_account button', () => {
render(createElement(AccountSettings));
const buttons = Array.from(container.querySelectorAll('button'));
expect(buttons.some((b) => (b.textContent ?? '').includes('delete_account'))).toBe(true);
});
});
@@ -3,34 +3,10 @@
margin-bottom: 10px;
}
.setting textarea {
display: block;
font-family: dejavu sans mono, consolas, andale mono, lucida console, monospace;
outline: none;
border: 1px solid #aaa;
font-size: 11px;
margin: 0 2px 0 0;
padding: 2px 4px 3px;
min-height: 150px;
width: 95%;
}
.setting textarea:focus {
border-color: 1px solid #98e;
}
.setting button {
cursor: pointer;
}
.saveResetAccount {
margin-top: 15px;
}
.setting textarea {
margin-bottom: 5px;
}
.setting div {
margin-top: 5px;
}
@@ -1,7 +1,6 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import { useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { createAccount, deleteAccount, exportAccount, importAccount, setAccount, setActiveAccount, useAccount, useAccounts } from '@plebbit/plebbit-react-hooks';
import stringify from 'json-stringify-pretty-compact';
import { createAccount, deleteAccount, exportAccount, importAccount, setActiveAccount, useAccount, useAccounts } from '@plebbit/plebbit-react-hooks';
import styles from './account-settings.module.css';
import { Capacitor } from '@capacitor/core';
import { useLocation, useNavigate } from 'react-router-dom';
@@ -33,24 +32,6 @@ const AccountSettingsEditor = ({
}) => {
const { t } = useTranslation();
const location = useLocation();
const accountJson = useMemo(
() =>
stringify({
account: {
...account,
author: { ...account?.author, avatar: undefined },
plebbit: undefined,
karma: undefined,
plebbitReactOptions: undefined,
unreadNotificationCount: undefined,
},
}),
[account],
);
const [text, setText] = useState(() => accountJson);
const { accounts } = useAccounts();
const switchToNewAccountRef = useRef(false);
const navigate = useNavigate();
@@ -93,29 +74,6 @@ const AccountSettingsEditor = ({
}
};
const saveAccount = async () => {
const parsed = safeParseJSON<{ account: Record<string, unknown> }>(text);
if (!parsed?.account) {
alert('Invalid JSON');
return;
}
const newAccount = parsed.account;
const result = await withErrorHandling(
() => setAccount({ ...newAccount, id: account?.id }),
(error) => {
if (error instanceof Error) {
alert(error.message);
console.log(error);
} else {
console.error('An unknown error occurred:', error);
}
},
);
if (result !== undefined) {
alert(`Saved ${newAccount.name}`);
}
};
const handleExportAccount = async () => {
const accountString = await withErrorHandling(
() => exportAccount(),
@@ -247,10 +205,8 @@ const AccountSettingsEditor = ({
})}
</div>
</div>
<div></div>
<textarea value={text} onChange={(e) => setText(e.target.value)} autoCorrect='off' autoComplete='off' spellCheck='false' />
<div>
<button onClick={saveAccount}>{t('save_changes')}</button> <button onClick={() => setText(accountJson)}>{t('reset_changes')}</button>
<button onClick={() => navigate('/settings/account-data', { state: { returnTo: location.pathname + location.hash } })}>{t('edit')}</button>
<button className={styles.deleteAccount} onClick={() => _deleteAccount(account?.name ?? '')}>
{t('delete_account')}
</button>