@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,483 +1,483 @@
1
- import router, { useRouter } from 'next/router'
2
- import {
3
- type FormEvent,
4
- Fragment,
5
- type PropsWithChildren,
6
- useCallback,
7
- useEffect,
8
- useMemo,
9
- useRef,
10
- useState,
11
- } from 'react'
12
-
13
- import { Button } from '@keystar/ui/button'
14
- import { AlertDialog, DialogContainer, DialogTrigger } from '@keystar/ui/dialog'
15
- import { Icon } from '@keystar/ui/icon'
16
- import { fileWarningIcon } from '@keystar/ui/icon/icons/fileWarningIcon'
17
- import { Box, VStack } from '@keystar/ui/layout'
18
- import { ProgressCircle } from '@keystar/ui/progress'
19
- import { SlotProvider } from '@keystar/ui/slots'
20
- import { toastQueue } from '@keystar/ui/toast'
21
- import { Heading, Text } from '@keystar/ui/typography'
22
-
23
- import { CombinedGraphQLErrors, gql, useMutation } from '../../../../admin-ui/apollo'
24
- import { CreateButtonLink } from '../../../../admin-ui/components/CreateButtonLink'
25
- import { ErrorDetailsDialog } from '../../../../admin-ui/components/Errors'
26
- import { GraphQLErrorNotice } from '../../../../admin-ui/components/GraphQLErrorNotice'
27
- import { PageContainer } from '../../../../admin-ui/components/PageContainer'
28
- import { useList, useListItem } from '../../../../admin-ui/context'
29
- import {
30
- deserializeItemToValue,
31
- Fields,
32
- getConditionalFilterFieldKeys,
33
- isActionAvailable,
34
- resolveActionMode,
35
- serializeItemForConditionalFilters,
36
- serializeValueToOperationItem,
37
- useHasChanges,
38
- useInvalidFields,
39
- } from '../../../../admin-ui/utils'
40
- import { pick } from '../../../../admin-ui/utils/pick'
41
- import type {
42
- ActionMeta,
43
- BaseListTypeInfo,
44
- ConditionalFilter,
45
- ConditionalFilterCase,
46
- ListMeta,
47
- } from '../../../../types'
48
- import { BaseToolbar, ColumnLayout, ItemPageHeader, StickySidebar } from './common'
49
-
50
- type ItemPageProps = {
51
- listKey: string
52
- }
53
-
54
- function useEventCallback<Func extends (...args: any[]) => unknown>(callback: Func): Func {
55
- const callbackRef = useRef(callback)
56
- const cb = useCallback((...args: any[]) => {
57
- return callbackRef.current(...args)
58
- }, [])
59
- useEffect(() => {
60
- callbackRef.current = callback
61
- })
62
- return cb as any
63
- }
64
-
65
- function DeleteButton({
66
- list,
67
- itemId,
68
- itemLabel,
69
- }: {
70
- list: ListMeta
71
- itemId: string
72
- itemLabel: string
73
- }) {
74
- const [errorDialogValue, setErrorDialogValue] = useState<Error | null>(null)
75
- const router = useRouter()
76
- const [deleteItem] = useMutation(
77
- gql`mutation ($id: ID!) {
78
- ${list.graphql.names.deleteMutationName}(where: { id: $id }) {
79
- id
80
- }
81
- }`,
82
- { variables: { id: itemId } }
83
- )
84
-
85
- return (
86
- <Fragment>
87
- <DialogTrigger>
88
- <Button tone="critical">Delete</Button>
89
- <AlertDialog
90
- tone="critical"
91
- title="Delete item"
92
- cancelLabel="Cancel"
93
- primaryActionLabel="Yes, delete"
94
- onPrimaryAction={async () => {
95
- try {
96
- await deleteItem()
97
- } catch (err: any) {
98
- toastQueue.critical('Unable to delete item', {
99
- actionLabel: 'Details',
100
- onAction: () => setErrorDialogValue(err),
101
- shouldCloseOnAction: true,
102
- })
103
- return
104
- }
105
-
106
- toastQueue.neutral(`${list.singular} deleted.`, {
107
- timeout: 5000,
108
- })
109
- router.push(list.isSingleton ? '/' : `/${list.path}`)
110
- }}
111
- >
112
- <Text>
113
- Are you sure you want to delete <strong style={{ fontWeight: 600 }}>{itemLabel}</strong>
114
- ? This action cannot be undone.
115
- </Text>
116
- </AlertDialog>
117
- </DialogTrigger>
118
-
119
- <DialogContainer onDismiss={() => setErrorDialogValue(null)} isDismissable>
120
- {errorDialogValue && (
121
- <ErrorDetailsDialog title="Unable to delete item" error={errorDialogValue} />
122
- )}
123
- </DialogContainer>
124
- </Fragment>
125
- )
126
- }
127
-
128
- function ItemNotFound(props: PropsWithChildren) {
129
- return (
130
- <VStack
131
- alignItems="center"
132
- backgroundColor="surface"
133
- borderRadius="medium"
134
- gap="large"
135
- justifyContent="center"
136
- minHeight="scale.3000"
137
- padding="xlarge"
138
- >
139
- <Icon src={fileWarningIcon} color="neutralEmphasis" size="large" />
140
- <Heading align="center">Not found</Heading>
141
- <SlotProvider slots={{ text: { align: 'center', maxWidth: 'scale.5000' } }}>
142
- {props.children}
143
- </SlotProvider>
144
- </VStack>
145
- )
146
- }
147
-
148
- function ResetButton(props: { onReset: () => void; hasChanges?: boolean }) {
149
- return (
150
- <DialogTrigger>
151
- <Button tone="accent" isDisabled={!props.hasChanges}>
152
- Reset
153
- </Button>
154
- <AlertDialog
155
- title="Reset changes"
156
- cancelLabel="Cancel"
157
- primaryActionLabel="Yes, reset"
158
- autoFocusButton="primary"
159
- onPrimaryAction={props.onReset}
160
- >
161
- Are you sure? Any unsaved changes will be lost and cannot be recovered.
162
- </AlertDialog>
163
- </DialogTrigger>
164
- )
165
- }
166
-
167
- function ItemForm({
168
- listKey,
169
- initialValue,
170
- value,
171
- onChange,
172
- itemLabel,
173
- onSaveSuccess,
174
- fieldModes,
175
- fieldPositions,
176
- isRequireds,
177
- }: {
178
- listKey: string
179
- initialValue: Record<string, unknown>
180
- value: Record<string, unknown>
181
- onChange: (value: Record<string, unknown>) => void
182
- itemLabel: string
183
- onSaveSuccess: () => void
184
- fieldModes: Record<
185
- string,
186
- ConditionalFilter<'edit' | 'read' | 'hidden', 'read' | 'hidden', BaseListTypeInfo>
187
- >
188
- isRequireds: Record<string, ConditionalFilterCase<BaseListTypeInfo>>
189
- fieldPositions: Record<string, 'form' | 'sidebar'>
190
- }) {
191
- const list = useList(listKey)
192
- const itemId = initialValue.id as string
193
- const [updateError, setUpdateError] = useState<Error | null>(null)
194
- const [update, { loading, error }] = useMutation(
195
- gql`mutation ($id: ID!, $data: ${list.graphql.names.updateInputName}!) {
196
- item: ${list.graphql.names.updateMutationName}(where: { id: $id }, data: $data) {
197
- id
198
- }
199
- }`,
200
- { errorPolicy: 'all' }
201
- )
202
-
203
- function resetValueState() {
204
- onChange(initialValue)
205
- }
206
-
207
- const invalidFields = useInvalidFields(list.fields, value, isRequireds)
208
- const [forceValidation, setForceValidation] = useState(false)
209
- const onSave = useEventCallback(async (e: FormEvent<HTMLFormElement>) => {
210
- if (e.target !== e.currentTarget) return
211
- e.preventDefault()
212
- const newForceValidation = invalidFields.size !== 0
213
- setForceValidation(newForceValidation)
214
- if (newForceValidation) return
215
-
216
- const { error: _error } = await update({
217
- variables: {
218
- id: itemId,
219
- data: serializeValueToOperationItem('update', list.fields, value, initialValue),
220
- },
221
- })
222
-
223
- const error = CombinedGraphQLErrors.is(_error)
224
- ? _error.errors.find(x => x.path === undefined || x.path?.length === 1)
225
- : _error
226
- if (error) {
227
- toastQueue.critical('Unable to save item', {
228
- actionLabel: 'Details',
229
- onAction: () => setUpdateError(new Error(error.message)),
230
- shouldCloseOnAction: true,
231
- })
232
- return
233
- }
234
-
235
- toastQueue.positive(`Saved changes to ${list.singular.toLocaleLowerCase()}.`, {
236
- timeout: 5000,
237
- })
238
-
239
- onSaveSuccess()
240
- })
241
-
242
- const hasChangedFields = useHasChanges('update', list.fields, value, initialValue)
243
-
244
- return (
245
- <Fragment>
246
- <form onSubmit={onSave} style={{ display: 'contents' }}>
247
- {/*
248
- Workaround for react-aria "bug" where pressing enter in a form field
249
- moves focus to the submit button.
250
- See: https://github.com/adobe/react-spectrum/issues/5940
251
- */}
252
- <button type="submit" style={{ display: 'none' }} />
253
- <VStack gap="large" gridArea="main" marginTop="xlarge" minWidth={0}>
254
- <GraphQLErrorNotice
255
- errors={
256
- CombinedGraphQLErrors.is(error)
257
- ? error.errors.filter(x => x.path === undefined || x.path?.length === 1)
258
- : [error]
259
- }
260
- />
261
- <Fields
262
- view="itemView"
263
- position="form"
264
- fields={list.fields}
265
- groups={list.groups}
266
- forceValidation={forceValidation}
267
- invalidFields={invalidFields}
268
- fieldModes={fieldModes}
269
- fieldPositions={fieldPositions}
270
- onChange={onChange}
271
- value={value}
272
- isRequireds={isRequireds}
273
- />
274
- </VStack>
275
-
276
- <StickySidebar>
277
- <Fields
278
- view="itemView"
279
- position="sidebar"
280
- fields={list.fields}
281
- groups={list.groups}
282
- forceValidation={forceValidation}
283
- invalidFields={invalidFields}
284
- onChange={onChange}
285
- value={value}
286
- fieldModes={fieldModes}
287
- fieldPositions={fieldPositions}
288
- isRequireds={isRequireds}
289
- />
290
- </StickySidebar>
291
-
292
- <BaseToolbar>
293
- <Button
294
- isDisabled={!hasChangedFields}
295
- isPending={loading}
296
- prominence="high"
297
- type="submit"
298
- >
299
- Save
300
- </Button>
301
- <ResetButton hasChanges={hasChangedFields} onReset={resetValueState} />
302
- <Box flex />
303
- {!list.hideDelete ? (
304
- <DeleteButton list={list} itemId={itemId} itemLabel={itemLabel} />
305
- ) : null}
306
- </BaseToolbar>
307
- </form>
308
-
309
- <DialogContainer onDismiss={() => setUpdateError(null)} isDismissable>
310
- {updateError && <ErrorDetailsDialog title="Unable to save item" error={updateError} />}
311
- </DialogContainer>
312
- </Fragment>
313
- )
314
- }
315
-
316
- export const getItemPage = (props: ItemPageProps) => () => <ItemPage {...props} />
317
-
318
- function ItemPage({ listKey }: ItemPageProps) {
319
- const list = useList(listKey)
320
- const id_ = useRouter().query.id
321
- const [itemId] = Array.isArray(id_) ? id_ : [id_]
322
- const { data, error, loading, refetch } = useListItem(listKey, itemId ?? null)
323
- const item = data?.item
324
- const itemLabel_ = item?.[list.labelField] ?? item?.id
325
- const itemLabel = typeof itemLabel_ === 'string' ? itemLabel_ : (itemId ?? '')
326
-
327
- const pageLoading = loading || itemId === undefined
328
- const pageLabel = itemLabel || itemId
329
- const pageTitle = list.isSingleton || typeof pageLabel !== 'string' ? list.label : pageLabel
330
- const initialValue = useMemo(() => {
331
- if (!item) return null
332
- return deserializeItemToValue(list.fields, item)
333
- }, [list.fields, data?.item])
334
- const [value, setValue] = useState<Record<string, unknown> | null>(null)
335
- useEffect(() => {
336
- setValue(initialValue)
337
- }, [initialValue])
338
-
339
- const { actionModes, fieldModes, fieldPositions, isRequireds } = useMemo(() => {
340
- type ActionMode = (typeof list.actions)[number]['itemView']['actionMode']
341
- type ListField = (typeof list.fields)[keyof typeof list.fields]
342
- type FieldMode = ListField['itemView']['fieldMode']
343
- type FieldPosition = ListField['itemView']['fieldPosition']
344
- type IsRequired = ListField['itemView']['isRequired']
345
-
346
- const actionModes: Record<string, ActionMode> = {}
347
- for (const action of list.actions) {
348
- actionModes[action.key] = action.itemView.actionMode
349
- }
350
- const fieldModes: Record<string, FieldMode> = {}
351
- const fieldPositions: Record<string, FieldPosition> = {}
352
- const isRequireds: Record<string, IsRequired> = {}
353
- for (const [fieldKey, field] of Object.entries(list.fields)) {
354
- fieldModes[fieldKey] = field.itemView.fieldMode
355
- fieldPositions[fieldKey] = field.itemView.fieldPosition
356
- isRequireds[fieldKey] = field.itemView.isRequired
357
- }
358
- for (const field of data?.nixxie?.adminMeta?.list?.fields ?? []) {
359
- if (
360
- !field?.itemView ||
361
- !field.key ||
362
- !field.itemView.fieldMode ||
363
- !field.itemView.fieldPosition ||
364
- field.itemView.isRequired == null
365
- )
366
- continue
367
- fieldModes[field.key] = field.itemView.fieldMode
368
- fieldPositions[field.key] = field.itemView.fieldPosition
369
- isRequireds[field.key] = field.itemView.isRequired
370
- }
371
- for (const action of data?.nixxie?.adminMeta?.list?.actions ?? []) {
372
- if (!action?.itemView?.actionMode || !action.key) continue
373
- actionModes[action.key] = action.itemView.actionMode
374
- }
375
-
376
- return {
377
- actionModes,
378
- fieldModes,
379
- fieldPositions,
380
- isRequireds,
381
- }
382
- }, [
383
- data?.nixxie?.adminMeta?.list?.actions,
384
- data?.nixxie?.adminMeta?.list?.fields,
385
- list.actions,
386
- list.fields,
387
- ])
388
-
389
- const actionsInContext = useMemo(() => {
390
- if (!value) return []
391
- return list.actions
392
- .map(action => {
393
- const fieldKeys = getConditionalFilterFieldKeys(actionModes[action.key])
394
- const fields = pick(list.fields, fieldKeys)
395
- const serializedValue = serializeItemForConditionalFilters(fields, value)
396
- return {
397
- ...action,
398
- itemView: {
399
- ...action.itemView,
400
- actionMode: resolveActionMode(actionModes[action.key], serializedValue),
401
- },
402
- }
403
- })
404
- .filter(action => isActionAvailable(action, action.itemView))
405
- }, [actionModes, list.actions, list.fields, value])
406
-
407
- function onAction(action: ActionMeta, resultId: string | null) {
408
- const { navigation } = action.itemView
409
-
410
- if ((navigation === 'follow' && resultId === itemId) || navigation === 'refetch') {
411
- refetch()
412
- } else if (navigation === 'follow' && resultId) {
413
- router.push(`/${list.path}/${resultId}`)
414
- } else {
415
- router.push(list.isSingleton ? '/' : `/${list.path}`)
416
- }
417
- }
418
-
419
- return (
420
- <PageContainer
421
- title={pageTitle}
422
- header={
423
- <ItemPageHeader
424
- list={list}
425
- actions={actionsInContext}
426
- label={typeof pageLabel !== 'string' ? 'Loading...' : pageLabel}
427
- title={pageTitle}
428
- item={item ?? null}
429
- value={value ?? initialValue}
430
- initialValue={initialValue}
431
- onAction={onAction}
432
- />
433
- }
434
- >
435
- {pageLoading ? (
436
- <VStack height="100%" alignItems="center" justifyContent="center">
437
- <ProgressCircle aria-label="loading item data" size="large" isIndeterminate />
438
- </VStack>
439
- ) : (
440
- <ColumnLayout>
441
- <Box marginY="xlarge">
442
- <GraphQLErrorNotice errors={[error]} />
443
- {item == null &&
444
- (list.isSingleton ? (
445
- itemId === '1' ? (
446
- <ItemNotFound>
447
- <Text>“{list.label}” doesn’t exist, or you don’t have access to it.</Text>
448
- {!list.hideCreate && <CreateButtonLink list={list} />}
449
- </ItemNotFound>
450
- ) : (
451
- <ItemNotFound>
452
- <Text>
453
- An item with ID <strong>“{itemId}”</strong> does not exist.
454
- </Text>
455
- </ItemNotFound>
456
- )
457
- ) : (
458
- <ItemNotFound>
459
- <Text>
460
- The item with ID <strong>“{itemId}”</strong> doesn’t exist, or you don’t have
461
- access to it.
462
- </Text>
463
- </ItemNotFound>
464
- ))}
465
- </Box>
466
- {initialValue && value && (
467
- <ItemForm
468
- fieldModes={fieldModes}
469
- fieldPositions={fieldPositions}
470
- isRequireds={isRequireds}
471
- listKey={listKey}
472
- itemLabel={itemLabel}
473
- initialValue={initialValue}
474
- value={value}
475
- onChange={setValue}
476
- onSaveSuccess={refetch}
477
- />
478
- )}
479
- </ColumnLayout>
480
- )}
481
- </PageContainer>
482
- )
483
- }
1
+ import router, { useRouter } from 'next/router'
2
+ import {
3
+ type FormEvent,
4
+ Fragment,
5
+ type PropsWithChildren,
6
+ useCallback,
7
+ useEffect,
8
+ useMemo,
9
+ useRef,
10
+ useState,
11
+ } from 'react'
12
+
13
+ import { Button } from '@keystar/ui/button'
14
+ import { AlertDialog, DialogContainer, DialogTrigger } from '@keystar/ui/dialog'
15
+ import { Icon } from '@keystar/ui/icon'
16
+ import { fileWarningIcon } from '@keystar/ui/icon/icons/fileWarningIcon'
17
+ import { Box, VStack } from '@keystar/ui/layout'
18
+ import { ProgressCircle } from '@keystar/ui/progress'
19
+ import { SlotProvider } from '@keystar/ui/slots'
20
+ import { toastQueue } from '@keystar/ui/toast'
21
+ import { Heading, Text } from '@keystar/ui/typography'
22
+
23
+ import { CombinedGraphQLErrors, gql, useMutation } from '../../../../admin-ui/apollo'
24
+ import { CreateButtonLink } from '../../../../admin-ui/components/CreateButtonLink'
25
+ import { ErrorDetailsDialog } from '../../../../admin-ui/components/Errors'
26
+ import { GraphQLErrorNotice } from '../../../../admin-ui/components/GraphQLErrorNotice'
27
+ import { PageContainer } from '../../../../admin-ui/components/PageContainer'
28
+ import { useList, useListItem } from '../../../../admin-ui/context'
29
+ import {
30
+ deserializeItemToValue,
31
+ Fields,
32
+ getConditionalFilterFieldKeys,
33
+ isActionAvailable,
34
+ resolveActionMode,
35
+ serializeItemForConditionalFilters,
36
+ serializeValueToOperationItem,
37
+ useHasChanges,
38
+ useInvalidFields,
39
+ } from '../../../../admin-ui/utils'
40
+ import { pick } from '../../../../admin-ui/utils/pick'
41
+ import type {
42
+ ActionMeta,
43
+ BaseListTypeInfo,
44
+ ConditionalFilter,
45
+ ConditionalFilterCase,
46
+ ListMeta,
47
+ } from '../../../../types'
48
+ import { BaseToolbar, ColumnLayout, ItemPageHeader, StickySidebar } from './common'
49
+
50
+ type ItemPageProps = {
51
+ listKey: string
52
+ }
53
+
54
+ function useEventCallback<Func extends (...args: any[]) => unknown>(callback: Func): Func {
55
+ const callbackRef = useRef(callback)
56
+ const cb = useCallback((...args: any[]) => {
57
+ return callbackRef.current(...args)
58
+ }, [])
59
+ useEffect(() => {
60
+ callbackRef.current = callback
61
+ })
62
+ return cb as any
63
+ }
64
+
65
+ function DeleteButton({
66
+ list,
67
+ itemId,
68
+ itemLabel,
69
+ }: {
70
+ list: ListMeta
71
+ itemId: string
72
+ itemLabel: string
73
+ }) {
74
+ const [errorDialogValue, setErrorDialogValue] = useState<Error | null>(null)
75
+ const router = useRouter()
76
+ const [deleteItem] = useMutation(
77
+ gql`mutation ($id: ID!) {
78
+ ${list.graphql.names.deleteMutationName}(where: { id: $id }) {
79
+ id
80
+ }
81
+ }`,
82
+ { variables: { id: itemId } }
83
+ )
84
+
85
+ return (
86
+ <Fragment>
87
+ <DialogTrigger>
88
+ <Button tone="critical">Delete</Button>
89
+ <AlertDialog
90
+ tone="critical"
91
+ title="Delete item"
92
+ cancelLabel="Cancel"
93
+ primaryActionLabel="Yes, delete"
94
+ onPrimaryAction={async () => {
95
+ try {
96
+ await deleteItem()
97
+ } catch (err: any) {
98
+ toastQueue.critical('Unable to delete item', {
99
+ actionLabel: 'Details',
100
+ onAction: () => setErrorDialogValue(err),
101
+ shouldCloseOnAction: true,
102
+ })
103
+ return
104
+ }
105
+
106
+ toastQueue.neutral(`${list.singular} deleted.`, {
107
+ timeout: 5000,
108
+ })
109
+ router.push(list.isSingleton ? '/' : `/${list.path}`)
110
+ }}
111
+ >
112
+ <Text>
113
+ Are you sure you want to delete <strong style={{ fontWeight: 600 }}>{itemLabel}</strong>
114
+ ? This action cannot be undone.
115
+ </Text>
116
+ </AlertDialog>
117
+ </DialogTrigger>
118
+
119
+ <DialogContainer onDismiss={() => setErrorDialogValue(null)} isDismissable>
120
+ {errorDialogValue && (
121
+ <ErrorDetailsDialog title="Unable to delete item" error={errorDialogValue} />
122
+ )}
123
+ </DialogContainer>
124
+ </Fragment>
125
+ )
126
+ }
127
+
128
+ function ItemNotFound(props: PropsWithChildren) {
129
+ return (
130
+ <VStack
131
+ alignItems="center"
132
+ backgroundColor="surface"
133
+ borderRadius="medium"
134
+ gap="large"
135
+ justifyContent="center"
136
+ minHeight="scale.3000"
137
+ padding="xlarge"
138
+ >
139
+ <Icon src={fileWarningIcon} color="neutralEmphasis" size="large" />
140
+ <Heading align="center">Not found</Heading>
141
+ <SlotProvider slots={{ text: { align: 'center', maxWidth: 'scale.5000' } }}>
142
+ {props.children}
143
+ </SlotProvider>
144
+ </VStack>
145
+ )
146
+ }
147
+
148
+ function ResetButton(props: { onReset: () => void; hasChanges?: boolean }) {
149
+ return (
150
+ <DialogTrigger>
151
+ <Button tone="accent" isDisabled={!props.hasChanges}>
152
+ Reset
153
+ </Button>
154
+ <AlertDialog
155
+ title="Reset changes"
156
+ cancelLabel="Cancel"
157
+ primaryActionLabel="Yes, reset"
158
+ autoFocusButton="primary"
159
+ onPrimaryAction={props.onReset}
160
+ >
161
+ Are you sure? Any unsaved changes will be lost and cannot be recovered.
162
+ </AlertDialog>
163
+ </DialogTrigger>
164
+ )
165
+ }
166
+
167
+ function ItemForm({
168
+ listKey,
169
+ initialValue,
170
+ value,
171
+ onChange,
172
+ itemLabel,
173
+ onSaveSuccess,
174
+ fieldModes,
175
+ fieldPositions,
176
+ isRequireds,
177
+ }: {
178
+ listKey: string
179
+ initialValue: Record<string, unknown>
180
+ value: Record<string, unknown>
181
+ onChange: (value: Record<string, unknown>) => void
182
+ itemLabel: string
183
+ onSaveSuccess: () => void
184
+ fieldModes: Record<
185
+ string,
186
+ ConditionalFilter<'edit' | 'read' | 'hidden', 'read' | 'hidden', BaseListTypeInfo>
187
+ >
188
+ isRequireds: Record<string, ConditionalFilterCase<BaseListTypeInfo>>
189
+ fieldPositions: Record<string, 'form' | 'sidebar'>
190
+ }) {
191
+ const list = useList(listKey)
192
+ const itemId = initialValue.id as string
193
+ const [updateError, setUpdateError] = useState<Error | null>(null)
194
+ const [update, { loading, error }] = useMutation(
195
+ gql`mutation ($id: ID!, $data: ${list.graphql.names.updateInputName}!) {
196
+ item: ${list.graphql.names.updateMutationName}(where: { id: $id }, data: $data) {
197
+ id
198
+ }
199
+ }`,
200
+ { errorPolicy: 'all' }
201
+ )
202
+
203
+ function resetValueState() {
204
+ onChange(initialValue)
205
+ }
206
+
207
+ const invalidFields = useInvalidFields(list.fields, value, isRequireds)
208
+ const [forceValidation, setForceValidation] = useState(false)
209
+ const onSave = useEventCallback(async (e: FormEvent<HTMLFormElement>) => {
210
+ if (e.target !== e.currentTarget) return
211
+ e.preventDefault()
212
+ const newForceValidation = invalidFields.size !== 0
213
+ setForceValidation(newForceValidation)
214
+ if (newForceValidation) return
215
+
216
+ const { error: _error } = await update({
217
+ variables: {
218
+ id: itemId,
219
+ data: serializeValueToOperationItem('update', list.fields, value, initialValue),
220
+ },
221
+ })
222
+
223
+ const error = CombinedGraphQLErrors.is(_error)
224
+ ? _error.errors.find(x => x.path === undefined || x.path?.length === 1)
225
+ : _error
226
+ if (error) {
227
+ toastQueue.critical('Unable to save item', {
228
+ actionLabel: 'Details',
229
+ onAction: () => setUpdateError(new Error(error.message)),
230
+ shouldCloseOnAction: true,
231
+ })
232
+ return
233
+ }
234
+
235
+ toastQueue.positive(`Saved changes to ${list.singular.toLocaleLowerCase()}.`, {
236
+ timeout: 5000,
237
+ })
238
+
239
+ onSaveSuccess()
240
+ })
241
+
242
+ const hasChangedFields = useHasChanges('update', list.fields, value, initialValue)
243
+
244
+ return (
245
+ <Fragment>
246
+ <form onSubmit={onSave} style={{ display: 'contents' }}>
247
+ {/*
248
+ Workaround for react-aria "bug" where pressing enter in a form field
249
+ moves focus to the submit button.
250
+ See: https://github.com/adobe/react-spectrum/issues/5940
251
+ */}
252
+ <button type="submit" style={{ display: 'none' }} />
253
+ <VStack gap="large" gridArea="main" marginTop="xlarge" minWidth={0}>
254
+ <GraphQLErrorNotice
255
+ errors={
256
+ CombinedGraphQLErrors.is(error)
257
+ ? error.errors.filter(x => x.path === undefined || x.path?.length === 1)
258
+ : [error]
259
+ }
260
+ />
261
+ <Fields
262
+ view="itemView"
263
+ position="form"
264
+ fields={list.fields}
265
+ groups={list.groups}
266
+ forceValidation={forceValidation}
267
+ invalidFields={invalidFields}
268
+ fieldModes={fieldModes}
269
+ fieldPositions={fieldPositions}
270
+ onChange={onChange}
271
+ value={value}
272
+ isRequireds={isRequireds}
273
+ />
274
+ </VStack>
275
+
276
+ <StickySidebar>
277
+ <Fields
278
+ view="itemView"
279
+ position="sidebar"
280
+ fields={list.fields}
281
+ groups={list.groups}
282
+ forceValidation={forceValidation}
283
+ invalidFields={invalidFields}
284
+ onChange={onChange}
285
+ value={value}
286
+ fieldModes={fieldModes}
287
+ fieldPositions={fieldPositions}
288
+ isRequireds={isRequireds}
289
+ />
290
+ </StickySidebar>
291
+
292
+ <BaseToolbar>
293
+ <Button
294
+ isDisabled={!hasChangedFields}
295
+ isPending={loading}
296
+ prominence="high"
297
+ type="submit"
298
+ >
299
+ Save
300
+ </Button>
301
+ <ResetButton hasChanges={hasChangedFields} onReset={resetValueState} />
302
+ <Box flex />
303
+ {!list.hideDelete ? (
304
+ <DeleteButton list={list} itemId={itemId} itemLabel={itemLabel} />
305
+ ) : null}
306
+ </BaseToolbar>
307
+ </form>
308
+
309
+ <DialogContainer onDismiss={() => setUpdateError(null)} isDismissable>
310
+ {updateError && <ErrorDetailsDialog title="Unable to save item" error={updateError} />}
311
+ </DialogContainer>
312
+ </Fragment>
313
+ )
314
+ }
315
+
316
+ export const getItemPage = (props: ItemPageProps) => () => <ItemPage {...props} />
317
+
318
+ function ItemPage({ listKey }: ItemPageProps) {
319
+ const list = useList(listKey)
320
+ const id_ = useRouter().query.id
321
+ const [itemId] = Array.isArray(id_) ? id_ : [id_]
322
+ const { data, error, loading, refetch } = useListItem(listKey, itemId ?? null)
323
+ const item = data?.item
324
+ const itemLabel_ = item?.[list.labelField] ?? item?.id
325
+ const itemLabel = typeof itemLabel_ === 'string' ? itemLabel_ : (itemId ?? '')
326
+
327
+ const pageLoading = loading || itemId === undefined
328
+ const pageLabel = itemLabel || itemId
329
+ const pageTitle = list.isSingleton || typeof pageLabel !== 'string' ? list.label : pageLabel
330
+ const initialValue = useMemo(() => {
331
+ if (!item) return null
332
+ return deserializeItemToValue(list.fields, item)
333
+ }, [list.fields, data?.item])
334
+ const [value, setValue] = useState<Record<string, unknown> | null>(null)
335
+ useEffect(() => {
336
+ setValue(initialValue)
337
+ }, [initialValue])
338
+
339
+ const { actionModes, fieldModes, fieldPositions, isRequireds } = useMemo(() => {
340
+ type ActionMode = (typeof list.actions)[number]['itemView']['actionMode']
341
+ type ListField = (typeof list.fields)[keyof typeof list.fields]
342
+ type FieldMode = ListField['itemView']['fieldMode']
343
+ type FieldPosition = ListField['itemView']['fieldPosition']
344
+ type IsRequired = ListField['itemView']['isRequired']
345
+
346
+ const actionModes: Record<string, ActionMode> = {}
347
+ for (const action of list.actions) {
348
+ actionModes[action.key] = action.itemView.actionMode
349
+ }
350
+ const fieldModes: Record<string, FieldMode> = {}
351
+ const fieldPositions: Record<string, FieldPosition> = {}
352
+ const isRequireds: Record<string, IsRequired> = {}
353
+ for (const [fieldKey, field] of Object.entries(list.fields)) {
354
+ fieldModes[fieldKey] = field.itemView.fieldMode
355
+ fieldPositions[fieldKey] = field.itemView.fieldPosition
356
+ isRequireds[fieldKey] = field.itemView.isRequired
357
+ }
358
+ for (const field of data?.nixxie?.adminMeta?.list?.fields ?? []) {
359
+ if (
360
+ !field?.itemView ||
361
+ !field.key ||
362
+ !field.itemView.fieldMode ||
363
+ !field.itemView.fieldPosition ||
364
+ field.itemView.isRequired == null
365
+ )
366
+ continue
367
+ fieldModes[field.key] = field.itemView.fieldMode
368
+ fieldPositions[field.key] = field.itemView.fieldPosition
369
+ isRequireds[field.key] = field.itemView.isRequired
370
+ }
371
+ for (const action of data?.nixxie?.adminMeta?.list?.actions ?? []) {
372
+ if (!action?.itemView?.actionMode || !action.key) continue
373
+ actionModes[action.key] = action.itemView.actionMode
374
+ }
375
+
376
+ return {
377
+ actionModes,
378
+ fieldModes,
379
+ fieldPositions,
380
+ isRequireds,
381
+ }
382
+ }, [
383
+ data?.nixxie?.adminMeta?.list?.actions,
384
+ data?.nixxie?.adminMeta?.list?.fields,
385
+ list.actions,
386
+ list.fields,
387
+ ])
388
+
389
+ const actionsInContext = useMemo(() => {
390
+ if (!value) return []
391
+ return list.actions
392
+ .map(action => {
393
+ const fieldKeys = getConditionalFilterFieldKeys(actionModes[action.key])
394
+ const fields = pick(list.fields, fieldKeys)
395
+ const serializedValue = serializeItemForConditionalFilters(fields, value)
396
+ return {
397
+ ...action,
398
+ itemView: {
399
+ ...action.itemView,
400
+ actionMode: resolveActionMode(actionModes[action.key], serializedValue),
401
+ },
402
+ }
403
+ })
404
+ .filter(action => isActionAvailable(action, action.itemView))
405
+ }, [actionModes, list.actions, list.fields, value])
406
+
407
+ function onAction(action: ActionMeta, resultId: string | null) {
408
+ const { navigation } = action.itemView
409
+
410
+ if ((navigation === 'follow' && resultId === itemId) || navigation === 'refetch') {
411
+ refetch()
412
+ } else if (navigation === 'follow' && resultId) {
413
+ router.push(`/${list.path}/${resultId}`)
414
+ } else {
415
+ router.push(list.isSingleton ? '/' : `/${list.path}`)
416
+ }
417
+ }
418
+
419
+ return (
420
+ <PageContainer
421
+ title={pageTitle}
422
+ header={
423
+ <ItemPageHeader
424
+ list={list}
425
+ actions={actionsInContext}
426
+ label={typeof pageLabel !== 'string' ? 'Loading...' : pageLabel}
427
+ title={pageTitle}
428
+ item={item ?? null}
429
+ value={value ?? initialValue}
430
+ initialValue={initialValue}
431
+ onAction={onAction}
432
+ />
433
+ }
434
+ >
435
+ {pageLoading ? (
436
+ <VStack height="100%" alignItems="center" justifyContent="center">
437
+ <ProgressCircle aria-label="loading item data" size="large" isIndeterminate />
438
+ </VStack>
439
+ ) : (
440
+ <ColumnLayout>
441
+ <Box marginY="xlarge">
442
+ <GraphQLErrorNotice errors={[error]} />
443
+ {item == null &&
444
+ (list.isSingleton ? (
445
+ itemId === '1' ? (
446
+ <ItemNotFound>
447
+ <Text>“{list.label}” doesn’t exist, or you don’t have access to it.</Text>
448
+ {!list.hideCreate && <CreateButtonLink list={list} />}
449
+ </ItemNotFound>
450
+ ) : (
451
+ <ItemNotFound>
452
+ <Text>
453
+ An item with ID <strong>“{itemId}”</strong> does not exist.
454
+ </Text>
455
+ </ItemNotFound>
456
+ )
457
+ ) : (
458
+ <ItemNotFound>
459
+ <Text>
460
+ The item with ID <strong>“{itemId}”</strong> doesn’t exist, or you don’t have
461
+ access to it.
462
+ </Text>
463
+ </ItemNotFound>
464
+ ))}
465
+ </Box>
466
+ {initialValue && value && (
467
+ <ItemForm
468
+ fieldModes={fieldModes}
469
+ fieldPositions={fieldPositions}
470
+ isRequireds={isRequireds}
471
+ listKey={listKey}
472
+ itemLabel={itemLabel}
473
+ initialValue={initialValue}
474
+ value={value}
475
+ onChange={setValue}
476
+ onSaveSuccess={refetch}
477
+ />
478
+ )}
479
+ </ColumnLayout>
480
+ )}
481
+ </PageContainer>
482
+ )
483
+ }