Skip to content

Commit

Permalink
[1918] Add support for filtering tree based representations
Browse files Browse the repository at this point in the history
Bug: #1918
Signed-off-by: Axel RICHARD <axel.richard@obeo.fr>
  • Loading branch information
AxelRICHARD authored and sbegaudeau committed May 15, 2023
1 parent e90ffd1 commit b20691b
Show file tree
Hide file tree
Showing 11 changed files with 302 additions and 65 deletions.
11 changes: 10 additions & 1 deletion CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ In the _styleDescription_, the definition of a color are now a select list of al
- https://github.com/eclipse-sirius/sirius-components/issues/1304[#1304] [tree] Fix an issue where dropping an element from the tree to a diagram used the current selection instead of the dragged tree item.
- https://github.com/eclipse-sirius/sirius-components/issues/1839[#1839] [view] Remove default AQL expression on Create Edge and Create Node since they did not work anymore.
- https://github.com/eclipse-sirius/sirius-components/issues/1940[#1940] [sirius-web] Remove duplicated spring-boot-starter-test dependency in sirius-web-sample-application
- https://github.com/eclipse-sirius/sirius-components/issues/1952[#1952] [view] Fix a regression introduced in 2023.4.0 where View-based Forms could no longer be instanciated
- https://github.com/eclipse-sirius/sirius-components/issues/1952[#1952] [view] Fix a regression introduced in 2023.4.0 where View-based Forms could no longer be instantiated

=== New Features

Expand All @@ -52,6 +52,15 @@ A large set of features will have to be updated in order to stop considering the
- https://github.com/eclipse-sirius/sirius-components/issues/1946[1#946] Enabled child extenders in the View DSL implementation.
This allows downstream projects and applications to provide their own sub-types of the DSL types (e.g. new WidgetDescriptions).
In addition to registering the extension metadmodel itself, users must provide a `ChildExtenderProvider` bean for their extensions to be properly integrated.
- https://github.com/eclipse-sirius/sirius-components/issues/1918[#1918] [tree] Its is now possible to filter tree items in trees.
After selected a tree item, hit Ctrl+f (or Cmd+f on macOS) to enable the filter bar.
+
image:doc/screenshots/filterBar.png[Filter Bar,30%,30%]
+
All visible tree items containing the value typed in the filter bar will be highlighted.
The filter button inside the filter bar allows to filter (hide) all visible tree items not containing the value typed in the filter bar.
+
image:doc/screenshots/filterBarFilterButton.png[Filter Bar Filter Button,30%,30%]

=== Improvements

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
= Filtering tree based representations

== First task

- [x] Shape written, reviewed and merged
- [x] ADR written
- [x] First prototype of the user interface
- [x] Changelog updated

== Second task
- [ ] Implement expand all action in tree item context menu
- [ ] First version of the user interface done with Cypress tests
- [ ] User guide updated with screenshots


Binary file added doc/screenshots/filterBar.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added doc/screenshots/filterBarFilterButton.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import React, { useContext, useEffect, useRef, useState } from 'react';
import { TreeItemProps } from './TreeItem.types';
import { TreeItemArrow } from './TreeItemArrow';
import { TreeItemContextMenu, TreeItemContextMenuContext } from './TreeItemContextMenu';
import { isFilterCandidate } from './filterTreeItem';

