-
Notifications
You must be signed in to change notification settings - Fork 84
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Feat(Data Mapper V2): Starting on functions panel (#4835)
* started function list panel * styling and adding icons * icons display correctly * some PR cleanup * icons fix * fixed test * expand and collapse work * PR comments * fixed lockfile * fixed import * build issue
- Loading branch information
1 parent
d7763d6
commit 4a401a7
Showing
22 changed files
with
2,445 additions
and
643 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
21 changes: 21 additions & 0 deletions
21
libs/data-mapper-v2/src/components/functionIcon/FunctionIcon.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import type { FunctionCategory } from '../../models'; | ||
import { iconForFunction, iconForFunctionCategory } from '../../utils/Icon.Utils'; | ||
|
||
export interface FunctionIconProps { | ||
functionKey: string; | ||
functionName: string; | ||
categoryName: FunctionCategory; | ||
color: string; | ||
iconSize: number; | ||
} | ||
|
||
export const FunctionIcon = ({ functionKey, functionName, categoryName, color, iconSize }: FunctionIconProps) => { | ||
const FunctionIcon = iconForFunction(functionKey, color, iconSize); | ||
const CategoryIcon = iconForFunctionCategory(categoryName); | ||
|
||
return FunctionIcon ? ( | ||
FunctionIcon | ||
) : ( | ||
<CategoryIcon style={{ height: iconSize, width: iconSize }} fontSize={`${iconSize}px`} title={functionName} primaryFill={color} /> | ||
); | ||
}; |
157 changes: 157 additions & 0 deletions
157
libs/data-mapper-v2/src/components/functionList/FunctionList.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
import type { RootState } from '../../core/state/Store'; | ||
import type { FunctionData } from '../../models/Function'; | ||
import { FunctionCategory } from '../../models/Function'; | ||
import { hasOnlyCustomInputType } from '../../utils/Function.Utils'; | ||
import { LogCategory, LogService } from '../../utils/Logging.Utils'; | ||
import FunctionListHeader from './FunctionListHeader'; | ||
import FunctionListItem from './FunctionListItem'; | ||
import type { TreeItemValue, TreeOpenChangeData, TreeOpenChangeEvent } from '@fluentui/react-components'; | ||
import { Tree } from '@fluentui/react-components'; | ||
import { SearchBox } from '@fluentui/react-search'; | ||
import Fuse from 'fuse.js'; | ||
import React, { useMemo, useState } from 'react'; | ||
import { useStyles } from './styles'; | ||
import { useIntl } from 'react-intl'; | ||
import { useSelector } from 'react-redux'; | ||
|
||
const fuseFunctionSearchOptions: Fuse.IFuseOptions<FunctionData> = { | ||
includeScore: true, | ||
minMatchCharLength: 2, | ||
includeMatches: true, | ||
threshold: 0.4, | ||
ignoreLocation: true, | ||
keys: ['key', 'functionName', 'displayName', 'category'], | ||
}; | ||
|
||
export const functionCategoryItemKeyPrefix = 'category&'; | ||
|
||
export interface FunctionDataTreeItem extends FunctionData { | ||
isExpanded?: boolean; | ||
children: FunctionDataTreeItem[]; | ||
} | ||
|
||
export const FunctionList = () => { | ||
const styles = useStyles(); | ||
const intl = useIntl(); | ||
|
||
const [openItems, setOpenItems] = React.useState<Iterable<TreeItemValue>>(Object.values(FunctionCategory)); | ||
const handleOpenChange = (event: TreeOpenChangeEvent, data: TreeOpenChangeData) => { | ||
setOpenItems(data.openItems); | ||
}; | ||
|
||
const functionData = useSelector((state: RootState) => state.function.availableFunctions); | ||
const inlineFunctionInputOutputKeys = useSelector( | ||
(state: RootState) => state.dataMap.present.curDataMapOperation.inlineFunctionInputOutputKeys | ||
); | ||
|
||
const [searchTerm, setSearchTerm] = useState<string>(''); | ||
|
||
const stringResources = useMemo( | ||
() => ({ | ||
SEARCH_FUNCTIONS: intl.formatMessage({ | ||
defaultMessage: 'Search Functions', | ||
id: '2xQWRt', | ||
description: 'Search Functions', | ||
}), | ||
}), | ||
[intl] | ||
); | ||
|
||
const functionListTree = useMemo(() => { | ||
// Can safely typecast as we just use the root's children[] | ||
const newFunctionListTree = {} as FunctionDataTreeItem; | ||
newFunctionListTree.children = []; | ||
|
||
// Try/catch here to for critical Function-related errors to be caught by us & telemetry | ||
try { | ||
if (functionData) { | ||
const functionCategoryDictionary: { | ||
[key: string]: FunctionDataTreeItem; | ||
} = {}; | ||
let functionsList: FunctionData[] = [...functionData]; | ||
functionsList.sort((a, b) => a.displayName?.localeCompare(b.displayName)); // Alphabetically sort Functions | ||
|
||
// Create dictionary for Function Categories | ||
Object.values(FunctionCategory).forEach((category) => { | ||
const categoryItem = { isExpanded: true } as FunctionDataTreeItem; | ||
categoryItem.children = []; | ||
categoryItem.key = `${functionCategoryItemKeyPrefix}${category}`; | ||
|
||
functionCategoryDictionary[category] = categoryItem; | ||
}); | ||
|
||
// NOTE: Explicitly use this instead of isAddingInlineFunction to track inlineFunctionInputOutputKeys value changes | ||
if (inlineFunctionInputOutputKeys.length === 2) { | ||
// Functions with no inputs shouldn't be shown when adding inline functions | ||
functionsList = functionsList.filter((functionNode) => functionNode.inputs.length !== 0); | ||
// Functions with only custom input shouldn't be shown when adding inline either | ||
functionsList = functionsList.filter((functionNode) => !hasOnlyCustomInputType(functionNode)); | ||
} | ||
|
||
if (searchTerm) { | ||
const fuse = new Fuse(functionsList, fuseFunctionSearchOptions); | ||
functionsList = fuse.search(searchTerm).map((result) => result.item); | ||
} | ||
|
||
// Add functions to their respective categories | ||
functionsList.forEach((functionData) => { | ||
functionCategoryDictionary[functionData.category].children.push({ | ||
...functionData, | ||
children: [], | ||
}); | ||
|
||
// If there's a search term, all present categories should be expanded as | ||
// they only show when they have Functions that match the search | ||
if (searchTerm) { | ||
functionCategoryDictionary[functionData.category].isExpanded = true; | ||
setOpenItems([...openItems, functionData.category]); | ||
} | ||
}); | ||
|
||
// Add function categories as children to the tree root, filtering out any that don't have any children | ||
newFunctionListTree.children = Object.values(functionCategoryDictionary).filter((category) => category.children.length > 0); | ||
} | ||
} catch (error) { | ||
if (typeof error === 'string') { | ||
LogService.error(LogCategory.FunctionList, 'functionListError', { | ||
message: error, | ||
}); | ||
throw new Error(`Function List Error: ${error}`); | ||
} | ||
if (error instanceof Error) { | ||
LogService.error(LogCategory.FunctionList, 'functionListError', { | ||
message: error.message, | ||
}); | ||
throw new Error(`Function List Error: ${error.message}`); | ||
} | ||
} | ||
|
||
return newFunctionListTree; | ||
}, [functionData, searchTerm, inlineFunctionInputOutputKeys]); | ||
|
||
const treeItems = functionListTree.children.map((node) => { | ||
return node.key.startsWith(functionCategoryItemKeyPrefix) ? ( | ||
<FunctionListHeader category={node.key.replace(functionCategoryItemKeyPrefix, '') as FunctionCategory} functions={node} /> | ||
) : ( | ||
<FunctionListItem functionData={node as FunctionData} /> | ||
); | ||
}); | ||
|
||
return ( | ||
<> | ||
<span style={{ position: 'sticky', top: 0, zIndex: 2 }}> | ||
<SearchBox | ||
placeholder={stringResources.SEARCH_FUNCTIONS} | ||
className={styles.functionSearchBox} | ||
value={searchTerm} | ||
size="small" | ||
onChange={(_e, data) => setSearchTerm(data.value ?? '')} | ||
/> | ||
</span> | ||
|
||
<Tree appearance="transparent" className={styles.functionTree} onOpenChange={handleOpenChange} openItems={openItems}> | ||
{treeItems} | ||
</Tree> | ||
</> | ||
); | ||
}; |
32 changes: 32 additions & 0 deletions
32
libs/data-mapper-v2/src/components/functionList/FunctionListHeader.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import type { FunctionCategory } from '../../models/Function'; | ||
import { getFunctionBrandingForCategory } from '../../utils/Function.Utils'; | ||
import { Text, Tree, TreeItem, TreeItemLayout } from '@fluentui/react-components'; | ||
import type { FunctionDataTreeItem } from './FunctionList'; | ||
import FunctionListItem from './FunctionListItem'; | ||
import { useStyles } from './styles'; | ||
|
||
interface FunctionListHeaderProps { | ||
category: FunctionCategory; | ||
functions: FunctionDataTreeItem; | ||
} | ||
|
||
const FunctionListHeader = ({ category, functions }: FunctionListHeaderProps) => { | ||
const styles = useStyles(); | ||
|
||
const categoryName = getFunctionBrandingForCategory(category).displayName; | ||
|
||
const functionItems = functions.children.map((func) => { | ||
return <FunctionListItem key={func.displayName} functionData={func} />; | ||
}); | ||
|
||
return ( | ||
<TreeItem key={category} value={category} itemType="branch"> | ||
<TreeItemLayout> | ||
<Text className={styles.headerText}>{categoryName}</Text> | ||
</TreeItemLayout> | ||
<Tree>{functionItems}</Tree> | ||
</TreeItem> | ||
); | ||
}; | ||
|
||
export default FunctionListHeader; |
41 changes: 41 additions & 0 deletions
41
libs/data-mapper-v2/src/components/functionList/FunctionListItem.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import { customTokens } from '../../core'; | ||
import type { FunctionData } from '../../models/Function'; | ||
import { getFunctionBrandingForCategory } from '../../utils/Function.Utils'; | ||
import { FunctionIcon } from '../functionIcon/FunctionIcon'; | ||
import { Button, Caption1, TreeItem, TreeItemLayout, tokens } from '@fluentui/react-components'; | ||
import { useStyles } from './styles'; | ||
|
||
interface FunctionListItemProps { | ||
functionData: FunctionData; | ||
} | ||
|
||
const FunctionListItem = ({ functionData }: FunctionListItemProps) => { | ||
const styles = useStyles(); | ||
const fnBranding = getFunctionBrandingForCategory(functionData.category); | ||
|
||
return ( | ||
<TreeItem className={styles.functionTreeItem} itemType="leaf"> | ||
<TreeItemLayout className={styles.functionTreeItem}> | ||
<Button key={functionData.key} alt-text={functionData.displayName} className={styles.listButton}> | ||
<div className={styles.iconContainer} style={{ backgroundColor: customTokens[fnBranding.colorTokenName] }}> | ||
<FunctionIcon | ||
functionKey={functionData.key} | ||
functionName={functionData.functionName} | ||
categoryName={functionData.category} | ||
color={tokens.colorNeutralForegroundInverted} | ||
iconSize={10} | ||
/> | ||
</div> | ||
|
||
<Caption1 truncate block className={styles.functionNameText}> | ||
{functionData.displayName} | ||
</Caption1> | ||
|
||
<span style={{ marginLeft: 'auto' }} /> | ||
</Button> | ||
</TreeItemLayout> | ||
</TreeItem> | ||
); | ||
}; | ||
|
||
export default FunctionListItem; |
59 changes: 59 additions & 0 deletions
59
libs/data-mapper-v2/src/components/functionList/styles.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import { makeStyles, shorthands, tokens, typographyStyles } from '@fluentui/react-components'; | ||
|
||
const fnIconSize = '12px'; | ||
|
||
export const useStyles = makeStyles({ | ||
headerText: { | ||
...typographyStyles.caption1, | ||
...shorthands.borderRadius(tokens.borderRadiusMedium), | ||
paddingLeft: tokens.spacingHorizontalXS, | ||
fontSize: '13px', | ||
marginTop: '8px', | ||
marginBottom: '8px', | ||
}, | ||
functionSearchBox: { | ||
width: '210px', | ||
}, | ||
functionTree: { | ||
backgroundColor: '#E8F3FE', | ||
width: '210px', | ||
}, | ||
functionTreeItem: { | ||
backgroundColor: '#E8F3FE', | ||
paddingLeft: '10px', | ||
}, | ||
listButton: { | ||
height: '30px', | ||
width: '100%', | ||
display: 'flex', | ||
backgroundColor: '#E8F3FE', | ||
...shorthands.border('0px'), | ||
...shorthands.padding('1px 4px 1px 4px'), | ||
':hover': { | ||
backgroundColor: '#E8F3FE', | ||
}, | ||
}, | ||
iconContainer: { | ||
height: fnIconSize, | ||
flexShrink: '0 !important', | ||
flexBasis: fnIconSize, | ||
...shorthands.borderRadius(tokens.borderRadiusCircular), | ||
color: tokens.colorNeutralBackground1, | ||
display: 'flex', | ||
justifyContent: 'center', | ||
alignItems: 'center', | ||
}, | ||
functionNameText: { | ||
width: '210px', | ||
paddingLeft: '4px', | ||
paddingRight: '4px', | ||
fontSize: '13px', | ||
color: '#242424', | ||
...shorthands.overflow('hidden'), | ||
}, | ||
treeItem: { | ||
':hover': { | ||
backgroundColor: '#E8F3FE', | ||
}, | ||
}, | ||
}); |
Oops, something went wrong.