Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(COREL): update title & date validation #7148

Merged
merged 12 commits into from
Jul 15, 2024
136 changes: 100 additions & 36 deletions packages/sanity/src/core/bundles/components/dialog/BundleForm.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
/* eslint-disable i18next/no-literal-string */
import {CalendarIcon} from '@sanity/icons'
import {Box, Button, Card, Flex, Popover, Stack, Text, TextArea, TextInput} from '@sanity/ui'
import {useCallback, useMemo, useState} from 'react'
import {useDateTimeFormat, useTranslation} from 'sanity'
import {Box, Button, Flex, Popover, Stack, Text, TextArea, TextInput} from '@sanity/ui'
import {useCallback, useEffect, useMemo, useState} from 'react'
import {
FormFieldHeaderText,
type FormNodeValidation,
useBundles,
useDateTimeFormat,
useTranslation,
} from 'sanity'
import speakingurl from 'speakingurl'

import {type CalendarLabels} from '../../../form/inputs/DateInputs/base/calendar/types'
Expand All @@ -14,16 +20,78 @@ import {BundleIconEditorPicker, type BundleIconEditorPickerValue} from './Bundle

export function BundleForm(props: {
onChange: (params: Partial<BundleDocument>) => void
onError: (errorsExist: boolean) => void
value: Partial<BundleDocument>
}): JSX.Element {
const {onChange, value} = props
const {onChange, onError, value} = props
const {title, description, icon, hue, publishAt} = value

const dateFormatter = useDateTimeFormat()

const [showTitleValidation, setShowTitleValidation] = useState(false)
const [showDateValidation, setShowDateValidation] = useState(false)
const [showDatePicker, setShowDatePicker] = useState(false)
const [showBundleExists, setShowBundleExists] = useState(false)
const [showIsDraftPublishError, setShowIsDraftPublishError] = useState(false)

const [isInitialRender, setIsInitialRender] = useState(true)
const {data} = useBundles()

const [titleErrors, setTitleErrors] = useState<FormNodeValidation[]>([])
const [dateErrors, setDateErrors] = useState<FormNodeValidation[]>([])

useEffect(() => {
const newTitleErrors: FormNodeValidation[] = []
const newDateErrors: FormNodeValidation[] = []

// if the title is 'drafts' or 'published', show an error
// TODO localize text
if (showIsDraftPublishError) {
newTitleErrors.push({
level: 'error',
message: "Title cannot be 'drafts' or 'published'",
path: [],
})
}

// if the bundle already exists, show an error
// TODO localize text
if (showBundleExists) {
newTitleErrors.push({
level: 'error',
message: 'Bundle already exists',
path: [],
})
}

// if the title is empty (but on not first render), show an error
// TODO localize text
if (!isInitialRender && title?.length === 0) {
newTitleErrors.push({
level: 'error',
message: 'Bundle needs a name',
path: [],
})
}

// if the date is invalid, show an error
// TODO localize text
if (showDateValidation) {
newDateErrors.push({
level: 'error',
message: 'Should be an empty or valid date',
path: [],
})
}

setTitleErrors(newTitleErrors)
setDateErrors(newDateErrors)
}, [
isInitialRender,
showBundleExists,
showDateValidation,
showIsDraftPublishError,
title?.length,
])

const publishAtDisplayValue = useMemo(() => {
if (!publishAt) return ''
Expand All @@ -45,16 +113,32 @@ export function BundleForm(props: {
const handleBundleTitleChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
const pickedTitle = event.target.value
const pickedNameExists =
data && data.find((bundle) => bundle.name === speakingurl(pickedTitle))

if (isDraftOrPublished(pickedTitle) || pickedNameExists) {
if (isDraftOrPublished(pickedTitle)) {
setShowIsDraftPublishError(true)
} else {
setShowIsDraftPublishError(false)
}

if (isDraftOrPublished(pickedTitle)) {
setShowTitleValidation(true)
if (pickedNameExists) {
setShowBundleExists(true)
} else {
setShowBundleExists(false)
}
onError(true)
} else {
setShowTitleValidation(false)
setShowIsDraftPublishError(false)
setShowBundleExists(false)
onError(false)
}

setIsInitialRender(false)
onChange({...value, title: pickedTitle, name: speakingurl(pickedTitle)})
},
[onChange, value],
[data, onChange, onError, value],
)

const handleBundleDescriptionChange = useCallback(
Expand Down Expand Up @@ -112,24 +196,13 @@ export function BundleForm(props: {
<BundleIconEditorPicker onChange={handleIconValueChange} value={iconValue} />
</Flex>
<Stack space={3}>
{showTitleValidation && (
<Card tone="critical" padding={3} radius={2}>
<Text align="center" muted size={1}>
{/* localize & validate copy & UI */}
Title cannot be "drafts" or "published"
</Text>
</Card>
)}

{/* TODO ADD CHECK FOR EXISTING NAMES AND AVOID DUPLICATES */}
<Text size={1} weight="medium">
{/* localize text */}
Title
</Text>
{/* localize text */}
<FormFieldHeaderText title="Title" validation={titleErrors} />
<TextInput
data-testid="bundle-form-title"
onChange={handleBundleTitleChange}
customValidity={titleErrors.length > 0 ? 'error' : undefined}
value={title}
data-testid="bundle-form-title"
/>
</Stack>

Expand All @@ -146,18 +219,8 @@ export function BundleForm(props: {
</Stack>

<Stack space={3}>
<Text size={1} weight="medium">
{/* localize text */}
Schedule for publishing at
</Text>
{showDateValidation && (
<Card tone="critical" padding={3} radius={2}>
<Text align="center" muted size={1}>
{/* localize & validate copy & UI */}
Should be an empty or valid date
</Text>
</Card>
)}
{/* localize text */}
<FormFieldHeaderText title="Schedule for publishing at" validation={dateErrors} />

<TextInput
suffix={
Expand Down Expand Up @@ -190,6 +253,7 @@ export function BundleForm(props: {
value={displayDate}
onChange={handlePublishAtInputChange}
data-testid="bundle-form-publish-at"
customValidity={dateErrors.length > 0 ? 'error' : undefined}
/>
</Stack>
</Stack>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {type FormEvent, useCallback, useState} from 'react'
import {type BundleDocument} from '../../../store/bundles/types'
import {useBundleOperations} from '../../../store/bundles/useBundleOperations'
import {usePerspective} from '../../hooks/usePerspective'
import {isDraftOrPublished} from '../../util/dummyGetters'
import {BundleForm} from './BundleForm'

interface CreateBundleDialogProps {
Expand All @@ -16,6 +15,7 @@ interface CreateBundleDialogProps {
export function CreateBundleDialog(props: CreateBundleDialogProps): JSX.Element {
const {onCancel, onCreate} = props
const {createBundle} = useBundleOperations()
const [hasErrors, setHasErrors] = useState(false)

const [value, setValue] = useState<Partial<BundleDocument>>({
name: '',
Expand Down Expand Up @@ -53,6 +53,10 @@ export function CreateBundleDialog(props: CreateBundleDialogProps): JSX.Element
setValue(changedValue)
}, [])

const handleOnError = useCallback((errorsExist: boolean) => {
setHasErrors(errorsExist)
}, [])

return (
<Dialog
animate
Expand All @@ -64,11 +68,11 @@ export function CreateBundleDialog(props: CreateBundleDialogProps): JSX.Element
>
<form onSubmit={handleOnSubmit}>
<Box padding={6}>
<BundleForm onChange={handleOnChange} value={value} />
<BundleForm onChange={handleOnChange} onError={handleOnError} value={value} />
</Box>
<Flex justify="flex-end" padding={3}>
<Button
disabled={!value.title || isDraftOrPublished(value.title) || isCreating}
disabled={!value.title || isCreating || hasErrors}
iconRight={ArrowRightIcon}
type="submit"
// localize Text
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
import {beforeEach, describe, expect, it, jest} from '@jest/globals'
import {fireEvent, render, screen} from '@testing-library/react'
import {type BundleDocument} from 'sanity'
import {type BundleDocument, useDateTimeFormat} from 'sanity'

import {useBundles} from '../../../../store/bundles'
import {createWrapper} from '../../../util/tests/createWrapper'
import {BundleForm} from '../BundleForm'

jest.mock('sanity', () => ({
useDateTimeFormat: jest.fn().mockReturnValue({format: jest.fn().mockReturnValue('Mocked date')}),
useTranslation: jest.fn().mockReturnValue({t: jest.fn().mockReturnValue('Mocked translation')}),
jest.mock('../../../../../core/hooks/useDateTimeFormat', () => ({
useDateTimeFormat: jest.fn(),
}))

jest.mock('../../../../store/bundles', () => ({
useBundles: jest.fn(),
}))

const mockUseBundleStore = useBundles as jest.Mock<typeof useBundles>
const mockUseDateTimeFormat = useDateTimeFormat as jest.Mock

describe('BundleForm', () => {
const onChangeMock = jest.fn()
const onErrorMock = jest.fn()
const valueMock: Partial<BundleDocument> = {
title: '',
description: '',
Expand All @@ -22,12 +30,23 @@ describe('BundleForm', () => {

beforeEach(async () => {
onChangeMock.mockClear()
onErrorMock.mockClear()

mockUseBundleStore.mockReturnValue({
data: [],
loading: true,
dispatch: jest.fn(),
})

mockUseDateTimeFormat.mockReturnValue({format: jest.fn().mockReturnValue('Mocked date')})

const wrapper = await createWrapper()
render(<BundleForm onChange={onChangeMock} value={valueMock} />, {wrapper})
render(<BundleForm onChange={onChangeMock} value={valueMock} onError={onErrorMock} />, {
wrapper,
})
})

it('should render the form fields', async () => {
it('should render the form fields', () => {
expect(screen.getByTestId('bundle-form-title')).toBeInTheDocument()
expect(screen.getByTestId('bundle-form-description')).toBeInTheDocument()
expect(screen.getByTestId('bundle-form-publish-at')).toBeInTheDocument()
Expand Down Expand Up @@ -60,4 +79,59 @@ describe('BundleForm', () => {

expect(onChangeMock).toHaveBeenCalledWith({...valueMock, publishAt: ''})
})

it('should show an error when the title is "drafts"', () => {
const titleInput = screen.getByTestId('bundle-form-title')

fireEvent.change(titleInput, {target: {value: 'drafts'}})

expect(screen.getByTestId('input-validation-icon-error')).toBeInTheDocument()
})

it('should show an error when the title is "published"', () => {
const titleInput = screen.getByTestId('bundle-form-title')
fireEvent.change(titleInput, {target: {value: 'published'}})

expect(screen.getByTestId('input-validation-icon-error')).toBeInTheDocument()
})

it('should show an error when the bundle already exists', () => {
// Mock the data returned by useBundles hook
const mockData: BundleDocument[] = [
{
_type: 'bundle',
_id: 'existing-bundle',
_createdAt: '2022-01-01T00:00:00Z',
authorId: 'author-id',
name: 'existing-bundle',
title: 'Existing Bundle',
icon: 'cube',
hue: 'gray',
publishAt: '2022-01-01',
_updatedAt: '',
_rev: '',
},
// Add more mock data if needed
]
mockUseBundleStore.mockReturnValue({data: mockData, loading: false, dispatch: jest.fn()})

const titleInput = screen.getByTestId('bundle-form-title')
fireEvent.change(titleInput, {target: {value: 'existing-bundle'}})

expect(screen.getByTestId('input-validation-icon-error')).toBeInTheDocument()
})

it('should show an error when the title is empty', () => {
const titleInput = screen.getByTestId('bundle-form-title')
fireEvent.change(titleInput, {target: {value: ' '}})

expect(screen.getByTestId('input-validation-icon-error')).toBeInTheDocument()
})

it('should show an error when the publishAt input value is invalid', () => {
const publishAtInput = screen.getByTestId('bundle-form-publish-at')
fireEvent.change(publishAtInput, {target: {value: 'invalid-date'}})

expect(screen.getByTestId('input-validation-icon-error')).toBeInTheDocument()
})
})
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import {beforeEach, describe, expect, it, jest} from '@jest/globals'
import {fireEvent, render, screen} from '@testing-library/react'
import {type BundleDocument} from 'sanity'

import {createWrapper} from '../../../util/tests/createWrapper'
import {BundleIconEditorPicker} from '../BundleIconEditorPicker'
import {BundleIconEditorPicker, type BundleIconEditorPickerValue} from '../BundleIconEditorPicker'

describe('BundleIconEditorPicker', () => {
const onChangeMock = jest.fn()
const valueMock: Partial<BundleDocument> = {
const valueMock: BundleIconEditorPickerValue = {
hue: 'gray',
icon: 'cube',
}
Expand Down
Loading
Loading