@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,358 +1,358 @@
1
- import { useRouter } from 'next/router'
2
- import type { HTMLAttributes, ReactNode } from 'react'
3
- import { Fragment, useMemo, useState } from 'react'
4
- import isDeepEqual from 'fast-deep-equal'
5
-
6
- import { ActionGroup } from '@keystar/ui/action-group'
7
- import { Breadcrumbs, Item } from '@keystar/ui/breadcrumbs'
8
- import { AlertDialog, DialogContainer } from '@keystar/ui/dialog'
9
- import { Icon } from '@keystar/ui/icon'
10
- import { allIcons as KeystarIcons } from '@keystar/ui/icon/all'
11
- import { Grid, HStack } from '@keystar/ui/layout'
12
- import { breakpointQueries, css, tokenSchema } from '@keystar/ui/style'
13
- import { toastQueue } from '@keystar/ui/toast'
14
- import { Heading, Text } from '@keystar/ui/typography'
15
- import { gql, type TypedDocumentNode, useApolloClient } from '../../../../admin-ui/apollo'
16
- import { Container, CONTAINER_MAX } from '../../../../admin-ui/components/Container'
17
- import { ErrorDetailsDialog } from '../../../../admin-ui/components/Errors'
18
- import {
19
- getActionArguments,
20
- getActionGraphQLArgs,
21
- getActionGraphQLVariables,
22
- } from '../../../../admin-ui/utils/actionData'
23
- import type { ActionMeta, ListMeta } from '../../../../types'
24
-
25
- export function ItemPageHeader({
26
- list,
27
- actions,
28
- item,
29
- value,
30
- initialValue,
31
- label,
32
- title = label,
33
- onAction,
34
- }: {
35
- list: ListMeta
36
- actions: ActionMeta[]
37
- item: Record<string, unknown> | null
38
- value: Record<string, unknown> | null
39
- initialValue: Record<string, unknown> | null
40
- label: string
41
- title: string
42
- onAction: ((action: ActionMeta, resultId: string | null) => void) | null
43
- }) {
44
- const router = useRouter()
45
-
46
- return (
47
- <Grid
48
- // fill space; take over layout from the `PageContainer` flex wrapper
49
- flex
50
- // make sure actions don't run into the primary element, even though it'll truncate
51
- gap="medium"
52
- // best efforts to ensure actions collapse first, then the title/breadcrumbs may truncate
53
- columns={`minmax(50cqw, auto) minmax(${tokenSchema.size.element.regular}, max-content)`}
54
- // grid areas required because the `ActionGroup` implements focus
55
- // sentinels (span) before and after the root element
56
- areas={['primary secondary']}
57
- // treat this area like a container
58
- minWidth={0}
59
- maxWidth={CONTAINER_MAX}
60
- >
61
- {list.isSingleton ? (
62
- <Heading elementType="h1" size="small" gridArea="primary" truncate>
63
- {list.label}
64
- </Heading>
65
- ) : (
66
- <>
67
- <Breadcrumbs size="medium" gridArea="primary">
68
- <Item href={`/${list.path}`}>{list.label}</Item>
69
- <Item href={router.asPath}>{label}</Item>
70
- </Breadcrumbs>
71
-
72
- {/* Every page must have an H1 for accessibility. */}
73
- <Text elementType="h1" visuallyHidden>
74
- {title}
75
- </Text>
76
- </>
77
- )}
78
-
79
- {item && onAction && actions.length > 0 && (
80
- <ItemActions
81
- list={list}
82
- item={item}
83
- value={value}
84
- initialValue={initialValue}
85
- actions={actions}
86
- onAction={onAction}
87
- />
88
- )}
89
- </Grid>
90
- )
91
- }
92
-
93
- function replace(
94
- s: string,
95
- list: ListMeta,
96
- args: ActionMeta & {
97
- itemLabel?: string
98
- }
99
- ) {
100
- return s
101
- .replaceAll('{itemLabel}', args.itemLabel ?? '')
102
- .replaceAll('{Label}', args.label)
103
- .replaceAll('{label}', args.label.toLowerCase())
104
- .replaceAll('{Plural}', list.plural)
105
- .replaceAll('{plural}', list.plural.toLowerCase())
106
- .replaceAll('{Singular}', list.singular)
107
- .replaceAll('{singular}', list.singular.toLowerCase())
108
- }
109
-
110
- type ActionError = {
111
- action: ActionMeta
112
- error: Error
113
- }
114
-
115
- function ItemActions({
116
- list,
117
- item,
118
- value,
119
- initialValue,
120
- actions,
121
- onAction,
122
- }: {
123
- list: ListMeta
124
- item: Record<string, unknown>
125
- value: Record<string, unknown> | null
126
- initialValue: Record<string, unknown> | null
127
- actions: ActionMeta[]
128
- onAction: (action: ActionMeta, resultId: string | null) => void
129
- }) {
130
- const apolloClient = useApolloClient()
131
- const actionItems = useMemo(
132
- () =>
133
- actions.map(action => ({
134
- id: action.key,
135
- label: action.label,
136
- icon: action.icon ? KeystarIcons[action.icon] : null,
137
- })),
138
- [actions]
139
- )
140
- const [actionError, setActionError] = useState<ActionError | null>(null)
141
- const [activeAction, setActiveAction] = useState<ActionMeta | null>(null)
142
- const [blockedAction, setBlockedAction] = useState<ActionMeta | null>(null)
143
- const itemLabel_ = item[list.labelField] ?? item.id
144
- const itemLabel = typeof itemLabel_ === 'string' ? itemLabel_ : (item.id as string)
145
- const hasUnsavedChanges = useMemo(
146
- () => value !== null && initialValue !== null && !isDeepEqual(value, initialValue),
147
- [initialValue, value]
148
- )
149
-
150
- const disabledKeys = useMemo(
151
- () =>
152
- actions.filter(action => action.itemView.actionMode === 'disabled').map(action => action.key),
153
- [actions]
154
- )
155
-
156
- async function onTryAction(action: ActionMeta, confirmed: boolean) {
157
- setActiveAction(null)
158
-
159
- if (hasUnsavedChanges) {
160
- setBlockedAction(action)
161
- return
162
- }
163
-
164
- if (!confirmed && !action.itemView.hidePrompt) {
165
- setActiveAction(action)
166
- return
167
- }
168
-
169
- const { messages: m } = action
170
- try {
171
- const argsForAction = initialValue ? getActionArguments(list, action, initialValue) : {}
172
- const graphqlArgs = getActionGraphQLArgs(action)
173
- const graphqlVariables = getActionGraphQLVariables(action)
174
- const mutation = (
175
- action.graphql.arguments.length
176
- ? gql`mutation ${action.graphql.names.one}($id: ID!, ${graphqlVariables}) {
177
- result: ${action.graphql.names.one}(where: { id: $id }, ${graphqlArgs}) {
178
- id
179
- }
180
- }`
181
- : gql`mutation ${action.graphql.names.one}($id: ID!) {
182
- result: ${action.graphql.names.one}(where: { id: $id }) {
183
- id
184
- }
185
- }`
186
- ) as TypedDocumentNode<{ result: { id: string } }, { id: string } & Record<string, unknown>>
187
- const data = await apolloClient.mutate({
188
- mutation,
189
- variables: action.graphql.arguments.length
190
- ? { id: item.id as string, ...argsForAction }
191
- : { id: item.id as string },
192
- })
193
-
194
- if (!action.itemView.hideToast) {
195
- toastQueue.neutral(replace(m.success, list, { ...action, itemLabel }), {
196
- timeout: 5000,
197
- })
198
- }
199
-
200
- onAction(action, data.data?.result?.id ?? null)
201
- } catch (error: any) {
202
- toastQueue.critical(replace(m.fail, list, { ...action, itemLabel }), {
203
- actionLabel: 'Details',
204
- onAction: () => setActionError({ action, error }),
205
- shouldCloseOnAction: true,
206
- })
207
- }
208
- }
209
-
210
- return (
211
- <Fragment>
212
- <ActionGroup
213
- gridArea="secondary"
214
- disabledKeys={disabledKeys}
215
- overflowMode="collapse"
216
- items={actionItems}
217
- onAction={key => {
218
- const action = list.actions.find(action => action.key === key)
219
- if (!action) return
220
- onTryAction(action, false)
221
- }}
222
- >
223
- {item => (
224
- <Item textValue={item.label}>
225
- {item.icon && <Icon src={item.icon} />}
226
- <Text>{item.label}</Text>
227
- </Item>
228
- )}
229
- </ActionGroup>
230
- <DialogContainer onDismiss={() => setActiveAction(null)}>
231
- {activeAction && (
232
- <AlertDialog
233
- title={replace(activeAction.messages.promptTitle, list, { ...activeAction, itemLabel })}
234
- cancelLabel="Cancel"
235
- primaryActionLabel={replace(activeAction.messages.promptConfirmLabel, list, {
236
- ...activeAction,
237
- itemLabel,
238
- })}
239
- onPrimaryAction={async () => {
240
- await onTryAction(activeAction, true)
241
- }}
242
- >
243
- {replace(activeAction.messages.prompt, list, { ...activeAction, itemLabel })}
244
- </AlertDialog>
245
- )}
246
- </DialogContainer>
247
-
248
- <DialogContainer onDismiss={() => setBlockedAction(null)}>
249
- {blockedAction && (
250
- <AlertDialog
251
- title="Unsaved changes"
252
- primaryActionLabel="Dismiss"
253
- onPrimaryAction={() => setBlockedAction(null)}
254
- >
255
- Please save or reset your changes before attempting to{' '}
256
- {replace('{label}', list, blockedAction)}.
257
- </AlertDialog>
258
- )}
259
- </DialogContainer>
260
-
261
- <DialogContainer onDismiss={() => setActionError(null)} isDismissable>
262
- {actionError && (
263
- <ErrorDetailsDialog
264
- title={replace(actionError.action.messages.fail, list, {
265
- ...actionError.action,
266
- itemLabel,
267
- })}
268
- error={actionError.error}
269
- />
270
- )}
271
- </DialogContainer>
272
- </Fragment>
273
- )
274
- }
275
-
276
- export function ColumnLayout(props: HTMLAttributes<HTMLDivElement>) {
277
- return (
278
- // this container must be relative to catch absolute children
279
- // particularly the "expanded" document-field, which needs a height of 100%
280
- <Container position="relative" height="100%" paddingX="xlarge">
281
- <div
282
- className={css({
283
- display: 'grid',
284
- columnGap: tokenSchema.size.space.xlarge,
285
- gridTemplateAreas: '"main" "sidebar" "toolbar"',
286
-
287
- [breakpointQueries.above.tablet]: {
288
- gridTemplateColumns: `2fr minmax(${tokenSchema.size.scale[3600]}, 1fr)`,
289
- gridTemplateAreas: '"main sidebar" "toolbar toolbar"',
290
- },
291
- })}
292
- {...props}
293
- />
294
- </Container>
295
- )
296
- }
297
-
298
- export function StickySidebar(props: HTMLAttributes<HTMLDivElement>) {
299
- return (
300
- <div
301
- className={css({
302
- gridArea: 'sidebar',
303
- marginTop: tokenSchema.size.space.xlarge,
304
-
305
- [breakpointQueries.above.tablet]: {
306
- alignSelf: 'start',
307
- marginBottom: tokenSchema.size.element.xlarge,
308
- // sync with toolbar height
309
- paddingBottom: tokenSchema.size.element.xlarge,
310
- position: 'sticky',
311
- top: tokenSchema.size.space.xlarge,
312
- },
313
- })}
314
- {...props}
315
- />
316
- )
317
- }
318
-
319
- export function BaseToolbar(props: { children: ReactNode }) {
320
- return (
321
- <Grid
322
- backgroundColor="surface"
323
- columns="subgrid"
324
- gridArea="toolbar"
325
- insetBottom={0}
326
- marginTop="xlarge"
327
- position={{ tablet: 'sticky' }}
328
- zIndex={20}
329
- UNSAFE_className={css({
330
- // fuzzy mask sidebar fields, which slide behind the un-bordered portion
331
- // of the sticky toolbar
332
- [breakpointQueries.above.tablet]: {
333
- '&::after': {
334
- boxShadow: `0 -4px 4px 1px ${tokenSchema.color.background.surface}`,
335
- content: '""',
336
- },
337
- },
338
- })}
339
- >
340
- <HStack
341
- alignItems="center"
342
- borderTop="neutral"
343
- gap="regular"
344
- height="element.xlarge"
345
- UNSAFE_className={css({
346
- // stretch horizontally to ensure field focus-rings are covered
347
- [breakpointQueries.above.mobile]: {
348
- backgroundColor: tokenSchema.color.background.surface,
349
- marginInline: `calc(${tokenSchema.size.alias.focusRing} * -1)`,
350
- paddingInline: tokenSchema.size.alias.focusRing,
351
- },
352
- })}
353
- >
354
- {props.children}
355
- </HStack>
356
- </Grid>
357
- )
358
- }
1
+ import { useRouter } from 'next/router'
2
+ import type { HTMLAttributes, ReactNode } from 'react'
3
+ import { Fragment, useMemo, useState } from 'react'
4
+ import isDeepEqual from 'fast-deep-equal'
5
+
6
+ import { ActionGroup } from '@keystar/ui/action-group'
7
+ import { Breadcrumbs, Item } from '@keystar/ui/breadcrumbs'
8
+ import { AlertDialog, DialogContainer } from '@keystar/ui/dialog'
9
+ import { Icon } from '@keystar/ui/icon'
10
+ import { allIcons as KeystarIcons } from '@keystar/ui/icon/all'
11
+ import { Grid, HStack } from '@keystar/ui/layout'
12
+ import { breakpointQueries, css, tokenSchema } from '@keystar/ui/style'
13
+ import { toastQueue } from '@keystar/ui/toast'
14
+ import { Heading, Text } from '@keystar/ui/typography'
15
+ import { gql, type TypedDocumentNode, useApolloClient } from '../../../../admin-ui/apollo'
16
+ import { Container, CONTAINER_MAX } from '../../../../admin-ui/components/Container'
17
+ import { ErrorDetailsDialog } from '../../../../admin-ui/components/Errors'
18
+ import {
19
+ getActionArguments,
20
+ getActionGraphQLArgs,
21
+ getActionGraphQLVariables,
22
+ } from '../../../../admin-ui/utils/actionData'
23
+ import type { ActionMeta, ListMeta } from '../../../../types'
24
+
25
+ export function ItemPageHeader({
26
+ list,
27
+ actions,
28
+ item,
29
+ value,
30
+ initialValue,
31
+ label,
32
+ title = label,
33
+ onAction,
34
+ }: {
35
+ list: ListMeta
36
+ actions: ActionMeta[]
37
+ item: Record<string, unknown> | null
38
+ value: Record<string, unknown> | null
39
+ initialValue: Record<string, unknown> | null
40
+ label: string
41
+ title: string
42
+ onAction: ((action: ActionMeta, resultId: string | null) => void) | null
43
+ }) {
44
+ const router = useRouter()
45
+
46
+ return (
47
+ <Grid
48
+ // fill space; take over layout from the `PageContainer` flex wrapper
49
+ flex
50
+ // make sure actions don't run into the primary element, even though it'll truncate
51
+ gap="medium"
52
+ // best efforts to ensure actions collapse first, then the title/breadcrumbs may truncate
53
+ columns={`minmax(50cqw, auto) minmax(${tokenSchema.size.element.regular}, max-content)`}
54
+ // grid areas required because the `ActionGroup` implements focus
55
+ // sentinels (span) before and after the root element
56
+ areas={['primary secondary']}
57
+ // treat this area like a container
58
+ minWidth={0}
59
+ maxWidth={CONTAINER_MAX}
60
+ >
61
+ {list.isSingleton ? (
62
+ <Heading elementType="h1" size="small" gridArea="primary" truncate>
63
+ {list.label}
64
+ </Heading>
65
+ ) : (
66
+ <>
67
+ <Breadcrumbs size="medium" gridArea="primary">
68
+ <Item href={`/${list.path}`}>{list.label}</Item>
69
+ <Item href={router.asPath}>{label}</Item>
70
+ </Breadcrumbs>
71
+
72
+ {/* Every page must have an H1 for accessibility. */}
73
+ <Text elementType="h1" visuallyHidden>
74
+ {title}
75
+ </Text>
76
+ </>
77
+ )}
78
+
79
+ {item && onAction && actions.length > 0 && (
80
+ <ItemActions
81
+ list={list}
82
+ item={item}
83
+ value={value}
84
+ initialValue={initialValue}
85
+ actions={actions}
86
+ onAction={onAction}
87
+ />
88
+ )}
89
+ </Grid>
90
+ )
91
+ }
92
+
93
+ function replace(
94
+ s: string,
95
+ list: ListMeta,
96
+ args: ActionMeta & {
97
+ itemLabel?: string
98
+ }
99
+ ) {
100
+ return s
101
+ .replaceAll('{itemLabel}', args.itemLabel ?? '')
102
+ .replaceAll('{Label}', args.label)
103
+ .replaceAll('{label}', args.label.toLowerCase())
104
+ .replaceAll('{Plural}', list.plural)
105
+ .replaceAll('{plural}', list.plural.toLowerCase())
106
+ .replaceAll('{Singular}', list.singular)
107
+ .replaceAll('{singular}', list.singular.toLowerCase())
108
+ }
109
+
110
+ type ActionError = {
111
+ action: ActionMeta
112
+ error: Error
113
+ }
114
+
115
+ function ItemActions({
116
+ list,
117
+ item,
118
+ value,
119
+ initialValue,
120
+ actions,
121
+ onAction,
122
+ }: {
123
+ list: ListMeta
124
+ item: Record<string, unknown>
125
+ value: Record<string, unknown> | null
126
+ initialValue: Record<string, unknown> | null
127
+ actions: ActionMeta[]
128
+ onAction: (action: ActionMeta, resultId: string | null) => void
129
+ }) {
130
+ const apolloClient = useApolloClient()
131
+ const actionItems = useMemo(
132
+ () =>
133
+ actions.map(action => ({
134
+ id: action.key,
135
+ label: action.label,
136
+ icon: action.icon ? KeystarIcons[action.icon] : null,
137
+ })),
138
+ [actions]
139
+ )
140
+ const [actionError, setActionError] = useState<ActionError | null>(null)
141
+ const [activeAction, setActiveAction] = useState<ActionMeta | null>(null)
142
+ const [blockedAction, setBlockedAction] = useState<ActionMeta | null>(null)
143
+ const itemLabel_ = item[list.labelField] ?? item.id
144
+ const itemLabel = typeof itemLabel_ === 'string' ? itemLabel_ : (item.id as string)
145
+ const hasUnsavedChanges = useMemo(
146
+ () => value !== null && initialValue !== null && !isDeepEqual(value, initialValue),
147
+ [initialValue, value]
148
+ )
149
+
150
+ const disabledKeys = useMemo(
151
+ () =>
152
+ actions.filter(action => action.itemView.actionMode === 'disabled').map(action => action.key),
153
+ [actions]
154
+ )
155
+
156
+ async function onTryAction(action: ActionMeta, confirmed: boolean) {
157
+ setActiveAction(null)
158
+
159
+ if (hasUnsavedChanges) {
160
+ setBlockedAction(action)
161
+ return
162
+ }
163
+
164
+ if (!confirmed && !action.itemView.hidePrompt) {
165
+ setActiveAction(action)
166
+ return
167
+ }
168
+
169
+ const { messages: m } = action
170
+ try {
171
+ const argsForAction = initialValue ? getActionArguments(list, action, initialValue) : {}
172
+ const graphqlArgs = getActionGraphQLArgs(action)
173
+ const graphqlVariables = getActionGraphQLVariables(action)
174
+ const mutation = (
175
+ action.graphql.arguments.length
176
+ ? gql`mutation ${action.graphql.names.one}($id: ID!, ${graphqlVariables}) {
177
+ result: ${action.graphql.names.one}(where: { id: $id }, ${graphqlArgs}) {
178
+ id
179
+ }
180
+ }`
181
+ : gql`mutation ${action.graphql.names.one}($id: ID!) {
182
+ result: ${action.graphql.names.one}(where: { id: $id }) {
183
+ id
184
+ }
185
+ }`
186
+ ) as TypedDocumentNode<{ result: { id: string } }, { id: string } & Record<string, unknown>>
187
+ const data = await apolloClient.mutate({
188
+ mutation,
189
+ variables: action.graphql.arguments.length
190
+ ? { id: item.id as string, ...argsForAction }
191
+ : { id: item.id as string },
192
+ })
193
+
194
+ if (!action.itemView.hideToast) {
195
+ toastQueue.neutral(replace(m.success, list, { ...action, itemLabel }), {
196
+ timeout: 5000,
197
+ })
198
+ }
199
+
200
+ onAction(action, data.data?.result?.id ?? null)
201
+ } catch (error: any) {
202
+ toastQueue.critical(replace(m.fail, list, { ...action, itemLabel }), {
203
+ actionLabel: 'Details',
204
+ onAction: () => setActionError({ action, error }),
205
+ shouldCloseOnAction: true,
206
+ })
207
+ }
208
+ }
209
+
210
+ return (
211
+ <Fragment>
212
+ <ActionGroup
213
+ gridArea="secondary"
214
+ disabledKeys={disabledKeys}
215
+ overflowMode="collapse"
216
+ items={actionItems}
217
+ onAction={key => {
218
+ const action = list.actions.find(action => action.key === key)
219
+ if (!action) return
220
+ onTryAction(action, false)
221
+ }}
222
+ >
223
+ {item => (
224
+ <Item textValue={item.label}>
225
+ {item.icon && <Icon src={item.icon} />}
226
+ <Text>{item.label}</Text>
227
+ </Item>
228
+ )}
229
+ </ActionGroup>
230
+ <DialogContainer onDismiss={() => setActiveAction(null)}>
231
+ {activeAction && (
232
+ <AlertDialog
233
+ title={replace(activeAction.messages.promptTitle, list, { ...activeAction, itemLabel })}
234
+ cancelLabel="Cancel"
235
+ primaryActionLabel={replace(activeAction.messages.promptConfirmLabel, list, {
236
+ ...activeAction,
237
+ itemLabel,
238
+ })}
239
+ onPrimaryAction={async () => {
240
+ await onTryAction(activeAction, true)
241
+ }}
242
+ >
243
+ {replace(activeAction.messages.prompt, list, { ...activeAction, itemLabel })}
244
+ </AlertDialog>
245
+ )}
246
+ </DialogContainer>
247
+
248
+ <DialogContainer onDismiss={() => setBlockedAction(null)}>
249
+ {blockedAction && (
250
+ <AlertDialog
251
+ title="Unsaved changes"
252
+ primaryActionLabel="Dismiss"
253
+ onPrimaryAction={() => setBlockedAction(null)}
254
+ >
255
+ Please save or reset your changes before attempting to{' '}
256
+ {replace('{label}', list, blockedAction)}.
257
+ </AlertDialog>
258
+ )}
259
+ </DialogContainer>
260
+
261
+ <DialogContainer onDismiss={() => setActionError(null)} isDismissable>
262
+ {actionError && (
263
+ <ErrorDetailsDialog
264
+ title={replace(actionError.action.messages.fail, list, {
265
+ ...actionError.action,
266
+ itemLabel,
267
+ })}
268
+ error={actionError.error}
269
+ />
270
+ )}
271
+ </DialogContainer>
272
+ </Fragment>
273
+ )
274
+ }
275
+
276
+ export function ColumnLayout(props: HTMLAttributes<HTMLDivElement>) {
277
+ return (
278
+ // this container must be relative to catch absolute children
279
+ // particularly the "expanded" document-field, which needs a height of 100%
280
+ <Container position="relative" height="100%" paddingX="xlarge">
281
+ <div
282
+ className={css({
283
+ display: 'grid',
284
+ columnGap: tokenSchema.size.space.xlarge,
285
+ gridTemplateAreas: '"main" "sidebar" "toolbar"',
286
+
287
+ [breakpointQueries.above.tablet]: {
288
+ gridTemplateColumns: `2fr minmax(${tokenSchema.size.scale[3600]}, 1fr)`,
289
+ gridTemplateAreas: '"main sidebar" "toolbar toolbar"',
290
+ },
291
+ })}
292
+ {...props}
293
+ />
294
+ </Container>
295
+ )
296
+ }
297
+
298
+ export function StickySidebar(props: HTMLAttributes<HTMLDivElement>) {
299
+ return (
300
+ <div
301
+ className={css({
302
+ gridArea: 'sidebar',
303
+ marginTop: tokenSchema.size.space.xlarge,
304
+
305
+ [breakpointQueries.above.tablet]: {
306
+ alignSelf: 'start',
307
+ marginBottom: tokenSchema.size.element.xlarge,
308
+ // sync with toolbar height
309
+ paddingBottom: tokenSchema.size.element.xlarge,
310
+ position: 'sticky',
311
+ top: tokenSchema.size.space.xlarge,
312
+ },
313
+ })}
314
+ {...props}
315
+ />
316
+ )
317
+ }
318
+
319
+ export function BaseToolbar(props: { children: ReactNode }) {
320
+ return (
321
+ <Grid
322
+ backgroundColor="surface"
323
+ columns="subgrid"
324
+ gridArea="toolbar"
325
+ insetBottom={0}
326
+ marginTop="xlarge"
327
+ position={{ tablet: 'sticky' }}
328
+ zIndex={20}
329
+ UNSAFE_className={css({
330
+ // fuzzy mask sidebar fields, which slide behind the un-bordered portion
331
+ // of the sticky toolbar
332
+ [breakpointQueries.above.tablet]: {
333
+ '&::after': {
334
+ boxShadow: `0 -4px 4px 1px ${tokenSchema.color.background.surface}`,
335
+ content: '""',
336
+ },
337
+ },
338
+ })}
339
+ >
340
+ <HStack
341
+ alignItems="center"
342
+ borderTop="neutral"
343
+ gap="regular"
344
+ height="element.xlarge"
345
+ UNSAFE_className={css({
346
+ // stretch horizontally to ensure field focus-rings are covered
347
+ [breakpointQueries.above.mobile]: {
348
+ backgroundColor: tokenSchema.color.background.surface,
349
+ marginInline: `calc(${tokenSchema.size.alias.focusRing} * -1)`,
350
+ paddingInline: tokenSchema.size.alias.focusRing,
351
+ },
352
+ })}
353
+ >
354
+ {props.children}
355
+ </HStack>
356
+ </Grid>
357
+ )
358
+ }