ui(admin): add "working" React admin ui

This commit is contained in:
Matthew Penner 2022-12-15 19:06:14 -07:00
parent d1c7494933
commit 5402584508
No known key found for this signature in database
199 changed files with 13387 additions and 151 deletions

View file

@ -0,0 +1,80 @@
import extractSearchFilters from '@/helpers/extractSearchFilters';
type TestCase = [ string, 0 | Record<string, string[]> ];
describe('@/helpers/extractSearchFilters.ts', function () {
const cases: TestCase[] = [
[ '', {} ],
[ 'hello world', {} ],
[ 'bar:xyz foo:abc', { bar: [ 'xyz' ], foo: [ 'abc' ] } ],
[ 'hello foo:abc', { foo: [ 'abc' ] } ],
[ 'hello foo:abc world another bar:xyz hodor', { foo: [ 'abc' ], bar: [ 'xyz' ] } ],
[ 'foo:1 foo:2 foo: 3 foo:4', { foo: [ '1', '2', '4' ] } ],
[ ' foo:123 foo:bar:123 foo: foo:string', { foo: [ '123', 'bar:123', 'string' ] } ],
[ 'foo:1 bar:2 baz:3', { foo: [ '1' ], bar: [ '2' ] } ],
[ 'hello "world this" is quoted', {} ],
[ 'hello "world foo:123 is" quoted', {} ],
[ 'hello foo:"this is quoted" bar:"this \\"is deeply\\" quoted" world foo:another', {
foo: [ 'this is quoted', 'another' ],
bar: [ 'this "is deeply" quoted' ],
} ],
];
it.each(cases)('should return expected filters: [%s]', function (input, output) {
expect(extractSearchFilters(input, [ 'foo', 'bar' ])).toStrictEqual({
filters: output,
});
});
it('should allow modification of the default parameter', function () {
expect(extractSearchFilters('hello world', [ 'foo' ], { defaultFilter: 'default_param', returnUnmatched: true })).toStrictEqual({
filters: {
default_param: [ 'hello world' ],
},
});
expect(extractSearchFilters('foo:123 bar', [ 'foo' ], { defaultFilter: 'default_param' })).toStrictEqual({
filters: {
foo: [ '123' ],
},
});
});
it.each([
[ '', {} ],
[ 'hello world', { '*': [ 'hello world' ] } ],
[ 'hello world foo:123 bar:456', { foo: [ '123' ], bar: [ '456' ], '*': [ 'hello world' ] } ],
[ 'hello world foo:123 another string', { foo: [ '123' ], '*': [ 'hello world another string' ] } ],
])('should return unmatched parameters: %s', function (input, output) {
expect(extractSearchFilters(input, [ 'foo', 'bar' ], { returnUnmatched: true })).toStrictEqual({
filters: output,
});
});
it.each([
[ '', {} ],
[ 'hello world', { '*': [ 'hello', 'world' ] } ],
[ 'hello world foo:123 bar:456', { foo: [ '123' ], bar: [ '456' ], '*': [ 'hello', 'world' ] } ],
[ 'hello world foo:123 another string', { foo: [ '123' ], '*': [ 'hello', 'world', 'another', 'string' ] } ],
])('should split unmatched parameters: %s', function (input, output) {
expect(extractSearchFilters(input, [ 'foo', 'bar' ], {
returnUnmatched: true,
splitUnmatched: true,
})).toStrictEqual({
filters: output,
});
});
it.each([ true, false ])('should return the unsplit value (splitting: %s)', function (split) {
const extracted = extractSearchFilters('hello foo:123 bar:123 world', [ 'foo' ], {
returnUnmatched: true,
splitUnmatched: split,
});
expect(extracted).toStrictEqual({
filters: {
foo: [ '123' ],
'*': split ? [ 'hello', 'bar:123', 'world' ] : [ 'hello bar:123 world' ],
},
});
});
});

View file

@ -0,0 +1,49 @@
import { QueryBuilderParams } from '@/api/http';
import splitStringWhitespace from '@/helpers/splitStringWhitespace';
interface Options<D extends string = string> {
defaultFilter?: D;
splitUnmatched?: boolean;
returnUnmatched?: boolean;
}
const extractSearchFilters = <T extends string, D extends string = '*'> (
str: string,
params: Readonly<T[]>,
options?: Options<D>,
): QueryBuilderParams<T> | QueryBuilderParams<D> | QueryBuilderParams<T & D> => {
const opts: Required<Options<D>> = {
defaultFilter: options?.defaultFilter || '*' as D,
splitUnmatched: options?.splitUnmatched || false,
returnUnmatched: options?.returnUnmatched || false,
};
const filters: Map<T, string[]> = new Map();
const unmatched: string[] = [];
for (const segment of splitStringWhitespace(str)) {
const parts = segment.split(':');
const filter = parts[0] as T;
const value = parts.slice(1).join(':');
if (!filter || (parts.length > 1 && filter && !value)) {
// do nothing
} else if (!params.includes(filter)) {
unmatched.push(segment);
} else {
filters.set(filter, [ ...(filters.get(filter) || []), value ]);
}
}
if (opts.returnUnmatched && str.trim().length > 0) {
filters.set(opts.defaultFilter as any, opts.splitUnmatched ? unmatched : [ unmatched.join(' ') ]);
}
if (filters.size === 0) {
return { filters: {} };
}
// @ts-expect-error
return { filters: Object.fromEntries(filters) };
};
export default extractSearchFilters;

View file

@ -0,0 +1,16 @@
import splitStringWhitespace from '@/helpers/splitStringWhitespace';
describe('@/helpers/splitStringWhitespace.ts', function () {
it.each([
[ '', [] ],
[ 'hello world', [ 'hello', 'world' ] ],
[ ' hello world ', [ 'hello', 'world' ] ],
[ 'hello123 world 123 $$ s ', [ 'hello123', 'world', '123', '$$', 's' ] ],
[ 'hello world! how are you?', [ 'hello', 'world!', 'how', 'are', 'you?' ] ],
[ 'hello "foo bar baz" world', [ 'hello', 'foo bar baz', 'world' ] ],
[ 'hello "foo \\"bar bar \\" baz" world', [ 'hello', 'foo "bar bar " baz', 'world' ] ],
[ 'hello "foo "bar baz" baz" world', [ 'hello', 'foo bar', 'baz baz', 'world' ] ],
])('should handle string: %s', function (input, output) {
expect(splitStringWhitespace(input)).toStrictEqual(output);
});
});

View file

@ -0,0 +1,27 @@
/**
* Takes a string and splits it into an array by whitespace, ignoring any
* text that is wrapped in quotes. You must escape quotes within a quoted
* string, otherwise it will just split on those.
*
* Derived from https://stackoverflow.com/a/46946420
*/
export default (str: string): string[] => {
let quoted = false;
const parts = [ '' ] as string[];
for (const char of (str.trim().match(/\\?.|^$/g) || [])) {
if (char === '"') {
quoted = !quoted;
} else if (!quoted && char === ' ') {
parts.push('');
} else {
parts[Math.max(parts.length - 1, 0)] += char.replace(/\\(.)/, '$1');
}
}
if (parts.length === 1 && parts[0] === '') {
return [];
}
return parts;
};