@nixxie-cms/core 2.0.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/admin-ui/components/dist/nixxie-cms-core-admin-ui-components.cjs.js +7 -7
  2. package/admin-ui/components/dist/nixxie-cms-core-admin-ui-components.esm.js +7 -7
  3. package/admin-ui/context/dist/nixxie-cms-core-admin-ui-context.cjs.js +2 -2
  4. package/admin-ui/context/dist/nixxie-cms-core-admin-ui-context.esm.js +2 -2
  5. package/admin-ui/utils/dist/nixxie-cms-core-admin-ui-utils.cjs.js +5 -5
  6. package/admin-ui/utils/dist/nixxie-cms-core-admin-ui-utils.esm.js +3 -3
  7. package/dist/{CreateItemDialog-7008b050.esm.js → CreateItemDialog-66621fe8.esm.js} +3 -3
  8. package/dist/{CreateItemDialog-a0cab315.cjs.js → CreateItemDialog-96b044ce.cjs.js} +4 -4
  9. package/dist/{Field-47f85161.esm.js → Field-1820c4e6.esm.js} +1 -0
  10. package/dist/{Field-ed8d7627.cjs.js → Field-38d3cdf9.cjs.js} +1 -0
  11. package/dist/GraphQLErrorNotice-7594a9f8.esm.js +64 -0
  12. package/dist/GraphQLErrorNotice-c8890f80.cjs.js +66 -0
  13. package/dist/{PageContainer-5ae731cc.esm.js → PageContainer-355cfbfa.esm.js} +362 -156
  14. package/dist/{PageContainer-abd7159f.cjs.js → PageContainer-4095555a.cjs.js} +361 -155
  15. package/dist/{context-af9957ed.esm.js → context-2924eaaa.esm.js} +53 -58
  16. package/dist/{context-b5204629.cjs.js → context-2ce61d0b.cjs.js} +53 -58
  17. package/dist/declarations/src/admin-ui/components/GraphQLErrorNotice.d.ts.map +1 -1
  18. package/dist/declarations/src/admin-ui/components/Navigation.d.ts.map +1 -1
  19. package/dist/declarations/src/admin-ui/components/PageContainer.d.ts.map +1 -1
  20. package/dist/declarations/src/admin-ui/context.d.ts.map +1 -1
  21. package/dist/declarations/src/admin-ui/utils/useCreateItem.d.ts.map +1 -1
  22. package/dist/declarations/src/fields/types/bigInt/views/index.d.ts +2 -1
  23. package/dist/declarations/src/fields/types/bigInt/views/index.d.ts.map +1 -1
  24. package/dist/declarations/src/fields/types/bytes/views/index.d.ts +2 -1
  25. package/dist/declarations/src/fields/types/bytes/views/index.d.ts.map +1 -1
  26. package/dist/declarations/src/fields/types/calendarDay/views/index.d.ts.map +1 -1
  27. package/dist/declarations/src/fields/types/decimal/views/index.d.ts +2 -1
  28. package/dist/declarations/src/fields/types/decimal/views/index.d.ts.map +1 -1
  29. package/dist/declarations/src/fields/types/float/views/index.d.ts +2 -1
  30. package/dist/declarations/src/fields/types/float/views/index.d.ts.map +1 -1
  31. package/dist/declarations/src/fields/types/integer/views/index.d.ts +2 -1
  32. package/dist/declarations/src/fields/types/integer/views/index.d.ts.map +1 -1
  33. package/dist/declarations/src/fields/types/json/views/index.d.ts.map +1 -1
  34. package/dist/declarations/src/fields/types/multiselect/views/index.d.ts.map +1 -1
  35. package/dist/declarations/src/fields/types/relationship/views/index.d.ts.map +1 -1
  36. package/dist/declarations/src/fields/types/select/views/index.d.ts.map +1 -1
  37. package/dist/declarations/src/fields/types/text/views/index.d.ts +2 -1
  38. package/dist/declarations/src/fields/types/text/views/index.d.ts.map +1 -1
  39. package/dist/declarations/src/fields/types/timestamp/views/index.d.ts.map +1 -1
  40. package/dist/declarations/src/fields/types/virtual/views/index.d.ts.map +1 -1
  41. package/dist/declarations/src/internal-unstable/admin-ui/pages/HomePage/index.d.ts.map +1 -1
  42. package/dist/declarations/src/internal-unstable/admin-ui/pages/ItemPage/index.d.ts.map +1 -1
  43. package/dist/pick-4c785a54.esm.js +34 -0
  44. package/dist/pick-906341bb.cjs.js +37 -0
  45. package/dist/{useCreateItem-1f94d252.esm.js → useCreateItem-36a75f1c.esm.js} +26 -26
  46. package/dist/{useCreateItem-1be4987e.cjs.js → useCreateItem-acf06f77.cjs.js} +37 -37
  47. package/dist/{useFilter-acc9d413.cjs.js → useFilter-c29f17a8.cjs.js} +1 -1
  48. package/dist/{useFilter-9b6db1f9.esm.js → useFilter-f79b2abb.esm.js} +1 -1
  49. package/dist/{Fields-956d9a14.esm.js → usePreventNavigation-093389dd.esm.js} +28 -2
  50. package/dist/{Fields-e2c28056.cjs.js → usePreventNavigation-d4f9f4fa.cjs.js} +27 -0
  51. package/fields/types/bigInt/views/dist/nixxie-cms-core-fields-types-bigInt-views.cjs.js +8 -0
  52. package/fields/types/bigInt/views/dist/nixxie-cms-core-fields-types-bigInt-views.esm.js +8 -1
  53. package/fields/types/bytes/views/dist/nixxie-cms-core-fields-types-bytes-views.cjs.js +14 -3
  54. package/fields/types/bytes/views/dist/nixxie-cms-core-fields-types-bytes-views.esm.js +15 -5
  55. package/fields/types/calendarDay/views/dist/nixxie-cms-core-fields-types-calendarDay-views.cjs.js +2 -1
  56. package/fields/types/calendarDay/views/dist/nixxie-cms-core-fields-types-calendarDay-views.esm.js +2 -1
  57. package/fields/types/decimal/views/dist/nixxie-cms-core-fields-types-decimal-views.cjs.js +10 -1
  58. package/fields/types/decimal/views/dist/nixxie-cms-core-fields-types-decimal-views.esm.js +10 -2
  59. package/fields/types/file/views/dist/nixxie-cms-core-fields-types-file-views.cjs.js +1 -1
  60. package/fields/types/file/views/dist/nixxie-cms-core-fields-types-file-views.esm.js +2 -2
  61. package/fields/types/float/views/dist/nixxie-cms-core-fields-types-float-views.cjs.js +10 -1
  62. package/fields/types/float/views/dist/nixxie-cms-core-fields-types-float-views.esm.js +10 -2
  63. package/fields/types/image/views/dist/nixxie-cms-core-fields-types-image-views.cjs.js +2 -1
  64. package/fields/types/image/views/dist/nixxie-cms-core-fields-types-image-views.esm.js +2 -1
  65. package/fields/types/integer/views/dist/nixxie-cms-core-fields-types-integer-views.cjs.js +8 -0
  66. package/fields/types/integer/views/dist/nixxie-cms-core-fields-types-integer-views.esm.js +8 -1
  67. package/fields/types/json/views/dist/nixxie-cms-core-fields-types-json-views.cjs.js +19 -4
  68. package/fields/types/json/views/dist/nixxie-cms-core-fields-types-json-views.esm.js +19 -4
  69. package/fields/types/multiselect/views/dist/nixxie-cms-core-fields-types-multiselect-views.cjs.js +18 -3
  70. package/fields/types/multiselect/views/dist/nixxie-cms-core-fields-types-multiselect-views.esm.js +18 -3
  71. package/fields/types/password/views/dist/nixxie-cms-core-fields-types-password-views.cjs.js +1 -1
  72. package/fields/types/password/views/dist/nixxie-cms-core-fields-types-password-views.esm.js +1 -1
  73. package/fields/types/relationship/views/dist/nixxie-cms-core-fields-types-relationship-views.cjs.js +9 -7
  74. package/fields/types/relationship/views/dist/nixxie-cms-core-fields-types-relationship-views.esm.js +9 -7
  75. package/fields/types/select/views/dist/nixxie-cms-core-fields-types-select-views.cjs.js +3 -2
  76. package/fields/types/select/views/dist/nixxie-cms-core-fields-types-select-views.esm.js +3 -2
  77. package/fields/types/text/views/dist/nixxie-cms-core-fields-types-text-views.cjs.js +14 -3
  78. package/fields/types/text/views/dist/nixxie-cms-core-fields-types-text-views.esm.js +15 -5
  79. package/fields/types/timestamp/views/dist/nixxie-cms-core-fields-types-timestamp-views.cjs.js +2 -1
  80. package/fields/types/timestamp/views/dist/nixxie-cms-core-fields-types-timestamp-views.esm.js +2 -1
  81. package/fields/types/virtual/views/dist/nixxie-cms-core-fields-types-virtual-views.cjs.js +13 -1
  82. package/fields/types/virtual/views/dist/nixxie-cms-core-fields-types-virtual-views.esm.js +14 -2
  83. package/internal-unstable/admin-ui/pages/App/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-App.cjs.js +3 -3
  84. package/internal-unstable/admin-ui/pages/App/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-App.esm.js +3 -3
  85. package/internal-unstable/admin-ui/pages/CreateItemPage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-CreateItemPage.cjs.js +7 -7
  86. package/internal-unstable/admin-ui/pages/CreateItemPage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-CreateItemPage.esm.js +6 -6
  87. package/internal-unstable/admin-ui/pages/HomePage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-HomePage.cjs.js +34 -33
  88. package/internal-unstable/admin-ui/pages/HomePage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-HomePage.esm.js +35 -34
  89. package/internal-unstable/admin-ui/pages/ItemPage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-ItemPage.cjs.js +53 -13
  90. package/internal-unstable/admin-ui/pages/ItemPage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-ItemPage.esm.js +52 -12
  91. package/internal-unstable/admin-ui/pages/ListPage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-ListPage.cjs.js +36 -25
  92. package/internal-unstable/admin-ui/pages/ListPage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-ListPage.esm.js +36 -25
  93. package/package.json +2 -2
  94. package/src/admin-ui/components/CommandPalette.tsx +134 -27
  95. package/src/admin-ui/components/CreateButtonLink.tsx +20 -46
  96. package/src/admin-ui/components/GraphQLErrorNotice.tsx +39 -33
  97. package/src/admin-ui/components/Logo.tsx +5 -5
  98. package/src/admin-ui/components/Navigation.tsx +41 -27
  99. package/src/admin-ui/components/PageContainer.tsx +171 -15
  100. package/src/admin-ui/components/WelcomeDialog.tsx +14 -14
  101. package/src/admin-ui/context.tsx +5 -2
  102. package/src/admin-ui/utils/useCreateItem.ts +21 -1
  103. package/src/fields/types/bigInt/views/index.tsx +10 -1
  104. package/src/fields/types/bytes/views/index.tsx +14 -1
  105. package/src/fields/types/calendarDay/views/index.tsx +2 -1
  106. package/src/fields/types/decimal/views/index.tsx +7 -1
  107. package/src/fields/types/file/views/Field.tsx +1 -1
  108. package/src/fields/types/float/views/index.tsx +7 -1
  109. package/src/fields/types/image/views/index.tsx +1 -1
  110. package/src/fields/types/integer/views/index.tsx +5 -0
  111. package/src/fields/types/json/views/index.tsx +20 -2
  112. package/src/fields/types/multiselect/views/index.tsx +7 -3
  113. package/src/fields/types/password/views/index.tsx +1 -1
  114. package/src/fields/types/relationship/views/index.tsx +1 -0
  115. package/src/fields/types/select/views/index.tsx +2 -1
  116. package/src/fields/types/text/views/index.tsx +14 -1
  117. package/src/fields/types/timestamp/views/__tests__/index.tsx +68 -68
  118. package/src/fields/types/timestamp/views/index.tsx +2 -1
  119. package/src/fields/types/virtual/views/index.tsx +17 -2
  120. package/src/internal-unstable/admin-ui/pages/HomePage/index.tsx +40 -31
  121. package/src/internal-unstable/admin-ui/pages/ItemPage/index.tsx +36 -3
  122. package/src/internal-unstable/admin-ui/pages/ListPage/PaginationControls.tsx +20 -33
  123. package/src/internal-unstable/admin-ui/pages/ListPage/index.tsx +24 -16
  124. package/tests/conditional-filters.test.ts +333 -326
  125. package/dist/GraphQLErrorNotice-cd74180d.cjs.js +0 -57
  126. package/dist/GraphQLErrorNotice-d9f0931b.esm.js +0 -55
  127. package/dist/pick-5fe45878.cjs.js +0 -71
  128. package/dist/pick-b7ef3115.esm.js +0 -68
