@nixxie-cms/core 1.0.3 → 2.0.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 (203) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/CHANGES-1.1.md +134 -0
  3. package/context/dist/nixxie-cms-core-context.cjs.js +4 -3
  4. package/context/dist/nixxie-cms-core-context.esm.js +3 -2
  5. package/dist/declarations/src/access.d.ts +2 -2
  6. package/dist/declarations/src/access.d.ts.map +1 -1
  7. package/dist/declarations/src/admin-ui/components/Navigation.d.ts +2 -2
  8. package/dist/declarations/src/admin-ui/components/Navigation.d.ts.map +1 -1
  9. package/dist/declarations/src/admin-ui/context.d.ts +6 -6
  10. package/dist/declarations/src/admin-ui/context.d.ts.map +1 -1
  11. package/dist/declarations/src/admin-ui/utils/Fields.d.ts +3 -3
  12. package/dist/declarations/src/admin-ui/utils/Fields.d.ts.map +1 -1
  13. package/dist/declarations/src/admin-ui/utils/filters.d.ts +5 -5
  14. package/dist/declarations/src/admin-ui/utils/filters.d.ts.map +1 -1
  15. package/dist/declarations/src/admin-ui/utils/useCreateItem.d.ts +3 -3
  16. package/dist/declarations/src/admin-ui/utils/useCreateItem.d.ts.map +1 -1
  17. package/dist/declarations/src/admin-ui/utils/utils.d.ts +2 -2
  18. package/dist/declarations/src/admin-ui/utils/utils.d.ts.map +1 -1
  19. package/dist/declarations/src/context.d.ts +1 -1
  20. package/dist/declarations/src/context.d.ts.map +1 -1
  21. package/dist/declarations/src/fields/types/bigInt/index.d.ts +3 -3
  22. package/dist/declarations/src/fields/types/bigInt/index.d.ts.map +1 -1
  23. package/dist/declarations/src/fields/types/bytes/index.d.ts +3 -3
  24. package/dist/declarations/src/fields/types/bytes/index.d.ts.map +1 -1
  25. package/dist/declarations/src/fields/types/calendarDay/index.d.ts +3 -3
  26. package/dist/declarations/src/fields/types/calendarDay/index.d.ts.map +1 -1
  27. package/dist/declarations/src/fields/types/checkbox/index.d.ts +3 -3
  28. package/dist/declarations/src/fields/types/checkbox/index.d.ts.map +1 -1
  29. package/dist/declarations/src/fields/types/decimal/index.d.ts +3 -3
  30. package/dist/declarations/src/fields/types/decimal/index.d.ts.map +1 -1
  31. package/dist/declarations/src/fields/types/file/index.d.ts +4 -4
  32. package/dist/declarations/src/fields/types/file/index.d.ts.map +1 -1
  33. package/dist/declarations/src/fields/types/float/index.d.ts +3 -3
  34. package/dist/declarations/src/fields/types/float/index.d.ts.map +1 -1
  35. package/dist/declarations/src/fields/types/image/index.d.ts +4 -4
  36. package/dist/declarations/src/fields/types/image/index.d.ts.map +1 -1
  37. package/dist/declarations/src/fields/types/integer/index.d.ts +3 -3
  38. package/dist/declarations/src/fields/types/integer/index.d.ts.map +1 -1
  39. package/dist/declarations/src/fields/types/json/index.d.ts +3 -3
  40. package/dist/declarations/src/fields/types/json/index.d.ts.map +1 -1
  41. package/dist/declarations/src/fields/types/multiselect/index.d.ts +3 -3
  42. package/dist/declarations/src/fields/types/multiselect/index.d.ts.map +1 -1
  43. package/dist/declarations/src/fields/types/multiselect/views/index.d.ts.map +1 -1
  44. package/dist/declarations/src/fields/types/password/index.d.ts +3 -3
  45. package/dist/declarations/src/fields/types/password/index.d.ts.map +1 -1
  46. package/dist/declarations/src/fields/types/relationship/index.d.ts +8 -8
  47. package/dist/declarations/src/fields/types/relationship/index.d.ts.map +1 -1
  48. package/dist/declarations/src/fields/types/relationship/views/ComboboxMany.d.ts +3 -3
  49. package/dist/declarations/src/fields/types/relationship/views/ComboboxMany.d.ts.map +1 -1
  50. package/dist/declarations/src/fields/types/relationship/views/ComboboxSingle.d.ts +3 -3
  51. package/dist/declarations/src/fields/types/relationship/views/ComboboxSingle.d.ts.map +1 -1
  52. package/dist/declarations/src/fields/types/relationship/views/index.d.ts +3 -3
  53. package/dist/declarations/src/fields/types/relationship/views/index.d.ts.map +1 -1
  54. package/dist/declarations/src/fields/types/relationship/views/types.d.ts +3 -3
  55. package/dist/declarations/src/fields/types/relationship/views/types.d.ts.map +1 -1
  56. package/dist/declarations/src/fields/types/select/index.d.ts +3 -3
  57. package/dist/declarations/src/fields/types/select/index.d.ts.map +1 -1
  58. package/dist/declarations/src/fields/types/text/index.d.ts +3 -3
  59. package/dist/declarations/src/fields/types/text/index.d.ts.map +1 -1
  60. package/dist/declarations/src/fields/types/timestamp/index.d.ts +3 -3
  61. package/dist/declarations/src/fields/types/timestamp/index.d.ts.map +1 -1
  62. package/dist/declarations/src/fields/types/virtual/index.d.ts +7 -7
  63. package/dist/declarations/src/fields/types/virtual/index.d.ts.map +1 -1
  64. package/dist/declarations/src/helpers.d.ts +249 -13
  65. package/dist/declarations/src/helpers.d.ts.map +1 -1
  66. package/dist/declarations/src/index.d.ts +9 -4
  67. package/dist/declarations/src/index.d.ts.map +1 -1
  68. package/dist/declarations/src/internal-unstable/admin-ui/pages/ListPage/index.d.ts.map +1 -1
  69. package/dist/declarations/src/lib/admin-meta.d.ts +11 -11
  70. package/dist/declarations/src/lib/admin-meta.d.ts.map +1 -1
  71. package/dist/declarations/src/lib/core/access-control.d.ts +18 -18
  72. package/dist/declarations/src/lib/core/access-control.d.ts.map +1 -1
  73. package/dist/declarations/src/lib/core/cascade.d.ts +47 -0
  74. package/dist/declarations/src/lib/core/cascade.d.ts.map +1 -0
  75. package/dist/declarations/src/lib/core/initialise-lists.d.ts +27 -24
  76. package/dist/declarations/src/lib/core/initialise-lists.d.ts.map +1 -1
  77. package/dist/declarations/src/lib/env.d.ts +9 -0
  78. package/dist/declarations/src/lib/env.d.ts.map +1 -0
  79. package/dist/declarations/src/lib/system.d.ts +1 -1
  80. package/dist/declarations/src/lib/system.d.ts.map +1 -1
  81. package/dist/declarations/src/list-features.d.ts +162 -0
  82. package/dist/declarations/src/list-features.d.ts.map +1 -0
  83. package/dist/declarations/src/schema.d.ts +24 -23
  84. package/dist/declarations/src/schema.d.ts.map +1 -1
  85. package/dist/declarations/src/session.d.ts +75 -0
  86. package/dist/declarations/src/session.d.ts.map +1 -1
  87. package/dist/declarations/src/types/admin-meta.d.ts +11 -11
  88. package/dist/declarations/src/types/admin-meta.d.ts.map +1 -1
  89. package/dist/declarations/src/types/config/access-control.d.ts +42 -42
  90. package/dist/declarations/src/types/config/access-control.d.ts.map +1 -1
  91. package/dist/declarations/src/types/config/fields.d.ts +19 -19
  92. package/dist/declarations/src/types/config/fields.d.ts.map +1 -1
  93. package/dist/declarations/src/types/config/hooks.d.ts +131 -131
  94. package/dist/declarations/src/types/config/hooks.d.ts.map +1 -1
  95. package/dist/declarations/src/types/config/index.d.ts +190 -8
  96. package/dist/declarations/src/types/config/index.d.ts.map +1 -1
  97. package/dist/declarations/src/types/config/lists.d.ts +146 -108
  98. package/dist/declarations/src/types/config/lists.d.ts.map +1 -1
  99. package/dist/declarations/src/types/context.d.ts +507 -47
  100. package/dist/declarations/src/types/context.d.ts.map +1 -1
  101. package/dist/declarations/src/types/next-fields.d.ts +28 -28
  102. package/dist/declarations/src/types/next-fields.d.ts.map +1 -1
  103. package/dist/declarations/src/types/type-info.d.ts +3 -3
  104. package/dist/declarations/src/types/type-info.d.ts.map +1 -1
  105. package/dist/{express-455ae20c.cjs.js → express-84d534c2.cjs.js} +6 -6
  106. package/dist/{express-7559ca2d.esm.js → express-d0a4ce99.esm.js} +6 -6
  107. package/dist/{index-15c8f81e.esm.js → index-5d8b0b4e.esm.js} +363 -183
  108. package/dist/index-6055753b.cjs.js +393 -0
  109. package/dist/{index-42045902.cjs.js → index-ac29f382.cjs.js} +363 -185
  110. package/dist/index-f1703b7b.esm.js +386 -0
  111. package/dist/nixxie-cms-core.cjs.js +1388 -30
  112. package/dist/nixxie-cms-core.esm.js +1362 -24
  113. package/dist/{non-null-graphql-add6bb3d.cjs.js → non-null-graphql-4a44c122.cjs.js} +1 -1
  114. package/dist/{non-null-graphql-a84ed64d.esm.js → non-null-graphql-8c5feaae.esm.js} +1 -1
  115. package/dist/{resolve-hooks-165a9ce2.cjs.js → resolve-hooks-10a5f84c.cjs.js} +240 -6
  116. package/dist/{resolve-hooks-6813a045.esm.js → resolve-hooks-9e676794.esm.js} +238 -7
  117. package/dist/{system-a321642d.cjs.js → system-6b37a5f8.cjs.js} +33 -7
  118. package/dist/{system-03e49e4f.esm.js → system-e591d821.esm.js} +33 -7
  119. package/fields/dist/nixxie-cms-core-fields.cjs.js +29 -576
  120. package/fields/dist/nixxie-cms-core-fields.esm.js +18 -565
  121. package/fields/types/bytes/dist/nixxie-cms-core-fields-types-bytes.cjs.js +4 -2
  122. package/fields/types/bytes/dist/nixxie-cms-core-fields-types-bytes.esm.js +4 -2
  123. package/fields/types/multiselect/views/dist/nixxie-cms-core-fields-types-multiselect-views.cjs.js +1 -6
  124. package/fields/types/multiselect/views/dist/nixxie-cms-core-fields-types-multiselect-views.esm.js +1 -6
  125. package/fields/types/password/dist/nixxie-cms-core-fields-types-password.cjs.js +4 -2
  126. package/fields/types/password/dist/nixxie-cms-core-fields-types-password.esm.js +4 -2
  127. package/internal-unstable/artifacts/dist/nixxie-cms-core-internal-unstable-artifacts.cjs.js +4 -3
  128. package/internal-unstable/artifacts/dist/nixxie-cms-core-internal-unstable-artifacts.esm.js +4 -3
  129. package/package.json +4 -4
  130. package/scripts/cli/dist/nixxie-cms-core-scripts-cli.cjs.js +4 -3
  131. package/scripts/cli/dist/nixxie-cms-core-scripts-cli.esm.js +4 -3
  132. package/scripts/dist/nixxie-cms-core-scripts.cjs.js +4 -3
  133. package/scripts/dist/nixxie-cms-core-scripts.esm.js +4 -3
  134. package/session/dist/nixxie-cms-core-session.cjs.js +286 -0
  135. package/session/dist/nixxie-cms-core-session.esm.js +279 -1
  136. package/src/access.ts +25 -25
  137. package/src/admin-ui/admin-meta-graphql.ts +5 -5
  138. package/src/admin-ui/components/CreateButtonLink.tsx +46 -46
  139. package/src/admin-ui/components/Navigation.tsx +3 -3
  140. package/src/admin-ui/context.tsx +6 -6
  141. package/src/admin-ui/utils/Fields.tsx +241 -241
  142. package/src/admin-ui/utils/actionData.ts +36 -36
  143. package/src/admin-ui/utils/filters.ts +148 -148
  144. package/src/admin-ui/utils/useCreateItem.ts +171 -171
  145. package/src/admin-ui/utils/utils.tsx +127 -127
  146. package/src/context.ts +1 -1
  147. package/src/fields/non-null-graphql.ts +115 -115
  148. package/src/fields/types/bigInt/index.ts +6 -6
  149. package/src/fields/types/bytes/index.ts +6 -6
  150. package/src/fields/types/calendarDay/index.ts +18 -19
  151. package/src/fields/types/checkbox/index.ts +6 -6
  152. package/src/fields/types/decimal/index.ts +6 -6
  153. package/src/fields/types/file/index.ts +8 -8
  154. package/src/fields/types/float/index.ts +6 -6
  155. package/src/fields/types/image/index.ts +8 -8
  156. package/src/fields/types/integer/index.ts +6 -6
  157. package/src/fields/types/json/index.ts +5 -5
  158. package/src/fields/types/multiselect/index.ts +7 -7
  159. package/src/fields/types/multiselect/views/index.tsx +149 -151
  160. package/src/fields/types/password/index.ts +6 -6
  161. package/src/fields/types/relationship/index.ts +13 -13
  162. package/src/fields/types/relationship/views/ComboboxMany.tsx +110 -110
  163. package/src/fields/types/relationship/views/ComboboxSingle.tsx +115 -115
  164. package/src/fields/types/relationship/views/ContextualActions.tsx +139 -139
  165. package/src/fields/types/relationship/views/index.tsx +492 -492
  166. package/src/fields/types/relationship/views/types.ts +46 -46
  167. package/src/fields/types/relationship/views/useApolloQuery.ts +185 -185
  168. package/src/fields/types/relationship/views/useFilter.tsx +109 -109
  169. package/src/fields/types/select/index.ts +6 -6
  170. package/src/fields/types/text/index.ts +6 -6
  171. package/src/fields/types/timestamp/index.ts +23 -21
  172. package/src/fields/types/virtual/index.ts +11 -11
  173. package/src/helpers.ts +773 -42
  174. package/src/index.ts +66 -24
  175. package/src/internal-unstable/admin-ui/pages/ItemPage/common.tsx +4 -4
  176. package/src/internal-unstable/admin-ui/pages/ItemPage/index.tsx +5 -5
  177. package/src/internal-unstable/admin-ui/pages/ListPage/index.tsx +8 -8
  178. package/src/lib/admin-meta.ts +369 -369
  179. package/src/lib/context/createContext.ts +6 -0
  180. package/src/lib/core/access-control.ts +434 -434
  181. package/src/lib/core/cascade.ts +236 -0
  182. package/src/lib/core/initialise-lists.ts +49 -33
  183. package/src/lib/core/mutations/index.ts +7 -0
  184. package/src/lib/core/mutations/nested-mutation-many-input-resolvers.ts +145 -145
  185. package/src/lib/core/mutations/nested-mutation-one-input-resolvers.ts +71 -71
  186. package/src/lib/core/queries/output-field.ts +178 -178
  187. package/src/lib/env.ts +50 -0
  188. package/src/lib/id-field.ts +2 -2
  189. package/src/lib/system.ts +221 -207
  190. package/src/lib/typescript-schema-printer.ts +227 -227
  191. package/src/list-features.ts +476 -0
  192. package/src/schema.ts +92 -22
  193. package/src/session.ts +225 -0
  194. package/src/types/admin-meta.ts +218 -218
  195. package/src/types/config/access-control.ts +186 -186
  196. package/src/types/config/fields.ts +96 -96
  197. package/src/types/config/hooks.ts +529 -529
  198. package/src/types/config/index.ts +206 -7
  199. package/src/types/config/lists.ts +606 -565
  200. package/src/types/context.ts +592 -55
  201. package/src/types/next-fields.ts +31 -31
  202. package/src/types/type-info.ts +38 -38
  203. package/src/types/type-tests.ts +21 -21
