1
0
mirror of https://github.com/mattermost/focalboard.git synced 2024-11-24 08:22:29 +02:00

Implement Date ranges (#660)

* initial commit

* update to work around state issue

* update comments

* temp commit

* add unit tests, cleanup

* language extract, lint fix

* update stored object to be same as state object

* fixing merge issues

* update dates to use locale

* linter fixes, test cleanup

* self review

* add unit test, fixes

* fix linter

* remove dateFormat from calls

* move DateRange to components

* update translations

* spacing fix

* update tests, fix some hour inconsistencies

* remove logging

* more updates

* more cleanup

* update to disply mm/dd/yyyy when editing

* update to disply mm/dd/yyyy when editing

* input date use locale

* update css to make selection round

* update css to make begin/end round
This commit is contained in:
Scott Bishel 2021-08-06 10:44:01 -06:00 committed by GitHub
parent c734cfb8d1
commit 383723a128
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 827 additions and 30 deletions

View File

@ -8,8 +8,8 @@
"typescriptreact"
],
"files.exclude": {
".vscode": true,
"**/__snapshots__": true,
".vscode": false,
"**/__snapshots__": false,
"**/node_modules": true,
"bin": true,
"files": true,

View File

@ -32,12 +32,13 @@
"ContentBlock.moveDown": "Move down",
"ContentBlock.moveUp": "Move up",
"ContentBlock.text": "text",
"DashboardPage.title": "Welcome to Focalboard (Beta)!",
"DashboardPage.message": "Use Focalboard to create and track tasks for projects big and small using familiar kanban-boards, tables, and other views.",
"DashboardPage.title": "Welcome to Focalboard (Beta)!",
"Dialog.closeDialog": "Close dialog",
"EditableDayPicker.today": "Today",
"EmptyCenterPanel.no-content": "Add or select a board from the sidebar to get started.",
"EmptyCenterPanel.workspace": "This is the workspace for:",
"Error.websocket-closed": "Websocket connection closed, connection interrupted. If this persists, check your server or web proxy configuration.",
"Filter.includes": "includes",
"Filter.is-empty": "is empty",
"Filter.is-not-empty": "is not empty",
@ -90,37 +91,22 @@
"Sidebar.add-board": "+ Add Board",
"Sidebar.add-template": "New template",
"Sidebar.changePassword": "Change password",
"Sidebar.chinese": "Traditional Chinese",
"Sidebar.dark-theme": "Dark theme",
"Sidebar.default-theme": "Default theme",
"Sidebar.delete-board": "Delete board",
"Sidebar.delete-template": "Delete",
"Sidebar.duplicate-board": "Duplicate board",
"Sidebar.dutch": "Dutch",
"Sidebar.edit-template": "Edit",
"Sidebar.empty-board": "Empty board",
"Sidebar.english": "English",
"Sidebar.export-archive": "Export archive",
"Sidebar.french": "French",
"Sidebar.german": "German",
"Sidebar.import-archive": "Import archive",
"Sidebar.invite-users": "Invite Users",
"Sidebar.japanese": "Japanese",
"Sidebar.light-theme": "Light theme",
"Sidebar.logout": "Log out",
"Sidebar.no-views-in-board": "No pages inside",
"Sidebar.occitan": "Occitan",
"Sidebar.random-icons": "Random icons",
"Sidebar.russian": "Russian",
"Sidebar.select-a-template": "Select a template",
"Sidebar.set-language": "Set language",
"Sidebar.set-theme": "Set theme",
"Sidebar.settings": "Settings",
"Sidebar.simplified-chinese": "Simplified Chinese",
"Sidebar.spanish": "Spanish",
"Sidebar.system-theme": "System theme",
"Sidebar.template-from-board": "New template from board",
"Sidebar.turkish": "Turkish",
"Sidebar.untitled": "Untitled",
"Sidebar.untitled-board": "(Untitled Board)",
"Sidebar.untitled-view": "(Untitled View)",
@ -147,6 +133,7 @@
"ViewHeader.delete-template": "Delete",
"ViewHeader.edit-template": "Edit",
"ViewHeader.empty-card": "Empty card",
"ViewHeader.export-board-archive": "Export board archive",
"ViewHeader.export-complete": "Export complete!",
"ViewHeader.export-csv": "Export to CSV",
"ViewHeader.export-failed": "Export failed!",
@ -160,11 +147,11 @@
"ViewHeader.share-board": "Share board",
"ViewHeader.sort": "Sort",
"ViewHeader.untitled": "Untitled",
"ViewTitle.hide-description": "Hide description",
"ViewTitle.hide-description": "hide description",
"ViewTitle.pick-icon": "Pick icon",
"ViewTitle.random-icon": "Random",
"ViewTitle.remove-icon": "Remove icon",
"ViewTitle.show-description": "Show description",
"ViewTitle.show-description": "show description",
"ViewTitle.untitled-board": "Untitled board",
"Workspace.editing-board-template": "You're editing a board template",
"default-properties.title": "Title",
@ -175,4 +162,4 @@
"login.register-button": "or create an account if you don't have one",
"register.login-button": "or login if you already have an account",
"register.signup-title": "Sign up for your account"
}
}