@@ -38,6 +38,7 @@ import {
38
38
  useInvalidFields,
39
39
  } from '../../../../admin-ui/utils'
40
40
  import { pick } from '../../../../admin-ui/utils/pick'
41
+ import { usePreventNavigation } from '../../../../admin-ui/utils/usePreventNavigation'
41
42
  import type {
42
43
  ActionMeta,
43
44
  BaseCollectionTypeInfo,
@@ -62,6 +63,22 @@ function useEventCallback<Func extends (...args: any[]) => unknown>(callback: Fu
62
63
  return cb as any
63
64
  }
64
65
 
66
+ // after forcing validation, move focus to the first invalid field so the user
67
+ // can see and fix it. runs on the next frame so the DOM reflects the new
68
+ // aria-invalid state, and is fully defensive so it can never throw.
69
+ function focusFirstInvalidField() {
70
+ requestAnimationFrame(() => {
71
+ try {
72
+ const el = document.querySelector<HTMLElement>('[aria-invalid="true"]')
73
+ if (!el) return
74
+ el.focus({ preventScroll: false })
75
+ el.scrollIntoView({ block: 'center', behavior: 'smooth' })
76
+ } catch {
77
+ // ignore — focusing is a best-effort enhancement
78
+ }
79
+ })
80
+ }
81
+
65
82
  function DeleteButton({
66
83
  list,
67
84
  itemId,
@@ -155,7 +172,6 @@ function ResetButton(props: { onReset: () => void; hasChanges?: boolean }) {
155
172
  title="Reset changes"
156
173
  cancelLabel="Cancel"
157
174
  primaryActionLabel="Yes, reset"
158
- autoFocusButton="primary"
159
175
  onPrimaryAction={props.onReset}
160
176
  >
161
177
  Are you sure? Any unsaved changes will be lost and cannot be recovered.
@@ -211,7 +227,11 @@ function ItemForm({
211
227
  e.preventDefault()
212
228
  const newForceValidation = invalidFields.size !== 0
213
229
  setForceValidation(newForceValidation)
214
- if (newForceValidation) return
230
+ if (newForceValidation) {
231
+ toastQueue.critical('Please fix the highlighted field(s) before saving.')
232
+ focusFirstInvalidField()
233
+ return
234
+ }
215
235
 
216
236
  const { error: _error } = await update({
217
237
  variables: {
@@ -236,10 +256,18 @@ function ItemForm({
236
256
  timeout: 5000,
237
257
  })
238
258
 
259
+ // reset the navigation guard before refetch so saving and then navigating
260
+ // away doesn't prompt for unsaved changes
261
+ shouldPreventNavigationRef.current = false
239
262
  onSaveSuccess()
240
263
  })
241
264
 
242
265
  const hasChangedFields = useHasChanges('update', list.fields, value, initialValue)
266
+ const shouldPreventNavigationRef = useRef(hasChangedFields)
267
+ useEffect(() => {
268
+ shouldPreventNavigationRef.current = hasChangedFields
269
+ }, [hasChangedFields])
270
+ usePreventNavigation(shouldPreventNavigationRef)
243
271
 
244
272
  return (
245
273
  <Fragment>
@@ -324,7 +352,12 @@ function ItemPage({ listKey }: ItemPageProps) {
324
352
  const itemLabel_ = item?.[list.labelField] ?? item?.id
325
353
  const itemLabel = typeof itemLabel_ === 'string' ? itemLabel_ : (itemId ?? '')
326
354
 
327
- const pageLoading = loading || itemId === undefined
355
+ // Only show the full-page spinner on the INITIAL load (no data yet). On a
356
+ // post-save refetch Apollo keeps `data` populated while `loading` flips back
357
+ // to true; gating on `!data` keeps the form mounted (no scroll reset / flash)
358
+ // during background refetches while still showing the spinner before the
359
+ // first result arrives.
360
+ const pageLoading = (loading && !data) || itemId === undefined
328
361
  const pageLabel = itemLabel || itemId
329
362
  const pageTitle = list.isSingleton || typeof pageLabel !== 'string' ? list.label : pageLabel
330
363
  const initialValue = useMemo(() => {
@@ -4,17 +4,11 @@ import { chevronLeftIcon } from '@keystar/ui/icon/icons/chevronLeftIcon'
4
4
  import { chevronRightIcon } from '@keystar/ui/icon/icons/chevronRightIcon'
5
5
  import { undo2Icon } from '@keystar/ui/icon/icons/undo2Icon'
6
6
  import { HStack } from '@keystar/ui/layout'
7
- import { Picker } from '@keystar/ui/picker'
8
- import { Item } from '@keystar/ui/tag'
7
+ import { NumberField } from '@keystar/ui/number-field'
8
+ import { Item, Picker } from '@keystar/ui/picker'
9
9
  import { Tooltip, TooltipTrigger } from '@keystar/ui/tooltip'
10
10
  import { Text } from '@keystar/ui/typography'
11
11
  import type { ReactNode } from 'react'
12
- import { useMemo } from 'react'
13
-
14
- type PageItem = {
15
- label: string
16
- id: number
17
- }
18
12
 
19
13
  export function PaginationControls(props: {
20
14
  singular: string
@@ -34,16 +28,11 @@ export function PaginationControls(props: {
34
28
  const prevPage = currentPage - 1
35
29
  const lastPage = Math.max(Math.ceil(total / pageSize), 1)
36
30
 
37
- const pageItems = useMemo(() => {
38
- const result: PageItem[] = []
39
- for (let page = 1; page <= lastPage; page++) {
40
- result.push({
41
- id: page,
42
- label: String(page),
43
- })
44
- }
45
- return result
46
- }, [lastPage])
31
+ const goToPage = (value: number) => {
32
+ if (!Number.isFinite(value)) return
33
+ const page = Math.min(Math.max(Math.round(value), 1), lastPage)
34
+ if (page !== currentPage) props.onChangePage(page)
35
+ }
47
36
 
48
37
  return (
49
38
  <HStack
@@ -90,24 +79,22 @@ export function PaginationControls(props: {
90
79
 
91
80
  {/*
92
81
  right-side
93
- mobile: next/prev
94
- desktop^: current page (picker), next/prev
82
+ mobile: page jump, next/prev
83
+ desktop^: page jump, next/prev
95
84
  */}
96
85
  <HStack gap="large" alignItems="center">
97
- <HStack isHidden={{ below: 'desktop' }} gap="regular" alignItems="center">
98
- <Picker
99
- // prominence="low"
100
- aria-label={`Page number, of ${lastPage} pages`}
101
- items={pageItems}
102
- onSelectionChange={page => {
103
- props.onChangePage(Number(page))
104
- }}
105
- selectedKey={currentPage}
86
+ <HStack gap="regular" alignItems="center">
87
+ <NumberField
88
+ aria-label="Go to page"
89
+ hideStepper
90
+ minValue={1}
91
+ maxValue={lastPage}
92
+ step={1}
93
+ value={currentPage}
94
+ onChange={goToPage}
106
95
  width="scale.1000"
107
- >
108
- {item => <Item>{item.label}</Item>}
109
- </Picker>
110
- <Text>of {lastPage} pages</Text>
96
+ />
97
+ <Text>of {lastPage}</Text>
111
98
  </HStack>
112
99
  <HStack gap="regular">
113
100
  <ActionButton
@@ -897,21 +897,29 @@ function ActionItemsDialog({
897
897
  id
898
898
  }
899
899
  }`
900
+ // The ids actually included in the mutation payload, in the exact order
901
+ // they are sent to the server. Server error paths reference indices into
902
+ // this array, so error/affected-record attribution must be done against it
903
+ // rather than the original (possibly larger) itemIds list.
904
+ const sentIds: string[] = []
905
+ const mutationData =
906
+ action.key === 'delete'
907
+ ? itemIds.map(id => {
908
+ sentIds.push(id)
909
+ return { id }
910
+ })
911
+ : itemIds.flatMap(id => {
912
+ const row = items.find(item => String(item.id) === id)
913
+ if (!row) {
914
+ return []
915
+ }
916
+ const deserialized = deserializeItemToValue(list.fields, row)
917
+ const args = getActionArguments(list, action, deserialized)
918
+ sentIds.push(id)
919
+ return { where: { id }, ...args }
920
+ })
900
921
  const [actionOnItems] = useMutation<{ results?: ({ id: string } | null)[] }>(actionMutation, {
901
- variables:
902
- action.key === 'delete'
903
- ? { where: itemIds.map(id => ({ id })) }
904
- : {
905
- data: itemIds.flatMap(id => {
906
- const row = items.find(item => String(item.id) === id)
907
- if (!row) {
908
- return []
909
- }
910
- const deserialized = deserializeItemToValue(list.fields, row)
911
- const args = getActionArguments(list, action, deserialized)
912
- return { where: { id }, ...args }
913
- }),
914
- },
922
+ variables: action.key === 'delete' ? { where: mutationData } : { data: mutationData },
915
923
  errorPolicy: 'all',
916
924
  })
917
925
  const { messages: m } = action
@@ -927,14 +935,14 @@ function ActionItemsDialog({
927
935
  for (const err of error.errors ?? []) {
928
936
  const i = err.path?.[1]
929
937
  if (typeof i !== 'number') continue
930
- const itemId = itemIds[i]
938
+ const itemId = sentIds[i]
931
939
 
932
940
  failed.add(itemId)
933
941
  actionErrors[itemId] ??= []
934
942
  actionErrors[itemId].push(err)
935
943
  }
936
944
  }
937
- const countSuccess = itemIds.length - countFail
945
+ const countSuccess = sentIds.length - countFail
938
946
 
939
947
  if (countSuccess) {
940
948
  toastQueue.neutral(