@@ -1,492 +1,492 @@
1
- import { useListFormatter } from '@react-aria/i18n'
2
- import { Fragment, useState } from 'react'
3
-
4
- import { DialogContainer } from '@keystar/ui/dialog'
5
- import { HStack, VStack } from '@keystar/ui/layout'
6
- import { TextLink } from '@keystar/ui/link'
7
- import { Item, TagGroup } from '@keystar/ui/tag'
8
- import { TextField } from '@keystar/ui/text-field'
9
- import { Numeral, Text } from '@keystar/ui/typography'
10
-
11
- import { BuildItemDialog } from '../../../../admin-ui/components'
12
- import { useList } from '../../../../admin-ui/context'
13
- import type {
14
- CellComponent,
15
- FieldControllerConfig,
16
- FieldProps,
17
- ListSortDescriptor,
18
- } from '../../../../types'
19
-
20
- import { ActionButton } from '@keystar/ui/button'
21
- import { Icon } from '@keystar/ui/icon'
22
- import { arrowUpRightIcon } from '@keystar/ui/icon/icons/arrowUpRightIcon'
23
- import { ComboboxMany } from './ComboboxMany'
24
- import { ComboboxSingle } from './ComboboxSingle'
25
- import {
26
- buildQueryForRelationshipFieldWithForeignField,
27
- ContextualActions,
28
- } from './ContextualActions'
29
- import { RelationshipTable } from './RelationshipTable'
30
- import type { RelationshipController, RelationshipValue } from './types'
31
-
32
- export { ComboboxMany, ComboboxSingle }
33
-
34
- export function Field(props: FieldProps<typeof controller>) {
35
- const { autoFocus, field, forceValidation = false, onChange, value, isRequired } = props
36
- const foreignList = useList(field.refListKey)
37
- const [dialogIsOpen, setDialogOpen] = useState(false)
38
- const description = field.description || undefined
39
- const isReadOnly = onChange === undefined
40
- const [counter, setCounter] = useState(1)
41
-
42
- if (value.kind === 'count') {
43
- if (field.display === 'table') {
44
- return <RelationshipTable field={field} value={value} />
45
- }
46
- const textField = (
47
- <TextField
48
- autoFocus={autoFocus}
49
- label={field.label}
50
- description={description}
51
- isReadOnly
52
- value={value.count.toString()}
53
- width="alias.singleLineWidth"
54
- />
55
- )
56
- if (!field.refFieldKey) return textField
57
- return (
58
- <HStack gap="small" alignItems="end">
59
- {textField}
60
- <ActionButton
61
- href={`/${foreignList.path}?${buildQueryForRelationshipFieldWithForeignField(foreignList, field.refFieldKey, value.id)}`}
62
- >
63
- <Icon src={arrowUpRightIcon} />
64
- </ActionButton>
65
- </HStack>
66
- )
67
- }
68
-
69
- return (
70
- <Fragment>
71
- <VStack gap="medium">
72
- <ContextualActions onAdd={() => setDialogOpen(true)} {...props}>
73
- {value.kind === 'many' ? (
74
- <ComboboxMany
75
- autoFocus={autoFocus}
76
- label={field.label}
77
- description={description}
78
- forceValidation={forceValidation}
79
- isReadOnly={isReadOnly}
80
- isRequired={isRequired}
81
- list={foreignList}
82
- labelField={field.refLabelField}
83
- searchFields={field.refSearchFields}
84
- filter={field.selectFilter}
85
- sort={field.selectSort}
86
- state={{
87
- kind: 'many',
88
- value: value.value,
89
- onChange(newItems) {
90
- onChange?.({ ...value, value: newItems })
91
- },
92
- }}
93
- />
94
- ) : (
95
- <ComboboxSingle
96
- autoFocus={autoFocus}
97
- label={field.label}
98
- description={description}
99
- forceValidation={forceValidation}
100
- isReadOnly={isReadOnly}
101
- isRequired={isRequired}
102
- list={foreignList}
103
- labelField={field.refLabelField}
104
- searchFields={field.refSearchFields}
105
- filter={field.selectFilter}
106
- sort={field.selectSort}
107
- state={{
108
- kind: 'one',
109
- value: value.value,
110
- onChange(newItem) {
111
- onChange?.({ ...value, value: newItem })
112
- },
113
- }}
114
- />
115
- )}
116
- </ContextualActions>
117
-
118
- {value.kind === 'many' && (
119
- <TagGroup
120
- aria-label={`related ${foreignList.plural}`}
121
- isRequired={isRequired}
122
- items={value.value.map(item => ({
123
- id: item.id.toString() ?? '',
124
- label: item.label ?? '',
125
- href: item.built ? '' : `/${foreignList.path}/${item.id}`,
126
- }))}
127
- maxRows={2}
128
- onRemove={
129
- isReadOnly
130
- ? undefined
131
- : keys => {
132
- onChange?.({
133
- ...value,
134
- value: value.value.filter(item => !keys.has(item.id)),
135
- })
136
- }
137
- }
138
- renderEmptyState={() => (
139
- <Text color="neutralSecondary" size="small">
140
- No related {foreignList.plural.toLowerCase()}…
141
- </Text>
142
- )}
143
- >
144
- {renderItem}
145
- </TagGroup>
146
- )}
147
- </VStack>
148
-
149
- {!isReadOnly && (
150
- <DialogContainer onDismiss={() => setDialogOpen(false)}>
151
- {dialogIsOpen && (
152
- <BuildItemDialog
153
- listKey={foreignList.key}
154
- onChange={builtItemData => {
155
- const id = `_____temporary_${counter}`
156
- const label =
157
- (builtItemData?.[foreignList.labelField] as string | null) ??
158
- `[Unnamed ${foreignList.singular} ${counter}]`
159
- setDialogOpen(false)
160
- setCounter(counter + 1)
161
-
162
- if (value.kind === 'many') {
163
- onChange({
164
- ...value,
165
- value: [
166
- ...value.value,
167
- {
168
- id,
169
- label,
170
- data: builtItemData,
171
- built: true,
172
- },
173
- ],
174
- })
175
- } else if (value.kind === 'one') {
176
- onChange({
177
- ...value,
178
- value: {
179
- id,
180
- label,
181
- data: builtItemData,
182
- built: true,
183
- },
184
- })
185
- }
186
- }}
187
- />
188
- )}
189
- </DialogContainer>
190
- )}
191
- </Fragment>
192
- )
193
- }
194
-
195
- // NOTE: fix for `TagGroup` perf issue, should typically be okay to just
196
- // inline the render function
197
- function renderItem(item: { id: string; href: string; label: string }) {
198
- if (item.href === '') return <Item>{item.label}</Item>
199
- return <Item href={item.href}>{item.label}</Item>
200
- }
201
-
202
- export const Cell: CellComponent<typeof controller> = ({ field, item }) => {
203
- const list = useList(field.refListKey)
204
-
205
- if (field.display === 'count' || field.display === 'table') {
206
- const count = item[`${field.fieldKey}Count`] as number
207
- return count != null ? <Numeral value={count} abbreviate /> : null
208
- }
209
-
210
- const data = item[field.fieldKey]
211
- const items = (Array.isArray(data) ? data : [data]).filter(Boolean)
212
- const displayItems = items.length < 3 ? items : items.slice(0, 2)
213
- const overflow = items.length < 3 ? 0 : items.length - 2
214
-
215
- return (
216
- <Text>
217
- {displayItems.map((item, index) => (
218
- <Fragment key={item.id}>
219
- {index ? ', ' : ''}
220
- <TextLink href={`/${list.path}/${item.id}`}>{item.label || item.id}</TextLink>
221
- </Fragment>
222
- ))}
223
- {overflow ? `, and ${overflow} more` : null}
224
- </Text>
225
- )
226
- }
227
-
228
- export function controller(
229
- config: FieldControllerConfig<
230
- {
231
- refFieldKey?: string
232
- refListKey: string
233
- many: boolean
234
- hideCreate: boolean
235
- refLabelField: string
236
- refSearchFields: string[]
237
- } & (
238
- | {
239
- displayMode: 'select'
240
- filter: Record<string, any> | null
241
- sort: ListSortDescriptor<string> | null
242
- }
243
- | { displayMode: 'count' }
244
- | {
245
- displayMode: 'table'
246
- refFieldKey: string
247
- initialSort: ListSortDescriptor<string> | null
248
- columns: string[] | null
249
- }
250
- )
251
- >
252
- ): RelationshipController {
253
- const { listKey, fieldKey: fieldKey, label, description } = config
254
- const { displayMode, hideCreate, many, refFieldKey, refLabelField, refListKey, refSearchFields } =
255
- config.fieldMeta
256
-
257
- return {
258
- refFieldKey,
259
- many,
260
- listKey,
261
- fieldKey,
262
- label,
263
- description,
264
- display: displayMode,
265
- refLabelField,
266
- refSearchFields,
267
- refListKey,
268
- graphqlSelection:
269
- displayMode === 'count' || displayMode === 'table'
270
- ? `${fieldKey}Count`
271
- : `${fieldKey}${many && config.fieldMeta.sort ? `(orderBy: { ${config.fieldMeta.sort.field}: ${config.fieldMeta.sort.direction.toLowerCase()} })` : ''} {
272
- id
273
- label: ${refLabelField}
274
- }`,
275
- hideCreate: hideCreate || displayMode === 'table',
276
- columns: displayMode === 'table' ? config.fieldMeta.columns : null,
277
- initialSort: displayMode === 'table' ? config.fieldMeta.initialSort : null,
278
- selectFilter: displayMode === 'select' ? config.fieldMeta.filter : null,
279
- selectSort: displayMode === 'select' ? config.fieldMeta.sort : null,
280
- // note we're not making the state kind: 'count' when ui.displayMode is set to 'count'.
281
- // that ui.displayMode: 'count' is really just a way to have reasonable performance
282
- // because our other UIs don't handle relationships with a large number of items well
283
- // but that's not a problem here since we're creating a new item so we might as well them a better UI
284
- defaultValue: many
285
- ? {
286
- kind: 'many',
287
- id: null,
288
- initialValue: [],
289
- value: [],
290
- }
291
- : {
292
- kind: 'one',
293
- id: null,
294
- value: null,
295
- initialValue: null,
296
- },
297
- validate(value, opts) {
298
- if ('count' in value) return true
299
- return opts.isRequired
300
- ? value.kind === 'one'
301
- ? value.value !== null
302
- : value.value.length > 0
303
- : true
304
- },
305
- deserialize: data => {
306
- if (displayMode === 'count' || displayMode === 'table') {
307
- return {
308
- id: data.id,
309
- kind: 'count',
310
- count: data[`${config.fieldKey}Count`] ?? 0,
311
- }
312
- }
313
- if (many) {
314
- const value = (data[config.fieldKey] || []).map((x: any) => ({
315
- id: x.id,
316
- label: x.label || x.id,
317
- }))
318
- return {
319
- kind: 'many',
320
- id: data.id,
321
- initialValue: value,
322
- value,
323
- }
324
- }
325
- let value = data[config.fieldKey]
326
- if (value) {
327
- value = {
328
- id: value.id,
329
- label: value.label || value.id,
330
- }
331
- }
332
- return {
333
- kind: 'one',
334
- id: data.id,
335
- value,
336
- initialValue: value,
337
- }
338
- },
339
- serialize: state => {
340
- if (state.kind === 'many') {
341
- const newAllIds = new Set(state.value.map(x => x.id))
342
- const initialIds = new Set(state.initialValue.map(x => x.id))
343
- const disconnect = state.initialValue
344
- .filter(x => !newAllIds.has(x.id))
345
- .map(x => ({ id: x.id }))
346
- const connect = state.value
347
- .filter(x => !x.built && !initialIds.has(x.id))
348
- .map(x => ({ id: x.id }))
349
- const create = state.value.filter(x => x.built).map(x => x.data)
350
- const output = {
351
- ...(disconnect.length ? { disconnect } : {}),
352
- ...(connect.length ? { connect } : {}),
353
- ...(create.length ? { create } : {}),
354
- }
355
-
356
- if (Object.keys(output).length) {
357
- return {
358
- [config.fieldKey]: output,
359
- }
360
- }
361
- } else if (state.kind === 'one') {
362
- if (state.initialValue && !state.value) return { [config.fieldKey]: { disconnect: true } }
363
- if (state.value?.built) {
364
- return {
365
- [config.fieldKey]: {
366
- create: state.value.data,
367
- },
368
- }
369
- }
370
- if (state.value && state.value.id !== state.initialValue?.id) {
371
- return {
372
- [config.fieldKey]: {
373
- connect: {
374
- id: state.value.id,
375
- },
376
- },
377
- }
378
- }
379
- }
380
- return {}
381
- },
382
- filter: {
383
- Filter(props) {
384
- const foreignList = useList(refListKey)
385
- if (props.type === 'empty' || props.type === 'not_empty') return null
386
- // TODO: show labels rather than ids
387
- if (props.type === 'is' || props.type === 'not_is') {
388
- return (
389
- <ComboboxSingle
390
- autoFocus
391
- aria-label={label}
392
- isReadOnly={false}
393
- labelField={refLabelField}
394
- searchFields={refSearchFields}
395
- list={foreignList}
396
- state={{
397
- kind: 'one',
398
- value:
399
- typeof props.value === 'string'
400
- ? { id: props.value, label: props.value, built: false }
401
- : null,
402
- onChange(newItem) {
403
- props.onChange(newItem === null ? null : newItem.id.toString())
404
- },
405
- }}
406
- filter={config.fieldMeta.displayMode === 'select' ? config.fieldMeta.filter : null}
407
- sort={config.fieldMeta.displayMode === 'select' ? config.fieldMeta.sort : null}
408
- />
409
- )
410
- }
411
- const ids = Array.isArray(props.value) ? props.value : []
412
- const value = ids.map((id): RelationshipValue => ({ id, label: id, built: false }))
413
- return (
414
- <VStack gap="medium">
415
- <ComboboxMany
416
- autoFocus
417
- aria-label={label}
418
- isReadOnly={false}
419
- labelField={refLabelField}
420
- searchFields={refSearchFields}
421
- list={foreignList}
422
- state={{
423
- kind: 'many',
424
- value,
425
- onChange(newItem) {
426
- props.onChange(newItem.map(x => x.id.toString()))
427
- },
428
- }}
429
- filter={config.fieldMeta.displayMode === 'select' ? config.fieldMeta.filter : null}
430
- sort={config.fieldMeta.displayMode === 'select' ? config.fieldMeta.sort : null}
431
- />
432
- <TagGroup
433
- aria-label={`related ${foreignList.plural}`}
434
- items={value.map(item => ({
435
- id: item.id.toString() ?? '',
436
- label: item.label ?? '',
437
- href: item.built ? '' : `/${foreignList.path}/${item.id}`,
438
- }))}
439
- maxRows={2}
440
- onRemove={keys => {
441
- props.onChange(ids.filter(id => !keys.has(id)))
442
- }}
443
- renderEmptyState={() => (
444
- <Text color="neutralSecondary" size="small">
445
- Select related {foreignList.plural.toLowerCase()}…
446
- </Text>
447
- )}
448
- >
449
- {renderItem}
450
- </TagGroup>
451
- </VStack>
452
- )
453
- },
454
- Label({ label, type, value }) {
455
- const listFormatter = useListFormatter({
456
- style: 'short',
457
- type: 'disjunction',
458
- })
459
-
460
- if (['empty', 'not_empty'].includes(type)) return label.toLowerCase()
461
- if (['is', 'not_is'].includes(type)) return `${label.toLowerCase()} ${value}`
462
- return `${label.toLowerCase()} (${listFormatter.format(value || [''])})`
463
- },
464
- graphql: ({ type, value }) => {
465
- if (type === 'empty' && !many) return { [config.fieldKey]: { equals: null } }
466
- if (type === 'empty' && many) return { [config.fieldKey]: { none: {} } }
467
- if (type === 'not_empty' && !many) return { [config.fieldKey]: { not: { equals: null } } }
468
- if (type === 'not_empty' && many) return { [config.fieldKey]: { some: {} } }
469
- if (type === 'is') return { [config.fieldKey]: { id: { equals: value } } }
470
- if (type === 'not_is') return { [config.fieldKey]: { not: { id: { equals: value } } } }
471
- if (type === 'some') return { [config.fieldKey]: { some: { id: { in: value } } } }
472
- if (type === 'not_some')
473
- return { [config.fieldKey]: { not: { some: { id: { in: value } } } } }
474
- return { [config.fieldKey]: { [type]: value } } // uh
475
- },
476
- parseGraphQL: () => [],
477
- types: {
478
- empty: { label: 'Is empty', initialValue: null },
479
- not_empty: { label: 'Is not empty', initialValue: null },
480
- ...(many
481
- ? {
482
- some: { label: 'Is one of', initialValue: [] },
483
- not_some: { label: 'Is not one of', initialValue: [] },
484
- }
485
- : {
486
- is: { label: 'Is', initialValue: null },
487
- not_is: { label: 'Is not', initialValue: null },
488
- }),
489
- },
490
- },
491
- }
492
- }
1
+ import { useListFormatter } from '@react-aria/i18n'
2
+ import { Fragment, useState } from 'react'
3
+
4
+ import { DialogContainer } from '@keystar/ui/dialog'
5
+ import { HStack, VStack } from '@keystar/ui/layout'
6
+ import { TextLink } from '@keystar/ui/link'
7
+ import { Item, TagGroup } from '@keystar/ui/tag'
8
+ import { TextField } from '@keystar/ui/text-field'
9
+ import { Numeral, Text } from '@keystar/ui/typography'
10
+
11
+ import { BuildItemDialog } from '../../../../admin-ui/components'
12
+ import { useList } from '../../../../admin-ui/context'
13
+ import type {
14
+ CellComponent,
15
+ FieldControllerConfig,
16
+ FieldProps,
17
+ CollectionSortDescriptor,
18
+ } from '../../../../types'
19
+
20
+ import { ActionButton } from '@keystar/ui/button'
21
+ import { Icon } from '@keystar/ui/icon'
22
+ import { arrowUpRightIcon } from '@keystar/ui/icon/icons/arrowUpRightIcon'
23
+ import { ComboboxMany } from './ComboboxMany'
24
+ import { ComboboxSingle } from './ComboboxSingle'
25
+ import {
26
+ buildQueryForRelationshipFieldWithForeignField,
27
+ ContextualActions,
28
+ } from './ContextualActions'
29
+ import { RelationshipTable } from './RelationshipTable'
30
+ import type { RelationshipController, RelationshipValue } from './types'
31
+
32
+ export { ComboboxMany, ComboboxSingle }
33
+
34
+ export function Field(props: FieldProps<typeof controller>) {
35
+ const { autoFocus, field, forceValidation = false, onChange, value, isRequired } = props
36
+ const foreignList = useList(field.refListKey)
37
+ const [dialogIsOpen, setDialogOpen] = useState(false)
38
+ const description = field.description || undefined
39
+ const isReadOnly = onChange === undefined
40
+ const [counter, setCounter] = useState(1)
41
+
42
+ if (value.kind === 'count') {
43
+ if (field.display === 'table') {
44
+ return <RelationshipTable field={field} value={value} />
45
+ }
46
+ const textField = (
47
+ <TextField
48
+ autoFocus={autoFocus}
49
+ label={field.label}
50
+ description={description}
51
+ isReadOnly
52
+ value={value.count.toString()}
53
+ width="alias.singleLineWidth"
54
+ />
55
+ )
56
+ if (!field.refFieldKey) return textField
57
+ return (
58
+ <HStack gap="small" alignItems="end">
59
+ {textField}
60
+ <ActionButton
61
+ href={`/${foreignList.path}?${buildQueryForRelationshipFieldWithForeignField(foreignList, field.refFieldKey, value.id)}`}
62
+ >
63
+ <Icon src={arrowUpRightIcon} />
64
+ </ActionButton>
65
+ </HStack>
66
+ )
67
+ }
68
+
69
+ return (
70
+ <Fragment>
71
+ <VStack gap="medium">
72
+ <ContextualActions onAdd={() => setDialogOpen(true)} {...props}>
73
+ {value.kind === 'many' ? (
74
+ <ComboboxMany
75
+ autoFocus={autoFocus}
76
+ label={field.label}
77
+ description={description}
78
+ forceValidation={forceValidation}
79
+ isReadOnly={isReadOnly}
80
+ isRequired={isRequired}
81
+ list={foreignList}
82
+ labelField={field.refLabelField}
83
+ searchFields={field.refSearchFields}
84
+ filter={field.selectFilter}
85
+ sort={field.selectSort}
86
+ state={{
87
+ kind: 'many',
88
+ value: value.value,
89
+ onChange(newItems) {
90
+ onChange?.({ ...value, value: newItems })
91
+ },
92
+ }}
93
+ />
94
+ ) : (
95
+ <ComboboxSingle
96
+ autoFocus={autoFocus}
97
+ label={field.label}
98
+ description={description}
99
+ forceValidation={forceValidation}
100
+ isReadOnly={isReadOnly}
101
+ isRequired={isRequired}
102
+ list={foreignList}
103
+ labelField={field.refLabelField}
104
+ searchFields={field.refSearchFields}
105
+ filter={field.selectFilter}
106
+ sort={field.selectSort}
107
+ state={{
108
+ kind: 'one',
109
+ value: value.value,
110
+ onChange(newItem) {
111
+ onChange?.({ ...value, value: newItem })
112
+ },
113
+ }}
114
+ />
115
+ )}
116
+ </ContextualActions>
117
+
118
+ {value.kind === 'many' && (
119
+ <TagGroup
120
+ aria-label={`related ${foreignList.plural}`}
121
+ isRequired={isRequired}
122
+ items={value.value.map(item => ({
123
+ id: item.id.toString() ?? '',
124
+ label: item.label ?? '',
125
+ href: item.built ? '' : `/${foreignList.path}/${item.id}`,
126
+ }))}
127
+ maxRows={2}
128
+ onRemove={
129
+ isReadOnly
130
+ ? undefined
131
+ : keys => {
132
+ onChange?.({
133
+ ...value,
134
+ value: value.value.filter(item => !keys.has(item.id)),
135
+ })
136
+ }
137
+ }
138
+ renderEmptyState={() => (
139
+ <Text color="neutralSecondary" size="small">
140
+ No related {foreignList.plural.toLowerCase()}…
141
+ </Text>
142
+ )}
143
+ >
144
+ {renderItem}
145
+ </TagGroup>
146
+ )}
147
+ </VStack>
148
+
149
+ {!isReadOnly && (
150
+ <DialogContainer onDismiss={() => setDialogOpen(false)}>
151
+ {dialogIsOpen && (
152
+ <BuildItemDialog
153
+ listKey={foreignList.key}
154
+ onChange={builtItemData => {
155
+ const id = `_____temporary_${counter}`
156
+ const label =
157
+ (builtItemData?.[foreignList.labelField] as string | null) ??
158
+ `[Unnamed ${foreignList.singular} ${counter}]`
159
+ setDialogOpen(false)
160
+ setCounter(counter + 1)
161
+
162
+ if (value.kind === 'many') {
163
+ onChange({
164
+ ...value,
165
+ value: [
166
+ ...value.value,
167
+ {
168
+ id,
169
+ label,
170
+ data: builtItemData,
171
+ built: true,
172
+ },
173
+ ],
174
+ })
175
+ } else if (value.kind === 'one') {
176
+ onChange({
177
+ ...value,
178
+ value: {
179
+ id,
180
+ label,
181
+ data: builtItemData,
182
+ built: true,
183
+ },
184
+ })
185
+ }
186
+ }}
187
+ />
188
+ )}
189
+ </DialogContainer>
190
+ )}
191
+ </Fragment>
192
+ )
193
+ }
194
+
195
+ // NOTE: fix for `TagGroup` perf issue, should typically be okay to just
196
+ // inline the render function
197
+ function renderItem(item: { id: string; href: string; label: string }) {
198
+ if (item.href === '') return <Item>{item.label}</Item>
199
+ return <Item href={item.href}>{item.label}</Item>
200
+ }
201
+
202
+ export const Cell: CellComponent<typeof controller> = ({ field, item }) => {
203
+ const list = useList(field.refListKey)
204
+
205
+ if (field.display === 'count' || field.display === 'table') {
206
+ const count = item[`${field.fieldKey}Count`] as number
207
+ return count != null ? <Numeral value={count} abbreviate /> : null
208
+ }
209
+
210
+ const data = item[field.fieldKey]
211
+ const items = (Array.isArray(data) ? data : [data]).filter(Boolean)
212
+ const displayItems = items.length < 3 ? items : items.slice(0, 2)
213
+ const overflow = items.length < 3 ? 0 : items.length - 2
214
+
215
+ return (
216
+ <Text>
217
+ {displayItems.map((item, index) => (
218
+ <Fragment key={item.id}>
219
+ {index ? ', ' : ''}
220
+ <TextLink href={`/${list.path}/${item.id}`}>{item.label || item.id}</TextLink>
221
+ </Fragment>
222
+ ))}
223
+ {overflow ? `, and ${overflow} more` : null}
224
+ </Text>
225
+ )
226
+ }
227
+
228
+ export function controller(
229
+ config: FieldControllerConfig<
230
+ {
231
+ refFieldKey?: string
232
+ refListKey: string
233
+ many: boolean
234
+ hideCreate: boolean
235
+ refLabelField: string
236
+ refSearchFields: string[]
237
+ } & (
238
+ | {
239
+ displayMode: 'select'
240
+ filter: Record<string, any> | null
241
+ sort: CollectionSortDescriptor<string> | null
242
+ }
243
+ | { displayMode: 'count' }
244
+ | {
245
+ displayMode: 'table'
246
+ refFieldKey: string
247
+ initialSort: CollectionSortDescriptor<string> | null
248
+ columns: string[] | null
249
+ }
250
+ )
251
+ >
252
+ ): RelationshipController {
253
+ const { listKey, fieldKey: fieldKey, label, description } = config
254
+ const { displayMode, hideCreate, many, refFieldKey, refLabelField, refListKey, refSearchFields } =
255
+ config.fieldMeta
256
+
257
+ return {
258
+ refFieldKey,
259
+ many,
260
+ listKey,
261
+ fieldKey,
262
+ label,
263
+ description,
264
+ display: displayMode,
265
+ refLabelField,
266
+ refSearchFields,
267
+ refListKey,
268
+ graphqlSelection:
269
+ displayMode === 'count' || displayMode === 'table'
270
+ ? `${fieldKey}Count`
271
+ : `${fieldKey}${many && config.fieldMeta.sort ? `(orderBy: { ${config.fieldMeta.sort.field}: ${config.fieldMeta.sort.direction.toLowerCase()} })` : ''} {
272
+ id
273
+ label: ${refLabelField}
274
+ }`,
275
+ hideCreate: hideCreate || displayMode === 'table',
276
+ columns: displayMode === 'table' ? config.fieldMeta.columns : null,
277
+ initialSort: displayMode === 'table' ? config.fieldMeta.initialSort : null,
278
+ selectFilter: displayMode === 'select' ? config.fieldMeta.filter : null,
279
+ selectSort: displayMode === 'select' ? config.fieldMeta.sort : null,
280
+ // note we're not making the state kind: 'count' when ui.displayMode is set to 'count'.
281
+ // that ui.displayMode: 'count' is really just a way to have reasonable performance
282
+ // because our other UIs don't handle relationships with a large number of items well
283
+ // but that's not a problem here since we're creating a new item so we might as well them a better UI
284
+ defaultValue: many
285
+ ? {
286
+ kind: 'many',
287
+ id: null,
288
+ initialValue: [],
289
+ value: [],
290
+ }
291
+ : {
292
+ kind: 'one',
293
+ id: null,
294
+ value: null,
295
+ initialValue: null,
296
+ },
297
+ validate(value, opts) {
298
+ if ('count' in value) return true
299
+ return opts.isRequired
300
+ ? value.kind === 'one'
301
+ ? value.value !== null
302
+ : value.value.length > 0
303
+ : true
304
+ },
305
+ deserialize: data => {
306
+ if (displayMode === 'count' || displayMode === 'table') {
307
+ return {
308
+ id: data.id,
309
+ kind: 'count',
310
+ count: data[`${config.fieldKey}Count`] ?? 0,
311
+ }
312
+ }
313
+ if (many) {
314
+ const value = (data[config.fieldKey] || []).map((x: any) => ({
315
+ id: x.id,
316
+ label: x.label || x.id,
317
+ }))
318
+ return {
319
+ kind: 'many',
320
+ id: data.id,
321
+ initialValue: value,
322
+ value,
323
+ }
324
+ }
325
+ let value = data[config.fieldKey]
326
+ if (value) {
327
+ value = {
328
+ id: value.id,
329
+ label: value.label || value.id,
330
+ }
331
+ }
332
+ return {
333
+ kind: 'one',
334
+ id: data.id,
335
+ value,
336
+ initialValue: value,
337
+ }
338
+ },
339
+ serialize: state => {
340
+ if (state.kind === 'many') {
341
+ const newAllIds = new Set(state.value.map(x => x.id))
342
+ const initialIds = new Set(state.initialValue.map(x => x.id))
343
+ const disconnect = state.initialValue
344
+ .filter(x => !newAllIds.has(x.id))
345
+ .map(x => ({ id: x.id }))
346
+ const connect = state.value
347
+ .filter(x => !x.built && !initialIds.has(x.id))
348
+ .map(x => ({ id: x.id }))
349
+ const create = state.value.filter(x => x.built).map(x => x.data)
350
+ const output = {
351
+ ...(disconnect.length ? { disconnect } : {}),
352
+ ...(connect.length ? { connect } : {}),
353
+ ...(create.length ? { create } : {}),
354
+ }
355
+
356
+ if (Object.keys(output).length) {
357
+ return {
358
+ [config.fieldKey]: output,
359
+ }
360
+ }
361
+ } else if (state.kind === 'one') {
362
+ if (state.initialValue && !state.value) return { [config.fieldKey]: { disconnect: true } }
363
+ if (state.value?.built) {
364
+ return {
365
+ [config.fieldKey]: {
366
+ create: state.value.data,
367
+ },
368
+ }
369
+ }
370
+ if (state.value && state.value.id !== state.initialValue?.id) {
371
+ return {
372
+ [config.fieldKey]: {
373
+ connect: {
374
+ id: state.value.id,
375
+ },
376
+ },
377
+ }
378
+ }
379
+ }
380
+ return {}
381
+ },
382
+ filter: {
383
+ Filter(props) {
384
+ const foreignList = useList(refListKey)
385
+ if (props.type === 'empty' || props.type === 'not_empty') return null
386
+ // TODO: show labels rather than ids
387
+ if (props.type === 'is' || props.type === 'not_is') {
388
+ return (
389
+ <ComboboxSingle
390
+ autoFocus
391
+ aria-label={label}
392
+ isReadOnly={false}
393
+ labelField={refLabelField}
394
+ searchFields={refSearchFields}
395
+ list={foreignList}
396
+ state={{
397
+ kind: 'one',
398
+ value:
399
+ typeof props.value === 'string'
400
+ ? { id: props.value, label: props.value, built: false }
401
+ : null,
402
+ onChange(newItem) {
403
+ props.onChange(newItem === null ? null : newItem.id.toString())
404
+ },
405
+ }}
406
+ filter={config.fieldMeta.displayMode === 'select' ? config.fieldMeta.filter : null}
407
+ sort={config.fieldMeta.displayMode === 'select' ? config.fieldMeta.sort : null}
408
+ />
409
+ )
410
+ }
411
+ const ids = Array.isArray(props.value) ? props.value : []
412
+ const value = ids.map((id): RelationshipValue => ({ id, label: id, built: false }))
413
+ return (
414
+ <VStack gap="medium">
415
+ <ComboboxMany
416
+ autoFocus
417
+ aria-label={label}
418
+ isReadOnly={false}
419
+ labelField={refLabelField}
420
+ searchFields={refSearchFields}
421
+ list={foreignList}
422
+ state={{
423
+ kind: 'many',
424
+ value,
425
+ onChange(newItem) {
426
+ props.onChange(newItem.map(x => x.id.toString()))
427
+ },
428
+ }}
429
+ filter={config.fieldMeta.displayMode === 'select' ? config.fieldMeta.filter : null}
430
+ sort={config.fieldMeta.displayMode === 'select' ? config.fieldMeta.sort : null}
431
+ />
432
+ <TagGroup
433
+ aria-label={`related ${foreignList.plural}`}
434
+ items={value.map(item => ({
435
+ id: item.id.toString() ?? '',
436
+ label: item.label ?? '',
437
+ href: item.built ? '' : `/${foreignList.path}/${item.id}`,
438
+ }))}
439
+ maxRows={2}
440
+ onRemove={keys => {
441
+ props.onChange(ids.filter(id => !keys.has(id)))
442
+ }}
443
+ renderEmptyState={() => (
444
+ <Text color="neutralSecondary" size="small">
445
+ Select related {foreignList.plural.toLowerCase()}…
446
+ </Text>
447
+ )}
448
+ >
449
+ {renderItem}
450
+ </TagGroup>
451
+ </VStack>
452
+ )
453
+ },
454
+ Label({ label, type, value }) {
455
+ const listFormatter = useListFormatter({
456
+ style: 'short',
457
+ type: 'disjunction',
458
+ })
459
+
460
+ if (['empty', 'not_empty'].includes(type)) return label.toLowerCase()
461
+ if (['is', 'not_is'].includes(type)) return `${label.toLowerCase()} ${value}`
462
+ return `${label.toLowerCase()} (${listFormatter.format(value || [''])})`
463
+ },
464
+ graphql: ({ type, value }) => {
465
+ if (type === 'empty' && !many) return { [config.fieldKey]: { equals: null } }
466
+ if (type === 'empty' && many) return { [config.fieldKey]: { none: {} } }
467
+ if (type === 'not_empty' && !many) return { [config.fieldKey]: { not: { equals: null } } }
468
+ if (type === 'not_empty' && many) return { [config.fieldKey]: { some: {} } }
469
+ if (type === 'is') return { [config.fieldKey]: { id: { equals: value } } }
470
+ if (type === 'not_is') return { [config.fieldKey]: { not: { id: { equals: value } } } }
471
+ if (type === 'some') return { [config.fieldKey]: { some: { id: { in: value } } } }
472
+ if (type === 'not_some')
473
+ return { [config.fieldKey]: { not: { some: { id: { in: value } } } } }
474
+ return { [config.fieldKey]: { [type]: value } } // uh
475
+ },
476
+ parseGraphQL: () => [],
477
+ types: {
478
+ empty: { label: 'Is empty', initialValue: null },
479
+ not_empty: { label: 'Is not empty', initialValue: null },
480
+ ...(many
481
+ ? {
482
+ some: { label: 'Is one of', initialValue: [] },
483
+ not_some: { label: 'Is not one of', initialValue: [] },
484
+ }
485
+ : {
486
+ is: { label: 'Is', initialValue: null },
487
+ not_is: { label: 'Is not', initialValue: null },
488
+ }),
489
+ },
490
+ },
491
+ }
492
+ }