@nixxie-cms/core 1.0.0 → 1.0.2

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 (187) hide show
  1. package/README.md +2 -2
  2. package/admin-ui/components/dist/nixxie-cms-core-admin-ui-components.cjs.js +4 -4
  3. package/admin-ui/components/dist/nixxie-cms-core-admin-ui-components.esm.js +4 -4
  4. package/admin-ui/context/dist/nixxie-cms-core-admin-ui-context.cjs.js +2 -2
  5. package/admin-ui/context/dist/nixxie-cms-core-admin-ui-context.esm.js +2 -2
  6. package/context/dist/nixxie-cms-core-context.cjs.js +2 -2
  7. package/context/dist/nixxie-cms-core-context.esm.js +2 -2
  8. package/dist/{CreateItemDialog-33335548.esm.js → CreateItemDialog-7008b050.esm.js} +1 -1
  9. package/dist/{CreateItemDialog-56cf59b7.cjs.js → CreateItemDialog-a0cab315.cjs.js} +1 -1
  10. package/dist/{PageContainer-7db73317.esm.js → PageContainer-5ae731cc.esm.js} +25 -18
  11. package/dist/{PageContainer-27c27f10.cjs.js → PageContainer-abd7159f.cjs.js} +25 -18
  12. package/dist/{admin-meta-graphql-6f7f5331.esm.js → admin-meta-graphql-0e6e606e.esm.js} +1 -1
  13. package/dist/{admin-meta-graphql-c8f926e9.cjs.js → admin-meta-graphql-306c224a.cjs.js} +1 -1
  14. package/dist/{context-3132c3ed.esm.js → context-af9957ed.esm.js} +2 -2
  15. package/dist/{context-e7a45152.cjs.js → context-b5204629.cjs.js} +2 -2
  16. package/dist/declarations/src/admin-ui/components/Navigation.d.ts.map +1 -1
  17. package/dist/declarations/src/admin-ui/components/PageContainer.d.ts.map +1 -1
  18. package/dist/declarations/src/helpers.d.ts.map +1 -1
  19. package/dist/declarations/src/index.d.ts +1 -0
  20. package/dist/declarations/src/index.d.ts.map +1 -1
  21. package/dist/declarations/src/internal-unstable/admin-ui/id-field-view.d.ts.map +1 -0
  22. package/dist/declarations/src/internal-unstable/admin-ui/pages/App/index.d.ts.map +1 -0
  23. package/dist/declarations/src/internal-unstable/admin-ui/pages/CreateItemPage/index.d.ts.map +1 -0
  24. package/dist/declarations/src/internal-unstable/admin-ui/pages/HomePage/index.d.ts.map +1 -0
  25. package/dist/declarations/src/internal-unstable/admin-ui/pages/ItemPage/index.d.ts.map +1 -0
  26. package/dist/declarations/src/internal-unstable/admin-ui/pages/ListPage/index.d.ts.map +1 -0
  27. package/dist/declarations/src/internal-unstable/admin-ui/pages/NoAccessPage/index.d.ts.map +1 -0
  28. package/dist/declarations/src/internal-unstable/artifacts.d.ts.map +1 -0
  29. package/dist/declarations/src/lib/core/initialise-lists.d.ts +1 -1
  30. package/dist/declarations/src/schema.d.ts.map +1 -1
  31. package/dist/declarations/src/types/config/index.d.ts +60 -1
  32. package/dist/declarations/src/types/config/index.d.ts.map +1 -1
  33. package/dist/declarations/src/types/config/lists.d.ts +4 -4
  34. package/dist/declarations/src/types/context.d.ts +150 -0
  35. package/dist/declarations/src/types/context.d.ts.map +1 -1
  36. package/dist/declarations/src/types/next-fields.d.ts +1 -1
  37. package/dist/{express-e9ed9a7d.cjs.js → express-455ae20c.cjs.js} +1 -1
  38. package/dist/{express-6743b918.esm.js → express-7559ca2d.esm.js} +1 -1
  39. package/dist/{index-ac01583b.cjs.js → index-89635494.cjs.js} +4 -4
  40. package/dist/{index-24b78415.esm.js → index-baa799e0.esm.js} +4 -4
  41. package/dist/nixxie-cms-core.cjs.js +104 -77
  42. package/dist/nixxie-cms-core.esm.js +104 -77
  43. package/dist/{non-null-graphql-5315718c.esm.js → non-null-graphql-a84ed64d.esm.js} +1 -1
  44. package/dist/{non-null-graphql-17b83ddc.cjs.js → non-null-graphql-add6bb3d.cjs.js} +1 -1
  45. package/dist/{resolve-hooks-66fe8a8e.cjs.js → resolve-hooks-165a9ce2.cjs.js} +1 -1
  46. package/dist/{resolve-hooks-17aafd37.esm.js → resolve-hooks-6813a045.esm.js} +2 -2
  47. package/dist/{system-dfec2f0a.esm.js → system-03e49e4f.esm.js} +8 -4
  48. package/dist/{system-48c5f6df.cjs.js → system-a321642d.cjs.js} +8 -4
  49. package/dist/{useFilter-0b5a1ee6.esm.js → useFilter-9b6db1f9.esm.js} +1 -1
  50. package/dist/{useFilter-1a4e6900.cjs.js → useFilter-acc9d413.cjs.js} +1 -1
  51. package/fields/dist/nixxie-cms-core-fields.cjs.js +16 -16
  52. package/fields/dist/nixxie-cms-core-fields.esm.js +17 -17
  53. package/fields/types/bytes/dist/nixxie-cms-core-fields-types-bytes.cjs.js +3 -3
  54. package/fields/types/bytes/dist/nixxie-cms-core-fields-types-bytes.esm.js +3 -3
  55. package/fields/types/bytes/views/dist/nixxie-cms-core-fields-types-bytes-views.cjs.js +1 -1
  56. package/fields/types/bytes/views/dist/nixxie-cms-core-fields-types-bytes-views.esm.js +1 -1
  57. package/fields/types/password/dist/nixxie-cms-core-fields-types-password.cjs.js +3 -3
  58. package/fields/types/password/dist/nixxie-cms-core-fields-types-password.esm.js +3 -3
  59. package/fields/types/relationship/views/dist/nixxie-cms-core-fields-types-relationship-views.cjs.js +4 -4
  60. package/fields/types/relationship/views/dist/nixxie-cms-core-fields-types-relationship-views.esm.js +4 -4
  61. package/fields/types/select/views/dist/nixxie-cms-core-fields-types-select-views.cjs.js +1 -1
  62. package/fields/types/select/views/dist/nixxie-cms-core-fields-types-select-views.esm.js +1 -1
  63. package/fields/types/text/views/dist/nixxie-cms-core-fields-types-text-views.cjs.js +1 -1
  64. package/fields/types/text/views/dist/nixxie-cms-core-fields-types-text-views.esm.js +1 -1
  65. package/internal-unstable/admin-ui/id-field-view/dist/nixxie-cms-core-internal-unstable-admin-ui-id-field-view.cjs.d.ts +2 -0
  66. package/internal-unstable/admin-ui/id-field-view/dist/nixxie-cms-core-internal-unstable-admin-ui-id-field-view.cjs.js +244 -0
  67. package/internal-unstable/admin-ui/id-field-view/dist/nixxie-cms-core-internal-unstable-admin-ui-id-field-view.esm.js +235 -0
  68. package/internal-unstable/admin-ui/id-field-view/package.json +4 -0
  69. package/internal-unstable/admin-ui/next-config/package.json +4 -0
  70. package/internal-unstable/admin-ui/pages/App/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-App.cjs.d.ts +2 -0
  71. package/internal-unstable/admin-ui/pages/App/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-App.cjs.js +59 -0
  72. package/internal-unstable/admin-ui/pages/App/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-App.esm.js +55 -0
  73. package/internal-unstable/admin-ui/pages/App/package.json +4 -0
  74. package/internal-unstable/admin-ui/pages/CreateItemPage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-CreateItemPage.cjs.d.ts +2 -0
  75. package/internal-unstable/admin-ui/pages/CreateItemPage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-CreateItemPage.cjs.js +116 -0
  76. package/internal-unstable/admin-ui/pages/CreateItemPage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-CreateItemPage.esm.js +112 -0
  77. package/internal-unstable/admin-ui/pages/CreateItemPage/package.json +4 -0
  78. package/internal-unstable/admin-ui/pages/HomePage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-HomePage.cjs.d.ts +2 -0
  79. package/internal-unstable/admin-ui/pages/HomePage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-HomePage.cjs.js +336 -0
  80. package/internal-unstable/admin-ui/pages/HomePage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-HomePage.esm.js +332 -0
  81. package/internal-unstable/admin-ui/pages/HomePage/package.json +4 -0
  82. package/internal-unstable/admin-ui/pages/ItemPage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-ItemPage.cjs.d.ts +2 -0
  83. package/internal-unstable/admin-ui/pages/ItemPage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-ItemPage.cjs.js +463 -0
  84. package/internal-unstable/admin-ui/pages/ItemPage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-ItemPage.esm.js +455 -0
  85. package/internal-unstable/admin-ui/pages/ItemPage/package.json +4 -0
  86. package/internal-unstable/admin-ui/pages/ListPage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-ListPage.cjs.d.ts +2 -0
  87. package/internal-unstable/admin-ui/pages/ListPage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-ListPage.cjs.js +1195 -0
  88. package/internal-unstable/admin-ui/pages/ListPage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-ListPage.esm.js +1187 -0
  89. package/internal-unstable/admin-ui/pages/ListPage/package.json +4 -0
  90. package/internal-unstable/admin-ui/pages/NoAccessPage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-NoAccessPage.cjs.d.ts +2 -0
  91. package/internal-unstable/admin-ui/pages/NoAccessPage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-NoAccessPage.cjs.js +40 -0
  92. package/internal-unstable/admin-ui/pages/NoAccessPage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-NoAccessPage.esm.js +35 -0
  93. package/internal-unstable/admin-ui/pages/NoAccessPage/package.json +4 -0
  94. package/internal-unstable/artifacts/dist/nixxie-cms-core-internal-unstable-artifacts.cjs.d.ts +2 -0
  95. package/internal-unstable/artifacts/dist/nixxie-cms-core-internal-unstable-artifacts.cjs.js +51 -0
  96. package/internal-unstable/artifacts/dist/nixxie-cms-core-internal-unstable-artifacts.esm.js +38 -0
  97. package/internal-unstable/artifacts/package.json +4 -0
  98. package/package.json +44 -44
  99. package/scripts/cli/dist/nixxie-cms-core-scripts-cli.cjs.js +44 -15
  100. package/scripts/cli/dist/nixxie-cms-core-scripts-cli.esm.js +44 -15
  101. package/scripts/dist/nixxie-cms-core-scripts.cjs.js +3 -3
  102. package/scripts/dist/nixxie-cms-core-scripts.esm.js +3 -3
  103. package/src/admin-ui/admin-meta-graphql.ts +168 -168
  104. package/src/admin-ui/components/CommandPalette.tsx +433 -431
  105. package/src/admin-ui/components/Navigation.tsx +389 -385
  106. package/src/admin-ui/components/PageContainer.tsx +311 -310
  107. package/src/admin-ui/components/WelcomeDialog.tsx +1 -1
  108. package/src/admin-ui/context.tsx +338 -338
  109. package/src/admin-ui/templates/app.ts +60 -60
  110. package/src/admin-ui/templates/create-item.ts +5 -5
  111. package/src/admin-ui/templates/home.ts +2 -2
  112. package/src/admin-ui/templates/item.tsx +5 -5
  113. package/src/admin-ui/templates/list.tsx +5 -5
  114. package/src/admin-ui/templates/next-config.ts +29 -0
  115. package/src/admin-ui/templates/no-access.ts +7 -7
  116. package/src/fields/types/bigInt/index.ts +181 -181
  117. package/src/fields/types/bytes/index.ts +275 -275
  118. package/src/fields/types/calendarDay/index.ts +194 -194
  119. package/src/fields/types/checkbox/index.ts +76 -76
  120. package/src/fields/types/decimal/index.ts +182 -182
  121. package/src/fields/types/file/index.ts +168 -168
  122. package/src/fields/types/float/index.ts +133 -133
  123. package/src/fields/types/image/index.ts +244 -244
  124. package/src/fields/types/integer/index.ts +156 -156
  125. package/src/fields/types/json/index.ts +77 -77
  126. package/src/fields/types/multiselect/index.ts +212 -212
  127. package/src/fields/types/password/index.ts +241 -241
  128. package/src/fields/types/relationship/index.ts +381 -381
  129. package/src/fields/types/relationship/views/RelationshipTable.tsx +190 -190
  130. package/src/fields/types/select/index.ts +226 -226
  131. package/src/fields/types/text/index.ts +207 -207
  132. package/src/fields/types/timestamp/index.ts +116 -116
  133. package/src/fields/types/virtual/index.ts +108 -108
  134. package/src/helpers.ts +342 -316
  135. package/src/index.ts +4 -0
  136. package/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/id-field-view.tsx +167 -167
  137. package/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/App/index.tsx +22 -22
  138. package/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/CreateItemPage/index.tsx +71 -71
  139. package/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/HomePage/index.tsx +333 -333
  140. package/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/ItemPage/common.tsx +358 -358
  141. package/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/ItemPage/index.tsx +483 -483
  142. package/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/ListPage/FilterAdd.tsx +221 -221
  143. package/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/ListPage/PaginationControls.tsx +170 -170
  144. package/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/ListPage/Tag.tsx +72 -72
  145. package/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/ListPage/index.tsx +1006 -1006
  146. package/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/NoAccessPage/index.tsx +24 -24
  147. package/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/artifacts.ts +5 -5
  148. package/src/lib/context/createContext.ts +165 -161
  149. package/src/lib/core/initialise-lists.ts +1097 -1097
  150. package/src/lib/id-field.ts +214 -214
  151. package/src/lib/telemetry.ts +342 -342
  152. package/src/schema.ts +237 -233
  153. package/src/scripts/telemetry.ts +1 -1
  154. package/src/types/config/index.ts +400 -333
  155. package/src/types/config/lists.ts +4 -4
  156. package/src/types/context.ts +700 -530
  157. package/src/types/next-fields.ts +499 -499
  158. package/src/types/telemetry.ts +51 -51
  159. package/tests/telemetry.test.ts +361 -361
  160. package/CHANGELOG.md +0 -3158
  161. package/___internal-do-not-use-will-break-in-patch/admin-ui/id-field-view/package.json +0 -4
  162. package/___internal-do-not-use-will-break-in-patch/admin-ui/next-config/package.json +0 -4
  163. package/___internal-do-not-use-will-break-in-patch/admin-ui/pages/App/package.json +0 -4
  164. package/___internal-do-not-use-will-break-in-patch/admin-ui/pages/CreateItemPage/package.json +0 -4
  165. package/___internal-do-not-use-will-break-in-patch/admin-ui/pages/HomePage/package.json +0 -4
  166. package/___internal-do-not-use-will-break-in-patch/admin-ui/pages/ItemPage/package.json +0 -4
  167. package/___internal-do-not-use-will-break-in-patch/admin-ui/pages/ListPage/package.json +0 -4
  168. package/___internal-do-not-use-will-break-in-patch/admin-ui/pages/NoAccessPage/package.json +0 -4
  169. package/___internal-do-not-use-will-break-in-patch/artifacts/package.json +0 -4
  170. package/dist/declarations/src/___internal-do-not-use-will-break-in-patch/admin-ui/id-field-view.d.ts.map +0 -1
  171. package/dist/declarations/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/App/index.d.ts.map +0 -1
  172. package/dist/declarations/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/CreateItemPage/index.d.ts.map +0 -1
  173. package/dist/declarations/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/HomePage/index.d.ts.map +0 -1
  174. package/dist/declarations/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/ItemPage/index.d.ts.map +0 -1
  175. package/dist/declarations/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/ListPage/index.d.ts.map +0 -1
  176. package/dist/declarations/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/NoAccessPage/index.d.ts.map +0 -1
  177. package/dist/declarations/src/___internal-do-not-use-will-break-in-patch/artifacts.d.ts.map +0 -1
  178. /package/dist/{common-1a350e11.cjs.js → common-5933f758.cjs.js} +0 -0
  179. /package/dist/{common-29fc82e6.esm.js → common-ea5c441a.esm.js} +0 -0
  180. /package/dist/declarations/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/id-field-view.d.ts +0 -0
  181. /package/dist/declarations/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/App/index.d.ts +0 -0
  182. /package/dist/declarations/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/CreateItemPage/index.d.ts +0 -0
  183. /package/dist/declarations/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/HomePage/index.d.ts +0 -0
  184. /package/dist/declarations/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/ItemPage/index.d.ts +0 -0
  185. /package/dist/declarations/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/ListPage/index.d.ts +0 -0
  186. /package/dist/declarations/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/NoAccessPage/index.d.ts +0 -0
  187. /package/dist/declarations/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/artifacts.d.ts +0 -0
