DownloadRulesTab: migrate to Functional Component

This commit is contained in:
Jesse Chan
2021-01-05 12:08:26 +08:00
parent 477d2f5c40
commit 4dc9944bda
5 changed files with 479 additions and 566 deletions
+3 -8
View File
@@ -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 -1
View File
@@ -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 {