@nixxie-cms/core 2.0.0 → 2.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nixxie-cms/core",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/nixxiecms/nixxie/tree/main/packages/core"
@@ -302,7 +302,7 @@
302
302
  "@types/react-dom": "^19.2.3",
303
303
  "@types/resolve": "^1.20.2",
304
304
  "@types/uuid": "^11.0.0",
305
- "@nixxie-cms/core": "^2.0.0"
305
+ "@nixxie-cms/core": "^2.1.0"
306
306
  },
307
307
  "preconstruct": {
308
308
  "entrypoints": [
@@ -1,68 +1,68 @@
1
- import { controller } from '../index'
2
-
3
- const STUBCONFIG = {
4
- listKey: 'timestamp',
5
- fieldKey: 'timestamp',
6
- label: 'foo',
7
- description: '',
8
- customViews: {},
9
- } as const
10
-
11
- describe('controller', () => {
12
- describe('validate', () => {
13
- it('null is OK if not required', () => {
14
- const { validate } = controller({
15
- ...STUBCONFIG,
16
- fieldMeta: {
17
- defaultValue: null,
18
- updatedAt: false,
19
- },
20
- })
21
- expect(
22
- validate!(
23
- {
24
- kind: 'create',
25
- value: null,
26
- },
27
- { isRequired: false }
28
- )
29
- ).toBe(true)
30
- })
31
- it('isRequired enforces required (null)', () => {
32
- const { validate } = controller({
33
- ...STUBCONFIG,
34
- fieldMeta: {
35
- defaultValue: null,
36
- updatedAt: false,
37
- },
38
- })
39
- expect(
40
- validate!(
41
- {
42
- kind: 'create',
43
- value: null,
44
- },
45
- { isRequired: true }
46
- )
47
- ).toBe('foo is required')
48
- })
49
- it('isRequired enforces required (value)', () => {
50
- const { validate } = controller({
51
- ...STUBCONFIG,
52
- fieldMeta: {
53
- defaultValue: null,
54
- updatedAt: false,
55
- },
56
- })
57
- expect(
58
- validate!(
59
- {
60
- kind: 'create',
61
- value: new Date().toJSON(),
62
- },
63
- { isRequired: true }
64
- )
65
- ).toBe(true)
66
- })
67
- })
68
- })
1
+ import { controller } from '../index'
2
+
3
+ const STUBCONFIG = {
4
+ listKey: 'timestamp',
5
+ fieldKey: 'timestamp',
6
+ label: 'foo',
7
+ description: '',
8
+ customViews: {},
9
+ } as const
10
+
11
+ describe('controller', () => {
12
+ describe('validate', () => {
13
+ it('null is OK if not required', () => {
14
+ const { validate } = controller({
15
+ ...STUBCONFIG,
16
+ fieldMeta: {
17
+ defaultValue: null,
18
+ updatedAt: false,
19
+ },
20
+ })
21
+ expect(
22
+ validate!(
23
+ {
24
+ kind: 'create',
25
+ value: null,
26
+ },
27
+ { isRequired: false }
28
+ )
29
+ ).toBe(true)
30
+ })
31
+ it('isRequired enforces required (null)', () => {
32
+ const { validate } = controller({
33
+ ...STUBCONFIG,
34
+ fieldMeta: {
35
+ defaultValue: null,
36
+ updatedAt: false,
37
+ },
38
+ })
39
+ expect(
40
+ validate!(
41
+ {
42
+ kind: 'create',
43
+ value: null,
44
+ },
45
+ { isRequired: true }
46
+ )
47
+ ).toBe(false)
48
+ })
49
+ it('isRequired enforces required (value)', () => {
50
+ const { validate } = controller({
51
+ ...STUBCONFIG,
52
+ fieldMeta: {
53
+ defaultValue: null,
54
+ updatedAt: false,
55
+ },
56
+ })
57
+ expect(
58
+ validate!(
59
+ {
60
+ kind: 'create',
61
+ value: new Date().toJSON(),
62
+ },
63
+ { isRequired: true }
64
+ )
65
+ ).toBe(true)
66
+ })
67
+ })
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
+ })