@@ -1,221 +1,221 @@
1
- import { type FormEvent, Fragment, useId, useMemo, useRef, useState } from 'react'
2
-
3
- import { ActionButton, Button, ButtonGroup } from '@keystar/ui/button'
4
- import { Dialog, DialogTrigger } from '@keystar/ui/dialog'
5
- import { Icon } from '@keystar/ui/icon'
6
- import { chevronDownIcon } from '@keystar/ui/icon/icons/chevronDownIcon'
7
- import { Grid } from '@keystar/ui/layout'
8
- import { Item, Menu, MenuTrigger } from '@keystar/ui/menu'
9
- import { Picker } from '@keystar/ui/picker'
10
- import { Content } from '@keystar/ui/slots'
11
- import { Heading, Text } from '@keystar/ui/typography'
12
-
13
- import type { Filter } from '.'
14
- import { useList } from '../../../../admin-ui/context'
15
- import type { FieldMeta, JSONValue } from '../../../../types'
16
-
17
- type State =
18
- | { kind: 'selecting-field' }
19
- | { kind: 'filter-value'; fieldPath: string; filterType: string; filterValue: JSONValue }
20
-
21
- export function FilterAdd({
22
- listKey,
23
- onAdd,
24
- isDisabled,
25
- }: {
26
- listKey: string
27
- onAdd: (filter: Filter) => void
28
- isDisabled?: boolean
29
- }) {
30
- const triggerRef = useRef<HTMLButtonElement | null>(null)
31
- const [state, setState] = useState<State>({ kind: 'selecting-field' })
32
- const [forceValidation, setForceValidation] = useState(false)
33
- const formId = useId()
34
-
35
- const { fieldsWithFilters, filtersByFieldThenType, list } = useFilterFields(listKey)
36
- const resetState = () => {
37
- setState({ kind: 'selecting-field' })
38
- setForceValidation(false)
39
- // This is a bit of a hack to ensure the trigger button is focused after the
40
- // dialog closes, since we're forking the render
41
- setTimeout(() => {
42
- triggerRef?.current?.focus()
43
- }, 200)
44
- }
45
- const onSubmit = (event: FormEvent) => {
46
- if (event.target !== event.currentTarget) return
47
- event.preventDefault()
48
- setForceValidation(true)
49
-
50
- if (state.kind !== 'filter-value') return
51
-
52
- // TODO: Special "empty" types need to be documented somewhere. Filters that
53
- // have no editable value, basically `null` or `!null`. Which offers:
54
- // * better DX — we can avoid weird nullable types and UIs that don't make sense
55
- // * better UX — users don't have to jump through mental hoops, like "is not exactly" + submit empty field
56
- if (
57
- state.filterType !== 'empty' &&
58
- state.filterType !== 'not_empty' &&
59
- state.filterValue == null
60
- ) {
61
- return
62
- }
63
-
64
- onAdd({
65
- field: state.fieldPath,
66
- type: state.filterType,
67
- value: state.filterValue,
68
- })
69
- resetState()
70
- }
71
-
72
- if (state.kind === 'filter-value') {
73
- const { Filter } = fieldsWithFilters[state.fieldPath].controller.filter
74
- const fieldLabel = list.fields[state.fieldPath].label
75
- const filterTypes = filtersByFieldThenType[state.fieldPath]
76
- const typeLabel = filterTypes[state.filterType]
77
- return (
78
- <DialogTrigger
79
- type="popover"
80
- mobileType="tray"
81
- defaultOpen
82
- onOpenChange={isOpen => !isOpen && resetState()}
83
- >
84
- <ActionButton>
85
- <Text>Filter</Text>
86
- <Icon src={chevronDownIcon} />
87
- </ActionButton>
88
- <Dialog>
89
- <Heading>Filter by {fieldLabel.toLocaleLowerCase()}</Heading>
90
- <Content>
91
- <form onSubmit={onSubmit} id={formId}>
92
- {/*
93
- Workaround for react-aria "bug" where pressing enter in a form field
94
- moves focus to the submit button.
95
- See: https://github.com/adobe/react-spectrum/issues/5940
96
- */}
97
- <button type="submit" form={formId} style={{ display: 'none' }} />
98
- <Grid gap="large" rows="auto minmax(0, 1fr)" height="100%">
99
- <Picker
100
- width="100%"
101
- aria-label="filter type"
102
- isRequired
103
- items={Object.keys(filterTypes).map(filterType => ({
104
- label: filterTypes[filterType],
105
- value: filterType,
106
- }))}
107
- selectedKey={state.filterType}
108
- onSelectionChange={key => {
109
- if (key) {
110
- setState({
111
- kind: 'filter-value',
112
- fieldPath: state.fieldPath,
113
- filterValue:
114
- fieldsWithFilters[state.fieldPath].controller.filter.types[key]
115
- .initialValue,
116
- filterType: key as string,
117
- })
118
- }
119
- }}
120
- >
121
- {item => <Item key={item.value}>{item.label}</Item>}
122
- </Picker>
123
-
124
- {/*
125
- TODO: support validation, default to field controller's validate function?
126
- validate?: (value: JSONValue) => string | undefined
127
- */}
128
- <Filter
129
- autoFocus
130
- context="add"
131
- forceValidation={forceValidation}
132
- typeLabel={typeLabel}
133
- type={state.filterType}
134
- value={state.filterValue}
135
- onChange={value => {
136
- setState(state => ({
137
- ...state,
138
- filterValue: value,
139
- }))
140
- }}
141
- />
142
- </Grid>
143
- </form>
144
- </Content>
145
- <ButtonGroup>
146
- <Button onPress={resetState}>Cancel</Button>
147
- <Button prominence="high" type="submit" form={formId}>
148
- Add
149
- </Button>
150
- </ButtonGroup>
151
- </Dialog>
152
- </DialogTrigger>
153
- )
154
- }
155
-
156
- return (
157
- <Fragment>
158
- <MenuTrigger>
159
- <ActionButton ref={triggerRef} isDisabled={isDisabled}>
160
- <Text>Filter</Text>
161
- <Icon src={chevronDownIcon} />
162
- </ActionButton>
163
- <Menu
164
- items={Object.keys(filtersByFieldThenType).map(fieldPath => ({
165
- label: fieldsWithFilters[fieldPath].label,
166
- value: fieldPath,
167
- }))}
168
- onAction={fieldPath => {
169
- const filterType = Object.keys(filtersByFieldThenType[fieldPath])[0]
170
- setState({
171
- kind: 'filter-value',
172
- fieldPath: fieldPath as string,
173
- filterType,
174
- filterValue:
175
- fieldsWithFilters[fieldPath].controller.filter.types[filterType].initialValue,
176
- })
177
- }}
178
- >
179
- {item => <Item key={item.value}>{item.label}</Item>}
180
- </Menu>
181
- </MenuTrigger>
182
- </Fragment>
183
- )
184
- }
185
-
186
- // TODO: broken if user uses the same filter twice
187
- function useFilterFields(listKey: string) {
188
- const list = useList(listKey)
189
- const fieldsWithFilters = useMemo(() => {
190
- const fieldsWithFilters: Record<
191
- string,
192
- FieldMeta & { controller: { filter: NonNullable<FieldMeta['controller']['filter']> } }
193
- > = {}
194
- for (const fieldPath in list.fields) {
195
- const field = list.fields[fieldPath]
196
- if (field.isFilterable && field.controller.filter) {
197
- fieldsWithFilters[fieldPath] = field as any
198
- }
199
- }
200
- return fieldsWithFilters
201
- }, [list.fields])
202
-
203
- const filtersByFieldThenType = useMemo(() => {
204
- const filtersByFieldThenType: Record<string, Record<string, string>> = {}
205
- for (const fieldPath in fieldsWithFilters) {
206
- const field = fieldsWithFilters[fieldPath]
207
- const filters: Record<string, string> = {}
208
- for (const filterType in field.controller.filter.types) {
209
- filters[filterType] = field.controller.filter.types[filterType].label
210
- }
211
- filtersByFieldThenType[fieldPath] = filters
212
- }
213
- return filtersByFieldThenType
214
- }, [fieldsWithFilters])
215
-
216
- return {
217
- fieldsWithFilters,
218
- filtersByFieldThenType,
219
- list,
220
- }
221
- }
1
+ import { type FormEvent, Fragment, useId, useMemo, useRef, useState } from 'react'
2
+
3
+ import { ActionButton, Button, ButtonGroup } from '@keystar/ui/button'
4
+ import { Dialog, DialogTrigger } from '@keystar/ui/dialog'
5
+ import { Icon } from '@keystar/ui/icon'
6
+ import { chevronDownIcon } from '@keystar/ui/icon/icons/chevronDownIcon'
7
+ import { Grid } from '@keystar/ui/layout'
8
+ import { Item, Menu, MenuTrigger } from '@keystar/ui/menu'
9
+ import { Picker } from '@keystar/ui/picker'
10
+ import { Content } from '@keystar/ui/slots'
11
+ import { Heading, Text } from '@keystar/ui/typography'
12
+
13
+ import type { Filter } from '.'
14
+ import { useList } from '../../../../admin-ui/context'
15
+ import type { FieldMeta, JSONValue } from '../../../../types'
16
+
17
+ type State =
18
+ | { kind: 'selecting-field' }
19
+ | { kind: 'filter-value'; fieldPath: string; filterType: string; filterValue: JSONValue }
20
+
21
+ export function FilterAdd({
22
+ listKey,
23
+ onAdd,
24
+ isDisabled,
25
+ }: {
26
+ listKey: string
27
+ onAdd: (filter: Filter) => void
28
+ isDisabled?: boolean
29
+ }) {
30
+ const triggerRef = useRef<HTMLButtonElement | null>(null)
31
+ const [state, setState] = useState<State>({ kind: 'selecting-field' })
32
+ const [forceValidation, setForceValidation] = useState(false)
33
+ const formId = useId()
34
+
35
+ const { fieldsWithFilters, filtersByFieldThenType, list } = useFilterFields(listKey)
36
+ const resetState = () => {
37
+ setState({ kind: 'selecting-field' })
38
+ setForceValidation(false)
39
+ // This is a bit of a hack to ensure the trigger button is focused after the
40
+ // dialog closes, since we're forking the render
41
+ setTimeout(() => {
42
+ triggerRef?.current?.focus()
43
+ }, 200)
44
+ }
45
+ const onSubmit = (event: FormEvent) => {
46
+ if (event.target !== event.currentTarget) return
47
+ event.preventDefault()
48
+ setForceValidation(true)
49
+
50
+ if (state.kind !== 'filter-value') return
51
+
52
+ // TODO: Special "empty" types need to be documented somewhere. Filters that
53
+ // have no editable value, basically `null` or `!null`. Which offers:
54
+ // * better DX — we can avoid weird nullable types and UIs that don't make sense
55
+ // * better UX — users don't have to jump through mental hoops, like "is not exactly" + submit empty field
56
+ if (
57
+ state.filterType !== 'empty' &&
58
+ state.filterType !== 'not_empty' &&
59
+ state.filterValue == null
60
+ ) {
61
+ return
62
+ }
63
+
64
+ onAdd({
65
+ field: state.fieldPath,
66
+ type: state.filterType,
67
+ value: state.filterValue,
68
+ })
69
+ resetState()
70
+ }
71
+
72
+ if (state.kind === 'filter-value') {
73
+ const { Filter } = fieldsWithFilters[state.fieldPath].controller.filter
74
+ const fieldLabel = list.fields[state.fieldPath].label
75
+ const filterTypes = filtersByFieldThenType[state.fieldPath]
76
+ const typeLabel = filterTypes[state.filterType]
77
+ return (
78
+ <DialogTrigger
79
+ type="popover"
80
+ mobileType="tray"
81
+ defaultOpen
82
+ onOpenChange={isOpen => !isOpen && resetState()}
83
+ >
84
+ <ActionButton>
85
+ <Text>Filter</Text>
86
+ <Icon src={chevronDownIcon} />
87
+ </ActionButton>
88
+ <Dialog>
89
+ <Heading>Filter by {fieldLabel.toLocaleLowerCase()}</Heading>
90
+ <Content>
91
+ <form onSubmit={onSubmit} id={formId}>
92
+ {/*
93
+ Workaround for react-aria "bug" where pressing enter in a form field
94
+ moves focus to the submit button.
95
+ See: https://github.com/adobe/react-spectrum/issues/5940
96
+ */}
97
+ <button type="submit" form={formId} style={{ display: 'none' }} />
98
+ <Grid gap="large" rows="auto minmax(0, 1fr)" height="100%">
99
+ <Picker
100
+ width="100%"
101
+ aria-label="filter type"
102
+ isRequired
103
+ items={Object.keys(filterTypes).map(filterType => ({
104
+ label: filterTypes[filterType],
105
+ value: filterType,
106
+ }))}
107
+ selectedKey={state.filterType}
108
+ onSelectionChange={key => {
109
+ if (key) {
110
+ setState({
111
+ kind: 'filter-value',
112
+ fieldPath: state.fieldPath,
113
+ filterValue:
114
+ fieldsWithFilters[state.fieldPath].controller.filter.types[key]
115
+ .initialValue,
116
+ filterType: key as string,
117
+ })
118
+ }
119
+ }}
120
+ >
121
+ {item => <Item key={item.value}>{item.label}</Item>}
122
+ </Picker>
123
+
124
+ {/*
125
+ TODO: support validation, default to field controller's validate function?
126
+ validate?: (value: JSONValue) => string | undefined
127
+ */}
128
+ <Filter
129
+ autoFocus
130
+ context="add"
131
+ forceValidation={forceValidation}
132
+ typeLabel={typeLabel}
133
+ type={state.filterType}
134
+ value={state.filterValue}
135
+ onChange={value => {
136
+ setState(state => ({
137
+ ...state,
138
+ filterValue: value,
139
+ }))
140
+ }}
141
+ />
142
+ </Grid>
143
+ </form>
144
+ </Content>
145
+ <ButtonGroup>
146
+ <Button onPress={resetState}>Cancel</Button>
147
+ <Button prominence="high" type="submit" form={formId}>
148
+ Add
149
+ </Button>
150
+ </ButtonGroup>
151
+ </Dialog>
152
+ </DialogTrigger>
153
+ )
154
+ }
155
+
156
+ return (
157
+ <Fragment>
158
+ <MenuTrigger>
159
+ <ActionButton ref={triggerRef} isDisabled={isDisabled}>
160
+ <Text>Filter</Text>
161
+ <Icon src={chevronDownIcon} />
162
+ </ActionButton>
163
+ <Menu
164
+ items={Object.keys(filtersByFieldThenType).map(fieldPath => ({
165
+ label: fieldsWithFilters[fieldPath].label,
166
+ value: fieldPath,
167
+ }))}
168
+ onAction={fieldPath => {
169
+ const filterType = Object.keys(filtersByFieldThenType[fieldPath])[0]
170
+ setState({
171
+ kind: 'filter-value',
172
+ fieldPath: fieldPath as string,
173
+ filterType,
174
+ filterValue:
175
+ fieldsWithFilters[fieldPath].controller.filter.types[filterType].initialValue,
176
+ })
177
+ }}
178
+ >
179
+ {item => <Item key={item.value}>{item.label}</Item>}
180
+ </Menu>
181
+ </MenuTrigger>
182
+ </Fragment>
183
+ )
184
+ }
185
+
186
+ // TODO: broken if user uses the same filter twice
187
+ function useFilterFields(listKey: string) {
188
+ const list = useList(listKey)
189
+ const fieldsWithFilters = useMemo(() => {
190
+ const fieldsWithFilters: Record<
191
+ string,
192
+ FieldMeta & { controller: { filter: NonNullable<FieldMeta['controller']['filter']> } }
193
+ > = {}
194
+ for (const fieldPath in list.fields) {
195
+ const field = list.fields[fieldPath]
196
+ if (field.isFilterable && field.controller.filter) {
197
+ fieldsWithFilters[fieldPath] = field as any
198
+ }
199
+ }
200
+ return fieldsWithFilters
201
+ }, [list.fields])
202
+
203
+ const filtersByFieldThenType = useMemo(() => {
204
+ const filtersByFieldThenType: Record<string, Record<string, string>> = {}
205
+ for (const fieldPath in fieldsWithFilters) {
206
+ const field = fieldsWithFilters[fieldPath]
207
+ const filters: Record<string, string> = {}
208
+ for (const filterType in field.controller.filter.types) {
209
+ filters[filterType] = field.controller.filter.types[filterType].label
210
+ }
211
+ filtersByFieldThenType[fieldPath] = filters
212
+ }
213
+ return filtersByFieldThenType
214
+ }, [fieldsWithFilters])
215
+
216
+ return {
217
+ fieldsWithFilters,
218
+ filtersByFieldThenType,
219
+ list,
220
+ }
221
+ }