@nixxie-cms/core 2.0.0 → 2.2.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 (128) hide show
  1. package/admin-ui/components/dist/nixxie-cms-core-admin-ui-components.cjs.js +7 -7
  2. package/admin-ui/components/dist/nixxie-cms-core-admin-ui-components.esm.js +7 -7
  3. package/admin-ui/context/dist/nixxie-cms-core-admin-ui-context.cjs.js +2 -2
  4. package/admin-ui/context/dist/nixxie-cms-core-admin-ui-context.esm.js +2 -2
  5. package/admin-ui/utils/dist/nixxie-cms-core-admin-ui-utils.cjs.js +5 -5
  6. package/admin-ui/utils/dist/nixxie-cms-core-admin-ui-utils.esm.js +3 -3
  7. package/dist/{CreateItemDialog-7008b050.esm.js → CreateItemDialog-66621fe8.esm.js} +3 -3
  8. package/dist/{CreateItemDialog-a0cab315.cjs.js → CreateItemDialog-96b044ce.cjs.js} +4 -4
  9. package/dist/{Field-47f85161.esm.js → Field-1820c4e6.esm.js} +1 -0
  10. package/dist/{Field-ed8d7627.cjs.js → Field-38d3cdf9.cjs.js} +1 -0
  11. package/dist/GraphQLErrorNotice-7594a9f8.esm.js +64 -0
  12. package/dist/GraphQLErrorNotice-c8890f80.cjs.js +66 -0
  13. package/dist/{PageContainer-5ae731cc.esm.js → PageContainer-355cfbfa.esm.js} +362 -156
  14. package/dist/{PageContainer-abd7159f.cjs.js → PageContainer-4095555a.cjs.js} +361 -155
  15. package/dist/{context-af9957ed.esm.js → context-2924eaaa.esm.js} +53 -58
  16. package/dist/{context-b5204629.cjs.js → context-2ce61d0b.cjs.js} +53 -58
  17. package/dist/declarations/src/admin-ui/components/GraphQLErrorNotice.d.ts.map +1 -1
  18. package/dist/declarations/src/admin-ui/components/Navigation.d.ts.map +1 -1
  19. package/dist/declarations/src/admin-ui/components/PageContainer.d.ts.map +1 -1
  20. package/dist/declarations/src/admin-ui/context.d.ts.map +1 -1
  21. package/dist/declarations/src/admin-ui/utils/useCreateItem.d.ts.map +1 -1
  22. package/dist/declarations/src/fields/types/bigInt/views/index.d.ts +2 -1
  23. package/dist/declarations/src/fields/types/bigInt/views/index.d.ts.map +1 -1
  24. package/dist/declarations/src/fields/types/bytes/views/index.d.ts +2 -1
  25. package/dist/declarations/src/fields/types/bytes/views/index.d.ts.map +1 -1
  26. package/dist/declarations/src/fields/types/calendarDay/views/index.d.ts.map +1 -1
  27. package/dist/declarations/src/fields/types/decimal/views/index.d.ts +2 -1
  28. package/dist/declarations/src/fields/types/decimal/views/index.d.ts.map +1 -1
  29. package/dist/declarations/src/fields/types/float/views/index.d.ts +2 -1
  30. package/dist/declarations/src/fields/types/float/views/index.d.ts.map +1 -1
  31. package/dist/declarations/src/fields/types/integer/views/index.d.ts +2 -1
  32. package/dist/declarations/src/fields/types/integer/views/index.d.ts.map +1 -1
  33. package/dist/declarations/src/fields/types/json/views/index.d.ts.map +1 -1
  34. package/dist/declarations/src/fields/types/multiselect/views/index.d.ts.map +1 -1
  35. package/dist/declarations/src/fields/types/relationship/views/index.d.ts.map +1 -1
  36. package/dist/declarations/src/fields/types/select/views/index.d.ts.map +1 -1
  37. package/dist/declarations/src/fields/types/text/views/index.d.ts +2 -1
  38. package/dist/declarations/src/fields/types/text/views/index.d.ts.map +1 -1
  39. package/dist/declarations/src/fields/types/timestamp/views/index.d.ts.map +1 -1
  40. package/dist/declarations/src/fields/types/virtual/views/index.d.ts.map +1 -1
  41. package/dist/declarations/src/internal-unstable/admin-ui/pages/HomePage/index.d.ts.map +1 -1
  42. package/dist/declarations/src/internal-unstable/admin-ui/pages/ItemPage/index.d.ts.map +1 -1
  43. package/dist/pick-4c785a54.esm.js +34 -0
  44. package/dist/pick-906341bb.cjs.js +37 -0
  45. package/dist/{useCreateItem-1f94d252.esm.js → useCreateItem-36a75f1c.esm.js} +26 -26
  46. package/dist/{useCreateItem-1be4987e.cjs.js → useCreateItem-acf06f77.cjs.js} +37 -37
  47. package/dist/{useFilter-acc9d413.cjs.js → useFilter-c29f17a8.cjs.js} +1 -1
  48. package/dist/{useFilter-9b6db1f9.esm.js → useFilter-f79b2abb.esm.js} +1 -1
  49. package/dist/{Fields-956d9a14.esm.js → usePreventNavigation-093389dd.esm.js} +28 -2
  50. package/dist/{Fields-e2c28056.cjs.js → usePreventNavigation-d4f9f4fa.cjs.js} +27 -0
  51. package/fields/types/bigInt/views/dist/nixxie-cms-core-fields-types-bigInt-views.cjs.js +8 -0
  52. package/fields/types/bigInt/views/dist/nixxie-cms-core-fields-types-bigInt-views.esm.js +8 -1
  53. package/fields/types/bytes/views/dist/nixxie-cms-core-fields-types-bytes-views.cjs.js +14 -3
  54. package/fields/types/bytes/views/dist/nixxie-cms-core-fields-types-bytes-views.esm.js +15 -5
  55. package/fields/types/calendarDay/views/dist/nixxie-cms-core-fields-types-calendarDay-views.cjs.js +2 -1
  56. package/fields/types/calendarDay/views/dist/nixxie-cms-core-fields-types-calendarDay-views.esm.js +2 -1
  57. package/fields/types/decimal/views/dist/nixxie-cms-core-fields-types-decimal-views.cjs.js +10 -1
  58. package/fields/types/decimal/views/dist/nixxie-cms-core-fields-types-decimal-views.esm.js +10 -2
  59. package/fields/types/file/views/dist/nixxie-cms-core-fields-types-file-views.cjs.js +1 -1
  60. package/fields/types/file/views/dist/nixxie-cms-core-fields-types-file-views.esm.js +2 -2
  61. package/fields/types/float/views/dist/nixxie-cms-core-fields-types-float-views.cjs.js +10 -1
  62. package/fields/types/float/views/dist/nixxie-cms-core-fields-types-float-views.esm.js +10 -2
  63. package/fields/types/image/views/dist/nixxie-cms-core-fields-types-image-views.cjs.js +2 -1
  64. package/fields/types/image/views/dist/nixxie-cms-core-fields-types-image-views.esm.js +2 -1
  65. package/fields/types/integer/views/dist/nixxie-cms-core-fields-types-integer-views.cjs.js +8 -0
  66. package/fields/types/integer/views/dist/nixxie-cms-core-fields-types-integer-views.esm.js +8 -1
  67. package/fields/types/json/views/dist/nixxie-cms-core-fields-types-json-views.cjs.js +19 -4
  68. package/fields/types/json/views/dist/nixxie-cms-core-fields-types-json-views.esm.js +19 -4
  69. package/fields/types/multiselect/views/dist/nixxie-cms-core-fields-types-multiselect-views.cjs.js +18 -3
  70. package/fields/types/multiselect/views/dist/nixxie-cms-core-fields-types-multiselect-views.esm.js +18 -3
  71. package/fields/types/password/views/dist/nixxie-cms-core-fields-types-password-views.cjs.js +1 -1
  72. package/fields/types/password/views/dist/nixxie-cms-core-fields-types-password-views.esm.js +1 -1
  73. package/fields/types/relationship/views/dist/nixxie-cms-core-fields-types-relationship-views.cjs.js +9 -7
  74. package/fields/types/relationship/views/dist/nixxie-cms-core-fields-types-relationship-views.esm.js +9 -7
  75. package/fields/types/select/views/dist/nixxie-cms-core-fields-types-select-views.cjs.js +3 -2
  76. package/fields/types/select/views/dist/nixxie-cms-core-fields-types-select-views.esm.js +3 -2
  77. package/fields/types/text/views/dist/nixxie-cms-core-fields-types-text-views.cjs.js +14 -3
  78. package/fields/types/text/views/dist/nixxie-cms-core-fields-types-text-views.esm.js +15 -5
  79. package/fields/types/timestamp/views/dist/nixxie-cms-core-fields-types-timestamp-views.cjs.js +2 -1
  80. package/fields/types/timestamp/views/dist/nixxie-cms-core-fields-types-timestamp-views.esm.js +2 -1
  81. package/fields/types/virtual/views/dist/nixxie-cms-core-fields-types-virtual-views.cjs.js +13 -1
  82. package/fields/types/virtual/views/dist/nixxie-cms-core-fields-types-virtual-views.esm.js +14 -2
  83. package/internal-unstable/admin-ui/pages/App/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-App.cjs.js +3 -3
  84. package/internal-unstable/admin-ui/pages/App/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-App.esm.js +3 -3
  85. package/internal-unstable/admin-ui/pages/CreateItemPage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-CreateItemPage.cjs.js +7 -7
  86. package/internal-unstable/admin-ui/pages/CreateItemPage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-CreateItemPage.esm.js +6 -6
  87. package/internal-unstable/admin-ui/pages/HomePage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-HomePage.cjs.js +34 -33
  88. package/internal-unstable/admin-ui/pages/HomePage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-HomePage.esm.js +35 -34
  89. package/internal-unstable/admin-ui/pages/ItemPage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-ItemPage.cjs.js +53 -13
  90. package/internal-unstable/admin-ui/pages/ItemPage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-ItemPage.esm.js +52 -12
  91. package/internal-unstable/admin-ui/pages/ListPage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-ListPage.cjs.js +36 -25
  92. package/internal-unstable/admin-ui/pages/ListPage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-ListPage.esm.js +36 -25
  93. package/package.json +2 -2
  94. package/src/admin-ui/components/CommandPalette.tsx +134 -27
  95. package/src/admin-ui/components/CreateButtonLink.tsx +20 -46
  96. package/src/admin-ui/components/GraphQLErrorNotice.tsx +39 -33
  97. package/src/admin-ui/components/Logo.tsx +5 -5
  98. package/src/admin-ui/components/Navigation.tsx +41 -27
  99. package/src/admin-ui/components/PageContainer.tsx +171 -15
  100. package/src/admin-ui/components/WelcomeDialog.tsx +14 -14
  101. package/src/admin-ui/context.tsx +5 -2
  102. package/src/admin-ui/utils/useCreateItem.ts +21 -1
  103. package/src/fields/types/bigInt/views/index.tsx +10 -1
  104. package/src/fields/types/bytes/views/index.tsx +14 -1
  105. package/src/fields/types/calendarDay/views/index.tsx +2 -1
  106. package/src/fields/types/decimal/views/index.tsx +7 -1
  107. package/src/fields/types/file/views/Field.tsx +1 -1
  108. package/src/fields/types/float/views/index.tsx +7 -1
  109. package/src/fields/types/image/views/index.tsx +1 -1
  110. package/src/fields/types/integer/views/index.tsx +5 -0
  111. package/src/fields/types/json/views/index.tsx +20 -2
  112. package/src/fields/types/multiselect/views/index.tsx +7 -3
  113. package/src/fields/types/password/views/index.tsx +1 -1
  114. package/src/fields/types/relationship/views/index.tsx +1 -0
  115. package/src/fields/types/select/views/index.tsx +2 -1
  116. package/src/fields/types/text/views/index.tsx +14 -1
  117. package/src/fields/types/timestamp/views/__tests__/index.tsx +68 -68
  118. package/src/fields/types/timestamp/views/index.tsx +2 -1
  119. package/src/fields/types/virtual/views/index.tsx +17 -2
  120. package/src/internal-unstable/admin-ui/pages/HomePage/index.tsx +40 -31
  121. package/src/internal-unstable/admin-ui/pages/ItemPage/index.tsx +36 -3
  122. package/src/internal-unstable/admin-ui/pages/ListPage/PaginationControls.tsx +20 -33
  123. package/src/internal-unstable/admin-ui/pages/ListPage/index.tsx +24 -16
  124. package/tests/conditional-filters.test.ts +333 -326
  125. package/dist/GraphQLErrorNotice-cd74180d.cjs.js +0 -57
  126. package/dist/GraphQLErrorNotice-d9f0931b.esm.js +0 -55
  127. package/dist/pick-5fe45878.cjs.js +0 -71
  128. package/dist/pick-b7ef3115.esm.js +0 -68
