Basic custom profile fields
This commit is contained in:
parent
b66b68b37c
commit
cd1a404351
2 changed files with 100 additions and 73 deletions
57
app/soapbox/components/ui/streamfield/streamfield.tsx
Normal file
57
app/soapbox/components/ui/streamfield/streamfield.tsx
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import Button from '../button/button';
|
||||||
|
import Stack from '../stack/stack';
|
||||||
|
|
||||||
|
interface IStreamfield {
|
||||||
|
values: any[],
|
||||||
|
onAddItem?: () => void,
|
||||||
|
onChange: (values: any[]) => void,
|
||||||
|
component: React.ComponentType<{ onChange: (value: any) => void, value: any }>,
|
||||||
|
maxItems?: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
/** List of inputs that can be added or removed. */
|
||||||
|
const Streamfield: React.FC<IStreamfield> = ({
|
||||||
|
values,
|
||||||
|
onAddItem,
|
||||||
|
onChange,
|
||||||
|
component: Component,
|
||||||
|
maxItems = Infinity,
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
const handleChange = (i: number) => {
|
||||||
|
return (value: any) => {
|
||||||
|
const newData = [...values];
|
||||||
|
newData[i] = value;
|
||||||
|
onChange(newData);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack space={4}>
|
||||||
|
|
||||||
|
<Stack>
|
||||||
|
{values.map((value, i) => {
|
||||||
|
return (
|
||||||
|
<Component key={i} onChange={handleChange(i)} value={value} />
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
{onAddItem && (
|
||||||
|
<Button
|
||||||
|
icon={require('@tabler/icons/icons/plus.svg')}
|
||||||
|
onClick={onAddItem}
|
||||||
|
disabled={values.length >= maxItems}
|
||||||
|
theme='ghost'
|
||||||
|
block
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Streamfield;
|
|
@ -7,11 +7,12 @@ import snackbar from 'soapbox/actions/snackbar';
|
||||||
import {
|
import {
|
||||||
Checkbox,
|
Checkbox,
|
||||||
} from 'soapbox/features/forms';
|
} from 'soapbox/features/forms';
|
||||||
import { useAppDispatch, useOwnAccount, useFeatures } from 'soapbox/hooks';
|
import { useAppSelector, useAppDispatch, useOwnAccount, useFeatures } from 'soapbox/hooks';
|
||||||
import { normalizeAccount } from 'soapbox/normalizers';
|
import { normalizeAccount } from 'soapbox/normalizers';
|
||||||
import resizeImage from 'soapbox/utils/resize_image';
|
import resizeImage from 'soapbox/utils/resize_image';
|
||||||
|
|
||||||
import { Button, Column, Form, FormActions, FormGroup, Input, Textarea } from '../../components/ui';
|
import { Button, Column, Form, FormActions, FormGroup, Input, Textarea, HStack } from '../../components/ui';
|
||||||
|
import Streamfield from '../../components/ui/streamfield/streamfield';
|
||||||
|
|
||||||
import ProfilePreview from './components/profile_preview';
|
import ProfilePreview from './components/profile_preview';
|
||||||
|
|
||||||
|
@ -40,13 +41,6 @@ const messages = defineMessages({
|
||||||
cancel: { id: 'common.cancel', defaultMessage: 'Cancel' },
|
cancel: { id: 'common.cancel', defaultMessage: 'Cancel' },
|
||||||
});
|
});
|
||||||
|
|
||||||
// /** Forces fields to be maxFields size, filling empty values. */
|
|
||||||
// const normalizeFields = (fields, maxFields: number) => (
|
|
||||||
// ImmutableList(fields).setSize(Math.max(fields.size, maxFields)).map(field =>
|
|
||||||
// field ? field : ImmutableMap({ name: '', value: '' }),
|
|
||||||
// )
|
|
||||||
// );
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Profile metadata `name` and `value`.
|
* Profile metadata `name` and `value`.
|
||||||
* (By default, max 4 fields and 255 characters per property/value)
|
* (By default, max 4 fields and 255 characters per property/value)
|
||||||
|
@ -121,7 +115,7 @@ const accountToCredentials = (account: Account): AccountCredentials => {
|
||||||
display_name: account.display_name,
|
display_name: account.display_name,
|
||||||
note: account.source.get('note'),
|
note: account.source.get('note'),
|
||||||
locked: account.locked,
|
locked: account.locked,
|
||||||
fields_attributes: [...account.source.get<Iterable<AccountCredentialsField>>('fields', [])],
|
fields_attributes: [...account.source.get<Iterable<AccountCredentialsField>>('fields', []).toJS()],
|
||||||
stranger_notifications: account.getIn(['pleroma', 'notification_settings', 'block_from_strangers']) === true,
|
stranger_notifications: account.getIn(['pleroma', 'notification_settings', 'block_from_strangers']) === true,
|
||||||
accepts_email_list: account.getIn(['pleroma', 'accepts_email_list']) === true,
|
accepts_email_list: account.getIn(['pleroma', 'accepts_email_list']) === true,
|
||||||
hide_followers: hideNetwork,
|
hide_followers: hideNetwork,
|
||||||
|
@ -134,6 +128,27 @@ const accountToCredentials = (account: Account): AccountCredentials => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface IProfileField {
|
||||||
|
value: AccountCredentialsField,
|
||||||
|
onChange: (field: AccountCredentialsField) => void,
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProfileField: React.FC<IProfileField> = ({ value, onChange }) => {
|
||||||
|
|
||||||
|
const handleChange = (key: string): React.ChangeEventHandler<HTMLInputElement> => {
|
||||||
|
return e => {
|
||||||
|
onChange({ ...value, [key]: e.currentTarget.value });
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HStack space={2} grow>
|
||||||
|
<Input type='text' value={value.name} onChange={handleChange('name')} />
|
||||||
|
<Input type='text' value={value.value} onChange={handleChange('value')} />
|
||||||
|
</HStack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
/** Edit profile page. */
|
/** Edit profile page. */
|
||||||
const EditProfile: React.FC = () => {
|
const EditProfile: React.FC = () => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
@ -141,7 +156,7 @@ const EditProfile: React.FC = () => {
|
||||||
|
|
||||||
const account = useOwnAccount();
|
const account = useOwnAccount();
|
||||||
const features = useFeatures();
|
const features = useFeatures();
|
||||||
// const maxFields = useAppSelector(state => state.instance.pleroma.getIn(['metadata', 'fields_limits', 'max_fields'], 4) as number);
|
const maxFields = useAppSelector(state => state.instance.pleroma.getIn(['metadata', 'fields_limits', 'max_fields'], 4) as number);
|
||||||
|
|
||||||
const [isLoading, setLoading] = useState(false);
|
const [isLoading, setLoading] = useState(false);
|
||||||
const [data, setData] = useState<AccountCredentials>({});
|
const [data, setData] = useState<AccountCredentials>({});
|
||||||
|
@ -229,27 +244,15 @@ const EditProfile: React.FC = () => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// handleFieldChange = (i, key) => {
|
const handleFieldsChange = (fields: AccountCredentialsField[]) => {
|
||||||
// return (e) => {
|
updateData('fields_attributes', fields);
|
||||||
// this.setState({
|
};
|
||||||
// fields: this.state.fields.setIn([i, key], e.target.value),
|
|
||||||
// });
|
const handleAddField = () => {
|
||||||
// };
|
const oldFields = data.fields_attributes || [];
|
||||||
// };
|
const fields = [...oldFields, { name: '', value: '' }];
|
||||||
//
|
updateData('fields_attributes', fields);
|
||||||
// handleAddField = () => {
|
};
|
||||||
// this.setState({
|
|
||||||
// fields: this.state.fields.push(ImmutableMap({ name: '', value: '' })),
|
|
||||||
// });
|
|
||||||
// };
|
|
||||||
//
|
|
||||||
// handleDeleteField = i => {
|
|
||||||
// return () => {
|
|
||||||
// this.setState({
|
|
||||||
// fields: normalizeFields(this.state.fields.delete(i), Math.min(this.props.maxFields, 4)),
|
|
||||||
// });
|
|
||||||
// };
|
|
||||||
// };
|
|
||||||
|
|
||||||
/** Memoized avatar preview URL. */
|
/** Memoized avatar preview URL. */
|
||||||
const avatarUrl = useMemo(() => {
|
const avatarUrl = useMemo(() => {
|
||||||
|
@ -412,47 +415,14 @@ const EditProfile: React.FC = () => {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* </FieldsGroup> */}
|
<Streamfield
|
||||||
{/*<FieldsGroup>
|
values={data.fields_attributes || []}
|
||||||
<div className='fields-row__column fields-group'>
|
onChange={handleFieldsChange}
|
||||||
<div className='input with_block_label'>
|
onAddItem={handleAddField}
|
||||||
<label><FormattedMessage id='edit_profile.fields.meta_fields_label' defaultMessage='Profile metadata' /></label>
|
component={ProfileField}
|
||||||
<span className='hint'>
|
maxItems={maxFields}
|
||||||
<FormattedMessage id='edit_profile.hints.meta_fields' defaultMessage='You can have up to {count, plural, one {# item} other {# items}} displayed as a table on your profile' values={{ count: maxFields }} />
|
/>
|
||||||
</span>
|
|
||||||
{
|
|
||||||
this.state.fields.map((field, i) => (
|
|
||||||
<div className='row' key={i}>
|
|
||||||
<TextInput
|
|
||||||
placeholder={intl.formatMessage(messages.metaFieldLabel)}
|
|
||||||
value={field.get('name')}
|
|
||||||
onChange={this.handleFieldChange(i, 'name')}
|
|
||||||
/>
|
|
||||||
<TextInput
|
|
||||||
placeholder={intl.formatMessage(messages.metaFieldContent)}
|
|
||||||
value={field.get('value')}
|
|
||||||
onChange={this.handleFieldChange(i, 'value')}
|
|
||||||
/>
|
|
||||||
{
|
|
||||||
this.state.fields.size > 4 && <Icon className='delete-field' src={require('@tabler/icons/icons/circle-x.svg')} onClick={this.handleDeleteField(i)} />
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
{
|
|
||||||
this.state.fields.size < maxFields && (
|
|
||||||
<div className='actions add-row'>
|
|
||||||
<div name='button' type='button' role='presentation' className='btn button button-secondary' onClick={this.handleAddField}>
|
|
||||||
<Icon src={require('@tabler/icons/icons/circle-plus.svg')} />
|
|
||||||
<FormattedMessage id='edit_profile.meta_fields.add' defaultMessage='Add new item' />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</FieldsGroup>*/}
|
|
||||||
{/* </fieldset> */}
|
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<Button to='/settings' theme='ghost'>
|
<Button to='/settings' theme='ghost'>
|
||||||
{intl.formatMessage(messages.cancel)}
|
{intl.formatMessage(messages.cancel)}
|
||||||
|
|
Loading…
Reference in a new issue