mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
feat(settings): move account json editing to dedicated full editor route
This commit is contained in:
+16
-10
@@ -34,6 +34,7 @@ import PostForm from './components/post-form';
|
||||
import BoardBlotter from './components/board-blotter';
|
||||
import BoardsBar from './components/boardsbar';
|
||||
|
||||
const AccountDataEditor = lazy(() => import('./views/account-data-editor'));
|
||||
const BoardsBarEditModal = lazy(() => import('./components/boardsbar-edit-modal'));
|
||||
const CreateBoardModal = lazy(() => import('./components/create-board-modal'));
|
||||
const DirectoryModal = lazy(() => import('./components/directory-modal'));
|
||||
@@ -47,7 +48,7 @@ preloadThemeAssets();
|
||||
const hasModQueueAccessRole = (role?: string): boolean => role === 'admin' || role === 'owner' || role === 'moderator';
|
||||
|
||||
const BoardLayout = () => {
|
||||
const { accountCommentIndex, boardIdentifier } = useParams();
|
||||
const { accountCommentIndex, boardIdentifier, pageNumber } = useParams();
|
||||
const location = useLocation();
|
||||
const isMobile = useIsMobile();
|
||||
const isInAllView = isAllView(location.pathname);
|
||||
@@ -83,6 +84,10 @@ const BoardLayout = () => {
|
||||
? `${subplebbitAddress}-${location.pathname.replace(/\/settings$/, '')}`
|
||||
: `${subplebbitAddress}-${location.pathname}`;
|
||||
|
||||
if (pageNumber === '1') {
|
||||
return <Navigate to='/not-found' replace />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.boardLayout}>
|
||||
<BoardsBar />
|
||||
@@ -180,14 +185,6 @@ const CatalogFeedRoute = () => {
|
||||
return <Catalog viewType={viewType} boardIdentifier={params.boardIdentifier} />;
|
||||
};
|
||||
|
||||
const PageOneGuard = () => {
|
||||
const { pageNumber } = useParams();
|
||||
if (pageNumber === '1') {
|
||||
return <Navigate to='/not-found' replace />;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const ModQueueRoute = () => {
|
||||
const { boardIdentifier } = useParams();
|
||||
const account = useAccount();
|
||||
@@ -244,6 +241,14 @@ const App = () => {
|
||||
<Route path='/faq' element={<FAQ />} />
|
||||
<Route path='/rules/:boardIdentifier?' element={<Rules />} />
|
||||
<Route path='/blotter' element={<Blotter />} />
|
||||
<Route
|
||||
path='/settings/account-data'
|
||||
element={
|
||||
<Suspense fallback={null}>
|
||||
<AccountDataEditor />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route element={<BoardLayout />}>
|
||||
{/* Canonical multiboard routes (no time filter) */}
|
||||
<Route path='/all' element={boardFeedElement} />
|
||||
@@ -269,7 +274,8 @@ const App = () => {
|
||||
<Route path='/subs/*' element={<Navigate to='/not-found' replace />} />
|
||||
<Route path='/mod/*' element={<Navigate to='/not-found' replace />} />
|
||||
|
||||
<Route path='/:boardIdentifier/:pageNumber' element={<PageOneGuard />} />
|
||||
<Route path='/:boardIdentifier/:pageNumber' element={boardFeedElement} />
|
||||
<Route path='/:boardIdentifier/:pageNumber/settings' element={boardFeedElement} />
|
||||
<Route path='/:boardIdentifier' element={boardFeedElement} />
|
||||
<Route path='/:boardIdentifier/settings' element={boardFeedElement} />
|
||||
<Route path='/:boardIdentifier/catalog' element={catalogFeedElement} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { buildEditableAccountJson, safeParseAccountJson, buildSavePayload } from '../account-editor-utils';
|
||||
|
||||
describe('buildEditableAccountJson', () => {
|
||||
it('strips runtime-only fields from account', () => {
|
||||
const account = {
|
||||
id: 'abc',
|
||||
name: 'Account 1',
|
||||
author: { address: '0x123', shortAddress: '0x1...3', avatar: { url: 'https://example.com' } },
|
||||
plebbit: { someOption: true },
|
||||
karma: 42,
|
||||
plebbitReactOptions: { foo: 'bar' },
|
||||
unreadNotificationCount: 5,
|
||||
};
|
||||
const result = JSON.parse(buildEditableAccountJson(account));
|
||||
expect(result.account.id).toBe('abc');
|
||||
expect(result.account.name).toBe('Account 1');
|
||||
expect(result.account.author.address).toBe('0x123');
|
||||
expect(result.account.author.avatar).toBeUndefined();
|
||||
expect(result.account.plebbit).toBeUndefined();
|
||||
expect(result.account.karma).toBeUndefined();
|
||||
expect(result.account.plebbitReactOptions).toBeUndefined();
|
||||
expect(result.account.unreadNotificationCount).toBeUndefined();
|
||||
});
|
||||
|
||||
it('handles undefined account', () => {
|
||||
const result = buildEditableAccountJson(undefined);
|
||||
expect(result).toBeTruthy();
|
||||
const parsed = JSON.parse(result);
|
||||
expect(parsed.account).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('safeParseAccountJson', () => {
|
||||
it('parses valid account JSON', () => {
|
||||
const result = safeParseAccountJson('{"account": {"name": "test"}}');
|
||||
expect(result).toEqual({ account: { name: 'test' } });
|
||||
});
|
||||
|
||||
it('returns null for invalid JSON', () => {
|
||||
expect(safeParseAccountJson('not json')).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null for JSON without account key', () => {
|
||||
expect(safeParseAccountJson('{"name": "test"}')).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null for non-object account', () => {
|
||||
expect(safeParseAccountJson('{"account": "string"}')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildSavePayload', () => {
|
||||
it('preserves original account id', () => {
|
||||
const parsed = { account: { name: 'updated', id: 'wrong-id' } };
|
||||
const result = buildSavePayload(parsed, 'original-id');
|
||||
expect(result.id).toBe('original-id');
|
||||
expect(result.name).toBe('updated');
|
||||
});
|
||||
|
||||
it('handles undefined original id', () => {
|
||||
const parsed = { account: { name: 'test' } };
|
||||
const result = buildSavePayload(parsed, undefined);
|
||||
expect(result.id).toBeUndefined();
|
||||
expect(result.name).toBe('test');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
import stringify from 'json-stringify-pretty-compact';
|
||||
|
||||
type AccountLike = {
|
||||
id?: string;
|
||||
name?: string;
|
||||
author?: { address?: string; shortAddress?: string; avatar?: unknown };
|
||||
plebbit?: unknown;
|
||||
karma?: unknown;
|
||||
plebbitReactOptions?: unknown;
|
||||
unreadNotificationCount?: unknown;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
/**
|
||||
* Build the editable JSON string for an account, stripping runtime-only fields.
|
||||
*/
|
||||
export const buildEditableAccountJson = (account: AccountLike | undefined): string =>
|
||||
stringify({
|
||||
account: {
|
||||
...account,
|
||||
author: { ...account?.author, avatar: undefined },
|
||||
plebbit: undefined,
|
||||
karma: undefined,
|
||||
plebbitReactOptions: undefined,
|
||||
unreadNotificationCount: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
export const safeParseAccountJson = (text: string): { account: Record<string, unknown> } | null => {
|
||||
try {
|
||||
const parsed = JSON.parse(text);
|
||||
if (parsed && typeof parsed === 'object' && parsed.account && typeof parsed.account === 'object') {
|
||||
return parsed as { account: Record<string, unknown> };
|
||||
}
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Build a save-ready account payload, preserving the original account id.
|
||||
*/
|
||||
export const buildSavePayload = (parsed: { account: Record<string, unknown> }, originalId: string | undefined): Record<string, unknown> => ({
|
||||
...parsed.account,
|
||||
id: originalId,
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
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 AccountDataEditor from '../account-data-editor';
|
||||
|
||||
(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('../../../lib/utils/account-editor-utils', () => ({
|
||||
buildEditableAccountJson: () => '{"account": {"name": "test"}}',
|
||||
safeParseAccountJson: vi.fn((text: string) => {
|
||||
try {
|
||||
const p = JSON.parse(text);
|
||||
return p?.account ? p : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
buildSavePayload: vi.fn((parsed: { account: Record<string, unknown> }, id: string) => ({ ...parsed.account, id })),
|
||||
}));
|
||||
|
||||
vi.mock('@plebbit/plebbit-react-hooks', () => ({
|
||||
useAccount: () => ({ id: 'test-id', name: 'Account 1', author: { address: '0x123', shortAddress: '0x1...3' } }),
|
||||
setAccount: vi.fn(),
|
||||
}));
|
||||
|
||||
let root: Root;
|
||||
let container: HTMLDivElement;
|
||||
|
||||
const render = (children: React.ReactNode) => {
|
||||
act(() => {
|
||||
root.render(createElement(MemoryRouter, { initialEntries: ['/settings/account-data'] }, children));
|
||||
});
|
||||
};
|
||||
|
||||
describe('AccountDataEditor', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
act(() => root.unmount());
|
||||
container.remove();
|
||||
});
|
||||
|
||||
it('renders warning gate initially', () => {
|
||||
render(createElement(AccountDataEditor));
|
||||
expect(container.textContent).toContain('private_key_warning_title');
|
||||
});
|
||||
|
||||
it('shows go_back and continue buttons in warning gate', () => {
|
||||
render(createElement(AccountDataEditor));
|
||||
const buttons = Array.from(container.querySelectorAll('button'));
|
||||
const texts = buttons.map((b) => b.textContent ?? '');
|
||||
expect(texts.some((t) => t.includes('go_back'))).toBe(true);
|
||||
expect(texts.some((t) => t.includes('continue'))).toBe(true);
|
||||
});
|
||||
|
||||
it('does not show editor controls in warning phase', () => {
|
||||
render(createElement(AccountDataEditor));
|
||||
const buttons = Array.from(container.querySelectorAll('button'));
|
||||
const texts = buttons.map((b) => b.textContent ?? '');
|
||||
expect(texts.some((t) => t.includes('save'))).toBe(false);
|
||||
expect(texts.some((t) => t.includes('reset_changes'))).toBe(false);
|
||||
expect(texts.some((t) => t.includes('return_to_settings'))).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
.container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.warningGate {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.warningTitle {
|
||||
font-weight: 700;
|
||||
font-size: 1.2em;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.warningDescription {
|
||||
color: #666;
|
||||
margin-bottom: 24px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.warningButtons {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.editorContainer {
|
||||
font-family: monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-top: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.fallbackWarning {
|
||||
color: #b8860b;
|
||||
background: #fff8e0;
|
||||
border: 1px solid #e0c000;
|
||||
padding: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.loadingMessage {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { setAccount, useAccount } from '@plebbit/plebbit-react-hooks';
|
||||
import { buildEditableAccountJson, safeParseAccountJson, buildSavePayload } from '../../lib/utils/account-editor-utils';
|
||||
import styles from './account-data-editor.module.css';
|
||||
|
||||
const DEFAULT_RETURN_TO = '/subs/settings#account-settings';
|
||||
|
||||
const loadAce = async () => {
|
||||
const [aceModule] = await Promise.all([import('react-ace'), import('ace-builds/src-noconflict/mode-json'), import('ace-builds/src-noconflict/theme-monokai')]);
|
||||
return aceModule.default;
|
||||
};
|
||||
|
||||
const AccountDataEditor = () => {
|
||||
const { t } = useTranslation();
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const account = useAccount();
|
||||
const returnTo = (location.state as { returnTo?: string } | null)?.returnTo ?? DEFAULT_RETURN_TO;
|
||||
|
||||
const [phase, setPhase] = useState<'warning' | 'loading' | 'editor' | 'fallback'>('warning');
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const [AceEditor, setAceEditor] = useState<React.ComponentType<any> | null>(null);
|
||||
const [text, setText] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (phase !== 'loading') return;
|
||||
loadAce()
|
||||
.then((Editor) => {
|
||||
setAceEditor(() => Editor);
|
||||
setText(buildEditableAccountJson(account));
|
||||
setPhase('editor');
|
||||
})
|
||||
.catch(() => {
|
||||
setText(buildEditableAccountJson(account));
|
||||
setPhase('fallback');
|
||||
});
|
||||
}, [phase, account]);
|
||||
|
||||
const handleGoBack = () => navigate(returnTo);
|
||||
const handleContinue = () => setPhase('loading');
|
||||
const handleReset = () => setText(buildEditableAccountJson(account));
|
||||
const handleReturn = () => navigate(returnTo);
|
||||
|
||||
const handleSave = async () => {
|
||||
const parsed = safeParseAccountJson(text);
|
||||
if (!parsed) {
|
||||
alert('Invalid JSON');
|
||||
return;
|
||||
}
|
||||
const payload = buildSavePayload(parsed, account?.id);
|
||||
try {
|
||||
await setAccount(payload);
|
||||
navigate(returnTo);
|
||||
window.location.reload();
|
||||
} catch (e) {
|
||||
alert(e instanceof Error ? e.message : 'Error saving');
|
||||
}
|
||||
};
|
||||
|
||||
if (phase === 'warning') {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.warningGate}>
|
||||
<div className={styles.warningTitle}>{t('private_key_warning_title')}</div>
|
||||
<div className={styles.warningDescription}>{t('private_key_warning_description')}</div>
|
||||
<div className={styles.warningButtons}>
|
||||
<button type='button' onClick={handleGoBack}>
|
||||
{t('go_back')}
|
||||
</button>
|
||||
<button type='button' onClick={handleContinue}>
|
||||
{t('continue')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (phase === 'loading') {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.loadingMessage}>{t('loading_editor')}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
{phase === 'fallback' && <div className={styles.fallbackWarning}>{t('editor_fallback_warning')}</div>}
|
||||
<div className={styles.editorContainer}>
|
||||
{phase === 'editor' && AceEditor ? (
|
||||
<AceEditor mode='json' theme='monokai' width='100%' height='500px' fontSize={13} showPrintMargin={false} value={text} onChange={setText} />
|
||||
) : (
|
||||
<textarea
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
style={{ width: '100%', height: '500px', fontFamily: 'monospace', fontSize: 13 }}
|
||||
spellCheck={false}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.controls}>
|
||||
<button type='button' onClick={handleSave}>
|
||||
{t('save')}
|
||||
</button>
|
||||
<button type='button' onClick={handleReset}>
|
||||
{t('reset_changes')}
|
||||
</button>
|
||||
<button type='button' onClick={handleReturn}>
|
||||
{t('return_to_settings')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccountDataEditor;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './account-data-editor';
|
||||
Reference in New Issue
Block a user