Add partial blacklist form
This commit is contained in:
parent
7528fe831f
commit
fa6dfb0395
8 changed files with 185 additions and 58 deletions
|
@ -2,17 +2,17 @@ import './BlacklistForm.scss';
|
||||||
import AddButton from '../ui/AddButton';
|
import AddButton from '../ui/AddButton';
|
||||||
import DeleteButton from '../ui/DeleteButton';
|
import DeleteButton from '../ui/DeleteButton';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { BlacklistJSON } from '../../../shared/settings/Blacklist';
|
import Blacklist, { BlacklistItem } from '../../../shared/settings/Blacklist';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
value: BlacklistJSON;
|
value: Blacklist;
|
||||||
onChange: (value: BlacklistJSON) => void;
|
onChange: (value: Blacklist) => void;
|
||||||
onBlur: () => void;
|
onBlur: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
class BlacklistForm extends React.Component<Props> {
|
class BlacklistForm extends React.Component<Props> {
|
||||||
public static defaultProps: Props = {
|
public static defaultProps: Props = {
|
||||||
value: [],
|
value: new Blacklist([]),
|
||||||
onChange: () => {},
|
onChange: () => {},
|
||||||
onBlur: () => {},
|
onBlur: () => {},
|
||||||
};
|
};
|
||||||
|
@ -20,24 +20,22 @@ class BlacklistForm extends React.Component<Props> {
|
||||||
render() {
|
render() {
|
||||||
return <div className='form-blacklist-form'>
|
return <div className='form-blacklist-form'>
|
||||||
{
|
{
|
||||||
this.props.value
|
this.props.value.items.map((item, index) => {
|
||||||
.map((item, index) => {
|
if (item.partial) {
|
||||||
if (typeof item !== 'string') {
|
return null;
|
||||||
// TODO support partial blacklist;
|
}
|
||||||
return null;
|
return <div key={index} className='form-blacklist-form-row'>
|
||||||
}
|
<input data-index={index} type='text' name='url'
|
||||||
return <div key={index} className='form-blacklist-form-row'>
|
className='column-url' value={item.pattern}
|
||||||
<input data-index={index} type='text' name='url'
|
onChange={this.bindValue.bind(this)}
|
||||||
className='column-url' value={item}
|
onBlur={this.props.onBlur}
|
||||||
onChange={this.bindValue.bind(this)}
|
/>
|
||||||
onBlur={this.props.onBlur}
|
<DeleteButton data-index={index} name='delete'
|
||||||
/>
|
onClick={this.bindValue.bind(this)}
|
||||||
<DeleteButton data-index={index} name='delete'
|
onBlur={this.props.onBlur}
|
||||||
onClick={this.bindValue.bind(this)}
|
/>
|
||||||
onBlur={this.props.onBlur}
|
</div>;
|
||||||
/>
|
})
|
||||||
</div>;
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
<AddButton name='add' style={{ float: 'right' }}
|
<AddButton name='add' style={{ float: 'right' }}
|
||||||
onClick={this.bindValue.bind(this)} />
|
onClick={this.bindValue.bind(this)} />
|
||||||
|
@ -47,17 +45,17 @@ class BlacklistForm extends React.Component<Props> {
|
||||||
bindValue(e: any) {
|
bindValue(e: any) {
|
||||||
let name = e.target.name;
|
let name = e.target.name;
|
||||||
let index = e.target.getAttribute('data-index');
|
let index = e.target.getAttribute('data-index');
|
||||||
let next = this.props.value.slice();
|
let items = this.props.value.items;
|
||||||
|
|
||||||
if (name === 'url') {
|
if (name === 'url') {
|
||||||
next[index] = e.target.value;
|
items[index] = new BlacklistItem(e.target.value, false, []);
|
||||||
} else if (name === 'add') {
|
} else if (name === 'add') {
|
||||||
next.push('');
|
items.push(new BlacklistItem('', false, []));
|
||||||
} else if (name === 'delete') {
|
} else if (name === 'delete') {
|
||||||
next.splice(index, 1);
|
items.splice(index, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.props.onChange(next);
|
this.props.onChange(new Blacklist(items));
|
||||||
if (name === 'delete') {
|
if (name === 'delete') {
|
||||||
this.props.onBlur();
|
this.props.onBlur();
|
||||||
}
|
}
|
||||||
|
|
28
src/settings/components/form/PartialBlacklistForm.scss
Normal file
28
src/settings/components/form/PartialBlacklistForm.scss
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
.form-partial-blacklist-form {
|
||||||
|
@mixin row-base {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.column-url {
|
||||||
|
flex: 5;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.column-keys {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.column-delete {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-header {
|
||||||
|
@include row-base;
|
||||||
|
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-row {
|
||||||
|
@include row-base;
|
||||||
|
}
|
||||||
|
}
|
79
src/settings/components/form/PartialBlacklistForm.tsx
Normal file
79
src/settings/components/form/PartialBlacklistForm.tsx
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
import './PartialBlacklistForm.scss';
|
||||||
|
import AddButton from '../ui/AddButton';
|
||||||
|
import DeleteButton from '../ui/DeleteButton';
|
||||||
|
import React from 'react';
|
||||||
|
import Blacklist, { BlacklistItem } from '../../../shared/settings/Blacklist';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
value: Blacklist;
|
||||||
|
onChange: (value: Blacklist) => void;
|
||||||
|
onBlur: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
class PartialBlacklistForm extends React.Component<Props> {
|
||||||
|
public static defaultProps: Props = {
|
||||||
|
value: new Blacklist([]),
|
||||||
|
onChange: () => {},
|
||||||
|
onBlur: () => {},
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <div className='form-partial-blacklist-form'>
|
||||||
|
<div className='form-partial-blacklist-form-header'>
|
||||||
|
<div className='column-url'>URL</div>
|
||||||
|
<div className='column-keys'>Keys</div>
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
this.props.value.items.map((item, index) => {
|
||||||
|
if (!item.partial) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return <div key={index} className='form-partial-blacklist-form-row'>
|
||||||
|
<input data-index={index} type='text' name='url'
|
||||||
|
className='column-url' value={item.pattern}
|
||||||
|
onChange={this.bindValue.bind(this)}
|
||||||
|
onBlur={this.props.onBlur}
|
||||||
|
/>
|
||||||
|
<input data-index={index} type='text' name='keys'
|
||||||
|
className='column-keys' value={item.keys.join(',')}
|
||||||
|
onChange={this.bindValue.bind(this)}
|
||||||
|
onBlur={this.props.onBlur}
|
||||||
|
/>
|
||||||
|
<DeleteButton data-index={index} name='delete'
|
||||||
|
onClick={this.bindValue.bind(this)}
|
||||||
|
onBlur={this.props.onBlur}
|
||||||
|
/>
|
||||||
|
</div>;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
<AddButton name='add' style={{ float: 'right' }}
|
||||||
|
onClick={this.bindValue.bind(this)} />
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
bindValue(e: any) {
|
||||||
|
let name = e.target.name;
|
||||||
|
let index = e.target.getAttribute('data-index');
|
||||||
|
let items = this.props.value.items;
|
||||||
|
|
||||||
|
if (name === 'url') {
|
||||||
|
let current = items[index];
|
||||||
|
items[index] = new BlacklistItem(e.target.value, true, current.keys);
|
||||||
|
} else if (name === 'keys') {
|
||||||
|
let current = items[index];
|
||||||
|
items[index] = new BlacklistItem(
|
||||||
|
current.pattern, true, e.target.value.split(','));
|
||||||
|
} else if (name === 'add') {
|
||||||
|
items.push(new BlacklistItem('', true, []));
|
||||||
|
} else if (name === 'delete') {
|
||||||
|
items.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.props.onChange(new Blacklist(items));
|
||||||
|
if (name === 'delete') {
|
||||||
|
this.props.onBlur();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PartialBlacklistForm;
|
|
@ -6,6 +6,7 @@ import SearchForm from './form/SearchForm';
|
||||||
import KeymapsForm from './form/KeymapsForm';
|
import KeymapsForm from './form/KeymapsForm';
|
||||||
import BlacklistForm from './form/BlacklistForm';
|
import BlacklistForm from './form/BlacklistForm';
|
||||||
import PropertiesForm from './form/PropertiesForm';
|
import PropertiesForm from './form/PropertiesForm';
|
||||||
|
import PartialBlacklistForm from './form/PartialBlacklistForm';
|
||||||
import * as settingActions from '../../settings/actions/setting';
|
import * as settingActions from '../../settings/actions/setting';
|
||||||
import SettingData, {
|
import SettingData, {
|
||||||
FormKeymaps, FormSearch, FormSettings, JSONTextSettings,
|
FormKeymaps, FormSearch, FormSettings, JSONTextSettings,
|
||||||
|
@ -53,7 +54,15 @@ class SettingsComponent extends React.Component<Props> {
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>Blacklist</legend>
|
<legend>Blacklist</legend>
|
||||||
<BlacklistForm
|
<BlacklistForm
|
||||||
value={form.blacklist.toJSON()}
|
value={form.blacklist}
|
||||||
|
onChange={this.bindBlacklistForm.bind(this)}
|
||||||
|
onBlur={this.save.bind(this)}
|
||||||
|
/>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<legend>Partial blacklist</legend>
|
||||||
|
<PartialBlacklistForm
|
||||||
|
value={form.blacklist}
|
||||||
onChange={this.bindBlacklistForm.bind(this)}
|
onChange={this.bindBlacklistForm.bind(this)}
|
||||||
onBlur={this.save.bind(this)}
|
onBlur={this.save.bind(this)}
|
||||||
/>
|
/>
|
||||||
|
@ -138,11 +147,10 @@ class SettingsComponent extends React.Component<Props> {
|
||||||
this.props.dispatch(settingActions.set(data));
|
this.props.dispatch(settingActions.set(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
bindBlacklistForm(value: any) {
|
bindBlacklistForm(blacklist: Blacklist) {
|
||||||
let data = new SettingData({
|
let data = new SettingData({
|
||||||
source: this.props.source,
|
source: this.props.source,
|
||||||
form: (this.props.form as FormSettings).buildWithBlacklist(
|
form: (this.props.form as FormSettings).buildWithBlacklist(blacklist),
|
||||||
Blacklist.fromJSON(value)),
|
|
||||||
});
|
});
|
||||||
this.props.dispatch(settingActions.set(data));
|
this.props.dispatch(settingActions.set(data));
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import Key from './Key';
|
||||||
|
|
||||||
export type BlacklistItemJSON = string | {
|
export type BlacklistItemJSON = string | {
|
||||||
url: string,
|
url: string,
|
||||||
keys: string[],
|
keys: string[],
|
||||||
|
@ -31,7 +33,9 @@ export class BlacklistItem {
|
||||||
|
|
||||||
public readonly keys: string[];
|
public readonly keys: string[];
|
||||||
|
|
||||||
private constructor(
|
private readonly keyEntities: Key[];
|
||||||
|
|
||||||
|
constructor(
|
||||||
pattern: string,
|
pattern: string,
|
||||||
partial: boolean,
|
partial: boolean,
|
||||||
keys: string[]
|
keys: string[]
|
||||||
|
@ -40,6 +44,7 @@ export class BlacklistItem {
|
||||||
this.regex = regexFromWildcard(pattern);
|
this.regex = regexFromWildcard(pattern);
|
||||||
this.partial = partial;
|
this.partial = partial;
|
||||||
this.keys = keys;
|
this.keys = keys;
|
||||||
|
this.keyEntities = this.keys.map(Key.fromMapKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromJSON(raw: any): BlacklistItem {
|
static fromJSON(raw: any): BlacklistItem {
|
||||||
|
@ -81,17 +86,20 @@ export class BlacklistItem {
|
||||||
: this.regex.test(url.host);
|
: this.regex.test(url.host);
|
||||||
}
|
}
|
||||||
|
|
||||||
includeKey(url: URL, keys: string): boolean {
|
includeKey(url: URL, key: Key): boolean {
|
||||||
if (!this.matches(url)) {
|
if (!this.matches(url)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return !this.partial || this.keys.includes(keys);
|
if (!this.partial) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return this.keyEntities.some(k => k.equals(key));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class Blacklist {
|
export default class Blacklist {
|
||||||
constructor(
|
constructor(
|
||||||
private blacklist: BlacklistItem[],
|
public readonly items: BlacklistItem[],
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,14 +112,14 @@ export default class Blacklist {
|
||||||
}
|
}
|
||||||
|
|
||||||
toJSON(): BlacklistJSON {
|
toJSON(): BlacklistJSON {
|
||||||
return this.blacklist.map(item => item.toJSON());
|
return this.items.map(item => item.toJSON());
|
||||||
}
|
}
|
||||||
|
|
||||||
includesEntireBlacklist(url: URL): boolean {
|
includesEntireBlacklist(url: URL): boolean {
|
||||||
return this.blacklist.some(item => !item.partial && item.matches(url));
|
return this.items.some(item => !item.partial && item.matches(url));
|
||||||
}
|
}
|
||||||
|
|
||||||
includeKey(url: URL, key: string) {
|
includeKey(url: URL, key: Key) {
|
||||||
return this.blacklist.some(item => item.includeKey(url, key));
|
return this.items.some(item => item.includeKey(url, key));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import Key from '../../shared/settings/Key';
|
import Key from './Key';
|
||||||
|
|
||||||
export default class KeySequence {
|
export default class KeySequence {
|
||||||
constructor(
|
constructor(
|
||||||
|
|
|
@ -2,13 +2,16 @@ import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import ReactTestRenderer from 'react-test-renderer';
|
import ReactTestRenderer from 'react-test-renderer';
|
||||||
import ReactTestUtils from 'react-dom/test-utils';
|
import ReactTestUtils from 'react-dom/test-utils';
|
||||||
import BlacklistForm from 'settings/components/form/BlacklistForm'
|
import { expect } from 'chai'
|
||||||
|
|
||||||
|
import BlacklistForm from '../../../../src/settings/components/form/BlacklistForm'
|
||||||
|
import Blacklist from '../../../../src/shared/settings/Blacklist';
|
||||||
|
|
||||||
describe("settings/form/BlacklistForm", () => {
|
describe("settings/form/BlacklistForm", () => {
|
||||||
describe('render', () => {
|
describe('render', () => {
|
||||||
it('renders BlacklistForm', () => {
|
it('renders BlacklistForm', () => {
|
||||||
let root = ReactTestRenderer.create(
|
let root = ReactTestRenderer.create(
|
||||||
<BlacklistForm value={['*.slack.com', 'www.google.com/maps']} />,
|
<BlacklistForm value={Blacklist.fromJSON(['*.slack.com', 'www.google.com/maps'])} />,
|
||||||
).root;
|
).root;
|
||||||
|
|
||||||
let children = root.children[0].children;
|
let children = root.children[0].children;
|
||||||
|
@ -43,10 +46,10 @@ describe("settings/form/BlacklistForm", () => {
|
||||||
it('invokes onChange event on edit', (done) => {
|
it('invokes onChange event on edit', (done) => {
|
||||||
ReactTestUtils.act(() => {
|
ReactTestUtils.act(() => {
|
||||||
ReactDOM.render(<BlacklistForm
|
ReactDOM.render(<BlacklistForm
|
||||||
value={['*.slack.com', 'www.google.com/maps*']}
|
value={Blacklist.fromJSON(['*.slack.com', 'www.google.com/maps*'])}
|
||||||
onChange={value => {
|
onChange={value => {
|
||||||
expect(value).to.have.lengthOf(2);
|
let urls = value.items.map(item => item.pattern);
|
||||||
expect(value).to.have.members(['gitter.im', 'www.google.com/maps*']);
|
expect(urls).to.have.members(['gitter.im', 'www.google.com/maps*']);
|
||||||
done();
|
done();
|
||||||
}}
|
}}
|
||||||
/>, container)
|
/>, container)
|
||||||
|
@ -60,10 +63,10 @@ describe("settings/form/BlacklistForm", () => {
|
||||||
it('invokes onChange event on delete', (done) => {
|
it('invokes onChange event on delete', (done) => {
|
||||||
ReactTestUtils.act(() => {
|
ReactTestUtils.act(() => {
|
||||||
ReactDOM.render(<BlacklistForm
|
ReactDOM.render(<BlacklistForm
|
||||||
value={['*.slack.com', 'www.google.com/maps*']}
|
value={Blacklist.fromJSON(['*.slack.com', 'www.google.com/maps*'])}
|
||||||
onChange={value => {
|
onChange={value => {
|
||||||
expect(value).to.have.lengthOf(1);
|
let urls = value.items.map(item => item.pattern);
|
||||||
expect(value).to.have.members(['www.google.com/maps*']);
|
expect(urls).to.have.members(['www.google.com/maps*']);
|
||||||
done();
|
done();
|
||||||
}}
|
}}
|
||||||
/>, container)
|
/>, container)
|
||||||
|
@ -76,10 +79,10 @@ describe("settings/form/BlacklistForm", () => {
|
||||||
it('invokes onChange event on add', (done) => {
|
it('invokes onChange event on add', (done) => {
|
||||||
ReactTestUtils.act(() => {
|
ReactTestUtils.act(() => {
|
||||||
ReactDOM.render(<BlacklistForm
|
ReactDOM.render(<BlacklistForm
|
||||||
value={['*.slack.com']}
|
value={Blacklist.fromJSON(['*.slack.com'])}
|
||||||
onChange={value => {
|
onChange={value => {
|
||||||
expect(value).to.have.lengthOf(2);
|
let urls = value.items.map(item => item.pattern);
|
||||||
expect(value).to.have.members(['*.slack.com', '']);
|
expect(urls).to.have.members(['*.slack.com', '']);
|
||||||
done();
|
done();
|
||||||
}}
|
}}
|
||||||
/>, container);
|
/>, container);
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import Blacklist, { BlacklistItem } from '../../../src/shared/settings/Blacklist';
|
import Blacklist, { BlacklistItem } from '../../../src/shared/settings/Blacklist';
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
|
import Key from '../../../src/shared/settings/Key';
|
||||||
|
|
||||||
describe('BlacklistItem', () => {
|
describe('BlacklistItem', () => {
|
||||||
describe('#fromJSON', () => {
|
describe('#fromJSON', () => {
|
||||||
|
@ -82,11 +83,13 @@ describe('BlacklistItem', () => {
|
||||||
|
|
||||||
describe('#includesPartialKeys', () => {
|
describe('#includesPartialKeys', () => {
|
||||||
it('matches with partial keys', () => {
|
it('matches with partial keys', () => {
|
||||||
let item = BlacklistItem.fromJSON({url: 'google.com', keys: ['j', 'k']});
|
let item = BlacklistItem.fromJSON({url: 'google.com', keys: ['j', 'k', '<C-U>']});
|
||||||
|
|
||||||
expect(item.includeKey(new URL('http://google.com/maps'), 'j')).to.be.true;
|
expect(item.includeKey(new URL('http://google.com/maps'), Key.fromMapKey('j'))).to.be.true;
|
||||||
expect(item.includeKey(new URL('http://google.com/maps'), 'z')).to.be.false;
|
expect(item.includeKey(new URL('http://google.com/maps'), Key.fromMapKey('<C-U>'))).to.be.true;
|
||||||
expect(item.includeKey(new URL('http://maps.google.com/'), 'j')).to.be.false;
|
expect(item.includeKey(new URL('http://google.com/maps'), Key.fromMapKey('z'))).to.be.false;
|
||||||
|
expect(item.includeKey(new URL('http://google.com/maps'), Key.fromMapKey('u'))).to.be.false;
|
||||||
|
expect(item.includeKey(new URL('http://maps.google.com/'), Key.fromMapKey('j'))).to.be.false;
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -147,9 +150,9 @@ describe('Blacklist', () => {
|
||||||
{ url: 'github.com', keys: ['j', 'k'] },
|
{ url: 'github.com', keys: ['j', 'k'] },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(blacklist.includeKey(new URL('https://google.com'), 'j')).to.be.true;
|
expect(blacklist.includeKey(new URL('https://google.com'), Key.fromMapKey('j'))).to.be.true;
|
||||||
expect(blacklist.includeKey(new URL('https://github.com'), 'j')).to.be.true;
|
expect(blacklist.includeKey(new URL('https://github.com'), Key.fromMapKey('j'))).to.be.true;
|
||||||
expect(blacklist.includeKey(new URL('https://github.com'), 'a')).to.be.false;
|
expect(blacklist.includeKey(new URL('https://github.com'), Key.fromMapKey('a'))).to.be.false;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Reference in a new issue