@kronor/dtv 0.2.9 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/dist/assets/InterVariable-CWi-zmRD.woff2 +0 -0
  2. package/dist/assets/InterVariable-Italic-d6KXgdvN.woff2 +0 -0
  3. package/dist/assets/index-BNgm6iTo.js +2105 -0
  4. package/dist/assets/index-CKkrV_Rb.css +1 -0
  5. package/dist/assets/primeicons-C6QP2o4f.woff2 +0 -0
  6. package/dist/assets/primeicons-DMOk5skT.eot +0 -0
  7. package/dist/assets/primeicons-Dr5RGzOO.svg +345 -0
  8. package/dist/assets/primeicons-MpK4pl85.ttf +0 -0
  9. package/dist/assets/primeicons-WjwUDZjB.woff +0 -0
  10. package/dist/index.html +17 -0
  11. package/package.json +5 -1
  12. package/.editorconfig +0 -12
  13. package/.github/copilot-instructions.md +0 -64
  14. package/.github/workflows/ci.yml +0 -51
  15. package/.husky/pre-commit +0 -8
  16. package/e2e/app.spec.ts +0 -6
  17. package/e2e/cell-renderer-setfilterstate.spec.ts +0 -63
  18. package/e2e/filter-sharing.spec.ts +0 -113
  19. package/e2e/filter-url-persistence.spec.ts +0 -36
  20. package/e2e/graphqlMock.ts +0 -144
  21. package/e2e/multi-field-filters.spec.ts +0 -95
  22. package/e2e/pagination.spec.ts +0 -38
  23. package/e2e/payment-request-email-filter.spec.ts +0 -67
  24. package/e2e/save-filter-splitbutton.spec.ts +0 -68
  25. package/e2e/simple-view-email-filter.spec.ts +0 -67
  26. package/e2e/simple-view-transforms.spec.ts +0 -171
  27. package/e2e/simple-view.spec.ts +0 -104
  28. package/e2e/transform-regression.spec.ts +0 -108
  29. package/eslint.config.js +0 -30
  30. package/index.html +0 -17
  31. package/jest.config.js +0 -10
  32. package/playwright.config.ts +0 -54
  33. package/src/App.externalRuntime.test.ts +0 -190
  34. package/src/App.tsx +0 -540
  35. package/src/assets/react.svg +0 -1
  36. package/src/components/AIAssistantForm.tsx +0 -241
  37. package/src/components/FilterForm.test.ts +0 -82
  38. package/src/components/FilterForm.tsx +0 -375
  39. package/src/components/PhoneNumberFilter.tsx +0 -102
  40. package/src/components/SavedFilterList.tsx +0 -181
  41. package/src/components/SpeechInput.tsx +0 -67
  42. package/src/components/Table.tsx +0 -119
  43. package/src/components/TablePagination.tsx +0 -40
  44. package/src/components/aiAssistant.test.ts +0 -270
  45. package/src/components/aiAssistant.ts +0 -291
  46. package/src/framework/cell-renderer-components/CurrencyAmount.tsx +0 -30
  47. package/src/framework/cell-renderer-components/LayoutHelpers.tsx +0 -74
  48. package/src/framework/cell-renderer-components/Link.tsx +0 -28
  49. package/src/framework/cell-renderer-components/Mapping.tsx +0 -11
  50. package/src/framework/cell-renderer-components.test.ts +0 -353
  51. package/src/framework/column-definition.tsx +0 -85
  52. package/src/framework/currency.test.ts +0 -46
  53. package/src/framework/currency.ts +0 -62
  54. package/src/framework/data.staticConditions.test.ts +0 -46
  55. package/src/framework/data.test.ts +0 -167
  56. package/src/framework/data.ts +0 -162
  57. package/src/framework/filter-form-state.test.ts +0 -189
  58. package/src/framework/filter-form-state.ts +0 -185
  59. package/src/framework/filter-sharing.test.ts +0 -135
  60. package/src/framework/filter-sharing.ts +0 -118
  61. package/src/framework/filters.ts +0 -194
  62. package/src/framework/graphql.buildHasuraConditions.test.ts +0 -473
  63. package/src/framework/graphql.paginationKey.test.ts +0 -29
  64. package/src/framework/graphql.test.ts +0 -286
  65. package/src/framework/graphql.ts +0 -462
  66. package/src/framework/native-runtime/index.tsx +0 -33
  67. package/src/framework/native-runtime/nativeComponents.test.ts +0 -108
  68. package/src/framework/runtime-reference.test.ts +0 -172
  69. package/src/framework/runtime.ts +0 -15
  70. package/src/framework/saved-filters.test.ts +0 -422
  71. package/src/framework/saved-filters.ts +0 -293
  72. package/src/framework/state.test.ts +0 -86
  73. package/src/framework/state.ts +0 -148
  74. package/src/framework/transform.test.ts +0 -51
  75. package/src/framework/view-parser-initialvalues.test.ts +0 -228
  76. package/src/framework/view-parser.ts +0 -714
  77. package/src/framework/view.test.ts +0 -1805
  78. package/src/framework/view.ts +0 -38
  79. package/src/index.css +0 -6
  80. package/src/main.tsx +0 -99
  81. package/src/views/index.ts +0 -12
  82. package/src/views/payment-requests/components/NoRowsExtendDateRange.tsx +0 -37
  83. package/src/views/payment-requests/components/PaymentMethod.tsx +0 -184
  84. package/src/views/payment-requests/components/PaymentStatusTag.tsx +0 -61
  85. package/src/views/payment-requests/index.ts +0 -1
  86. package/src/views/payment-requests/runtime.tsx +0 -145
  87. package/src/views/payment-requests/view.json +0 -692
  88. package/src/views/payment-requests-initial-values.test.ts +0 -73
  89. package/src/views/request-log/index.ts +0 -2
  90. package/src/views/request-log/runtime.tsx +0 -47
  91. package/src/views/request-log/view.json +0 -123
  92. package/src/views/simple-test-view/index.ts +0 -3
  93. package/src/views/simple-test-view/runtime.tsx +0 -85
  94. package/src/views/simple-test-view/view.json +0 -191
  95. package/src/vite-env.d.ts +0 -1
  96. package/tailwind.config.js +0 -7
  97. package/tsconfig.app.json +0 -26
  98. package/tsconfig.jest.json +0 -6
  99. package/tsconfig.json +0 -7
  100. package/tsconfig.node.json +0 -24
  101. package/vite.config.ts +0 -11
  102. /package/{public → dist}/vite.svg +0 -0
