reorganize styles and pages (wip); update readme
This commit is contained in:
@@ -1,5 +1,49 @@
|
||||
# Config
|
||||
# About the Config editing section
|
||||
|
||||
An adventure with React, React Hooks and Ant Design forms.
|
||||
|
||||
## General data flow in this React app
|
||||
|
||||
### First things to note
|
||||
- When the Admin app loads, the `ServerStatusContext` (in addition to checking server `/status` on a timer) makes a call to the `/serverconfig` API to get your config details. This data will be stored as **`serverConfig`** in app state, and _provided_ to the app via `useContext` hook.
|
||||
|
||||
- The `serverConfig` in state is be the central source of data that pre-populates the forms.
|
||||
|
||||
- The `ServerStatusContext` also provides a method for components to update the serverConfig state, called `setFieldInConfigState()`.
|
||||
|
||||
- After you have updated a config value in a form field, and successfully submitted it through its endpoint, you should call `setFieldInConfigState` to update the global state with the new value.
|
||||
|
||||
- Each top field of the serverConfig has its own API update endpoint.
|
||||
|
||||
### Form Flow
|
||||
Each form input (or group of inputs) you make, you should
|
||||
1. Get the field values that you want out of `serverConfig` from ServerStatusContext with `useContext`.
|
||||
2. Next we'll have to put these field values of interest into a `useState` in each grouping. This will help you edit the form.
|
||||
3. Because ths config data is populated asynchronously, Use a `useEffect` to check when that data has arrived before putting it into state.
|
||||
4. You will be using the state's value to populate the `defaultValue` and the `value` props of each Ant input component (`Input`, `Toggle`, `Switch`, `Select`, `Slider` are currently used).
|
||||
5. When an `onChange` event fires for each type of input component, you will update the local state of each page with the changed value.
|
||||
6. Depending on the form, an `onChange` of the input component, or a subsequent `onClick` of a submit button will take the value from local state and POST the field's API.
|
||||
7. `onSuccess` of the post, you should update the global app state with the new value.
|
||||
|
||||
There are also a variety of other local states to manage the display of error/success messaging.
|
||||
|
||||
## Using Ant's `<Form>` with `form-textfield`.
|
||||
You may see that a couple of pages (currently Public Details and Server Details page), is mainly a grouping of similar Text fields.
|
||||
|
||||
`const [form] = Form.useForm();`
|
||||
`form.setFieldsValue(initialValues);`
|
||||
|
||||
|
||||
A special `TextField` component was created to be used with form.
|
||||
|
||||
|
||||
## Potential Optimizations
|
||||
|
||||
Looking back at the pages with `<Form>` + `form-textfield`, t
|
||||
|
||||
This pattern might be overly engineered.
|
||||
|
||||
There are also a few patterns across all the form groups that repeat quite a bit. Perhaps these patterns could be consolidated into a custom hook that could handle all the steps.
|
||||
|
||||
TODO: explain how to use <Form> and how the custom `form-xxxx` components work together.
|
||||
|
||||
@@ -48,4 +92,12 @@ TODO:
|
||||
- page headers - diff color?
|
||||
- fix social handles icon in table
|
||||
- consolidate things into 1 page?
|
||||
- things could use smaller font?
|
||||
- things could use smaller font?
|
||||
- maybe convert common form pattern to custom hook?
|
||||
|
||||
|
||||
Possibly over engineered
|
||||
|
||||
https://uxcandy.co/demo/label_pro/preview/demo_2/pages/forms/form-elements.html
|
||||
|
||||
https://www.bootstrapdash.com/demo/corona/jquery/template/modern-vertical/pages/forms/basic_elements.html
|
||||
296
web/pages/components/config/edit-social-links.tsx
Normal file
296
web/pages/components/config/edit-social-links.tsx
Normal file
@@ -0,0 +1,296 @@
|
||||
import React, { useState, useContext, useEffect } from 'react';
|
||||
import { Typography, Table, Button, Modal, Input } from 'antd';
|
||||
import { ColumnsType } from 'antd/lib/table';
|
||||
import { DeleteOutlined } from '@ant-design/icons';
|
||||
import SocialDropdown from './social-icons-dropdown';
|
||||
import { fetchData, NEXT_PUBLIC_API_HOST, SOCIAL_PLATFORMS_LIST } from '../../../utils/apis';
|
||||
import { ServerStatusContext } from '../../../utils/server-status-context';
|
||||
import { API_SOCIAL_HANDLES, postConfigUpdateToAPI, RESET_TIMEOUT, SUCCESS_STATES, DEFAULT_SOCIAL_HANDLE, OTHER_SOCIAL_HANDLE_OPTION } from './constants';
|
||||
import { SocialHandle } from '../../../types/config-section';
|
||||
import {isValidUrl} from '../../../utils/urls';
|
||||
|
||||
import configStyles from '../../../styles/config-pages.module.scss';
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
export default function EditSocialLinks() {
|
||||
const [availableIconsList, setAvailableIconsList] = useState([]);
|
||||
const [currentSocialHandles, setCurrentSocialHandles] = useState([]);
|
||||
|
||||
const [displayModal, setDisplayModal] = useState(false);
|
||||
const [displayOther, setDisplayOther] = useState(false);
|
||||
const [modalProcessing, setModalProcessing] = useState(false);
|
||||
const [editId, setEditId] = useState(-1);
|
||||
|
||||
// current data inside modal
|
||||
const [modalDataState, setModalDataState] = useState(DEFAULT_SOCIAL_HANDLE);
|
||||
|
||||
const [submitStatus, setSubmitStatus] = useState(null);
|
||||
const [submitStatusMessage, setSubmitStatusMessage] = useState('');
|
||||
|
||||
const serverStatusData = useContext(ServerStatusContext);
|
||||
const { serverConfig, setFieldInConfigState } = serverStatusData || {};
|
||||
|
||||
const { instanceDetails } = serverConfig;
|
||||
const { socialHandles: initialSocialHandles } = instanceDetails;
|
||||
|
||||
let resetTimer = null;
|
||||
|
||||
const getAvailableIcons = async () => {
|
||||
try {
|
||||
const result = await fetchData(SOCIAL_PLATFORMS_LIST, { auth: false });
|
||||
const list = Object.keys(result).map(item => ({
|
||||
key: item,
|
||||
...result[item],
|
||||
}));
|
||||
setAvailableIconsList(list);
|
||||
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
// do nothing
|
||||
}
|
||||
};
|
||||
|
||||
const selectedOther = modalDataState.platform !== '' && !availableIconsList.find(item => item.key === modalDataState.platform);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
getAvailableIcons();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (instanceDetails.socialHandles) {
|
||||
setCurrentSocialHandles(initialSocialHandles);
|
||||
}
|
||||
}, [instanceDetails]);
|
||||
|
||||
|
||||
const resetStates = () => {
|
||||
setSubmitStatus(null);
|
||||
setSubmitStatusMessage('');
|
||||
resetTimer = null;
|
||||
clearTimeout(resetTimer);
|
||||
};
|
||||
const resetModal = () => {
|
||||
setDisplayModal(false);
|
||||
setEditId(-1);
|
||||
setDisplayOther(false);
|
||||
setModalProcessing(false);
|
||||
setModalDataState({...DEFAULT_SOCIAL_HANDLE});
|
||||
};
|
||||
|
||||
const handleModalCancel = () => {
|
||||
resetModal();
|
||||
};
|
||||
|
||||
const updateModalState = (fieldName: string, value: string) => {
|
||||
setModalDataState({
|
||||
...modalDataState,
|
||||
[fieldName]: value,
|
||||
});
|
||||
};
|
||||
const handleDropdownSelect = (value: string) => {
|
||||
if (value === OTHER_SOCIAL_HANDLE_OPTION) {
|
||||
setDisplayOther(true);
|
||||
updateModalState('platform', '');
|
||||
} else {
|
||||
setDisplayOther(false);
|
||||
updateModalState('platform', value);
|
||||
}
|
||||
};
|
||||
const handleOtherNameChange = event => {
|
||||
const { value } = event.target;
|
||||
updateModalState('platform', value);
|
||||
};
|
||||
const handleUrlChange = event => {
|
||||
const { value } = event.target;
|
||||
updateModalState('url', value);
|
||||
};
|
||||
|
||||
|
||||
// posts all the variants at once as an array obj
|
||||
const postUpdateToAPI = async (postValue: any) => {
|
||||
await postConfigUpdateToAPI({
|
||||
apiPath: API_SOCIAL_HANDLES,
|
||||
data: { value: postValue },
|
||||
onSuccess: () => {
|
||||
setFieldInConfigState({ fieldName: 'socialHandles', value: postValue, path: 'instanceDetails' });
|
||||
|
||||
// close modal
|
||||
setModalProcessing(false);
|
||||
handleModalCancel();
|
||||
|
||||
setSubmitStatus('success');
|
||||
setSubmitStatusMessage('Social Handles updated.');
|
||||
resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
|
||||
},
|
||||
onError: (message: string) => {
|
||||
setSubmitStatus('error');
|
||||
setSubmitStatusMessage(message);
|
||||
resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// on Ok, send all of dataState to api
|
||||
// show loading
|
||||
// close modal when api is done
|
||||
const handleModalOk = () => {
|
||||
setModalProcessing(true);
|
||||
const postData = currentSocialHandles.length ? [
|
||||
...currentSocialHandles,
|
||||
]: [];
|
||||
if (editId === -1) {
|
||||
postData.push(modalDataState);
|
||||
} else {
|
||||
postData.splice(editId, 1, modalDataState);
|
||||
}
|
||||
postUpdateToAPI(postData);
|
||||
};
|
||||
|
||||
const handleDeleteItem = index => {
|
||||
const postData = [
|
||||
...currentSocialHandles,
|
||||
];
|
||||
postData.splice(index, 1);
|
||||
postUpdateToAPI(postData);
|
||||
};
|
||||
|
||||
const socialHandlesColumns: ColumnsType<SocialHandle> = [
|
||||
{
|
||||
title: "#",
|
||||
dataIndex: "key",
|
||||
key: "key"
|
||||
},
|
||||
{
|
||||
title: "Platform",
|
||||
dataIndex: "platform",
|
||||
key: "platform",
|
||||
render: (platform: string) => {
|
||||
const platformInfo = availableIconsList.find(item => item.key === platform);
|
||||
if (!platformInfo) {
|
||||
return platform;
|
||||
}
|
||||
const { icon, platform: platformName } = platformInfo;
|
||||
return (
|
||||
<>
|
||||
<span className="option-icon">
|
||||
<img src={`${NEXT_PUBLIC_API_HOST}${icon}`} alt="" className="option-icon" />
|
||||
</span>
|
||||
<span className="option-label">{platformName}</span>
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
title: "Url Link",
|
||||
dataIndex: "url",
|
||||
key: "url",
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
dataIndex: '',
|
||||
key: 'edit',
|
||||
render: (data, record, index) => {
|
||||
return (
|
||||
<span className="actions">
|
||||
<Button type="primary" size="small" onClick={() => {
|
||||
setEditId(index);
|
||||
setModalDataState({...currentSocialHandles[index]});
|
||||
setDisplayModal(true);
|
||||
}}>
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
className="delete-button"
|
||||
icon={<DeleteOutlined />}
|
||||
size="small"
|
||||
onClick={() => handleDeleteItem(index)}
|
||||
/>
|
||||
</span>
|
||||
)},
|
||||
},
|
||||
];
|
||||
|
||||
const {
|
||||
icon: newStatusIcon = null,
|
||||
message: newStatusMessage = '',
|
||||
} = SUCCESS_STATES[submitStatus] || {};
|
||||
const statusMessage = (
|
||||
<div className={`status-message ${submitStatus || ''}`}>
|
||||
{newStatusIcon} {newStatusMessage} {submitStatusMessage}
|
||||
</div>
|
||||
);
|
||||
|
||||
const okButtonProps = {
|
||||
disabled: !isValidUrl(modalDataState.url)
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div className={configStyles.socialLinksEditor}>
|
||||
<Title level={2}>Social Links</Title>
|
||||
<p>Add all your social media handles and links to your other profiles here.</p>
|
||||
|
||||
{statusMessage}
|
||||
|
||||
<Table
|
||||
className="dataTable"
|
||||
pagination={false}
|
||||
size="small"
|
||||
rowKey={record => record.url}
|
||||
columns={socialHandlesColumns}
|
||||
dataSource={currentSocialHandles}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
title="Edit Social Handle"
|
||||
visible={displayModal}
|
||||
onOk={handleModalOk}
|
||||
onCancel={handleModalCancel}
|
||||
confirmLoading={modalProcessing}
|
||||
okButtonProps={okButtonProps}
|
||||
>
|
||||
<SocialDropdown
|
||||
iconList={availableIconsList}
|
||||
selectedOption={selectedOther ? OTHER_SOCIAL_HANDLE_OPTION : modalDataState.platform}
|
||||
onSelected={handleDropdownSelect}
|
||||
/>
|
||||
{
|
||||
displayOther
|
||||
? (
|
||||
<>
|
||||
<Input
|
||||
placeholder="Other"
|
||||
defaultValue={modalDataState.platform}
|
||||
onChange={handleOtherNameChange}
|
||||
/>
|
||||
<br/>
|
||||
</>
|
||||
) : null
|
||||
}
|
||||
<br/>
|
||||
|
||||
URL
|
||||
<Input
|
||||
placeholder="Url to page"
|
||||
defaultValue={modalDataState.url}
|
||||
value={modalDataState.url}
|
||||
onChange={handleUrlChange}
|
||||
/>
|
||||
|
||||
{statusMessage}
|
||||
</Modal>
|
||||
<br />
|
||||
<Button type="primary" onClick={() => {
|
||||
resetModal();
|
||||
setDisplayModal(true);
|
||||
}}>
|
||||
Add a new social link
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -155,10 +155,9 @@ export default function TextField(props: TextFieldProps) {
|
||||
|
||||
return (
|
||||
<div className={`textfield-container type-${type}`}>
|
||||
<div className="textfield-label">{label}</div>
|
||||
<div className="textfield">
|
||||
<InfoTip tip={tip} />
|
||||
<Form.Item
|
||||
label={label}
|
||||
name={fieldName}
|
||||
hasFeedback
|
||||
validateStatus={submitStatus}
|
||||
@@ -176,8 +175,8 @@ export default function TextField(props: TextFieldProps) {
|
||||
{...fieldProps}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
</div>
|
||||
<InfoTip tip={tip} />
|
||||
|
||||
{ hasChanged ? <Button type="primary" size="small" className="submit-button" onClick={handleSubmit}>Update</Button> : null }
|
||||
|
||||
|
||||
@@ -141,12 +141,14 @@ export default function MainLayout(props) {
|
||||
<Menu.Item key="config-public-details">
|
||||
<Link href="/config-public-details">Public Details</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="config-social-items">
|
||||
<Link href="/config-social-items">Social items</Link>
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Item key="config-page-content">
|
||||
<Link href="/config-page-content">Custom page content</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="config-social-links">
|
||||
<Link href="/config-social-links">Social links</Link>
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Item key="config-server-details">
|
||||
<Link href="/config-server-details">Server Details</Link>
|
||||
</Menu.Item>
|
||||
|
||||
Reference in New Issue
Block a user