Don't use property value for key construction. (#2317)

* Don't use property value for key construction.

* Use value from props as current value in `DateRange` component:
 - this allows to update it properly in the dialog when the value on the server is updated
 - current value is stored in `PropertyValueElement` similar to other property value components
 - fixed stale closure in `Modal` component
 - unit tests for `DateRange` updated (wrapper with current value created)
This commit is contained in:
kamre 2022-02-15 19:24:31 +03:00 committed by GitHub
parent ed33918d0a
commit 46f4185d30
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 59 additions and 49 deletions

View file

@ -135,10 +135,9 @@ const CardDetailProperties = (props: Props) => {
return ( return (
<div className='octo-propertylist CardDetailProperties'> <div className='octo-propertylist CardDetailProperties'>
{board.fields.cardProperties.map((propertyTemplate: IPropertyTemplate) => { {board.fields.cardProperties.map((propertyTemplate: IPropertyTemplate) => {
const propertyValue = card.fields.properties[propertyTemplate.id]
return ( return (
<div <div
key={propertyTemplate.id + '-' + propertyTemplate.type + '-' + propertyValue} key={propertyTemplate.id + '-' + propertyTemplate.type}
className='octo-propertyrow' className='octo-propertyrow'
> >
{props.readonly && <div className='octo-propertyname octo-propertyname--readonly'>{propertyTemplate.name}</div>} {props.readonly && <div className='octo-propertyname octo-propertyname--readonly'>{propertyTemplate.name}</div>}

View file

@ -1,6 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import React, {useRef, useEffect} from 'react' import React, {useRef, useEffect, useCallback} from 'react'
import IconButton from '../widgets/buttons/iconButton' import IconButton from '../widgets/buttons/iconButton'
import CloseIcon from '../widgets/icons/close' import CloseIcon from '../widgets/icons/close'
@ -17,20 +17,19 @@ const Modal = (props: Props): JSX.Element => {
const {position, onClose, children} = props const {position, onClose, children} = props
const closeOnBlur = (e: Event) => { const closeOnBlur = useCallback((e: Event) => {
if (e.target && node.current?.contains(e.target as Node)) { if (e.target && node.current?.contains(e.target as Node)) {
return return
} }
onClose() onClose()
} }, [onClose])
useEffect(() => { useEffect(() => {
document.addEventListener('click', closeOnBlur, true) document.addEventListener('click', closeOnBlur, true)
return () => { return () => {
document.removeEventListener('click', closeOnBlur, true) document.removeEventListener('click', closeOnBlur, true)
} }
}, []) }, [closeOnBlur])
return ( return (
<div <div

View file

@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import React from 'react' import React, {useState} from 'react'
import {render} from '@testing-library/react' import {render} from '@testing-library/react'
import userEvent from '@testing-library/user-event' import userEvent from '@testing-library/user-event'
import {IntlProvider} from 'react-intl' import {IntlProvider} from 'react-intl'
@ -17,6 +17,27 @@ const June15 = new Date(Date.UTC(new Date().getFullYear(), 5, 15, 12))
const June15Local = new Date(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)) const June20 = new Date(Date.UTC(new Date().getFullYear(), 5, 20, 12))
type Props = {
initialValue?: string
showEmptyPlaceholder?: boolean
onChange?: (value: string) => void
}
const DateRangeWrapper = (props: Props): JSX.Element => {
const [value, setValue] = useState(props.initialValue || '')
return (
<DateRange
className='octo-propertyvalue'
value={value}
showEmptyPlaceholder={props.showEmptyPlaceholder}
onChange={(newValue) => {
setValue(newValue)
props.onChange?.(newValue)
}}
/>
)
}
describe('components/properties/dateRange', () => { describe('components/properties/dateRange', () => {
beforeEach(() => { beforeEach(() => {
// Quick fix to disregard console error when unmounting a component // Quick fix to disregard console error when unmounting a component
@ -26,9 +47,8 @@ describe('components/properties/dateRange', () => {
test('returns default correctly', () => { test('returns default correctly', () => {
const component = wrapIntl( const component = wrapIntl(
<DateRange <DateRangeWrapper
className='octo-propertyvalue' initialValue=''
value={''}
onChange={jest.fn()} onChange={jest.fn()}
/>, />,
) )
@ -40,9 +60,8 @@ describe('components/properties/dateRange', () => {
test('returns local correctly - es local', () => { test('returns local correctly - es local', () => {
const component = ( const component = (
<IntlProvider locale='es'> <IntlProvider locale='es'>
<DateRange <DateRangeWrapper
className='octo-propertyvalue' initialValue={June15Local.getTime().toString()}
value={June15Local.getTime().toString()}
onChange={jest.fn()} onChange={jest.fn()}
/> />
</IntlProvider> </IntlProvider>
@ -57,9 +76,8 @@ describe('components/properties/dateRange', () => {
test('handles calendar click event', () => { test('handles calendar click event', () => {
const callback = jest.fn() const callback = jest.fn()
const component = wrapIntl( const component = wrapIntl(
<DateRange <DateRangeWrapper
className='octo-propertyvalue' initialValue=''
value={''}
showEmptyPlaceholder={true} showEmptyPlaceholder={true}
onChange={callback} onChange={callback}
/>, />,
@ -84,9 +102,8 @@ describe('components/properties/dateRange', () => {
test('handles setting range', () => { test('handles setting range', () => {
const callback = jest.fn() const callback = jest.fn()
const component = wrapIntl( const component = wrapIntl(
<DateRange <DateRangeWrapper
className='octo-propertyvalue' initialValue={''}
value={''}
showEmptyPlaceholder={true} showEmptyPlaceholder={true}
onChange={callback} onChange={callback}
/>, />,
@ -121,9 +138,8 @@ describe('components/properties/dateRange', () => {
test('handle clear', () => { test('handle clear', () => {
const callback = jest.fn() const callback = jest.fn()
const component = wrapIntl( const component = wrapIntl(
<DateRange <DateRangeWrapper
className='octo-propertyvalue' initialValue={June15Local.getTime().toString()}
value={June15Local.getTime().toString()}
onChange={callback} onChange={callback}
/>, />,
) )
@ -146,9 +162,8 @@ describe('components/properties/dateRange', () => {
test('set via text input', () => { test('set via text input', () => {
const callback = jest.fn() const callback = jest.fn()
const component = wrapIntl( const component = wrapIntl(
<DateRange <DateRangeWrapper
className='octo-propertyvalue' initialValue={'{"from": ' + June15.getTime().toString() + ',"to": ' + June20.getTime().toString() + '}'}
value={'{"from": ' + June15.getTime().toString() + ',"to": ' + June20.getTime().toString() + '}'}
onChange={callback} onChange={callback}
/>, />,
) )
@ -183,9 +198,8 @@ describe('components/properties/dateRange', () => {
const component = ( const component = (
<IntlProvider locale='es'> <IntlProvider locale='es'>
<DateRange <DateRangeWrapper
className='octo-propertyvalue' initialValue={'{"from": ' + June15.getTime().toString() + ',"to": ' + June20.getTime().toString() + '}'}
value={'{"from": ' + June15.getTime().toString() + ',"to": ' + June20.getTime().toString() + '}'}
onChange={callback} onChange={callback}
/> />
</IntlProvider> </IntlProvider>
@ -218,9 +232,8 @@ describe('components/properties/dateRange', () => {
test('cancel set via text input', () => { test('cancel set via text input', () => {
const callback = jest.fn() const callback = jest.fn()
const component = wrapIntl( const component = wrapIntl(
<DateRange <DateRangeWrapper
className='octo-propertyvalue' initialValue={'{"from": ' + June15.getTime().toString() + ',"to": ' + June20.getTime().toString() + '}'}
value={'{"from": ' + June15.getTime().toString() + ',"to": ' + June20.getTime().toString() + '}'}
onChange={callback} onChange={callback}
/>, />,
) )
@ -248,9 +261,8 @@ describe('components/properties/dateRange', () => {
test('handles `Today` button click event', () => { test('handles `Today` button click event', () => {
const callback = jest.fn() const callback = jest.fn()
const component = wrapIntl( const component = wrapIntl(
<DateRange <DateRangeWrapper
className='octo-propertyvalue' initialValue={''}
value={''}
showEmptyPlaceholder={true} showEmptyPlaceholder={true}
onChange={callback} onChange={callback}
/>, />,

View file

@ -1,6 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import React, {useState} from 'react' import React, {useMemo, useState} from 'react'
import {useIntl} from 'react-intl' import {useIntl} from 'react-intl'
import {DateUtils} from 'react-day-picker' import {DateUtils} from 'react-day-picker'
import MomentLocaleUtils from 'react-day-picker/moment' import MomentLocaleUtils from 'react-day-picker/moment'
@ -50,6 +50,10 @@ export function createDatePropertyFromString(initialValue: string) : DatePropert
return dateProperty return dateProperty
} }
function datePropertyToString(dateProperty: DateProperty): string {
return dateProperty.from || dateProperty.to ? JSON.stringify(dateProperty) : ''
}
const loadedLocales: Record<string, moment.Locale> = {} const loadedLocales: Record<string, moment.Locale> = {}
function DateRange(props: Props): JSX.Element { function DateRange(props: Props): JSX.Element {
@ -68,7 +72,7 @@ function DateRange(props: Props): JSX.Element {
return new Date(date).getTimezoneOffset() * 60 * 1000 return new Date(date).getTimezoneOffset() * 60 * 1000
} }
const [dateProperty, setDateProperty] = useState<DateProperty>(createDatePropertyFromString(value as string)) const dateProperty = useMemo(() => createDatePropertyFromString(value as string), [value])
const [showDialog, setShowDialog] = useState(false) const [showDialog, setShowDialog] = useState(false)
// Keep dateProperty as UTC, // Keep dateProperty as UTC,
@ -127,7 +131,7 @@ function DateRange(props: Props): JSX.Element {
rangeUTC.to -= dateProperty.includeTime ? 0 : timeZoneOffset(rangeUTC.to) rangeUTC.to -= dateProperty.includeTime ? 0 : timeZoneOffset(rangeUTC.to)
} }
setDateProperty(rangeUTC) onChange(datePropertyToString(rangeUTC))
setFromInput(getDisplayDate(range.from ? new Date(range.from) : undefined)) setFromInput(getDisplayDate(range.from ? new Date(range.from) : undefined))
setToInput(getDisplayDate(range.to ? new Date(range.to) : undefined)) setToInput(getDisplayDate(range.to ? new Date(range.to) : undefined))
} }
@ -141,16 +145,7 @@ function DateRange(props: Props): JSX.Element {
} }
const onClose = () => { const onClose = () => {
// not actually setting here, onChange(datePropertyToString(dateProperty))
// but using to retreive the current state
setDateProperty((current) => {
if (current && current.from) {
onChange(JSON.stringify(current))
} else {
onChange('')
}
return {...current}
})
setShowDialog(false) setShowDialog(false)
} }

View file

@ -171,7 +171,12 @@ const PropertyValueElement = (props:Props): JSX.Element => {
className='octo-propertyvalue' className='octo-propertyvalue'
value={value.toString()} value={value.toString()}
showEmptyPlaceholder={showEmptyPlaceholder} showEmptyPlaceholder={showEmptyPlaceholder}
onChange={(newValue) => mutator.changePropertyValue(card, propertyTemplate.id, newValue)} onChange={(newValue) => {
if (value !== newValue) {
setValue(newValue)
mutator.changePropertyValue(card, propertyTemplate.id, newValue)
}
}}
/> />
) )
} else if (propertyTemplate.type === 'url') { } else if (propertyTemplate.type === 'url') {