feat(subplebbit settings): add json editor

This commit is contained in:
plebeius
2025-08-02 23:45:00 +02:00
parent a07fd0062e
commit 0f4058ea13
6 changed files with 219 additions and 20 deletions

View File

@@ -224,7 +224,7 @@
"add_moderator": "add a moderator",
"add_rule": "add a rule",
"json_settings": "JSON settings",
"json_settings_info": "quickly copy or paste the community settings",
"json_settings_info": "quickly copy and paste the community settings",
"address_setting_info": "Set a readable community address using a crypto domain",
"enter_crypto_address": "Please enter a valid crypto address.",
"check_for_updates": "<1>Check</1> for updates",

View File

@@ -15,6 +15,7 @@ import PostPage from './views/post-page';
import Profile from './views/profile';
import Settings from './views/settings';
import AccountDataEditor from './views/settings/account-data-editor';
import SubplebbitDataEditor from './views/subplebbit-settings/subplebbit-data-editor';
import SubmitPage from './views/submit-page';
import Subplebbit from './views/subplebbit';
import SubplebbitSettings from './views/subplebbit-settings';
@@ -97,6 +98,7 @@ const App = () => {
<Route path='/settings' element={<Settings />} />
<Route path='/p/:subplebbitAddress/settings' element={<SubplebbitSettings />} />
<Route path='/p/:subplebbitAddress/settings/editor' element={<SubplebbitDataEditor />} />
<Route path='/settings/plebbit-options' element={<Settings />} />
<Route path='/settings/content-options' element={<Settings />} />
<Route path='/settings/account-data' element={<AccountDataEditor />} />

View File

@@ -176,7 +176,7 @@ export const isSubplebbitAboutView = (pathname: string, params: ParamsType): boo
};
export const isSubplebbitSettingsView = (pathname: string, params: ParamsType): boolean => {
return params.subplebbitAddress ? pathname === `/p/${params.subplebbitAddress}/settings` : false;
return params.subplebbitAddress ? pathname === `/p/${params.subplebbitAddress}/settings` || pathname === `/p/${params.subplebbitAddress}/settings/editor` : false;
};
export const isSubplebbitSubmitView = (pathname: string, params: ParamsType): boolean => {

View File

@@ -0,0 +1 @@
export { default } from './subplebbit-data-editor';

View File

@@ -0,0 +1,211 @@
import React, { useEffect, useMemo, useState, lazy, Suspense, Component } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { usePublishSubplebbitEdit, useSubplebbit } from '@plebbit/plebbit-react-hooks';
import useTheme from '../../../stores/use-theme-store';
import styles from '../../settings/account-data-editor/account-data-editor.module.css';
import useIsMobile from '../../../hooks/use-is-mobile';
import LoadingEllipsis from '../../../components/loading-ellipsis';
import useSubplebbitSettingsStore from '../../../stores/use-subplebbit-settings-store';
import { useParams } from 'react-router-dom';
import ErrorDisplay from '../../../components/error-display';
import useStateString from '../../../hooks/use-state-string';
class EditorErrorBoundary extends Component<{ children: React.ReactNode; fallback: React.ReactNode }> {
constructor(props: { children: React.ReactNode; fallback: React.ReactNode }) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error: any, errorInfo: any) {
console.error('Ace Editor failed to load:', error, errorInfo);
}
render() {
if ((this.state as any).hasError) {
return this.props.fallback;
}
return this.props.children;
}
}
const LazyAceEditor = lazy(async () => {
const ReactAceModule = await import('react-ace');
await import('ace-builds/src-noconflict/mode-json');
await import('ace-builds/src-noconflict/theme-github');
await import('ace-builds/src-noconflict/theme-tomorrow_night');
return ReactAceModule;
});
const FallbackEditor = ({ value, onChange, height, disabled }: { value: string; onChange: (value: string) => void; height: string; disabled?: boolean }) => {
const { t } = useTranslation();
return (
<div>
<div className={styles.infobar}>{t('editor_fallback_warning', 'Advanced editor failed to load. Using basic text editor as fallback.')}</div>
<textarea value={value} onChange={(e) => onChange(e.target.value)} className={styles.fallbackEditor} style={{ height }} spellCheck={false} disabled={disabled} />
</div>
);
};
const SubplebbitDataEditor = () => {
const { t } = useTranslation();
const isMobile = useIsMobile();
const theme = useTheme((state) => state.theme);
const [text, setText] = useState('');
const { subplebbitAddress } = useParams<{ subplebbitAddress: string }>();
const subplebbit = useSubplebbit({ subplebbitAddress });
const { address, challenges, createdAt, description, error, rules, settings, suggested, roles, title } = subplebbit || {};
const hasLoaded = !!createdAt;
const { publishSubplebbitEditOptions, setSubplebbitSettingsStore, resetSubplebbitSettingsStore } = useSubplebbitSettingsStore();
const { error: publishSubplebbitEditError, publishSubplebbitEdit } = usePublishSubplebbitEdit(publishSubplebbitEditOptions);
const subplebbitSettings = useMemo(
() => JSON.stringify({ title, description, address, suggested, rules, roles, settings, challenges, subplebbitAddress }, null, 2),
[title, description, address, suggested, rules, roles, settings, challenges, subplebbitAddress],
);
useEffect(() => {
setText(subplebbitSettings);
}, [subplebbitSettings]);
const [showSaving, setShowSaving] = useState(false);
const [currentError, setCurrentError] = useState<Error | undefined>(undefined);
const saveSubplebbitSettings = async () => {
try {
setShowSaving(true);
setCurrentError(undefined);
console.log('Saving subplebbit with options:', publishSubplebbitEditOptions);
await publishSubplebbitEdit();
setShowSaving(false);
if (publishSubplebbitEditError) {
setCurrentError(publishSubplebbitEditError);
alert(publishSubplebbitEditError.message || 'Error: ' + publishSubplebbitEditError);
} else {
alert(t('settings_saved', { subplebbitAddress }));
}
} catch (e) {
if (e instanceof Error) {
console.warn(e);
setCurrentError(e);
alert(`failed editing subplebbit: ${e.message}`);
} else {
console.error('An unknown error occurred:', e);
}
}
};
// Set store for loaded subplebbit settings when editing
useEffect(() => {
if (hasLoaded) {
resetSubplebbitSettingsStore();
setSubplebbitSettingsStore({
title: title ?? '',
description: description ?? '',
address,
suggested: suggested ?? {},
rules: rules ?? [],
roles: roles ?? {},
settings: settings ?? {},
challenges: challenges ?? [],
subplebbitAddress,
});
}
}, [
hasLoaded,
resetSubplebbitSettingsStore,
setSubplebbitSettingsStore,
title,
description,
address,
suggested,
rules,
roles,
settings,
challenges,
subplebbitAddress,
]);
const loadingStateString = useStateString(subplebbit);
if (!hasLoaded) {
return (
<>
{error?.message && (
<div className={styles.error}>
<ErrorDisplay error={error} />
</div>
)}
<div className={styles.loading}>
<LoadingEllipsis string={loadingStateString || t('loading')} />
</div>
</>
);
}
return (
<div className={styles.content}>
<EditorErrorBoundary
fallback={<FallbackEditor value={text} onChange={setText} height={isMobile ? 'calc(80vh - 95px)' : 'calc(90vh - 77px)'} disabled={showSaving} />}
>
<Suspense
fallback={
<div className={styles.loading}>
<LoadingEllipsis string={t('loading_editor')} />
</div>
}
>
<LazyAceEditor
mode='json'
theme={theme === 'dark' ? 'tomorrow_night' : 'github'}
value={text}
onChange={setText}
name='ACCOUNT_DATA_EDITOR'
editorProps={{ $blockScrolling: true }}
className={styles.editor}
width='100%'
height={isMobile ? 'calc(80vh - 95px)' : 'calc(90vh - 77px)'}
setOptions={{
useWorker: false,
enableBasicAutocompletion: false,
enableLiveAutocompletion: false,
enableSnippets: false,
showPrintMargin: false,
highlightActiveLine: true,
showGutter: true,
foldStyle: 'markbeginend',
showFoldWidgets: true,
readOnly: showSaving,
}}
fontSize={14}
/>
</Suspense>
</EditorErrorBoundary>
{currentError && <div className={styles.error}>error: {currentError.message || 'unknown error'}</div>}
{showSaving ? (
<div className={styles.loading}>
<LoadingEllipsis string={t('saving')} />
</div>
) : (
<div className={styles.buttons}>
<Trans
i18nKey='save_reset_changes'
components={{
1: <button key='saveSubplebbitSettingsButton' onClick={saveSubplebbitSettings} />,
2: <button key='resetSubplebbitSettingsButton' onClick={() => setText(subplebbitSettings)} />,
}}
/>
</div>
)}
</div>
);
};
export default SubplebbitDataEditor;

