diff --git a/packages/@sanity/form-builder/src/inputs/DateInputs/BaseDateTimeInput.js b/packages/@sanity/form-builder/src/inputs/DateInputs/BaseDateTimeInput.js new file mode 100644 index 00000000000..83e9645c9f2 --- /dev/null +++ b/packages/@sanity/form-builder/src/inputs/DateInputs/BaseDateTimeInput.js @@ -0,0 +1,207 @@ +// @flow +import moment from 'moment' +import type Moment from 'moment' +import DatePicker from 'react-datepicker' +import 'react-datepicker/dist/react-datepicker-cssmodules.css' // eslint-disable-line import/no-unassigned-import +import React from 'react' +import FormField from 'part:@sanity/components/formfields/default' +import TextInput from 'part:@sanity/components/textinputs/default' +import styles from './styles/BaseDateTimeInput.css' +import type {Marker} from '../../typedefs' +import Dialog from 'part:@sanity/components/dialogs/default' +import Button from 'part:@sanity/components/buttons/default' +import CalendarIcon from 'part:@sanity/base/calendar-icon' + +type Action = { + name: string +} + +type Props = { + value: ?Moment, + markers: Array, + dateOnly?: boolean, + dateFormat: string, + timeFormat?: string, + timeStep?: number, + todayLabel: string, + title: ?string, + description: ?string, + placeholder: ?string, + readOnly: ?boolean, + onChange: (?Moment) => void, + level: number +} + +const getFormat = (dateFormat, timeFormat) => dateFormat + (timeFormat ? ` ${timeFormat}` : '') + +type State = { + inputValue: ?string, + isDialogOpen: boolean +} + +export default class BaseDateTimeInput extends React.Component { + _datepicker: ?DatePicker + + state = { + inputValue: null, + isDialogOpen: false + } + + handleInputChange = (event: SyntheticEvent) => { + const inputValue = event.currentTarget.value + const {onChange, dateFormat, timeFormat} = this.props + const parsed = moment(inputValue, getFormat(dateFormat, timeFormat), true) + if (parsed.isValid()) { + onChange(parsed) + } else { + this.setState({inputValue: inputValue}) + } + } + + handleDialogChange = (nextMoment?: Moment) => { + const {onChange} = this.props + onChange(nextMoment) + this.setState({inputValue: null}) + this.close() + } + + handleBlur = () => { + this.setState({inputValue: null}) + } + + focus() { + if (this._datepicker) { + this._datepicker.input.focus() + } + } + + setDatePicker = (datePicker: ?DatePicker) => { + this._datepicker = datePicker + } + + handleKeyDown = (event: SyntheticKeyboardEvent<*>) => { + if (event.key === 'Enter') { + this.open() + } + } + open = () => { + this.setState({ + isDialogOpen: true + }) + } + + close = () => { + this.setState({ + isDialogOpen: false + }) + } + + handleDialogOpen = this.open + handleDialogClose = this.close + + handleDialogAction = (action: Action) => { + if (action.name === 'close') { + this.close() + } + + if (action.name === 'now') { + this.handleDialogChange(moment()) + } + } + + render() { + const { + value, + markers, + dateOnly, + dateFormat, + timeFormat, + title, + description, + todayLabel, + readOnly, + timeStep, + level + } = this.props + + const {inputValue, isDialogOpen} = this.state + + const format = getFormat(dateFormat, timeFormat) + const placeholder = this.props.placeholder || `e.g. ${moment().format(format)}` + + const validation = markers.filter(marker => marker.type === 'validation') + const errors = validation.filter(marker => marker.level === 'error') + + return ( + + {readOnly && ( + 0 ? errors[0].item.message : ''} + readOnly + value={value ? value.format(format) : ''} + /> + )} + {!readOnly && ( +
0 ? styles.inputWrapperWithError : styles.inputWrapper}> + + +
+ )} + {isDialogOpen && ( + +
+ +
+
+ )} +
+ ) + } +} diff --git a/packages/@sanity/form-builder/src/inputs/DateInputs/DateInput.js b/packages/@sanity/form-builder/src/inputs/DateInputs/DateInput.js new file mode 100644 index 00000000000..fe6e1f33bc9 --- /dev/null +++ b/packages/@sanity/form-builder/src/inputs/DateInputs/DateInput.js @@ -0,0 +1,89 @@ +// @flow +import type Moment from 'moment' +import moment from 'moment' +import React from 'react' +import PatchEvent, {set, unset} from '../../PatchEvent' +import type {Marker} from '../../typedefs' +import BaseDateTimeInput from './BaseDateTimeInput' + +type ParsedOptions = { + dateFormat: string, + calendarTodayLabel: string +} + +type SchemaOptions = { + dateFormat?: string, + calendarTodayLabel?: string +} + +// This is the format dates are stored on +const VALUE_FORMAT = 'YYYY-MM-DD' + +// default to how they are stored +const DEFAULT_DATE_FORMAT = VALUE_FORMAT + +type Props = { + value: string, + markers: Array, + type: { + name: string, + title: string, + description: string, + options?: SchemaOptions, + placeholder?: string + }, + readOnly: ?boolean, + onChange: PatchEvent => void, + level: number +} + +function parseOptions(options: SchemaOptions = {}): ParsedOptions { + return { + dateFormat: options.dateFormat || DEFAULT_DATE_FORMAT, + calendarTodayLabel: options.calendarTodayLabel || 'Today' + } +} + +export default class DateInput extends React.Component { + baseDateTimeInputRef: ?BaseDateTimeInput = null + + handleChange = (nextMoment?: Moment) => { + const patch = nextMoment ? set(nextMoment.format(VALUE_FORMAT)) : unset() + this.props.onChange(PatchEvent.from([patch])) + } + + focus() { + if (this.baseDateTimeInputRef) { + this.baseDateTimeInputRef.focus() + } + } + + setBaseInput = (baseInput: ?BaseDateTimeInput) => { + this.baseDateTimeInputRef = baseInput + } + + render() { + const {value, markers, type, readOnly, level} = this.props + const {title, description} = type + const momentValue: ?Moment = value ? moment(value) : null + + const options = parseOptions(type.options) + + return ( + + ) + } +} diff --git a/packages/@sanity/form-builder/src/inputs/DateInputs/DateTimeInput.js b/packages/@sanity/form-builder/src/inputs/DateInputs/DateTimeInput.js new file mode 100644 index 00000000000..8b4864793fe --- /dev/null +++ b/packages/@sanity/form-builder/src/inputs/DateInputs/DateTimeInput.js @@ -0,0 +1,95 @@ +import type Moment from 'moment' +// @flow +import moment from 'moment' +import 'react-datepicker/dist/react-datepicker-cssmodules.css' // eslint-disable-line import/no-unassigned-import +import {uniqueId} from 'lodash' +import React from 'react' +import PatchEvent, {set, unset} from '../../PatchEvent' +import type {Marker} from '../../typedefs' +import BaseDateTimeInput from './BaseDateTimeInput' + +type ParsedOptions = { + dateFormat: string, + timeFormat: string, + timeStep: number, + calendarTodayLabel: string +} + +type SchemaOptions = { + dateFormat?: string, + timeFormat?: string, + timeStep?: number, + calendarTodayLabel?: string +} + +const DEFAULT_DATE_FORMAT = 'YYYY-MM-DD' +const DEFAULT_TIME_FORMAT = 'HH:mm' + +type Props = { + value: string, + markers: Array, + type: { + name: string, + title: string, + description: string, + options?: SchemaOptions, + placeholder?: string + }, + readOnly: ?boolean, + onChange: PatchEvent => void, + level: number +} + +function parseOptions(options: SchemaOptions = {}): ParsedOptions { + return { + dateFormat: options.dateFormat || DEFAULT_DATE_FORMAT, + timeFormat: options.timeFormat || DEFAULT_TIME_FORMAT, + timeStep: ('timeStep' in options && Number(options.timeStep)) || 15, + calendarTodayLabel: options.calendarTodayLabel || 'Now' + } +} + +export default class DateInput extends React.Component { + baseDateTimeInputRef: ?BaseDateTimeInput = null + + handleChange = (nextMoment?: Moment) => { + const patch = nextMoment ? set(nextMoment.toDate().toJSON()) : unset() + this.props.onChange(PatchEvent.from([patch])) + } + + focus() { + if (this.baseDateTimeInputRef) { + this.baseDateTimeInputRef.focus() + } + } + + setBaseInput = (baseInput: ?BaseDateTimeInput) => { + this.baseDateTimeInputRef = baseInput + } + + render() { + const {value, markers, type, readOnly, level} = this.props + const {title, description} = type + const momentValue: ?Moment = value ? moment(value) : null + + const options = parseOptions(type.options) + + return ( + + ) + } +} diff --git a/packages/@sanity/form-builder/src/inputs/DateInputs/index.js b/packages/@sanity/form-builder/src/inputs/DateInputs/index.js new file mode 100644 index 00000000000..8daec3df3d5 --- /dev/null +++ b/packages/@sanity/form-builder/src/inputs/DateInputs/index.js @@ -0,0 +1,2 @@ +export {default as DateTimeInput} from './DateTimeInput' +export {default as DateInput} from './DateInput' diff --git a/packages/@sanity/form-builder/src/inputs/DateTimeInput/styles/DateTimeInput.css b/packages/@sanity/form-builder/src/inputs/DateInputs/styles/BaseDateTimeInput.css similarity index 98% rename from packages/@sanity/form-builder/src/inputs/DateTimeInput/styles/DateTimeInput.css rename to packages/@sanity/form-builder/src/inputs/DateInputs/styles/BaseDateTimeInput.css index b3e734d7295..3dc23f93b18 100644 --- a/packages/@sanity/form-builder/src/inputs/DateTimeInput/styles/DateTimeInput.css +++ b/packages/@sanity/form-builder/src/inputs/DateInputs/styles/BaseDateTimeInput.css @@ -55,7 +55,7 @@ outline: none; } -.datepicker { +.datePicker { position: relative; display: block; border-radius: var(--react-datepicker-border-radius); @@ -70,7 +70,7 @@ background-color: transparent; } -.root { +.dialogDatePicker { @nest & :global(.react-datepicker) { border-radius: var(--react-datepicker-border-radius) var(--react-datepicker-border-radius) 0 0; border: none; @@ -162,8 +162,8 @@ } } -.rootWithTime { - composes: root; +.dialogDatePickerWithTime { + composes: dialogDatePicker; @nest & :global(.react-datepicker__header--time) { padding: 0; diff --git a/packages/@sanity/form-builder/src/inputs/DateTimeInput/styles/variables.css b/packages/@sanity/form-builder/src/inputs/DateInputs/styles/variables.css similarity index 100% rename from packages/@sanity/form-builder/src/inputs/DateTimeInput/styles/variables.css rename to packages/@sanity/form-builder/src/inputs/DateInputs/styles/variables.css diff --git a/packages/@sanity/form-builder/src/inputs/DateTimeInput/DateTimeInput.js b/packages/@sanity/form-builder/src/inputs/DateTimeInput/DateTimeInput.js deleted file mode 100644 index 2a44af06ee4..00000000000 --- a/packages/@sanity/form-builder/src/inputs/DateTimeInput/DateTimeInput.js +++ /dev/null @@ -1,262 +0,0 @@ -// @flow -import moment from 'moment' -import type Moment from 'moment' -import DatePicker from 'react-datepicker' -import 'react-datepicker/dist/react-datepicker-cssmodules.css' // eslint-disable-line import/no-unassigned-import -import {uniqueId} from 'lodash' -import React from 'react' -import FormField from 'part:@sanity/components/formfields/default' -import TextInput from 'part:@sanity/components/textinputs/default' -import styles from './styles/DateTimeInput.css' -import PatchEvent, {set, unset} from '../../PatchEvent' -import type {Marker} from '../../typedefs' -import Dialog from 'part:@sanity/components/dialogs/default' -import Button from 'part:@sanity/components/buttons/default' -import CalendarIcon from 'part:@sanity/base/calendar-icon' - -type ParsedOptions = { - dateFormat: string, - timeFormat: string, - timeStep: number, - calendarTodayLabel: string -} - -type SchemaOptions = { - dateFormat?: string, - timeFormat?: string, - timeStep?: number, - calendarTodayLabel?: string -} - -const DEFAULT_DATE_FORMAT = 'YYYY-MM-DD' -const DEFAULT_TIME_FORMAT = 'HH:mm' - -type Props = { - value: string, - markers: Array, - type: { - name: string, - title: string, - description: string, - options?: SchemaOptions - }, - readOnly: ?boolean, - onChange: PatchEvent => void, - level: number -} - -function parseOptions(options: SchemaOptions = {}): ParsedOptions { - return { - dateFormat: options.dateFormat || DEFAULT_DATE_FORMAT, - timeFormat: options.timeFormat || DEFAULT_TIME_FORMAT, - timeStep: ('timeStep' in options && Number(options.timeStep)) || 15, - calendarTodayLabel: options.calendarTodayLabel || 'Today' - } -} - -const getFormat = (options: ParsedOptions) => `${options.dateFormat} ${options.timeFormat}` - -type State = { - inputValue: ?string -} - -export default class DateInput extends React.Component { - _datepicker: ?DatePicker - inputId: string = uniqueId('date-input') - - state = { - inputValue: null, - isActive: false - } - - handleInputChange = (event: SyntheticEvent) => { - const inputValue = event.currentTarget.value - const parsed = moment(inputValue, getFormat(parseOptions(this.props.type.options)), true) - if (parsed.isValid()) { - this.setMoment(parsed) - } else { - this.setState({inputValue: inputValue}) - } - } - - handleChange = (nextMoment?: Moment) => { - this.setState({inputValue: null}) - if (nextMoment) { - this.setMoment(nextMoment) - } else { - this.unset() - } - } - handleBlur = () => { - this.setState({inputValue: null}) - } - - setMoment(nextMoment: Moment) { - this.set(nextMoment.toDate().toJSON()) - this.setState({inputValue: null}) - } - - set(value: string) { - this.props.onChange(PatchEvent.from([set(value)])) - } - - unset() { - this.props.onChange(PatchEvent.from([unset()])) - } - focus() { - if (this._datepicker) { - this._datepicker.input.focus() - } - } - - setDatePicker = (datePicker: ?DatePicker) => { - this._datepicker = datePicker - } - - setDialogDatePicker = (datePicker: ?DatePicker) => { - this._dialogdatepicker = datePicker - } - - handleKeyDown = event => { - if (event.key === 'Enter') { - this.handleOpen() - } - } - - handleClose = event => { - this.setState({ - isActive: false - }) - } - - handleOpen = () => { - this.setState({ - isActive: true - }) - } - - handleDialogAction = action => { - if (action.name === 'close') { - this.handleClose() - } - - if (action.name === 'today') { - this.setMoment(moment()) - } - } - - render() { - const {value, markers, type, readOnly, level, ...rest} = this.props - const {inputValue, isActive} = this.state - const {title, description} = type - const momentValue: ?Moment = value ? moment(value) : null - - const options = parseOptions(type.options) - - const placeholder = type.placeholder || `e.g. ${moment().format(getFormat(options))}` - - const DIALOG_ACTIONS = [ - { - index: 1, - name: 'close', - title: 'Close' - }, - { - index: 2, - name: 'today', - kind: 'simple', - color: 'primary', - title: options.calendarTodayLabel, - secondary: true - } - ] - - const validation = markers.filter(marker => marker.type === 'validation') - const errors = validation.filter(marker => marker.level === 'error') - - return ( - - {readOnly && ( - 0 ? errors[0].item.message : ''} - readOnly - value={momentValue ? momentValue.format(getFormat(options)) : ''} - /> - )} - {!readOnly && ( -
0 ? styles.inputWrapperWithError : styles.inputWrapper}> - - -
- )} - {isActive && ( - -
- -
-
- )} -
- ) - } -} diff --git a/packages/@sanity/form-builder/src/inputs/DateTimeInput/index.js b/packages/@sanity/form-builder/src/inputs/DateTimeInput/index.js deleted file mode 100644 index 7ab1ab3e849..00000000000 --- a/packages/@sanity/form-builder/src/inputs/DateTimeInput/index.js +++ /dev/null @@ -1 +0,0 @@ -export {default} from './DateTimeInput' diff --git a/packages/@sanity/form-builder/src/sanity/inputResolver/defaultInputs.js b/packages/@sanity/form-builder/src/sanity/inputResolver/defaultInputs.js index 3e85f5ee60d..1469183b87a 100644 --- a/packages/@sanity/form-builder/src/sanity/inputResolver/defaultInputs.js +++ b/packages/@sanity/form-builder/src/sanity/inputResolver/defaultInputs.js @@ -3,7 +3,7 @@ import EmailInput from '../../inputs/EmailInput' import NumberInput from '../../inputs/NumberInput' import ObjectInput from '../../inputs/ObjectInput' import StringInput from '../../inputs/StringInput' -import DateTimeInput from '../../inputs/DateTimeInput' +import {DateTimeInput, DateInput} from '../../inputs/DateInputs' import TextInput from '../../inputs/TextInput' import UrlInput from '../../inputs/UrlInput' import SlugInput from '../../inputs/Slug/SlugInput' @@ -20,6 +20,7 @@ export default { text: TextInput, email: EmailInput, datetime: DateTimeInput, + date: DateInput, url: UrlInput, image: Image, file: File, diff --git a/packages/@sanity/schema/src/sanity/coreTypes.js b/packages/@sanity/schema/src/sanity/coreTypes.js index 71a840d5692..eb2e9eaef76 100644 --- a/packages/@sanity/schema/src/sanity/coreTypes.js +++ b/packages/@sanity/schema/src/sanity/coreTypes.js @@ -5,6 +5,7 @@ export default [ {name: 'block', jsonType: 'object'}, {name: 'boolean', jsonType: 'boolean'}, {name: 'datetime', jsonType: 'string'}, + {name: 'date', jsonType: 'string'}, {name: 'document', jsonType: 'object'}, {name: 'email', jsonType: 'string'}, {name: 'file', jsonType: 'object'}, diff --git a/packages/test-studio/schemas/date.js b/packages/test-studio/schemas/date.js new file mode 100644 index 00000000000..aee4e06c874 --- /dev/null +++ b/packages/test-studio/schemas/date.js @@ -0,0 +1,67 @@ +import icon from 'react-icons/lib/fa/calendar' + +export default { + name: 'dateTest', + type: 'document', + title: 'Date test', + icon, + fields: [ + { + name: 'title', + type: 'string', + title: 'Title' + }, + { + name: 'justDefaults', + type: 'date', + title: 'Datetime with default config' + }, + { + name: 'aDateWithCustomDateFormat', + type: 'date', + title: 'A date field with custom date format', + options: { + dateFormat: 'Do. MMMM YYYY' + } + }, + { + name: 'justARegularStringFieldInBetween', + type: 'string', + title: 'Some string', + description: 'A string field in between' + }, + { + name: 'aDateWithDefaults', + type: 'date', + title: 'A date field with defaults' + }, + { + name: 'aReadOnlyDate', + type: 'date', + title: 'A read only date', + readOnly: true + }, + { + name: 'customPlaceholder', + type: 'date', + title: 'Date without custom placeholder', + placeholder: 'Enter a date here' + }, + { + name: 'inArray', + type: 'array', + of: [ + { + type: 'object', + fields: [ + { + name: 'date', + type: 'date', + title: 'A date field in an array' + } + ] + } + ] + } + ] +} diff --git a/packages/test-studio/schemas/schema.js b/packages/test-studio/schemas/schema.js index 860629ee24c..1fa61336e0d 100644 --- a/packages/test-studio/schemas/schema.js +++ b/packages/test-studio/schemas/schema.js @@ -41,6 +41,7 @@ import focus from './focus' import previewImageUrlTest from './previewImageUrlTest' import previewMediaTest from './previewMediaTest' import species from './species' +import date from './date' import invalidPreviews from './invalidPreviews' export default createSchema({ @@ -59,6 +60,7 @@ export default createSchema({ objects, fieldsets, datetime, + date, richDateTest, validation, arrays,