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

fix: make "value" behavior consistent #1171

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
29 changes: 17 additions & 12 deletions ui/src/checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import * as Fluent from '@fluentui/react'
import { B, Id, S } from 'h2o-wave'
import React from 'react'
import { useControlledComponent } from './hooks'
import { wave } from './ui'

/**
Expand Down Expand Up @@ -55,24 +56,28 @@ export interface Checkbox {
}

export const
XCheckbox = ({ model: m }: { model: Checkbox }) => {
const onChange = (_e?: React.FormEvent<HTMLElement>, checked?: B) => {
wave.args[m.name] = checked === null ? null : !!checked
if (m.trigger) wave.push()
}
XCheckbox = (props: { model: Checkbox }) => {
const
{ name, label, disabled, indeterminate, trigger } = props.model,
onChange = (_e?: React.FormEvent<HTMLElement>, checked?: B) => {
setValue(checked)
wave.args[name] = checked === null ? null : !!checked
if (trigger) wave.push()
},
[value, setValue] = useControlledComponent(props, props.model.value)

// eslint-disable-next-line react-hooks/exhaustive-deps
React.useEffect(() => { wave.args[m.name] = !!m.value }, [])
React.useEffect(() => { wave.args[name] = !!value }, [])

return (
<Fluent.Checkbox
data-test={m.name}
inputProps={{ 'data-test': m.name } as React.ButtonHTMLAttributes<HTMLButtonElement>}
label={m.label}
defaultIndeterminate={m.indeterminate}
defaultChecked={m.value}
data-test={name}
inputProps={{ 'data-test': name } as React.ButtonHTMLAttributes<HTMLButtonElement>}
label={label}
defaultIndeterminate={indeterminate}
checked={!!value}
onChange={onChange}
disabled={m.disabled}
disabled={disabled}
/>
)
}
9 changes: 5 additions & 4 deletions ui/src/checklist.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { B, Id, S, U } from 'h2o-wave'
import React from 'react'
import { stylesheet } from 'typestyle'
import { Choice } from './choice_group'
import { useControlledComponent } from './hooks'
import { clas, margin } from './theme'
import { wave } from './ui'

Expand Down Expand Up @@ -74,13 +75,13 @@ export const
const
defaultSelection = React.useMemo(() => new Set<S>(m.values), [m.values]),
getMappedChoices = React.useCallback(() => m.choices?.map(c => ({ c, selected: defaultSelection.has(c.name) })) || [], [defaultSelection, m.choices]),
[choices, setChoices] = React.useState(getMappedChoices()),
[choices, setChoices] = useControlledComponent(m, getMappedChoices()),
capture = (choices: { c: Choice, selected: B }[]) => {
wave.args[m.name] = choices.filter(({ selected }) => selected).map(({ c }) => c.name)
if (m.trigger) wave.push()
},
select = (value: B) => {
const _choices = choices.map(({ c, selected }) => ({ c, selected: c.disabled ? selected : value }))
const _choices = choices.map(({ c, selected }: { c: Choice, selected: B}) => ({ c, selected: c.disabled ? selected : value }))
setChoices(_choices)
capture(_choices)
},
Expand All @@ -92,7 +93,7 @@ export const
setChoices(_choices)
capture(_choices)
},
items = choices.map(({ c, selected }, i) => (
items = choices.map(({ c, selected }: { c: Choice, selected: B}, i: number) => (
<Fluent.Checkbox
key={i}
data-test={`checkbox-${i + 1}`}
Expand All @@ -106,7 +107,7 @@ export const
))
// eslint-disable-next-line react-hooks/exhaustive-deps
React.useEffect(() => { wave.args[m.name] = m.values || [] }, [])
React.useEffect(() => { setChoices(getMappedChoices()) }, [getMappedChoices, m.choices])
React.useEffect(() => { setChoices(getMappedChoices()) }, [getMappedChoices, m.choices, setChoices])
return (
<div data-test={m.name}>
<Fluent.Label>{m.label}</Fluent.Label>
Expand Down
22 changes: 13 additions & 9 deletions ui/src/choice_group.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import * as Fluent from '@fluentui/react'
import { B, Id, S } from 'h2o-wave'
import React from 'react'
import { useControlledComponent } from './hooks'
import { wave } from './ui'

/**
Expand Down Expand Up @@ -65,25 +66,28 @@ export interface ChoiceGroup {
}

export const
XChoiceGroup = ({ model: m }: { model: ChoiceGroup }) => {
XChoiceGroup = (props: { model: ChoiceGroup }) => {
const
{ name, label, required, trigger } = props.model,
[value, setValue] = useControlledComponent(props, props.model.value),
optionStyles = { choiceFieldWrapper: { marginRight: 15 } },
options = (m.choices || []).map(({ name, label, disabled }): Fluent.IChoiceGroupOption => ({ key: name, text: label || name, disabled, styles: optionStyles })),
options = (props.model.choices || []).map(({ name, label, disabled }): Fluent.IChoiceGroupOption => ({ key: name, text: label || name, disabled, styles: optionStyles })),
onChange = (_e?: React.FormEvent<HTMLElement>, option?: Fluent.IChoiceGroupOption) => {
if (option) wave.args[m.name] = option.key
if (m.trigger) wave.push()
setValue(option?.key)
if (option) wave.args[name] = option.key
if (trigger) wave.push()
}

// eslint-disable-next-line react-hooks/exhaustive-deps
React.useEffect(() => { wave.args[m.name] = m.value || null }, [])
React.useEffect(() => { wave.args[name] = value || null }, [])

return (
<Fluent.ChoiceGroup
styles={{ flexContainer: { display: 'flex', flexWrap: 'wrap' } }}
data-test={m.name}
label={m.label}
required={m.required}
defaultSelectedKey={m.value}
data-test={name}
label={label}
required={required}
selectedKey={value}
options={options}
onChange={onChange}
/>
Expand Down
6 changes: 4 additions & 2 deletions ui/src/combobox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import * as Fluent from '@fluentui/react'
import { B, Id, S } from 'h2o-wave'
import React from 'react'
import { useControlledComponent } from './hooks'
import { wave } from './ui'

/**
Expand Down Expand Up @@ -57,9 +58,10 @@ export interface Combobox {


export const
XCombobox = ({ model: m }: { model: Combobox }) => {
XCombobox = (props: { model: Combobox }) => {
const
[text, setText] = React.useState(m.value),
m = props.model,
[text, setText] = useControlledComponent(props, m.value),
options = (m.choices || []).map((text, i): Fluent.IComboBoxOption => ({ key: `${i}`, text })),
onChange = (_e: React.FormEvent<Fluent.IComboBox>, option?: Fluent.IComboBoxOption, _index?: number, value?: string) => {
const v = option?.text || value || ''
Expand Down
2 changes: 1 addition & 1 deletion ui/src/copyable_text.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export const XCopyableText = ({ model }: { model: CopyableText }) => {

return (
<div data-test={name} className={multiline ? css.multiContainer : css.compactContainer}>
<Fluent.TextField componentRef={ref} defaultValue={value} label={label} multiline={multiline} styles={{ root: { width: pc(100) } }} readOnly />
<Fluent.TextField componentRef={ref} value={value} label={label} multiline={multiline} styles={{ root: { width: pc(100) } }} readOnly />
<Fluent.PrimaryButton
title='Copy to clipboard'
onClick={onClick}
Expand Down
15 changes: 8 additions & 7 deletions ui/src/date_picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import * as Fluent from '@fluentui/react'
import { B, Id, S, U } from 'h2o-wave'
import React from 'react'
import { useControlledComponent } from './hooks'
import { wave } from './ui'

/**
Expand Down Expand Up @@ -56,19 +57,19 @@ const
if (ss.length !== 3) return undefined
const ymd = ss.map(s => parseInt(s, 10)).filter(n => !isNaN(n))
if (ymd.length !== 3) return undefined
return new Date(ymd[0], ymd[1] - 1, ymd[2])
return new Date(ymd[0], ymd[1] - 1, ymd[2], 0, 0, 0)
}

export const
XDatePicker = ({ model: m }: { model: DatePicker }) => {
XDatePicker = (props: { model: DatePicker }) => {
const
m = props.model,
defaultVal = m.value || null,
parsedVal = defaultVal ? parseDate(defaultVal) : null,
[value, setValue] = React.useState<Date | undefined>(parsedVal ? new Date(parsedVal) : undefined),
[value, setValue] = useControlledComponent(props, m.value),
onSelectDate = (d: Date | null | undefined) => {
const val = (d === null || d === undefined) ? defaultVal : formatDate(d)
const val = !d ? defaultVal : formatDate(d)
wave.args[m.name] = val
setValue(val ? new Date(`${val} 00:00:00`) : undefined)
setValue(val)
if (m.trigger) wave.push()
}

Expand All @@ -79,7 +80,7 @@ export const
<Fluent.DatePicker
data-test={m.name}
label={m.label}
value={value}
value={parseDate(value)}
placeholder={m.placeholder}
disabled={m.disabled}
onSelectDate={onSelectDate}
Expand Down
32 changes: 25 additions & 7 deletions ui/src/dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { B, Id, S, U } from 'h2o-wave'
import React from 'react'
import { stylesheet } from 'typestyle'
import { Choice } from './choice_group'
import { useControlledComponent } from './hooks'
import { fuzzysearch } from './parts/utils'
import { clas, cssVar, pc } from './theme'
import { wave } from './ui'
Expand Down Expand Up @@ -81,11 +82,13 @@ const
marginTop: 16
}
}),
BaseDropdown = ({ name, label, required, disabled, value, values, choices, trigger, placeholder }: Dropdown) => {
BaseDropdown = (props: Dropdown) => {
const
{ name, label, required, disabled, value, values, choices, trigger, placeholder } = props,
isMultivalued = !!values,
selection = React.useMemo(() => isMultivalued ? new Set<S>(values) : null, [isMultivalued, values]),
[selectedOptions, setSelectedOptions] = React.useState(Array.from(selection || [])),
[selected, setSelected] = useControlledComponent(props, value),
[selectedOptions, setSelectedOptions] = useControlledComponent(props, values),
options = (choices || []).map(({ name, label, disabled }): Fluent.IDropdownOption => ({ key: name, text: label || name, disabled })),
onChange = (_e?: React.FormEvent<HTMLElement>, option?: Fluent.IDropdownOption) => {
if (option) {
Expand All @@ -97,6 +100,7 @@ const
wave.args[name] = selectedOpts
setSelectedOptions(selectedOpts)
} else {
setSelected(String(option.key))
wave.args[name] = optionKey
}
}
Expand Down Expand Up @@ -134,7 +138,7 @@ const
required={required}
disabled={disabled}
multiSelect={isMultivalued || undefined}
defaultSelectedKey={!isMultivalued ? value : undefined}
selectedKey={!isMultivalued ? selected : undefined}
selectedKeys={isMultivalued ? selectedOptions : undefined}
onChange={onChange}
/>
Expand All @@ -151,8 +155,9 @@ const
},
ROW_HEIGHT = 44,
PAGE_SIZE = 40,
DialogDropdown = ({ name, choices, values, value, disabled, required, trigger, placeholder, label }: Dropdown) => {
DialogDropdown = (props: Dropdown) => {
const
{ name, choices, values, value, disabled, required, trigger, placeholder, label } = props,
isMultivalued = !!values,
[isDialogHidden, setIsDialogHidden] = React.useState(true),
initialSelectedMap = React.useMemo(() => {
Expand All @@ -162,14 +167,15 @@ const
}, [value, values]),
items = React.useMemo<DropdownItem[]>(() => choices?.map(({ name, label }, idx) => ({ name, text: label || name, idx, checked: initialSelectedMap.has(name) })) || [], [initialSelectedMap, choices]),
[filteredItems, setFilteredItems] = React.useState(items),
[textValue, setTextValue] = React.useState(() => {
getInitialTextValue = () => {
if (!values?.length && !value) return

const itemsMap = new Map<S, S>(items.map(({ name, text }) => [name, text]))

if (values?.length) return values.map(v => itemsMap.get(v) || '').filter(Boolean).join(', ')
if (value) return itemsMap.get(value)
}),
},
[textValue, setTextValue] = useControlledComponent(props, getInitialTextValue()),
toggleDialog = React.useCallback(() => setIsDialogHidden(!isDialogHidden), [isDialogHidden]),
cancelDialog = React.useCallback(() => {
toggleDialog()
Expand All @@ -184,7 +190,7 @@ const
initialSelectedMap.clear()
result.forEach(({ name }) => initialSelectedMap.set(name, true))
cancelDialog()
}, [cancelDialog, initialSelectedMap, items, name, trigger]),
}, [cancelDialog, initialSelectedMap, items, name, trigger, setTextValue]),
selectAll = (checked = true) => () => setFilteredItems(filteredItems.map(i => { i.checked = checked; return i })),
onSearchChange = (_e?: React.ChangeEvent<HTMLInputElement>, newVal = '') => setFilteredItems(newVal ? items.filter(({ text }) => fuzzysearch(text, newVal)) : items),
onChecked = React.useCallback((idx: U) => (_ev?: React.FormEvent<HTMLElement | HTMLInputElement>, checked = false) => {
Expand All @@ -203,6 +209,18 @@ const
checked={item.checked} />
: null, [onChecked]),
getPageSpecification = React.useCallback(() => ({ itemCount: PAGE_SIZE, height: ROW_HEIGHT * PAGE_SIZE, } as Fluent.IPageSpecification), [])

React.useEffect(() => {
setFilteredItems(items => {
return items.map(i => {
if (isMultivalued ? props?.values?.includes(i.name) : i.name === props.value) {
return ({ ...i, checked: true})
} else {
return ({ ...i, checked: false })
}
})
})
}, [props, initialSelectedMap, isMultivalued])

return (
<>
Expand Down
7 changes: 7 additions & 0 deletions ui/src/hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { useEffect, useState } from "react"

export function useControlledComponent(props: any, val: any) {
const [value, setValue] = useState(val)
useEffect(() => setValue(val), [props, val])
return [value, setValue]
}