owncast/web/pages/actions.tsx
gingervitis d0eb1446f3 fixes for various admin issues (#181)
* up max char count for variant name to fix https://github.com/owncast/owncast/issues/1037

* max widthing the line chart canvas size so it scales with the page.
fixes
- https://github.com/owncast/owncast/issues/842
- https://github.com/owncast/owncast/issues/1024

* A fix to make Storage Endpoint URL validation have better feedback.

- give the field a type of "url"
- give the field a pattern to check
- have native browser handle the validation
- if the field is invalid, use :invalid selector to turn the text red on blur.

fixes: https://github.com/owncast/owncast/issues/1000

* a fix for https://github.com/owncast/owncast/issues/874

* - fixes for https://github.com/owncast/owncast/issues/972
Add optional prop to text field to trim() whitespaces from field. Apply logic to mostly url fields.

- move textfield blur if invalid turn red to globaal

* - a fix for bug:  https://github.com/owncast/owncast/issues/998
don't return null if platform name not found because its custom.

- clean up react key problem on socialhandles table

* fix react key issue on Actions table

* fix for https://github.com/owncast/owncast/issues/1008 to display 'other' field when editing an item not in predefined social list

* clean up other potential react key warnings

* Prettified Code!

Co-authored-by: gingervitis <gingervitis@users.noreply.github.com>
2021-05-22 23:27:51 -07:00

323 lines
8.5 KiB
TypeScript

// comment
import React, { useState, useEffect, useContext } from 'react';
import { Table, Space, Button, Modal, Checkbox, Input, Typography } from 'antd';
import { ServerStatusContext } from '../utils/server-status-context';
import { DeleteOutlined } from '@ant-design/icons';
import isValidUrl, { DEFAULT_TEXTFIELD_URL_PATTERN } from '../utils/urls';
import FormStatusIndicator from '../components/config/form-status-indicator';
import {
createInputStatus,
StatusState,
STATUS_ERROR,
STATUS_PROCESSING,
STATUS_SUCCESS,
} from '../utils/input-statuses';
import {
postConfigUpdateToAPI,
API_EXTERNAL_ACTIONS,
RESET_TIMEOUT,
} from '../utils/config-constants';
const { Title, Paragraph } = Typography;
let resetTimer = null;
interface Props {
onCancel: () => void;
onOk: any; // todo: make better type
visible: boolean;
}
function NewActionModal(props: Props) {
const { onOk, onCancel, visible } = props;
const [actionUrl, setActionUrl] = useState('');
const [actionTitle, setActionTitle] = useState('');
const [actionDescription, setActionDescription] = useState('');
const [actionIcon, setActionIcon] = useState('');
const [actionColor, setActionColor] = useState('');
const [openExternally, setOpenExternally] = useState(false);
function save() {
onOk(actionUrl, actionTitle, actionDescription, actionIcon, actionColor, openExternally);
setActionUrl('');
setActionTitle('');
setActionDescription('');
setActionIcon('');
setActionColor('');
setOpenExternally(false);
}
function canSave(): Boolean {
try {
const validationObject = new URL(actionUrl);
if (validationObject.protocol !== 'https:') {
return false;
}
} catch {
return false;
}
return isValidUrl(actionUrl) && actionTitle !== '';
}
const okButtonProps = {
disabled: !canSave(),
};
const onOpenExternallyChanged = checkbox => {
setOpenExternally(checkbox.target.checked);
};
return (
<Modal
title="Create New Action"
visible={visible}
onOk={save}
onCancel={onCancel}
okButtonProps={okButtonProps}
>
<div>
Add the URL for the external action you want to present.{' '}
<strong>Only HTTPS urls are supported.</strong>
<p>
<a href="https://owncast.online/docs" target="_blank" rel="noopener noreferrer">
Read more about external actions.
</a>
</p>
<p>
<Input
value={actionUrl}
required
placeholder="https://myserver.com/action (required)"
onChange={input => setActionUrl(input.currentTarget.value.trim())}
type="url"
pattern={DEFAULT_TEXTFIELD_URL_PATTERN}
/>
</p>
<p>
<Input
value={actionTitle}
required
placeholder="Your action title (required)"
onChange={input => setActionTitle(input.currentTarget.value)}
/>
</p>
<p>
<Input
value={actionDescription}
placeholder="Optional description"
onChange={input => setActionDescription(input.currentTarget.value)}
/>
</p>
<p>
<Input
value={actionIcon}
placeholder="https://myserver.com/action/icon.png (optional)"
onChange={input => setActionIcon(input.currentTarget.value)}
/>
</p>
<p>
<Input
type="color"
value={actionColor}
onChange={input => setActionColor(input.currentTarget.value)}
/>
Optional background color of the action button.
</p>
<Checkbox
checked={openExternally}
defaultChecked={openExternally}
onChange={onOpenExternallyChanged}
>
Open in a new tab instead of within your page.
</Checkbox>
</div>
</Modal>
);
}
export default function Actions() {
const serverStatusData = useContext(ServerStatusContext);
const { serverConfig, setFieldInConfigState } = serverStatusData || {};
const { externalActions } = serverConfig;
const [actions, setActions] = useState([]);
const [isModalVisible, setIsModalVisible] = useState(false);
const [submitStatus, setSubmitStatus] = useState(null);
const resetStates = () => {
setSubmitStatus(null);
resetTimer = null;
clearTimeout(resetTimer);
};
useEffect(() => {
setActions(externalActions || []);
}, [externalActions]);
const columns = [
{
title: '',
key: 'delete',
render: (text, record) => (
<Space size="middle">
<Button onClick={() => handleDelete(record)} icon={<DeleteOutlined />} />
</Space>
),
},
{
title: 'Name',
dataIndex: 'title',
key: 'title',
},
{
title: 'Description',
dataIndex: 'description',
key: 'description',
},
{
title: 'URL',
dataIndex: 'url',
key: 'url',
},
{
title: 'Icon',
dataIndex: 'icon',
key: 'icon',
render: (url: string) => {
return url ? <img style={{ width: '2vw' }} src={url} /> : null;
},
},
{
title: 'Color',
dataIndex: 'color',
key: 'color',
render: (color: string) => {
return color ? <div style={{ backgroundColor: color, height: '30px' }}>{color}</div> : null;
},
},
{
title: 'Opens',
dataIndex: 'openExternally',
key: 'openExternally',
render: (openExternally: boolean) => {
return openExternally ? 'In a new tab' : 'In a modal';
},
},
];
async function handleDelete(action) {
let actionsData = [...actions];
const index = actions.findIndex(item => item.url === action.url);
actionsData.splice(index, 1);
setActions(actionsData);
save(actionsData);
try {
} catch (error) {
console.error(error);
}
}
async function handleSave(
url: string,
title: string,
description: string,
icon: string,
color: string,
openExternally: boolean,
) {
try {
let actionsData = [...actions];
const updatedActions = actionsData.concat({
url,
title,
description,
icon,
color,
openExternally,
});
setActions(updatedActions);
await save(updatedActions);
} catch (error) {
console.error(error);
}
}
async function save(actionsData) {
await postConfigUpdateToAPI({
apiPath: API_EXTERNAL_ACTIONS,
data: { value: actionsData },
onSuccess: () => {
setFieldInConfigState({ fieldName: 'externalActions', value: actionsData, path: '' });
setSubmitStatus(createInputStatus(STATUS_SUCCESS, 'Updated.'));
resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
},
onError: (message: string) => {
console.log(message);
setSubmitStatus(createInputStatus(STATUS_ERROR, message));
resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
},
});
}
const showCreateModal = () => {
setIsModalVisible(true);
};
const handleModalSaveButton = (
actionUrl: string,
actionTitle: string,
actionDescription: string,
actionIcon: string,
actionColor: string,
openExternally: boolean,
) => {
setIsModalVisible(false);
handleSave(actionUrl, actionTitle, actionDescription, actionIcon, actionColor, openExternally);
};
const handleModalCancelButton = () => {
setIsModalVisible(false);
};
return (
<div>
<Title>External Actions</Title>
<Paragraph>
External action URLs are 3rd party UI you can display, embedded, into your Owncast page when
a user clicks on a button to launch your action.
</Paragraph>
<Paragraph>
Read more about how to use actions, with examples, at{' '}
<a
href="https://owncast.online/thirdparty/?source=admin"
target="_blank"
rel="noopener noreferrer"
>
our documentation
</a>
.
</Paragraph>
<Table
rowKey={record => `${record.title}-${record.url}`}
columns={columns}
dataSource={actions}
pagination={false}
/>
<br />
<Button type="primary" onClick={showCreateModal}>
Create New Action
</Button>
<FormStatusIndicator status={submitStatus} />
<NewActionModal
visible={isModalVisible}
onOk={handleModalSaveButton}
onCancel={handleModalCancelButton}
/>
</div>
);
}