View File

@@ -323,30 +323,15 @@ const Moderators = ({ isReadOnly = false }: { isReadOnly?: boolean }) => {
const JSONSettings = ({ isReadOnly = false }: { isReadOnly?: boolean }) => {
const { t } = useTranslation();
const { challenges, title, description, address, suggested, rules, roles, settings, subplebbitAddress, setSubplebbitSettingsStore } = useSubplebbitSettingsStore();
const [text, setText] = useState('');
useEffect(() => {
const JSONSettings = JSON.stringify({ title, description, address, suggested, rules, roles, settings, challenges, subplebbitAddress }, null, 2);
setText(JSONSettings);
}, [challenges, title, description, address, suggested, rules, roles, settings, subplebbitAddress]);
const handleChange = (newText: string) => {
setText(newText);
try {
const newSettings = JSON.parse(newText);
setSubplebbitSettingsStore(newSettings);
} catch (e) {
console.error('Invalid JSON format');
}
};
const navigate = useNavigate();
const { subplebbitAddress } = useParams<{ subplebbitAddress: string }>();
return (
<div className={`${styles.box}`}>
<div className={`${styles.boxTitle} ${styles.JSONSettingsTitle}`}>{t('json_settings')}</div>
<div className={styles.boxSubtitle}>{t('json_settings_info')}</div>
<div className={`${styles.boxInput} ${styles.JSONSettings}`}>
<textarea onChange={(e) => handleChange(e.target.value)} autoCorrect='off' autoComplete='off' spellCheck='false' value={text} disabled={isReadOnly} />
<button onClick={() => navigate(`/p/${subplebbitAddress}/settings/editor`)}>{t('edit')}</button>
</div>
</div>
);