aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--webui/src/__tests__/query.ts61
-rw-r--r--webui/src/pages/list/Filter.tsx44
-rw-r--r--webui/src/pages/list/FilterToolbar.tsx13
3 files changed, 76 insertions, 42 deletions
diff --git a/webui/src/__tests__/query.ts b/webui/src/__tests__/query.ts
index 2f04817c..f8ff6360 100644
--- a/webui/src/__tests__/query.ts
+++ b/webui/src/__tests__/query.ts
@@ -7,49 +7,76 @@ 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'],
});
});
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:abc freetext`)).toEqual({
+ label: [`abc`],
+ freetext: [''],
+ });
+
+ expect(parse(`label:abc with "quotes" in freetext`)).toEqual({
+ label: [`abc`],
+ with: [''],
+ quotes: [''],
+ in: [''],
+ freetext: [''],
+ });
+
+ 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('foo:\'bar "nested" quotes\'')).toEqual({
- foo: ['bar "nested" quotes'],
+ expect(parse(`label:"multi word label with 'nested' quotes"`)).toEqual({
+ label: [`"multi word label with 'nested' quotes"`],
});
- expect(parse("foo:'escaped\\' quotes'")).toEqual({
- foo: ["escaped' quotes"],
+ expect(parse(`foo:'escaped\\' quotes'`)).toEqual({
+ foo: [`'escaped\\' quotes'`],
});
});
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('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..84b08029 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(/(\w+)(:('.*'|".*"|\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 isDoubleQuotedRegEx = RegExp(/^'.*'$/);
+ const isSingleQuotedRegEx = 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