@nixxie-cms/core 1.0.0 → 1.0.1

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 (186) 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 +15 -15
  100. package/scripts/cli/dist/nixxie-cms-core-scripts-cli.esm.js +15 -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/no-access.ts +7 -7
  115. package/src/fields/types/bigInt/index.ts +181 -181
  116. package/src/fields/types/bytes/index.ts +275 -275
  117. package/src/fields/types/calendarDay/index.ts +194 -194
  118. package/src/fields/types/checkbox/index.ts +76 -76
  119. package/src/fields/types/decimal/index.ts +182 -182
  120. package/src/fields/types/file/index.ts +168 -168
  121. package/src/fields/types/float/index.ts +133 -133
  122. package/src/fields/types/image/index.ts +244 -244
  123. package/src/fields/types/integer/index.ts +156 -156
  124. package/src/fields/types/json/index.ts +77 -77
  125. package/src/fields/types/multiselect/index.ts +212 -212
  126. package/src/fields/types/password/index.ts +241 -241
  127. package/src/fields/types/relationship/index.ts +381 -381
  128. package/src/fields/types/relationship/views/RelationshipTable.tsx +190 -190
  129. package/src/fields/types/select/index.ts +226 -226
  130. package/src/fields/types/text/index.ts +207 -207
  131. package/src/fields/types/timestamp/index.ts +116 -116
  132. package/src/fields/types/virtual/index.ts +108 -108
  133. package/src/helpers.ts +342 -316
  134. package/src/index.ts +4 -0
  135. package/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/id-field-view.tsx +167 -167
  136. package/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/App/index.tsx +22 -22
  137. package/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/CreateItemPage/index.tsx +71 -71
  138. package/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/HomePage/index.tsx +333 -333
  139. package/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/ItemPage/common.tsx +358 -358
  140. package/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/ItemPage/index.tsx +483 -483
  141. package/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/ListPage/FilterAdd.tsx +221 -221
  142. package/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/ListPage/PaginationControls.tsx +170 -170
  143. package/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/ListPage/Tag.tsx +72 -72
  144. package/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/ListPage/index.tsx +1006 -1006
  145. package/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/NoAccessPage/index.tsx +24 -24
  146. package/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/artifacts.ts +5 -5
  147. package/src/lib/context/createContext.ts +165 -161
  148. package/src/lib/core/initialise-lists.ts +1097 -1097
  149. package/src/lib/id-field.ts +214 -214
  150. package/src/lib/telemetry.ts +342 -342
  151. package/src/schema.ts +237 -233
  152. package/src/scripts/telemetry.ts +1 -1
  153. package/src/types/config/index.ts +400 -333
  154. package/src/types/config/lists.ts +4 -4
  155. package/src/types/context.ts +700 -530
  156. package/src/types/next-fields.ts +499 -499
  157. package/src/types/telemetry.ts +51 -51
  158. package/tests/telemetry.test.ts +361 -361
  159. package/CHANGELOG.md +0 -3158
  160. package/___internal-do-not-use-will-break-in-patch/admin-ui/id-field-view/package.json +0 -4
  161. package/___internal-do-not-use-will-break-in-patch/admin-ui/next-config/package.json +0 -4
  162. package/___internal-do-not-use-will-break-in-patch/admin-ui/pages/App/package.json +0 -4
  163. package/___internal-do-not-use-will-break-in-patch/admin-ui/pages/CreateItemPage/package.json +0 -4
  164. package/___internal-do-not-use-will-break-in-patch/admin-ui/pages/HomePage/package.json +0 -4
  165. package/___internal-do-not-use-will-break-in-patch/admin-ui/pages/ItemPage/package.json +0 -4
  166. package/___internal-do-not-use-will-break-in-patch/admin-ui/pages/ListPage/package.json +0 -4
  167. package/___internal-do-not-use-will-break-in-patch/admin-ui/pages/NoAccessPage/package.json +0 -4
  168. package/___internal-do-not-use-will-break-in-patch/artifacts/package.json +0 -4
  169. package/dist/declarations/src/___internal-do-not-use-will-break-in-patch/admin-ui/id-field-view.d.ts.map +0 -1
  170. package/dist/declarations/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/App/index.d.ts.map +0 -1
  171. package/dist/declarations/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/CreateItemPage/index.d.ts.map +0 -1
  172. package/dist/declarations/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/HomePage/index.d.ts.map +0 -1
  173. package/dist/declarations/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/ItemPage/index.d.ts.map +0 -1
  174. package/dist/declarations/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/ListPage/index.d.ts.map +0 -1
  175. package/dist/declarations/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/NoAccessPage/index.d.ts.map +0 -1
  176. package/dist/declarations/src/___internal-do-not-use-will-break-in-patch/artifacts.d.ts.map +0 -1
  177. /package/dist/{common-1a350e11.cjs.js → common-5933f758.cjs.js} +0 -0
  178. /package/dist/{common-29fc82e6.esm.js → common-ea5c441a.esm.js} +0 -0
  179. /package/dist/declarations/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/id-field-view.d.ts +0 -0
  180. /package/dist/declarations/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/App/index.d.ts +0 -0
  181. /package/dist/declarations/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/CreateItemPage/index.d.ts +0 -0
  182. /package/dist/declarations/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/HomePage/index.d.ts +0 -0
  183. /package/dist/declarations/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/ItemPage/index.d.ts +0 -0
  184. /package/dist/declarations/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/ListPage/index.d.ts +0 -0
  185. /package/dist/declarations/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/NoAccessPage/index.d.ts +0 -0
  186. /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
+ }