const renameTreeItemMutation = gql`
mutation renameTreeItem($input: RenameTreeItemInput!) {
Expand Down Expand Up @@ -102,6 +103,9 @@ const useTreeItemStyle = makeStyles((theme) => ({
ul: {
marginLeft: theme.spacing(3),
},
highlight: {
backgroundColor: theme.palette.navigation.leftBackground,
},
}));

// The list of characters that will enable the direct edit mechanism.
Expand All @@ -116,6 +120,8 @@ export const TreeItem = ({
selection,
setSelection,
readOnly,
textToHighlight,
isFilterEnabled,
}: TreeItemProps) => {
const classes = useTreeItemStyle();
const { httpOrigin } = useContext(ServerContext);
Expand Down Expand Up @@ -242,6 +248,8 @@ export const TreeItem = ({
selection={selection}
setSelection={setSelection}
readOnly={readOnly}
textToHighlight={textToHighlight}
isFilterEnabled={isFilterEnabled}
/>
</li>
);
Expand Down Expand Up @@ -273,6 +281,7 @@ export const TreeItem = ({
if (item.imageURL) {
image = <img height="16" width="16" alt={item.kind} src={httpOrigin + item.imageURL}></img>;
}
const highlightRegExp = new RegExp(`(${textToHighlight})`, 'gi');
let text;
if (editingMode) {
const handleChange = (event) => {
Expand Down Expand Up @@ -331,9 +340,28 @@ export const TreeItem = ({
/>
);
} else {
let itemLabel: JSX.Element;
const splitLabelWithTextToHighlight: string[] = item.label.split(highlightRegExp);
if (textToHighlight === '' || splitLabelWithTextToHighlight.length === 1) {
itemLabel = <>{item.label}</>;
} else {
const languages: string[] = Array.from(navigator.languages);
itemLabel = (
<>
{splitLabelWithTextToHighlight.map((value, index) => {
const shouldHighlight = value.localeCompare(textToHighlight, languages, { sensitivity: 'base' }) === 0;
return (
<span key={value + index} className={shouldHighlight ? classes.highlight : ''}>
{value}
</span>
);
})}
</>
);
}
text = (
<Typography variant="body2" className={`${classes.label} ${selected ? classes.selectedLabel : ''}`}>
{item.label}
{itemLabel}
</Typography>
);
}
Expand Down Expand Up @@ -412,47 +440,53 @@ export const TreeItem = ({

const shouldDisplayMoreButton = item.deletable || item.editable || treeItemMenuContributionComponents.length > 0;

/* ref, tabindex and onFocus are used to set the React component focusabled and to set the focus to the corresponding DOM part */
return (
<>
<div className={className}>
<TreeItemArrow item={item} depth={depth} onExpand={onExpand} data-testid={`${item.label}-toggle`} />
<div
ref={refDom}
tabIndex={0}
onKeyDown={onBeginEditing}
draggable={draggable}
onClick={onClick}
onDragStart={dragStart}
onDragOver={dragOver}
data-treeitemid={item.id}
data-haschildren={item.hasChildren.toString()}
data-depth={depth}
data-expanded={item.expanded.toString()}
data-testid={dataTestid}>
<div className={classes.content}>
<div
className={classes.imageAndLabel}
onDoubleClick={() => item.hasChildren && onExpand(item.id, depth)}
title={tooltipText}
data-testid={item.label}>
{image}
{text}
let currentTreeItem: JSX.Element | null;
if (isFilterEnabled && isFilterCandidate(item, highlightRegExp)) {
currentTreeItem = null;
} else {
/* ref, tabindex and onFocus are used to set the React component focusabled and to set the focus to the corresponding DOM part */
currentTreeItem = (
<>
<div className={className}>
<TreeItemArrow item={item} depth={depth} onExpand={onExpand} data-testid={`${item.label}-toggle`} />
<div
ref={refDom}
tabIndex={0}
onKeyDown={onBeginEditing}
draggable={draggable}
onClick={onClick}
onDragStart={dragStart}
onDragOver={dragOver}
data-treeitemid={item.id}
data-haschildren={item.hasChildren.toString()}
data-depth={depth}
data-expanded={item.expanded.toString()}
data-testid={dataTestid}>
<div className={classes.content}>
<div
className={classes.imageAndLabel}
onDoubleClick={() => item.hasChildren && onExpand(item.id, depth)}
title={tooltipText}
data-testid={item.label}>
{image}
{text}
</div>
{shouldDisplayMoreButton ? (
<IconButton
className={classes.more}
size="small"
onClick={openContextMenu}
data-testid={`${item.label}-more`}>
<MoreVertIcon style={{ fontSize: 12 }} />
</IconButton>
) : null}
</div>
{shouldDisplayMoreButton ? (
<IconButton
className={classes.more}
size="small"
onClick={openContextMenu}
data-testid={`${item.label}-more`}>
<MoreVertIcon style={{ fontSize: 12 }} />
</IconButton>
) : null}
</div>
</div>
</div>
{children}
{contextMenu}
</>
);
{children}
{contextMenu}
</>
);
}
return <>{currentTreeItem}</>;
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2021, 2022 Obeo.
* Copyright (c) 2021, 2023 Obeo.
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* which accompanies this distribution, and is available at
Expand All @@ -22,4 +22,6 @@ export interface TreeItemProps {
selection: Selection;
setSelection: (selection: Selection) => void;
readOnly: boolean;
textToHighlight: string | null;
isFilterEnabled: boolean;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*******************************************************************************
* Copyright (c) 2023 Obeo.
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Obeo - initial API and implementation
*******************************************************************************/
import { GQLTreeItem } from '../views/ExplorerView.types';

export const isFilterCandidate = (treeItem: GQLTreeItem, highlightRegExp: RegExp): boolean => {
let filter: boolean = false;
const splitLabelWithTextToHighlight: string[] = treeItem.label.split(highlightRegExp);
if (splitLabelWithTextToHighlight.length > 1) {
filter = false;
} else if (!treeItem.hasChildren && splitLabelWithTextToHighlight.length === 1) {
filter = true;
} else if (
treeItem.hasChildren &&
treeItem.expanded &&
treeItem.children.map((child) => child.label.split(highlightRegExp).length).every((v) => v === 1)
) {
filter = treeItem.children.map((child) => isFilterCandidate(child, highlightRegExp)).every((v) => v === true);
}
return filter;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*******************************************************************************
* Copyright (c) 2023 Obeo.
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Obeo - initial API and implementation
*******************************************************************************/
import IconButton from '@material-ui/core/IconButton';
import InputAdornment from '@material-ui/core/InputAdornment';
import TextField from '@material-ui/core/TextField';
import { Theme, makeStyles } from '@material-ui/core/styles';
import { FilterList } from '@material-ui/icons';
import CloseIcon from '@material-ui/icons/Close';
import { useState } from 'react';
import { FilterBarProps, FilterBarState } from './FilterBar.types';

const useFilterBarStyles = makeStyles((theme: Theme) => ({
filterbar: {
display: 'flex',
flexDirection: 'row',
overflow: 'hidden',
paddingLeft: theme.spacing(1),
paddingRight: theme.spacing(1),
paddingBottom: theme.spacing(1),
},
textfield: {
height: theme.spacing(3),
fontSize: theme.typography.fontSize,
},
}));
export const FilterBar = ({ onTextChange, onFilterButtonClick, onClose }: FilterBarProps) => {
const classes = useFilterBarStyles();

const initialState: FilterBarState = {
filterEnabled: false,
};
const [state, setState] = useState<FilterBarState>(initialState);

return (
<div className={classes.filterbar}>
<TextField
id="filterbar-textfield"
data-testid="filterbar-textfield"
name="filterbar-textfield"
placeholder="Type to filter"
spellCheck={false}
variant="outlined"
size="small"
margin="none"
autoFocus={true}
multiline={false}
fullWidth
onChange={onTextChange}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label="filter"
size="small"
onClick={() => {
onFilterButtonClick(!state.filterEnabled);
setState((prevState) => {
return { filterEnabled: !prevState.filterEnabled };
});
}}>
<FilterList fontSize="small" color={state.filterEnabled ? 'primary' : 'secondary'} />
</IconButton>
</InputAdornment>
),
className: classes.textfield,
}}
/>
<IconButton size="small" aria-label="close" color="inherit" onClick={onClose}>
<CloseIcon fontSize="small" />
</IconButton>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*******************************************************************************
* Copyright (c) 2023 Obeo.
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Obeo - initial API and implementation
*******************************************************************************/

export interface FilterBarProps {
onTextChange: React.ChangeEventHandler<HTMLTextAreaElement | HTMLInputElement>;
onFilterButtonClick: (enabled: boolean) => void;
onClose: () => void;
}

export interface FilterBarState {
filterEnabled: boolean;
}
Loading

0 comments on commit b20691b

Please sign in to comment.