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 1 commit
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
179 changes: 94 additions & 85 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,122 @@ 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 isEmpty = data.reduce(
(acc, step) => acc && step.output.length === 0,
true,
)

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

const collapseList = () => {
setListLength(SHORT_LIST_LENGTH)
}
return (
// max height = 256px (variable list) + 48px (from choose data)
<Flex w="100%" maxH="304px" boxShadow="sm">
{/* Select step to find variable list */}
m0nggh marked this conversation as resolved.
Show resolved Hide resolved
<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 maxH="256px" overflowY="auto">
{data.map((option, index) => (
<div key={`primary-suggestion-${option.name}`}>
<Text
m0nggh marked this conversation as resolved.
Show resolved Hide resolved
pl={4}
py={3}
bg={
!!option.output?.length && current === index
? '#E9EAEE'
: undefined
}
textStyle="subhead-1"
color="base.content.strong"
onClick={() =>
setCurrent((currentIndex) =>
currentIndex === index ? null : index,
)
}
_hover={{
backgroundColor: '#F8F9FA',
cursor: 'pointer',
m0nggh marked this conversation as resolved.
Show resolved Hide resolved
}}
>
{option.name}
</Text>
</div>
))}
</Box>
</Box>

useEffect(() => {
setListLength(SHORT_LIST_LENGTH)
}, [current])
<Box>
<Divider orientation="vertical" borderColor="base.divider.medium" />
</Box>

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>
{/* Variables List */}
<Box flexGrow={1} maxW="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) => (
<div key={`primary-suggestion-${option.name}`}>
<ListItemButton
divider
onClick={() =>
setCurrent((currentIndex) =>
currentIndex === index ? null : index,
)
}
sx={{ py: 0.5 }}
>
<ListItemText primary={option.name} />
{!!option.output?.length &&
(current === index ? <ExpandLess /> : <ExpandMore />)}
</ListItemButton>

<Collapse in={current === index} timeout="auto" unmountOnExit>
<div key={`primary-suggestion-${option.name}-variables`}>
<Collapse in={current === index} unmountOnExit>
m0nggh marked this conversation as resolved.
Show resolved Hide resolved
<VariablesList
variables={(option.output ?? []).slice(0, listLength)}
variables={option.output ?? []}
onClick={onSuggestionClick}
listHeight={LIST_HEIGHT}
/>

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

{listLength === Infinity && (
<Button fullWidth onClick={collapseList}>
Show less
</Button>
)}
</Collapse>
</div>
))}
</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

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,
},
]}
>
<Suggestions data={data} onSuggestionClick={onSuggestionClick} />
</Popper>
<Popover isOpen={open} initialFocusRef={editorRef}>
<PopoverTrigger>
<div />
</PopoverTrigger>
{/* To account for window position when scrolling */}
<PopoverContent
width={editorRef?.current?.offsetWidth}
marginBottom={`calc(${editorRef?.current?.offsetHeight}px - 10px)`}
marginTop="-10px"
>
<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
105 changes: 48 additions & 57 deletions packages/frontend/src/components/VariablesList/index.tsx
Original file line number Diff line number Diff line change
@@ -1,79 +1,70 @@
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="0.5rem 1rem"
_hover={
onClick
? {
backgroundColor: '#FEF8FB',
cursor: 'pointer',
}
: undefined
}
_active={
onClick
? {
backgroundColor: '#CF1A681F',
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="256px"
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