@nixxie-cms/core 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/admin-ui/components/dist/nixxie-cms-core-admin-ui-components.cjs.js +7 -7
- package/admin-ui/components/dist/nixxie-cms-core-admin-ui-components.esm.js +7 -7
- package/admin-ui/context/dist/nixxie-cms-core-admin-ui-context.cjs.js +2 -2
- package/admin-ui/context/dist/nixxie-cms-core-admin-ui-context.esm.js +2 -2
- package/admin-ui/utils/dist/nixxie-cms-core-admin-ui-utils.cjs.js +5 -5
- package/admin-ui/utils/dist/nixxie-cms-core-admin-ui-utils.esm.js +3 -3
- package/dist/{CreateItemDialog-7008b050.esm.js → CreateItemDialog-66621fe8.esm.js} +3 -3
- package/dist/{CreateItemDialog-a0cab315.cjs.js → CreateItemDialog-96b044ce.cjs.js} +4 -4
- package/dist/{Field-47f85161.esm.js → Field-1820c4e6.esm.js} +1 -0
- package/dist/{Field-ed8d7627.cjs.js → Field-38d3cdf9.cjs.js} +1 -0
- package/dist/GraphQLErrorNotice-7594a9f8.esm.js +64 -0
- package/dist/GraphQLErrorNotice-c8890f80.cjs.js +66 -0
- package/dist/{PageContainer-5ae731cc.esm.js → PageContainer-355cfbfa.esm.js} +362 -156
- package/dist/{PageContainer-abd7159f.cjs.js → PageContainer-4095555a.cjs.js} +361 -155
- package/dist/{context-af9957ed.esm.js → context-2924eaaa.esm.js} +53 -58
- package/dist/{context-b5204629.cjs.js → context-2ce61d0b.cjs.js} +53 -58
- package/dist/declarations/src/admin-ui/components/GraphQLErrorNotice.d.ts.map +1 -1
- package/dist/declarations/src/admin-ui/components/Navigation.d.ts.map +1 -1
- package/dist/declarations/src/admin-ui/components/PageContainer.d.ts.map +1 -1
- package/dist/declarations/src/admin-ui/context.d.ts.map +1 -1
- package/dist/declarations/src/admin-ui/utils/useCreateItem.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/bigInt/views/index.d.ts +2 -1
- package/dist/declarations/src/fields/types/bigInt/views/index.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/bytes/views/index.d.ts +2 -1
- package/dist/declarations/src/fields/types/bytes/views/index.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/calendarDay/views/index.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/decimal/views/index.d.ts +2 -1
- package/dist/declarations/src/fields/types/decimal/views/index.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/float/views/index.d.ts +2 -1
- package/dist/declarations/src/fields/types/float/views/index.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/integer/views/index.d.ts +2 -1
- package/dist/declarations/src/fields/types/integer/views/index.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/json/views/index.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/multiselect/views/index.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/relationship/views/index.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/select/views/index.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/text/views/index.d.ts +2 -1
- package/dist/declarations/src/fields/types/text/views/index.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/timestamp/views/index.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/virtual/views/index.d.ts.map +1 -1
- package/dist/declarations/src/internal-unstable/admin-ui/pages/HomePage/index.d.ts.map +1 -1
- package/dist/declarations/src/internal-unstable/admin-ui/pages/ItemPage/index.d.ts.map +1 -1
- package/dist/pick-4c785a54.esm.js +34 -0
- package/dist/pick-906341bb.cjs.js +37 -0
- package/dist/{useCreateItem-1f94d252.esm.js → useCreateItem-36a75f1c.esm.js} +26 -26
- package/dist/{useCreateItem-1be4987e.cjs.js → useCreateItem-acf06f77.cjs.js} +37 -37
- package/dist/{useFilter-acc9d413.cjs.js → useFilter-c29f17a8.cjs.js} +1 -1
- package/dist/{useFilter-9b6db1f9.esm.js → useFilter-f79b2abb.esm.js} +1 -1
- package/dist/{Fields-956d9a14.esm.js → usePreventNavigation-093389dd.esm.js} +28 -2
- package/dist/{Fields-e2c28056.cjs.js → usePreventNavigation-d4f9f4fa.cjs.js} +27 -0
- package/fields/types/bigInt/views/dist/nixxie-cms-core-fields-types-bigInt-views.cjs.js +8 -0
- package/fields/types/bigInt/views/dist/nixxie-cms-core-fields-types-bigInt-views.esm.js +8 -1
- package/fields/types/bytes/views/dist/nixxie-cms-core-fields-types-bytes-views.cjs.js +14 -3
- package/fields/types/bytes/views/dist/nixxie-cms-core-fields-types-bytes-views.esm.js +15 -5
- package/fields/types/calendarDay/views/dist/nixxie-cms-core-fields-types-calendarDay-views.cjs.js +2 -1
- package/fields/types/calendarDay/views/dist/nixxie-cms-core-fields-types-calendarDay-views.esm.js +2 -1
- package/fields/types/decimal/views/dist/nixxie-cms-core-fields-types-decimal-views.cjs.js +10 -1
- package/fields/types/decimal/views/dist/nixxie-cms-core-fields-types-decimal-views.esm.js +10 -2
- package/fields/types/file/views/dist/nixxie-cms-core-fields-types-file-views.cjs.js +1 -1
- package/fields/types/file/views/dist/nixxie-cms-core-fields-types-file-views.esm.js +2 -2
- package/fields/types/float/views/dist/nixxie-cms-core-fields-types-float-views.cjs.js +10 -1
- package/fields/types/float/views/dist/nixxie-cms-core-fields-types-float-views.esm.js +10 -2
- package/fields/types/image/views/dist/nixxie-cms-core-fields-types-image-views.cjs.js +2 -1
- package/fields/types/image/views/dist/nixxie-cms-core-fields-types-image-views.esm.js +2 -1
- package/fields/types/integer/views/dist/nixxie-cms-core-fields-types-integer-views.cjs.js +8 -0
- package/fields/types/integer/views/dist/nixxie-cms-core-fields-types-integer-views.esm.js +8 -1
- package/fields/types/json/views/dist/nixxie-cms-core-fields-types-json-views.cjs.js +19 -4
- package/fields/types/json/views/dist/nixxie-cms-core-fields-types-json-views.esm.js +19 -4
- package/fields/types/multiselect/views/dist/nixxie-cms-core-fields-types-multiselect-views.cjs.js +18 -3
- package/fields/types/multiselect/views/dist/nixxie-cms-core-fields-types-multiselect-views.esm.js +18 -3
- package/fields/types/password/views/dist/nixxie-cms-core-fields-types-password-views.cjs.js +1 -1
- package/fields/types/password/views/dist/nixxie-cms-core-fields-types-password-views.esm.js +1 -1
- package/fields/types/relationship/views/dist/nixxie-cms-core-fields-types-relationship-views.cjs.js +9 -7
- package/fields/types/relationship/views/dist/nixxie-cms-core-fields-types-relationship-views.esm.js +9 -7
- package/fields/types/select/views/dist/nixxie-cms-core-fields-types-select-views.cjs.js +3 -2
- package/fields/types/select/views/dist/nixxie-cms-core-fields-types-select-views.esm.js +3 -2
- package/fields/types/text/views/dist/nixxie-cms-core-fields-types-text-views.cjs.js +14 -3
- package/fields/types/text/views/dist/nixxie-cms-core-fields-types-text-views.esm.js +15 -5
- package/fields/types/timestamp/views/dist/nixxie-cms-core-fields-types-timestamp-views.cjs.js +2 -1
- package/fields/types/timestamp/views/dist/nixxie-cms-core-fields-types-timestamp-views.esm.js +2 -1
- package/fields/types/virtual/views/dist/nixxie-cms-core-fields-types-virtual-views.cjs.js +13 -1
- package/fields/types/virtual/views/dist/nixxie-cms-core-fields-types-virtual-views.esm.js +14 -2
- package/internal-unstable/admin-ui/pages/App/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-App.cjs.js +3 -3
- package/internal-unstable/admin-ui/pages/App/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-App.esm.js +3 -3
- package/internal-unstable/admin-ui/pages/CreateItemPage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-CreateItemPage.cjs.js +7 -7
- package/internal-unstable/admin-ui/pages/CreateItemPage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-CreateItemPage.esm.js +6 -6
- package/internal-unstable/admin-ui/pages/HomePage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-HomePage.cjs.js +34 -33
- package/internal-unstable/admin-ui/pages/HomePage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-HomePage.esm.js +35 -34
- package/internal-unstable/admin-ui/pages/ItemPage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-ItemPage.cjs.js +53 -13
- package/internal-unstable/admin-ui/pages/ItemPage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-ItemPage.esm.js +52 -12
- package/internal-unstable/admin-ui/pages/ListPage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-ListPage.cjs.js +36 -25
- package/internal-unstable/admin-ui/pages/ListPage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-ListPage.esm.js +36 -25
- package/package.json +2 -2
- package/src/admin-ui/components/CommandPalette.tsx +134 -27
- package/src/admin-ui/components/CreateButtonLink.tsx +20 -46
- package/src/admin-ui/components/GraphQLErrorNotice.tsx +39 -33
- package/src/admin-ui/components/Logo.tsx +5 -5
- package/src/admin-ui/components/Navigation.tsx +41 -27
- package/src/admin-ui/components/PageContainer.tsx +171 -15
- package/src/admin-ui/components/WelcomeDialog.tsx +14 -14
- package/src/admin-ui/context.tsx +5 -2
- package/src/admin-ui/utils/useCreateItem.ts +21 -1
- package/src/fields/types/bigInt/views/index.tsx +10 -1
- package/src/fields/types/bytes/views/index.tsx +14 -1
- package/src/fields/types/calendarDay/views/index.tsx +2 -1
- package/src/fields/types/decimal/views/index.tsx +7 -1
- package/src/fields/types/file/views/Field.tsx +1 -1
- package/src/fields/types/float/views/index.tsx +7 -1
- package/src/fields/types/image/views/index.tsx +1 -1
- package/src/fields/types/integer/views/index.tsx +5 -0
- package/src/fields/types/json/views/index.tsx +20 -2
- package/src/fields/types/multiselect/views/index.tsx +7 -3
- package/src/fields/types/password/views/index.tsx +1 -1
- package/src/fields/types/relationship/views/index.tsx +1 -0
- package/src/fields/types/select/views/index.tsx +2 -1
- package/src/fields/types/text/views/index.tsx +14 -1
- package/src/fields/types/timestamp/views/index.tsx +2 -1
- package/src/fields/types/virtual/views/index.tsx +17 -2
- package/src/internal-unstable/admin-ui/pages/HomePage/index.tsx +40 -31
- package/src/internal-unstable/admin-ui/pages/ItemPage/index.tsx +36 -3
- package/src/internal-unstable/admin-ui/pages/ListPage/PaginationControls.tsx +20 -33
- package/src/internal-unstable/admin-ui/pages/ListPage/index.tsx +24 -16
- package/dist/GraphQLErrorNotice-cd74180d.cjs.js +0 -57
- package/dist/GraphQLErrorNotice-d9f0931b.esm.js +0 -55
- package/dist/pick-5fe45878.cjs.js +0 -71
- package/dist/pick-b7ef3115.esm.js +0 -68
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { useState } from 'react'
|
|
2
2
|
|
|
3
3
|
import { TextField } from '@keystar/ui/text-field'
|
|
4
|
+
import { Text } from '@keystar/ui/typography'
|
|
4
5
|
|
|
5
6
|
import {
|
|
7
|
+
type CellComponent,
|
|
6
8
|
type FieldController,
|
|
7
9
|
type FieldControllerConfig,
|
|
8
10
|
type FieldProps,
|
|
@@ -205,7 +207,7 @@ export function Field({
|
|
|
205
207
|
errorMessage={(forceValidation || isDirty) && validate(value)}
|
|
206
208
|
isReadOnly={isReadOnly}
|
|
207
209
|
isRequired={isRequired}
|
|
208
|
-
inputMode="
|
|
210
|
+
inputMode="decimal"
|
|
209
211
|
width="alias.singleLineWidth"
|
|
210
212
|
onBlur={() => setDirty(true)}
|
|
211
213
|
onChange={x => onChange?.({ ...value, value: x === '' ? null : x })}
|
|
@@ -213,3 +215,7 @@ export function Field({
|
|
|
213
215
|
/>
|
|
214
216
|
)
|
|
215
217
|
}
|
|
218
|
+
|
|
219
|
+
export const Cell: CellComponent<typeof controller> = ({ value }) => {
|
|
220
|
+
return value != null && value !== '' ? <Text>{value}</Text> : null
|
|
221
|
+
}
|
|
@@ -164,7 +164,7 @@ function FileDetails(props: PropsWithChildren<FileData>) {
|
|
|
164
164
|
gap="medium"
|
|
165
165
|
padding="medium"
|
|
166
166
|
>
|
|
167
|
-
<a href={props.href} download={props.name}>
|
|
167
|
+
<a href={props.href} download={props.name} aria-label={`Download ${props.name}`}>
|
|
168
168
|
<HStack
|
|
169
169
|
alignItems="center"
|
|
170
170
|
backgroundColor="surfaceTertiary"
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { useState } from 'react'
|
|
2
2
|
|
|
3
3
|
import { TextField } from '@keystar/ui/text-field'
|
|
4
|
+
import { Text } from '@keystar/ui/typography'
|
|
4
5
|
|
|
5
6
|
import type {
|
|
7
|
+
CellComponent,
|
|
6
8
|
FieldController,
|
|
7
9
|
FieldControllerConfig,
|
|
8
10
|
FieldProps,
|
|
@@ -205,7 +207,7 @@ export function Field({
|
|
|
205
207
|
errorMessage={(forceValidation || isDirty) && validate(value)}
|
|
206
208
|
isReadOnly={isReadOnly}
|
|
207
209
|
isRequired={isRequired}
|
|
208
|
-
inputMode="
|
|
210
|
+
inputMode="decimal"
|
|
209
211
|
width="alias.singleLineWidth"
|
|
210
212
|
onBlur={() => setDirty(true)}
|
|
211
213
|
onChange={x => onChange?.({ ...value, value: x === '' ? null : x })}
|
|
@@ -213,3 +215,7 @@ export function Field({
|
|
|
213
215
|
/>
|
|
214
216
|
)
|
|
215
217
|
}
|
|
218
|
+
|
|
219
|
+
export const Cell: CellComponent<typeof controller> = ({ value }) => {
|
|
220
|
+
return value != null && value !== '' ? <Text>{value}</Text> : null
|
|
221
|
+
}
|
|
@@ -15,7 +15,7 @@ export const Cell: CellComponent<typeof controller> = ({ value }) => {
|
|
|
15
15
|
width: 24,
|
|
16
16
|
}}
|
|
17
17
|
>
|
|
18
|
-
<img style={{ maxHeight: '100%', maxWidth: '100%' }} src={value.url} />
|
|
18
|
+
<img alt="" style={{ maxHeight: '100%', maxWidth: '100%' }} src={value.url} />
|
|
19
19
|
</div>
|
|
20
20
|
)
|
|
21
21
|
}
|
|
@@ -7,6 +7,7 @@ import { NumberField } from '@keystar/ui/number-field'
|
|
|
7
7
|
import { Heading, Text } from '@keystar/ui/typography'
|
|
8
8
|
|
|
9
9
|
import type {
|
|
10
|
+
CellComponent,
|
|
10
11
|
FieldController,
|
|
11
12
|
FieldControllerConfig,
|
|
12
13
|
FieldProps,
|
|
@@ -253,3 +254,7 @@ export function Field({
|
|
|
253
254
|
/>
|
|
254
255
|
)
|
|
255
256
|
}
|
|
257
|
+
|
|
258
|
+
export const Cell: CellComponent<typeof controller> = ({ value }) => {
|
|
259
|
+
return typeof value === 'number' && Number.isFinite(value) ? <Text>{value}</Text> : null
|
|
260
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
|
|
1
3
|
import { css, tokenSchema } from '@keystar/ui/style'
|
|
2
4
|
import { TextArea } from '@keystar/ui/text-field'
|
|
3
5
|
import { Text } from '@keystar/ui/typography'
|
|
@@ -12,7 +14,9 @@ import type {
|
|
|
12
14
|
|
|
13
15
|
export const Field = (props: FieldProps<typeof controller>) => {
|
|
14
16
|
const { autoFocus, field, forceValidation, onChange, value } = props
|
|
15
|
-
const
|
|
17
|
+
const [isDirty, setDirty] = useState(false)
|
|
18
|
+
const errorMessage =
|
|
19
|
+
(isDirty || forceValidation) && !isValidJSON(value) ? 'Invalid JSON' : undefined
|
|
16
20
|
|
|
17
21
|
return (
|
|
18
22
|
<TextArea
|
|
@@ -22,6 +26,7 @@ export const Field = (props: FieldProps<typeof controller>) => {
|
|
|
22
26
|
isReadOnly={onChange === undefined}
|
|
23
27
|
label={field.label}
|
|
24
28
|
onChange={onChange}
|
|
29
|
+
onBlur={() => setDirty(true)}
|
|
25
30
|
value={value}
|
|
26
31
|
UNSAFE_className={css({
|
|
27
32
|
textarea: {
|
|
@@ -33,8 +38,21 @@ export const Field = (props: FieldProps<typeof controller>) => {
|
|
|
33
38
|
)
|
|
34
39
|
}
|
|
35
40
|
|
|
41
|
+
function isValidJSON(value: string) {
|
|
42
|
+
if (!value) return true
|
|
43
|
+
try {
|
|
44
|
+
JSON.parse(value)
|
|
45
|
+
return true
|
|
46
|
+
} catch {
|
|
47
|
+
return false
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
36
51
|
export const Cell: CellComponent<typeof controller> = ({ value }) => {
|
|
37
|
-
|
|
52
|
+
if (value == null) return null
|
|
53
|
+
const stringified = JSON.stringify(value)
|
|
54
|
+
const truncated = stringified.length > 100 ? `${stringified.slice(0, 100)}…` : stringified
|
|
55
|
+
return <Text>{truncated}</Text>
|
|
38
56
|
}
|
|
39
57
|
|
|
40
58
|
type Config = FieldControllerConfig<{ defaultValue: JSONValue }>
|
|
@@ -89,7 +89,7 @@ function CheckboxesModeField(props: FieldProps<typeof controller>) {
|
|
|
89
89
|
|
|
90
90
|
export const Cell: CellComponent<typeof controller> = ({ value = [], field }) => {
|
|
91
91
|
const listFormatter = useListFormatter({ style: 'short', type: 'conjunction' })
|
|
92
|
-
const labels = (value as string[]).map(x => field.valuesToOptionsWithStringValues[x]
|
|
92
|
+
const labels = (value as string[]).map(x => field.valuesToOptionsWithStringValues[x]?.label ?? x)
|
|
93
93
|
|
|
94
94
|
const cellContent =
|
|
95
95
|
value.length > 3
|
|
@@ -135,12 +135,16 @@ export function controller(config: Config): FieldController<Value, Option[]> & {
|
|
|
135
135
|
type: config.fieldMeta.type,
|
|
136
136
|
options: optionsWithStringValues,
|
|
137
137
|
valuesToOptionsWithStringValues,
|
|
138
|
-
defaultValue: config.fieldMeta.defaultValue.map(
|
|
138
|
+
defaultValue: config.fieldMeta.defaultValue.map(
|
|
139
|
+
x => valuesToOptionsWithStringValues[x] ?? { label: x.toString(), value: x.toString() }
|
|
140
|
+
),
|
|
139
141
|
deserialize: data => {
|
|
140
142
|
// if we get null from the GraphQL API (which will only happen if field read access control failed)
|
|
141
143
|
// we'll just show it as nothing being selected for now.
|
|
142
144
|
const values: readonly string[] | readonly number[] = data[config.fieldKey] ?? []
|
|
143
|
-
const selectedOptions = values.map(
|
|
145
|
+
const selectedOptions = values.map(
|
|
146
|
+
x => valuesToOptionsWithStringValues[x] ?? { label: x.toString(), value: x.toString() }
|
|
147
|
+
)
|
|
144
148
|
return selectedOptions
|
|
145
149
|
},
|
|
146
150
|
serialize: value => ({ [config.fieldKey]: value.map(x => parseValue(x.value)) }),
|
|
@@ -181,7 +181,7 @@ export function Field(props: FieldProps<typeof controller>) {
|
|
|
181
181
|
|
|
182
182
|
<Flex gap="regular">
|
|
183
183
|
<ToggleButton
|
|
184
|
-
aria-label=
|
|
184
|
+
aria-label={secureTextEntry ? 'Show password' : 'Hide password'}
|
|
185
185
|
isSelected={!secureTextEntry}
|
|
186
186
|
onPress={() => setSecureTextEntry(bool => !bool)}
|
|
187
187
|
>
|
|
@@ -58,6 +58,7 @@ export function Field(props: FieldProps<typeof controller>) {
|
|
|
58
58
|
<HStack gap="small" alignItems="end">
|
|
59
59
|
{textField}
|
|
60
60
|
<ActionButton
|
|
61
|
+
aria-label={`Go to ${field.label}`}
|
|
61
62
|
href={`/${foreignList.path}?${buildQueryForRelationshipFieldWithForeignField(foreignList, field.refFieldKey, value.id)}`}
|
|
62
63
|
>
|
|
63
64
|
<Icon src={arrowUpRightIcon} />
|
|
@@ -148,8 +148,9 @@ export function Field(props: FieldProps<typeof controller>) {
|
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
export const Cell: CellComponent<typeof controller> = ({ value, field }) => {
|
|
151
|
+
if (value == null) return null
|
|
151
152
|
const label = field.options.find(x => x.value === value)?.label
|
|
152
|
-
return <Text>{label}</Text>
|
|
153
|
+
return <Text>{label ?? String(value)}</Text>
|
|
153
154
|
}
|
|
154
155
|
|
|
155
156
|
export type AdminSelectFieldMeta = {
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { useState } from 'react'
|
|
2
2
|
import { TextArea, TextField } from '@keystar/ui/text-field'
|
|
3
|
+
import { Text } from '@keystar/ui/typography'
|
|
3
4
|
|
|
4
5
|
import type { TextFieldMeta } from '..'
|
|
5
6
|
import { NullableFieldWrapper } from '../../../../admin-ui/components'
|
|
6
7
|
import { entriesTyped } from '../../../../lib/core/utils'
|
|
7
8
|
import type {
|
|
9
|
+
CellComponent,
|
|
8
10
|
FieldController,
|
|
9
11
|
FieldControllerConfig,
|
|
10
12
|
FieldProps,
|
|
@@ -46,7 +48,14 @@ export function Field(props: FieldProps<typeof controller>) {
|
|
|
46
48
|
label={field.label}
|
|
47
49
|
errorMessage={
|
|
48
50
|
!!validationMessages.length && (shouldShowErrors || forceValidation)
|
|
49
|
-
? validationMessages.
|
|
51
|
+
? validationMessages.length === 1
|
|
52
|
+
? validationMessages[0]
|
|
53
|
+
: validationMessages.map((message, i) => (
|
|
54
|
+
<Text key={i}>
|
|
55
|
+
{i > 0 && <br />}
|
|
56
|
+
{message}
|
|
57
|
+
</Text>
|
|
58
|
+
))
|
|
50
59
|
: undefined
|
|
51
60
|
}
|
|
52
61
|
isDisabled={isNull}
|
|
@@ -73,6 +82,10 @@ export function Field(props: FieldProps<typeof controller>) {
|
|
|
73
82
|
)
|
|
74
83
|
}
|
|
75
84
|
|
|
85
|
+
export const Cell: CellComponent<typeof controller> = ({ value }) => {
|
|
86
|
+
return value ? <Text truncate>{value}</Text> : null
|
|
87
|
+
}
|
|
88
|
+
|
|
76
89
|
type Config = FieldControllerConfig<TextFieldMeta>
|
|
77
90
|
|
|
78
91
|
type Validation = {
|
|
@@ -44,12 +44,13 @@ export function Field(props: FieldProps<typeof controller>) {
|
|
|
44
44
|
description={field.description}
|
|
45
45
|
isDisabled={!parsedValue}
|
|
46
46
|
isReadOnly
|
|
47
|
+
placeholder="yyyy-mm-dd --:--:--"
|
|
47
48
|
value={
|
|
48
49
|
parsedValue
|
|
49
50
|
? isReadonlyUTC
|
|
50
51
|
? parsedValue.toAbsoluteString()
|
|
51
52
|
: dateFormatter.format(parsedValue.toDate())
|
|
52
|
-
: '
|
|
53
|
+
: ''
|
|
53
54
|
}
|
|
54
55
|
/>
|
|
55
56
|
{!!parsedValue && (
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TextField } from '@keystar/ui/text-field'
|
|
1
|
+
import { TextArea, TextField } from '@keystar/ui/text-field'
|
|
2
2
|
import { Text } from '@keystar/ui/typography'
|
|
3
3
|
import type {
|
|
4
4
|
CellComponent,
|
|
@@ -21,13 +21,28 @@ export function Field(props: FieldProps<typeof controller>) {
|
|
|
21
21
|
const { autoFocus, field, value } = props
|
|
22
22
|
if (value === createViewValue) return null
|
|
23
23
|
|
|
24
|
+
const stringified = stringify(value)
|
|
25
|
+
// multi-line values (e.g. pretty-printed JSON) get clipped in a single-line
|
|
26
|
+
// TextField, so render them in a read-only TextArea instead
|
|
27
|
+
if (stringified.includes('\n')) {
|
|
28
|
+
return (
|
|
29
|
+
<TextArea
|
|
30
|
+
autoFocus={autoFocus}
|
|
31
|
+
description={field.description}
|
|
32
|
+
label={field.label}
|
|
33
|
+
isReadOnly={true}
|
|
34
|
+
value={stringified}
|
|
35
|
+
/>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
24
39
|
return (
|
|
25
40
|
<TextField
|
|
26
41
|
autoFocus={autoFocus}
|
|
27
42
|
description={field.description}
|
|
28
43
|
label={field.label}
|
|
29
44
|
isReadOnly={true}
|
|
30
|
-
value={
|
|
45
|
+
value={stringified}
|
|
31
46
|
/>
|
|
32
47
|
)
|
|
33
48
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useMemo } from 'react'
|
|
2
2
|
|
|
3
|
-
import { css } from '@keystar/ui/style'
|
|
3
|
+
import { css, tokenSchema } from '@keystar/ui/style'
|
|
4
4
|
import { Heading } from '@keystar/ui/typography'
|
|
5
5
|
|
|
6
6
|
import { gql, type TypedDocumentNode, useQuery } from '../../../../admin-ui/apollo'
|
|
@@ -24,14 +24,14 @@ function CollectionCard({ listKey, count, href, createHref }: CardProps) {
|
|
|
24
24
|
<div
|
|
25
25
|
className={css({
|
|
26
26
|
position: 'relative',
|
|
27
|
-
backgroundColor:
|
|
28
|
-
border:
|
|
27
|
+
backgroundColor: tokenSchema.color.background.canvas,
|
|
28
|
+
border: `1px solid ${tokenSchema.color.border.muted}`,
|
|
29
29
|
borderRadius: 8,
|
|
30
30
|
overflow: 'hidden',
|
|
31
31
|
transition: 'box-shadow 180ms, border-color 180ms',
|
|
32
32
|
'&:hover': {
|
|
33
|
-
boxShadow:
|
|
34
|
-
borderColor:
|
|
33
|
+
boxShadow: `0 2px 12px ${tokenSchema.color.shadow.muted}`,
|
|
34
|
+
borderColor: tokenSchema.color.border.emphasis,
|
|
35
35
|
},
|
|
36
36
|
})}
|
|
37
37
|
>
|
|
@@ -44,7 +44,7 @@ function CollectionCard({ listKey, count, href, createHref }: CardProps) {
|
|
|
44
44
|
fontWeight: 600,
|
|
45
45
|
letterSpacing: '0.09em',
|
|
46
46
|
textTransform: 'uppercase',
|
|
47
|
-
color:
|
|
47
|
+
color: tokenSchema.color.foreground.neutralSecondary,
|
|
48
48
|
display: 'flex',
|
|
49
49
|
alignItems: 'center',
|
|
50
50
|
justifyContent: 'space-between',
|
|
@@ -69,11 +69,11 @@ function CollectionCard({ listKey, count, href, createHref }: CardProps) {
|
|
|
69
69
|
display: 'inline-flex',
|
|
70
70
|
alignItems: 'center',
|
|
71
71
|
justifyContent: 'center',
|
|
72
|
-
width:
|
|
73
|
-
height:
|
|
72
|
+
width: 28,
|
|
73
|
+
height: 28,
|
|
74
74
|
borderRadius: 5,
|
|
75
|
-
border:
|
|
76
|
-
color:
|
|
75
|
+
border: `1px solid ${tokenSchema.color.border.muted}`,
|
|
76
|
+
color: tokenSchema.color.foreground.neutralSecondary,
|
|
77
77
|
textDecoration: 'none',
|
|
78
78
|
fontSize: 14,
|
|
79
79
|
lineHeight: 1,
|
|
@@ -81,9 +81,9 @@ function CollectionCard({ listKey, count, href, createHref }: CardProps) {
|
|
|
81
81
|
marginLeft: 6,
|
|
82
82
|
transition: 'border-color 130ms, color 130ms, background 130ms',
|
|
83
83
|
'&:hover': {
|
|
84
|
-
borderColor:
|
|
85
|
-
color:
|
|
86
|
-
background:
|
|
84
|
+
borderColor: tokenSchema.color.scale.black,
|
|
85
|
+
color: tokenSchema.color.foreground.neutralEmphasis,
|
|
86
|
+
background: tokenSchema.color.background.surfaceSecondary,
|
|
87
87
|
},
|
|
88
88
|
})}
|
|
89
89
|
>
|
|
@@ -111,7 +111,10 @@ function CollectionCard({ listKey, count, href, createHref }: CardProps) {
|
|
|
111
111
|
fontSize: 34,
|
|
112
112
|
fontWeight: 700,
|
|
113
113
|
letterSpacing: '-0.05em',
|
|
114
|
-
color:
|
|
114
|
+
color:
|
|
115
|
+
count === null
|
|
116
|
+
? tokenSchema.color.border.muted
|
|
117
|
+
: tokenSchema.color.foreground.neutralEmphasis,
|
|
115
118
|
lineHeight: 1,
|
|
116
119
|
fontVariantNumeric: 'tabular-nums',
|
|
117
120
|
})}
|
|
@@ -123,12 +126,12 @@ function CollectionCard({ listKey, count, href, createHref }: CardProps) {
|
|
|
123
126
|
className={css({
|
|
124
127
|
margin: 0,
|
|
125
128
|
fontSize: 12,
|
|
126
|
-
color:
|
|
129
|
+
color: tokenSchema.color.foreground.neutralSecondary,
|
|
127
130
|
display: 'flex',
|
|
128
131
|
alignItems: 'center',
|
|
129
132
|
gap: 4,
|
|
130
133
|
transition: 'color 130ms',
|
|
131
|
-
'a:hover &': { color:
|
|
134
|
+
'a:hover &': { color: tokenSchema.color.foreground.neutralEmphasis },
|
|
132
135
|
})}
|
|
133
136
|
>
|
|
134
137
|
{list.isSingleton ? 'Open' : 'View all'}
|
|
@@ -167,7 +170,7 @@ function QuickCreate() {
|
|
|
167
170
|
fontWeight: 600,
|
|
168
171
|
letterSpacing: '0.10em',
|
|
169
172
|
textTransform: 'uppercase',
|
|
170
|
-
color:
|
|
173
|
+
color: tokenSchema.color.foreground.neutralSecondary,
|
|
171
174
|
})}
|
|
172
175
|
>
|
|
173
176
|
Quick Create
|
|
@@ -184,18 +187,18 @@ function QuickCreate() {
|
|
|
184
187
|
paddingInline: '12px',
|
|
185
188
|
paddingBlock: '6px',
|
|
186
189
|
borderRadius: 6,
|
|
187
|
-
border:
|
|
188
|
-
backgroundColor:
|
|
190
|
+
border: `1px solid ${tokenSchema.color.border.muted}`,
|
|
191
|
+
backgroundColor: tokenSchema.color.background.canvas,
|
|
189
192
|
fontSize: 12.5,
|
|
190
193
|
fontWeight: 500,
|
|
191
|
-
color:
|
|
194
|
+
color: tokenSchema.color.foreground.neutral,
|
|
192
195
|
textDecoration: 'none',
|
|
193
196
|
transition: 'border-color 130ms, color 130ms, background 130ms',
|
|
194
197
|
|
|
195
198
|
'&:hover': {
|
|
196
|
-
borderColor:
|
|
197
|
-
color:
|
|
198
|
-
background:
|
|
199
|
+
borderColor: tokenSchema.color.scale.black,
|
|
200
|
+
color: tokenSchema.color.foreground.neutralEmphasis,
|
|
201
|
+
background: tokenSchema.color.background.surface,
|
|
199
202
|
},
|
|
200
203
|
})}
|
|
201
204
|
>
|
|
@@ -260,18 +263,24 @@ export function HomePage() {
|
|
|
260
263
|
|
|
261
264
|
{/* Page title */}
|
|
262
265
|
<div className={css({ marginBottom: 28 })}>
|
|
263
|
-
<
|
|
266
|
+
<h2
|
|
264
267
|
className={css({
|
|
265
268
|
margin: '0 0 4px',
|
|
266
269
|
fontSize: 20,
|
|
267
270
|
fontWeight: 700,
|
|
268
271
|
letterSpacing: '-0.03em',
|
|
269
|
-
color:
|
|
272
|
+
color: tokenSchema.color.foreground.neutralEmphasis,
|
|
270
273
|
})}
|
|
271
274
|
>
|
|
272
275
|
Overview
|
|
273
|
-
</
|
|
274
|
-
<p
|
|
276
|
+
</h2>
|
|
277
|
+
<p
|
|
278
|
+
className={css({
|
|
279
|
+
margin: 0,
|
|
280
|
+
fontSize: 13,
|
|
281
|
+
color: tokenSchema.color.foreground.neutralSecondary,
|
|
282
|
+
})}
|
|
283
|
+
>
|
|
275
284
|
{visibleLists.length} collection{visibleLists.length !== 1 ? 's' : ''}
|
|
276
285
|
</p>
|
|
277
286
|
</div>
|
|
@@ -305,7 +314,7 @@ export function HomePage() {
|
|
|
305
314
|
className={css({
|
|
306
315
|
marginTop: 48,
|
|
307
316
|
fontSize: 11.5,
|
|
308
|
-
color:
|
|
317
|
+
color: tokenSchema.color.foreground.neutralSecondary,
|
|
309
318
|
display: 'flex',
|
|
310
319
|
alignItems: 'center',
|
|
311
320
|
gap: 6,
|
|
@@ -315,12 +324,12 @@ export function HomePage() {
|
|
|
315
324
|
className={css({
|
|
316
325
|
display: 'inline-flex',
|
|
317
326
|
padding: '1px 6px',
|
|
318
|
-
background:
|
|
319
|
-
border:
|
|
327
|
+
background: tokenSchema.color.background.canvas,
|
|
328
|
+
border: `1px solid ${tokenSchema.color.border.muted}`,
|
|
320
329
|
borderRadius: 4,
|
|
321
330
|
fontSize: 10.5,
|
|
322
331
|
fontFamily: 'inherit',
|
|
323
|
-
color:
|
|
332
|
+
color: tokenSchema.color.foreground.neutralSecondary,
|
|
324
333
|
})}
|
|
325
334
|
>
|
|
326
335
|
⌘K
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
useInvalidFields,
|
|
39
39
|
} from '../../../../admin-ui/utils'
|
|
40
40
|
import { pick } from '../../../../admin-ui/utils/pick'
|
|
41
|
+
import { usePreventNavigation } from '../../../../admin-ui/utils/usePreventNavigation'
|
|
41
42
|
import type {
|
|
42
43
|
ActionMeta,
|
|
43
44
|
BaseCollectionTypeInfo,
|
|
@@ -62,6 +63,22 @@ function useEventCallback<Func extends (...args: any[]) => unknown>(callback: Fu
|
|
|
62
63
|
return cb as any
|
|
63
64
|
}
|
|
64
65
|
|
|
66
|
+
// after forcing validation, move focus to the first invalid field so the user
|
|
67
|
+
// can see and fix it. runs on the next frame so the DOM reflects the new
|
|
68
|
+
// aria-invalid state, and is fully defensive so it can never throw.
|
|
69
|
+
function focusFirstInvalidField() {
|
|
70
|
+
requestAnimationFrame(() => {
|
|
71
|
+
try {
|
|
72
|
+
const el = document.querySelector<HTMLElement>('[aria-invalid="true"]')
|
|
73
|
+
if (!el) return
|
|
74
|
+
el.focus({ preventScroll: false })
|
|
75
|
+
el.scrollIntoView({ block: 'center', behavior: 'smooth' })
|
|
76
|
+
} catch {
|
|
77
|
+
// ignore — focusing is a best-effort enhancement
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
|
|
65
82
|
function DeleteButton({
|
|
66
83
|
list,
|
|
67
84
|
itemId,
|
|
@@ -155,7 +172,6 @@ function ResetButton(props: { onReset: () => void; hasChanges?: boolean }) {
|
|
|
155
172
|
title="Reset changes"
|
|
156
173
|
cancelLabel="Cancel"
|
|
157
174
|
primaryActionLabel="Yes, reset"
|
|
158
|
-
autoFocusButton="primary"
|
|
159
175
|
onPrimaryAction={props.onReset}
|
|
160
176
|
>
|
|
161
177
|
Are you sure? Any unsaved changes will be lost and cannot be recovered.
|
|
@@ -211,7 +227,11 @@ function ItemForm({
|
|
|
211
227
|
e.preventDefault()
|
|
212
228
|
const newForceValidation = invalidFields.size !== 0
|
|
213
229
|
setForceValidation(newForceValidation)
|
|
214
|
-
if (newForceValidation)
|
|
230
|
+
if (newForceValidation) {
|
|
231
|
+
toastQueue.critical('Please fix the highlighted field(s) before saving.')
|
|
232
|
+
focusFirstInvalidField()
|
|
233
|
+
return
|
|
234
|
+
}
|
|
215
235
|
|
|
216
236
|
const { error: _error } = await update({
|
|
217
237
|
variables: {
|
|
@@ -236,10 +256,18 @@ function ItemForm({
|
|
|
236
256
|
timeout: 5000,
|
|
237
257
|
})
|
|
238
258
|
|
|
259
|
+
// reset the navigation guard before refetch so saving and then navigating
|
|
260
|
+
// away doesn't prompt for unsaved changes
|
|
261
|
+
shouldPreventNavigationRef.current = false
|
|
239
262
|
onSaveSuccess()
|
|
240
263
|
})
|
|
241
264
|
|
|
242
265
|
const hasChangedFields = useHasChanges('update', list.fields, value, initialValue)
|
|
266
|
+
const shouldPreventNavigationRef = useRef(hasChangedFields)
|
|
267
|
+
useEffect(() => {
|
|
268
|
+
shouldPreventNavigationRef.current = hasChangedFields
|
|
269
|
+
}, [hasChangedFields])
|
|
270
|
+
usePreventNavigation(shouldPreventNavigationRef)
|
|
243
271
|
|
|
244
272
|
return (
|
|
245
273
|
<Fragment>
|
|
@@ -324,7 +352,12 @@ function ItemPage({ listKey }: ItemPageProps) {
|
|
|
324
352
|
const itemLabel_ = item?.[list.labelField] ?? item?.id
|
|
325
353
|
const itemLabel = typeof itemLabel_ === 'string' ? itemLabel_ : (itemId ?? '')
|
|
326
354
|
|
|
327
|
-
|
|
355
|
+
// Only show the full-page spinner on the INITIAL load (no data yet). On a
|
|
356
|
+
// post-save refetch Apollo keeps `data` populated while `loading` flips back
|
|
357
|
+
// to true; gating on `!data` keeps the form mounted (no scroll reset / flash)
|
|
358
|
+
// during background refetches while still showing the spinner before the
|
|
359
|
+
// first result arrives.
|
|
360
|
+
const pageLoading = (loading && !data) || itemId === undefined
|
|
328
361
|
const pageLabel = itemLabel || itemId
|
|
329
362
|
const pageTitle = list.isSingleton || typeof pageLabel !== 'string' ? list.label : pageLabel
|
|
330
363
|
const initialValue = useMemo(() => {
|
|
@@ -4,17 +4,11 @@ import { chevronLeftIcon } from '@keystar/ui/icon/icons/chevronLeftIcon'
|
|
|
4
4
|
import { chevronRightIcon } from '@keystar/ui/icon/icons/chevronRightIcon'
|
|
5
5
|
import { undo2Icon } from '@keystar/ui/icon/icons/undo2Icon'
|
|
6
6
|
import { HStack } from '@keystar/ui/layout'
|
|
7
|
-
import {
|
|
8
|
-
import { Item } from '@keystar/ui/
|
|
7
|
+
import { NumberField } from '@keystar/ui/number-field'
|
|
8
|
+
import { Item, Picker } from '@keystar/ui/picker'
|
|
9
9
|
import { Tooltip, TooltipTrigger } from '@keystar/ui/tooltip'
|
|
10
10
|
import { Text } from '@keystar/ui/typography'
|
|
11
11
|
import type { ReactNode } from 'react'
|
|
12
|
-
import { useMemo } from 'react'
|
|
13
|
-
|
|
14
|
-
type PageItem = {
|
|
15
|
-
label: string
|
|
16
|
-
id: number
|
|
17
|
-
}
|
|
18
12
|
|
|
19
13
|
export function PaginationControls(props: {
|
|
20
14
|
singular: string
|
|
@@ -34,16 +28,11 @@ export function PaginationControls(props: {
|
|
|
34
28
|
const prevPage = currentPage - 1
|
|
35
29
|
const lastPage = Math.max(Math.ceil(total / pageSize), 1)
|
|
36
30
|
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
label: String(page),
|
|
43
|
-
})
|
|
44
|
-
}
|
|
45
|
-
return result
|
|
46
|
-
}, [lastPage])
|
|
31
|
+
const goToPage = (value: number) => {
|
|
32
|
+
if (!Number.isFinite(value)) return
|
|
33
|
+
const page = Math.min(Math.max(Math.round(value), 1), lastPage)
|
|
34
|
+
if (page !== currentPage) props.onChangePage(page)
|
|
35
|
+
}
|
|
47
36
|
|
|
48
37
|
return (
|
|
49
38
|
<HStack
|
|
@@ -90,24 +79,22 @@ export function PaginationControls(props: {
|
|
|
90
79
|
|
|
91
80
|
{/*
|
|
92
81
|
right-side
|
|
93
|
-
mobile: next/prev
|
|
94
|
-
desktop^:
|
|
82
|
+
mobile: page jump, next/prev
|
|
83
|
+
desktop^: page jump, next/prev
|
|
95
84
|
*/}
|
|
96
85
|
<HStack gap="large" alignItems="center">
|
|
97
|
-
<HStack
|
|
98
|
-
<
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
|
|
86
|
+
<HStack gap="regular" alignItems="center">
|
|
87
|
+
<NumberField
|
|
88
|
+
aria-label="Go to page"
|
|
89
|
+
hideStepper
|
|
90
|
+
minValue={1}
|
|
91
|
+
maxValue={lastPage}
|
|
92
|
+
step={1}
|
|
93
|
+
value={currentPage}
|
|
94
|
+
onChange={goToPage}
|
|
106
95
|
width="scale.1000"
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
</Picker>
|
|
110
|
-
<Text>of {lastPage} pages</Text>
|
|
96
|
+
/>
|
|
97
|
+
<Text>of {lastPage}</Text>
|
|
111
98
|
</HStack>
|
|
112
99
|
<HStack gap="regular">
|
|
113
100
|
<ActionButton
|