mirror of
https://github.com/zoriya/flood.git
synced 2026-06-08 12:42:41 +00:00
FeedsTab: migrate to Functional Component
This commit is contained in:
@@ -12,27 +12,17 @@ const FeedActions = {
|
||||
axios
|
||||
.put(`${baseURI}api/feed-monitor/feeds`, options)
|
||||
.then((json) => json.data)
|
||||
.then(
|
||||
() => {
|
||||
FeedActions.fetchFeedMonitors();
|
||||
},
|
||||
() => {
|
||||
// do nothing.
|
||||
},
|
||||
),
|
||||
.then(() => {
|
||||
FeedActions.fetchFeedMonitors();
|
||||
}),
|
||||
|
||||
modifyFeed: (id: string, options: ModifyFeedOptions) =>
|
||||
axios
|
||||
.patch(`${baseURI}api/feed-monitor/feeds/${id}`, options)
|
||||
.then((json) => json.data)
|
||||
.then(
|
||||
() => {
|
||||
FeedActions.fetchFeedMonitors();
|
||||
},
|
||||
() => {
|
||||
// do nothing.
|
||||
},
|
||||
),
|
||||
.then(() => {
|
||||
FeedActions.fetchFeedMonitors();
|
||||
}),
|
||||
|
||||
addRule: (options: AddRuleOptions) =>
|
||||
axios
|
||||
|
||||
@@ -7,8 +7,8 @@ import type {Rule} from '@shared/types/Feed';
|
||||
import {Button, Form, FormError, FormRow, FormRowItem} from '../../../ui';
|
||||
import DownloadRuleForm from './DownloadRuleForm';
|
||||
import FeedActions from '../../../actions/FeedActions';
|
||||
import {isNotEmpty, isRegExValid} from '../../../util/validators';
|
||||
import ModalFormSectionHeader from '../ModalFormSectionHeader';
|
||||
import * as validators from '../../../util/validators';
|
||||
import DownloadRuleList from './DownloadRuleList';
|
||||
|
||||
const initialRule: AddRuleOptions = {
|
||||
@@ -23,25 +23,25 @@ const initialRule: AddRuleOptions = {
|
||||
|
||||
const validatedFields = {
|
||||
destination: {
|
||||
isValid: validators.isNotEmpty,
|
||||
isValid: isNotEmpty,
|
||||
error: 'feeds.validation.must.specify.destination',
|
||||
},
|
||||
feedID: {
|
||||
isValid: (value: string | undefined) => validators.isNotEmpty(value) && value !== 'placeholder',
|
||||
isValid: (value: string | undefined) => isNotEmpty(value) && value !== 'placeholder',
|
||||
error: 'feeds.validation.must.select.feed',
|
||||
},
|
||||
label: {
|
||||
isValid: validators.isNotEmpty,
|
||||
isValid: isNotEmpty,
|
||||
error: 'feeds.validation.must.specify.label',
|
||||
},
|
||||
match: {
|
||||
isValid: (value: string | undefined) => validators.isNotEmpty(value) && validators.isRegExValid(value),
|
||||
isValid: (value: string | undefined) => isNotEmpty(value) && isRegExValid(value),
|
||||
error: 'feeds.validation.invalid.regular.expression',
|
||||
},
|
||||
exclude: {
|
||||
isValid: (value: string | undefined) => {
|
||||
if (validators.isNotEmpty(value)) {
|
||||
return validators.isRegExValid(value);
|
||||
if (isNotEmpty(value)) {
|
||||
return isRegExValid(value);
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -55,10 +55,17 @@ type ValidatedField = keyof typeof validatedFields;
|
||||
const validateField = (validatedField: ValidatedField, value: string | undefined): string | undefined =>
|
||||
validatedFields[validatedField]?.isValid(value) ? undefined : validatedFields[validatedField]?.error;
|
||||
|
||||
interface RuleFormData extends Omit<Rule, 'tags' | 'feedIDs'> {
|
||||
interface RuleFormData {
|
||||
check: string;
|
||||
exclude: string;
|
||||
destination: string;
|
||||
field: string;
|
||||
feedID: string;
|
||||
label: string;
|
||||
match: string;
|
||||
tags: string;
|
||||
isBasePath: boolean;
|
||||
startOnLoad: boolean;
|
||||
}
|
||||
|
||||
const DownloadRulesTab: FC = () => {
|
||||
@@ -88,7 +95,7 @@ const DownloadRulesTab: FC = () => {
|
||||
(() => {
|
||||
const {check, match = '', exclude = ''} = ruleFormData;
|
||||
|
||||
if (validators.isNotEmpty(check) && validators.isRegExValid(match) && validators.isRegExValid(exclude)) {
|
||||
if (isNotEmpty(check) && isRegExValid(match) && isRegExValid(exclude)) {
|
||||
const isMatched = new RegExp(match, 'gi').test(check);
|
||||
const isExcluded = exclude !== '' && new RegExp(exclude, 'gi').test(check);
|
||||
return isMatched && !isExcluded;
|
||||
@@ -107,21 +114,21 @@ const DownloadRulesTab: FC = () => {
|
||||
|
||||
const formData = formRef.current.getFormData() as Partial<RuleFormData>;
|
||||
|
||||
setIsSubmitting(true);
|
||||
setErrors(
|
||||
Object.keys(validatedFields).reduce((memo, key) => {
|
||||
const validatedField = key as ValidatedField;
|
||||
const currentErrors = Object.keys(validatedFields).reduce((memo, key) => {
|
||||
const validatedField = key as ValidatedField;
|
||||
|
||||
return {
|
||||
...memo,
|
||||
[validatedField]: validateField(validatedField, formData[validatedField]),
|
||||
};
|
||||
}, {} as Record<string, string | undefined>),
|
||||
);
|
||||
return {
|
||||
...memo,
|
||||
[validatedField]: validateField(validatedField, formData[validatedField]),
|
||||
};
|
||||
}, {} as Record<string, string | undefined>);
|
||||
setErrors(currentErrors);
|
||||
|
||||
const isFormValid = Object.keys(errors).every((key) => errors[key] === undefined);
|
||||
const isFormValid = Object.keys(currentErrors).every((key) => currentErrors[key] === undefined);
|
||||
|
||||
if (isFormChanged && isFormValid) {
|
||||
setIsSubmitting(true);
|
||||
|
||||
if (currentRule?._id != null) {
|
||||
await FeedActions.removeFeedMonitor(currentRule._id);
|
||||
}
|
||||
@@ -139,17 +146,17 @@ const DownloadRulesTab: FC = () => {
|
||||
}).then(
|
||||
() => {
|
||||
formRef.current?.resetForm();
|
||||
setCurrentRule(null);
|
||||
setErrors({});
|
||||
setCurrentRule(null);
|
||||
setIsEditing(false);
|
||||
},
|
||||
(err: Error) => {
|
||||
setErrors({backend: err.message});
|
||||
() => {
|
||||
setErrors({backend: 'general.error.unknown'});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
setIsSubmitting(false);
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
}}
|
||||
ref={formRef}>
|
||||
<ModalFormSectionHeader>
|
||||
@@ -158,7 +165,7 @@ const DownloadRulesTab: FC = () => {
|
||||
{Object.keys(errors).reduce((memo: ReactNodeArray, key) => {
|
||||
if (errors[key as ValidatedField] != null) {
|
||||
memo.push(
|
||||
<FormRow key={key}>
|
||||
<FormRow key={`error-${key}`}>
|
||||
<FormError>{intl.formatMessage({id: errors?.[key as ValidatedField]})}</FormError>
|
||||
</FormRow>,
|
||||
);
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import {FC} from 'react';
|
||||
import {FormattedMessage, useIntl} from 'react-intl';
|
||||
|
||||
import type {Feed} from '@shared/types/Feed';
|
||||
|
||||
import {Button, FormRow, FormRowGroup, Select, SelectItem, Textbox} from '../../../ui';
|
||||
|
||||
interface FeedFormProps {
|
||||
currentFeed: Feed | null;
|
||||
defaultFeed: Pick<Feed, 'interval' | 'label' | 'url'>;
|
||||
intervalMultipliers: Readonly<Array<{message: string; value: number}>>;
|
||||
isSubmitting: boolean;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
const FeedForm: FC<FeedFormProps> = ({
|
||||
currentFeed,
|
||||
defaultFeed,
|
||||
intervalMultipliers,
|
||||
isSubmitting,
|
||||
onCancel,
|
||||
}: FeedFormProps) => {
|
||||
const intl = useIntl();
|
||||
const feedInterval = currentFeed?.interval ?? defaultFeed.interval;
|
||||
|
||||
let defaultIntervalTextValue = feedInterval;
|
||||
let defaultIntervalMultiplier = 1;
|
||||
|
||||
intervalMultipliers.forEach((interval) => {
|
||||
const intervalMultiplier = interval.value;
|
||||
|
||||
if (feedInterval % intervalMultiplier === 0) {
|
||||
defaultIntervalTextValue = feedInterval / intervalMultiplier;
|
||||
defaultIntervalMultiplier = intervalMultiplier;
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<FormRowGroup>
|
||||
<FormRow>
|
||||
<Textbox
|
||||
id="label"
|
||||
label={intl.formatMessage({
|
||||
id: 'feeds.label',
|
||||
})}
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'feeds.label',
|
||||
})}
|
||||
defaultValue={currentFeed?.label ?? defaultFeed.label}
|
||||
/>
|
||||
<Textbox
|
||||
id="interval"
|
||||
label={intl.formatMessage({
|
||||
id: 'feeds.select.interval',
|
||||
})}
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'feeds.interval',
|
||||
})}
|
||||
defaultValue={defaultIntervalTextValue}
|
||||
width="one-eighth"
|
||||
/>
|
||||
<Select labelOffset defaultID={defaultIntervalMultiplier} id="intervalMultiplier" width="one-eighth">
|
||||
{intervalMultipliers.map((interval) => (
|
||||
<SelectItem key={interval.value} id={interval.value}>
|
||||
{intl.formatMessage({id: interval.message})}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<Textbox
|
||||
id="url"
|
||||
label={intl.formatMessage({
|
||||
id: 'feeds.url',
|
||||
})}
|
||||
placeholder={intl.formatMessage({id: 'feeds.url'})}
|
||||
defaultValue={currentFeed?.url ?? defaultFeed?.url}
|
||||
/>
|
||||
<Button labelOffset onClick={onCancel}>
|
||||
<FormattedMessage id="button.cancel" />
|
||||
</Button>
|
||||
<Button labelOffset type="submit" isLoading={isSubmitting}>
|
||||
<FormattedMessage id="button.save.feed" />
|
||||
</Button>
|
||||
</FormRow>
|
||||
</FormRowGroup>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeedForm;
|
||||
@@ -0,0 +1,56 @@
|
||||
import {FC, ReactNodeArray} from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {observer} from 'mobx-react';
|
||||
|
||||
import {Checkbox, FormRow} from '../../../ui';
|
||||
|
||||
import FeedStore from '../../../stores/FeedStore';
|
||||
|
||||
interface FeedItemsProps {
|
||||
selectedFeedID: string;
|
||||
}
|
||||
|
||||
const FeedItems: FC<FeedItemsProps> = observer(({selectedFeedID}: FeedItemsProps) => {
|
||||
const {items} = FeedStore;
|
||||
|
||||
const itemElements: ReactNodeArray = [];
|
||||
if (selectedFeedID) {
|
||||
const titleOccurrences: Record<string, number> = {};
|
||||
items.forEach((item, index) => {
|
||||
let {title} = item;
|
||||
const occurrence = titleOccurrences[title];
|
||||
|
||||
if (occurrence == null) {
|
||||
titleOccurrences[title] = 2;
|
||||
} else {
|
||||
title = `${title} #${occurrence}`;
|
||||
titleOccurrences[title] += 1;
|
||||
}
|
||||
|
||||
itemElements.push(
|
||||
<li className="interactive-list__item interactive-list__item--stacked-content feed-list__feed" key={title}>
|
||||
<div className="interactive-list__label feed-list__feed-label">{title}</div>
|
||||
<Checkbox id={`${index}`} />
|
||||
</li>,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<FormRow>
|
||||
{itemElements.length === 0 ? (
|
||||
<ul className="interactive-list">
|
||||
<li className="interactive-list__item">
|
||||
<div className="interactive-list__label">
|
||||
<FormattedMessage id="feeds.no.items.matching" />
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
) : (
|
||||
<ul className="interactive-list feed-list">{itemElements}</ul>
|
||||
)}
|
||||
</FormRow>
|
||||
);
|
||||
});
|
||||
|
||||
export default FeedItems;
|
||||
@@ -0,0 +1,120 @@
|
||||
import {FC, useRef, useState} from 'react';
|
||||
import {FormattedMessage, useIntl} from 'react-intl';
|
||||
import {observer} from 'mobx-react';
|
||||
|
||||
import {Button, Form, FormRow, Select, SelectItem, Textbox} from '../../../ui';
|
||||
|
||||
import FeedActions from '../../../actions/FeedActions';
|
||||
import FeedItems from './FeedItems';
|
||||
import FeedStore from '../../../stores/FeedStore';
|
||||
import ModalFormSectionHeader from '../ModalFormSectionHeader';
|
||||
import UIActions from '../../../actions/UIActions';
|
||||
|
||||
const FeedItemsForm: FC = observer(() => {
|
||||
const intl = useIntl();
|
||||
const manualAddingFormRef = useRef<Form>(null);
|
||||
const [selectedFeedID, setSelectedFeedID] = useState<string | null>(null);
|
||||
|
||||
const {feeds} = FeedStore;
|
||||
|
||||
if (selectedFeedID != null) {
|
||||
if (!feeds.some((feed) => feed._id === selectedFeedID)) {
|
||||
setSelectedFeedID(null);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Form
|
||||
className="inverse"
|
||||
onChange={({event, formData}) => {
|
||||
const feedBrowseForm = formData as {feedID: string; search: string};
|
||||
if ((event.target as HTMLInputElement).type !== 'checkbox') {
|
||||
setSelectedFeedID(feedBrowseForm.feedID);
|
||||
FeedActions.fetchItems({
|
||||
id: feedBrowseForm.feedID,
|
||||
search: feedBrowseForm.search,
|
||||
});
|
||||
}
|
||||
}}
|
||||
onSubmit={() => {
|
||||
if (manualAddingFormRef.current == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = manualAddingFormRef.current.getFormData();
|
||||
|
||||
// TODO: Properly handle array of array of URLs
|
||||
const torrentsToDownload = FeedStore.items
|
||||
.filter((_item, index) => formData[index])
|
||||
.map((item, index) => ({id: index, value: item.urls[0]}));
|
||||
|
||||
UIActions.displayModal({
|
||||
id: 'add-torrents',
|
||||
initialURLs: torrentsToDownload,
|
||||
});
|
||||
}}
|
||||
ref={manualAddingFormRef}>
|
||||
<ModalFormSectionHeader>
|
||||
<FormattedMessage id="feeds.browse.feeds" />
|
||||
</ModalFormSectionHeader>
|
||||
<FormRow>
|
||||
<Select
|
||||
disabled={!feeds.length}
|
||||
grow={false}
|
||||
id="feedID"
|
||||
label={intl.formatMessage({
|
||||
id: 'feeds.select.feed',
|
||||
})}
|
||||
width="three-eighths">
|
||||
{!feeds.length
|
||||
? [
|
||||
<SelectItem key="empty" id="placeholder" placeholder>
|
||||
<em>
|
||||
<FormattedMessage id="feeds.no.feeds.available" />
|
||||
</em>
|
||||
</SelectItem>,
|
||||
]
|
||||
: feeds.reduce(
|
||||
(feedOptions, feed) => {
|
||||
if (feed._id == null) {
|
||||
return feedOptions;
|
||||
}
|
||||
|
||||
return feedOptions.concat(
|
||||
<SelectItem key={feed._id} id={feed._id}>
|
||||
{feed.label}
|
||||
</SelectItem>,
|
||||
);
|
||||
},
|
||||
[
|
||||
<SelectItem key="select-feed" id="placeholder" placeholder>
|
||||
<em>
|
||||
<FormattedMessage id="feeds.select.feed" />
|
||||
</em>
|
||||
</SelectItem>,
|
||||
],
|
||||
)}
|
||||
</Select>
|
||||
{selectedFeedID && (
|
||||
<Textbox
|
||||
id="search"
|
||||
label={intl.formatMessage({
|
||||
id: 'feeds.search.term',
|
||||
})}
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'feeds.search',
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
{selectedFeedID && (
|
||||
<Button key="button" type="submit" labelOffset>
|
||||
<FormattedMessage id="button.download" />
|
||||
</Button>
|
||||
)}
|
||||
</FormRow>
|
||||
{selectedFeedID && <FeedItems selectedFeedID={selectedFeedID} />}
|
||||
</Form>
|
||||
);
|
||||
});
|
||||
|
||||
export default FeedItemsForm;
|
||||
@@ -0,0 +1,111 @@
|
||||
import {FC} from 'react';
|
||||
import {observer} from 'mobx-react';
|
||||
import {FormattedMessage, useIntl} from 'react-intl';
|
||||
|
||||
import type {Feed} from '@shared/types/Feed';
|
||||
|
||||
import Close from '../../icons/Close';
|
||||
import Edit from '../../icons/Edit';
|
||||
import FeedStore from '../../../stores/FeedStore';
|
||||
|
||||
interface FeedListProps {
|
||||
currentFeed: Feed | null;
|
||||
intervalMultipliers: Readonly<Array<{message: string; value: number}>>;
|
||||
onSelect: (feed: Feed) => void;
|
||||
onRemove: (feed: Feed) => void;
|
||||
}
|
||||
|
||||
const FeedList: FC<FeedListProps> = observer(
|
||||
({currentFeed, intervalMultipliers, onSelect, onRemove}: FeedListProps) => {
|
||||
const {feeds} = FeedStore;
|
||||
const intl = useIntl();
|
||||
|
||||
if (feeds.length === 0) {
|
||||
return (
|
||||
<ul className="interactive-list">
|
||||
<li className="interactive-list__item">
|
||||
<FormattedMessage id="feeds.no.feeds.defined" />
|
||||
</li>
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ul className="interactive-list feed-list">
|
||||
{feeds.map((feed) => {
|
||||
const matchedCount = feed.count || 0;
|
||||
|
||||
let intervalText = `${feed.interval}`;
|
||||
let intervalMultiplierMessage = intervalMultipliers[0].message;
|
||||
|
||||
intervalMultipliers.forEach((interval) => {
|
||||
if (feed.interval % interval.value === 0) {
|
||||
intervalText = `${feed.interval / interval.value}`;
|
||||
intervalMultiplierMessage = interval.message;
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<li
|
||||
className="interactive-list__item interactive-list__item--stacked-content feed-list__feed"
|
||||
key={feed._id}>
|
||||
<div className="interactive-list__label">
|
||||
<ul className="interactive-list__detail-list">
|
||||
<li
|
||||
className="interactive-list__detail-list__item
|
||||
interactive-list__detail--primary">
|
||||
{feed.label}
|
||||
</li>
|
||||
<li
|
||||
className="interactive-list__detail-list__item
|
||||
interactive-list__detail-list__item--overflow
|
||||
interactive-list__detail interactive-list__detail--secondary">
|
||||
<FormattedMessage id="feeds.match.count" values={{count: matchedCount}} />
|
||||
</li>
|
||||
{feed === currentFeed && (
|
||||
<li
|
||||
className="interactive-list__detail-list__item
|
||||
interactive-list__detail--primary">
|
||||
Modifying
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
<ul className="interactive-list__detail-list">
|
||||
<li
|
||||
className="interactive-list__detail-list__item
|
||||
interactive-list__detail interactive-list__detail--tertiary">
|
||||
{`${intervalText} ${intl.formatMessage({id: intervalMultiplierMessage})}`}
|
||||
</li>
|
||||
<li
|
||||
className="interactive-list__detail-list__item
|
||||
interactive-list__detail-list__item--overflow
|
||||
interactive-list__detail interactive-list__detail--tertiary">
|
||||
<a href={feed.url} rel="noopener noreferrer" target="_blank">
|
||||
{feed.url}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<span
|
||||
className="interactive-list__icon interactive-list__icon--action"
|
||||
onClick={() => {
|
||||
onSelect(feed);
|
||||
}}>
|
||||
<Edit />
|
||||
</span>
|
||||
<span
|
||||
className="interactive-list__icon interactive-list__icon--action interactive-list__icon--action--warning"
|
||||
onClick={() => {
|
||||
onRemove(feed);
|
||||
}}>
|
||||
<Close />
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default FeedList;
|
||||
@@ -1,50 +1,43 @@
|
||||
import {Component} from 'react';
|
||||
import {injectIntl, WrappedComponentProps} from 'react-intl';
|
||||
import {FC, useEffect} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
|
||||
import DownloadRulesTab from './DownloadRulesTab';
|
||||
import FeedActions from '../../../actions/FeedActions';
|
||||
import FeedStore from '../../../stores/FeedStore';
|
||||
import FeedsTab from './FeedsTab';
|
||||
import Modal from '../Modal';
|
||||
|
||||
class FeedsModal extends Component<WrappedComponentProps> {
|
||||
componentDidMount() {
|
||||
const FeedsModal: FC = () => {
|
||||
const intl = useIntl();
|
||||
|
||||
useEffect(() => {
|
||||
FeedActions.fetchFeedMonitors();
|
||||
}
|
||||
}, []);
|
||||
|
||||
render() {
|
||||
const tabs = {
|
||||
feeds: {
|
||||
content: FeedsTab,
|
||||
props: {
|
||||
feedStore: FeedStore,
|
||||
},
|
||||
label: this.props.intl.formatMessage({
|
||||
id: 'feeds.tabs.feeds',
|
||||
}),
|
||||
},
|
||||
downloadRules: {
|
||||
content: DownloadRulesTab,
|
||||
props: {
|
||||
feedStore: FeedStore,
|
||||
},
|
||||
label: this.props.intl.formatMessage({
|
||||
id: 'feeds.tabs.download.rules',
|
||||
}),
|
||||
},
|
||||
};
|
||||
const tabs = {
|
||||
feeds: {
|
||||
content: FeedsTab,
|
||||
label: intl.formatMessage({
|
||||
id: 'feeds.tabs.feeds',
|
||||
}),
|
||||
},
|
||||
downloadRules: {
|
||||
content: DownloadRulesTab,
|
||||
label: intl.formatMessage({
|
||||
id: 'feeds.tabs.download.rules',
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
heading={this.props.intl.formatMessage({
|
||||
id: 'feeds.tabs.heading',
|
||||
})}
|
||||
orientation="horizontal"
|
||||
size="large"
|
||||
tabs={tabs}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Modal
|
||||
heading={intl.formatMessage({
|
||||
id: 'feeds.tabs.heading',
|
||||
})}
|
||||
orientation="horizontal"
|
||||
size="large"
|
||||
tabs={tabs}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default injectIntl(FeedsModal);
|
||||
export default FeedsModal;
|
||||
|
||||
@@ -1,605 +1,204 @@
|
||||
import {defineMessages, FormattedMessage, injectIntl, WrappedComponentProps} from 'react-intl';
|
||||
import {observer} from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
import throttle from 'lodash/throttle';
|
||||
import {FC, ReactNodeArray, useRef, useState} from 'react';
|
||||
import {FormattedMessage, useIntl} from 'react-intl';
|
||||
|
||||
import type {AddFeedOptions} from '@shared/types/api/feed-monitor';
|
||||
import type {Feed} from '@shared/types/Feed';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Form,
|
||||
FormError,
|
||||
FormRow,
|
||||
FormRowGroup,
|
||||
FormRowItem,
|
||||
Select,
|
||||
SelectItem,
|
||||
Textbox,
|
||||
} from '../../../ui';
|
||||
import Close from '../../icons/Close';
|
||||
import Edit from '../../icons/Edit';
|
||||
import {Button, Form, FormError, FormRow, FormRowItem} from '../../../ui';
|
||||
import FeedActions from '../../../actions/FeedActions';
|
||||
import FeedStore from '../../../stores/FeedStore';
|
||||
import FeedForm from './FeedForm';
|
||||
import FeedItemsForm from './FeedItemsForm';
|
||||
import FeedList from './FeedList';
|
||||
import {isNotEmpty, isPositiveInteger, isURLValid} from '../../../util/validators';
|
||||
import ModalFormSectionHeader from '../ModalFormSectionHeader';
|
||||
import UIActions from '../../../actions/UIActions';
|
||||
import * as validators from '../../../util/validators';
|
||||
|
||||
type ValidatedFields = 'url' | 'label' | 'interval';
|
||||
|
||||
interface FeedFormData extends Feed {
|
||||
url: string;
|
||||
label: string;
|
||||
interval: number;
|
||||
intervalMultiplier: number;
|
||||
}
|
||||
|
||||
interface FeedsTabStates {
|
||||
errors?: {
|
||||
[field in ValidatedFields]?: string;
|
||||
};
|
||||
currentlyEditingFeed: Partial<Feed> | null;
|
||||
selectedFeedID: string | null;
|
||||
}
|
||||
|
||||
const MESSAGES = defineMessages({
|
||||
mustSpecifyURL: {
|
||||
id: 'feeds.validation.must.specify.valid.feed.url',
|
||||
},
|
||||
mustSpecifyLabel: {
|
||||
id: 'feeds.validation.must.specify.label',
|
||||
},
|
||||
intervalNotPositive: {
|
||||
id: 'feeds.validation.interval.not.positive',
|
||||
},
|
||||
min: {
|
||||
id: 'feeds.time.min',
|
||||
},
|
||||
hr: {
|
||||
id: 'feeds.time.hr',
|
||||
},
|
||||
day: {
|
||||
id: 'feeds.time.day',
|
||||
},
|
||||
const validatedFields = {
|
||||
url: {
|
||||
id: 'feeds.url',
|
||||
isValid: isURLValid,
|
||||
error: 'feeds.validation.must.specify.valid.feed.url',
|
||||
},
|
||||
label: {
|
||||
id: 'feeds.label',
|
||||
isValid: isNotEmpty,
|
||||
error: 'feeds.validation.must.specify.label',
|
||||
},
|
||||
interval: {
|
||||
id: 'feeds.interval',
|
||||
isValid: isPositiveInteger,
|
||||
error: 'feeds.validation.interval.not.positive',
|
||||
},
|
||||
tags: {
|
||||
id: 'feeds.tags',
|
||||
},
|
||||
search: {
|
||||
id: 'feeds.search',
|
||||
},
|
||||
});
|
||||
} as const;
|
||||
|
||||
type ValidatedField = keyof typeof validatedFields;
|
||||
|
||||
const validateField = (validatedField: ValidatedField, value: string | undefined): string | undefined =>
|
||||
validatedFields[validatedField]?.isValid(value) ? undefined : validatedFields[validatedField]?.error;
|
||||
|
||||
interface FeedFormData {
|
||||
url: string;
|
||||
label: string;
|
||||
interval: string;
|
||||
intervalMultiplier: string;
|
||||
}
|
||||
|
||||
const INTERVAL_MULTIPLIERS = [
|
||||
{
|
||||
message: MESSAGES.min,
|
||||
message: 'feeds.time.min',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
message: MESSAGES.hr,
|
||||
message: 'feeds.time.hr',
|
||||
value: 60,
|
||||
},
|
||||
{
|
||||
message: MESSAGES.day,
|
||||
message: 'feeds.time.day',
|
||||
value: 1440,
|
||||
},
|
||||
] as const;
|
||||
|
||||
const defaultFeed = {
|
||||
const defaultFeed: AddFeedOptions = {
|
||||
label: '',
|
||||
interval: 5,
|
||||
url: '',
|
||||
};
|
||||
|
||||
@observer
|
||||
class FeedsTab extends React.Component<WrappedComponentProps, FeedsTabStates> {
|
||||
formRef: Form | null = null;
|
||||
const FeedsTab: FC = () => {
|
||||
const formRef = useRef<Form>(null);
|
||||
const intl = useIntl();
|
||||
const [currentFeed, setCurrentFeed] = useState<Feed | null>(null);
|
||||
const [errors, setErrors] = useState<Record<string, string | undefined>>({});
|
||||
const [isEditing, setIsEditing] = useState<boolean>(false);
|
||||
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
|
||||
|
||||
manualAddingFormRef: Form | null = null;
|
||||
|
||||
validatedFields = {
|
||||
url: {
|
||||
isValid: validators.isURLValid,
|
||||
error: this.props.intl.formatMessage(MESSAGES.mustSpecifyURL),
|
||||
},
|
||||
label: {
|
||||
isValid: validators.isNotEmpty,
|
||||
error: this.props.intl.formatMessage(MESSAGES.mustSpecifyLabel),
|
||||
},
|
||||
interval: {
|
||||
isValid: validators.isPositiveInteger,
|
||||
error: this.props.intl.formatMessage(MESSAGES.intervalNotPositive),
|
||||
},
|
||||
};
|
||||
|
||||
checkFieldValidity = throttle((fieldName: ValidatedFields, fieldValue) => {
|
||||
const {errors} = this.state;
|
||||
|
||||
if (errors == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (errors[fieldName] && this.validatedFields[fieldName].isValid(fieldValue)) {
|
||||
delete errors[fieldName];
|
||||
this.setState({errors});
|
||||
}
|
||||
}, 150);
|
||||
|
||||
constructor(props: WrappedComponentProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
errors: {},
|
||||
currentlyEditingFeed: null,
|
||||
selectedFeedID: null,
|
||||
};
|
||||
}
|
||||
|
||||
getAmendedFormData(): Pick<Feed, 'url' | 'label' | 'interval'> | null {
|
||||
if (this.formRef == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const formData = this.formRef.getFormData() as Partial<FeedFormData>;
|
||||
|
||||
const {url, label} = formData;
|
||||
if (url == null || label == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let {interval} = defaultFeed;
|
||||
if (formData.interval != null && formData.intervalMultiplier != null) {
|
||||
interval = formData.interval * formData.intervalMultiplier;
|
||||
}
|
||||
|
||||
return {url, label, interval};
|
||||
}
|
||||
|
||||
getIntervalSelectOptions() {
|
||||
return INTERVAL_MULTIPLIERS.map((interval) => (
|
||||
<SelectItem key={interval.value} id={interval.value}>
|
||||
{this.props.intl.formatMessage(interval.message)}
|
||||
</SelectItem>
|
||||
));
|
||||
}
|
||||
|
||||
getModifyFeedForm(feed: Partial<Feed>) {
|
||||
const feedInterval = feed.interval || defaultFeed.interval;
|
||||
|
||||
let defaultIntervalTextValue = feedInterval;
|
||||
let defaultIntervalMultiplier = 1;
|
||||
|
||||
INTERVAL_MULTIPLIERS.forEach((interval) => {
|
||||
const intervalMultiplier = interval.value;
|
||||
|
||||
if (feedInterval % intervalMultiplier === 0) {
|
||||
defaultIntervalTextValue = feedInterval / intervalMultiplier;
|
||||
defaultIntervalMultiplier = intervalMultiplier;
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<FormRowGroup>
|
||||
<FormRow>
|
||||
<Textbox
|
||||
id="label"
|
||||
label={this.props.intl.formatMessage(MESSAGES.label)}
|
||||
placeholder={this.props.intl.formatMessage(MESSAGES.label)}
|
||||
defaultValue={feed.label}
|
||||
/>
|
||||
<Textbox
|
||||
id="interval"
|
||||
label={this.props.intl.formatMessage({
|
||||
id: 'feeds.select.interval',
|
||||
})}
|
||||
placeholder={this.props.intl.formatMessage(MESSAGES.interval)}
|
||||
defaultValue={defaultIntervalTextValue}
|
||||
width="one-eighth"
|
||||
/>
|
||||
<Select labelOffset defaultID={defaultIntervalMultiplier} id="intervalMultiplier" width="one-eighth">
|
||||
{this.getIntervalSelectOptions()}
|
||||
</Select>
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<Textbox
|
||||
id="url"
|
||||
label={this.props.intl.formatMessage({
|
||||
id: 'feeds.url',
|
||||
})}
|
||||
placeholder={this.props.intl.formatMessage(MESSAGES.url)}
|
||||
defaultValue={feed.url}
|
||||
/>
|
||||
<Button labelOffset onClick={() => this.setState({currentlyEditingFeed: null})}>
|
||||
<FormattedMessage id="button.cancel" />
|
||||
</Button>
|
||||
<Button labelOffset type="submit">
|
||||
<FormattedMessage id="button.save.feed" />
|
||||
</Button>
|
||||
</FormRow>
|
||||
</FormRowGroup>
|
||||
);
|
||||
}
|
||||
|
||||
getFeedsListItem(feed: Feed) {
|
||||
const {intl} = this.props;
|
||||
const matchedCount = feed.count || 0;
|
||||
|
||||
let intervalText = `${feed.interval}`;
|
||||
let intervalMultiplierMessage = INTERVAL_MULTIPLIERS[0].message;
|
||||
|
||||
INTERVAL_MULTIPLIERS.forEach((interval) => {
|
||||
if (feed.interval % interval.value === 0) {
|
||||
intervalText = `${feed.interval / interval.value}`;
|
||||
intervalMultiplierMessage = interval.message;
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<li className="interactive-list__item interactive-list__item--stacked-content feed-list__feed" key={feed._id}>
|
||||
<div className="interactive-list__label">
|
||||
<ul className="interactive-list__detail-list">
|
||||
<li
|
||||
className="interactive-list__detail-list__item
|
||||
interactive-list__detail--primary">
|
||||
{feed.label}
|
||||
</li>
|
||||
<li
|
||||
className="interactive-list__detail-list__item
|
||||
interactive-list__detail-list__item--overflow
|
||||
interactive-list__detail interactive-list__detail--secondary">
|
||||
<FormattedMessage id="feeds.match.count" values={{count: matchedCount}} />
|
||||
</li>
|
||||
{feed === this.state.currentlyEditingFeed && (
|
||||
<li
|
||||
className="interactive-list__detail-list__item
|
||||
interactive-list__detail--primary">
|
||||
Modifying
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
<ul className="interactive-list__detail-list">
|
||||
<li
|
||||
className="interactive-list__detail-list__item
|
||||
interactive-list__detail interactive-list__detail--tertiary">
|
||||
{`${intervalText} ${intl.formatMessage(intervalMultiplierMessage)}`}
|
||||
</li>
|
||||
<li
|
||||
className="interactive-list__detail-list__item
|
||||
interactive-list__detail-list__item--overflow
|
||||
interactive-list__detail interactive-list__detail--tertiary">
|
||||
<a href={feed.url} rel="noopener noreferrer" target="_blank">
|
||||
{feed.url}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<span
|
||||
className="interactive-list__icon interactive-list__icon--action"
|
||||
onClick={() => this.handleModifyFeedClick(feed)}>
|
||||
<Edit />
|
||||
</span>
|
||||
<span
|
||||
className="interactive-list__icon interactive-list__icon--action interactive-list__icon--action--warning"
|
||||
onClick={() => this.handleRemoveFeedClick(feed)}>
|
||||
<Close />
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
getFeedAddForm(errors: React.ReactNode) {
|
||||
return (
|
||||
return (
|
||||
<div>
|
||||
<Form
|
||||
className="inverse"
|
||||
onChange={this.handleFormChange}
|
||||
onSubmit={this.handleFormSubmit}
|
||||
ref={(ref) => {
|
||||
this.formRef = ref;
|
||||
}}>
|
||||
onChange={({event, formData}) => {
|
||||
const validatedField = (event.target as HTMLInputElement).name as ValidatedField;
|
||||
const feedForm = (formData as unknown) as FeedFormData;
|
||||
|
||||
setErrors({
|
||||
...errors,
|
||||
[validatedField]: validateField(validatedField, feedForm[validatedField]),
|
||||
});
|
||||
}}
|
||||
onSubmit={async () => {
|
||||
const feedForm = (formRef.current?.getFormData() as unknown) as FeedFormData;
|
||||
if (formRef.current == null || feedForm == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
|
||||
const currentErrors = Object.keys(validatedFields).reduce((memo, key) => {
|
||||
const validatedField = key as ValidatedField;
|
||||
|
||||
return {
|
||||
...memo,
|
||||
[validatedField]: validateField(validatedField, feedForm[validatedField]),
|
||||
};
|
||||
}, {} as Record<string, string | undefined>);
|
||||
setErrors(currentErrors);
|
||||
|
||||
const isFormValid = Object.keys(currentErrors).every((key) => currentErrors[key] === undefined);
|
||||
|
||||
if (isFormValid) {
|
||||
const feed: AddFeedOptions = {
|
||||
label: feedForm.label,
|
||||
url: feedForm.url,
|
||||
interval: Number(feedForm.interval) * Number(feedForm.intervalMultiplier),
|
||||
};
|
||||
|
||||
let success = true;
|
||||
try {
|
||||
if (currentFeed === null) {
|
||||
await FeedActions.addFeed(feed);
|
||||
} else if (currentFeed?._id != null) {
|
||||
await FeedActions.modifyFeed(currentFeed._id, feed);
|
||||
}
|
||||
} catch {
|
||||
success = false;
|
||||
}
|
||||
|
||||
if (success) {
|
||||
formRef.current.resetForm();
|
||||
setErrors({});
|
||||
setCurrentFeed(null);
|
||||
setIsEditing(false);
|
||||
} else {
|
||||
setErrors({backend: 'general.error.unknown'});
|
||||
}
|
||||
}
|
||||
|
||||
setIsSubmitting(false);
|
||||
}}
|
||||
ref={formRef}>
|
||||
<ModalFormSectionHeader>
|
||||
<FormattedMessage id="feeds.existing.feeds" />
|
||||
</ModalFormSectionHeader>
|
||||
{errors}
|
||||
{Object.keys(errors).reduce((memo: ReactNodeArray, key) => {
|
||||
if (errors[key as ValidatedField] != null) {
|
||||
memo.push(
|
||||
<FormRow key={`error-${key}`}>
|
||||
<FormError>{intl.formatMessage({id: errors?.[key as ValidatedField]})}</FormError>
|
||||
</FormRow>,
|
||||
);
|
||||
}
|
||||
|
||||
return memo;
|
||||
}, [])}
|
||||
<FormRow>
|
||||
<FormRowItem>{this.getFeedsList()}</FormRowItem>
|
||||
<FormRowItem>
|
||||
<FeedList
|
||||
currentFeed={currentFeed}
|
||||
intervalMultipliers={INTERVAL_MULTIPLIERS}
|
||||
onSelect={(feed) => {
|
||||
setCurrentFeed(feed);
|
||||
setIsEditing(true);
|
||||
}}
|
||||
onRemove={(feed) => {
|
||||
if (feed === currentFeed) {
|
||||
if (isEditing) {
|
||||
setErrors({});
|
||||
setIsEditing(false);
|
||||
}
|
||||
|
||||
setCurrentFeed(null);
|
||||
}
|
||||
|
||||
if (feed._id != null) {
|
||||
FeedActions.removeFeedMonitor(feed._id);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormRowItem>
|
||||
</FormRow>
|
||||
{this.state.currentlyEditingFeed ? (
|
||||
this.getModifyFeedForm(this.state.currentlyEditingFeed)
|
||||
{isEditing ? (
|
||||
<FeedForm
|
||||
currentFeed={currentFeed}
|
||||
defaultFeed={defaultFeed}
|
||||
intervalMultipliers={INTERVAL_MULTIPLIERS}
|
||||
isSubmitting={isSubmitting}
|
||||
onCancel={() => {
|
||||
setErrors({});
|
||||
setIsEditing(false);
|
||||
setCurrentFeed(null);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<FormRow>
|
||||
<FormRowItem width="auto">{null}</FormRowItem>
|
||||
<Button onClick={() => this.handleAddFeedClick()}>
|
||||
<FormRowItem width="auto" />
|
||||
<Button
|
||||
onClick={() => {
|
||||
setIsEditing(true);
|
||||
}}>
|
||||
<FormattedMessage id="button.new" />
|
||||
</Button>
|
||||
</FormRow>
|
||||
)}
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
<FeedItemsForm />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
getFeedsList() {
|
||||
const {feeds} = FeedStore;
|
||||
|
||||
if (feeds.length === 0) {
|
||||
return (
|
||||
<ul className="interactive-list">
|
||||
<li className="interactive-list__item">
|
||||
<FormattedMessage id="feeds.no.feeds.defined" />
|
||||
</li>
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
const feedsList = feeds.map((feed) => this.getFeedsListItem(feed));
|
||||
|
||||
return <ul className="interactive-list feed-list">{feedsList}</ul>;
|
||||
}
|
||||
|
||||
getFeedItemsForm() {
|
||||
const {feeds, items} = FeedStore;
|
||||
|
||||
const itemElements: React.ReactNodeArray = [];
|
||||
if (this.state.selectedFeedID) {
|
||||
const titleOccurrences: Record<string, number> = {};
|
||||
items.forEach((item, index) => {
|
||||
let {title} = item;
|
||||
const occurrence = titleOccurrences[title];
|
||||
|
||||
if (occurrence == null) {
|
||||
titleOccurrences[title] = 2;
|
||||
} else {
|
||||
title = `${title} #${occurrence}`;
|
||||
titleOccurrences[title] += 1;
|
||||
}
|
||||
|
||||
itemElements.push(
|
||||
<li className="interactive-list__item interactive-list__item--stacked-content feed-list__feed" key={title}>
|
||||
<div className="interactive-list__label feed-list__feed-label">{title}</div>
|
||||
<Checkbox id={`${index}`} />
|
||||
</li>,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Form
|
||||
className="inverse"
|
||||
onChange={this.handleBrowseFeedChange}
|
||||
onSubmit={this.handleBrowseFeedSubmit}
|
||||
ref={(ref) => {
|
||||
this.manualAddingFormRef = ref;
|
||||
}}>
|
||||
<ModalFormSectionHeader>
|
||||
<FormattedMessage id="feeds.browse.feeds" />
|
||||
</ModalFormSectionHeader>
|
||||
<FormRow>
|
||||
<Select
|
||||
disabled={!feeds.length}
|
||||
grow={false}
|
||||
id="feedID"
|
||||
label={this.props.intl.formatMessage({
|
||||
id: 'feeds.select.feed',
|
||||
})}
|
||||
width="three-eighths">
|
||||
{!feeds.length
|
||||
? [
|
||||
<SelectItem key="empty" id="placeholder" placeholder>
|
||||
<em>
|
||||
<FormattedMessage id="feeds.no.feeds.available" />
|
||||
</em>
|
||||
</SelectItem>,
|
||||
]
|
||||
: feeds.reduce(
|
||||
(feedOptions, feed) => {
|
||||
if (feed._id == null) {
|
||||
return feedOptions;
|
||||
}
|
||||
|
||||
return feedOptions.concat(
|
||||
<SelectItem key={feed._id} id={feed._id}>
|
||||
{feed.label}
|
||||
</SelectItem>,
|
||||
);
|
||||
},
|
||||
[
|
||||
<SelectItem key="select-feed" id="placeholder" placeholder>
|
||||
<em>
|
||||
<FormattedMessage id="feeds.select.feed" />
|
||||
</em>
|
||||
</SelectItem>,
|
||||
],
|
||||
)}
|
||||
</Select>
|
||||
{this.renderSearchField()}
|
||||
{this.renderDownloadButton()}
|
||||
</FormRow>
|
||||
{this.state.selectedFeedID ? (
|
||||
<FormRow>
|
||||
{itemElements.length === 0 ? (
|
||||
<ul className="interactive-list">
|
||||
<li className="interactive-list__item">
|
||||
<div className="interactive-list__label">
|
||||
<FormattedMessage id="feeds.no.items.matching" />
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
) : (
|
||||
<ul className="interactive-list feed-list">{itemElements}</ul>
|
||||
)}
|
||||
</FormRow>
|
||||
) : null}
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
handleFormSubmit = () => {
|
||||
const {errors, isValid} = this.validateForm();
|
||||
|
||||
if (!isValid) {
|
||||
this.setState({errors});
|
||||
} else {
|
||||
const currentFeed = this.state.currentlyEditingFeed;
|
||||
const formData = this.getAmendedFormData();
|
||||
|
||||
if (formData != null) {
|
||||
if (currentFeed === defaultFeed) {
|
||||
FeedActions.addFeed(formData);
|
||||
} else if (currentFeed?._id != null) {
|
||||
FeedActions.modifyFeed(currentFeed._id, formData);
|
||||
}
|
||||
}
|
||||
if (this.formRef != null) {
|
||||
this.formRef.resetForm();
|
||||
}
|
||||
this.setState({currentlyEditingFeed: null});
|
||||
}
|
||||
};
|
||||
|
||||
handleFormChange = ({
|
||||
event,
|
||||
formData,
|
||||
}: {
|
||||
event: Event | React.FormEvent<HTMLFormElement>;
|
||||
formData: Record<string, unknown>;
|
||||
}) => {
|
||||
const validatedField = (event.target as HTMLInputElement).name as ValidatedFields;
|
||||
const feedForm = formData as Partial<Feed>;
|
||||
this.checkFieldValidity(validatedField, feedForm[validatedField]);
|
||||
};
|
||||
|
||||
handleRemoveFeedClick = (feed: Feed) => {
|
||||
if (feed._id != null) {
|
||||
FeedActions.removeFeedMonitor(feed._id);
|
||||
}
|
||||
|
||||
if (feed === this.state.currentlyEditingFeed) {
|
||||
this.setState({currentlyEditingFeed: null});
|
||||
}
|
||||
};
|
||||
|
||||
handleAddFeedClick = () => {
|
||||
this.setState({currentlyEditingFeed: defaultFeed});
|
||||
};
|
||||
|
||||
handleModifyFeedClick = (feed: Feed) => {
|
||||
this.setState({currentlyEditingFeed: feed});
|
||||
};
|
||||
|
||||
handleBrowseFeedChange = (input: {
|
||||
event: Event | React.FormEvent<HTMLFormElement>;
|
||||
formData: Record<string, unknown>;
|
||||
}) => {
|
||||
const feedBrowseForm = input.formData as {feedID: string; search: string};
|
||||
if ((input.event.target as HTMLInputElement).type !== 'checkbox') {
|
||||
this.setState({selectedFeedID: feedBrowseForm.feedID});
|
||||
FeedActions.fetchItems({
|
||||
id: feedBrowseForm.feedID,
|
||||
search: feedBrowseForm.search,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
handleBrowseFeedSubmit = () => {
|
||||
if (this.manualAddingFormRef == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = this.manualAddingFormRef.getFormData();
|
||||
|
||||
// TODO: Properly handle array of array of URLs
|
||||
const torrentsToDownload = FeedStore.items
|
||||
.filter((_item, index) => formData[index])
|
||||
.map((item, index) => ({id: index, value: item.urls[0]}));
|
||||
|
||||
UIActions.displayModal({
|
||||
id: 'add-torrents',
|
||||
initialURLs: torrentsToDownload,
|
||||
});
|
||||
};
|
||||
|
||||
validateForm(): {errors?: FeedsTabStates['errors']; isValid: boolean} {
|
||||
if (this.formRef == null) {
|
||||
return {isValid: false};
|
||||
}
|
||||
|
||||
const formData = this.formRef.getFormData();
|
||||
const errors = Object.keys(this.validatedFields).reduce((memo: FeedsTabStates['errors'], field) => {
|
||||
const fieldName = field as ValidatedFields;
|
||||
const fieldValue = `${formData[fieldName]}`;
|
||||
|
||||
return {
|
||||
...memo,
|
||||
...(!this.validatedFields[fieldName].isValid(fieldValue) && memo != null
|
||||
? {fieldName: this.validatedFields[fieldName].error}
|
||||
: {}),
|
||||
};
|
||||
}, {});
|
||||
|
||||
if (errors == null) {
|
||||
return {isValid: true};
|
||||
}
|
||||
|
||||
return {errors, isValid: !Object.keys(errors).length};
|
||||
}
|
||||
|
||||
renderSearchField = () => {
|
||||
const {selectedFeedID} = this.state;
|
||||
|
||||
if (selectedFeedID == null) return null;
|
||||
|
||||
return (
|
||||
<Textbox
|
||||
id="search"
|
||||
label={this.props.intl.formatMessage({
|
||||
id: 'feeds.search.term',
|
||||
})}
|
||||
placeholder={this.props.intl.formatMessage(MESSAGES.search)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
renderDownloadButton = () => {
|
||||
const {selectedFeedID} = this.state;
|
||||
|
||||
if (selectedFeedID == null) return null;
|
||||
|
||||
return (
|
||||
<Button key="button" type="submit" labelOffset>
|
||||
<FormattedMessage id="button.download" />
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
let errors = null;
|
||||
if (this.state.errors != null) {
|
||||
errors = Object.keys(this.state.errors).map((error) => {
|
||||
const errorID = error as ValidatedFields;
|
||||
if (this.state.errors?.[errorID] == null) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<FormRow key={errorID}>
|
||||
<FormError>{this.state.errors[errorID]}</FormError>
|
||||
</FormRow>
|
||||
);
|
||||
});
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
{this.getFeedAddForm(errors)}
|
||||
{this.getFeedItemsForm()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default injectIntl(FeedsTab);
|
||||
export default FeedsTab;
|
||||
|
||||
@@ -2,7 +2,7 @@ import classnames from 'classnames';
|
||||
import * as React from 'react';
|
||||
|
||||
export interface FormRowItemProps {
|
||||
children: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
|
||||
className?: string;
|
||||
type?: string;
|
||||
|
||||
@@ -13,9 +13,10 @@ export const isRegExValid = (regExToCheck: string) => {
|
||||
return true;
|
||||
};
|
||||
|
||||
export const isURLValid = (url: string) => url != null && url !== '' && url.match(matchURL) !== null;
|
||||
export const isURLValid = (url: string | undefined): url is string =>
|
||||
url != null && url !== '' && url.match(matchURL) !== null;
|
||||
|
||||
export const isPositiveInteger = (value: number | string) => {
|
||||
export const isPositiveInteger = (value: number | string | undefined) => {
|
||||
if (value === null || value === '') return false;
|
||||
|
||||
const number = parseInt(`${value}`, 10);
|
||||
|
||||
Reference in New Issue
Block a user