mirror of
https://github.com/zoriya/flood.git
synced 2026-06-06 12:02:13 +00:00
DownloadRulesTab: migrate to Functional Component
This commit is contained in:
@@ -38,14 +38,9 @@ const FeedActions = {
|
||||
axios
|
||||
.put(`${baseURI}api/feed-monitor/rules`, options)
|
||||
.then((json) => json.data)
|
||||
.then(
|
||||
() => {
|
||||
FeedActions.fetchFeedMonitors();
|
||||
},
|
||||
() => {
|
||||
// do nothing.
|
||||
},
|
||||
),
|
||||
.then(() => {
|
||||
FeedActions.fetchFeedMonitors();
|
||||
}),
|
||||
|
||||
fetchFeedMonitors: () =>
|
||||
axios
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
import {FC} from 'react';
|
||||
import {FormattedMessage, useIntl} from 'react-intl';
|
||||
|
||||
import type {AddRuleOptions} from '@shared/types/api/feed-monitor';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
FormElementAddon,
|
||||
FormRow,
|
||||
FormRowGroup,
|
||||
FormRowItem,
|
||||
Select,
|
||||
SelectItem,
|
||||
Textbox,
|
||||
} from '../../../ui';
|
||||
import Checkmark from '../../icons/Checkmark';
|
||||
import FeedStore from '../../../stores/FeedStore';
|
||||
import FilesystemBrowserTextbox from '../../general/form-elements/FilesystemBrowserTextbox';
|
||||
import TagSelect from '../../general/form-elements/TagSelect';
|
||||
|
||||
interface DownloadRuleFormProps {
|
||||
rule: AddRuleOptions;
|
||||
isSubmitting: boolean;
|
||||
isPatternMatched: boolean;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
const DownloadRuleForm: FC<DownloadRuleFormProps> = ({
|
||||
rule,
|
||||
isSubmitting,
|
||||
isPatternMatched,
|
||||
onCancel,
|
||||
}: DownloadRuleFormProps) => {
|
||||
const {feeds} = FeedStore;
|
||||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
<FormRowGroup>
|
||||
<FormRow>
|
||||
<Textbox
|
||||
id="label"
|
||||
label={intl.formatMessage({
|
||||
id: 'feeds.label',
|
||||
})}
|
||||
defaultValue={rule.label}
|
||||
/>
|
||||
<Select
|
||||
disabled={!feeds.length}
|
||||
id="feedID"
|
||||
label={intl.formatMessage({
|
||||
id: 'feeds.applicable.feed',
|
||||
})}
|
||||
defaultID={rule.feedIDs?.[0]}>
|
||||
{feeds.length === 0
|
||||
? [
|
||||
<SelectItem key="empty" id="placeholder" placeholder>
|
||||
<em>
|
||||
<FormattedMessage id="feeds.no.feeds.available" />
|
||||
</em>
|
||||
</SelectItem>,
|
||||
]
|
||||
: feeds.reduce(
|
||||
(feedOptions, feed) =>
|
||||
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>
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<Textbox
|
||||
id="match"
|
||||
label={intl.formatMessage({
|
||||
id: 'feeds.match.pattern',
|
||||
})}
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'feeds.regEx',
|
||||
})}
|
||||
defaultValue={rule.match}
|
||||
width="three-eighths"
|
||||
/>
|
||||
<Textbox
|
||||
id="exclude"
|
||||
label={intl.formatMessage({
|
||||
id: 'feeds.exclude.pattern',
|
||||
})}
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'feeds.regEx',
|
||||
})}
|
||||
defaultValue={rule.exclude}
|
||||
width="three-eighths"
|
||||
/>
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<Textbox
|
||||
addonPlacement="after"
|
||||
id="check"
|
||||
label={intl.formatMessage({
|
||||
id: 'feeds.test.match',
|
||||
})}
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'feeds.check',
|
||||
})}>
|
||||
{isPatternMatched && (
|
||||
<FormElementAddon>
|
||||
<Checkmark />
|
||||
</FormElementAddon>
|
||||
)}
|
||||
</Textbox>
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<FormRowItem>
|
||||
<FilesystemBrowserTextbox
|
||||
id="destination"
|
||||
label={intl.formatMessage({
|
||||
id: 'feeds.torrent.destination',
|
||||
})}
|
||||
selectable="directories"
|
||||
suggested={rule.destination}
|
||||
showBasePathToggle
|
||||
/>
|
||||
</FormRowItem>
|
||||
<TagSelect
|
||||
id="tags"
|
||||
label={intl.formatMessage({
|
||||
id: 'feeds.apply.tags',
|
||||
})}
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'feeds.tags',
|
||||
})}
|
||||
defaultValue={rule.tags}
|
||||
/>
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<br />
|
||||
<Checkbox id="startOnLoad" defaultChecked={rule.startOnLoad} matchTextboxHeight>
|
||||
<FormattedMessage id="feeds.start.on.load" />
|
||||
</Checkbox>
|
||||
<Button onClick={onCancel}>
|
||||
<FormattedMessage id="button.cancel" />
|
||||
</Button>
|
||||
<Button type="submit" isLoading={isSubmitting}>
|
||||
<FormattedMessage id="button.save.feed" />
|
||||
</Button>
|
||||
</FormRow>
|
||||
</FormRowGroup>
|
||||
);
|
||||
};
|
||||
|
||||
export default DownloadRuleForm;
|
||||
@@ -0,0 +1,123 @@
|
||||
import {FC} from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {observer} from 'mobx-react';
|
||||
|
||||
import {Rule} from '@shared/types/Feed';
|
||||
|
||||
import Close from '../../icons/Close';
|
||||
import Edit from '../../icons/Edit';
|
||||
import FeedStore from '../../../stores/FeedStore';
|
||||
|
||||
interface DownloadRuleListProps {
|
||||
currentRule: Rule | null;
|
||||
onSelect: (rule: Rule) => void;
|
||||
onRemove: (rule: Rule) => void;
|
||||
}
|
||||
|
||||
const DownloadRuleList: FC<DownloadRuleListProps> = observer(
|
||||
({currentRule, onSelect, onRemove}: DownloadRuleListProps) => {
|
||||
const {rules} = FeedStore;
|
||||
|
||||
if (rules.length === 0) {
|
||||
return (
|
||||
<ul className="interactive-list">
|
||||
<li className="interactive-list__item">
|
||||
<FormattedMessage id="feeds.no.rules.defined" />
|
||||
</li>
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ul className="interactive-list">
|
||||
{rules.map((rule) => {
|
||||
const matchedCount = rule.count || 0;
|
||||
let excludeNode = null;
|
||||
let tags = null;
|
||||
|
||||
if (rule.exclude) {
|
||||
excludeNode = (
|
||||
<li
|
||||
className="interactive-list__detail-list__item
|
||||
interactive-list__detail interactive-list__detail--tertiary">
|
||||
<FormattedMessage id="feeds.exclude" />
|
||||
{': '}
|
||||
{rule.exclude}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
if (rule.tags && rule.tags.length > 0) {
|
||||
const tagNodes = rule.tags.map((tag) => (
|
||||
<span className="tag" key={tag}>
|
||||
{tag}
|
||||
</span>
|
||||
));
|
||||
|
||||
tags = (
|
||||
<li className="interactive-list__detail-list__item interactive-list__detail interactive-list__detail--tertiary">
|
||||
<FormattedMessage id="feeds.tags" />
|
||||
{': '}
|
||||
{tagNodes}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<li className="interactive-list__item interactive-list__item--stacked-content" key={rule._id}>
|
||||
<div className="interactive-list__label">
|
||||
<ul className="interactive-list__detail-list">
|
||||
<li
|
||||
className="interactive-list__detail-list__item
|
||||
interactive-list__detail--primary">
|
||||
{rule.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>
|
||||
{rule === currentRule && (
|
||||
<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"
|
||||
style={{
|
||||
maxWidth: '50%',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
}}>
|
||||
<FormattedMessage id="feeds.match" />
|
||||
{': '}
|
||||
{rule.match}
|
||||
</li>
|
||||
<div style={{width: '100%'}} />
|
||||
{excludeNode}
|
||||
{tags}
|
||||
</ul>
|
||||
</div>
|
||||
<span className="interactive-list__icon interactive-list__icon--action" onClick={() => onSelect(rule)}>
|
||||
<Edit />
|
||||
</span>
|
||||
<span
|
||||
className="interactive-list__icon interactive-list__icon--action interactive-list__icon--action--warning"
|
||||
onClick={() => onRemove(rule)}>
|
||||
<Close />
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default DownloadRuleList;
|
||||
@@ -1,85 +1,19 @@
|
||||
import {defineMessages, FormattedMessage, injectIntl, WrappedComponentProps} from 'react-intl';
|
||||
import {observer} from 'mobx-react';
|
||||
import throttle from 'lodash/throttle';
|
||||
import * as React from 'react';
|
||||
import {FC, ReactNodeArray, useRef, useState} from 'react';
|
||||
import {FormattedMessage, useIntl} from 'react-intl';
|
||||
|
||||
import type {AddRuleOptions} from '@shared/types/api/feed-monitor';
|
||||
import type {Rule} from '@shared/types/Feed';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Form,
|
||||
FormElementAddon,
|
||||
FormError,
|
||||
FormRow,
|
||||
FormRowGroup,
|
||||
FormRowItem,
|
||||
Select,
|
||||
SelectItem,
|
||||
Textbox,
|
||||
} from '../../../ui';
|
||||
import Checkmark from '../../icons/Checkmark';
|
||||
import Close from '../../icons/Close';
|
||||
import Edit from '../../icons/Edit';
|
||||
import {Button, Form, FormError, FormRow, FormRowItem} from '../../../ui';
|
||||
import DownloadRuleForm from './DownloadRuleForm';
|
||||
import FeedActions from '../../../actions/FeedActions';
|
||||
import FeedStore from '../../../stores/FeedStore';
|
||||
import FilesystemBrowserTextbox from '../../general/form-elements/FilesystemBrowserTextbox';
|
||||
import ModalFormSectionHeader from '../ModalFormSectionHeader';
|
||||
import TagSelect from '../../general/form-elements/TagSelect';
|
||||
import * as validators from '../../../util/validators';
|
||||
import DownloadRuleList from './DownloadRuleList';
|
||||
|
||||
type ValidatedFields = 'destination' | 'feedID' | 'label' | 'match' | 'exclude';
|
||||
|
||||
interface RuleFormData extends Omit<Rule, 'tags' | 'feedIDs'> {
|
||||
check: string;
|
||||
feedID: string;
|
||||
tags: string;
|
||||
}
|
||||
|
||||
interface DownloadRulesTabStates {
|
||||
errors?: {
|
||||
[field in ValidatedFields]?: string;
|
||||
};
|
||||
isSubmitting: boolean;
|
||||
isFormChanged: boolean;
|
||||
currentlyEditingRule: Partial<Rule> | null;
|
||||
doesPatternMatchTest: boolean;
|
||||
}
|
||||
|
||||
const MESSAGES = defineMessages({
|
||||
mustSpecifyDestination: {
|
||||
id: 'feeds.validation.must.specify.destination',
|
||||
},
|
||||
mustSelectFeed: {
|
||||
id: 'feeds.validation.must.select.feed',
|
||||
},
|
||||
mustSpecifyLabel: {
|
||||
id: 'feeds.validation.must.specify.label',
|
||||
},
|
||||
invalidRegularExpression: {
|
||||
id: 'feeds.validation.invalid.regular.expression',
|
||||
},
|
||||
url: {
|
||||
id: 'feeds.url',
|
||||
},
|
||||
label: {
|
||||
id: 'feeds.label',
|
||||
},
|
||||
regEx: {
|
||||
id: 'feeds.regEx',
|
||||
},
|
||||
tags: {
|
||||
id: 'feeds.tags',
|
||||
},
|
||||
check: {
|
||||
id: 'feeds.check',
|
||||
},
|
||||
});
|
||||
|
||||
const defaultRule = {
|
||||
const initialRule: AddRuleOptions = {
|
||||
label: '',
|
||||
feedIDs: [''],
|
||||
feedIDs: [],
|
||||
match: '',
|
||||
exclude: '',
|
||||
tags: [],
|
||||
@@ -87,494 +21,195 @@ const defaultRule = {
|
||||
startOnLoad: false,
|
||||
};
|
||||
|
||||
@observer
|
||||
class DownloadRulesTab extends React.Component<WrappedComponentProps, DownloadRulesTabStates> {
|
||||
formRef: Form | null = null;
|
||||
|
||||
validatedFields = {
|
||||
destination: {
|
||||
isValid: validators.isNotEmpty,
|
||||
error: this.props.intl.formatMessage(MESSAGES.mustSpecifyDestination),
|
||||
},
|
||||
feedID: {
|
||||
isValid: validators.isNotEmpty,
|
||||
error: this.props.intl.formatMessage(MESSAGES.mustSelectFeed),
|
||||
},
|
||||
label: {
|
||||
isValid: validators.isNotEmpty,
|
||||
error: this.props.intl.formatMessage(MESSAGES.mustSpecifyLabel),
|
||||
},
|
||||
match: {
|
||||
isValid: (value: string) => validators.isNotEmpty(value) && validators.isRegExValid(value),
|
||||
error: this.props.intl.formatMessage(MESSAGES.invalidRegularExpression),
|
||||
},
|
||||
exclude: {
|
||||
isValid: (value: string) => {
|
||||
if (validators.isNotEmpty(value)) {
|
||||
return validators.isRegExValid(value);
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
error: this.props.intl.formatMessage(MESSAGES.invalidRegularExpression),
|
||||
},
|
||||
};
|
||||
|
||||
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: {},
|
||||
isSubmitting: false,
|
||||
isFormChanged: false,
|
||||
currentlyEditingRule: null,
|
||||
doesPatternMatchTest: false,
|
||||
};
|
||||
}
|
||||
|
||||
getAmendedFormData(): AddRuleOptions | null {
|
||||
if (this.formRef == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const formData = this.formRef.getFormData() as Partial<RuleFormData>;
|
||||
if (formData == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const feedIDs = [formData.feedID || ''];
|
||||
|
||||
delete formData.feedID;
|
||||
delete formData.check;
|
||||
|
||||
return {
|
||||
...defaultRule,
|
||||
...formData,
|
||||
feedIDs,
|
||||
...(formData.tags != null
|
||||
? {
|
||||
tags: formData.tags.split(','),
|
||||
}
|
||||
: {
|
||||
tags: [],
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
getModifyRuleForm(rule: Partial<Rule>) {
|
||||
const {doesPatternMatchTest, currentlyEditingRule} = this.state;
|
||||
const {feeds} = FeedStore;
|
||||
|
||||
return (
|
||||
<FormRowGroup key={currentlyEditingRule == null ? 'default' : currentlyEditingRule._id}>
|
||||
<FormRow>
|
||||
<Textbox
|
||||
id="label"
|
||||
label={this.props.intl.formatMessage({
|
||||
id: 'feeds.label',
|
||||
})}
|
||||
defaultValue={rule.label}
|
||||
/>
|
||||
<Select
|
||||
disabled={!feeds.length}
|
||||
id="feedID"
|
||||
label={this.props.intl.formatMessage({
|
||||
id: 'feeds.applicable.feed',
|
||||
})}
|
||||
defaultID={rule.feedIDs?.[0]}>
|
||||
{feeds.length === 0
|
||||
? [
|
||||
<SelectItem key="empty" id="placeholder" placeholder>
|
||||
<em>
|
||||
<FormattedMessage id="feeds.no.feeds.available" />
|
||||
</em>
|
||||
</SelectItem>,
|
||||
]
|
||||
: feeds.reduce(
|
||||
(feedOptions, feed) =>
|
||||
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>
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<Textbox
|
||||
id="match"
|
||||
label={this.props.intl.formatMessage({
|
||||
id: 'feeds.match.pattern',
|
||||
})}
|
||||
placeholder={this.props.intl.formatMessage(MESSAGES.regEx)}
|
||||
defaultValue={rule.match}
|
||||
width="three-eighths"
|
||||
/>
|
||||
<Textbox
|
||||
id="exclude"
|
||||
label={this.props.intl.formatMessage({
|
||||
id: 'feeds.exclude.pattern',
|
||||
})}
|
||||
placeholder={this.props.intl.formatMessage(MESSAGES.regEx)}
|
||||
defaultValue={rule.exclude}
|
||||
width="three-eighths"
|
||||
/>
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<Textbox
|
||||
addonPlacement="after"
|
||||
id="check"
|
||||
label={this.props.intl.formatMessage({
|
||||
id: 'feeds.test.match',
|
||||
})}
|
||||
placeholder={this.props.intl.formatMessage(MESSAGES.check)}>
|
||||
{doesPatternMatchTest && (
|
||||
<FormElementAddon>
|
||||
<Checkmark />
|
||||
</FormElementAddon>
|
||||
)}
|
||||
</Textbox>
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<FormRowItem>
|
||||
<FilesystemBrowserTextbox
|
||||
id="destination"
|
||||
label={this.props.intl.formatMessage({
|
||||
id: 'feeds.torrent.destination',
|
||||
})}
|
||||
selectable="directories"
|
||||
suggested={rule.destination}
|
||||
showBasePathToggle
|
||||
/>
|
||||
</FormRowItem>
|
||||
<TagSelect
|
||||
id="tags"
|
||||
label={this.props.intl.formatMessage({
|
||||
id: 'feeds.apply.tags',
|
||||
})}
|
||||
placeholder={this.props.intl.formatMessage(MESSAGES.tags)}
|
||||
defaultValue={rule.tags}
|
||||
/>
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<br />
|
||||
<Checkbox id="startOnLoad" defaultChecked={rule.startOnLoad} matchTextboxHeight>
|
||||
<FormattedMessage id="feeds.start.on.load" />
|
||||
</Checkbox>
|
||||
<Button onClick={() => this.setState({currentlyEditingRule: null})}>
|
||||
<FormattedMessage id="button.cancel" />
|
||||
</Button>
|
||||
<Button type="submit" isLoading={this.state.isSubmitting}>
|
||||
<FormattedMessage id="button.save.feed" />
|
||||
</Button>
|
||||
</FormRow>
|
||||
</FormRowGroup>
|
||||
);
|
||||
}
|
||||
|
||||
getRulesListItem(rule: Rule) {
|
||||
const matchedCount = rule.count || 0;
|
||||
let excludeNode = null;
|
||||
let tags = null;
|
||||
|
||||
if (rule.exclude) {
|
||||
excludeNode = (
|
||||
<li
|
||||
className="interactive-list__detail-list__item
|
||||
interactive-list__detail interactive-list__detail--tertiary">
|
||||
<FormattedMessage id="feeds.exclude" />
|
||||
{': '}
|
||||
{rule.exclude}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
if (rule.tags && rule.tags.length > 0) {
|
||||
const tagNodes = rule.tags.map((tag) => (
|
||||
<span className="tag" key={tag}>
|
||||
{tag}
|
||||
</span>
|
||||
));
|
||||
|
||||
tags = (
|
||||
<li className="interactive-list__detail-list__item interactive-list__detail interactive-list__detail--tertiary">
|
||||
<FormattedMessage id="feeds.tags" />
|
||||
{': '}
|
||||
{tagNodes}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<li className="interactive-list__item interactive-list__item--stacked-content" key={rule._id}>
|
||||
<div className="interactive-list__label">
|
||||
<ul className="interactive-list__detail-list">
|
||||
<li
|
||||
className="interactive-list__detail-list__item
|
||||
interactive-list__detail--primary">
|
||||
{rule.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>
|
||||
{rule === this.state.currentlyEditingRule && (
|
||||
<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"
|
||||
style={{
|
||||
maxWidth: '50%',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
}}>
|
||||
<FormattedMessage id="feeds.match" />
|
||||
{': '}
|
||||
{rule.match}
|
||||
</li>
|
||||
<div style={{width: '100%'}} />
|
||||
{excludeNode}
|
||||
{tags}
|
||||
</ul>
|
||||
</div>
|
||||
<span
|
||||
className="interactive-list__icon interactive-list__icon--action"
|
||||
onClick={() => this.handleModifyRuleClick(rule)}>
|
||||
<Edit />
|
||||
</span>
|
||||
<span
|
||||
className="interactive-list__icon interactive-list__icon--action interactive-list__icon--action--warning"
|
||||
onClick={() => this.handleRemoveRuleClick(rule)}>
|
||||
<Close />
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
getRulesList(): [React.ReactNode, React.ReactNode] {
|
||||
const {rules} = FeedStore;
|
||||
|
||||
if (rules.length === 0) {
|
||||
return [
|
||||
<ul className="interactive-list" key="before-editing">
|
||||
<li className="interactive-list__item">
|
||||
<FormattedMessage id="feeds.no.rules.defined" />
|
||||
</li>
|
||||
</ul>,
|
||||
null,
|
||||
];
|
||||
}
|
||||
|
||||
const rulesList = rules.map((rule) => this.getRulesListItem(rule));
|
||||
|
||||
if (this.state.currentlyEditingRule == null || this.state.currentlyEditingRule === defaultRule) {
|
||||
return [
|
||||
<ul className="interactive-list" key="before-editing">
|
||||
{rulesList}
|
||||
</ul>,
|
||||
null,
|
||||
];
|
||||
}
|
||||
|
||||
const editingRuleIndex = rules.indexOf(this.state.currentlyEditingRule as Rule);
|
||||
|
||||
return [
|
||||
<ul className="interactive-list" key="before-editing">
|
||||
{rulesList.slice(0, editingRuleIndex + 1)}
|
||||
</ul>,
|
||||
<ul className="interactive-list" key="after-editing">
|
||||
{rulesList.slice(editingRuleIndex + 1)}
|
||||
</ul>,
|
||||
];
|
||||
}
|
||||
|
||||
handleFormChange = ({
|
||||
event,
|
||||
formData,
|
||||
}: {
|
||||
event: Event | React.FormEvent<HTMLFormElement>;
|
||||
formData: Record<string, unknown>;
|
||||
}) => {
|
||||
const validatedField = (event.target as HTMLInputElement).name as ValidatedFields;
|
||||
const ruleFormData = formData as Partial<RuleFormData>;
|
||||
this.checkFieldValidity(validatedField, ruleFormData[validatedField]);
|
||||
this.checkMatchingPattern(
|
||||
ruleFormData.match != null ? ruleFormData.match : defaultRule.match,
|
||||
ruleFormData.exclude != null ? ruleFormData.exclude : defaultRule.exclude,
|
||||
ruleFormData.check != null ? ruleFormData.check : '',
|
||||
);
|
||||
this.setState({isFormChanged: true});
|
||||
};
|
||||
|
||||
handleFormSubmit = async () => {
|
||||
const {errors, isValid} = this.validateForm();
|
||||
|
||||
this.setState({isSubmitting: true});
|
||||
|
||||
if (!isValid) {
|
||||
this.setState({errors});
|
||||
} else {
|
||||
const currentRule = this.state.currentlyEditingRule;
|
||||
const formData = this.getAmendedFormData();
|
||||
|
||||
if (formData != null && this.state.isFormChanged) {
|
||||
if (currentRule !== null && currentRule !== defaultRule && currentRule._id != null) {
|
||||
await FeedActions.removeFeedMonitor(currentRule._id);
|
||||
}
|
||||
await FeedActions.addRule(formData);
|
||||
const validatedFields = {
|
||||
destination: {
|
||||
isValid: validators.isNotEmpty,
|
||||
error: 'feeds.validation.must.specify.destination',
|
||||
},
|
||||
feedID: {
|
||||
isValid: (value: string | undefined) => validators.isNotEmpty(value) && value !== 'placeholder',
|
||||
error: 'feeds.validation.must.select.feed',
|
||||
},
|
||||
label: {
|
||||
isValid: validators.isNotEmpty,
|
||||
error: 'feeds.validation.must.specify.label',
|
||||
},
|
||||
match: {
|
||||
isValid: (value: string | undefined) => validators.isNotEmpty(value) && validators.isRegExValid(value),
|
||||
error: 'feeds.validation.invalid.regular.expression',
|
||||
},
|
||||
exclude: {
|
||||
isValid: (value: string | undefined) => {
|
||||
if (validators.isNotEmpty(value)) {
|
||||
return validators.isRegExValid(value);
|
||||
}
|
||||
|
||||
if (this.formRef != null) {
|
||||
this.formRef.resetForm();
|
||||
}
|
||||
return true;
|
||||
},
|
||||
error: 'feeds.validation.invalid.regular.expression',
|
||||
},
|
||||
} as const;
|
||||
|
||||
this.setState({currentlyEditingRule: null});
|
||||
}
|
||||
type ValidatedField = keyof typeof validatedFields;
|
||||
|
||||
this.setState({isSubmitting: false});
|
||||
};
|
||||
const validateField = (validatedField: ValidatedField, value: string | undefined): string | undefined =>
|
||||
validatedFields[validatedField]?.isValid(value) ? undefined : validatedFields[validatedField]?.error;
|
||||
|
||||
handleAddRuleClick = () => {
|
||||
this.setState({currentlyEditingRule: defaultRule});
|
||||
};
|
||||
|
||||
handleRemoveRuleClick(rule: Rule) {
|
||||
if (rule._id != null) {
|
||||
FeedActions.removeFeedMonitor(rule._id);
|
||||
}
|
||||
|
||||
if (rule === this.state.currentlyEditingRule) {
|
||||
this.setState({currentlyEditingRule: null});
|
||||
}
|
||||
}
|
||||
|
||||
handleModifyRuleClick(rule: Rule) {
|
||||
this.setState({currentlyEditingRule: rule});
|
||||
}
|
||||
|
||||
checkMatchingPattern(match: RuleFormData['match'], exclude: RuleFormData['exclude'], check: RuleFormData['check']) {
|
||||
let doesPatternMatchTest = false;
|
||||
|
||||
if (validators.isNotEmpty(check) && validators.isRegExValid(match) && validators.isRegExValid(exclude)) {
|
||||
const isMatched = new RegExp(match, 'gi').test(check);
|
||||
const isExcluded = exclude !== '' && new RegExp(exclude, 'gi').test(check);
|
||||
doesPatternMatchTest = isMatched && !isExcluded;
|
||||
}
|
||||
|
||||
this.setState({doesPatternMatchTest});
|
||||
}
|
||||
|
||||
validateForm(): {
|
||||
errors?: DownloadRulesTabStates['errors'];
|
||||
isValid: boolean;
|
||||
} {
|
||||
const formData = this.getAmendedFormData();
|
||||
|
||||
if (formData == null) {
|
||||
return {isValid: false};
|
||||
}
|
||||
|
||||
const errors = Object.keys(this.validatedFields).reduce((accumulator: DownloadRulesTabStates['errors'], field) => {
|
||||
const fieldName = field as ValidatedFields;
|
||||
const fieldValue = fieldName === 'feedID' ? formData.feedIDs[0] : formData[fieldName];
|
||||
|
||||
if (!this.validatedFields[fieldName].isValid(fieldValue) && accumulator != null) {
|
||||
accumulator[fieldName] = this.validatedFields[fieldName].error;
|
||||
}
|
||||
|
||||
return accumulator;
|
||||
}, {});
|
||||
|
||||
if (errors == null) {
|
||||
return {isValid: true};
|
||||
}
|
||||
|
||||
return {errors, isValid: !Object.keys(errors).length};
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const [listBeforeEditingRule, listAfterEditingRule] = this.getRulesList();
|
||||
|
||||
return (
|
||||
<Form
|
||||
className="inverse"
|
||||
onChange={this.handleFormChange}
|
||||
onSubmit={this.handleFormSubmit}
|
||||
ref={(ref) => {
|
||||
this.formRef = ref;
|
||||
}}>
|
||||
<ModalFormSectionHeader>
|
||||
<FormattedMessage id="feeds.existing.rules" />
|
||||
</ModalFormSectionHeader>
|
||||
{errors}
|
||||
{listAfterEditingRule == null ? (
|
||||
<FormRow>
|
||||
<FormRowItem>{listBeforeEditingRule}</FormRowItem>
|
||||
</FormRow>
|
||||
) : (
|
||||
<FormRowGroup>
|
||||
<FormRow>{listBeforeEditingRule}</FormRow>
|
||||
{this.getModifyRuleForm(this.state.currentlyEditingRule as Partial<Rule>)}
|
||||
<FormRow>{listAfterEditingRule}</FormRow>
|
||||
</FormRowGroup>
|
||||
)}
|
||||
{this.state.currentlyEditingRule && listAfterEditingRule == null ? (
|
||||
this.getModifyRuleForm(this.state.currentlyEditingRule)
|
||||
) : (
|
||||
<FormRow>
|
||||
<br />
|
||||
<Button onClick={this.handleAddRuleClick}>
|
||||
<FormattedMessage id="button.new" />
|
||||
</Button>
|
||||
</FormRow>
|
||||
)}
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
interface RuleFormData extends Omit<Rule, 'tags' | 'feedIDs'> {
|
||||
check: string;
|
||||
feedID: string;
|
||||
tags: string;
|
||||
}
|
||||
|
||||
export default injectIntl(DownloadRulesTab);
|
||||
const DownloadRulesTab: FC = () => {
|
||||
const formRef = useRef<Form>(null);
|
||||
const intl = useIntl();
|
||||
|
||||
const [currentRule, setCurrentRule] = useState<Rule | null>(null);
|
||||
const [errors, setErrors] = useState<Record<string, string | undefined>>({});
|
||||
const [isEditing, setIsEditing] = useState<boolean>(false);
|
||||
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
|
||||
const [isFormChanged, setIsFormChanged] = useState<boolean>(false);
|
||||
const [isPatternMatched, setIsPatternMatched] = useState<boolean>(false);
|
||||
|
||||
return (
|
||||
<Form
|
||||
className="inverse"
|
||||
onChange={({event, formData}) => {
|
||||
const validatedField = (event.target as HTMLInputElement).name as ValidatedField;
|
||||
const ruleFormData = formData as Partial<RuleFormData>;
|
||||
|
||||
setErrors({
|
||||
...errors,
|
||||
[validatedField]: validateField(validatedField, ruleFormData[validatedField]),
|
||||
});
|
||||
|
||||
setIsPatternMatched(
|
||||
(() => {
|
||||
const {check, match = '', exclude = ''} = ruleFormData;
|
||||
|
||||
if (validators.isNotEmpty(check) && validators.isRegExValid(match) && validators.isRegExValid(exclude)) {
|
||||
const isMatched = new RegExp(match, 'gi').test(check);
|
||||
const isExcluded = exclude !== '' && new RegExp(exclude, 'gi').test(check);
|
||||
return isMatched && !isExcluded;
|
||||
}
|
||||
|
||||
return false;
|
||||
})(),
|
||||
);
|
||||
|
||||
setIsFormChanged(true);
|
||||
}}
|
||||
onSubmit={async () => {
|
||||
if (formRef.current == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = formRef.current.getFormData() as Partial<RuleFormData>;
|
||||
|
||||
setIsSubmitting(true);
|
||||
setErrors(
|
||||
Object.keys(validatedFields).reduce((memo, key) => {
|
||||
const validatedField = key as ValidatedField;
|
||||
|
||||
return {
|
||||
...memo,
|
||||
[validatedField]: validateField(validatedField, formData[validatedField]),
|
||||
};
|
||||
}, {} as Record<string, string | undefined>),
|
||||
);
|
||||
|
||||
const isFormValid = Object.keys(errors).every((key) => errors[key] === undefined);
|
||||
|
||||
if (isFormChanged && isFormValid) {
|
||||
if (currentRule?._id != null) {
|
||||
await FeedActions.removeFeedMonitor(currentRule._id);
|
||||
}
|
||||
|
||||
await FeedActions.addRule({
|
||||
label: formData.label ?? initialRule.label,
|
||||
feedIDs: [formData.feedID ?? ''],
|
||||
field: formData.field,
|
||||
match: formData.match ?? initialRule.match,
|
||||
exclude: formData.exclude ?? initialRule.exclude,
|
||||
destination: formData.destination ?? initialRule.destination,
|
||||
tags: formData.tags?.split(',') ?? initialRule.tags,
|
||||
startOnLoad: formData.startOnLoad ?? initialRule.startOnLoad,
|
||||
isBasePath: formData.isBasePath ?? false,
|
||||
}).then(
|
||||
() => {
|
||||
formRef.current?.resetForm();
|
||||
setCurrentRule(null);
|
||||
setErrors({});
|
||||
setIsEditing(false);
|
||||
},
|
||||
(err: Error) => {
|
||||
setErrors({backend: err.message});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
setIsSubmitting(false);
|
||||
}}
|
||||
ref={formRef}>
|
||||
<ModalFormSectionHeader>
|
||||
<FormattedMessage id="feeds.existing.rules" />
|
||||
</ModalFormSectionHeader>
|
||||
{Object.keys(errors).reduce((memo: ReactNodeArray, key) => {
|
||||
if (errors[key as ValidatedField] != null) {
|
||||
memo.push(
|
||||
<FormRow key={key}>
|
||||
<FormError>{intl.formatMessage({id: errors?.[key as ValidatedField]})}</FormError>
|
||||
</FormRow>,
|
||||
);
|
||||
}
|
||||
|
||||
return memo;
|
||||
}, [])}
|
||||
<FormRow>
|
||||
<FormRowItem>
|
||||
<DownloadRuleList
|
||||
currentRule={currentRule}
|
||||
onSelect={(rule) => {
|
||||
setCurrentRule(rule);
|
||||
setIsEditing(true);
|
||||
}}
|
||||
onRemove={(rule) => {
|
||||
if (rule._id != null) {
|
||||
FeedActions.removeFeedMonitor(rule._id);
|
||||
}
|
||||
|
||||
if (rule === currentRule) {
|
||||
setCurrentRule(null);
|
||||
setIsEditing(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormRowItem>
|
||||
</FormRow>
|
||||
{isEditing ? (
|
||||
<DownloadRuleForm
|
||||
rule={currentRule ?? initialRule}
|
||||
isPatternMatched={isPatternMatched}
|
||||
isSubmitting={isSubmitting}
|
||||
onCancel={() => {
|
||||
setCurrentRule(null);
|
||||
setIsEditing(false);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<FormRow>
|
||||
<br />
|
||||
<Button
|
||||
onClick={() => {
|
||||
setIsEditing(true);
|
||||
}}>
|
||||
<FormattedMessage id="button.new" />
|
||||
</Button>
|
||||
</FormRow>
|
||||
)}
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default DownloadRulesTab;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {url as matchURL} from '@shared/util/regEx';
|
||||
|
||||
export const isNotEmpty = (value: string) => value != null && value !== '';
|
||||
export const isNotEmpty = (value: string | undefined): value is string => value != null && value !== '';
|
||||
|
||||
export const isRegExValid = (regExToCheck: string) => {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user