diff --git a/.yarn/cache/@isaacs-cliui-npm-8.0.2-f4364666d5-4a473b9b32.zip b/.yarn/cache/@isaacs-cliui-npm-8.0.2-f4364666d5-4a473b9b32.zip new file mode 100644 index 00000000..d19176fa Binary files /dev/null and b/.yarn/cache/@isaacs-cliui-npm-8.0.2-f4364666d5-4a473b9b32.zip differ diff --git a/.yarn/cache/@pkgjs-parseargs-npm-0.11.0-cd2a3fe948-6ad6a00fc4.zip b/.yarn/cache/@pkgjs-parseargs-npm-0.11.0-cd2a3fe948-6ad6a00fc4.zip new file mode 100644 index 00000000..96f576f7 Binary files /dev/null and b/.yarn/cache/@pkgjs-parseargs-npm-0.11.0-cd2a3fe948-6ad6a00fc4.zip differ diff --git a/.yarn/cache/ansi-regex-npm-6.0.1-8d663a607d-1ff8b7667c.zip b/.yarn/cache/ansi-regex-npm-6.0.1-8d663a607d-1ff8b7667c.zip new file mode 100644 index 00000000..088e552d Binary files /dev/null and b/.yarn/cache/ansi-regex-npm-6.0.1-8d663a607d-1ff8b7667c.zip differ diff --git a/.yarn/cache/ansi-styles-npm-6.2.1-d43647018c-ef940f2f0c.zip b/.yarn/cache/ansi-styles-npm-6.2.1-d43647018c-ef940f2f0c.zip new file mode 100644 index 00000000..aa1bdfde Binary files /dev/null and b/.yarn/cache/ansi-styles-npm-6.2.1-d43647018c-ef940f2f0c.zip differ diff --git a/.yarn/cache/eastasianwidth-npm-0.2.0-c37eb16bd1-7d00d7cd8e.zip b/.yarn/cache/eastasianwidth-npm-0.2.0-c37eb16bd1-7d00d7cd8e.zip new file mode 100644 index 00000000..10385995 Binary files /dev/null and b/.yarn/cache/eastasianwidth-npm-0.2.0-c37eb16bd1-7d00d7cd8e.zip differ diff --git a/.yarn/cache/emoji-regex-npm-9.2.2-e6fac8d058-8487182da7.zip b/.yarn/cache/emoji-regex-npm-9.2.2-e6fac8d058-8487182da7.zip new file mode 100644 index 00000000..e6b0ab4d Binary files /dev/null and b/.yarn/cache/emoji-regex-npm-9.2.2-e6fac8d058-8487182da7.zip differ diff --git a/.yarn/cache/foreground-child-npm-3.1.1-77e78ed774-139d270bc8.zip b/.yarn/cache/foreground-child-npm-3.1.1-77e78ed774-139d270bc8.zip new file mode 100644 index 00000000..a288850f Binary files /dev/null and b/.yarn/cache/foreground-child-npm-3.1.1-77e78ed774-139d270bc8.zip differ diff --git a/.yarn/cache/glob-npm-10.3.3-2d9abea8c7-29190d3291.zip b/.yarn/cache/glob-npm-10.3.3-2d9abea8c7-29190d3291.zip new file mode 100644 index 00000000..e427b21f Binary files /dev/null and b/.yarn/cache/glob-npm-10.3.3-2d9abea8c7-29190d3291.zip differ diff --git a/.yarn/cache/jackspeak-npm-2.2.3-bcc5afec11-8add557045.zip b/.yarn/cache/jackspeak-npm-2.2.3-bcc5afec11-8add557045.zip new file mode 100644 index 00000000..b3c4c2d0 Binary files /dev/null and b/.yarn/cache/jackspeak-npm-2.2.3-bcc5afec11-8add557045.zip differ diff --git a/.yarn/cache/lru-cache-npm-10.0.1-0e1abf4c13-06f8d0e1ce.zip b/.yarn/cache/lru-cache-npm-10.0.1-0e1abf4c13-06f8d0e1ce.zip new file mode 100644 index 00000000..5e58076e Binary files /dev/null and b/.yarn/cache/lru-cache-npm-10.0.1-0e1abf4c13-06f8d0e1ce.zip differ diff --git a/.yarn/cache/minimatch-npm-9.0.3-69d7d6fad5-253487976b.zip b/.yarn/cache/minimatch-npm-9.0.3-69d7d6fad5-253487976b.zip new file mode 100644 index 00000000..265822fc Binary files /dev/null and b/.yarn/cache/minimatch-npm-9.0.3-69d7d6fad5-253487976b.zip differ diff --git a/.yarn/cache/minipass-npm-7.0.3-3b57909ee9-6f1614f5b5.zip b/.yarn/cache/minipass-npm-7.0.3-3b57909ee9-6f1614f5b5.zip new file mode 100644 index 00000000..3471604a Binary files /dev/null and b/.yarn/cache/minipass-npm-7.0.3-3b57909ee9-6f1614f5b5.zip differ diff --git a/.yarn/cache/path-scurry-npm-1.10.1-52bd946f2e-e2557cff3a.zip b/.yarn/cache/path-scurry-npm-1.10.1-52bd946f2e-e2557cff3a.zip new file mode 100644 index 00000000..e466a978 Binary files /dev/null and b/.yarn/cache/path-scurry-npm-1.10.1-52bd946f2e-e2557cff3a.zip differ diff --git a/.yarn/cache/rimraf-npm-5.0.1-26fb251a1d-bafce85391.zip b/.yarn/cache/rimraf-npm-5.0.1-26fb251a1d-bafce85391.zip new file mode 100644 index 00000000..3454cf88 Binary files /dev/null and b/.yarn/cache/rimraf-npm-5.0.1-26fb251a1d-bafce85391.zip differ diff --git a/.yarn/cache/signal-exit-npm-4.1.0-61fb957687-64c757b498.zip b/.yarn/cache/signal-exit-npm-4.1.0-61fb957687-64c757b498.zip new file mode 100644 index 00000000..7242dbce Binary files /dev/null and b/.yarn/cache/signal-exit-npm-4.1.0-61fb957687-64c757b498.zip differ diff --git a/.yarn/cache/string-width-npm-5.1.2-bf60531341-7369deaa29.zip b/.yarn/cache/string-width-npm-5.1.2-bf60531341-7369deaa29.zip new file mode 100644 index 00000000..bd884056 Binary files /dev/null and b/.yarn/cache/string-width-npm-5.1.2-bf60531341-7369deaa29.zip differ diff --git a/.yarn/cache/strip-ansi-npm-7.1.0-7453b80b79-859c73fcf2.zip b/.yarn/cache/strip-ansi-npm-7.1.0-7453b80b79-859c73fcf2.zip new file mode 100644 index 00000000..2cc856e8 Binary files /dev/null and b/.yarn/cache/strip-ansi-npm-7.1.0-7453b80b79-859c73fcf2.zip differ diff --git a/.yarn/cache/wrap-ansi-npm-8.1.0-26a4e6ae28-371733296d.zip b/.yarn/cache/wrap-ansi-npm-8.1.0-26a4e6ae28-371733296d.zip new file mode 100644 index 00000000..2ee78f31 Binary files /dev/null and b/.yarn/cache/wrap-ansi-npm-8.1.0-26a4e6ae28-371733296d.zip differ diff --git a/packages/metacore/src/metafilters/interfaces.ts b/packages/metacore/src/metafilters/interfaces.ts index 33c19e3c..f1dd660d 100644 --- a/packages/metacore/src/metafilters/interfaces.ts +++ b/packages/metacore/src/metafilters/interfaces.ts @@ -18,7 +18,7 @@ type CustomFunction = Nullable; * LT - Less than * LE - Less than or equal to */ -export type Operator = Nullable<'EQ' | 'LIKE' | 'GT' | 'LT' | 'GE' | 'LE' | 'NE'>; +export type Operator = Nullable<'EQ' | 'LIKE' | 'GT' | 'LT' | 'GE' | 'LE' | 'NE' | 'EMPTY' | 'NONEMPTY'>; export interface IBooleanInput { type: 'boolean'; diff --git a/packages/metacore/src/metaforms/interfaces.ts b/packages/metacore/src/metaforms/interfaces.ts index 78951bc0..c72b1eed 100644 --- a/packages/metacore/src/metaforms/interfaces.ts +++ b/packages/metacore/src/metaforms/interfaces.ts @@ -42,11 +42,17 @@ interface CommonProps { visible?: { targetName: string, value: any }; } +export type MultiSelectParent = {children: MultiSelectOptions, label: string}; +export type MultiSelectLeaf = { value: string | number; label: string }; + +export type MultiSelectOption = MultiSelectLeaf | MultiSelectParent; +export type MultiSelectOptions = Array + export interface MultiSelectMetaProps extends CommonProps { type: 'multiselect'; disabled?: boolean; placeholder?: string; - options?: Array<{ value: string | number; label: string }>; + options?: MultiSelectOptions; showSelectedCounter?: boolean; showFilterInput?: boolean; } diff --git a/packages/metafilters/package.json b/packages/metafilters/package.json index 01adc8f9..a76d95ed 100644 --- a/packages/metafilters/package.json +++ b/packages/metafilters/package.json @@ -22,7 +22,7 @@ "url": "https://github.com/flsy/meta/issues" }, "scripts": { - "build": "rm -rf ./lib && tsc -b", + "build": "rimraf ./lib && tsc -b", "test": "jest", "prepare": "yarn build", "prepublishOnly": "yarn test", @@ -40,6 +40,7 @@ "@types/jest": "^29.0.0", "@types/sqlite3": "^3.1.6", "jest": "^27.0.6", + "rimraf": "^5.0.1", "sqlite3": "^5.0.0", "ts-jest": "^27.0.4" }, diff --git a/packages/metafilters/src/__tests__/common.spec.ts b/packages/metafilters/src/__tests__/common.spec.ts index 82f85baf..071d918d 100644 --- a/packages/metafilters/src/__tests__/common.spec.ts +++ b/packages/metafilters/src/__tests__/common.spec.ts @@ -1,6 +1,6 @@ import metafilters from '../index'; -import { all, close } from '../sqliteUtils'; -import { exampleColumn, seed } from '../testData'; +import { all, close } from '../testHelpers/sqliteUtils'; +import { seed, exampleColumn } from '../testHelpers/testData'; describe('common code', () => { it('map records to table column names', async () => { @@ -20,7 +20,9 @@ describe('common code', () => { { id: 2, firstName: 'Alpha', lastName: 'Tree', age: 2, isValid: 1 }, { id: 3, firstName: 'Beta', lastName: 'Woods', age: 30, isValid: 1 }, { id: 5, firstName: 'Carol', lastName: 'RainForest', age: 18, isValid: 1 }, - { id: 6, firstName: '', lastName: 'FirstNameNull', age: 18, isValid: 1 }, + { id: 6, firstName: '', lastName: 'FirstNameEmptyStr', age: 18, isValid: 1 }, + { id: 7, firstName: null, lastName: 'FirstNameNull', age: 18, isValid: 1 }, + ]); }); it('map use all columns if not provided', async () => { diff --git a/packages/metafilters/src/__tests__/filters/combined.spec.ts b/packages/metafilters/src/__tests__/filters/combined.spec.ts index 3147010b..21bc2bd3 100644 --- a/packages/metafilters/src/__tests__/filters/combined.spec.ts +++ b/packages/metafilters/src/__tests__/filters/combined.spec.ts @@ -1,6 +1,6 @@ -import { exampleColumn, seed } from '../../testData'; import metafilters from '../../index'; -import { all, close, get } from '../../sqliteUtils'; +import { all, close, get } from '../../testHelpers/sqliteUtils'; +import { seed, exampleColumn } from '../../testHelpers/testData'; describe('filters combined', () => { diff --git a/packages/metafilters/src/__tests__/filters/numberFilter.spec.ts b/packages/metafilters/src/__tests__/filters/numberFilter.spec.ts index c6f1daa4..08264378 100644 --- a/packages/metafilters/src/__tests__/filters/numberFilter.spec.ts +++ b/packages/metafilters/src/__tests__/filters/numberFilter.spec.ts @@ -1,6 +1,6 @@ -import { exampleColumn, seed } from '../../testData'; import metafilters from '../../index'; -import { all, get } from '../../sqliteUtils'; +import { all, get } from '../../testHelpers/sqliteUtils'; +import { seed, exampleColumn } from '../../testHelpers/testData'; describe('number filter', () => { it('filter number by like operator', async () => { diff --git a/packages/metafilters/src/__tests__/filters/stringsFilter.spec.ts b/packages/metafilters/src/__tests__/filters/stringsFilter.spec.ts index b6adca1d..9803bbe6 100644 --- a/packages/metafilters/src/__tests__/filters/stringsFilter.spec.ts +++ b/packages/metafilters/src/__tests__/filters/stringsFilter.spec.ts @@ -1,6 +1,6 @@ -import { exampleColumn, seed } from '../../testData'; import metafilters from '../../index'; -import { all, close, get } from '../../sqliteUtils'; +import { all, close, get } from '../../testHelpers/sqliteUtils'; +import { seed, exampleColumn, seededData } from '../../testHelpers/testData'; describe('strings filter', () => { it('should return all when empty filter', async () => { @@ -23,8 +23,8 @@ describe('strings filter', () => { nodes: 'SELECT "id", "firstName", "lastName", "age", "isValid" FROM "person-dash";', }); - expect(count).toEqual({ count: 5 }); - expect(nodes.length).toEqual(5); + expect(count).toEqual({ count: seededData.length }); + expect(nodes.length).toEqual(seededData.length); }); it('should filter', async () => { @@ -196,8 +196,8 @@ describe('strings filter', () => { const nodes = await all(db, filters.nodes); await db.close(); - expect(count).toEqual({ count: 4 }); - expect(nodes.map(n => (n as any).lastName)).toMatchObject(['Tree', 'Woods', 'RainForest', 'FirstNameNull']); + expect(count).toEqual({ count: 5 }); + expect(nodes.map(n => (n as any).lastName)).toMatchObject(['Tree', 'Woods', 'RainForest', 'FirstNameEmptyStr', 'FirstNameNull']); }); it('filter by string default LIKE', async () => { @@ -220,7 +220,7 @@ describe('strings filter', () => { expect(nodes).toMatchObject([{ firstName: 'Alpha' }, { firstName: 'Beta' }, { firstName: 'Carol' }]); }); - it('filter by string with null value (which means: give me eerything)', async () => { + it('filter by string with null value (which means: give me everything)', async () => { const db = await seed(); const response = await metafilters('person-dash', exampleColumn, { filters: { firstName: { type: 'string', filters: [{ value: null }] } }, @@ -235,8 +235,8 @@ describe('strings filter', () => { nodes: 'SELECT "id", "firstName", "lastName", "age", "isValid" FROM "person-dash" WHERE coalesce("firstName", \'\') like \'%%\';', }); - expect(count).toEqual({ count: 5 }); - expect(nodes.length).toEqual(5); + expect(count).toEqual({ count: seededData.length }); + expect(nodes.length).toEqual(seededData.length); }); it('handles undefined filters', async () => { @@ -254,8 +254,8 @@ describe('strings filter', () => { nodes: 'SELECT "id", "firstName", "lastName", "age", "isValid" FROM "person-dash";', }); - expect(count).toEqual({ count: 5 }); - expect(nodes.length).toEqual(5); + expect(count).toEqual({ count: seededData.length }); + expect(nodes.length).toEqual(seededData.length); }); it('filter by string with user defined function', async () => { @@ -277,4 +277,45 @@ describe('strings filter', () => { expect(nodes.length).toEqual(1); expect(nodes).toMatchObject([{ firstName: 'Beta' }]); }); + + + it('filter by EMPTY operator', async () => { + const db = await seed(); + const response = await metafilters('person-dash', exampleColumn, { + filters: { firstName: { type: 'string', filters: [{operator: 'EMPTY', value: null}] } }, + }); + + const count = await get(db, response.count); + const nodes = await all(db, response.nodes); + await close(db); + + expect(response).toMatchObject({ + count: 'SELECT COUNT(*) as count FROM "person-dash" WHERE "firstName" is null;', + nodes: 'SELECT "id", "firstName", "lastName", "age", "isValid" FROM "person-dash" WHERE "firstName" is null;'} + ); + + expect(count).toEqual({ count: 1 }); + expect(nodes.length).toEqual(1); + expect(nodes).toMatchObject([{ firstName: null }]); + }); + + it('filter by NONEMPTY operator', async () => { + const db = await seed(); + const response = await metafilters('person-dash', exampleColumn, { + filters: { firstName: { type: 'string', filters: [{operator: 'NONEMPTY', value: null}] } }, + }); + + const count = await get(db, response.count); + const nodes = await all(db, response.nodes); + await close(db); + + expect(response).toMatchObject({ + count: 'SELECT COUNT(*) as count FROM "person-dash" WHERE "firstName" is not null;', + nodes: 'SELECT "id", "firstName", "lastName", "age", "isValid" FROM "person-dash" WHERE "firstName" is not null;'} + ); + + expect(count).toEqual({ count: 5 }); + expect(nodes.length).toEqual(5); + }); + }); \ No newline at end of file diff --git a/packages/metafilters/src/__tests__/limit.spec.ts b/packages/metafilters/src/__tests__/limit.spec.ts index 9edb49f9..7f86a656 100644 --- a/packages/metafilters/src/__tests__/limit.spec.ts +++ b/packages/metafilters/src/__tests__/limit.spec.ts @@ -1,6 +1,6 @@ import metafilters from '../index'; -import { all, close, get } from '../sqliteUtils'; -import { exampleColumn, seed } from '../testData'; +import { all, close, get } from '../testHelpers/sqliteUtils'; +import { seed, exampleColumn, seededData } from '../testHelpers/testData'; describe('limit', () => { it('return all records when no limit specified', async () => { @@ -11,9 +11,9 @@ describe('limit', () => { const nodes = await all(db, response.nodes); await close(db); - expect(count).toEqual({ count: 5 }); + expect(count).toEqual({ count: seededData.length }); expect(nodes[0]).toEqual({ id: 1, firstName: 'Joe', lastName: 'Forest', age: 52, isValid: 0 }); - expect(nodes.length).toEqual(5); + expect(nodes.length).toEqual(seededData.length); }); it('return all records when limit is larger than record count', async () => { @@ -22,8 +22,8 @@ describe('limit', () => { const count = await get(db, response.count); const nodes = await all(db, response.nodes); await close(db); - expect(count).toEqual({ count: 5 }); - expect(nodes.length).toEqual(5); + expect(count).toEqual({ count: seededData.length }); + expect(nodes.length).toEqual(seededData.length); }); it('limits returned records to 2', async () => { @@ -34,7 +34,7 @@ describe('limit', () => { const nodes = await all(db, response.nodes); await close(db); - expect(count).toEqual({ count: 5 }); + expect(count).toEqual({ count: seededData.length }); expect(nodes.length).toEqual(2); }); }); diff --git a/packages/metafilters/src/__tests__/pagination.spec.ts b/packages/metafilters/src/__tests__/pagination.spec.ts index 87c8eaba..25717a9e 100644 --- a/packages/metafilters/src/__tests__/pagination.spec.ts +++ b/packages/metafilters/src/__tests__/pagination.spec.ts @@ -1,6 +1,6 @@ -import { exampleColumn, seed } from '../testData'; import metafilters from '../index'; -import { all, close, get } from '../sqliteUtils'; +import { all, close, get } from '../testHelpers/sqliteUtils'; +import { seed, exampleColumn, seededData } from '../testHelpers/testData'; describe('pagination', () => { it('paginate sorted by keyColumn ASC', async () => { @@ -10,7 +10,7 @@ describe('pagination', () => { const count = await get(db, response1.count); const nodes = await all(db, response1.nodes); - expect(count).toEqual({ count: 5}); + expect(count).toEqual({ count: seededData.length}); expect(nodes.length).toEqual(3); expect(nodes).toEqual([ @@ -22,8 +22,11 @@ describe('pagination', () => { const response2 = await metafilters('person-dash', exampleColumn, { page: 1, limit: 3 }); const nodes2 = await all(db, response2.nodes); await close(db); - expect(nodes2.length).toEqual(2); - expect(nodes2).toEqual([{ id: 5, firstName: 'Carol', lastName: 'RainForest', isValid: 1, age: 18 }, { id: 6, firstName: '', lastName: 'FirstNameNull', isValid: 1, age: 18 }]); + expect(nodes2.length).toEqual(3); + expect(nodes2).toEqual([ + { id: 5, firstName: 'Carol', lastName: 'RainForest', isValid: 1, age: 18 }, + { id: 6, firstName: '', lastName: 'FirstNameEmptyStr', isValid: 1, age: 18 }, + { id: 7, firstName: null, lastName: 'FirstNameNull', isValid: 1, age: 18 }]); }); it('paginate sorted by keyColumn DESC', async () => { @@ -33,21 +36,21 @@ describe('pagination', () => { const count = await get(db, response1.count); const nodes = await all(db, response1.nodes); - expect(count).toEqual({ count: 5 }); + expect(count).toEqual({ count: seededData.length }); expect(nodes.length).toEqual(3); expect(nodes).toEqual([ - { id: 6, firstName: '', lastName: 'FirstNameNull', isValid: 1, age: 18 }, + { id: 7, firstName: null, lastName: 'FirstNameNull', isValid: 1, age: 18 }, + { id: 6, firstName: '', lastName: 'FirstNameEmptyStr', isValid: 1, age: 18 }, { id: 5, firstName: 'Carol', lastName: 'RainForest', isValid: 1, age: 18 }, - { id: 3, firstName: 'Beta', lastName: 'Woods', isValid: 1, age: 30 }, ]); const response2 = await metafilters( 'person-dash', exampleColumn, { page: 1, limit: 3, sort: { id: 'DESC' } }); const nodes2 = await all(db, response2.nodes); await close(db); - expect(nodes2.length).toEqual(2); - expect(nodes2).toEqual([{ id: 2, age: 2, firstName: 'Alpha', isValid: 1, lastName: 'Tree' }, { id: 1, age: 52, firstName: 'Joe', isValid: 0, lastName: 'Forest' }]); + expect(nodes2.length).toEqual(3); + expect(nodes2).toEqual([{ id: 3, age: 30, firstName: 'Beta', isValid: 1, lastName: 'Woods' }, { id: 2, age: 2, firstName: 'Alpha', isValid: 1, lastName: 'Tree' }, { id: 1, age: 52, firstName: 'Joe', isValid: 0, lastName: 'Forest' }]); }); it('return empty nodes with page number outside stored data', async () => { @@ -57,7 +60,7 @@ describe('pagination', () => { const count = await get(db, response1.count); const nodes = await all(db, response1.nodes); - expect(count).toEqual({ count: 5 }); + expect(count).toEqual({ count: seededData.length }); expect(nodes.length).toEqual(0); expect(nodes).toEqual([]); }); @@ -69,14 +72,14 @@ describe('pagination', () => { const count = await get(db, response1.count); const nodes = await all(db, response1.nodes); - expect(count).toEqual({ count: 5 }); - expect(nodes.length).toEqual(5); + expect(count).toEqual({ count: seededData.length }); + expect(nodes.length).toEqual(seededData.length); const response2 = await metafilters( 'person-dash', exampleColumn, { page: 10, sort: { firstName: 'DESC' } }); const count2 = await get(db, response2.count); const nodes2 = await all(db, response2.nodes); - expect(count2).toEqual({ count: 5 }); - expect(nodes2.length).toEqual(5); + expect(count2).toEqual({ count: seededData.length }); + expect(nodes2.length).toEqual(seededData.length); }); }); diff --git a/packages/metafilters/src/__tests__/sort.spec.ts b/packages/metafilters/src/__tests__/sort.spec.ts index 82af4c2d..10068173 100644 --- a/packages/metafilters/src/__tests__/sort.spec.ts +++ b/packages/metafilters/src/__tests__/sort.spec.ts @@ -1,6 +1,6 @@ import metafilters from '../index'; -import { close, get } from '../sqliteUtils'; -import { exampleColumn, seed } from '../testData'; +import { close, get } from '../testHelpers/sqliteUtils'; +import { seed, exampleColumn, seededData } from '../testHelpers/testData'; describe('sort', () => { it('return records sorted by string DESC', async () => { @@ -15,7 +15,7 @@ describe('sort', () => { nodes: 'SELECT "id", "firstName", "lastName", "age", "isValid" FROM "person-dash" ORDER BY "firstName" DESC;', }); - expect(count).toEqual({ count: 5 }); + expect(count).toEqual({ count: seededData.length }); }); it('handles undefined sort', async () => { const db = await seed(); @@ -29,6 +29,6 @@ describe('sort', () => { nodes: 'SELECT "id", "firstName", "lastName", "age", "isValid" FROM "person-dash";', }); - expect(count).toEqual({ count: 5 }); + expect(count).toEqual({ count: seededData.length }); }); }); diff --git a/packages/metafilters/src/filters.ts b/packages/metafilters/src/filters.ts index 4c814b71..f80b4439 100644 --- a/packages/metafilters/src/filters.ts +++ b/packages/metafilters/src/filters.ts @@ -21,6 +21,12 @@ const numberFilter = (name: string, filter: INumberInput): string[] => } return [columnName, '!=', value].join(' '); } + case 'EMPTY': { + return `${columnName} is null`; + } + case 'NONEMPTY': { + return `${columnName} is not null`; + } case 'EQ': default: if (value === null) { @@ -48,6 +54,12 @@ const stringOperator = (columnName: string, value: Nullable, operator?: } return `${columnName} != '${value}'`; } + case 'EMPTY': { + return `${columnName} is null`; + } + case 'NONEMPTY': { + return `${columnName} is not null`; + } case 'LIKE': default: { // TODO Zde by měly být rozlišeny dva stavy. diff --git a/packages/metafilters/src/sqliteUtils.ts b/packages/metafilters/src/testHelpers/sqliteUtils.ts similarity index 100% rename from packages/metafilters/src/sqliteUtils.ts rename to packages/metafilters/src/testHelpers/sqliteUtils.ts diff --git a/packages/metafilters/src/testData.ts b/packages/metafilters/src/testHelpers/testData.ts similarity index 60% rename from packages/metafilters/src/testData.ts rename to packages/metafilters/src/testHelpers/testData.ts index 23373a6e..5f9b0ac9 100644 --- a/packages/metafilters/src/testData.ts +++ b/packages/metafilters/src/testHelpers/testData.ts @@ -2,18 +2,20 @@ import { getDatabase, prepare, run } from './sqliteUtils'; export const exampleColumn = ['id', 'firstName', 'lastName', 'age', 'isValid']; +export const seededData = [ + [1, 'Joe', 'Forest', 52, false], + [2, 'Alpha', 'Tree', 2, true], + [3, 'Beta', 'Woods', 30, true], + [5, 'Carol', 'RainForest', 18, true], + [6, '', 'FirstNameEmptyStr', 18, true], + [7, null, 'FirstNameNull', 18, true], + +]; export const seed = async () => { const db = await getDatabase(':memory:'); await run(db, 'CREATE TABLE "person-dash" (id INT, firstName VARCHAR, lastName VARCHAR, age NUMBER, isValid BOOLEAN)'); - await prepare(db, 'INSERT INTO "person-dash" VALUES (?, ?, ?, ?, ?)', [ - [1, 'Joe', 'Forest', 52, false], - [2, 'Alpha', 'Tree', 2, true], - [3, 'Beta', 'Woods', 30, true], - [5, 'Carol', 'RainForest', 18, true], - [6, '', 'FirstNameNull', 18, true], - - ]); + await prepare(db, 'INSERT INTO "person-dash" VALUES (?, ?, ?, ?, ?)', seededData); return db; }; diff --git a/packages/react-metaforms/src/antd/FormComponents.stories.tsx b/packages/react-metaforms/src/antd/FormComponents.stories.tsx index 157ae66a..ffda85c1 100644 --- a/packages/react-metaforms/src/antd/FormComponents.stories.tsx +++ b/packages/react-metaforms/src/antd/FormComponents.stories.tsx @@ -12,7 +12,10 @@ import { getSelectMeta, getSubmitMeta, getTextareaMeta, - getTextMeta, getSegmentedSwitch, minlengthRule, requiredRule, + getTextMeta, + getSegmentedSwitch, + minlengthRule, + requiredRule, } from 'metaforms'; import React from 'react'; import Form from './Form'; @@ -34,9 +37,7 @@ export const Number = (args) => ( label: args.label, disabled: args.disabled, placeholder: args.placeholder, - validation: [ - requiredRule('Required field'), - ], + validation: [requiredRule('Required field')], }), getSubmitMeta({ name: 'submit', @@ -61,10 +62,7 @@ export const Text = (args) => ( label: args.label, disabled: args.disabled, placeholder: args.placeholder, - validation: [ - requiredRule('Required field'), - minlengthRule('Enter at least 2 characters', 2) - ], + validation: [requiredRule('Required field'), minlengthRule('Enter at least 2 characters', 2)], }), getSubmitMeta({ name: 'submit', @@ -89,10 +87,7 @@ export const Password = (args) => ( label: args.label, disabled: args.disabled, placeholder: args.placeholder, - validation: [ - requiredRule('Required field'), - minlengthRule('Enter at least 2 characters', 2) - ], + validation: [requiredRule('Required field'), minlengthRule('Enter at least 2 characters', 2)], }), getSubmitMeta({ name: 'submit', @@ -118,10 +113,7 @@ export const Textarea = (args) => ( disabled: args.disabled, placeholder: args.placeholder, rows: args.rows, - validation: [ - requiredRule('Required field'), - minlengthRule('Enter at least 2 characters', 2) - ], + validation: [requiredRule('Required field'), minlengthRule('Enter at least 2 characters', 2)], }), getSubmitMeta({ name: 'submit', @@ -145,9 +137,7 @@ export const Image = (args) => ( getImageMeta({ name: 'image', label: args.label, - validation: [ - requiredRule('Required field'), - ], + validation: [requiredRule('Required field')], }), getSubmitMeta({ name: 'submit', @@ -169,9 +159,7 @@ export const File = (args) => ( name: 'file', label: args.label, accept: args.accept, - validation: [ - requiredRule('Required field'), - ], + validation: [requiredRule('Required field')], }), getSubmitMeta({ name: 'submit', @@ -196,9 +184,7 @@ export const Date = (args) => ( disabled: args.disabled, placeholder: args.placeholder, withTimePicker: args.withTimePicker, - validation: [ - requiredRule('Required field'), - ], + validation: [requiredRule('Required field')], }), getSubmitMeta({ name: 'submit', @@ -234,9 +220,7 @@ export const Select = (args) => ( label: 'Two', }, ], - validation: [ - requiredRule('Required field'), - ], + validation: [requiredRule('Required field')], }), getSubmitMeta({ name: 'submit', @@ -260,9 +244,7 @@ export const Checkbox = (args) => ( name: 'checkbox', label: args.label, disabled: args.disabled, - validation: [ - requiredRule('Required field'), - ], + validation: [requiredRule('Required field')], }), getSubmitMeta({ name: 'submit', @@ -289,11 +271,9 @@ export const DateRange = (args) => ( size: args.smallSize ? 'small' : 'full', presets: { lastDay: 'Last day', - lastWeek: 'Last week' + lastWeek: 'Last week', }, - validation: [ - requiredRule('Required field'), - ], + validation: [requiredRule('Required field')], }), getSubmitMeta({ name: 'submit', @@ -307,7 +287,7 @@ export const DateRange = (args) => ( DateRange.args = { label: 'Date Range', withTimePicker: true, - smallSize: true + smallSize: true, }; export const Multiselect = (args) => ( @@ -321,7 +301,7 @@ export const Multiselect = (args) => ( options: [ { label: 'Dog 1', value: 1 }, { label: 'Cat 2', value: 2 }, - { label: 'Pig 3', value: 3 }, + { label: 'Pigs' }, { label: 'Rat 4', value: 4 }, { label: 'Dog 5', value: 5 }, { label: 'Cat 6', value: 6 }, @@ -387,22 +367,27 @@ export const SegmentedSwitch = (args) => ( initialValues={{ a: 'a', b: 'b', - c: 'c' + c: 'c', }} - fields={[...['first', 'second', 'third'].map(name => getSegmentedSwitch({ - name: name, - label: name, - size: args.size, - disabled: args.disabled, - align: args.alignRight ? 'right' : undefined, - options: [ - { label: args.showLabel && 'A Value', value: 'a', icon: args.showIcon ? 'write' : undefined }, - { label: args.showLabel && 'B Value', value: 'b', icon: args.showIcon ? 'read' : undefined }, - { label: args.showLabel && 'C Value', value: 'c', icon: args.showIcon ? 'none' : undefined } - ], - })), - getSubmitMeta({ name: 'submit', label: 'Save' }) - ]} onSubmit={handleSubmit} /> + fields={[ + ...['first', 'second', 'third'].map((name) => + getSegmentedSwitch({ + name: name, + label: name, + size: args.size, + disabled: args.disabled, + align: args.alignRight ? 'right' : undefined, + options: [ + { label: args.showLabel && 'A Value', value: 'a', icon: args.showIcon ? 'write' : undefined }, + { label: args.showLabel && 'B Value', value: 'b', icon: args.showIcon ? 'read' : undefined }, + { label: args.showLabel && 'C Value', value: 'c', icon: args.showIcon ? 'none' : undefined }, + ], + }), + ), + getSubmitMeta({ name: 'submit', label: 'Save' }), + ]} + onSubmit={handleSubmit} + /> ); SegmentedSwitch.args = { @@ -411,16 +396,16 @@ SegmentedSwitch.args = { size: 'small', alignRight: false, showIcon: true, - showLabel: true + showLabel: true, }; SegmentedSwitch.argTypes = { formLayout: { options: ['horizontal', 'vertical'], - control: {type: 'radio'}, + control: { type: 'radio' }, }, size: { options: ['small', 'medium', 'large'], - control: {type: 'radio'}, + control: { type: 'radio' }, }, }; diff --git a/packages/react-metaforms/src/antd/components/MultiSelectTree.tsx b/packages/react-metaforms/src/antd/components/MultiSelectTree.tsx new file mode 100644 index 00000000..7699856d --- /dev/null +++ b/packages/react-metaforms/src/antd/components/MultiSelectTree.tsx @@ -0,0 +1,201 @@ +import { Badge, Checkbox, List } from 'antd'; +import Fuse from 'fuse.js'; +import { head } from 'ramda'; +import React, { Ref, useEffect, useRef, useState } from 'react'; +import styled, { css } from 'styled-components'; +import { KeyboardKey, useBoolean, useKeyPress } from '../../hooks'; +import { toggleSelection } from '../../hooks/useSelection'; +import Input from './Input'; +import { MultiSelectMetaProps } from 'metaforms'; + +type MultiSelectMetaValue = string | number; + +interface ICheckboxListProps extends Omit { + size?: 'default' | 'large' | 'small'; + onChange: (value: MultiSelectMetaValue[]) => void; + onFocus?: (event: React.FocusEvent) => void; + onBlur?: (event: React.FocusEvent) => void; + showSelectedCounter?: boolean; + showFilterInput?: boolean; + value: MultiSelectMetaValue[]; +} + +const ListItem = styled(List.Item)<{ $focused: boolean }>` + ${({ $focused }) => + $focused && + css` + background: rgba(180, 180, 180, 0.1); + `} + cursor: pointer; +`; + +const ListItemContent = styled.div` + cursor: pointer; + display: flex; + column-gap: 10px; +`; + +const ToggleResults = styled.div` + padding: 0.5em 0; + + & > span { + margin-left: 0.5em; + } +`; + +const MultiselectTree = React.forwardRef( + ( + { + options = [], + value = [], + onChange, + placeholder, + disabled, + onFocus, + onBlur, + showSelectedCounter = true, + showFilterInput = true, + }: ICheckboxListProps, + ref: Ref, + ) => { + const focusedRef = useRef(null); + const [search, setSearch] = useState(); + const [focused, setFocused] = useState(); + const inputFocused = useBoolean(false); + const showSelected = useBoolean(false); + + const shouldRegisterKeyPress = Boolean(disabled || !inputFocused.value); + + const downPress = useKeyPress(KeyboardKey.arrowDown, shouldRegisterKeyPress); + const upPress = useKeyPress(KeyboardKey.arrowUp, shouldRegisterKeyPress); + const spacePress = useKeyPress(KeyboardKey.space, shouldRegisterKeyPress); + + const [filteredOptions, setFilteredOptions] = useState(options); + + const handleClick = (itemValue: MultiSelectMetaValue) => { + if (disabled) { + return; + } + + const opts = toggleSelection(itemValue, value); + const visibleOptions = showSelected.value ? filteredOptions.filter((o) => opts.includes(o.value)) : filteredOptions; + + if (inputFocused.value && !visibleOptions.find((vo) => vo.value === focused)) { + setFocused(head(visibleOptions)?.value); + } + + setFilteredOptions(visibleOptions); + onChange(opts); + }; + + const handleSelectAll = (checked: boolean) => { + onChange(checked ? options.map((opt) => opt.value) : []); + }; + + const handleInputFocus = (event: React.FocusEvent) => { + inputFocused.setTrue(); + setFocused(head(filteredOptions)?.value); + onFocus?.(event); + }; + + const handleInputBlur = (event: React.FocusEvent) => { + inputFocused.setFalse(); + setFocused(undefined); + onBlur?.(event); + }; + + useEffect(() => { + const fuse = new Fuse(options, { keys: ['label'] }); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const filteredOptions = search ? fuse.search(search).map((result) => result.item) : options; + const visibleOptions = showSelected.value ? filteredOptions.filter((o) => value.includes(o.value)) : filteredOptions; + setFilteredOptions(visibleOptions); + setFocused(inputFocused.value ? head(visibleOptions)?.value : undefined); + }, [search, options, showSelected.value]); + + useEffect(() => { + if (spacePress && focused) { + handleClick(focused); + } + }, [spacePress]); + + useEffect(() => { + const curr = focusedRef?.current; + if (curr) { + curr.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } + }, [focused]); + + useEffect(() => { + if (inputFocused.value) { + const focusedOptionIdx = filteredOptions.findIndex((o) => o.value === focused); + if (downPress) { + const newIndex = focusedOptionIdx + 1 >= filteredOptions.length ? filteredOptions.length - 1 : focusedOptionIdx + 1; + return setFocused(filteredOptions[newIndex]?.value); + } + + if (upPress) { + const newIndex = focusedOptionIdx - 1 < 0 ? 0 : focusedOptionIdx - 1; + return setFocused(filteredOptions[newIndex]?.value); + } + } + }, [downPress, upPress]); + + return ( + <> + {showFilterInput && ( + setSearch(e.target.value)} + onFocus={handleInputFocus} + onBlur={handleInputBlur} + /> + )} + + + handleSelectAll(e.target.checked)} + > + Označit vše + + + {showSelectedCounter && ( + <> + showSelected.setValue(e.target.checked)} /> + Zobrazit pouze vybrané {value && } + + )} + + { + const isSelected = value.includes(item.value); + const isFocused = focused === item.value; + return ( + handleClick(item.value)}> + + + {item.label} + + + ); + }} + /> + + ); + }, +); + +MultiselectTree.displayName = 'MultiselectTree'; + +export default MultiselectTree; diff --git a/packages/react-metaforms/src/antd/components/Multiselect.tsx b/packages/react-metaforms/src/antd/components/Multiselect.tsx index 996a9018..b08c7db3 100644 --- a/packages/react-metaforms/src/antd/components/Multiselect.tsx +++ b/packages/react-metaforms/src/antd/components/Multiselect.tsx @@ -1,4 +1,4 @@ -import { Badge, Checkbox, List } from 'antd'; +import { Badge, Checkbox, List, Tree } from 'antd'; import Fuse from 'fuse.js'; import { head } from 'ramda'; import React, { Ref, useEffect, useRef, useState } from 'react'; @@ -6,7 +6,7 @@ import styled, { css } from 'styled-components'; import { KeyboardKey, useBoolean, useKeyPress } from '../../hooks'; import { toggleSelection } from '../../hooks/useSelection'; import Input from './Input'; -import {MultiSelectMetaProps} from 'metaforms'; +import { MultiSelectLeaf, MultiSelectMetaProps, MultiSelectOption } from 'metaforms'; type MultiSelectMetaValue = string | number; @@ -23,7 +23,7 @@ interface ICheckboxListProps extends Omit { const ListItem = styled(List.Item)<{ $focused: boolean }>` ${({ $focused }) => $focused && - css` + css` background: rgba(180, 180, 180, 0.1); `} cursor: pointer; @@ -35,7 +35,7 @@ const ListItemContent = styled.div` column-gap: 10px; `; -const ToggleResults = styled.div` +const ToggleAllPanel = styled.div` padding: 0.5em 0; & > span { @@ -43,131 +43,163 @@ const ToggleResults = styled.div` } `; -const Multiselect = React.forwardRef(({ options = [], value = [], onChange, placeholder, disabled, onFocus, onBlur, showSelectedCounter = true, showFilterInput = true }: ICheckboxListProps, ref: Ref) => { - const focusedRef = useRef(null); - const [search, setSearch] = useState(); - const [focused, setFocused] = useState(); - const inputFocused = useBoolean(false); - const showSelected = useBoolean(false); - - const shouldRegisterKeyPress = Boolean(disabled || !inputFocused.value); - - const downPress = useKeyPress(KeyboardKey.arrowDown, shouldRegisterKeyPress); - const upPress = useKeyPress(KeyboardKey.arrowUp, shouldRegisterKeyPress); - const spacePress = useKeyPress(KeyboardKey.space, shouldRegisterKeyPress); - - const [filteredOptions, setFilteredOptions] = useState(options); - - const handleClick = (itemValue: MultiSelectMetaValue) => { - if (disabled) { - return; - } - - const opts = toggleSelection(itemValue, value); - const visibleOptions = showSelected.value ? filteredOptions.filter((o) => opts.includes(o.value)) : filteredOptions; - - if (inputFocused.value && !visibleOptions.find((vo) => vo.value === focused)) { - setFocused(head(visibleOptions)?.value); - } - - setFilteredOptions(visibleOptions); - onChange(opts); - }; - - const handleSelectAll = (checked: boolean) => { - onChange(checked ? options.map(opt => opt.value) : []); - }; - - const handleInputFocus = (event: React.FocusEvent) => { - inputFocused.setTrue(); - setFocused(head(filteredOptions)?.value); - onFocus?.(event); - }; - - const handleInputBlur = (event: React.FocusEvent) => { - inputFocused.setFalse(); - setFocused(undefined); - onBlur?.(event); - }; - - useEffect(() => { - const fuse = new Fuse(options, { keys: ['label'] }); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - const filteredOptions = search ? fuse.search(search).map((result) => result.item) : options; - const visibleOptions = showSelected.value ? filteredOptions.filter((o) => value.includes(o.value)) : filteredOptions; - setFilteredOptions(visibleOptions); - setFocused(inputFocused.value ? head(visibleOptions)?.value : undefined); - }, [search, options, showSelected.value]); - - useEffect(() => { - if (spacePress && focused) { - handleClick(focused); - } - }, [spacePress]); - - useEffect(() => { - const curr = focusedRef?.current; - if (curr) { - curr.scrollIntoView({ behavior: 'smooth', block: 'center' }); - } - }, [focused]); - - useEffect(() => { - if (inputFocused.value) { - const focusedOptionIdx = filteredOptions.findIndex((o) => o.value === focused); - if (downPress) { - const newIndex = focusedOptionIdx + 1 >= filteredOptions.length ? filteredOptions.length - 1 : focusedOptionIdx + 1; - return setFocused(filteredOptions[newIndex]?.value); +const Multiselect = React.forwardRef( + ( + { + options = [], + value = [], + onChange, + placeholder, + disabled, + onFocus, + onBlur, + showSelectedCounter = true, + showFilterInput = true, + }: ICheckboxListProps, + ref: Ref, + ) => { + const focusedRef = useRef(null); + const [search, setSearch] = useState(); + const [focused, setFocused] = useState(); + const inputFocused = useBoolean(false); + const showSelected = useBoolean(false); + + const shouldRegisterKeyPress = Boolean(disabled || !inputFocused.value); + + const downPress = useKeyPress(KeyboardKey.arrowDown, shouldRegisterKeyPress); + const upPress = useKeyPress(KeyboardKey.arrowUp, shouldRegisterKeyPress); + const spacePress = useKeyPress(KeyboardKey.space, shouldRegisterKeyPress); + + const isMultiSelectLeaf = (option: MultiSelectOption): option is MultiSelectLeaf => option['children'] === undefined; + const isOnlyLabelItem = (option: MultiSelectLeaf) => option.value === undefined; + + const [filteredOptions, setFilteredOptions] = useState(options); + + const handleClick = (itemValue: MultiSelectMetaValue) => { + if (disabled) { + return; } - if (upPress) { - const newIndex = focusedOptionIdx - 1 < 0 ? 0 : focusedOptionIdx - 1; - return setFocused(filteredOptions[newIndex]?.value); + const opts = toggleSelection(itemValue, value); + const visibleOptions = showSelected.value ? filteredOptions.filter((o) => opts.includes(o.value)) : filteredOptions; + + if (inputFocused.value && !visibleOptions.find((vo) => vo.value === focused)) { + setFocused(head(visibleOptions)?.value); + } + + setFilteredOptions(visibleOptions); + onChange(opts); + }; + + const handleSelectAll = (checked: boolean) => { + onChange(checked ? options.filter((o) => !isOnlyLabelItem(o)).map((opt) => opt.value) : []); + }; + + const handleInputFocus = (event: React.FocusEvent) => { + inputFocused.setTrue(); + setFocused(head(filteredOptions)?.value); + onFocus?.(event); + }; + + const handleInputBlur = (event: React.FocusEvent) => { + inputFocused.setFalse(); + setFocused(undefined); + onBlur?.(event); + }; + + useEffect(() => { + const fuse = new Fuse(options, { keys: ['label'] }); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const filteredOptions = search ? fuse.search(search).map((result) => result.item) : options; + const visibleOptions = showSelected.value ? filteredOptions.filter((o) => value.includes(o.value)) : filteredOptions; + setFilteredOptions(visibleOptions); + setFocused(inputFocused.value ? head(visibleOptions)?.value : undefined); + }, [search, options, showSelected.value]); + + useEffect(() => { + if (spacePress && focused) { + handleClick(focused); } - } - }, [downPress, upPress]); - - - return ( - <> - {showFilterInput && setSearch(e.target.value)} onFocus={handleInputFocus} onBlur={handleInputBlur} />} - - - handleSelectAll(e.target.checked)}> - Označit vše - - - {showSelectedCounter && ( - <> - showSelected.setValue(e.target.checked)} /> - Zobrazit pouze vybrané {value && } - + }, [spacePress]); + + useEffect(() => { + const curr = focusedRef?.current; + if (curr) { + curr.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } + }, [focused]); + + useEffect(() => { + if (inputFocused.value) { + const focusedOptionIdx = filteredOptions.findIndex((o) => o.value === focused); + if (downPress) { + const newIndex = focusedOptionIdx + 1 >= filteredOptions.length ? filteredOptions.length - 1 : focusedOptionIdx + 1; + return setFocused(filteredOptions[newIndex]?.value); + } + + if (upPress) { + const newIndex = focusedOptionIdx - 1 < 0 ? 0 : focusedOptionIdx - 1; + return setFocused(filteredOptions[newIndex]?.value); + } + } + }, [downPress, upPress]); + + return ( + <> + {showFilterInput && ( + setSearch(e.target.value)} + onFocus={handleInputFocus} + onBlur={handleInputBlur} + /> )} - - { - const isSelected = value.includes(item.value); - const isFocused = focused === item.value; - return ( - handleClick(item.value)}> - - - {item.label} - - - ); - }} - /> - - ); -}); + + + !isOnlyLabelItem(o)).length} + indeterminate={value.length && value.length < options.filter((o) => !isOnlyLabelItem(o)).length} + onChange={(e) => handleSelectAll(e.target.checked)} + > + Označit vše + + + {showSelectedCounter && ( + <> + showSelected.setValue(e.target.checked)} /> + Zobrazit pouze vybrané {value && } + + )} + + { + if (item.value === undefined) return {item.label}; + const isSelected = value.includes(item.value); + const isFocused = focused === item.value; + return ( + handleClick(item.value)}> + + + {item.label} + + + ); + }} + /> + + ); + }, +); Multiselect.displayName = 'Multiselect'; - export default Multiselect; diff --git a/packages/react-metaforms/src/core/Form.stories.tsx b/packages/react-metaforms/src/core/Form.stories.tsx index f32ff66c..19b843cf 100644 --- a/packages/react-metaforms/src/core/Form.stories.tsx +++ b/packages/react-metaforms/src/core/Form.stories.tsx @@ -1,33 +1,29 @@ import MetaForm, { isControlLayout } from './Form'; import React from 'react'; -import {getObjectMeta, getSubmitMeta, getTextMeta, isRequired, requiredRule } from 'metaforms'; +import { getObjectMeta, getSubmitMeta, getTextMeta, isRequired, requiredRule } from 'metaforms'; import { MetaField } from '@falsy/metacore'; import { action } from '@storybook/addon-actions'; import { isControlArray, isControlObject, isControlAction } from './Form'; const values = { user: { - name: 'John Doe' + name: 'John Doe', }, - roles: [{ name: 'Admin'}, { name: 'Superuser' }], - friends: ['Karel', 'Ondra'] + roles: [{ name: 'Admin' }, { name: 'Superuser' }], + friends: ['Karel', 'Ondra'], }; const fields: MetaField[] = [ getObjectMeta({ name: 'roles', array: true, - validation: [ - requiredRule('Please enter at least one role'), - ], + validation: [requiredRule('Please enter at least one role')], fields: [ getTextMeta({ name: 'name', label: 'Name', - validation: [ - requiredRule('Please enter role name') - ], - }) + validation: [requiredRule('Please enter role name')], + }), ], }), getTextMeta({ @@ -38,7 +34,7 @@ const fields: MetaField[] = [ getSubmitMeta({ name: 'submit', label: 'Submit', - }) + }), ]; const Label = (props: { fieldId: string; label: string; isRequired: boolean; children?: React.ReactChildren }) => ( @@ -58,20 +54,13 @@ const Submit = ({ isLoading, label, name }: any) => ( const Input = React.forwardRef((props: any, ref: React.Ref) => (
{props.label &&
)); Input.displayName = 'Input'; - export const Basic = () => { return ( { }} fields={fields} components={(componentProps) => { - if(isControlArray(componentProps)) { + if (isControlArray(componentProps)) { const { children, arrayHelpers } = componentProps; - return <>{children?.map((c, idx) => {c} )}


; + return ( + <> + {children?.map((c, idx) => ( + + {c}{' '} + + + ))} +
+ +
+
+ + ); } - if(isControlObject(componentProps)) { + if (isControlObject(componentProps)) { const { children } = componentProps; return
{children}
; } - if(isControlAction(componentProps)) { + if (isControlAction(componentProps)) { return ; } - if(isControlLayout(componentProps)) { + if (isControlLayout(componentProps)) { return null; } @@ -107,7 +113,17 @@ export const Basic = () => { switch (field.type) { case 'text': - return ; + return ( + + ); case 'submit': return ; default: @@ -121,4 +137,3 @@ export const Basic = () => { export default { title: 'Form', }; - diff --git a/yarn.lock b/yarn.lock index 036652cf..0fb39b70 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2121,6 +2121,20 @@ __metadata: languageName: node linkType: hard +"@isaacs/cliui@npm:^8.0.2": + version: 8.0.2 + resolution: "@isaacs/cliui@npm:8.0.2" + dependencies: + string-width: ^5.1.2 + string-width-cjs: "npm:string-width@^4.2.0" + strip-ansi: ^7.0.1 + strip-ansi-cjs: "npm:strip-ansi@^6.0.1" + wrap-ansi: ^8.1.0 + wrap-ansi-cjs: "npm:wrap-ansi@^7.0.0" + checksum: 4a473b9b32a7d4d3cfb7a614226e555091ff0c5a29a1734c28c72a182c2f6699b26fc6b5c2131dfd841e86b185aea714c72201d7c98c2fba5f17709333a67aeb + languageName: node + linkType: hard + "@isaacs/string-locale-compare@npm:^1.1.0": version: 1.1.0 resolution: "@isaacs/string-locale-compare@npm:1.1.0" @@ -3792,6 +3806,13 @@ __metadata: languageName: node linkType: hard +"@pkgjs/parseargs@npm:^0.11.0": + version: 0.11.0 + resolution: "@pkgjs/parseargs@npm:0.11.0" + checksum: 6ad6a00fc4f2f2cfc6bff76fb1d88b8ee20bc0601e18ebb01b6d4be583733a860239a521a7fbca73b612e66705078809483549d2b18f370eb346c5155c8e4a0f + languageName: node + linkType: hard + "@pmmmwh/react-refresh-webpack-plugin@npm:^0.5.3": version: 0.5.7 resolution: "@pmmmwh/react-refresh-webpack-plugin@npm:0.5.7" @@ -6840,6 +6861,13 @@ __metadata: languageName: node linkType: hard +"ansi-regex@npm:^6.0.1": + version: 6.0.1 + resolution: "ansi-regex@npm:6.0.1" + checksum: 1ff8b7667cded1de4fa2c9ae283e979fc87036864317da86a2e546725f96406746411d0d85e87a2d12fa5abd715d90006de7fa4fa0477c92321ad3b4c7d4e169 + languageName: node + linkType: hard + "ansi-styles@npm:^3.2.1": version: 3.2.1 resolution: "ansi-styles@npm:3.2.1" @@ -6865,6 +6893,13 @@ __metadata: languageName: node linkType: hard +"ansi-styles@npm:^6.1.0": + version: 6.2.1 + resolution: "ansi-styles@npm:6.2.1" + checksum: ef940f2f0ced1a6347398da88a91da7930c33ecac3c77b72c5905f8b8fe402c52e6fde304ff5347f616e27a742da3f1dc76de98f6866c69251ad0b07a66776d9 + languageName: node + linkType: hard + "ansi-to-html@npm:^0.6.11": version: 0.6.15 resolution: "ansi-to-html@npm:0.6.15" @@ -9930,6 +9965,13 @@ __metadata: languageName: node linkType: hard +"eastasianwidth@npm:^0.2.0": + version: 0.2.0 + resolution: "eastasianwidth@npm:0.2.0" + checksum: 7d00d7cd8e49b9afa762a813faac332dee781932d6f2c848dc348939c4253f1d4564341b7af1d041853bc3f32c2ef141b58e0a4d9862c17a7f08f68df1e0f1ed + languageName: node + linkType: hard + "ee-first@npm:1.1.1": version: 1.1.1 resolution: "ee-first@npm:1.1.1" @@ -9994,6 +10036,13 @@ __metadata: languageName: node linkType: hard +"emoji-regex@npm:^9.2.2": + version: 9.2.2 + resolution: "emoji-regex@npm:9.2.2" + checksum: 8487182da74aabd810ac6d6f1994111dfc0e331b01271ae01ec1eb0ad7b5ecc2bbbbd2f053c05cb55a1ac30449527d819bbfbf0e3de1023db308cbcb47f86601 + languageName: node + linkType: hard + "emojis-list@npm:^3.0.0": version: 3.0.0 resolution: "emojis-list@npm:3.0.0" @@ -11188,6 +11237,16 @@ __metadata: languageName: node linkType: hard +"foreground-child@npm:^3.1.0": + version: 3.1.1 + resolution: "foreground-child@npm:3.1.1" + dependencies: + cross-spawn: ^7.0.0 + signal-exit: ^4.0.1 + checksum: 139d270bc82dc9e6f8bc045fe2aae4001dc2472157044fdfad376d0a3457f77857fa883c1c8b21b491c6caade9a926a4bed3d3d2e8d3c9202b151a4cbbd0bcd5 + languageName: node + linkType: hard + "fork-ts-checker-webpack-plugin@npm:^4.1.6": version: 4.1.6 resolution: "fork-ts-checker-webpack-plugin@npm:4.1.6" @@ -11760,6 +11819,21 @@ __metadata: languageName: node linkType: hard +"glob@npm:^10.2.5": + version: 10.3.3 + resolution: "glob@npm:10.3.3" + dependencies: + foreground-child: ^3.1.0 + jackspeak: ^2.0.3 + minimatch: ^9.0.1 + minipass: ^5.0.0 || ^6.0.2 || ^7.0.0 + path-scurry: ^1.10.1 + bin: + glob: dist/cjs/src/bin.js + checksum: 29190d3291f422da0cb40b77a72fc8d2c51a36524e99b8bf412548b7676a6627489528b57250429612b6eec2e6fe7826d328451d3e694a9d15e575389308ec53 + languageName: node + linkType: hard + "glob@npm:^7.0.3, glob@npm:^7.1.1, glob@npm:^7.1.2, glob@npm:^7.1.3, glob@npm:^7.1.4, glob@npm:^7.1.6": version: 7.2.3 resolution: "glob@npm:7.2.3" @@ -13389,6 +13463,19 @@ __metadata: languageName: node linkType: hard +"jackspeak@npm:^2.0.3": + version: 2.2.3 + resolution: "jackspeak@npm:2.2.3" + dependencies: + "@isaacs/cliui": ^8.0.2 + "@pkgjs/parseargs": ^0.11.0 + dependenciesMeta: + "@pkgjs/parseargs": + optional: true + checksum: 8add557045eb51f619d247ac9786dbfa7ee4d52a0eb3fb488c2637aecfd15d12c284a4ff7dead2c1aba34d6228d9452e4509fb771daae87793a48786b095ee07 + languageName: node + linkType: hard + "jest-changed-files@npm:^27.1.1": version: 27.1.1 resolution: "jest-changed-files@npm:27.1.1" @@ -14713,6 +14800,13 @@ __metadata: languageName: node linkType: hard +"lru-cache@npm:^9.1.1 || ^10.0.0": + version: 10.0.1 + resolution: "lru-cache@npm:10.0.1" + checksum: 06f8d0e1ceabd76bb6f644a26dbb0b4c471b79c7b514c13c6856113879b3bf369eb7b497dad4ff2b7e2636db202412394865b33c332100876d838ad1372f0181 + languageName: node + linkType: hard + "lz-string@npm:^1.4.4": version: 1.4.4 resolution: "lz-string@npm:1.4.4" @@ -15044,6 +15138,7 @@ __metadata: fputils: ^1.2.4 jest: ^27.0.6 reflect-metadata: ^0.1.13 + rimraf: ^5.0.1 sqlite3: ^5.0.0 ts-jest: ^27.0.4 typescript: ~4.8.2 @@ -15284,6 +15379,15 @@ __metadata: languageName: node linkType: hard +"minimatch@npm:^9.0.1": + version: 9.0.3 + resolution: "minimatch@npm:9.0.3" + dependencies: + brace-expansion: ^2.0.1 + checksum: 253487976bf485b612f16bf57463520a14f512662e592e95c571afdab1442a6a6864b6c88f248ce6fc4ff0b6de04ac7aa6c8bb51e868e99d1d65eb0658a708b5 + languageName: node + linkType: hard + "minimist-options@npm:4.1.0": version: 4.1.0 resolution: "minimist-options@npm:4.1.0" @@ -15396,6 +15500,13 @@ __metadata: languageName: node linkType: hard +"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0": + version: 7.0.3 + resolution: "minipass@npm:7.0.3" + checksum: 6f1614f5b5b55568a46bca5fec0e7c46dac027691db27d0e1923a8192866903144cd962ac772c0e9f89b608ea818b702709c042bce98e190d258847d85461531 + languageName: node + linkType: hard + "minizlib@npm:^2.0.0, minizlib@npm:^2.1.1, minizlib@npm:^2.1.2": version: 2.1.2 resolution: "minizlib@npm:2.1.2" @@ -16914,6 +17025,16 @@ __metadata: languageName: node linkType: hard +"path-scurry@npm:^1.10.1": + version: 1.10.1 + resolution: "path-scurry@npm:1.10.1" + dependencies: + lru-cache: ^9.1.1 || ^10.0.0 + minipass: ^5.0.0 || ^6.0.2 || ^7.0.0 + checksum: e2557cff3a8fb8bc07afdd6ab163a92587884f9969b05bbbaf6fe7379348bfb09af9ed292af12ed32398b15fb443e81692047b786d1eeb6d898a51eb17ed7d90 + languageName: node + linkType: hard + "path-to-regexp@npm:0.1.7": version: 0.1.7 resolution: "path-to-regexp@npm:0.1.7" @@ -19007,6 +19128,17 @@ __metadata: languageName: node linkType: hard +"rimraf@npm:^5.0.1": + version: 5.0.1 + resolution: "rimraf@npm:5.0.1" + dependencies: + glob: ^10.2.5 + bin: + rimraf: dist/cjs/src/bin.js + checksum: bafce85391349a2d960847980bf9b5caa2a8887f481af630f1ea27e08288217293cec72d75e9a2ba35495c212789f66a7f3d23366ba6197026ab71c535126857 + languageName: node + linkType: hard + "ripemd160@npm:^2.0.0, ripemd160@npm:^2.0.1": version: 2.0.2 resolution: "ripemd160@npm:2.0.2" @@ -19447,6 +19579,13 @@ __metadata: languageName: node linkType: hard +"signal-exit@npm:^4.0.1": + version: 4.1.0 + resolution: "signal-exit@npm:4.1.0" + checksum: 64c757b498cb8629ffa5f75485340594d2f8189e9b08700e69199069c8e3070fb3e255f7ab873c05dc0b3cec412aea7402e10a5990cb6a050bd33ba062a6c549 + languageName: node + linkType: hard + "sisteransi@npm:^1.0.5": version: 1.0.5 resolution: "sisteransi@npm:1.0.5" @@ -19862,6 +20001,17 @@ __metadata: languageName: node linkType: hard +"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.1.0, string-width@npm:^4.2.0": + version: 4.2.2 + resolution: "string-width@npm:4.2.2" + dependencies: + emoji-regex: ^8.0.0 + is-fullwidth-code-point: ^3.0.0 + strip-ansi: ^6.0.0 + checksum: 343e089b0e66e0f72aab4ad1d9b6f2c9cc5255844b0c83fd9b53f2a3b3fd0421bdd6cb05be96a73117eb012db0887a6c1d64ca95aaa50c518e48980483fea0ab + languageName: node + linkType: hard + "string-width@npm:^1.0.2 || 2 || 3 || 4, string-width@npm:^4.0.0, string-width@npm:^4.2.2, string-width@npm:^4.2.3": version: 4.2.3 resolution: "string-width@npm:4.2.3" @@ -19873,14 +20023,14 @@ __metadata: languageName: node linkType: hard -"string-width@npm:^4.1.0, string-width@npm:^4.2.0": - version: 4.2.2 - resolution: "string-width@npm:4.2.2" +"string-width@npm:^5.0.1, string-width@npm:^5.1.2": + version: 5.1.2 + resolution: "string-width@npm:5.1.2" dependencies: - emoji-regex: ^8.0.0 - is-fullwidth-code-point: ^3.0.0 - strip-ansi: ^6.0.0 - checksum: 343e089b0e66e0f72aab4ad1d9b6f2c9cc5255844b0c83fd9b53f2a3b3fd0421bdd6cb05be96a73117eb012db0887a6c1d64ca95aaa50c518e48980483fea0ab + eastasianwidth: ^0.2.0 + emoji-regex: ^9.2.2 + strip-ansi: ^7.0.1 + checksum: 7369deaa29f21dda9a438686154b62c2c5f661f8dda60449088f9f980196f7908fc39fdd1803e3e01541970287cf5deae336798337e9319a7055af89dafa7193 languageName: node linkType: hard @@ -20009,6 +20159,15 @@ __metadata: languageName: node linkType: hard +"strip-ansi-cjs@npm:strip-ansi@^6.0.1, strip-ansi@npm:^6.0.1": + version: 6.0.1 + resolution: "strip-ansi@npm:6.0.1" + dependencies: + ansi-regex: ^5.0.1 + checksum: f3cd25890aef3ba6e1a74e20896c21a46f482e93df4a06567cebf2b57edabb15133f1f94e57434e0a958d61186087b1008e89c94875d019910a213181a14fc8c + languageName: node + linkType: hard + "strip-ansi@npm:^3.0.1": version: 3.0.1 resolution: "strip-ansi@npm:3.0.1" @@ -20027,12 +20186,12 @@ __metadata: languageName: node linkType: hard -"strip-ansi@npm:^6.0.1": - version: 6.0.1 - resolution: "strip-ansi@npm:6.0.1" +"strip-ansi@npm:^7.0.1": + version: 7.1.0 + resolution: "strip-ansi@npm:7.1.0" dependencies: - ansi-regex: ^5.0.1 - checksum: f3cd25890aef3ba6e1a74e20896c21a46f482e93df4a06567cebf2b57edabb15133f1f94e57434e0a958d61186087b1008e89c94875d019910a213181a14fc8c + ansi-regex: ^6.0.1 + checksum: 859c73fcf27869c22a4e4d8c6acfe690064659e84bef9458aa6d13719d09ca88dcfd40cbf31fd0be63518ea1a643fe070b4827d353e09533a5b0b9fd4553d64d languageName: node linkType: hard @@ -21775,7 +21934,7 @@ __metadata: languageName: node linkType: hard -"wrap-ansi@npm:^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0, wrap-ansi@npm:^7.0.0": version: 7.0.0 resolution: "wrap-ansi@npm:7.0.0" dependencies: @@ -21786,6 +21945,17 @@ __metadata: languageName: node linkType: hard +"wrap-ansi@npm:^8.1.0": + version: 8.1.0 + resolution: "wrap-ansi@npm:8.1.0" + dependencies: + ansi-styles: ^6.1.0 + string-width: ^5.0.1 + strip-ansi: ^7.0.1 + checksum: 371733296dc2d616900ce15a0049dca0ef67597d6394c57347ba334393599e800bab03c41d4d45221b6bc967b8c453ec3ae4749eff3894202d16800fdfe0e238 + languageName: node + linkType: hard + "wrappy@npm:1": version: 1.0.2 resolution: "wrappy@npm:1.0.2"