pl-fe: migrate config?
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
parent
44a4116a75
commit
ce2ac73fbe
7 changed files with 85 additions and 61 deletions
|
@ -17,7 +17,7 @@ const CryptoAddressInput: StreamfieldComponent<CryptoAddress> = ({ value, onChan
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
const handleChange = (key: 'ticker' | 'address' | 'note'): React.ChangeEventHandler<HTMLInputElement> => e => {
|
const handleChange = (key: 'ticker' | 'address' | 'note'): React.ChangeEventHandler<HTMLInputElement> => e => {
|
||||||
onChange(value.set(key, e.currentTarget.value));
|
onChange({ ...value, [key]: e.currentTarget.value });
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -16,7 +16,7 @@ const PromoPanelInput: StreamfieldComponent<FooterItem> = ({ value, onChange })
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
const handleChange = (key: 'title' | 'url'): React.ChangeEventHandler<HTMLInputElement> => e => {
|
const handleChange = (key: 'title' | 'url'): React.ChangeEventHandler<HTMLInputElement> => e => {
|
||||||
onChange(value.set(key, e.currentTarget.value));
|
onChange({ ...value, [key]: e.currentTarget.value });
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -19,11 +19,11 @@ const PromoPanelInput: StreamfieldComponent<PromoPanelItem> = ({ value, onChange
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
const handleIconChange = (icon: string) => {
|
const handleIconChange = (icon: string) => {
|
||||||
onChange(value.set('icon', icon));
|
onChange({ ...value, icon });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChange = (key: 'text' | 'url'): React.ChangeEventHandler<HTMLInputElement> => e => {
|
const handleChange = (key: 'text' | 'url'): React.ChangeEventHandler<HTMLInputElement> => e => {
|
||||||
onChange(value.set(key, e.currentTarget.value));
|
onChange({ ...value, [key]: e.currentTarget.value });
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
|
import { create } from 'mutative';
|
||||||
import React, { useState, useEffect, useMemo } from 'react';
|
import React, { useState, useEffect, useMemo } from 'react';
|
||||||
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||||
import * as v from 'valibot';
|
import * as v from 'valibot';
|
||||||
|
@ -22,7 +22,7 @@ import ThemeSelector from 'pl-fe/features/ui/components/theme-selector';
|
||||||
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
|
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
|
||||||
import { useAppSelector } from 'pl-fe/hooks/use-app-selector';
|
import { useAppSelector } from 'pl-fe/hooks/use-app-selector';
|
||||||
import { useFeatures } from 'pl-fe/hooks/use-features';
|
import { useFeatures } from 'pl-fe/hooks/use-features';
|
||||||
import { plFeConfigSchema } from 'pl-fe/normalizers/pl-fe/pl-fe-config';
|
import { cryptoAddressSchema, footerItemSchema, plFeConfigSchema, promoPanelItemSchema, type PlFeConfig } from 'pl-fe/normalizers/pl-fe/pl-fe-config';
|
||||||
import toast from 'pl-fe/toast';
|
import toast from 'pl-fe/toast';
|
||||||
|
|
||||||
import CryptoAddressInput from './components/crypto-address-input';
|
import CryptoAddressInput from './components/crypto-address-input';
|
||||||
|
@ -55,18 +55,11 @@ const messages = defineMessages({
|
||||||
sentryDsnHint: { id: 'plfe_config.sentry_dsn_hint', defaultMessage: 'DSN URL for error reporting. Works with Sentry and GlitchTip.' },
|
sentryDsnHint: { id: 'plfe_config.sentry_dsn_hint', defaultMessage: 'DSN URL for error reporting. Works with Sentry and GlitchTip.' },
|
||||||
});
|
});
|
||||||
|
|
||||||
type ValueGetter<T = Element> = (e: React.ChangeEvent<T>) => any;
|
type ValueGetter<T1 = Element, T2 = any> = (e: React.ChangeEvent<T1>) => T2;
|
||||||
type Template = ImmutableMap<string, any>;
|
type StreamItemConfigPath = ['promoPanel', 'items'] | ['navlinks', 'homeFooter'] | ['cryptoAddresses'];
|
||||||
type ConfigPath = Array<string | number>;
|
|
||||||
type ThemeChangeHandler = (theme: string) => void;
|
type ThemeChangeHandler = (theme: string) => void;
|
||||||
|
|
||||||
const templates: Record<string, Template> = {
|
const PlFeConfigEditor: React.FC = () => {
|
||||||
promoPanelItem: ImmutableMap({ icon: '', text: '', url: '' }),
|
|
||||||
footerItem: ImmutableMap({ title: '', url: '' }),
|
|
||||||
cryptoAddress: ImmutableMap({ ticker: '', address: '', note: '' }),
|
|
||||||
};
|
|
||||||
|
|
||||||
const PlFeConfig: React.FC = () => {
|
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
@ -75,26 +68,25 @@ const PlFeConfig: React.FC = () => {
|
||||||
const initialData = useAppSelector(state => state.plfe);
|
const initialData = useAppSelector(state => state.plfe);
|
||||||
|
|
||||||
const [isLoading, setLoading] = useState(false);
|
const [isLoading, setLoading] = useState(false);
|
||||||
const [data, setData] = useState(initialData);
|
const [data, setData] = useState(v.parse(plFeConfigSchema, initialData));
|
||||||
const [jsonEditorExpanded, setJsonEditorExpanded] = useState(false);
|
const [jsonEditorExpanded, setJsonEditorExpanded] = useState(false);
|
||||||
const [rawJSON, setRawJSON] = useState<string>(JSON.stringify(initialData, null, 2));
|
const [rawJSON, setRawJSON] = useState<string>(JSON.stringify(initialData, null, 2));
|
||||||
const [jsonValid, setJsonValid] = useState(true);
|
const [jsonValid, setJsonValid] = useState(true);
|
||||||
|
|
||||||
const plFe = useMemo(() => v.parse(plFeConfigSchema, data), [data]);
|
const plFe = useMemo(() => v.parse(plFeConfigSchema, data), [data]);
|
||||||
|
|
||||||
const setConfig = (path: ConfigPath, value: any) => {
|
const setConfig = (newData: PlFeConfig) => {
|
||||||
const newData = data.setIn(path, value);
|
|
||||||
setData(newData);
|
setData(newData);
|
||||||
setJsonValid(true);
|
setJsonValid(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const putConfig = (newData: any) => {
|
const putConfig = (newData: PlFeConfig) => {
|
||||||
setData(newData);
|
setData(newData);
|
||||||
setJsonValid(true);
|
setJsonValid(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit: React.FormEventHandler = (e) => {
|
const handleSubmit: React.FormEventHandler = (e) => {
|
||||||
dispatch(updatePlFeConfig(data.toJS())).then(() => {
|
dispatch(updatePlFeConfig(data)).then(() => {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
toast.success(intl.formatMessage(messages.saved));
|
toast.success(intl.formatMessage(messages.saved));
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
|
@ -104,15 +96,20 @@ const PlFeConfig: React.FC = () => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChange = (path: ConfigPath, getValue: ValueGetter<any>): React.ChangeEventHandler => e => {
|
const handleChange = (path: keyof PlFeConfig, getValue: ValueGetter<any, PlFeConfig[typeof path]>): React.ChangeEventHandler => e => {
|
||||||
setConfig(path, getValue(e));
|
const newData: PlFeConfig = { ...data, [path]: getValue(e) };
|
||||||
|
setConfig(newData);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleThemeChange = (path: ConfigPath): ThemeChangeHandler => theme => {
|
const handleThemeChange: ThemeChangeHandler = (theme) => {
|
||||||
setConfig(path, theme);
|
const newData = create(data, (draft) => {
|
||||||
|
if (!draft.defaultSettings) draft.defaultSettings = {};
|
||||||
|
draft.defaultSettings.themeMode = theme;
|
||||||
|
});
|
||||||
|
setConfig(newData);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFileChange = (path: ConfigPath): React.ChangeEventHandler<HTMLInputElement> => e => {
|
const handleFileChange = (path: keyof PlFeConfig): React.ChangeEventHandler<HTMLInputElement> => e => {
|
||||||
const file = e.target.files?.item(0);
|
const file = e.target.files?.item(0);
|
||||||
|
|
||||||
if (file) {
|
if (file) {
|
||||||
|
@ -122,19 +119,40 @@ const PlFeConfig: React.FC = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleStreamItemChange = (path: ConfigPath) => (values: any[]) => {
|
const handleStreamItemChange = (path: StreamItemConfigPath) => (values: any[]) => {
|
||||||
setConfig(path, ImmutableList(values));
|
const newData = create(data, (draft) => {
|
||||||
|
if (path[0] === 'cryptoAddresses') {
|
||||||
|
draft.cryptoAddresses = values;
|
||||||
|
} else {
|
||||||
|
// @ts-ignore
|
||||||
|
draft[path[0]][path[1]] = values;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setConfig(newData);
|
||||||
};
|
};
|
||||||
|
|
||||||
const addStreamItem = (path: ConfigPath, template: Template) => () => {
|
const addStreamItem = <T, >(path: StreamItemConfigPath, schema: v.BaseSchema<any, T, v.BaseIssue<unknown>>) => () => {
|
||||||
let items = data;
|
const newData = create(data, (draft) => {
|
||||||
path.forEach(key => items = items?.[key] || []);
|
if (path[0] === 'cryptoAddresses') {
|
||||||
setConfig(path, items.push(template));
|
draft.cryptoAddresses.push(v.parse(cryptoAddressSchema, {}));
|
||||||
|
} else {
|
||||||
|
// @ts-ignore
|
||||||
|
draft[path[0]][path[1]].push(v.parse(schema, {}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setConfig(newData);
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteStreamItem = (path: ConfigPath) => (i: number) => {
|
const deleteStreamItem = (path: StreamItemConfigPath) => (i: number) => {
|
||||||
const newData = data.deleteIn([...path, i]);
|
const newData = create(data, (draft) => {
|
||||||
setData(newData);
|
if (path[0] === 'cryptoAddresses') {
|
||||||
|
draft.cryptoAddresses = draft.cryptoAddresses.filter((_, index) => index !== i);
|
||||||
|
} else {
|
||||||
|
// @ts-ignore
|
||||||
|
draft[path[0]][path[1]] = draft[path[0]][path[1]].filter((_, index) => index !== i);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setConfig(newData);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEditJSON: React.ChangeEventHandler<HTMLTextAreaElement> = e => {
|
const handleEditJSON: React.ChangeEventHandler<HTMLTextAreaElement> = e => {
|
||||||
|
@ -144,7 +162,7 @@ const PlFeConfig: React.FC = () => {
|
||||||
const toggleJSONEditor = (expanded: boolean) => setJsonEditorExpanded(expanded);
|
const toggleJSONEditor = (expanded: boolean) => setJsonEditorExpanded(expanded);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
putConfig(initialData);
|
putConfig(v.parse(plFeConfigSchema, initialData));
|
||||||
}, [initialData]);
|
}, [initialData]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -153,7 +171,7 @@ const PlFeConfig: React.FC = () => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
try {
|
try {
|
||||||
const data = fromJS(JSON.parse(rawJSON));
|
const data = v.parse(plFeConfigSchema, JSON.parse(rawJSON));
|
||||||
putConfig(data);
|
putConfig(data);
|
||||||
} catch {
|
} catch {
|
||||||
setJsonValid(false);
|
setJsonValid(false);
|
||||||
|
@ -171,7 +189,7 @@ const PlFeConfig: React.FC = () => {
|
||||||
hintText={<FormattedMessage id='plfe_config.hints.logo' defaultMessage='SVG. At most 2 MB. Will be displayed to 50px height, maintaining aspect ratio' />}
|
hintText={<FormattedMessage id='plfe_config.hints.logo' defaultMessage='SVG. At most 2 MB. Will be displayed to 50px height, maintaining aspect ratio' />}
|
||||||
>
|
>
|
||||||
<FileInput
|
<FileInput
|
||||||
onChange={handleFileChange(['logo'])}
|
onChange={handleFileChange('logo')}
|
||||||
accept='image/svg+xml,image/png'
|
accept='image/svg+xml,image/png'
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
@ -183,8 +201,8 @@ const PlFeConfig: React.FC = () => {
|
||||||
<List>
|
<List>
|
||||||
<ListItem label={<FormattedMessage id='plfe_config.fields.theme_label' defaultMessage='Default theme' />}>
|
<ListItem label={<FormattedMessage id='plfe_config.fields.theme_label' defaultMessage='Default theme' />}>
|
||||||
<ThemeSelector
|
<ThemeSelector
|
||||||
value={plFe.defaultSettings.get('themeMode')}
|
value={plFe.defaultSettings?.themeMode}
|
||||||
onChange={handleThemeChange(['defaultSettings', 'themeMode'])}
|
onChange={handleThemeChange}
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
|
@ -202,14 +220,14 @@ const PlFeConfig: React.FC = () => {
|
||||||
<ListItem label={intl.formatMessage(messages.displayFqnLabel)}>
|
<ListItem label={intl.formatMessage(messages.displayFqnLabel)}>
|
||||||
<Toggle
|
<Toggle
|
||||||
checked={plFe.displayFqn === true}
|
checked={plFe.displayFqn === true}
|
||||||
onChange={handleChange(['displayFqn'], (e) => e.target.checked)}
|
onChange={handleChange('displayFqn', (e) => e.target.checked)}
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
<ListItem label={intl.formatMessage(messages.greentextLabel)}>
|
<ListItem label={intl.formatMessage(messages.greentextLabel)}>
|
||||||
<Toggle
|
<Toggle
|
||||||
checked={plFe.greentext === true}
|
checked={plFe.greentext === true}
|
||||||
onChange={handleChange(['greentext'], (e) => e.target.checked)}
|
onChange={handleChange('greentext', (e) => e.target.checked)}
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
|
@ -219,7 +237,7 @@ const PlFeConfig: React.FC = () => {
|
||||||
>
|
>
|
||||||
<Toggle
|
<Toggle
|
||||||
checked={plFe.feedInjection === true}
|
checked={plFe.feedInjection === true}
|
||||||
onChange={handleChange(['feedInjection'], (e) => e.target.checked)}
|
onChange={handleChange('feedInjection', (e) => e.target.checked)}
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
|
@ -229,14 +247,14 @@ const PlFeConfig: React.FC = () => {
|
||||||
>
|
>
|
||||||
<Toggle
|
<Toggle
|
||||||
checked={plFe.mediaPreview === true}
|
checked={plFe.mediaPreview === true}
|
||||||
onChange={handleChange(['mediaPreview'], (e) => e.target.checked)}
|
onChange={handleChange('mediaPreview', (e) => e.target.checked)}
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
<ListItem label={intl.formatMessage(messages.displayCtaLabel)}>
|
<ListItem label={intl.formatMessage(messages.displayCtaLabel)}>
|
||||||
<Toggle
|
<Toggle
|
||||||
checked={plFe.displayCta === true}
|
checked={plFe.displayCta === true}
|
||||||
onChange={handleChange(['displayCta'], (e) => e.target.checked)}
|
onChange={handleChange('displayCta', (e) => e.target.checked)}
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
|
@ -246,7 +264,7 @@ const PlFeConfig: React.FC = () => {
|
||||||
>
|
>
|
||||||
<Toggle
|
<Toggle
|
||||||
checked={plFe.authenticatedProfile === true}
|
checked={plFe.authenticatedProfile === true}
|
||||||
onChange={handleChange(['authenticatedProfile'], (e) => e.target.checked)}
|
onChange={handleChange('authenticatedProfile', (e) => e.target.checked)}
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
|
@ -258,7 +276,7 @@ const PlFeConfig: React.FC = () => {
|
||||||
type='text'
|
type='text'
|
||||||
placeholder='/timeline/local'
|
placeholder='/timeline/local'
|
||||||
value={String(data.redirectRootNoLogin || '')}
|
value={String(data.redirectRootNoLogin || '')}
|
||||||
onChange={handleChange(['redirectRootNoLogin'], (e) => e.target.value)}
|
onChange={handleChange('redirectRootNoLogin', (e) => e.target.value)}
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
|
@ -270,7 +288,7 @@ const PlFeConfig: React.FC = () => {
|
||||||
type='text'
|
type='text'
|
||||||
placeholder='https://01234abcdef@glitch.tip.tld/5678'
|
placeholder='https://01234abcdef@glitch.tip.tld/5678'
|
||||||
value={String(data.sentryDsn || '')}
|
value={String(data.sentryDsn || '')}
|
||||||
onChange={handleChange(['sentryDsn'], (e) => e.target.value)}
|
onChange={handleChange('sentryDsn', (e) => e.target.value)}
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
</List>
|
</List>
|
||||||
|
@ -285,7 +303,7 @@ const PlFeConfig: React.FC = () => {
|
||||||
component={PromoPanelInput}
|
component={PromoPanelInput}
|
||||||
values={plFe.promoPanel.items}
|
values={plFe.promoPanel.items}
|
||||||
onChange={handleStreamItemChange(['promoPanel', 'items'])}
|
onChange={handleStreamItemChange(['promoPanel', 'items'])}
|
||||||
onAddItem={addStreamItem(['promoPanel', 'items'], templates.promoPanel)}
|
onAddItem={addStreamItem(['promoPanel', 'items'], promoPanelItemSchema)}
|
||||||
onRemoveItem={deleteStreamItem(['promoPanel', 'items'])}
|
onRemoveItem={deleteStreamItem(['promoPanel', 'items'])}
|
||||||
draggable
|
draggable
|
||||||
/>
|
/>
|
||||||
|
@ -296,7 +314,7 @@ const PlFeConfig: React.FC = () => {
|
||||||
component={FooterLinkInput}
|
component={FooterLinkInput}
|
||||||
values={plFe.navlinks.homeFooter || []}
|
values={plFe.navlinks.homeFooter || []}
|
||||||
onChange={handleStreamItemChange(['navlinks', 'homeFooter'])}
|
onChange={handleStreamItemChange(['navlinks', 'homeFooter'])}
|
||||||
onAddItem={addStreamItem(['navlinks', 'homeFooter'], templates.footerItem)}
|
onAddItem={addStreamItem(['navlinks', 'homeFooter'], footerItemSchema)}
|
||||||
onRemoveItem={deleteStreamItem(['navlinks', 'homeFooter'])}
|
onRemoveItem={deleteStreamItem(['navlinks', 'homeFooter'])}
|
||||||
draggable
|
draggable
|
||||||
/>
|
/>
|
||||||
|
@ -306,7 +324,7 @@ const PlFeConfig: React.FC = () => {
|
||||||
type='text'
|
type='text'
|
||||||
placeholder={intl.formatMessage(messages.copyrightFooterLabel)}
|
placeholder={intl.formatMessage(messages.copyrightFooterLabel)}
|
||||||
value={plFe.copyright}
|
value={plFe.copyright}
|
||||||
onChange={handleChange(['copyright'], (e) => e.target.value)}
|
onChange={handleChange('copyright', (e) => e.target.value)}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
|
@ -321,7 +339,7 @@ const PlFeConfig: React.FC = () => {
|
||||||
type='text'
|
type='text'
|
||||||
placeholder={intl.formatMessage(messages.tileServerLabel)}
|
placeholder={intl.formatMessage(messages.tileServerLabel)}
|
||||||
value={plFe.tileServer}
|
value={plFe.tileServer}
|
||||||
onChange={handleChange(['tileServer'], (e) => e.target.value)}
|
onChange={handleChange('tileServer', (e) => e.target.value)}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
|
@ -330,7 +348,7 @@ const PlFeConfig: React.FC = () => {
|
||||||
type='text'
|
type='text'
|
||||||
placeholder={intl.formatMessage(messages.tileServerAttributionLabel)}
|
placeholder={intl.formatMessage(messages.tileServerAttributionLabel)}
|
||||||
value={plFe.tileServerAttribution}
|
value={plFe.tileServerAttribution}
|
||||||
onChange={handleChange(['tileServerAttribution'], (e) => e.target.value)}
|
onChange={handleChange('tileServerAttribution', (e) => e.target.value)}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
</>
|
</>
|
||||||
|
@ -346,7 +364,7 @@ const PlFeConfig: React.FC = () => {
|
||||||
component={CryptoAddressInput}
|
component={CryptoAddressInput}
|
||||||
values={plFe.cryptoAddresses}
|
values={plFe.cryptoAddresses}
|
||||||
onChange={handleStreamItemChange(['cryptoAddresses'])}
|
onChange={handleStreamItemChange(['cryptoAddresses'])}
|
||||||
onAddItem={addStreamItem(['cryptoAddresses'], templates.cryptoAddress)}
|
onAddItem={addStreamItem(['cryptoAddresses'], cryptoAddressSchema)}
|
||||||
onRemoveItem={deleteStreamItem(['cryptoAddresses'])}
|
onRemoveItem={deleteStreamItem(['cryptoAddresses'])}
|
||||||
draggable
|
draggable
|
||||||
/>
|
/>
|
||||||
|
@ -358,7 +376,7 @@ const PlFeConfig: React.FC = () => {
|
||||||
pattern='[0-9]+'
|
pattern='[0-9]+'
|
||||||
placeholder={intl.formatMessage(messages.cryptoDonatePanelLimitLabel)}
|
placeholder={intl.formatMessage(messages.cryptoDonatePanelLimitLabel)}
|
||||||
value={plFe.cryptoDonatePanel.limit}
|
value={plFe.cryptoDonatePanel.limit}
|
||||||
onChange={handleChange(['cryptoDonatePanel', 'limit'], (e) => Number(e.target.value))}
|
onChange={handleChange('cryptoDonatePanel', (e) => ({ limit: Number(e.target.value) }))}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
|
@ -395,4 +413,4 @@ const PlFeConfig: React.FC = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export { PlFeConfig as default };
|
export { PlFeConfigEditor as default };
|
||||||
|
|
|
@ -61,6 +61,8 @@ const ThemeEditor: React.FC<IThemeEditor> = () => {
|
||||||
const fileInput = useRef<HTMLInputElement>(null);
|
const fileInput = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const updateColors = (key: string) => (newColors: ColorGroup) => {
|
const updateColors = (key: string) => (newColors: ColorGroup) => {
|
||||||
|
if (typeof colors[key] === 'string') return;
|
||||||
|
|
||||||
setColors({
|
setColors({
|
||||||
...colors,
|
...colors,
|
||||||
[key]: {
|
[key]: {
|
||||||
|
|
|
@ -51,7 +51,7 @@ type PromoPanel = v.InferOutput<typeof promoPanelSchema>;
|
||||||
const footerItemSchema = coerceObject({
|
const footerItemSchema = coerceObject({
|
||||||
title: v.fallback(v.string(), ''),
|
title: v.fallback(v.string(), ''),
|
||||||
url: v.fallback(v.string(), ''),
|
url: v.fallback(v.string(), ''),
|
||||||
titleLocales: v.fallback(v.record(v.string(), v.string()), {})
|
titleLocales: v.fallback(v.record(v.string(), v.string()), {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
type FooterItem = v.InferOutput<typeof footerItemSchema>;
|
type FooterItem = v.InferOutput<typeof footerItemSchema>;
|
||||||
|
@ -150,6 +150,9 @@ const plFeConfigSchema = v.pipe(coerceObject({
|
||||||
type PlFeConfig = v.InferOutput<typeof plFeConfigSchema>;
|
type PlFeConfig = v.InferOutput<typeof plFeConfigSchema>;
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
promoPanelItemSchema,
|
||||||
|
footerItemSchema,
|
||||||
|
cryptoAddressSchema,
|
||||||
plFeConfigSchema,
|
plFeConfigSchema,
|
||||||
type PromoPanelItem,
|
type PromoPanelItem,
|
||||||
type PromoPanel,
|
type PromoPanel,
|
||||||
|
|
|
@ -10,10 +10,11 @@ import {
|
||||||
} from '../actions/pl-fe';
|
} from '../actions/pl-fe';
|
||||||
|
|
||||||
import type { PleromaConfig } from 'pl-api';
|
import type { PleromaConfig } from 'pl-api';
|
||||||
|
import type { PlFeConfig } from 'pl-fe/normalizers/pl-fe/pl-fe-config';
|
||||||
|
|
||||||
const initialState: Record<string, any> = {};
|
const initialState: Partial<PlFeConfig> = {};
|
||||||
|
|
||||||
const fallbackState = {
|
const fallbackState: Partial<PlFeConfig> = {
|
||||||
brandColor: '#d80482',
|
brandColor: '#d80482',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -46,12 +47,12 @@ const persistPlFeConfig = (plFeConfig: Record<string, any>, host: string) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const importPlFeConfig = (plFeConfig: Record<string, any>, host: string) => {
|
const importPlFeConfig = (plFeConfig: PlFeConfig, host: string) => {
|
||||||
persistPlFeConfig(plFeConfig, host);
|
persistPlFeConfig(plFeConfig, host);
|
||||||
return plFeConfig;
|
return plFeConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
const plfe = (state = initialState, action: Record<string, any>): Record<string, any> => {
|
const plfe = (state = initialState, action: Record<string, any>): Partial<PlFeConfig> => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case PLEROMA_PRELOAD_IMPORT:
|
case PLEROMA_PRELOAD_IMPORT:
|
||||||
return preloadImport(state, action);
|
return preloadImport(state, action);
|
||||||
|
|
Loading…
Reference in a new issue