@kronor/dtv 0.2.9

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 (97) hide show
  1. package/.editorconfig +12 -0
  2. package/.github/copilot-instructions.md +64 -0
  3. package/.github/workflows/ci.yml +51 -0
  4. package/.husky/pre-commit +8 -0
  5. package/README.md +63 -0
  6. package/docs/api/README.md +32 -0
  7. package/docs/api/cell-renderers.md +121 -0
  8. package/docs/api/no-rows-component.md +71 -0
  9. package/docs/api/runtime.md +78 -0
  10. package/e2e/app.spec.ts +6 -0
  11. package/e2e/cell-renderer-setfilterstate.spec.ts +63 -0
  12. package/e2e/filter-sharing.spec.ts +113 -0
  13. package/e2e/filter-url-persistence.spec.ts +36 -0
  14. package/e2e/graphqlMock.ts +144 -0
  15. package/e2e/multi-field-filters.spec.ts +95 -0
  16. package/e2e/pagination.spec.ts +38 -0
  17. package/e2e/payment-request-email-filter.spec.ts +67 -0
  18. package/e2e/save-filter-splitbutton.spec.ts +68 -0
  19. package/e2e/simple-view-email-filter.spec.ts +67 -0
  20. package/e2e/simple-view-transforms.spec.ts +171 -0
  21. package/e2e/simple-view.spec.ts +104 -0
  22. package/e2e/transform-regression.spec.ts +108 -0
  23. package/eslint.config.js +30 -0
  24. package/index.html +17 -0
  25. package/jest.config.js +10 -0
  26. package/package.json +45 -0
  27. package/playwright.config.ts +54 -0
  28. package/public/vite.svg +1 -0
  29. package/src/App.externalRuntime.test.ts +190 -0
  30. package/src/App.tsx +540 -0
  31. package/src/assets/react.svg +1 -0
  32. package/src/components/AIAssistantForm.tsx +241 -0
  33. package/src/components/FilterForm.test.ts +82 -0
  34. package/src/components/FilterForm.tsx +375 -0
  35. package/src/components/PhoneNumberFilter.tsx +102 -0
  36. package/src/components/SavedFilterList.tsx +181 -0
  37. package/src/components/SpeechInput.tsx +67 -0
  38. package/src/components/Table.tsx +119 -0
  39. package/src/components/TablePagination.tsx +40 -0
  40. package/src/components/aiAssistant.test.ts +270 -0
  41. package/src/components/aiAssistant.ts +291 -0
  42. package/src/framework/cell-renderer-components/CurrencyAmount.tsx +30 -0
  43. package/src/framework/cell-renderer-components/LayoutHelpers.tsx +74 -0
  44. package/src/framework/cell-renderer-components/Link.tsx +28 -0
  45. package/src/framework/cell-renderer-components/Mapping.tsx +11 -0
  46. package/src/framework/cell-renderer-components.test.ts +353 -0
  47. package/src/framework/column-definition.tsx +85 -0
  48. package/src/framework/currency.test.ts +46 -0
  49. package/src/framework/currency.ts +62 -0
  50. package/src/framework/data.staticConditions.test.ts +46 -0
  51. package/src/framework/data.test.ts +167 -0
  52. package/src/framework/data.ts +162 -0
  53. package/src/framework/filter-form-state.test.ts +189 -0
  54. package/src/framework/filter-form-state.ts +185 -0
  55. package/src/framework/filter-sharing.test.ts +135 -0
  56. package/src/framework/filter-sharing.ts +118 -0
  57. package/src/framework/filters.ts +194 -0
  58. package/src/framework/graphql.buildHasuraConditions.test.ts +473 -0
  59. package/src/framework/graphql.paginationKey.test.ts +29 -0
  60. package/src/framework/graphql.test.ts +286 -0
  61. package/src/framework/graphql.ts +462 -0
  62. package/src/framework/native-runtime/index.tsx +33 -0
  63. package/src/framework/native-runtime/nativeComponents.test.ts +108 -0
  64. package/src/framework/runtime-reference.test.ts +172 -0
  65. package/src/framework/runtime.ts +15 -0
  66. package/src/framework/saved-filters.test.ts +422 -0
  67. package/src/framework/saved-filters.ts +293 -0
  68. package/src/framework/state.test.ts +86 -0
  69. package/src/framework/state.ts +148 -0
  70. package/src/framework/transform.test.ts +51 -0
  71. package/src/framework/view-parser-initialvalues.test.ts +228 -0
  72. package/src/framework/view-parser.ts +714 -0
  73. package/src/framework/view.test.ts +1805 -0
  74. package/src/framework/view.ts +38 -0
  75. package/src/index.css +6 -0
  76. package/src/main.tsx +99 -0
  77. package/src/views/index.ts +12 -0
  78. package/src/views/payment-requests/components/NoRowsExtendDateRange.tsx +37 -0
  79. package/src/views/payment-requests/components/PaymentMethod.tsx +184 -0
  80. package/src/views/payment-requests/components/PaymentStatusTag.tsx +61 -0
  81. package/src/views/payment-requests/index.ts +1 -0
  82. package/src/views/payment-requests/runtime.tsx +145 -0
  83. package/src/views/payment-requests/view.json +692 -0
  84. package/src/views/payment-requests-initial-values.test.ts +73 -0
  85. package/src/views/request-log/index.ts +2 -0
  86. package/src/views/request-log/runtime.tsx +47 -0
  87. package/src/views/request-log/view.json +123 -0
  88. package/src/views/simple-test-view/index.ts +3 -0
  89. package/src/views/simple-test-view/runtime.tsx +85 -0
  90. package/src/views/simple-test-view/view.json +191 -0
  91. package/src/vite-env.d.ts +1 -0
  92. package/tailwind.config.js +7 -0
  93. package/tsconfig.app.json +26 -0
  94. package/tsconfig.jest.json +6 -0
  95. package/tsconfig.json +7 -0
  96. package/tsconfig.node.json +24 -0
  97. package/vite.config.ts +11 -0
