Skip to content

Commit

Permalink
Feat(Data Mapper V2): Starting on functions panel (#4835)
Browse files Browse the repository at this point in the history
* 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
DanielleCogs authored May 14, 2024
1 parent d7763d6 commit 4a401a7
Show file tree
Hide file tree
Showing 22 changed files with 2,445 additions and 643 deletions.
4 changes: 4 additions & 0 deletions Localize/lang/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
"1Fn5n+": "Required. The URI encoded string.",
"1KFpTX": "(UTC+03:00) Minsk",
"1NBvKu": "Convert the parameter argument to a floating-point number",
"1Xke9D": "open functions drawer",
"1ZSzl6": "Each run after configuration must have at least one status checked",
"1dlfUe": "Actions perform operations on data, communicate between systems, or run other tasks.",
"1eKQwo": "(UTC+08:00) Perth",
Expand Down Expand Up @@ -1001,6 +1002,7 @@
"_1Fn5n+.comment": "Required URI encoded string parameter to be converted using uriComponentToBinary function",
"_1KFpTX.comment": "Time zone value ",
"_1NBvKu.comment": "Label for description of custom float Function",
"_1Xke9D.comment": "aria label to open functions drawer",
"_1ZSzl6.comment": "error message for deselection of last run after status",
"_1dlfUe.comment": "Description of what Actions are, on a tooltip about Actions",
"_1eKQwo.comment": "Time zone value ",
Expand Down Expand Up @@ -2026,6 +2028,7 @@
"_i4Om5O.comment": "Error message for invalid integer array",
"_i8xPfO.comment": "Text on a no actions node",
"_iB8YKD.comment": "An accessability label that describes the about tab",
"_iBArTB.comment": "aria label to collapse functions drawer",
"_iCSHJG.comment": "Label for description of custom toUpper Function",
"_iE2+sy.comment": "Button to choose data type of the dynamically added parameter",
"_iEy9pT.comment": "Token picker mode to insert dynamic content",
Expand Down Expand Up @@ -2671,6 +2674,7 @@
"i4Om5O": "This contains an invalid value",
"i8xPfO": "No actions",
"iB8YKD": "About Tab",
"iBArTB": "collapse functions drawer",
"iCSHJG": "Converts a string to uppercase using the casing rules of the invariant culture",
"iE2+sy": "Choose the type of output",
"iEy9pT": "Dynamic content",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export const DataMapperStandaloneDesignerV1 = () => {
const isLightMode = theme === ThemeType.Light;

return (
<div style={{ flex: '1 1 1px', display: 'flex', flexDirection: 'column' }}>
<div style={{ flex: '1 1 1px', display: 'flex', flexDirection: 'column', height: '100vh' }}>
<div style={{ flex: '0 1 1px' }}>
<ThemeProvider theme={isLightMode ? AzureThemeLight : AzureThemeDark}>
<FluentProvider theme={isLightMode ? webLightTheme : webDarkTheme}>
Expand Down
10 changes: 5 additions & 5 deletions libs/data-mapper-v2/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@
"dependencies": {
"@fluentui/azure-themes": "8.5.70",
"@fluentui/react": "8.110.2",
"@fluentui/react-components": "9.42.0",
"@fluentui/react-components": "9.50.0",
"@fluentui/react-hooks": "8.6.20",
"@fluentui/react-icons": "2.0.224",
"@fluentui/react-migration-v8-v9": "^9.2.16",
"@fluentui/react-portal-compat": "^9.0.60",
"@fluentui/react-search": "^9.0.3",
"@microsoft/applicationinsights-react-js": "3.4.0",
"@microsoft/applicationinsights-web": "2.8.9",
"@microsoft/designer-ui": "workspace:*",
"@microsoft/logic-apps-designer": "workspace:*",
"@microsoft/logic-apps-shared": "workspace:*",
"@react-hookz/web": "22.0.0",
"@reduxjs/toolkit": "1.8.5",
Expand Down Expand Up @@ -52,10 +52,10 @@
"main": "src/index.ts",
"module": "src/index.ts",
"peerDependencies": {
"react": "^16.4.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.4.0 || ^17.0.0 || ^18.0.0",
"@tanstack/react-query": "4.36.1",
"@tanstack/react-query-devtools": "4.36.1"
"@tanstack/react-query-devtools": "4.36.1",
"react": "^16.4.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.4.0 || ^17.0.0 || ^18.0.0"
},
"publishConfig": {
"main": "build/lib/index.cjs",
Expand Down
21 changes: 21 additions & 0 deletions libs/data-mapper-v2/src/components/functionIcon/FunctionIcon.tsx
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 libs/data-mapper-v2/src/components/functionList/FunctionList.tsx
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>
</>
);
};
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;
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 libs/data-mapper-v2/src/components/functionList/styles.tsx
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',
},
},
});
Loading

0 comments on commit 4a401a7

Please sign in to comment.