diff --git a/browser/CHANGELOG.md b/browser/CHANGELOG.md index 1145ac613..ef9c19793 100644 --- a/browser/CHANGELOG.md +++ b/browser/CHANGELOG.md @@ -2,6 +2,12 @@ This changelog covers all three packages, as they are (for now) updated as a whole +## Unreleased + +### Atomic Browser + +- [#841](https://github.com/atomicdata-dev/atomic-server/issues/841) Add better inputs for `Timestamp` and `Date` datatypes. + ## v0.37.0 ### Atomic Browser diff --git a/browser/data-browser/src/components/forms/InputDate.tsx b/browser/data-browser/src/components/forms/InputDate.tsx new file mode 100644 index 000000000..f251e8139 --- /dev/null +++ b/browser/data-browser/src/components/forms/InputDate.tsx @@ -0,0 +1,61 @@ +import { InputProps } from './ResourceField'; +import { useValidation } from './formValidation/useValidation'; +import { styled } from 'styled-components'; +import { ErrorChipInput } from './ErrorChip'; +import { useString, validateDatatype } from '@tomic/react'; +import { InputStyled, InputWrapper } from './InputStyles'; +import { ChangeEvent } from 'react'; + +export function InputDate({ + resource, + property, + commit, + required, + ...props +}: InputProps): React.JSX.Element { + const [err, setErr, onBlur] = useValidation(); + const [value, setValue] = useString(resource, property.subject, { + commit, + validate: false, + }); + + const handleChange = (event: ChangeEvent) => { + const dateStr = event.target.value; + + if (required && dateStr) { + setErr('Required'); + setValue(undefined); + } else { + try { + validateDatatype(dateStr, property.datatype); + setValue(dateStr); + setErr(undefined); + } catch (e) { + setErr(e); + } + } + }; + + return ( + + + + + {err && {err}} + + ); +} + +const Wrapper = styled.div` + position: relative; +`; + +const StyledInputWrapper = styled(InputWrapper)` + width: min-content; +`; diff --git a/browser/data-browser/src/components/forms/InputSwitcher.tsx b/browser/data-browser/src/components/forms/InputSwitcher.tsx index 8789d1568..4f72b2286 100644 --- a/browser/data-browser/src/components/forms/InputSwitcher.tsx +++ b/browser/data-browser/src/components/forms/InputSwitcher.tsx @@ -8,6 +8,8 @@ import InputMarkdown from './InputMarkdown'; import InputNumber from './InputNumber'; import InputBoolean from './InputBoolean'; import InputSlug from './InputSlug'; +import { InputTimestamp } from './InputTimestamp'; +import { InputDate } from './InputDate'; /** Renders a fitting HTML input depending on the Datatype */ export default function InputSwitcher(props: InputProps): JSX.Element { @@ -44,9 +46,12 @@ export default function InputSwitcher(props: InputProps): JSX.Element { return ; } - // TODO: DateTime selector case Datatype.TIMESTAMP: { - return ; + return ; + } + + case Datatype.DATE: { + return ; } default: { diff --git a/browser/data-browser/src/components/forms/InputTimestamp.tsx b/browser/data-browser/src/components/forms/InputTimestamp.tsx new file mode 100644 index 000000000..06d46cbd1 --- /dev/null +++ b/browser/data-browser/src/components/forms/InputTimestamp.tsx @@ -0,0 +1,59 @@ +import { useNumber } from '@tomic/react'; +import { InputProps } from './ResourceField'; +import { useValidation } from './formValidation/useValidation'; +import { styled } from 'styled-components'; +import { ErrorChipInput } from './ErrorChip'; +import { InputStyled, InputWrapper } from './InputStyles'; +import { useDateTimeInput } from './hooks/useDateTimeInput'; + +export function InputTimestamp({ + resource, + property, + commit, + required, + ...props +}: InputProps): React.JSX.Element { + const [err, setErr, onBlur] = useValidation(); + const [value, setValue] = useNumber(resource, property.subject, { + commit, + validate: false, + }); + + const [localDate, handleChange] = useDateTimeInput( + value, + (time: number | undefined) => { + if (required && time === undefined) { + setErr('Required'); + setValue(undefined); + } else { + setErr(undefined); + setValue(time); + } + }, + ); + + return ( + + + + + {err && {err}} + + ); +} + +const Wrapper = styled.div` + flex: 1; + position: relative; +`; + +const StyledInputWrapper = styled(InputWrapper)` + width: min-content; +`; diff --git a/browser/data-browser/src/components/forms/hooks/useDateTimeInput.ts b/browser/data-browser/src/components/forms/hooks/useDateTimeInput.ts new file mode 100644 index 000000000..4065a56a5 --- /dev/null +++ b/browser/data-browser/src/components/forms/hooks/useDateTimeInput.ts @@ -0,0 +1,49 @@ +import { isNumber } from '@tomic/react'; +import { useCallback } from 'react'; + +const pad = (value: number): string => `${value}`.padStart(2, '0'); + +const timestampToDateTimeLocal = (timestamp: number): string => { + const date = new Date(timestamp); + + const year = date.getFullYear(); + const month = date.getMonth() + 1; + const day = date.getDate(); + const hours = date.getHours(); + const minutes = date.getMinutes(); + + return `${year}-${pad(month)}-${pad(day)}T${pad(hours)}:${pad(minutes)}`; +}; + +const dateTimeLocalToTimestamp = (dateTimeLocal: string): number => { + const date = new Date(dateTimeLocal); + + return date.getTime(); +}; + +export const useDateTimeInput = ( + value: number | undefined, + onChange: (value: number | undefined) => void, +): [ + localDate: string | undefined, + handleChange: (e: React.ChangeEvent) => void, +] => { + const handleChange = useCallback( + (e: React.ChangeEvent) => { + if (e.target.value) { + onChange(dateTimeLocalToTimestamp(e.target.value)); + } else { + onChange(undefined); + } + }, + [onChange], + ); + + let localDate: string | undefined = undefined; + + if (isNumber(value)) { + localDate = timestampToDateTimeLocal(value); + } + + return [localDate, handleChange]; +}; diff --git a/browser/data-browser/src/views/TablePage/EditorCells/DateTimeCell.tsx b/browser/data-browser/src/views/TablePage/EditorCells/DateTimeCell.tsx index fa4f1a5f7..d2f3de231 100644 --- a/browser/data-browser/src/views/TablePage/EditorCells/DateTimeCell.tsx +++ b/browser/data-browser/src/views/TablePage/EditorCells/DateTimeCell.tsx @@ -5,40 +5,17 @@ import { useResource, useString, } from '@tomic/react'; -import { useCallback, useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import { formatDate } from '../../../helpers/dates/formatDate'; import { InputBase } from './InputBase'; import { CellContainer, DisplayCellProps, EditCellProps } from './Type'; - -const pad = (value: number): string => `${value}`.padStart(2, '0'); - -const buildDateTimeLocalString = (date: Date): string => { - const year = date.getFullYear(); - const month = date.getMonth() + 1; - const day = date.getDate(); - const hours = date.getHours(); - const minutes = date.getMinutes(); - - return `${year}-${pad(month)}-${pad(day)}T${pad(hours)}:${pad(minutes)}`; -}; +import { useDateTimeInput } from '../../../components/forms/hooks/useDateTimeInput'; function DateTimeCellEdit({ value, onChange, }: EditCellProps): JSX.Element { - const handleChange = useCallback( - (e: React.ChangeEvent) => { - const date = new Date(e.target.value); - onChange(date.getTime()); - }, - [onChange], - ); - - let localDate: string | undefined = undefined; - - if (isNumber(value)) { - localDate = buildDateTimeLocalString(new Date(value)); - } + const [localDate, handleChange] = useDateTimeInput(value as number, onChange); return ( =4'} dev: true + /axios@1.6.2: + resolution: {integrity: sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==} + dependencies: + follow-redirects: 1.15.3 + form-data: 4.0.0 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + dev: true + /axios@1.6.2(debug@4.3.4): resolution: {integrity: sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==} dependencies: @@ -7343,6 +7342,17 @@ packages: ms: 2.1.3 dev: false + /debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + /debug@4.3.4(supports-color@9.4.0): resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} @@ -8840,6 +8850,16 @@ packages: from2: 2.3.0 dev: true + /follow-redirects@1.15.3: + resolution: {integrity: sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + dev: true + /follow-redirects@1.15.3(debug@4.3.4): resolution: {integrity: sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==} engines: {node: '>=4.0'} @@ -11071,7 +11091,7 @@ packages: cli-truncate: 2.1.0 commander: 6.2.1 cosmiconfig: 7.1.0 - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4 dedent: 0.7.0 enquirer: 2.4.1 execa: 4.1.0 @@ -11900,7 +11920,7 @@ packages: resolution: {integrity: sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==} dependencies: '@types/debug': 4.1.8 - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4 decode-named-character-reference: 1.0.2 micromark-core-commonmark: 1.1.0 micromark-factory-space: 1.1.0 @@ -16046,10 +16066,10 @@ packages: workbox-build: ^7.0.0 workbox-window: ^7.0.0 dependencies: - debug: 4.3.4(supports-color@9.4.0) + debug: 4.3.4 fast-glob: 3.3.2 pretty-bytes: 6.1.1 - vite: 5.0.12(@types/node@20.11.5) + vite: 5.0.12 workbox-build: 7.0.0 workbox-window: 7.0.0 transitivePeerDependencies: @@ -16061,15 +16081,50 @@ packages: peerDependencies: vite: ^2 || ^3 || ^4 || ^5 dependencies: - axios: 1.6.2(debug@4.3.4) + axios: 1.6.2 clean-css: 5.3.3 flat-cache: 3.0.4 picocolors: 1.0.0 - vite: 5.0.12(@types/node@20.11.5) + vite: 5.0.12 transitivePeerDependencies: - debug dev: true + /vite@5.0.12: + resolution: {integrity: sha512-4hsnEkG3q0N4Tzf1+t6NdN9dg/L3BM+q8SWgbSPnJvrgH2kgdyzfVJwbR1ic69/4uMJJ/3dqDZZE5/WwqW8U1w==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + dependencies: + esbuild: 0.19.6 + postcss: 8.4.33 + rollup: 4.6.0 + optionalDependencies: + fsevents: 2.3.3 + dev: true + /vite@5.0.12(@types/node@20.11.5): resolution: {integrity: sha512-4hsnEkG3q0N4Tzf1+t6NdN9dg/L3BM+q8SWgbSPnJvrgH2kgdyzfVJwbR1ic69/4uMJJ/3dqDZZE5/WwqW8U1w==} engines: {node: ^18.0.0 || >=20.0.0}