aboutsummaryrefslogtreecommitdiffstats
path: root/webui
diff options
context:
space:
mode:
authorMichael Muré <batolettre@gmail.com>2021-05-24 09:47:55 +0200
committerGitHub <noreply@github.com>2021-05-24 09:47:55 +0200
commit9ded45fe65c9a3af37dc05015a78d5782002e249 (patch)
treea2da60f07da4d66e7d434826edaf212c592d7c19 /webui
parent0cee27665b410dd22e3d075f7179c138bac166ab (diff)
parent7446a20db8907acedbd14ddcc6a99de577268c1c (diff)
downloadgit-bug-9ded45fe65c9a3af37dc05015a78d5782002e249.tar.gz
Merge pull request #658 from GlancingMind/fix-webui-quoted-filter-parameters
WebUI: Fix quoted filter strings
Diffstat (limited to 'webui')
-rw-r--r--webui/src/__tests__/query.ts121
-rw-r--r--webui/src/pages/list/Filter.tsx44
-rw-r--r--webui/src/pages/list/FilterToolbar.tsx13
3 files changed, 136 insertions, 42 deletions
diff --git a/webui/src/__tests__/query.ts b/webui/src/__tests__/query.ts
index 2f04817c..97ec75a6 100644
--- a/webui/src/__tests__/query.ts
+++ b/webui/src/__tests__/query.ts
@@ -7,49 +7,136 @@ it('parses a simple query', () => {
});
it('parses a query with multiple filters', () => {
- expect(parse('foo:bar baz:foo-bar')).toEqual({
+ expect(parse(`foo:bar baz:foo-bar`)).toEqual({
foo: ['bar'],
baz: ['foo-bar'],
});
+
+ expect(parse(`label:abc freetext`)).toEqual({
+ label: [`abc`],
+ freetext: [''],
+ });
+
+ expect(parse(`label:abc with "quotes" 'in' freetext`)).toEqual({
+ label: [`abc`],
+ with: [''],
+ '"quotes"': [''],
+ "'in'": [''],
+ freetext: [''],
+ });
});
it('parses a quoted query', () => {
- expect(parse('foo:"bar"')).toEqual({
- foo: ['bar'],
+ expect(parse(`foo:"bar"`)).toEqual({
+ foo: [`"bar"`],
});
- expect(parse("foo:'bar'")).toEqual({
- foo: ['bar'],
+ expect(parse(`foo:'bar'`)).toEqual({
+ foo: [`'bar'`],
+ });
+
+ expect(parse(`label:'multi word label'`)).toEqual({
+ label: [`'multi word label'`],
+ });
+
+ expect(parse(`label:"multi word label"`)).toEqual({
+ label: [`"multi word label"`],
+ });
+
+ expect(parse(`label:'multi word label with "nested" quotes'`)).toEqual({
+ label: [`'multi word label with "nested" quotes'`],
+ });
+
+ expect(parse(`label:"multi word label with 'nested' quotes"`)).toEqual({
+ label: [`"multi word label with 'nested' quotes"`],
+ });
+
+ expect(parse(`label:"with:quoated:colon"`)).toEqual({
+ label: [`"with:quoated:colon"`],
+ });
+
+ expect(parse(`label:'name ends after this ->' quote'`)).toEqual({
+ label: [`'name ends after this ->'`],
+ "quote'": [``],
});
- expect(parse('foo:\'bar "nested" quotes\'')).toEqual({
- foo: ['bar "nested" quotes'],
+ expect(parse(`label:"name ends after this ->" quote"`)).toEqual({
+ label: [`"name ends after this ->"`],
+ 'quote"': [``],
});
- expect(parse("foo:'escaped\\' quotes'")).toEqual({
- foo: ["escaped' quotes"],
+ expect(parse(`label:'this ->"<- quote belongs to label name'`)).toEqual({
+ label: [`'this ->"<- quote belongs to label name'`],
+ });
+
+ expect(parse(`label:"this ->'<- quote belongs to label name"`)).toEqual({
+ label: [`"this ->'<- quote belongs to label name"`],
+ });
+
+ expect(parse(`label:'names end with'whitespace not with quotes`)).toEqual({
+ label: [`'names end with'whitespace`],
+ not: [``],
+ with: [``],
+ quotes: [``],
+ });
+
+ expect(parse(`label:"names end with"whitespace not with quotes`)).toEqual({
+ label: [`"names end with"whitespace`],
+ not: [``],
+ with: [``],
+ quotes: [``],
+ });
+});
+
+it('should not escape nested quotes', () => {
+ expect(parse(`foo:'do not escape this ->'<- quote'`)).toEqual({
+ foo: [`'do not escape this ->'<-`],
+ "quote'": [``],
+ });
+
+ expect(parse(`foo:'do not escape this ->"<- quote'`)).toEqual({
+ foo: [`'do not escape this ->"<- quote'`],
+ });
+
+ expect(parse(`foo:"do not escape this ->"<- quote"`)).toEqual({
+ foo: [`"do not escape this ->"<-`],
+ 'quote"': [``],
+ });
+
+ expect(parse(`foo:"do not escape this ->'<- quote"`)).toEqual({
+ foo: [`"do not escape this ->'<- quote"`],
});
});
it('parses a query with repetitions', () => {
- expect(parse('foo:bar foo:baz')).toEqual({
+ expect(parse(`foo:bar foo:baz`)).toEqual({
foo: ['bar', 'baz'],
});
});
it('parses a complex query', () => {
- expect(parse('foo:bar foo:baz baz:"foobar" idont:\'know\'')).toEqual({
+ expect(parse(`foo:bar foo:baz baz:"foobar" idont:'know'`)).toEqual({
foo: ['bar', 'baz'],
- baz: ['foobar'],
- idont: ['know'],
+ baz: [`"foobar"`],
+ idont: [`'know'`],
});
});
+it('parses a key:value:value query', () => {
+ expect(parse(`meta:github:"https://github.com/MichaelMure/git-bug"`)).toEqual(
+ {
+ meta: [`github:"https://github.com/MichaelMure/git-bug"`],
+ }
+ );
+});
+
it('quotes values', () => {
- expect(quote('foo')).toEqual('foo');
- expect(quote('foo bar')).toEqual('"foo bar"');
- expect(quote('foo "bar"')).toEqual(`'foo "bar"'`);
- expect(quote(`foo "bar" 'baz'`)).toEqual(`"foo \\"bar\\" 'baz'"`);
+ expect(quote(`foo`)).toEqual(`foo`);
+ expect(quote(`foo bar`)).toEqual(`"foo bar"`);
+ expect(quote(`foo "bar"`)).toEqual(`"foo "bar""`);
+ expect(quote(`foo 'bar'`)).toEqual(`"foo "bar""`);
+ expect(quote(`'foo'`)).toEqual(`"foo"`);
+ expect(quote(`foo "bar" 'baz'`)).toEqual(`"foo "bar" "baz""`);
});
it('stringifies params', () => {
diff --git a/webui/src/pages/list/Filter.tsx b/webui/src/pages/list/Filter.tsx
index 3559b3ce..496fb3ba 100644
--- a/webui/src/pages/list/Filter.tsx
+++ b/webui/src/pages/list/Filter.tsx
@@ -35,52 +35,50 @@ const ITEM_HEIGHT = 48;
export type Query = { [key: string]: string[] };
function parse(query: string): Query {
- // TODO: extract the rest of the query?
const params: Query = {};
-
- // TODO: support escaping without quotes
- const re = /(\w+):([A-Za-z0-9-]+|(["'])(([^\3]|\\.)*)\3)+/g;
+ let re = new RegExp(/([^:\s]+)(:('[^']*'\S*|"[^"]*"\S*|\S*))?/, 'g');
let matches;
while ((matches = re.exec(query)) !== null) {
if (!params[matches[1]]) {
params[matches[1]] = [];
}
-
- let value;
- if (matches[4]) {
- value = matches[4];
+ if (matches[3] !== undefined) {
+ params[matches[1]].push(matches[3]);
} else {
- value = matches[2];
+ params[matches[1]].push('');
}
- value = value.replace(/\\(.)/g, '$1');
- params[matches[1]].push(value);
}
return params;
}
function quote(value: string): string {
- const hasSingle = value.includes("'");
- const hasDouble = value.includes('"');
const hasSpaces = value.includes(' ');
- if (!hasSingle && !hasDouble && !hasSpaces) {
- return value;
- }
+ const isSingleQuotedRegEx = RegExp(/^'.*'$/);
+ const isDoubleQuotedRegEx = RegExp(/^".*"$/);
+ const isQuoted = () =>
+ isDoubleQuotedRegEx.test(value) || isSingleQuotedRegEx.test(value);
- if (!hasDouble) {
- return `"${value}"`;
+ //Test if label name contains whitespace between quotes. If no quoates but
+ //whitespace, then quote string.
+ if (!isQuoted() && hasSpaces) {
+ value = `"${value}"`;
}
- if (!hasSingle) {
- return `'${value}'`;
+ //Convert single quote (tick) to double quote. This way quoting is always
+ //uniform and can be relied upon by the label menu
+ const hasSingle = value.includes(`'`);
+ if (hasSingle) {
+ value = value.replace(/'/g, `"`);
}
- value = value.replace(/"/g, '\\"');
- return `"${value}"`;
+ return value;
}
function stringify(params: Query): string {
const parts: string[][] = Object.entries(params).map(([key, values]) => {
- return values.map((value) => `${key}:${quote(value)}`);
+ return values.map((value) =>
+ value.length > 0 ? `${key}:${quote(value)}` : key
+ );
});
return new Array<string>().concat(...parts).join(' ');
}
diff --git a/webui/src/pages/list/FilterToolbar.tsx b/webui/src/pages/list/FilterToolbar.tsx
index e109578d..4ac579f5 100644
--- a/webui/src/pages/list/FilterToolbar.tsx
+++ b/webui/src/pages/list/FilterToolbar.tsx
@@ -56,6 +56,16 @@ function CountingFilter({ query, children, ...props }: CountingFilterProps) {
);
}
+function quoteLabel(value: string) {
+ const hasUnquotedColon = RegExp(/^[^'"].*:.*[^'"]$/);
+ if (hasUnquotedColon.test(value)) {
+ //quote values which contain a colon but are not quoted.
+ //E.g. abc:abc becomes "abc:abc"
+ return `"${value}"`;
+ }
+ return value;
+}
+
type Props = {
query: string;
queryLocation: (query: string) => LocationDescriptor;
@@ -87,7 +97,7 @@ function FilterToolbar({ query, queryLocation }: Props) {
labelsData.repository.validLabels.nodes
) {
labels = labelsData.repository.validLabels.nodes.map((node) => [
- node.name,
+ quoteLabel(node.name),
node.name,
node.color,
]);
@@ -131,7 +141,6 @@ function FilterToolbar({ query, queryLocation }: Props) {
[key]: [],
});
- // TODO: author/label filters
return (
<Toolbar className={classes.toolbar}>
<CountingFilter