@@ -1,326 +1,333 @@
1
- import { getConditionalFilterFieldKeys, testFilter } from '../src/admin-ui/utils/filters'
2
- import type { ActionMeta, ConditionalFilterCase } from '../src/types'
3
-
4
- describe('conditional filters', () => {
5
- test('flat field predicates still use implicit AND and field-level not', () => {
6
- expect(
7
- testFilter(
8
- {
9
- status: { equals: 'published' },
10
- priority: { in: ['high', 'urgent'], not: { equals: 'urgent' } },
11
- },
12
- {
13
- status: 'published',
14
- priority: 'high',
15
- }
16
- )
17
- ).toBe(true)
18
-
19
- expect(
20
- testFilter(
21
- {
22
- status: { equals: 'published' },
23
- priority: { in: ['high', 'urgent'], not: { equals: 'urgent' } },
24
- },
25
- {
26
- status: 'published',
27
- priority: 'urgent',
28
- }
29
- )
30
- ).toBe(false)
31
- })
32
-
33
- test('AND requires every nested clause to pass', () => {
34
- expect(
35
- testFilter(
36
- {
37
- AND: [{ status: { equals: 'published' } }, { featured: { equals: true } }],
38
- },
39
- {
40
- status: 'published',
41
- featured: true,
42
- }
43
- )
44
- ).toBe(true)
45
-
46
- expect(
47
- testFilter(
48
- {
49
- AND: [{ status: { equals: 'published' } }, { featured: { equals: true } }],
50
- },
51
- {
52
- status: 'published',
53
- featured: false,
54
- }
55
- )
56
- ).toBe(false)
57
- })
58
-
59
- test('OR requires at least one nested clause to pass', () => {
60
- expect(
61
- testFilter(
62
- {
63
- OR: [{ status: { equals: 'draft' } }, { featured: { equals: true } }],
64
- },
65
- {
66
- status: 'published',
67
- featured: true,
68
- }
69
- )
70
- ).toBe(true)
71
-
72
- expect(
73
- testFilter(
74
- {
75
- OR: [{ status: { equals: 'draft' } }, { featured: { equals: true } }],
76
- },
77
- {
78
- status: 'published',
79
- featured: false,
80
- }
81
- )
82
- ).toBe(false)
83
- })
84
-
85
- test('NOT negates a nested clause', () => {
86
- expect(
87
- testFilter(
88
- {
89
- NOT: { archived: { equals: true } },
90
- },
91
- {
92
- archived: false,
93
- }
94
- )
95
- ).toBe(true)
96
-
97
- expect(
98
- testFilter(
99
- {
100
- NOT: { archived: { equals: true } },
101
- },
102
- {
103
- archived: true,
104
- }
105
- )
106
- ).toBe(false)
107
- })
108
-
109
- test('field predicates and logical operators at the same level are implicitly ANDed', () => {
110
- expect(
111
- testFilter(
112
- {
113
- status: { equals: 'published' },
114
- OR: [{ featured: { equals: true } }, { priority: { equals: 'high' } }],
115
- },
116
- {
117
- status: 'published',
118
- featured: false,
119
- priority: 'high',
120
- }
121
- )
122
- ).toBe(true)
123
-
124
- expect(
125
- testFilter(
126
- {
127
- status: { equals: 'published' },
128
- OR: [{ featured: { equals: true } }, { priority: { equals: 'high' } }],
129
- },
130
- {
131
- status: 'draft',
132
- featured: true,
133
- priority: 'high',
134
- }
135
- )
136
- ).toBe(false)
137
- })
138
-
139
- test('AND, OR, NOT, and sibling field predicates are all implicitly ANDed together', () => {
140
- expect(
141
- testFilter(
142
- {
143
- status: { equals: 'published' },
144
- AND: [{ author: { equals: 'emma' } }],
145
- OR: [{ featured: { equals: true } }, { priority: { equals: 'high' } }],
146
- NOT: { archived: { equals: true } },
147
- },
148
- {
149
- status: 'published',
150
- author: 'emma',
151
- featured: false,
152
- priority: 'high',
153
- archived: false,
154
- }
155
- )
156
- ).toBe(true)
157
-
158
- expect(
159
- testFilter(
160
- {
161
- status: { equals: 'published' },
162
- AND: [{ author: { equals: 'emma' } }],
163
- OR: [{ featured: { equals: true } }, { priority: { equals: 'high' } }],
164
- NOT: { archived: { equals: true } },
165
- },
166
- {
167
- status: 'published',
168
- author: 'emma',
169
- featured: false,
170
- priority: 'low',
171
- archived: false,
172
- }
173
- )
174
- ).toBe(false)
175
-
176
- expect(
177
- testFilter(
178
- {
179
- status: { equals: 'published' },
180
- AND: [{ author: { equals: 'emma' } }],
181
- OR: [{ featured: { equals: true } }, { priority: { equals: 'high' } }],
182
- NOT: { archived: { equals: true } },
183
- },
184
- {
185
- status: 'published',
186
- author: 'emma',
187
- featured: true,
188
- priority: 'low',
189
- archived: true,
190
- }
191
- )
192
- ).toBe(false)
193
- })
194
-
195
- test('recursive combinations evaluate correctly', () => {
196
- const filter: ConditionalFilterCase<any> = {
197
- AND: [
198
- {
199
- OR: [{ priority: { equals: 'high' } }, { priority: { equals: 'urgent' } }],
200
- },
201
- {
202
- NOT: { isComplete: { equals: true } },
203
- },
204
- ],
205
- }
206
-
207
- expect(
208
- testFilter(filter, {
209
- priority: 'urgent',
210
- isComplete: false,
211
- })
212
- ).toBe(true)
213
-
214
- expect(
215
- testFilter(filter, {
216
- priority: 'low',
217
- isComplete: false,
218
- })
219
- ).toBe(false)
220
-
221
- expect(
222
- testFilter(filter, {
223
- priority: 'high',
224
- isComplete: true,
225
- })
226
- ).toBe(false)
227
- })
228
-
229
- test('collects field dependencies from nested action filters', () => {
230
- const actions = [
231
- {
232
- key: 'publish',
233
- graphql: { arguments: [], names: { one: 'publishPost', many: 'publishPosts' } },
234
- label: 'Publish',
235
- icon: null,
236
- messages: {
237
- promptTitle: '',
238
- promptTitleMany: '',
239
- prompt: '',
240
- promptMany: '',
241
- promptConfirmLabel: '',
242
- promptConfirmLabelMany: '',
243
- fail: '',
244
- failMany: '',
245
- success: '',
246
- successMany: '',
247
- },
248
- itemView: {
249
- actionMode: 'enabled',
250
- navigation: 'follow',
251
- hidePrompt: false,
252
- hideToast: false,
253
- },
254
- listView: {
255
- actionMode: {
256
- disabled: {
257
- AND: [
258
- { status: { equals: 'draft' } },
259
- {
260
- OR: [{ priority: { equals: 'high' } }, { NOT: { isComplete: { equals: true } } }],
261
- },
262
- ],
263
- },
264
- },
265
- },
266
- } as ActionMeta,
267
- ] satisfies ActionMeta[]
268
-
269
- const fieldKeys = new Set<string>(['title'])
270
- for (const action of actions) {
271
- for (const fieldKey of getConditionalFilterFieldKeys(action.listView.actionMode)) {
272
- fieldKeys.add(fieldKey)
273
- }
274
- }
275
-
276
- expect(fieldKeys).toEqual(['title', 'status', 'priority', 'isComplete'])
277
- })
278
-
279
- test('collects field dependencies from sibling AND, OR, NOT, and field predicates', () => {
280
- const actions = [
281
- {
282
- key: 'publish',
283
- graphql: { arguments: [], names: { one: 'publishPost', many: 'publishPosts' } },
284
- label: 'Publish',
285
- icon: null,
286
- messages: {
287
- promptTitle: '',
288
- promptTitleMany: '',
289
- prompt: '',
290
- promptMany: '',
291
- promptConfirmLabel: '',
292
- promptConfirmLabelMany: '',
293
- fail: '',
294
- failMany: '',
295
- success: '',
296
- successMany: '',
297
- },
298
- itemView: {
299
- actionMode: 'enabled',
300
- navigation: 'follow',
301
- hidePrompt: false,
302
- hideToast: false,
303
- },
304
- listView: {
305
- actionMode: {
306
- disabled: {
307
- status: { equals: 'draft' },
308
- AND: [{ author: { equals: 'emma' } }],
309
- OR: [{ priority: { equals: 'high' } }, { featured: { equals: true } }],
310
- NOT: { archived: { equals: true } },
311
- },
312
- },
313
- },
314
- } as ActionMeta,
315
- ] satisfies ActionMeta[]
316
-
317
- const fieldKeys = new Set<string>(['title'])
318
- for (const action of actions) {
319
- for (const fieldKey of getConditionalFilterFieldKeys(action.listView.actionMode)) {
320
- fieldKeys.add(fieldKey)
321
- }
322
- }
323
-
324
- expect(fieldKeys).toEqual(['title', 'author', 'priority', 'featured', 'archived', 'status'])
325
- })
326
- })
1
+ import { getConditionalFilterFieldKeys, testFilter } from '../src/admin-ui/utils/filters'
2
+ import type { ActionMeta, ConditionalFilterCase } from '../src/types'
3
+
4
+ describe('conditional filters', () => {
5
+ test('flat field predicates still use implicit AND and field-level not', () => {
6
+ expect(
7
+ testFilter(
8
+ {
9
+ status: { equals: 'published' },
10
+ priority: { in: ['high', 'urgent'], not: { equals: 'urgent' } },
11
+ },
12
+ {
13
+ status: 'published',
14
+ priority: 'high',
15
+ }
16
+ )
17
+ ).toBe(true)
18
+
19
+ expect(
20
+ testFilter(
21
+ {
22
+ status: { equals: 'published' },
23
+ priority: { in: ['high', 'urgent'], not: { equals: 'urgent' } },
24
+ },
25
+ {
26
+ status: 'published',
27
+ priority: 'urgent',
28
+ }
29
+ )
30
+ ).toBe(false)
31
+ })
32
+
33
+ test('AND requires every nested clause to pass', () => {
34
+ expect(
35
+ testFilter(
36
+ {
37
+ AND: [{ status: { equals: 'published' } }, { featured: { equals: true } }],
38
+ },
39
+ {
40
+ status: 'published',
41
+ featured: true,
42
+ }
43
+ )
44
+ ).toBe(true)
45
+
46
+ expect(
47
+ testFilter(
48
+ {
49
+ AND: [{ status: { equals: 'published' } }, { featured: { equals: true } }],
50
+ },
51
+ {
52
+ status: 'published',
53
+ featured: false,
54
+ }
55
+ )
56
+ ).toBe(false)
57
+ })
58
+
59
+ test('OR requires at least one nested clause to pass', () => {
60
+ expect(
61
+ testFilter(
62
+ {
63
+ OR: [{ status: { equals: 'draft' } }, { featured: { equals: true } }],
64
+ },
65
+ {
66
+ status: 'published',
67
+ featured: true,
68
+ }
69
+ )
70
+ ).toBe(true)
71
+
72
+ expect(
73
+ testFilter(
74
+ {
75
+ OR: [{ status: { equals: 'draft' } }, { featured: { equals: true } }],
76
+ },
77
+ {
78
+ status: 'published',
79
+ featured: false,
80
+ }
81
+ )
82
+ ).toBe(false)
83
+ })
84
+
85
+ test('NOT negates a nested clause', () => {
86
+ expect(
87
+ testFilter(
88
+ {
89
+ NOT: { archived: { equals: true } },
90
+ },
91
+ {
92
+ archived: false,
93
+ }
94
+ )
95
+ ).toBe(true)
96
+
97
+ expect(
98
+ testFilter(
99
+ {
100
+ NOT: { archived: { equals: true } },
101
+ },
102
+ {
103
+ archived: true,
104
+ }
105
+ )
106
+ ).toBe(false)
107
+ })
108
+
109
+ test('field predicates and logical operators at the same level are implicitly ANDed', () => {
110
+ expect(
111
+ testFilter(
112
+ {
113
+ status: { equals: 'published' },
114
+ OR: [{ featured: { equals: true } }, { priority: { equals: 'high' } }],
115
+ },
116
+ {
117
+ status: 'published',
118
+ featured: false,
119
+ priority: 'high',
120
+ }
121
+ )
122
+ ).toBe(true)
123
+
124
+ expect(
125
+ testFilter(
126
+ {
127
+ status: { equals: 'published' },
128
+ OR: [{ featured: { equals: true } }, { priority: { equals: 'high' } }],
129
+ },
130
+ {
131
+ status: 'draft',
132
+ featured: true,
133
+ priority: 'high',
134
+ }
135
+ )
136
+ ).toBe(false)
137
+ })
138
+
139
+ test('AND, OR, NOT, and sibling field predicates are all implicitly ANDed together', () => {
140
+ expect(
141
+ testFilter(
142
+ {
143
+ status: { equals: 'published' },
144
+ AND: [{ author: { equals: 'emma' } }],
145
+ OR: [{ featured: { equals: true } }, { priority: { equals: 'high' } }],
146
+ NOT: { archived: { equals: true } },
147
+ },
148
+ {
149
+ status: 'published',
150
+ author: 'emma',
151
+ featured: false,
152
+ priority: 'high',
153
+ archived: false,
154
+ }
155
+ )
156
+ ).toBe(true)
157
+
158
+ expect(
159
+ testFilter(
160
+ {
161
+ status: { equals: 'published' },
162
+ AND: [{ author: { equals: 'emma' } }],
163
+ OR: [{ featured: { equals: true } }, { priority: { equals: 'high' } }],
164
+ NOT: { archived: { equals: true } },
165
+ },
166
+ {
167
+ status: 'published',
168
+ author: 'emma',
169
+ featured: false,
170
+ priority: 'low',
171
+ archived: false,
172
+ }
173
+ )
174
+ ).toBe(false)
175
+
176
+ expect(
177
+ testFilter(
178
+ {
179
+ status: { equals: 'published' },
180
+ AND: [{ author: { equals: 'emma' } }],
181
+ OR: [{ featured: { equals: true } }, { priority: { equals: 'high' } }],
182
+ NOT: { archived: { equals: true } },
183
+ },
184
+ {
185
+ status: 'published',
186
+ author: 'emma',
187
+ featured: true,
188
+ priority: 'low',
189
+ archived: true,
190
+ }
191
+ )
192
+ ).toBe(false)
193
+ })
194
+
195
+ test('recursive combinations evaluate correctly', () => {
196
+ const filter: ConditionalFilterCase<any> = {
197
+ AND: [
198
+ {
199
+ OR: [{ priority: { equals: 'high' } }, { priority: { equals: 'urgent' } }],
200
+ },
201
+ {
202
+ NOT: { isComplete: { equals: true } },
203
+ },
204
+ ],
205
+ }
206
+
207
+ expect(
208
+ testFilter(filter, {
209
+ priority: 'urgent',
210
+ isComplete: false,
211
+ })
212
+ ).toBe(true)
213
+
214
+ expect(
215
+ testFilter(filter, {
216
+ priority: 'low',
217
+ isComplete: false,
218
+ })
219
+ ).toBe(false)
220
+
221
+ expect(
222
+ testFilter(filter, {
223
+ priority: 'high',
224
+ isComplete: true,
225
+ })
226
+ ).toBe(false)
227
+ })
228
+
229
+ test('collects field dependencies from nested action filters', () => {
230
+ const actions = [
231
+ {
232
+ key: 'publish',
233
+ graphql: { arguments: [], names: { one: 'publishPost', many: 'publishPosts' } },
234
+ label: 'Publish',
235
+ icon: null,
236
+ messages: {
237
+ promptTitle: '',
238
+ promptTitleMany: '',
239
+ prompt: '',
240
+ promptMany: '',
241
+ promptConfirmLabel: '',
242
+ promptConfirmLabelMany: '',
243
+ fail: '',
244
+ failMany: '',
245
+ success: '',
246
+ successMany: '',
247
+ },
248
+ itemView: {
249
+ actionMode: 'enabled',
250
+ navigation: 'follow',
251
+ hidePrompt: false,
252
+ hideToast: false,
253
+ },
254
+ listView: {
255
+ actionMode: {
256
+ disabled: {
257
+ AND: [
258
+ { status: { equals: 'draft' } },
259
+ {
260
+ OR: [{ priority: { equals: 'high' } }, { NOT: { isComplete: { equals: true } } }],
261
+ },
262
+ ],
263
+ },
264
+ },
265
+ },
266
+ } as ActionMeta,
267
+ ] satisfies ActionMeta[]
268
+
269
+ const fieldKeys = new Set<string>(['title'])
270
+ for (const action of actions) {
271
+ for (const fieldKey of getConditionalFilterFieldKeys(action.listView.actionMode)) {
272
+ fieldKeys.add(fieldKey)
273
+ }
274
+ }
275
+
276
+ expect([...fieldKeys]).toEqual(['title', 'status', 'priority', 'isComplete'])
277
+ })
278
+
279
+ test('collects field dependencies from sibling AND, OR, NOT, and field predicates', () => {
280
+ const actions = [
281
+ {
282
+ key: 'publish',
283
+ graphql: { arguments: [], names: { one: 'publishPost', many: 'publishPosts' } },
284
+ label: 'Publish',
285
+ icon: null,
286
+ messages: {
287
+ promptTitle: '',
288
+ promptTitleMany: '',
289
+ prompt: '',
290
+ promptMany: '',
291
+ promptConfirmLabel: '',
292
+ promptConfirmLabelMany: '',
293
+ fail: '',
294
+ failMany: '',
295
+ success: '',
296
+ successMany: '',
297
+ },
298
+ itemView: {
299
+ actionMode: 'enabled',
300
+ navigation: 'follow',
301
+ hidePrompt: false,
302
+ hideToast: false,
303
+ },
304
+ listView: {
305
+ actionMode: {
306
+ disabled: {
307
+ status: { equals: 'draft' },
308
+ AND: [{ author: { equals: 'emma' } }],
309
+ OR: [{ priority: { equals: 'high' } }, { featured: { equals: true } }],
310
+ NOT: { archived: { equals: true } },
311
+ },
312
+ },
313
+ },
314
+ } as ActionMeta,
315
+ ] satisfies ActionMeta[]
316
+
317
+ const fieldKeys = new Set<string>(['title'])
318
+ for (const action of actions) {
319
+ for (const fieldKey of getConditionalFilterFieldKeys(action.listView.actionMode)) {
320
+ fieldKeys.add(fieldKey)
321
+ }
322
+ }
323
+
324
+ expect([...fieldKeys]).toEqual([
325
+ 'title',
326
+ 'author',
327
+ 'priority',
328
+ 'featured',
329
+ 'archived',
330
+ 'status',
331
+ ])
332
+ })
333
+ })