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

PLU-237: chore: add variable list ui dropdown enhancement #566

Merged
merged 6 commits into from
May 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
191 changes: 103 additions & 88 deletions packages/frontend/src/components/RichTextEditor/SuggestionPopper.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import { useEffect, useState } from 'react'
import { ExpandLess, ExpandMore } from '@mui/icons-material'
import { useState } from 'react'
import {
Button,
Box,
Collapse,
List,
ListItemButton,
ListItemText,
Paper,
Popper,
Typography,
} from '@mui/material'
Divider,
Flex,
Popover,
PopoverContent,
PopoverTrigger,
Text,
} from '@chakra-ui/react'
import VariablesList from 'components/VariablesList'
import { StepWithVariables, Variable } from 'helpers/variables'

Expand All @@ -18,112 +17,128 @@ interface SuggestionsProps {
onSuggestionClick: (variable: Variable) => void
}

const SHORT_LIST_LENGTH = 4
const LIST_HEIGHT = 256

export default function Suggestions(props: SuggestionsProps) {
const { data, onSuggestionClick = () => null } = props
const [current, setCurrent] = useState<number | null>(0)
const [listLength, setListLength] = useState<number>(SHORT_LIST_LENGTH)
const [current, setCurrent] = useState<number>(0)

const isEmpty = data.reduce(
(acc, step) => acc && step.output.length === 0,
true,
)

const expandList = () => {
setListLength(Infinity)
}

const collapseList = () => {
setListLength(SHORT_LIST_LENGTH)
if (isEmpty) {
return (
<Text p={4} opacity={0.5} textStyle="body-1" color="base.content.medium">
No variables available
</Text>
)
}

useEffect(() => {
setListLength(SHORT_LIST_LENGTH)
}, [current])

return (
<Paper elevation={5} sx={{ width: '100%' }}>
<Typography variant="subtitle2" sx={{ p: 2, opacity: isEmpty ? 0.5 : 1 }}>
{isEmpty ? 'No variables available' : 'Variables'}
</Typography>
<List disablePadding>
{data.map((option, index) => (
<div key={`primary-suggestion-${option.name}`}>
<ListItemButton
divider
onClick={() =>
setCurrent((currentIndex) =>
currentIndex === index ? null : index,
)
// max height = 256px (variable list) + 48px (from choose data)
<Flex w="100%" boxShadow="sm">
{/* Select step to find variable list */}
<Box flexGrow={1}>
<Text
pt={4}
px={4}
pb={2}
textStyle="subhead-1"
color="base.content.medium"
>
Use data from...
</Text>
<Divider borderColor="base.divider.medium" />
<Box h={64} overflowY="auto">
{data.map((option, index) => (
<Text
key={`primary-suggestion-${option.name}`}
pl={4}
py={3}
bg={
!!option.output?.length && current === index
? 'secondary.100'
: undefined
}
sx={{ py: 0.5 }}
textStyle="subhead-1"
color="base.content.strong"
onClick={() => setCurrent(index)}
_hover={{
backgroundColor: 'secondary.50',
cursor: 'pointer',
}}
>
<ListItemText primary={option.name} />
{!!option.output?.length &&
(current === index ? <ExpandLess /> : <ExpandMore />)}
</ListItemButton>
{option.name}
</Text>
))}
</Box>
</Box>

<Collapse in={current === index} timeout="auto" unmountOnExit>
<VariablesList
variables={(option.output ?? []).slice(0, listLength)}
onClick={onSuggestionClick}
listHeight={LIST_HEIGHT}
/>
<Box>
<Divider orientation="vertical" borderColor="base.divider.medium" />
</Box>

{(option.output?.length || 0) > listLength && (
<Button fullWidth onClick={expandList}>
Show all
</Button>
)}

{listLength === Infinity && (
<Button fullWidth onClick={collapseList}>
Show less
</Button>
)}
</Collapse>
</div>
{/* Variables List */}
<Box flexGrow={1} w="50%">
<Text
pt={4}
px={4}
pb={2}
textStyle="subhead-1"
color="base.content.medium"
>
Choose data
</Text>
<Divider borderColor="base.divider.medium" />
{data.map((option, index) => (
<Collapse
key={`primary-suggestion-${option.name}-variables`}
in={current === index}
unmountOnExit
>
<VariablesList
variables={option.output ?? []}
onClick={onSuggestionClick}
/>
</Collapse>
))}
</List>
</Paper>
</Box>
</Flex>
)
}

interface SuggestionsPopperProps {
open: boolean
anchorEl: HTMLDivElement | null
editorRef: React.MutableRefObject<HTMLDivElement | null>
data: StepWithVariables[]
onSuggestionClick: (variable: Variable) => void
}

export const SuggestionsPopper = (props: SuggestionsPopperProps) => {
const { open, anchorEl, data, onSuggestionClick } = props
const { open, editorRef, data, onSuggestionClick } = props

const offsetVerticalMargin = editorRef?.current?.offsetHeight ?? 0

if (!open) {
return null
}

return (
<Popper
open={open}
anchorEl={anchorEl}
// Allow (ugly) scrolling in nested modals for small viewports; modals
// can't account for popper overflow if it is portalled to body.
disablePortal
style={{
width: anchorEl?.clientWidth,
// FIXME (ogp-weeloong): HACKY, temporary workaround. Needed to render
// sugestions within nested editors, since Chakra renders modals at 40
m0nggh marked this conversation as resolved.
Show resolved Hide resolved
// z-index. Will migrate to chakra Popover in separate PR if team is
// agreeable to flip change.
zIndex: 40,
}}
modifiers={[
{
name: 'flip',
enabled: true,
},
]}
<Popover
isOpen
initialFocusRef={editorRef}
offset={[0, offsetVerticalMargin + 1]} // this is adjusted based on DS input
>
<Suggestions data={data} onSuggestionClick={onSuggestionClick} />
</Popper>
<PopoverTrigger>
<div />
</PopoverTrigger>
{/* To account for window position when scrolling */}
<PopoverContent
width={editorRef?.current?.offsetWidth}
marginTop={`-${offsetVerticalMargin}px`}
>
<Suggestions data={data} onSuggestionClick={onSuggestionClick} />
</PopoverContent>
</Popover>
)
}
2 changes: 1 addition & 1 deletion packages/frontend/src/components/RichTextEditor/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ const Editor = ({
{variablesEnabled && (
<SuggestionsPopper
open={showVarSuggestions}
anchorEl={editorRef.current}
editorRef={editorRef}
data={stepsWithVariables}
onSuggestionClick={handleVariableClick}
/>
Expand Down
4 changes: 1 addition & 3 deletions packages/frontend/src/components/TestSubstep/TestResult.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,7 @@ export default function TestResult(props: TestResultsProps): JSX.Element {
}
</Text>
</Infobox>
<Box maxH="25rem" overflowY="scroll" w="100%">
<VariablesList variables={stepsWithVariables[0].output} />
</Box>
<VariablesList variables={stepsWithVariables[0].output} />
</Box>
)
}
106 changes: 49 additions & 57 deletions packages/frontend/src/components/VariablesList/index.tsx
Original file line number Diff line number Diff line change
@@ -1,79 +1,71 @@
import type { ComponentType } from 'react'
import List from '@mui/material/List'
import ListItem, { ListItemProps } from '@mui/material/ListItem'
import ListItemButton, {
ListItemButtonProps,
} from '@mui/material/ListItemButton'
import ListItemText from '@mui/material/ListItemText'
import { Box, Text } from '@chakra-ui/react'
import { type Variable } from 'helpers/variables'

function makeListItemComponent(
function makeVariableComponent(
variable: Variable,
onClick?: (variable: Variable) => void,
): ComponentType<ListItemButtonProps> | ComponentType<ListItemProps> {
if (onClick) {
return (props: ListItemButtonProps) => (
<ListItemButton
{...props}
// onClick doesn't work sometimes due to latency between mousedown and immediate mouseup event after
onMouseDown={() => {
onClick(variable)
}}
/>
)
}

return (props: ListItemProps) => <ListItem {...props} />
): JSX.Element {
return (
<Box
key={`suggestion-${variable.name}`}
data-test="variable-suggestion-item"
padding={onClick ? '0.5rem 1rem' : '1rem'}
borderBottom={onClick ? undefined : '1px solid #EDEDED'}
_hover={
onClick
? {
backgroundColor: 'secondary.50',
cursor: 'pointer',
}
: undefined
}
_active={
onClick
? {
backgroundColor: 'secondary.100',
cursor: 'pointer',
}
: undefined
}
// onClick doesn't work sometimes due to latency between mousedown and immediate mouseup event after
onMouseDown={
onClick
? () => {
onClick(variable)
}
: undefined
}
>
<Text textStyle="body-1" color="base.content.strong">
{variable.label ?? variable.name}
</Text>
<Text textStyle="body-2" color="base.content.medium">
{variable.displayedValue ?? variable.value?.toString() ?? ''}
</Text>
</Box>
)
}

interface VariablesListProps {
variables: Variable[]
onClick?: (variable: Variable) => void
listHeight?: number
}

export default function VariablesList(props: VariablesListProps) {
const { variables, onClick, listHeight } = props
const { variables, onClick } = props

if (!variables || variables.length === 0) {
return <></>
}

return (
<List
disablePadding
<Box
data-test="variable-suggestion-group"
sx={{ maxHeight: listHeight, overflowY: 'auto' }}
maxH={64}
overflowY="auto"
p={onClick ? undefined : '1rem'}
>
{variables.map((variable) => {
const ListItemComponent = makeListItemComponent(variable, onClick)
return (
<ListItemComponent
sx={{ pl: 4 }}
divider
data-test="variable-suggestion-item"
key={`suggestion-${variable.name}`}
>
<ListItemText
primary={variable.label ?? variable.name}
primaryTypographyProps={{
variant: 'subtitle1',
title: 'Property name',
sx: { fontWeight: 700 },
}}
secondary={
<>
{variable.displayedValue ?? variable.value?.toString() ?? ''}
</>
}
secondaryTypographyProps={{
variant: 'subtitle2',
title: 'Sample value',
}}
/>
</ListItemComponent>
)
})}
</List>
{variables.map((variable) => makeVariableComponent(variable, onClick))}
</Box>
)
}
Loading