@@ -0,0 +1,17 @@
1
+ <!doctype html>
2
+ <html lang="en" class="h-full">
3
+
4
+ <head>
5
+ <meta charset="UTF-8" />
6
+ <link rel="icon" type="image/svg+xml" href="/portal/static/assets/table-views/vite.svg" />
7
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
8
+ <title>Filters Demo</title>
9
+ <script type="module" crossorigin src="/portal/static/assets/table-views/assets/index-BNgm6iTo.js"></script>
10
+ <link rel="stylesheet" crossorigin href="/portal/static/assets/table-views/assets/index-CKkrV_Rb.css">
11
+ </head>
12
+
13
+ <body class="h-full w-full">
14
+ <div id="root"></div>
15
+ </body>
16
+
17
+ </html>
package/package.json CHANGED
@@ -1,7 +1,11 @@
1
1
  {
2
2
  "name": "@kronor/dtv",
3
- "version": "0.2.9",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
+ "files": [
6
+ "dist",
7
+ "docs"
8
+ ],
5
9
  "scripts": {
6
10
  "dev": "vite",
7
11
  "build": "tsc -b && vite build",
package/.editorconfig DELETED
@@ -1,12 +0,0 @@
1
- root = true
2
-
3
- [*.{ts, tsx, json}]
4
- charset = utf-8
5
- end_of_line = lf
6
- insert_final_newline = true
7
- trim_trailing_whitespace = true
8
- indent_style = space
9
- indent_size = 4
10
-
11
- [*.md]
12
- trim_trailing_whitespace = false
@@ -1,64 +0,0 @@
1
- # Copilot Instructions for dtv
2
-
3
- ## Project Overview
4
- - This is a React + TypeScript monorepo using Vite for development/build, Playwright for E2E tests, and Jest for unit tests.
5
- - The core domain is a declarative, schema-driven table view system for filtering, displaying, and interacting with data collections.
6
- - Major code is in `src/`, with key subfolders:
7
- - `src/framework/`: Table/view schema, filter logic, state, and data fetching.
8
- - `src/components/`: UI components, including filter forms, AI assistant, and table rendering.
9
- - `src/views/`: View definitions, each exporting a `View` object with schema, columns, and query config.
10
-
11
- ## Key Patterns & Conventions
12
- - **Filter Schema**: Filters are defined in `FilterFieldSchema` objects, with each filter requiring an `aiGenerated: boolean` field. See `src/framework/filters.ts` for types and helpers.
13
- - **AI Integration**: The AI assistant (see `src/components/AIAssistantForm.tsx` and `src/components/aiAssistant.ts`) can generate filters, which must set `aiGenerated: true`.
14
- - **View Registration**: Views can be defined in two formats:
15
- - **TSX Format** (legacy): Each view exports a `View` object with schema, columns, and query config
16
- - **JSON Format** (new): Views are organized in folders with `view.json` (schema) and `runtime.tsx` (cell renderers)
17
- - See `src/views/simple-test-view/` and `src/views/request-log/` for JSON format examples
18
- - See `src/views/payment-requests/` for TSX format example
19
- - **Type Safety**: All filter and view schemas are strongly typed. When adding new filters, always specify all required fields.
20
- - **Cell Renderers**: All cell renderers receive `setFilterState` as a required prop, allowing them to programmatically update filter state when users interact with table cells.
21
-
22
- ## Testing & Code Quality
23
- - **Unit Tests**: Run with `npm run test-unit` (Jest)
24
- - **E2E Tests**: Run with `npm test` or `npm run test` (Playwright)
25
- - **Linting**: Run with `npm run lint` (ESLint with TypeScript)
26
- - E2E test files are located in `e2e/` directory
27
- - Unit test files use `.test.ts` or `.test.tsx` extensions
28
- - The file `COPILOT_TEST_COMMAND.txt` in the repo root also specifies the canonical unit test command for AI tools.
29
-
30
- ### Linting & Code Standards
31
- - ESLint is configured with TypeScript, React hooks, and React refresh rules
32
- - Pre-commit hooks automatically run linting and tests before commits
33
- - CI pipeline runs linting, unit tests, and E2E tests on push/PR
34
- - **EditorConfig**: Follow `.editorconfig` formatting rules for all files:
35
- - Use 4 spaces for indentation (TypeScript, TSX, JSON)
36
- - UTF-8 encoding with LF line endings
37
- - Insert final newline and trim trailing whitespace
38
- - When generating or editing JSON files, always use 4-space indentation to match project standards
39
- - Key rules:
40
- - `@typescript-eslint/no-explicit-any` is disabled to allow `any` types when needed
41
- - Use `@ts-expect-error` with descriptive comments instead of `@ts-ignore`
42
- - React Hook dependency warnings can be suppressed with `// eslint-disable-next-line react-hooks/exhaustive-deps` when intentional
43
- - Fast refresh warnings are acceptable (non-blocking) for files that export both components and utilities
44
-
45
- ## Integration & Data Flow
46
- - Data is fetched via GraphQL using `graphql-request` (see `src/framework/data.ts`).
47
- - Views define their own GraphQL queries and filter schemas.
48
- - Filter expressions are serialized/deserialized using helpers in `src/framework/filters.ts`.
49
-
50
- ## Project-Specific Advice
51
- - Always update all usages of schema types when changing filter/view schema fields.
52
- - When adding new filters, ensure `aiGenerated` is set appropriately.
53
- - Use the helpers in `src/framework/filters.ts` for building filter expressions and controls.
54
- - All cell renderers must accept `setFilterState` as a required prop for programmatic filter updates.
55
- - For new E2E tests, add Playwright specs in `e2e/`.
56
- - For new unit tests, add Jest specs with `.test.ts` or `.test.tsx` extensions.
57
-
58
- ## Examples
59
- - See `src/views/paymentRequest.tsx` for a full-featured view definition.
60
- - See `src/components/AIAssistantForm.tsx` for AI-driven filter generation.
61
- - See `src/framework/filters.ts` for filter schema/type definitions and utilities.
62
-
63
- ---
64
- If you are unsure about a workflow or convention, check for a helper or type in `src/framework/` or look for examples in `src/views/` and `src/components/`.
@@ -1,51 +0,0 @@
1
- name: CI
2
-
3
- on:
4
- push:
5
- branches: [ main ]
6
- pull_request:
7
- branches: [ main ]
8
-
9
- jobs:
10
- test:
11
- timeout-minutes: 60
12
- runs-on: ubuntu-latest
13
-
14
- strategy:
15
- matrix:
16
- node-version: [20.x]
17
-
18
- steps:
19
- - name: Checkout code
20
- uses: actions/checkout@v4
21
-
22
- - name: Setup Node.js ${{ matrix.node-version }}
23
- uses: actions/setup-node@v4
24
- with:
25
- node-version: lts/*
26
- cache: 'npm'
27
-
28
- - name: Install dependencies
29
- run: npm ci
30
-
31
- - name: Build application
32
- run: npm run build
33
-
34
- - name: Run linting
35
- run: npm run lint
36
-
37
- - name: Run unit tests
38
- run: npm run test-unit
39
-
40
- # - name: Install Playwright browsers
41
- # run: npx playwright install --with-deps
42
-
43
- # - name: Run Playwright tests
44
- # run: npx playwright test
45
-
46
- - uses: actions/upload-artifact@v4
47
- if: ${{ !cancelled() }}
48
- with:
49
- name: playwright-report
50
- path: playwright-report/
51
- retention-days: 30
package/.husky/pre-commit DELETED
@@ -1,8 +0,0 @@
1
- #!/usr/bin/env sh
2
- . "$(dirname -- "$0")/_/husky.sh"
3
-
4
- # Run linting first
5
- npm run lint
6
-
7
- # Run tests if linting passes
8
- npm test && npm run test-unit
package/e2e/app.spec.ts DELETED
@@ -1,6 +0,0 @@
1
- import { test, expect } from '@playwright/test';
2
-
3
- test('basic test', async ({ page }) => {
4
- await page.goto('/');
5
- await expect(page.locator('body')).toBeVisible(); // Replace with a more specific selector for your app
6
- });
@@ -1,63 +0,0 @@
1
- import { test, expect } from '@playwright/test';
2
- import { mockPaginationGraphQL } from './graphqlMock';
3
-
4
- test.describe('Cell Renderer setFilterState', () => {
5
- test('should allow cell renderers to programmatically set filter state', async ({ page }) => {
6
- // Intercept the GraphQL request and mock the response
7
- await page.route('**/v1/graphql', mockPaginationGraphQL);
8
-
9
- // Navigate to the page
10
- await page.goto('/?test-view=simple-test-view');
11
-
12
- // Wait for the table to be present and visible
13
- const table = page.getByRole('table');
14
- await expect(table).toBeVisible();
15
-
16
- // Verify initial state - all rows should be visible
17
- await expect(table.getByText('Test 30', { exact: true })).toBeVisible();
18
- await expect(table.getByText('Test 29', { exact: true })).toBeVisible();
19
- await expect(table.getByText('Test 28', { exact: true })).toBeVisible();
20
-
21
- // Note: This test verifies that the table renders correctly with the new required prop
22
- // In a real implementation, you would:
23
- // 1. Create a cell renderer that has a clickable element
24
- // 2. Click that element to trigger setFilterState
25
- // 3. Verify that the filter state changes accordingly
26
-
27
- // Test that filters work normally (ensuring our changes don't break existing functionality)
28
- // Show filters first
29
- await page.getByText('Filters', { exact: true }).click();
30
-
31
- // Find the Amount input and apply a filter (simple-test-view shows filters by default)
32
- const amountLabel = page.getByText('Amount', { exact: true });
33
- const amountInput = amountLabel.locator('..').locator('~ div input');
34
- await amountInput.fill('260');
35
- await amountInput.press('Enter');
36
-
37
- // Assert that only the filtered rows are visible
38
- await expect(table.getByText('Test 30', { exact: true })).toBeVisible();
39
- await expect(table.getByText('Test 27', { exact: true })).toBeVisible();
40
- await expect(table.getByText('Test 25', { exact: true })).not.toBeVisible();
41
- });
42
-
43
- test('should maintain table functionality with required setFilterState prop', async ({ page }) => {
44
- // Intercept the GraphQL request and mock the response
45
- await page.route('**/v1/graphql', mockPaginationGraphQL);
46
-
47
- // Navigate to the payment request view which has more complex cell renderers
48
- await page.goto('/?test-view=payment-requests');
49
-
50
- // Wait for the table to be present and visible
51
- const table = page.getByRole('table');
52
- await expect(table).toBeVisible();
53
-
54
- // Verify that the table headers are rendered correctly
55
- await expect(table.getByText('Transaction')).toBeVisible();
56
- await expect(table.getByText('Status')).toBeVisible();
57
- await expect(table.getByText('Amount')).toBeVisible();
58
-
59
- // Verify that cell renderers are working (they now receive setFilterState as required prop)
60
- // The fact that the page loads without errors indicates our changes are working
61
- await expect(table).toBeVisible();
62
- });
63
- });
@@ -1,113 +0,0 @@
1
- import { test, expect } from '@playwright/test';
2
- import { mockPaginationGraphQL } from './graphqlMock';
3
-
4
- test.describe('Filter Sharing', () => {
5
- test.beforeEach(async ({ page, context }) => {
6
- // Grant clipboard permissions
7
- await context.grantPermissions(['clipboard-read', 'clipboard-write']);
8
-
9
- // Intercept the GraphQL request and mock the response
10
- await page.route('**/v1/graphql', mockPaginationGraphQL);
11
- });
12
-
13
- test('should show Share Filter button and handle sharing', async ({ page }) => {
14
- // Navigate to the simple test view
15
- await page.goto('/?test-view=simple-test-view');
16
-
17
- // Wait for the table to be present and visible
18
- const table = page.getByRole('table');
19
- await expect(table).toBeVisible();
20
-
21
- // Open the filter form
22
- await page.getByText('Filters', { exact: true }).click();
23
-
24
- // Verify Share Filter button exists in the filter form
25
- const shareButton = page.getByRole('button', { name: 'Share Filter' });
26
- await expect(shareButton).toBeVisible();
27
-
28
- // If button is enabled, try sharing (some filter state may be present)
29
- const isEnabled = await shareButton.isEnabled();
30
- if (isEnabled) {
31
- // Click the share button
32
- await shareButton.click();
33
-
34
- // Wait a bit for any potential toast to appear
35
- await page.waitForTimeout(1000);
36
-
37
- // Check if a toast appeared (it may or may not appear depending on success)
38
- const toastMessages = page.locator('.p-toast .p-toast-message');
39
- const toastCount = await toastMessages.count();
40
-
41
- if (toastCount > 0) {
42
- // If toast is present, verify it's about sharing
43
- const toastText = await toastMessages.first().textContent();
44
- expect(toastText).toMatch(/share|filter|copied|clipboard/i);
45
- }
46
-
47
- // The main test is that the button exists and can be clicked without error
48
- }
49
- });
50
-
51
- test('should enable share button when filter is set', async ({ page }) => {
52
- // Navigate to the simple test view
53
- await page.goto('/?test-view=simple-test-view');
54
-
55
- // Wait for the table to be present and visible
56
- const table = page.getByRole('table');
57
- await expect(table).toBeVisible();
58
-
59
- // Open the filter form
60
- await page.getByText('Filters', { exact: true }).click();
61
-
62
- // Set a filter value using the pattern from working tests
63
- const emailInput = page.getByText('Email', { exact: true }).locator('..').locator('~ div input');
64
- await emailInput.fill('test@example.com');
65
-
66
- // Apply the filter
67
- await page.getByLabel('Apply filter').click();
68
-
69
- // The filter form should still be open, verify Share Filter button is now enabled
70
- const shareButton = page.getByRole('button', { name: 'Share Filter' });
71
- await expect(shareButton).toBeEnabled();
72
-
73
- // Click the share button
74
- await shareButton.click();
75
-
76
- // Wait a bit for any potential toast to appear
77
- await page.waitForTimeout(1000);
78
-
79
- // Check if a toast appeared
80
- const toastMessages = page.locator('.p-toast .p-toast-message');
81
- const toastCount = await toastMessages.count();
82
-
83
- if (toastCount > 0) {
84
- // If toast is present, verify it's about sharing
85
- const toastText = await toastMessages.first().textContent();
86
- expect(toastText).toMatch(/share|filter|copied|clipboard/i);
87
- }
88
-
89
- // The main test is that the button works and can be clicked
90
- });
91
-
92
- test('should handle invalid filter URL parameter', async ({ page }) => {
93
- // Navigate with an invalid filter parameter
94
- await page.goto('/?test-view=simple-test-view&dtv-filter-state=invalid-base64-data');
95
-
96
- // Wait for the table to be present and visible (app should still load)
97
- const table = page.getByRole('table');
98
- await expect(table).toBeVisible();
99
-
100
- // Give the app time to process the invalid filter and show toast
101
- await page.waitForTimeout(1000);
102
-
103
- // Check if any toast appears (might be warning about invalid filter)
104
- const toast = page.locator('.p-toast');
105
- const toastVisible = await toast.isVisible();
106
-
107
- if (toastVisible) {
108
- // If toast is present, verify it contains relevant text
109
- const toastText = await toast.textContent();
110
- expect(toastText).toMatch(/invalid|filter|error/i);
111
- }
112
- });
113
- });
@@ -1,36 +0,0 @@
1
- import { test, expect } from '@playwright/test';
2
- import { mockPaginationGraphQL } from './graphqlMock';
3
-
4
- function getParam(url: string, key: string) { return new URL(url).searchParams.get(key); }
5
-
6
- // Base64 URL-safe pattern (rough)
7
- const b64urlRe = /^[A-Za-z0-9_-]+$/;
8
-
9
- test.describe('Filter state URL persistence flag', () => {
10
- test('disabled: applying filter does not set dtv-filter-state', async ({ page }) => {
11
- await page.route('**/v1/graphql', mockPaginationGraphQL);
12
- await page.goto('/?test-view=simple-test-view');
13
- await page.getByText('Filters', { exact: true }).click();
14
- const amountLabel = page.getByText('Amount', { exact: true });
15
- const amountInput = amountLabel.locator('..').locator('~ div input');
16
- await amountInput.fill('260');
17
- await amountInput.press('Enter');
18
- expect(getParam(page.url(), 'dtv-filter-state')).toBeNull();
19
- });
20
-
21
- test('enabled: applying filter sets and persists dtv-filter-state', async ({ page }) => {
22
- await page.route('**/v1/graphql', mockPaginationGraphQL);
23
- await page.goto('/?test-view=simple-test-view&sync-filter-state-to-url=true');
24
- await page.getByText('Filters', { exact: true }).click();
25
- const amountLabel = page.getByText('Amount', { exact: true });
26
- const amountInput = amountLabel.locator('..').locator('~ div input');
27
- await amountInput.fill('260');
28
- await amountInput.press('Enter');
29
- const encoded = getParam(page.url(), 'dtv-filter-state');
30
- expect(encoded).not.toBeNull();
31
- expect(encoded).toMatch(b64urlRe);
32
- await page.reload();
33
- await expect(page.getByRole('table')).toBeVisible();
34
- expect(getParam(page.url(), 'dtv-filter-state')).not.toBeNull();
35
- });
36
- });
@@ -1,144 +0,0 @@
1
- // Shared GraphQL mock handler for e2e tests (pagination and simple view)
2
- import { Route } from '@playwright/test';
3
-
4
- // Fixed dataset: 30 items (1.5 pages at 20 per page)
5
- const allRows = Array.from({ length: 30 }, (_, i) => ({
6
- id: i + 1,
7
- testField: `Test ${i + 1}`,
8
- amount: (i + 1) * 10,
9
- email: `user${i + 1}@example.com`,
10
- phone: `+467000000${(i + 1).toString().padStart(2, '0')}`
11
- }));
12
-
13
- export async function mockPaginationGraphQL(route: Route) {
14
- const request = route.request();
15
- const postData = request.postDataJSON?.();
16
- // Use paginationKey (id._gt or id._lt) to determine the start index, depending on orderBy
17
- let startIdx = 0;
18
- let pageSize = 20;
19
- let orderKey = 'id';
20
- let orderDir = 'asc';
21
- if (postData && postData.variables && typeof postData.variables.limit === 'number') {
22
- pageSize = postData.variables.limit;
23
- }
24
- if (postData && postData.variables && Array.isArray(postData.variables.orderBy) && postData.variables.orderBy.length > 0) {
25
- const order = postData.variables.orderBy[0];
26
- orderKey = Object.keys(order)[0];
27
- orderDir = order[orderKey];
28
- }
29
- // Sort allRows according to orderBy
30
- const sortedRows = allRows.slice();
31
- sortedRows.sort((a, b) => {
32
- if (a[orderKey] < b[orderKey]) return orderDir === 'ASC' ? -1 : 1;
33
- if (a[orderKey] > b[orderKey]) return orderDir === 'ASC' ? 1 : -1;
34
- return 0;
35
- });
36
- // Helper to recursively extract _gt/_lt for the pagination key from a condition tree
37
- function findPaginationCursor(cond: any, key: string, orderDir: string): number | undefined {
38
- if (!cond) return undefined;
39
- if (cond._and && Array.isArray(cond._and)) {
40
- for (const sub of cond._and) {
41
- const found = findPaginationCursor(sub, key, orderDir);
42
- if (found !== undefined) return found;
43
- }
44
- }
45
- if (cond._or && Array.isArray(cond._or)) {
46
- for (const sub of cond._or) {
47
- const found = findPaginationCursor(sub, key, orderDir);
48
- if (found !== undefined) return found;
49
- }
50
- }
51
- if (cond[key]) {
52
- if (orderDir === 'ASC' && cond[key]._gt !== undefined) return Number(cond[key]._gt);
53
- if (orderDir === 'DESC' && cond[key]._lt !== undefined) return Number(cond[key]._lt);
54
- }
55
- return undefined;
56
- }
57
- // Apply additional filters (e.g. by amount, email)
58
- function applyFilters(rows: typeof allRows, conditions: any): typeof allRows {
59
- if (!conditions) return rows;
60
-
61
- return rows.filter(row => evaluateCondition(row, conditions));
62
- }
63
-
64
- // Recursively evaluate a condition against a row
65
- function evaluateCondition(row: any, condition: any): boolean {
66
- // Handle logical operators
67
- if (condition._and) {
68
- return condition._and.every((subCondition: any) => evaluateCondition(row, subCondition));
69
- }
70
- if (condition._or) {
71
- return condition._or.some((subCondition: any) => evaluateCondition(row, subCondition));
72
- }
73
- if (condition._not) {
74
- return !evaluateCondition(row, condition._not);
75
- }
76
-
77
- // Handle field conditions
78
- let pass = true;
79
-
80
- // Check each field in the condition
81
- for (const [fieldName, fieldCondition] of Object.entries(condition)) {
82
- if (fieldName.startsWith('_')) continue; // Skip logical operators
83
-
84
- const fieldValue = row[fieldName];
85
- const ops = fieldCondition as any;
86
-
87
- if (ops._eq !== undefined) {
88
- if (fieldName === 'transformedField') {
89
- // Special handling for transformedField - convert back to testField
90
- const expectedValue = ops._eq.replace('prefix_', 'Test ');
91
- pass = pass && row.testField === expectedValue;
92
- } else {
93
- pass = pass && fieldValue === ops._eq;
94
- }
95
- }
96
- if (ops._neq !== undefined) pass = pass && fieldValue !== ops._neq;
97
- if (ops._gt !== undefined) pass = pass && fieldValue > ops._gt;
98
- if (ops._lt !== undefined) pass = pass && fieldValue < ops._lt;
99
- if (ops._gte !== undefined) pass = pass && fieldValue >= ops._gte;
100
- if (ops._lte !== undefined) pass = pass && fieldValue <= ops._lte;
101
- if (ops._in !== undefined) pass = pass && Array.isArray(ops._in) && ops._in.includes(fieldValue);
102
- if (ops._nin !== undefined) pass = pass && (!Array.isArray(ops._nin) || !ops._nin.includes(fieldValue));
103
- if (ops._like !== undefined) {
104
- // Convert SQL LIKE pattern to regex
105
- const pattern = ops._like.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/%/g, '.*').replace(/_/g, '.');
106
- const regex = new RegExp(`^${pattern}$`);
107
- pass = pass && regex.test(String(fieldValue));
108
- }
109
- if (ops._ilike !== undefined) {
110
- // Convert SQL ILIKE pattern to case-insensitive regex
111
- const pattern = ops._ilike.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/%/g, '.*').replace(/_/g, '.');
112
- const regex = new RegExp(`^${pattern}$`, 'i');
113
- pass = pass && regex.test(String(fieldValue));
114
- }
115
- if (ops._is_null !== undefined) pass = pass && ((fieldValue == null) === ops._is_null);
116
- }
117
-
118
- return pass;
119
- }
120
- // Pagination: use _gt for asc, _lt for desc, recursively
121
- let cursorValue: number | undefined = undefined;
122
- let filteredRows = sortedRows;
123
- if (postData && postData.variables && postData.variables.conditions) {
124
- // Apply filters first
125
- filteredRows = applyFilters(sortedRows, postData.variables.conditions);
126
- // Then pagination cursor
127
- cursorValue = findPaginationCursor(postData.variables.conditions, orderKey, orderDir);
128
- if (cursorValue !== undefined) {
129
- const idx = filteredRows.findIndex(r => Number((r as any)[orderKey]) === cursorValue);
130
- startIdx = idx >= 0 ? idx + 1 : 0;
131
- }
132
- }
133
-
134
- const data = filteredRows.slice(startIdx, startIdx + pageSize);
135
- await route.fulfill({
136
- status: 200,
137
- contentType: 'application/json',
138
- body: JSON.stringify({
139
- data: {
140
- simpleTestDataCollection: data
141
- }
142
- })
143
- });
144
- }
@@ -1,95 +0,0 @@
1
- import { test, expect } from '@playwright/test';
2
- import { mockPaginationGraphQL } from './graphqlMock';
3
-
4
- test.describe('Multi-field Filter Support', () => {
5
- test('should handle OR multi-field filters correctly', async ({ page }) => {
6
- // Intercept and mock GraphQL requests
7
- await page.route('**/v1/graphql', mockPaginationGraphQL);
8
-
9
- // Navigate to the simple test view
10
- await page.goto('/?test-view=simple-test-view');
11
-
12
- // Wait for the table to be present
13
- const table = page.getByRole('table');
14
- await expect(table).toBeVisible();
15
-
16
- // Show filters
17
- await page.getByText('Filters', { exact: true }).click();
18
-
19
- // Find the "Search Multiple Fields (OR)" filter
20
- const multiFieldLabel = page.getByText('Search Multiple Fields (OR)', { exact: true });
21
- await expect(multiFieldLabel).toBeVisible();
22
-
23
- // Find the input for this filter (it should be in the same container)
24
- const multiFieldInput = multiFieldLabel.locator('..').locator('~ div input');
25
-
26
- // Enter a search term that should match either testField or email
27
- await multiFieldInput.fill('%Test%');
28
- await multiFieldInput.press('Enter');
29
-
30
- // The table should still show data (our mock data has "Test" in testField)
31
- await expect(table).toBeVisible();
32
-
33
- // Check that we can see some test data - be more specific to avoid multiple matches
34
- await expect(table.getByText('Test 30')).toBeVisible();
35
- });
36
-
37
- test('should handle AND multi-field filters correctly', async ({ page }) => {
38
- // Intercept and mock GraphQL requests
39
- await page.route('**/v1/graphql', mockPaginationGraphQL);
40
-
41
- // Navigate to the simple test view
42
- await page.goto('/?test-view=simple-test-view');
43
-
44
- // Wait for the table to be present
45
- const table = page.getByRole('table');
46
- await expect(table).toBeVisible();
47
-
48
- // Show filters
49
- await page.getByText('Filters', { exact: true }).click();
50
-
51
- // Find the "Match Multiple Fields (AND)" filter
52
- const andFieldLabel = page.getByText('Match Multiple Fields (AND)', { exact: true });
53
- await expect(andFieldLabel).toBeVisible();
54
-
55
- // Find the input for this filter
56
- const andFieldInput = andFieldLabel.locator('..').locator('~ div input');
57
-
58
- // Enter a value that would need to match both fields exactly
59
- await andFieldInput.fill('exact_match');
60
- await andFieldInput.press('Enter');
61
-
62
- // The table should still be visible (even if no results, the structure remains)
63
- await expect(table).toBeVisible();
64
- });
65
-
66
- test('should work with existing filters', async ({ page }) => {
67
- // Intercept and mock GraphQL requests
68
- await page.route('**/v1/graphql', mockPaginationGraphQL);
69
-
70
- // Navigate to the simple test view
71
- await page.goto('/?test-view=simple-test-view');
72
-
73
- // Wait for the table to be present
74
- const table = page.getByRole('table');
75
- await expect(table).toBeVisible();
76
-
77
- // Show filters
78
- await page.getByText('Filters', { exact: true }).click();
79
-
80
- // Use both a regular filter and a multi-field filter
81
- const emailLabel = page.getByText('Email', { exact: true });
82
- const emailInput = emailLabel.locator('..').locator('~ div input');
83
- await emailInput.fill('test@example.com');
84
- await emailInput.press('Enter');
85
-
86
- // Also use the multi-field OR filter
87
- const multiFieldLabel = page.getByText('Search Multiple Fields (OR)', { exact: true });
88
- const multiFieldInput = multiFieldLabel.locator('..').locator('~ div input');
89
- await multiFieldInput.fill('%Test%');
90
- await multiFieldInput.press('Enter');
91
-
92
- // Both filters should work together
93
- await expect(table).toBeVisible();
94
- });
95
- });