diff --git a/.changeset/good-news-thank.md b/.changeset/good-news-thank.md new file mode 100644 index 000000000..f221a40e5 --- /dev/null +++ b/.changeset/good-news-thank.md @@ -0,0 +1,5 @@ +--- +'@keystatic/core': patch +--- + +Fix circular dependencies diff --git a/packages/keystatic/src/app/ItemPage.tsx b/packages/keystatic/src/app/ItemPage.tsx index 6d7590213..702b472c9 100644 --- a/packages/keystatic/src/app/ItemPage.tsx +++ b/packages/keystatic/src/app/ItemPage.tsx @@ -33,7 +33,6 @@ import { Heading, Text } from '@keystar/ui/typography'; import { Config } from '../config'; import { createGetPreviewProps } from '../form/preview-props'; import { fields } from '../form/api'; -import { SlugFieldInfo } from '../form/fields/text/ui'; import { clientSideValidateProp } from '../form/errors'; import { useEventCallback } from '../form/fields/document/DocumentEditor/ui-utils'; @@ -64,6 +63,7 @@ import { isGitHubConfig, } from './utils'; import { notFound } from './not-found'; +import { SlugFieldInfo } from '../form/fields/text/path-slug-context'; type ItemPageProps = { collection: string; diff --git a/packages/keystatic/src/app/entry-form.tsx b/packages/keystatic/src/app/entry-form.tsx index 3d0ce971c..be622aa7b 100644 --- a/packages/keystatic/src/app/entry-form.tsx +++ b/packages/keystatic/src/app/entry-form.tsx @@ -8,11 +8,11 @@ import { createContext, useContext } from 'react'; import { ReadonlyPropPath } from '../form/fields/document/DocumentEditor/component-blocks/utils'; import { - PathContextProvider, - SlugFieldProvider, AddToPathProvider, + PathContextProvider, SlugFieldInfo, -} from '../form/fields/text/ui'; + SlugFieldProvider, +} from '../form/fields/text/path-slug-context'; import { NonChildFieldComponentSchema, InnerFormValueContentFromPreviewProps, diff --git a/packages/keystatic/src/app/useItemData.ts b/packages/keystatic/src/app/useItemData.ts index 572416cd9..2b4844b0d 100644 --- a/packages/keystatic/src/app/useItemData.ts +++ b/packages/keystatic/src/app/useItemData.ts @@ -1,7 +1,7 @@ import LRUCache from 'lru-cache'; import { useCallback, useMemo } from 'react'; import { Config } from '../config'; -import { SlugFieldInfo } from '../form/fields/text/ui'; +import { SlugFieldInfo } from '../form/fields/text/path-slug-context'; import { ComponentSchema, fields } from '..'; import { parseProps } from '../form/parse-props'; import { getAuth } from './auth'; diff --git a/packages/keystatic/src/form/errors.ts b/packages/keystatic/src/form/errors.ts index abf2dfd2f..b3e816718 100644 --- a/packages/keystatic/src/form/errors.ts +++ b/packages/keystatic/src/form/errors.ts @@ -1,6 +1,6 @@ import { getSlugFromState } from '../app/utils'; import { ComponentSchema } from './api'; -import { SlugFieldInfo } from './fields/text/ui'; +import { SlugFieldInfo } from './fields/text/path-slug-context'; import { FieldDataError } from './fields/error'; import { PropValidationError } from './parse-props'; import { ReadonlyPropPath } from './fields/document/DocumentEditor/component-blocks/utils'; diff --git a/packages/keystatic/src/form/fields/array/ui.tsx b/packages/keystatic/src/form/fields/array/ui.tsx index 695496d47..ca72fe4de 100644 --- a/packages/keystatic/src/form/fields/array/ui.tsx +++ b/packages/keystatic/src/form/fields/array/ui.tsx @@ -29,7 +29,7 @@ import { } from '../../form-from-preview'; import { getInitialPropsValue } from '../../initial-values'; import { useEventCallback } from '../document/DocumentEditor/ui-utils'; -import { SlugFieldInfo } from '../text/ui'; +import { SlugFieldInfo } from '../text/path-slug-context'; import { ArrayField, ComponentSchema, GenericPreviewProps } from '../../api'; export function ArrayFieldInput( diff --git a/packages/keystatic/src/form/fields/conditional/ui.tsx b/packages/keystatic/src/form/fields/conditional/ui.tsx index 3180c36f8..8cdccebe7 100644 --- a/packages/keystatic/src/form/fields/conditional/ui.tsx +++ b/packages/keystatic/src/form/fields/conditional/ui.tsx @@ -11,7 +11,7 @@ import { isNonChildFieldPreviewProps, InnerFormValueContentFromPreviewProps, } from '../../form-from-preview'; -import { AddToPathProvider } from '../text/ui'; +import { AddToPathProvider } from '../text/path-slug-context'; export function ConditionalFieldInput< DiscriminantField extends BasicFormField, diff --git a/packages/keystatic/src/form/fields/date/index.tsx b/packages/keystatic/src/form/fields/date/index.tsx index fba030f1f..56723362a 100644 --- a/packages/keystatic/src/form/fields/date/index.tsx +++ b/packages/keystatic/src/form/fields/date/index.tsx @@ -6,35 +6,7 @@ import { basicFormFieldWithSimpleReaderParse, } from '../utils'; import { DateFieldInput } from './ui'; - -export function validateDate( - validation: { min?: string; max?: string; isRequired?: boolean } | undefined, - value: string | null, - label: string -) { - if (value !== null && !/^\d{4}-\d{2}-\d{2}$/.test(value)) { - return `${label} is not a valid date`; - } - - if (validation?.isRequired && value === null) { - return `${label} is required`; - } - if ((validation?.min || validation?.max) && value !== null) { - const date = new Date(value); - if (validation?.min !== undefined) { - const min = new Date(validation.min); - if (date < min) { - return `${label} must be after ${min.toLocaleDateString()}`; - } - } - if (validation?.max !== undefined) { - const max = new Date(validation.max); - if (date > max) { - return `${label} must be no later than ${max.toLocaleDateString()}`; - } - } - } -} +import { validateDate } from './validateDate'; export function date({ label, diff --git a/packages/keystatic/src/form/fields/date/ui.tsx b/packages/keystatic/src/form/fields/date/ui.tsx index 929155cd4..4dc3a1bd9 100644 --- a/packages/keystatic/src/form/fields/date/ui.tsx +++ b/packages/keystatic/src/form/fields/date/ui.tsx @@ -1,7 +1,7 @@ 'use client'; import { TextField } from '@keystar/ui/text-field'; import { useReducer } from 'react'; -import { validateDate } from '.'; +import { validateDate } from './validateDate'; import { FormFieldInputProps } from '../../api'; export function DateFieldInput( diff --git a/packages/keystatic/src/form/fields/date/validateDate.tsx b/packages/keystatic/src/form/fields/date/validateDate.tsx new file mode 100644 index 000000000..f9f8801be --- /dev/null +++ b/packages/keystatic/src/form/fields/date/validateDate.tsx @@ -0,0 +1,28 @@ +export function validateDate( + validation: { min?: string; max?: string; isRequired?: boolean } | undefined, + value: string | null, + label: string +) { + if (value !== null && !/^\d{4}-\d{2}-\d{2}$/.test(value)) { + return `${label} is not a valid date`; + } + + if (validation?.isRequired && value === null) { + return `${label} is required`; + } + if ((validation?.min || validation?.max) && value !== null) { + const date = new Date(value); + if (validation?.min !== undefined) { + const min = new Date(validation.min); + if (date < min) { + return `${label} must be after ${min.toLocaleDateString()}`; + } + } + if (validation?.max !== undefined) { + const max = new Date(validation.max); + if (date > max) { + return `${label} must be no later than ${max.toLocaleDateString()}`; + } + } + } +} diff --git a/packages/keystatic/src/form/fields/datetime/index.tsx b/packages/keystatic/src/form/fields/datetime/index.tsx index bdd1e3ea2..3d0a496f7 100644 --- a/packages/keystatic/src/form/fields/datetime/index.tsx +++ b/packages/keystatic/src/form/fields/datetime/index.tsx @@ -6,35 +6,7 @@ import { basicFormFieldWithSimpleReaderParse, } from '../utils'; import { DatetimeFieldInput } from './ui'; - -export function validateDatetime( - validation: { min?: string; max?: string; isRequired?: boolean } | undefined, - value: string | null, - label: string -) { - if (value !== null && !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/.test(value)) { - return `${label} is not a valid datetime`; - } - - if (validation?.isRequired && value === null) { - return `${label} is required`; - } - if ((validation?.min || validation?.max) && value !== null) { - const datetime = new Date(value); - if (validation?.min !== undefined) { - const min = new Date(validation.min); - if (datetime < min) { - return `${label} must be after ${min.toISOString()}`; - } - } - if (validation?.max !== undefined) { - const max = new Date(validation.max); - if (datetime > max) { - return `${label} must be no later than ${max.toISOString()}`; - } - } - } -} +import { validateDatetime } from './validateDatetime'; export function datetime({ label, diff --git a/packages/keystatic/src/form/fields/datetime/ui.tsx b/packages/keystatic/src/form/fields/datetime/ui.tsx index cac9b3d0e..cc5214f2d 100644 --- a/packages/keystatic/src/form/fields/datetime/ui.tsx +++ b/packages/keystatic/src/form/fields/datetime/ui.tsx @@ -2,7 +2,7 @@ 'use client'; import { TextField } from '@keystar/ui/text-field'; import { useReducer } from 'react'; -import { validateDatetime } from '.'; +import { validateDatetime } from './validateDatetime'; import { FormFieldInputProps } from '../../api'; export function DatetimeFieldInput( diff --git a/packages/keystatic/src/form/fields/datetime/validateDatetime.tsx b/packages/keystatic/src/form/fields/datetime/validateDatetime.tsx new file mode 100644 index 000000000..e62028cb4 --- /dev/null +++ b/packages/keystatic/src/form/fields/datetime/validateDatetime.tsx @@ -0,0 +1,28 @@ +export function validateDatetime( + validation: { min?: string; max?: string; isRequired?: boolean } | undefined, + value: string | null, + label: string +) { + if (value !== null && !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/.test(value)) { + return `${label} is not a valid datetime`; + } + + if (validation?.isRequired && value === null) { + return `${label} is required`; + } + if ((validation?.min || validation?.max) && value !== null) { + const datetime = new Date(value); + if (validation?.min !== undefined) { + const min = new Date(validation.min); + if (datetime < min) { + return `${label} must be after ${min.toISOString()}`; + } + } + if (validation?.max !== undefined) { + const max = new Date(validation.max); + if (datetime > max) { + return `${label} must be no later than ${max.toISOString()}`; + } + } + } +} diff --git a/packages/keystatic/src/form/fields/integer/index.tsx b/packages/keystatic/src/form/fields/integer/index.tsx index 50c67ab05..fc1fe3c85 100644 --- a/packages/keystatic/src/form/fields/integer/index.tsx +++ b/packages/keystatic/src/form/fields/integer/index.tsx @@ -6,31 +6,7 @@ import { basicFormFieldWithSimpleReaderParse, } from '../utils'; import { IntegerFieldInput } from './ui'; - -export function validateInteger( - validation: { min?: number; max?: number; isRequired?: boolean } | undefined, - value: unknown, - label: string -) { - if ( - value !== null && - (typeof value !== 'number' || !Number.isFinite(value)) - ) { - return `${label} is not a valid whole number`; - } - - if (validation?.isRequired && value === null) { - return `${label} is required`; - } - if (value !== null) { - if (validation?.min !== undefined && value < validation.min) { - return `${label} must be at least ${validation.min}`; - } - if (validation?.max !== undefined && value > validation.max) { - return `${label} must be at most ${validation.max}`; - } - } -} +import { validateInteger } from './validateInteger'; export function integer({ label, diff --git a/packages/keystatic/src/form/fields/integer/ui.tsx b/packages/keystatic/src/form/fields/integer/ui.tsx index 63681ee17..fbcbe3b26 100644 --- a/packages/keystatic/src/form/fields/integer/ui.tsx +++ b/packages/keystatic/src/form/fields/integer/ui.tsx @@ -1,7 +1,7 @@ 'use client'; import { NumberField } from '@keystar/ui/number-field'; import { useReducer } from 'react'; -import { validateInteger } from '.'; +import { validateInteger } from './validateInteger'; import { FormFieldInputProps } from '../../api'; export function IntegerFieldInput( diff --git a/packages/keystatic/src/form/fields/integer/validateInteger.tsx b/packages/keystatic/src/form/fields/integer/validateInteger.tsx new file mode 100644 index 000000000..b9441c3de --- /dev/null +++ b/packages/keystatic/src/form/fields/integer/validateInteger.tsx @@ -0,0 +1,24 @@ +export function validateInteger( + validation: { min?: number; max?: number; isRequired?: boolean } | undefined, + value: unknown, + label: string +) { + if ( + value !== null && + (typeof value !== 'number' || !Number.isFinite(value)) + ) { + return `${label} is not a valid whole number`; + } + + if (validation?.isRequired && value === null) { + return `${label} is required`; + } + if (value !== null) { + if (validation?.min !== undefined && value < validation.min) { + return `${label} must be at least ${validation.min}`; + } + if (validation?.max !== undefined && value > validation.max) { + return `${label} must be at most ${validation.max}`; + } + } +} diff --git a/packages/keystatic/src/form/fields/object/ui.tsx b/packages/keystatic/src/form/fields/object/ui.tsx index 2008475eb..78c0ada87 100644 --- a/packages/keystatic/src/form/fields/object/ui.tsx +++ b/packages/keystatic/src/form/fields/object/ui.tsx @@ -7,7 +7,7 @@ import { isNonChildFieldPreviewProps, InnerFormValueContentFromPreviewProps, } from '../../form-from-preview'; -import { AddToPathProvider } from '../text/ui'; +import { AddToPathProvider } from '../text/path-slug-context'; import { useId } from 'react'; import { Text } from '@keystar/ui/typography'; diff --git a/packages/keystatic/src/form/fields/slug/index.tsx b/packages/keystatic/src/form/fields/slug/index.tsx index 73baa5449..c9b94c091 100644 --- a/packages/keystatic/src/form/fields/slug/index.tsx +++ b/packages/keystatic/src/form/fields/slug/index.tsx @@ -1,6 +1,6 @@ import { FormFieldStoredValue, SlugFormField } from '../../api'; import slugify from '@sindresorhus/slugify'; -import { validateText } from '../text'; +import { validateText } from '../text/validateText'; import { SlugFieldInput } from './ui'; import { FieldDataError } from '../error'; import { Glob } from '../../..'; diff --git a/packages/keystatic/src/form/fields/slug/ui.tsx b/packages/keystatic/src/form/fields/slug/ui.tsx index 99ad94631..b7ce3bf60 100644 --- a/packages/keystatic/src/form/fields/slug/ui.tsx +++ b/packages/keystatic/src/form/fields/slug/ui.tsx @@ -4,8 +4,8 @@ import { Flex, Box } from '@keystar/ui/layout'; import { TextField } from '@keystar/ui/text-field'; import { useContext, useState } from 'react'; import { FormFieldInputProps } from '../../api'; -import { SlugFieldContext, PathContext } from '../text/ui'; -import { validateText } from '../text'; +import { SlugFieldContext, PathContext } from '../text/path-slug-context'; +import { validateText } from '../text/validateText'; const emptySet = new Set(); diff --git a/packages/keystatic/src/form/fields/text/index.tsx b/packages/keystatic/src/form/fields/text/index.tsx index 4d9d4bd1a..2d57c29fe 100644 --- a/packages/keystatic/src/form/fields/text/index.tsx +++ b/packages/keystatic/src/form/fields/text/index.tsx @@ -3,54 +3,7 @@ import { FormFieldStoredValue } from '../../..'; import { SlugFormField } from '../../api'; import { FieldDataError } from '../error'; import { TextFieldInput } from './ui'; - -export function validateText( - val: string, - min: number, - max: number, - fieldLabel: string, - slugInfo: { slugs: Set; glob: Glob } | undefined -) { - if (val.length < min) { - if (min === 1) { - return `${fieldLabel} must not be empty`; - } else { - return `${fieldLabel} must be at least ${min} characters long`; - } - } - if (val.length > max) { - return `${fieldLabel} must be no longer than ${max} characters`; - } - if (slugInfo) { - if (val === '') { - return `${fieldLabel} must not be empty`; - } - if (val === '..') { - return `${fieldLabel} must not be ..`; - } - if (val === '.') { - return `${fieldLabel} must not be .`; - } - if (slugInfo.glob === '**') { - const split = val.split('/'); - if (split.some(s => s === '..')) { - return `${fieldLabel} must not contain ..`; - } - if (split.some(s => s === '.')) { - return `${fieldLabel} must not be .`; - } - } - if ((slugInfo.glob === '*' ? /[\\/]/ : /[\\]/).test(val)) { - return `${fieldLabel} must not contain slashes`; - } - if (/^\s|\s$/.test(val)) { - return `${fieldLabel} must not start or end with spaces`; - } - if (slugInfo.slugs.has(val)) { - return `${fieldLabel} must be unique`; - } - } -} +import { validateText } from './validateText'; function parseAsNormalField(value: FormFieldStoredValue) { if (value === undefined) { diff --git a/packages/keystatic/src/form/fields/text/path-slug-context.tsx b/packages/keystatic/src/form/fields/text/path-slug-context.tsx new file mode 100644 index 000000000..f66064890 --- /dev/null +++ b/packages/keystatic/src/form/fields/text/path-slug-context.tsx @@ -0,0 +1,33 @@ +import { ReactNode, createContext, useContext, useMemo } from 'react'; +import { ReadonlyPropPath } from '../document/DocumentEditor/component-blocks/utils'; +import { Glob } from '../../..'; + +export function AddToPathProvider(props: { + part: string | number; + children: ReactNode; +}) { + const path = useContext(PathContext); + return ( + path.concat(props.part), [path, props.part])} + > + {props.children} + + ); +} + +export type SlugFieldInfo = { + field: string; + slugs: Set; + glob: Glob; +}; + +export const SlugFieldContext = createContext( + undefined +); + +export const SlugFieldProvider = SlugFieldContext.Provider; + +export const PathContext = createContext([]); + +export const PathContextProvider = PathContext.Provider; diff --git a/packages/keystatic/src/form/fields/text/ui.tsx b/packages/keystatic/src/form/fields/text/ui.tsx index e0021110c..d6eafc03b 100644 --- a/packages/keystatic/src/form/fields/text/ui.tsx +++ b/packages/keystatic/src/form/fields/text/ui.tsx @@ -1,40 +1,9 @@ 'use client'; -import { createContext, ReactNode, useContext, useMemo, useState } from 'react'; -import { Glob } from '../../../config'; -import { ReadonlyPropPath } from '../document/DocumentEditor/component-blocks/utils'; +import { useContext, useState } from 'react'; import { FormFieldInputProps } from '../../api'; import { TextArea, TextField } from '@keystar/ui/text-field'; -import { validateText } from '.'; - -export type SlugFieldInfo = { - field: string; - slugs: Set; - glob: Glob; -}; - -export const SlugFieldContext = createContext( - undefined -); - -export const SlugFieldProvider = SlugFieldContext.Provider; - -export const PathContext = createContext([]); - -export const PathContextProvider = PathContext.Provider; - -export function AddToPathProvider(props: { - part: string | number; - children: ReactNode; -}) { - const path = useContext(PathContext); - return ( - path.concat(props.part), [path, props.part])} - > - {props.children} - - ); -} +import { validateText } from './validateText'; +import { PathContext, SlugFieldContext } from './path-slug-context'; export function TextFieldInput( props: FormFieldInputProps & { diff --git a/packages/keystatic/src/form/fields/text/validateText.tsx b/packages/keystatic/src/form/fields/text/validateText.tsx new file mode 100644 index 000000000..e57f2c666 --- /dev/null +++ b/packages/keystatic/src/form/fields/text/validateText.tsx @@ -0,0 +1,49 @@ +import { Glob } from '../../../config'; + +export function validateText( + val: string, + min: number, + max: number, + fieldLabel: string, + slugInfo: { slugs: Set; glob: Glob } | undefined +) { + if (val.length < min) { + if (min === 1) { + return `${fieldLabel} must not be empty`; + } else { + return `${fieldLabel} must be at least ${min} characters long`; + } + } + if (val.length > max) { + return `${fieldLabel} must be no longer than ${max} characters`; + } + if (slugInfo) { + if (val === '') { + return `${fieldLabel} must not be empty`; + } + if (val === '..') { + return `${fieldLabel} must not be ..`; + } + if (val === '.') { + return `${fieldLabel} must not be .`; + } + if (slugInfo.glob === '**') { + const split = val.split('/'); + if (split.some(s => s === '..')) { + return `${fieldLabel} must not contain ..`; + } + if (split.some(s => s === '.')) { + return `${fieldLabel} must not be .`; + } + } + if ((slugInfo.glob === '*' ? /[\\/]/ : /[\\]/).test(val)) { + return `${fieldLabel} must not contain slashes`; + } + if (/^\s|\s$/.test(val)) { + return `${fieldLabel} must not start or end with spaces`; + } + if (slugInfo.slugs.has(val)) { + return `${fieldLabel} must be unique`; + } + } +} diff --git a/packages/keystatic/src/form/fields/url/index.tsx b/packages/keystatic/src/form/fields/url/index.tsx index 905bed8d0..711a3a555 100644 --- a/packages/keystatic/src/form/fields/url/index.tsx +++ b/packages/keystatic/src/form/fields/url/index.tsx @@ -1,4 +1,3 @@ -import { isValidURL } from '../document/DocumentEditor/isValidURL'; import { BasicFormField } from '../../api'; import { FieldDataError } from '../error'; import { @@ -7,20 +6,7 @@ import { basicFormFieldWithSimpleReaderParse, } from '../utils'; import { UrlFieldInput } from './ui'; - -export function validateUrl( - validation: { isRequired?: boolean } | undefined, - value: unknown, - label: string -) { - if (value !== null && (typeof value !== 'string' || !isValidURL(value))) { - return `${label} is not a valid URL`; - } - - if (validation?.isRequired && value === null) { - return `${label} is required`; - } -} +import { validateUrl } from './validateUrl'; export function url({ label, diff --git a/packages/keystatic/src/form/fields/url/ui.tsx b/packages/keystatic/src/form/fields/url/ui.tsx index 19ea2f904..202b09dfe 100644 --- a/packages/keystatic/src/form/fields/url/ui.tsx +++ b/packages/keystatic/src/form/fields/url/ui.tsx @@ -2,7 +2,7 @@ import { TextField } from '@keystar/ui/text-field'; import { useReducer } from 'react'; import { FormFieldInputProps } from '../../api'; -import { validateUrl } from '.'; +import { validateUrl } from './validateUrl'; export function UrlFieldInput( props: FormFieldInputProps & { diff --git a/packages/keystatic/src/form/fields/url/validateUrl.tsx b/packages/keystatic/src/form/fields/url/validateUrl.tsx new file mode 100644 index 000000000..6381d45fc --- /dev/null +++ b/packages/keystatic/src/form/fields/url/validateUrl.tsx @@ -0,0 +1,15 @@ +import { isValidURL } from '../document/DocumentEditor/isValidURL'; + +export function validateUrl( + validation: { isRequired?: boolean } | undefined, + value: unknown, + label: string +) { + if (value !== null && (typeof value !== 'string' || !isValidURL(value))) { + return `${label} is not a valid URL`; + } + + if (validation?.isRequired && value === null) { + return `${label} is required`; + } +} diff --git a/packages/keystatic/src/form/form-from-preview.tsx b/packages/keystatic/src/form/form-from-preview.tsx index 65b04bed5..8ac74c4fc 100644 --- a/packages/keystatic/src/form/form-from-preview.tsx +++ b/packages/keystatic/src/form/form-from-preview.tsx @@ -11,10 +11,10 @@ import { } from './api'; import { ReadonlyPropPath } from './fields/document/DocumentEditor/component-blocks/utils'; import { - PathContextProvider, SlugFieldInfo, + PathContextProvider, SlugFieldProvider, -} from './fields/text/ui'; +} from './fields/text/path-slug-context'; import { ObjectFieldInput } from './fields/object/ui'; import { ConditionalFieldInput } from './fields/conditional/ui'; import { ArrayFieldInput } from './fields/array/ui';