View File

@ -0,0 +1,103 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/properties/dateRange cancel set via text input 1`] = `
<div>
<div
class="DateRange "
>
<button
class="Button "
type="button"
>
<span>
June 15 -&gt; June 20
</span>
</button>
</div>
</div>
`;
exports[`components/properties/dateRange handle clear 1`] = `
<div>
<div
class="DateRange "
>
<button
class="Button "
type="button"
>
<span>
June 15
</span>
</button>
</div>
</div>
`;
exports[`components/properties/dateRange returns default correctly 1`] = `
<div>
<div
class="DateRange "
>
<button
class="Button "
type="button"
>
<span>
Empty
</span>
</button>
</div>
</div>
`;
exports[`components/properties/dateRange returns local correctly - es local 1`] = `
<div>
<div
class="DateRange "
>
<button
class="Button "
type="button"
>
<span>
15 de junio
</span>
</button>
</div>
</div>
`;
exports[`components/properties/dateRange set via text input 1`] = `
<div>
<div
class="DateRange "
>
<button
class="Button "
type="button"
>
<span>
June 15 -&gt; June 20
</span>
</button>
</div>
</div>
`;
exports[`components/properties/dateRange set via text input, es locale 1`] = `
<div>
<div
class="DateRange "
>
<button
class="Button "
type="button"
>
<span>
15 de junio -&gt; 20 de junio
</span>
</button>
</div>
</div>
`;

View File

@ -0,0 +1,155 @@
.DateRange {
.octo-propertyvalue {
white-space: none;
}
.inputContainer {
display: flex;
.Editable {
width: 50% !important;
}
}
input {
cursor: text;
overflow: hidden;
text-overflow: ellipsis;
min-height: 24px;
width: 100%;
margin: 2px;
&.active {
min-width: 100px;
}
&::placeholder {
color: rgba(var(--body-color), 0.4);
opacity: 1;
}
&.error {
border: 1px solid var(--error-color);
border-radius: var(--default-rad);
}
}
.Modal{
position: absolute;
top: 0px;
left: 0px;
margin-bottom: 100px;
}
.Button{
width: 100%;
justify-content: left;
&:hover {
background-color: transparent;
}
}
.menu-option {
display: flex;
flex-direction: row;
align-items: center;
font-size: 14px;
line-height: 24px;
font-weight: 400;
height: 32px;
padding: 4px 0px;
cursor: pointer;
&:hover {
background: rgba(var(--button-bg), 0.08);
}
> * {
margin-left: 6px;
}
> *:first-child {
margin-left: 0;
}
> .menu-name {
display: flex;
flex-grow: 1;
white-space: nowrap;
}
> .SubmenuTriangleIcon {
fill: rgba(var(--body-color), 0.7);
}
> .Icon {
opacity: 0.56;
width: 16px;
height: 16px;
line-height: 16px;
}
> .IconButton .Icon {
margin-right: 0;
}
}
.DayPicker {
}
.DayPickerInput-Overlay {
background-color: rgba(var(--main-bg));
box-shadow: 0 0 0 1px hsla(0, 0%, 0%, 0.1),0 4px 11px hsla(0, 0%, 0%, 0.1);
line-height: 100%;
}
.DayPicker-Month {
margin: 0;
margin-top: 1em;
display: flex;
flex-direction: column;
}
.DayPicker-Body {
display: flex;
flex-direction: column;
}
.DayPicker-WeekdaysRow {
display: flex;
flex-direction: row;
}
.DayPicker-Weekday {
display: block;
width: 36px;
height: 36px;
}
.DayPicker-Week {
display: flex;
flex-direction: row;
}
.DayPicker-Day {
border-radius: unset;
display: block;
width: 36px;
height: 36px;
}
.DayPicker-Day--today {
color: #c74655;
}
.DayPicker-Day--start {
border-top-left-radius: 50% !important;
border-bottom-left-radius: 50% !important;
}
.DayPicker-Day--end {
border-top-right-radius: 50% !important;
border-bottom-right-radius: 50% !important;
}
.DayPicker-Day--selected:not(.DayPicker-Day--disabled):not(.DayPicker-Day--outside) {
background-color: rgb(var(--button-bg));;
color: rgba(var(--button-fg), 0.5);
}
.DayPicker-Day--selected:not(.DayPicker-Day--start):not(.DayPicker-Day--end):not(.DayPicker-Day--outside) {
color: rgb(var(--button-bg));;
background-color: rgba(var(--button-bg), 0.2);
}
.DayPicker:not(.DayPicker--interactionDisabled)
.DayPicker-Day:not(.DayPicker-Day--disabled):not(.DayPicker-Day--selected):not(.DayPicker-Day--outside):hover {
background-color: rgba(var(--body-color), 0.2);
}
}

View File

@ -0,0 +1,245 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react'
import {render} from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import '@testing-library/jest-dom'
import {IntlProvider} from 'react-intl'
import DateRange from '../dateRange/dateRange'
const wrapIntl = (children: any) => <IntlProvider locale='en'>{children}</IntlProvider>
// create Dates for specific days for this year.
const June15 = new Date(Date.UTC(new Date().getFullYear(), 5, 15, 12))
const June15Local = new Date(new Date().getFullYear(), 5, 15, 12)
const June20 = new Date(Date.UTC(new Date().getFullYear(), 5, 20, 12))
describe('components/properties/dateRange', () => {
beforeEach(() => {
// Quick fix to disregard console error when unmounting a component
console.error = jest.fn()
document.execCommand = jest.fn()
})
test('returns default correctly', () => {
const component = wrapIntl(
<DateRange
className='octo-propertyvalue'
value={''}
onChange={jest.fn()}
/>,
)
const {container} = render(component)
expect(container).toMatchSnapshot()
})
test('returns local correctly - es local', () => {
const component = (
<IntlProvider locale='es'>
<DateRange
className='octo-propertyvalue'
value={June15Local.getTime().toString()}
onChange={jest.fn()}
/>
</IntlProvider>
)
const {container, getByText} = render(component)
const input = getByText('15 de junio')
expect(input).not.toBeNull()
expect(container).toMatchSnapshot()
})
test('handles calendar click event', () => {
const callback = jest.fn()
const component = wrapIntl(
<DateRange
className='octo-propertyvalue'
value={''}
onChange={callback}
/>,
)
const date = new Date()
const fifteenth = Date.UTC(date.getFullYear(), date.getMonth(), 15, 12)
const {getByText, getByTitle} = render(component)
const dayDisplay = getByText('Empty')
userEvent.click(dayDisplay)
const day = getByText('15')
const modal = getByTitle('Close').children[0]
userEvent.click(day)
userEvent.click(modal)
const rObject = {from: fifteenth}
expect(callback).toHaveBeenCalledWith(JSON.stringify(rObject))
})
test('handles setting range', () => {
const callback = jest.fn()
const component = wrapIntl(
<DateRange
className='octo-propertyvalue'
value={''}
onChange={callback}
/>,
)
// open modal
const {getByText, getByTitle} = render(component)
const dayDisplay = getByText('Empty')
userEvent.click(dayDisplay)
// select start date
const date = new Date()
const fifteenth = Date.UTC(date.getFullYear(), date.getMonth(), 15, 12)
const start = getByText('15')
userEvent.click(start)
// create range
const endDate = getByText('End date')
userEvent.click(endDate)
const twentieth = Date.UTC(date.getFullYear(), date.getMonth(), 20, 12)
const end = getByText('20')
const modal = getByTitle('Close').children[0]
userEvent.click(end)
userEvent.click(modal)
const rObject = {from: fifteenth, to: twentieth}
expect(callback).toHaveBeenCalledWith(JSON.stringify(rObject))
})
test('handle clear', () => {
const callback = jest.fn()
const component = wrapIntl(
<DateRange
className='octo-propertyvalue'
value={June15Local.getTime().toString()}
onChange={callback}
/>,
)
const {container, getByText, getByTitle} = render(component)
expect(container).toMatchSnapshot()
// open modal
const dayDisplay = getByText('June 15')
userEvent.click(dayDisplay)
const clear = getByText('Clear')
const modal = getByTitle('Close').children[0]
userEvent.click(clear)
userEvent.click(modal)
expect(callback).toHaveBeenCalledWith('')
})
test('set via text input', () => {
const callback = jest.fn()
const component = wrapIntl(
<DateRange
className='octo-propertyvalue'
value={'{"from": ' + June15.getTime().toString() + ',"to": ' + June20.getTime().toString() + '}'}
onChange={callback}
/>,
)
const {container, getByRole, getByTitle, getByDisplayValue} = render(component)
expect(container).toMatchSnapshot()
// open modal
const dayDisplay = getByRole('button', {name: 'June 15 -> June 20'})
userEvent.click(dayDisplay)
const fromInput = getByDisplayValue('June 15')
const toInput = getByDisplayValue('June 20')
userEvent.type(fromInput, '{selectall}{delay}07/15/2021{enter}')
userEvent.type(toInput, '{selectall}{delay}07/20/2021{enter}')
const July15 = new Date(Date.UTC(2021, 6, 15, 12))
const July20 = new Date(Date.UTC(2021, 6, 20, 12))
const modal = getByTitle('Close').children[0]
userEvent.click(modal)
// {from: '2021-07-15', to: '2021-07-20'}
const retVal = '{"from":' + July15.getTime().toString() + ',"to":' + July20.getTime().toString() + '}'
expect(callback).toHaveBeenCalledWith(retVal)
})
test('set via text input, es locale', () => {
const callback = jest.fn()
const component = (
<IntlProvider locale='es'>
<DateRange
className='octo-propertyvalue'
value={'{"from": ' + June15.getTime().toString() + ',"to": ' + June20.getTime().toString() + '}'}
onChange={callback}
/>
</IntlProvider>
)
const {container, getByRole, getByTitle, getByDisplayValue} = render(component)
expect(container).toMatchSnapshot()
// open modal
const dayDisplay = getByRole('button', {name: '15 de junio -> 20 de junio'})
userEvent.click(dayDisplay)
const fromInput = getByDisplayValue('15 de junio')
const toInput = getByDisplayValue('20 de junio')
userEvent.type(fromInput, '{selectall}15/07/2021{enter}')
userEvent.type(toInput, '{selectall}20/07/2021{enter}')
const July15 = new Date(Date.UTC(2021, 6, 15, 12))
const July20 = new Date(Date.UTC(2021, 6, 20, 12))
const modal = getByTitle('Close').children[0]
userEvent.click(modal)
// {from: '2021-07-15', to: '2021-07-20'}
const retVal = '{"from":' + July15.getTime().toString() + ',"to":' + July20.getTime().toString() + '}'
expect(callback).toHaveBeenCalledWith(retVal)
})
test('cancel set via text input', () => {
const callback = jest.fn()
const component = wrapIntl(
<DateRange
className='octo-propertyvalue'
value={'{"from": ' + June15.getTime().toString() + ',"to": ' + June20.getTime().toString() + '}'}
onChange={callback}
/>,
)
const {container, getByRole, getByTitle, getByDisplayValue} = render(component)
expect(container).toMatchSnapshot()
// open modal
const dayDisplay = getByRole('button', {name: 'June 15 -> June 20'})
userEvent.click(dayDisplay)
const fromInput = getByDisplayValue('June 15')
const toInput = getByDisplayValue('June 20')
userEvent.type(fromInput, '{selectall}07/15/2021{delay}{esc}')
userEvent.type(toInput, '{selectall}07/20/2021{delay}{esc}')
const modal = getByTitle('Close').children[0]
userEvent.click(modal)
// const retVal = {from: '2021-06-15', to: '2021-06-20'}
const retVal = '{"from":' + June15.getTime().toString() + ',"to":' + June20.getTime().toString() + '}'
expect(callback).toHaveBeenCalledWith(retVal)
})
})

View File

@ -0,0 +1,266 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useState} from 'react'
import {useIntl} from 'react-intl'
import {DateUtils} from 'react-day-picker'
import MomentLocaleUtils from 'react-day-picker/moment'
import DayPicker from 'react-day-picker/DayPicker'
import moment from 'moment'
import Editable from '../../../widgets/editable'
import SwitchOption from '../../../widgets/menu/switchOption'
import Button from '../../../widgets/buttons/button'
import Modal from '../../../components/modal'
import ModalWrapper from '../../../components/modalWrapper'
import 'react-day-picker/lib/style.css'
import './dateRange.scss'
import {Utils} from '../../../utils'
type Props = {
className: string
value: string
onChange: (value: string) => void
}
type DateProperty = {
from?: number
to?: number
includeTime?: boolean
timeZone?: string
}
const loadedLocales: Record<string, any> = {}
function DateRange(props: Props): JSX.Element {
const {className, value, onChange} = props
const intl = useIntl()
const timeZoneOffset = new Date().getTimezoneOffset() * 60 * 1000
const getDisplayDate = (date: Date | null | undefined) => {
let displayDate = ''
if (date) {
displayDate = Utils.displayDate(date, intl)
}
return displayDate
}
const createDatePropertyFromString = (initialValue: string) => {
let dateProperty: DateProperty = {}
if (initialValue) {
const singleDate = new Date(Number(initialValue))
if (singleDate && DateUtils.isDate(singleDate)) {
dateProperty.from = singleDate.getTime()
} else {
try {
dateProperty = JSON.parse(initialValue)
} catch {
//Don't do anything, return empty dateProperty
}
}
}
return dateProperty
}
const [dateProperty, setDateProperty] = useState<DateProperty>(createDatePropertyFromString(value as string))
const [showDialog, setShowDialog] = useState(false)
// Keep dateProperty as UTC,
// dateFrom / dateTo will need converted to local time, to ensure date stays consistent
// dateFrom / dateTo will be used for input and calendar dates
const dateFrom = dateProperty.from ? new Date(dateProperty.from + (dateProperty.includeTime ? 0 : timeZoneOffset)) : undefined
const dateTo = dateProperty.to ? new Date(dateProperty.to + (dateProperty.includeTime ? 0 : timeZoneOffset)) : undefined
const [fromInput, setFromInput] = useState<string>(getDisplayDate(dateFrom))
const [toInput, setToInput] = useState<string>(getDisplayDate(dateTo))
const isRange = dateTo !== undefined
const locale = intl.locale.toLowerCase()
if (locale && locale !== 'en' && !loadedLocales[locale]) {
/* eslint-disable global-require */
loadedLocales[locale] = require(`moment/locale/${locale}`)
/* eslint-disable global-require */
}
const handleDayClick = (day: Date) => {
const range : DateProperty = {}
if (isRange) {
const newRange = DateUtils.addDayToRange(day, {from: dateFrom, to: dateTo})
range.from = newRange.from?.getTime()
range.to = newRange.to?.getTime()
} else {
range.from = day.getTime()
range.to = undefined
}
saveRangeValue(range)
}
const onRangeClick = () => {
let range : DateProperty = {
from: dateFrom?.getTime(),
to: dateFrom?.getTime(),
}
if (isRange) {
range = ({
from: dateFrom?.getTime(),
to: undefined,
})
}
saveRangeValue(range)
}
const onClear = () => {
saveRangeValue({})
}
const saveRangeValue = (range: DateProperty) => {
const rangeUTC = {...range}
if (rangeUTC.from) {
rangeUTC.from -= dateProperty.includeTime ? 0 : timeZoneOffset
}
if (rangeUTC.to) {
rangeUTC.to -= dateProperty.includeTime ? 0 : timeZoneOffset
}
setDateProperty(rangeUTC)
setFromInput(getDisplayDate(range.from ? new Date(range.from) : undefined))
setToInput(getDisplayDate(range.to ? new Date(range.to) : undefined))
}
let displayValue = ''
if (dateFrom) {
displayValue = getDisplayDate(dateFrom)
}
if (dateTo) {
displayValue += ' -> ' + getDisplayDate(dateTo)
}
const onClose = () => {
// not actually setting here,
// but using to retreive the current state
setDateProperty((current) => {
if (current && current.from) {
onChange(JSON.stringify(current))
} else {
onChange('')
}
return {...current}
})
setShowDialog(false)
}
return (
<div className={'DateRange '}>
<Button
onClick={() => setShowDialog(true)}
>
{displayValue || intl.formatMessage({id: 'DateRange.empty', defaultMessage: 'Empty'})}
</Button>
{showDialog &&
<ModalWrapper>
<Modal
onClose={() => onClose()}
>
<div
className={className + '-overlayWrapper'}
>
<div className={className + '-overlay'}>
<div className={'inputContainer'}>
<Editable
value={fromInput}
placeholderText={moment.localeData(locale).longDateFormat('L')}
onFocus={() => {
if (dateFrom) {
return setFromInput(Utils.inputDate(dateFrom, intl))
}
return undefined
}}
onChange={setFromInput}
onSave={() => {
const newDate = MomentLocaleUtils.parseDate(fromInput, 'L', intl.locale)
if (newDate && DateUtils.isDate(newDate)) {
newDate.setHours(12)
const range : DateProperty = {
from: newDate.getTime(),
to: dateTo?.getTime(),
}
saveRangeValue(range)
} else {
setFromInput(getDisplayDate(dateFrom))
}
}}
onCancel={() => {
setFromInput(getDisplayDate(dateFrom))
}}
/>
{dateTo &&
<Editable
value={toInput}
placeholderText={moment.localeData(locale).longDateFormat('L')}
onFocus={() => {
if (dateTo) {
return setToInput(Utils.inputDate(dateTo, intl))
}
return undefined
}}
onChange={setToInput}
onSave={() => {
const newDate = MomentLocaleUtils.parseDate(toInput, 'L', intl.locale)
if (newDate && DateUtils.isDate(newDate)) {
newDate.setHours(12)
const range : DateProperty = {
from: dateFrom?.getTime(),
to: newDate.getTime(),
}
saveRangeValue(range)
} else {
setToInput(getDisplayDate(dateTo))
}
}}
onCancel={() => {
setToInput(getDisplayDate(dateTo))
}}
/>
}
</div>
<DayPicker
onDayClick={handleDayClick}
initialMonth={dateFrom || new Date()}
showOutsideDays={true}
locale={locale}
localeUtils={MomentLocaleUtils}
todayButton={intl.formatMessage({id: 'DateRange.today', defaultMessage: 'Today'})}
selectedDays={[dateFrom, dateTo ? {from: dateFrom, to: dateTo} : {from: dateFrom, to: dateFrom}]}
modifiers={dateTo ? {start: dateFrom, end: dateTo} : {start: dateFrom, end: dateFrom}}
/>
<hr/>
<SwitchOption
key={'EndDateOn'}
id={'EndDateOn'}
name={intl.formatMessage({id: 'DateRange.endDate', defaultMessage: 'End date'})}
isOn={isRange}
onClick={onRangeClick}
/>
<hr/>
<div
className='MenuOption menu-option'
>
<Button
onClick={onClear}
>
{intl.formatMessage({id: 'DateRange.clear', defaultMessage: 'Clear'})}
</Button>
</div>
</div>
</div>
</Modal>
</ModalWrapper>
}
</div>
)
}
export default DateRange

View File

@ -16,7 +16,6 @@ import ValueSelector from '../widgets/valueSelector'
import Label from '../widgets/label'
import EditableDayPicker from '../widgets/editableDayPicker'
import Switch from '../widgets/switch'
import IconButton from '../widgets/buttons/iconButton'
import CloseIcon from '../widgets/icons/close'
@ -28,6 +27,7 @@ import LastModifiedBy from './properties/lastModifiedBy/lastModifiedBy'
import LastModifiedAt from './properties/lastModifiedAt/lastModifiedAt'
import CreatedAt from './properties/createdAt/createdAt'
import CreatedBy from './properties/createdBy/createdBy'
import DateRange from './properties/dateRange/dateRange'
type Props = {
board: Board
@ -198,7 +198,7 @@ const PropertyValueElement = (props:Props): JSX.Element => {
return <div className='octo-propertyvalue'>{displayValue}</div>
}
return (
<EditableDayPicker
<DateRange
className='octo-propertyvalue'
value={value as string}
onChange={(newValue) => mutator.changePropertyValue(card, propertyTemplate.id, newValue)}

View File

@ -3,6 +3,8 @@
import {IntlShape} from 'react-intl'
import {DateUtils} from 'react-day-picker'
import {Block, createBlock} from './blocks/block'
import {IPropertyTemplate, createBoard} from './blocks/board'
import {BoardView, createBoardView} from './blocks/boardView'
@ -50,7 +52,23 @@ class OctoUtils {
}
case 'date': {
if (propertyValue) {
displayValue = Utils.displayDate(new Date(parseInt(propertyValue as string, 10)), intl)
const singleDate = new Date(parseInt(propertyValue as string, 10))
if (singleDate && DateUtils.isDate(singleDate)) {
displayValue = Utils.displayDate(new Date(parseInt(propertyValue as string, 10)), intl)
} else {
try {
const dateValue = JSON.parse(propertyValue as string)
if (dateValue.from) {
displayValue = Utils.displayDate(new Date(dateValue.from), intl)
}
if (dateValue.to) {
displayValue += ' -> '
displayValue += Utils.displayDate(new Date(dateValue.to), intl)
}
} catch {
// do nothing
}
}
}
break
}

View File

@ -86,6 +86,21 @@ describe('utils', () => {
})
})
describe('input date', () => {
const currentYear = new Date().getFullYear()
const date = new Date(currentYear, 6, 9)
it('should show mm/dd/yyyy for current year', () => {
const intl = createIntl({locale: 'en-us'})
expect(Utils.inputDate(date, intl)).toBe(`07/09/${currentYear}`)
})
it('should show dd/mm/yyyy for current year, es local', () => {
const intl = createIntl({locale: 'es-es'})
expect(Utils.inputDate(date, intl)).toBe(`09/07/${currentYear}`)
})
})
describe('display date and time', () => {
const intl = createIntl({locale: 'en-us'})

View File

@ -159,7 +159,6 @@ class Utils {
}
// Date and Time
private static yearOption(date: Date) {
const isCurrentYear = date.getFullYear() === new Date().getFullYear()
return isCurrentYear ? undefined : 'numeric'
@ -173,6 +172,14 @@ class Utils {
})
}
static inputDate(date: Date, intl: IntlShape): string {
return intl.formatDate(date, {
year: 'numeric',
month: '2-digit',
day: '2-digit',
})
}
static displayDateTime(date: Date, intl: IntlShape): string {
return intl.formatDate(date, {
year: Utils.yearOption(date),

View File

@ -16,6 +16,7 @@ export type EditableProps = {
validator?: (value: string) => boolean
onCancel?: () => void
onSave?: (saveType: 'onEnter'|'onEsc'|'onBlur') => void
onFocus?: () => void
}
export type Focusable = {
@ -33,7 +34,8 @@ export type ElementProps = {
onBlur: () => void,
onKeyDown: (e: React.KeyboardEvent<ElementType>) => void,
readOnly?: boolean,
spellCheck?: boolean
spellCheck?: boolean,
onFocus?: () => void,
}
export function useEditable(
@ -109,6 +111,7 @@ export function useEditable(
},
readOnly: readonly,
spellCheck: props.spellCheck,
onFocus: props.onFocus,
}
}

View File

@ -7,7 +7,6 @@
text-overflow: ellipsis;
border: 1px solid transparent;
min-height: 24px;
&.active {
min-width: 100px;
}
@ -21,17 +20,16 @@
}
}
}
.DayPickerInput-Overlay {
background-color: rgba(var(--main-bg));
box-shadow: 0 0 0 1px hsla(0, 0%, 0%, 0.1),0 4px 11px hsla(0, 0%, 0%, 0.1);
line-height: 100%;
}
.DayPicker-Day--today {
color: #c74655;
}
.DayPicker:not(.DayPicker--interactionDisabled)
.DayPicker-Day:not(.DayPicker-Day--disabled):not(.DayPicker-Day--selected):not(.DayPicker-Day--outside):hover {
background-color: rgba(var(--body-color), 0.2);