@@ -0,0 +1,171 @@
1
+ import { test, expect } from '@playwright/test';
2
+ import { mockPaginationGraphQL } from './graphqlMock';
3
+
4
+ test.describe('Simple View Transform Functionality', () => {
5
+
6
+ test('should apply transform functions when filtering by amount', async ({ page }) => {
7
+ // Intercept the GraphQL request and mock the response
8
+ await page.route('**/v1/graphql', mockPaginationGraphQL);
9
+
10
+ // Navigate to the page with the simple test view
11
+ await page.goto('/?test-view=simple-test-view');
12
+
13
+ // Wait for the table to be present and visible
14
+ const table = page.getByRole('table');
15
+ await expect(table).toBeVisible();
16
+
17
+ // Verify all rows are initially visible
18
+ await expect(table.getByText('Test 30', { exact: true })).toBeVisible();
19
+ await expect(table.getByText('Test 29', { exact: true })).toBeVisible();
20
+ await expect(table.getByText('Test 28', { exact: true })).toBeVisible();
21
+ await expect(table.getByText('Test 27', { exact: true })).toBeVisible();
22
+ await expect(table.getByText('Test 26', { exact: true })).toBeVisible();
23
+ await expect(table.getByText('Test 25', { exact: true })).toBeVisible();
24
+ await expect(table.getByText('$300', { exact: true })).toBeVisible(); // amount for Test 30
25
+ await expect(table.getByText('$290', { exact: true })).toBeVisible(); // amount for Test 29
26
+ await expect(table.getByText('$280', { exact: true })).toBeVisible(); // amount for Test 28
27
+ await expect(table.getByText('$270', { exact: true })).toBeVisible(); // amount for Test 27
28
+ await expect(table.getByText('$260', { exact: true })).toBeVisible(); // amount for Test 26
29
+ await expect(table.getByText('$250', { exact: true })).toBeVisible(); // amount for Test 25
30
+
31
+ // Show filters first
32
+ await page.getByText('Filters', { exact: true }).click();
33
+
34
+ // Find the amount filter input
35
+ const amountLabel = page.getByText('Amount', { exact: true });
36
+ const amountInput = amountLabel.locator('..').locator('~ div input');
37
+
38
+ // Enter 255 in the input field
39
+ // With transform: toQuery adds 5, so 255 becomes 260 in the query
40
+ // This should show only rows with amount >= 260 (Test 26, 27, 28, 29, 30)
41
+ await amountInput.fill('255');
42
+ await amountInput.press('Enter');
43
+
44
+ // Wait for filtering to complete and verify results
45
+ // These should still be visible (amount >= 260)
46
+ await expect(table.getByText('Test 30', { exact: true })).toBeVisible();
47
+ await expect(table.getByText('Test 29', { exact: true })).toBeVisible();
48
+ await expect(table.getByText('Test 28', { exact: true })).toBeVisible();
49
+ await expect(table.getByText('Test 27', { exact: true })).toBeVisible();
50
+ await expect(table.getByText('Test 26', { exact: true })).toBeVisible();
51
+ await expect(table.getByText('$300', { exact: true })).toBeVisible();
52
+ await expect(table.getByText('$290', { exact: true })).toBeVisible();
53
+ await expect(table.getByText('$280', { exact: true })).toBeVisible();
54
+ await expect(table.getByText('$270', { exact: true })).toBeVisible();
55
+ await expect(table.getByText('$260', { exact: true })).toBeVisible();
56
+
57
+ // These should not be visible (amount < 260)
58
+ await expect(table.getByText('Test 25', { exact: true })).not.toBeVisible();
59
+ await expect(table.getByText('Test 24', { exact: true })).not.toBeVisible();
60
+ await expect(table.getByText('Test 23', { exact: true })).not.toBeVisible();
61
+ await expect(table.getByText('$250', { exact: true })).not.toBeVisible();
62
+ await expect(table.getByText('$240', { exact: true })).not.toBeVisible();
63
+ await expect(table.getByText('$230', { exact: true })).not.toBeVisible();
64
+ });
65
+
66
+ test('should handle multiple filter value changes with transforms', async ({ page }) => {
67
+ // Intercept the GraphQL request and mock the response
68
+ await page.route('**/v1/graphql', mockPaginationGraphQL);
69
+
70
+ // Navigate to the page with the simple test view
71
+ await page.goto('/?test-view=simple-test-view');
72
+
73
+ // Wait for the table to be present and visible
74
+ const table = page.getByRole('table');
75
+ await expect(table).toBeVisible();
76
+
77
+ // Show filters first
78
+ await page.getByText('Filters', { exact: true }).click();
79
+
80
+ // Find the amount filter input
81
+ const amountLabel = page.getByText('Amount', { exact: true });
82
+ const amountInput = amountLabel.locator('..').locator('~ div input');
83
+
84
+ // Test 1: Enter 245 (transforms to 250), should show Test 25 and up
85
+ await amountInput.fill('245');
86
+ await amountInput.press('Enter');
87
+
88
+ await expect(table.getByText('Test 30', { exact: true })).toBeVisible();
89
+ await expect(table.getByText('Test 25', { exact: true })).toBeVisible();
90
+ await expect(table.getByText('Test 24', { exact: true })).not.toBeVisible();
91
+
92
+ // Test 2: Change to 265 (transforms to 270), should show Test 27 and up
93
+ await amountInput.clear();
94
+ await amountInput.fill('265');
95
+ await amountInput.press('Enter');
96
+
97
+ await expect(table.getByText('Test 30', { exact: true })).toBeVisible();
98
+ await expect(table.getByText('Test 27', { exact: true })).toBeVisible();
99
+ await expect(table.getByText('Test 26', { exact: true })).not.toBeVisible();
100
+ await expect(table.getByText('Test 25', { exact: true })).not.toBeVisible();
101
+
102
+ // Test 3: Clear filter should show all items again
103
+ await amountInput.clear();
104
+ await amountInput.press('Enter');
105
+
106
+ await expect(table.getByText('Test 30', { exact: true })).toBeVisible();
107
+ await expect(table.getByText('Test 25', { exact: true })).toBeVisible();
108
+ await expect(table.getByText('Test 24', { exact: true })).toBeVisible();
109
+ });
110
+
111
+ test('should handle key-value transform objects', async ({ page }) => {
112
+ // Intercept the GraphQL request and mock the response
113
+ await page.route('**/v1/graphql', mockPaginationGraphQL);
114
+
115
+ // Navigate to the page with the simple test view
116
+ await page.goto('/?test-view=simple-test-view');
117
+
118
+ // Wait for the table to be present and visible
119
+ const table = page.getByRole('table');
120
+ await expect(table).toBeVisible();
121
+
122
+ // Verify all rows are initially visible
123
+ await expect(table.getByText('Test 30', { exact: true })).toBeVisible();
124
+ await expect(table.getByText('Test 29', { exact: true })).toBeVisible();
125
+
126
+ // Show filters first
127
+ await page.getByText('Filters', { exact: true }).click();
128
+
129
+ // Find the extra filters panel
130
+ const extraFiltersPanel = page.locator('.p-panel-header', { hasText: 'Extra Filters' });
131
+ await expect(extraFiltersPanel).toBeVisible();
132
+
133
+ // Expand the extra filters panel if needed
134
+ if (await extraFiltersPanel.getAttribute('aria-expanded') !== 'true') {
135
+ await extraFiltersPanel.click();
136
+ }
137
+
138
+ // Find the key-value transform filter input
139
+ const keyValueLabel = page.getByText('Test Field (Key-Value Transform)', { exact: true });
140
+ await expect(keyValueLabel).toBeVisible();
141
+
142
+ const keyValueInput = keyValueLabel.locator('..').locator('~ div input');
143
+ await expect(keyValueInput).toBeVisible();
144
+
145
+ // Test 1: Enter a value that should be transformed
146
+ // The transform should add "prefix_" to the input and change the field to "transformedField"
147
+ await keyValueInput.fill('30');
148
+ await keyValueInput.press('Enter');
149
+
150
+ // Wait a bit for the filter to apply
151
+ await page.waitForTimeout(2000);
152
+
153
+ // Verify the correct row is shown (Test 30 should be visible since it matches the transformed query)
154
+ await expect(table.getByText('Test 30', { exact: true })).toBeVisible();
155
+ // Verify other rows are hidden
156
+ await expect(table.getByText('Test 29', { exact: true })).not.toBeVisible();
157
+
158
+ // Test 2: Clear the filter to verify all rows show again
159
+ await keyValueInput.clear();
160
+ await keyValueInput.press('Enter');
161
+
162
+ // Wait for the filter to clear
163
+ await page.waitForTimeout(1000);
164
+
165
+ // All rows should be visible again
166
+ await expect(table.getByText('Test 30', { exact: true })).toBeVisible();
167
+ await expect(table.getByText('Test 29', { exact: true })).toBeVisible();
168
+ await expect(table.getByText('Test 28', { exact: true })).toBeVisible();
169
+ });
170
+
171
+ });
@@ -0,0 +1,104 @@
1
+ import { test, expect } from '@playwright/test';
2
+ import { mockPaginationGraphQL } from './graphqlMock';
3
+
4
+ test.describe('Simple View Rendering', () => {
5
+
6
+ test('should filter by phone using a custom filter component', async ({ page }) => {
7
+ // Intercept the GraphQL request and mock the response
8
+ await page.route('**/v1/graphql', mockPaginationGraphQL);
9
+
10
+ // Navigate to the page with the simple test view
11
+ await page.goto('/?test-view=simple-test-view');
12
+
13
+ // Wait for the table to be present and visible
14
+ const table = page.getByRole('table');
15
+ await expect(table).toBeVisible();
16
+
17
+ // Show filters first
18
+ await page.getByText('Filters', { exact: true }).click();
19
+
20
+ // Find the phone filter input (by placeholder or input type)
21
+ const phoneInput = page.locator('input[placeholder="Phone number"]');
22
+
23
+ const phoneNumber = '+46700000025';
24
+ await phoneInput.fill(phoneNumber);
25
+
26
+ // Submit the filter form (by aria-label)
27
+ await page.getByLabel('Apply filter').click();
28
+
29
+ // Wait for the table to update and check that results are filtered
30
+ await expect(table.getByText(phoneNumber)).toBeVisible();
31
+ });
32
+ test('should render a view with a single column header and data', async ({ page }) => {
33
+ // Intercept the GraphQL request and mock the response
34
+ await page.route('**/v1/graphql', mockPaginationGraphQL);
35
+
36
+ // Navigate to the page with the simple test view
37
+ await page.goto('/?test-view=simple-test-view');
38
+
39
+ // Wait for the table to be present and visible
40
+ const table = page.getByRole('table');
41
+ await expect(table).toBeVisible();
42
+
43
+ // Get the expected header text from the simple test view JSON (first column name)
44
+ const expectedHeaderText = "Test Column Header";
45
+
46
+ // Locate the column header by its text content
47
+ const columnHeader = table.getByText(expectedHeaderText, { exact: true });
48
+
49
+ // Assert that the column header is visible
50
+ await expect(columnHeader).toBeVisible();
51
+
52
+ // Assert that all rows are visible before applying the filter
53
+ await expect(table.getByText('Test 30', { exact: true })).toBeVisible();
54
+ await expect(table.getByText('Test 29', { exact: true })).toBeVisible();
55
+ await expect(table.getByText('Test 28', { exact: true })).toBeVisible();
56
+ await expect(table.getByText('Test 27', { exact: true })).toBeVisible();
57
+ await expect(table.getByText('Test 25', { exact: true })).toBeVisible();
58
+ await expect(table.getByText('Test 24', { exact: true })).toBeVisible();
59
+ await expect(table.getByText('$300', { exact: true })).toBeVisible();
60
+ await expect(table.getByText('$290', { exact: true })).toBeVisible();
61
+ await expect(table.getByText('$280', { exact: true })).toBeVisible();
62
+ await expect(table.getByText('$270', { exact: true })).toBeVisible();
63
+ await expect(table.getByText('$250', { exact: true })).toBeVisible();
64
+ await expect(table.getByText('$240', { exact: true })).toBeVisible();
65
+
66
+ // Show filters first
67
+ await page.getByText('Filters', { exact: true }).click();
68
+
69
+ // Use the filter to only show rows with amount >= 30
70
+ // Find the Amount label, then its parent, then the sibling div, then the input inside
71
+ const amountLabel = page.getByText('Amount', { exact: true });
72
+ const amountInput = amountLabel.locator('..').locator('~ div input');
73
+ await amountInput.fill('260');
74
+ await amountInput.press('Enter');
75
+
76
+ // Assert that only the filtered rows are visible
77
+ await expect(table.getByText('Test 30', { exact: true })).toBeVisible();
78
+ await expect(table.getByText('Test 27', { exact: true })).toBeVisible();
79
+ await expect(table.getByText('Test 25', { exact: true })).not.toBeVisible();
80
+ await expect(table.getByText('Test 24', { exact: true })).not.toBeVisible();
81
+ });
82
+
83
+ test('should render filter group captions in the filter form', async ({ page }) => {
84
+ // Intercept the GraphQL request and mock the response
85
+ await page.route('**/v1/graphql', mockPaginationGraphQL);
86
+
87
+ // Navigate to the page with the simple test view
88
+ await page.goto('/?test-view=simple-test-view');
89
+
90
+ // Show filters first
91
+ await page.getByText('Filters', { exact: true }).click();
92
+
93
+ // Wait for the filter form to be present
94
+ const filterForm = page.locator('form');
95
+ await expect(filterForm).toBeVisible();
96
+
97
+ // Check for the presence of the new group label ("Extra Filters") as a Panel header
98
+ const extraFiltersPanel = page.locator('.p-panel-header', { hasText: 'Extra Filters' });
99
+ await expect(extraFiltersPanel).toBeVisible();
100
+
101
+ // Check that the filter label is present under the new group
102
+ await expect(page.getByText('Test Field', { exact: true })).toBeVisible();
103
+ });
104
+ });
@@ -0,0 +1,108 @@
1
+ import { test, expect } from '@playwright/test';
2
+ import { mockPaginationGraphQL } from './graphqlMock';
3
+
4
+ test.describe('Transform Regression Tests', () => {
5
+
6
+ test('should preserve display value after applying transform', async ({ page }) => {
7
+ // Intercept the GraphQL request and mock the response
8
+ await page.route('**/v1/graphql', mockPaginationGraphQL);
9
+
10
+ // Navigate to the page with the simple test view
11
+ await page.goto('/?test-view=simple-test-view');
12
+
13
+ // Wait for the table to be present and visible
14
+ const table = page.getByRole('table');
15
+ await expect(table).toBeVisible();
16
+
17
+ // Show filters first
18
+ await page.getByText('Filters', { exact: true }).click();
19
+
20
+ // Find the amount filter input
21
+ const amountLabel = page.getByText('Amount', { exact: true });
22
+ const amountInput = amountLabel.locator('..').locator('~ div input');
23
+
24
+ // Enter 255 in the input field
25
+ // With transform: toQuery adds 5, so 255 becomes 260 in the query
26
+ // Display should remain 255 (the user's input)
27
+ await amountInput.fill('255');
28
+
29
+ // Apply the filter
30
+ await page.getByRole('button', { name: 'Apply filter' }).click();
31
+
32
+ // Wait for the filter to be applied
33
+ await page.waitForTimeout(100);
34
+
35
+ // Check that the input still shows the original value (255) after applying the transform
36
+ // This is the regression test - it should show the original user input
37
+ await expect(amountInput).toHaveValue('255');
38
+ });
39
+
40
+ test('should correctly apply toQuery transforms', async ({ page }) => {
41
+ // Intercept the GraphQL request and mock the response
42
+ await page.route('**/v1/graphql', mockPaginationGraphQL);
43
+
44
+ // Navigate to the page with the simple test view
45
+ await page.goto('/?test-view=simple-test-view');
46
+
47
+ // Wait for the table to be present and visible
48
+ const table = page.getByRole('table');
49
+ await expect(table).toBeVisible();
50
+
51
+ // Show filters first
52
+ await page.getByText('Filters', { exact: true }).click();
53
+
54
+ // Find the amount filter input
55
+ const amountLabel = page.getByText('Amount', { exact: true });
56
+ const amountInput = amountLabel.locator('..').locator('~ div input');
57
+
58
+ // Test multiple values to ensure the transform roundtrip works correctly
59
+ const testValues = ['100', '255', '300'];
60
+
61
+ for (const testValue of testValues) {
62
+ // Clear and enter the test value
63
+ await amountInput.fill('');
64
+ await amountInput.fill(testValue);
65
+
66
+ // Apply the filter
67
+ await page.getByRole('button', { name: 'Apply filter' }).click();
68
+
69
+ // Wait for the filter to be applied
70
+ await page.waitForTimeout(100);
71
+
72
+ // Verify the input still shows the original value
73
+ await expect(amountInput).toHaveValue(testValue);
74
+ }
75
+ });
76
+
77
+ test('should handle empty values correctly with transforms', async ({ page }) => {
78
+ // Intercept the GraphQL request and mock the response
79
+ await page.route('**/v1/graphql', mockPaginationGraphQL);
80
+
81
+ // Navigate to the page with the simple test view
82
+ await page.goto('/?test-view=simple-test-view');
83
+
84
+ // Wait for the table to be present and visible
85
+ const table = page.getByRole('table');
86
+ await expect(table).toBeVisible();
87
+
88
+ // Show filters first
89
+ await page.getByText('Filters', { exact: true }).click();
90
+
91
+ // Find the amount filter input
92
+ const amountLabel = page.getByText('Amount', { exact: true });
93
+ const amountInput = amountLabel.locator('..').locator('~ div input');
94
+
95
+ // Enter a value, then clear it
96
+ await amountInput.fill('255');
97
+ await page.getByRole('button', { name: 'Apply filter' }).click();
98
+ await page.waitForTimeout(100);
99
+
100
+ // Clear the input
101
+ await amountInput.fill('');
102
+ await page.getByRole('button', { name: 'Apply filter' }).click();
103
+ await page.waitForTimeout(100);
104
+
105
+ // Verify the input remains empty
106
+ await expect(amountInput).toHaveValue('');
107
+ });
108
+ });
@@ -0,0 +1,30 @@
1
+ import js from '@eslint/js'
2
+ import globals from 'globals'
3
+ import reactHooks from 'eslint-plugin-react-hooks'
4
+ import reactRefresh from 'eslint-plugin-react-refresh'
5
+ import tseslint from 'typescript-eslint'
6
+
7
+ export default tseslint.config(
8
+ { ignores: ['dist'] },
9
+ {
10
+ extends: [js.configs.recommended, ...tseslint.configs.recommended],
11
+ files: ['**/*.{ts,tsx}'],
12
+ languageOptions: {
13
+ ecmaVersion: 2020,
14
+ globals: globals.browser,
15
+ },
16
+ plugins: {
17
+ 'react-hooks': reactHooks,
18
+ 'react-refresh': reactRefresh,
19
+ },
20
+ rules: {
21
+ ...reactHooks.configs.recommended.rules,
22
+ 'react-refresh/only-export-components': [
23
+ 'warn',
24
+ { allowConstantExport: true },
25
+ ],
26
+ '@typescript-eslint/no-explicit-any': 'off',
27
+ 'indent': ['error', 4, { 'SwitchCase': 1 }],
28
+ },
29
+ },
30
+ )
package/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="/vite.svg" />
7
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
8
+ <link href="./src/index.css" rel="stylesheet">
9
+ <title>Filters Demo</title>
10
+ </head>
11
+
12
+ <body class="h-full w-full">
13
+ <div id="root"></div>
14
+ <script type="module" src="./src/main.tsx"></script>
15
+ </body>
16
+
17
+ </html>
package/jest.config.js ADDED
@@ -0,0 +1,10 @@
1
+ export default {
2
+ preset: 'ts-jest',
3
+ testEnvironment: 'node',
4
+ testMatch: ['**/*.test.ts'],
5
+ globals: {
6
+ 'ts-jest': {
7
+ tsconfig: 'tsconfig.jest.json'
8
+ }
9
+ }
10
+ };
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@kronor/dtv",
3
+ "version": "0.2.9",
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "vite",
7
+ "build": "tsc -b && vite build",
8
+ "lint": "eslint .",
9
+ "preview": "vite preview",
10
+ "test": "playwright test",
11
+ "test-unit": "jest",
12
+ "prepare": "husky"
13
+ },
14
+ "dependencies": {
15
+ "graphql-request": "^7.1.2",
16
+ "primeflex": "^4.0.0",
17
+ "primeicons": "^7.0.0",
18
+ "primereact": "^10.9.5",
19
+ "react": "^19.0.0",
20
+ "react-dom": "^19.0.0",
21
+ "tailwindcss": "^4.1.1"
22
+ },
23
+ "devDependencies": {
24
+ "@eslint/js": "^9.21.0",
25
+ "@playwright/test": "^1.52.0",
26
+ "@tailwindcss/vite": "^4.1.1",
27
+ "@types/dom-speech-recognition": "^0.0.6",
28
+ "@types/jest": "^30.0.0",
29
+ "@types/node": "^22.15.21",
30
+ "@types/react": "^19.1.4",
31
+ "@types/react-dom": "^19.1.5",
32
+ "@vitejs/plugin-react": "^4.3.4",
33
+ "eslint": "^9.21.0",
34
+ "eslint-plugin-react-hooks": "^5.1.0",
35
+ "eslint-plugin-react-refresh": "^0.4.19",
36
+ "globals": "^15.15.0",
37
+ "husky": "^8.0.3",
38
+ "jest": "^30.0.3",
39
+ "jest-environment-jsdom": "^30.0.4",
40
+ "ts-jest": "^29.4.0",
41
+ "typescript": "~5.7.2",
42
+ "typescript-eslint": "^8.24.1",
43
+ "vite": "^6.2.0"
44
+ }
45
+ }
@@ -0,0 +1,54 @@
1
+ import { defineConfig, devices } from '@playwright/test';
2
+
3
+ /**
4
+ * See https://playwright.dev/docs/test-configuration.
5
+ */
6
+ export default defineConfig({
7
+ testDir: './e2e',
8
+ /* Run tests in files in parallel */
9
+ fullyParallel: !process.env.CI,
10
+ /* Fail the build on CI if you accidentally left test.only in the source code. */
11
+ forbidOnly: !!process.env.CI,
12
+ /* Retry on CI only */
13
+ retries: process.env.CI ? 2 : 0,
14
+ /* Opt out of parallel tests on CI. */
15
+ workers: process.env.CI ? 1 : undefined,
16
+ /* Reporter to use. See https://playwright.dev/docs/test-reporters */
17
+ reporter: process.env.CI ? [['github'], ['html']] : 'html',
18
+ /* Expect timeout for assertions */
19
+ expect: {
20
+ timeout: 10 * 1000, // 10 seconds for expect assertions
21
+ },
22
+ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
23
+ use: {
24
+ /* Base URL to use in actions like \`await page.goto('/')\`. */
25
+ baseURL: 'http://localhost:5173',
26
+
27
+ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
28
+ trace: 'on-first-retry',
29
+ },
30
+
31
+ /* Configure projects for major browsers */
32
+ projects: [
33
+ {
34
+ name: 'chromium',
35
+ use: { ...devices['Desktop Chrome'] },
36
+ },
37
+ // {
38
+ // name: 'firefox',
39
+ // use: { ...devices['Desktop Firefox'] },
40
+ // },
41
+
42
+ // {
43
+ // name: 'webkit',
44
+ // use: { ...devices['Desktop Safari'] },
45
+ // },
46
+ ],
47
+
48
+ /* Run your local dev server before starting the tests */
49
+ webServer: {
50
+ command: 'npm run dev',
51
+ url: 'http://localhost:5173',
52
+ reuseExistingServer: !process.env.CI,
53
+ },
54
+ });
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>