@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.
- package/dist/assets/InterVariable-CWi-zmRD.woff2 +0 -0
- package/dist/assets/InterVariable-Italic-d6KXgdvN.woff2 +0 -0
- package/dist/assets/index-BNgm6iTo.js +2105 -0
- package/dist/assets/index-CKkrV_Rb.css +1 -0
- package/dist/assets/primeicons-C6QP2o4f.woff2 +0 -0
- package/dist/assets/primeicons-DMOk5skT.eot +0 -0
- package/dist/assets/primeicons-Dr5RGzOO.svg +345 -0
- package/dist/assets/primeicons-MpK4pl85.ttf +0 -0
- package/dist/assets/primeicons-WjwUDZjB.woff +0 -0
- package/dist/index.html +17 -0
- package/package.json +5 -1
- package/.editorconfig +0 -12
- package/.github/copilot-instructions.md +0 -64
- package/.github/workflows/ci.yml +0 -51
- package/.husky/pre-commit +0 -8
- package/e2e/app.spec.ts +0 -6
- package/e2e/cell-renderer-setfilterstate.spec.ts +0 -63
- package/e2e/filter-sharing.spec.ts +0 -113
- package/e2e/filter-url-persistence.spec.ts +0 -36
- package/e2e/graphqlMock.ts +0 -144
- package/e2e/multi-field-filters.spec.ts +0 -95
- package/e2e/pagination.spec.ts +0 -38
- package/e2e/payment-request-email-filter.spec.ts +0 -67
- package/e2e/save-filter-splitbutton.spec.ts +0 -68
- package/e2e/simple-view-email-filter.spec.ts +0 -67
- package/e2e/simple-view-transforms.spec.ts +0 -171
- package/e2e/simple-view.spec.ts +0 -104
- package/e2e/transform-regression.spec.ts +0 -108
- package/eslint.config.js +0 -30
- package/index.html +0 -17
- package/jest.config.js +0 -10
- package/playwright.config.ts +0 -54
- package/src/App.externalRuntime.test.ts +0 -190
- package/src/App.tsx +0 -540
- package/src/assets/react.svg +0 -1
- package/src/components/AIAssistantForm.tsx +0 -241
- package/src/components/FilterForm.test.ts +0 -82
- package/src/components/FilterForm.tsx +0 -375
- package/src/components/PhoneNumberFilter.tsx +0 -102
- package/src/components/SavedFilterList.tsx +0 -181
- package/src/components/SpeechInput.tsx +0 -67
- package/src/components/Table.tsx +0 -119
- package/src/components/TablePagination.tsx +0 -40
- package/src/components/aiAssistant.test.ts +0 -270
- package/src/components/aiAssistant.ts +0 -291
- package/src/framework/cell-renderer-components/CurrencyAmount.tsx +0 -30
- package/src/framework/cell-renderer-components/LayoutHelpers.tsx +0 -74
- package/src/framework/cell-renderer-components/Link.tsx +0 -28
- package/src/framework/cell-renderer-components/Mapping.tsx +0 -11
- package/src/framework/cell-renderer-components.test.ts +0 -353
- package/src/framework/column-definition.tsx +0 -85
- package/src/framework/currency.test.ts +0 -46
- package/src/framework/currency.ts +0 -62
- package/src/framework/data.staticConditions.test.ts +0 -46
- package/src/framework/data.test.ts +0 -167
- package/src/framework/data.ts +0 -162
- package/src/framework/filter-form-state.test.ts +0 -189
- package/src/framework/filter-form-state.ts +0 -185
- package/src/framework/filter-sharing.test.ts +0 -135
- package/src/framework/filter-sharing.ts +0 -118
- package/src/framework/filters.ts +0 -194
- package/src/framework/graphql.buildHasuraConditions.test.ts +0 -473
- package/src/framework/graphql.paginationKey.test.ts +0 -29
- package/src/framework/graphql.test.ts +0 -286
- package/src/framework/graphql.ts +0 -462
- package/src/framework/native-runtime/index.tsx +0 -33
- package/src/framework/native-runtime/nativeComponents.test.ts +0 -108
- package/src/framework/runtime-reference.test.ts +0 -172
- package/src/framework/runtime.ts +0 -15
- package/src/framework/saved-filters.test.ts +0 -422
- package/src/framework/saved-filters.ts +0 -293
- package/src/framework/state.test.ts +0 -86
- package/src/framework/state.ts +0 -148
- package/src/framework/transform.test.ts +0 -51
- package/src/framework/view-parser-initialvalues.test.ts +0 -228
- package/src/framework/view-parser.ts +0 -714
- package/src/framework/view.test.ts +0 -1805
- package/src/framework/view.ts +0 -38
- package/src/index.css +0 -6
- package/src/main.tsx +0 -99
- package/src/views/index.ts +0 -12
- package/src/views/payment-requests/components/NoRowsExtendDateRange.tsx +0 -37
- package/src/views/payment-requests/components/PaymentMethod.tsx +0 -184
- package/src/views/payment-requests/components/PaymentStatusTag.tsx +0 -61
- package/src/views/payment-requests/index.ts +0 -1
- package/src/views/payment-requests/runtime.tsx +0 -145
- package/src/views/payment-requests/view.json +0 -692
- package/src/views/payment-requests-initial-values.test.ts +0 -73
- package/src/views/request-log/index.ts +0 -2
- package/src/views/request-log/runtime.tsx +0 -47
- package/src/views/request-log/view.json +0 -123
- package/src/views/simple-test-view/index.ts +0 -3
- package/src/views/simple-test-view/runtime.tsx +0 -85
- package/src/views/simple-test-view/view.json +0 -191
- package/src/vite-env.d.ts +0 -1
- package/tailwind.config.js +0 -7
- package/tsconfig.app.json +0 -26
- package/tsconfig.jest.json +0 -6
- package/tsconfig.json +0 -7
- package/tsconfig.node.json +0 -24
- package/vite.config.ts +0 -11
- /package/{public → dist}/vite.svg +0 -0
|
Binary file
|
|
Binary file
|
package/dist/index.html
ADDED
|
@@ -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
package/.editorconfig
DELETED
|
@@ -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/`.
|
package/.github/workflows/ci.yml
DELETED
|
@@ -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
package/e2e/app.spec.ts
DELETED
|
@@ -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
|
-
});
|
package/e2e/graphqlMock.ts
DELETED
|
@@ -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
|
-
});
|