diff --git a/backend/sirius-components-forms/src/main/java/org/eclipse/sirius/components/forms/components/TreeComponent.java b/backend/sirius-components-forms/src/main/java/org/eclipse/sirius/components/forms/components/TreeComponent.java index ba236faa0dc..6618d1ca17d 100644 --- a/backend/sirius-components-forms/src/main/java/org/eclipse/sirius/components/forms/components/TreeComponent.java +++ b/backend/sirius-components-forms/src/main/java/org/eclipse/sirius/components/forms/components/TreeComponent.java @@ -17,7 +17,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.stream.Collectors; +import java.util.Optional; import org.eclipse.sirius.components.forms.TreeNode; import org.eclipse.sirius.components.forms.description.TreeDescription; @@ -49,31 +49,27 @@ public Element render() { String id = treeDescription.getIdProvider().apply(variableManager); String label = treeDescription.getLabelProvider().apply(variableManager); + List children = List.of(new Element(DiagnosticComponent.class, new DiagnosticComponentProps(treeDescription, variableManager))); - // Compute the hierarchy and store the semantic elements by parent id - Map> nodesHierarchy = new LinkedHashMap<>(); - List rootItems = treeDescription.getElementsProvider().apply(variableManager); - nodesHierarchy.put(null, rootItems); - for (Object item : rootItems) { - this.addChildren(item, variableManager, treeDescription, nodesHierarchy); - } + // Compute the recursive structure of semantic elements + Map> childrenByParentId = new LinkedHashMap<>(); + this.computeChildren(null, variableManager, treeDescription, childrenByParentId); - // Build the actual TreeNodes + // Build the actual flat TreeNode list to represent it List nodes = new ArrayList<>(); - for (var entry : nodesHierarchy.entrySet()) { + for (var entry : childrenByParentId.entrySet()) { String parentId = entry.getKey(); - for (Object item : entry.getValue()) { + List semanticChildren = entry.getValue(); + for (Object semanticChild : semanticChildren) { VariableManager itemVariableManager = variableManager.createChild(); - itemVariableManager.put(CANDIDATE_VARIABLE, item); - TreeNode node = this.renderNode(itemVariableManager, treeDescription, parentId); - nodes.add(node); + itemVariableManager.put(CANDIDATE_VARIABLE, semanticChild); + nodes.add(this.renderNode(itemVariableManager, treeDescription, parentId)); } } - // Expand all for now - List expandedIds = nodes.stream().map(TreeNode::getId).collect(Collectors.toList()); - - List children = List.of(new Element(DiagnosticComponent.class, new DiagnosticComponentProps(treeDescription, variableManager))); + VariableManager expansionVariableManager = variableManager.createChild(); + expansionVariableManager.put("nodes", nodes); //$NON-NLS-1$ + List expandedIds = treeDescription.getExpandedNodeIdsProvider().apply(expansionVariableManager); // @formatter:off TreeElementProps treeElementProps = TreeElementProps.newTreeElementProps(id) @@ -87,17 +83,22 @@ public Element render() { return new Element(TreeElementProps.TYPE, treeElementProps); } - private void addChildren(Object item, VariableManager variableManager, TreeDescription treeDescription, Map> nodesHierarchy) { + private void computeChildren(Object semanticElement, VariableManager variableManager, TreeDescription treeDescription, Map> childrenByParentId) { VariableManager itemVariableManager = variableManager.createChild(); - itemVariableManager.put(CANDIDATE_VARIABLE, item); - String parentId = treeDescription.getNodeIdProvider().apply(itemVariableManager); - boolean hasChildren = treeDescription.getHasChildrenProvider().apply(itemVariableManager); - if (hasChildren) { - var children = treeDescription.getChildrenProvider().apply(itemVariableManager); - nodesHierarchy.put(parentId, children); - for (Object childItem : children) { - this.addChildren(childItem, variableManager, treeDescription, nodesHierarchy); - } + Object candidate = Optional.ofNullable(semanticElement).orElse(variableManager.get(VariableManager.SELF, Object.class).orElse(null)); + itemVariableManager.put(CANDIDATE_VARIABLE, candidate); + + String parentId; + if (semanticElement != null) { + parentId = treeDescription.getNodeIdProvider().apply(itemVariableManager); + } else { + parentId = null; + } + + List semanticChildren = treeDescription.getChildrenProvider().apply(itemVariableManager); + childrenByParentId.put(parentId, semanticChildren); + for (Object child : semanticChildren) { + this.computeChildren(child, variableManager, treeDescription, childrenByParentId); } } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 110fa0df065..c844d8b034b 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -15,6 +15,7 @@ "@apollo/client": "3.4.7", "@material-ui/core": "4.12.3", "@material-ui/icons": "4.11.2", + "@material-ui/lab": "^4.0.0-alpha.61", "@rollup/plugin-commonjs": "21.0.1", "@rollup/plugin-image": "2.1.1", "@rollup/plugin-node-resolve": "13.1.1", @@ -1969,6 +1970,33 @@ } } }, + "node_modules/@material-ui/lab": { + "version": "4.0.0-alpha.61", + "resolved": "https://registry.npmjs.org/@material-ui/lab/-/lab-4.0.0-alpha.61.tgz", + "integrity": "sha512-rSzm+XKiNUjKegj8bzt5+pygZeckNLOr+IjykH8sYdVk7dE9y2ZuUSofiMV2bJk3qU+JHwexmw+q0RyNZB9ugg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.4.4", + "@material-ui/utils": "^4.11.3", + "clsx": "^1.0.4", + "prop-types": "^15.7.2", + "react-is": "^16.8.0 || ^17.0.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "peerDependencies": { + "@material-ui/core": "^4.12.1", + "@types/react": "^16.8.6 || ^17.0.0", + "react": "^16.8.0 || ^17.0.0", + "react-dom": "^16.8.0 || ^17.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@material-ui/styles": { "version": "4.11.4", "resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.11.4.tgz", @@ -2054,9 +2082,9 @@ } }, "node_modules/@material-ui/utils": { - "version": "4.11.2", - "resolved": "https://registry.npmjs.org/@material-ui/utils/-/utils-4.11.2.tgz", - "integrity": "sha512-Uul8w38u+PICe2Fg2pDKCaIG7kOyhowZ9vjiC1FsVwPABTW8vPPKfF6OvxRq3IiBaI1faOJmgdvMG7rMJARBhA==", + "version": "4.11.3", + "resolved": "https://registry.npmjs.org/@material-ui/utils/-/utils-4.11.3.tgz", + "integrity": "sha512-ZuQPV4rBK/V1j2dIkSSEcH5uT6AaHuKWFfotADHsC0wVL1NLd2WkFCm4ZZbX33iO4ydl6V0GPngKm8HZQ2oujg==", "dev": true, "dependencies": { "@babel/runtime": "^7.4.4", @@ -15279,6 +15307,19 @@ "@babel/runtime": "^7.4.4" } }, + "@material-ui/lab": { + "version": "4.0.0-alpha.61", + "resolved": "https://registry.npmjs.org/@material-ui/lab/-/lab-4.0.0-alpha.61.tgz", + "integrity": "sha512-rSzm+XKiNUjKegj8bzt5+pygZeckNLOr+IjykH8sYdVk7dE9y2ZuUSofiMV2bJk3qU+JHwexmw+q0RyNZB9ugg==", + "dev": true, + "requires": { + "@babel/runtime": "^7.4.4", + "@material-ui/utils": "^4.11.3", + "clsx": "^1.0.4", + "prop-types": "^15.7.2", + "react-is": "^16.8.0 || ^17.0.0" + } + }, "@material-ui/styles": { "version": "4.11.4", "resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.11.4.tgz", @@ -15323,9 +15364,9 @@ "requires": {} }, "@material-ui/utils": { - "version": "4.11.2", - "resolved": "https://registry.npmjs.org/@material-ui/utils/-/utils-4.11.2.tgz", - "integrity": "sha512-Uul8w38u+PICe2Fg2pDKCaIG7kOyhowZ9vjiC1FsVwPABTW8vPPKfF6OvxRq3IiBaI1faOJmgdvMG7rMJARBhA==", + "version": "4.11.3", + "resolved": "https://registry.npmjs.org/@material-ui/utils/-/utils-4.11.3.tgz", + "integrity": "sha512-ZuQPV4rBK/V1j2dIkSSEcH5uT6AaHuKWFfotADHsC0wVL1NLd2WkFCm4ZZbX33iO4ydl6V0GPngKm8HZQ2oujg==", "dev": true, "requires": { "@babel/runtime": "^7.4.4", diff --git a/frontend/package.json b/frontend/package.json index ebf51d51c16..104cc66137e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -28,6 +28,7 @@ "@apollo/client": "3.4.7", "@material-ui/core": "4.12.3", "@material-ui/icons": "4.11.2", + "@material-ui/lab": "^4.0.0-alpha.61", "@rollup/plugin-commonjs": "21.0.1", "@rollup/plugin-image": "2.1.1", "@rollup/plugin-node-resolve": "13.1.1", diff --git a/frontend/src/form/Form.types.ts b/frontend/src/form/Form.types.ts index 9e9ce5e476d..618203fabb0 100644 --- a/frontend/src/form/Form.types.ts +++ b/frontend/src/form/Form.types.ts @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2021 Obeo. + * Copyright (c) 2021, 2022 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 @@ -112,3 +112,18 @@ export interface Link extends Widget { label: string; url: string; } + +export interface Tree extends Widget { + label: string; + nodes: TreeNode[]; + expandedNodesIds: string[]; +} + +export interface TreeNode { + id: string; + parentId: string; + label: string; + kind: string; + imageURL: string; + selectable: Boolean; +} diff --git a/frontend/src/form/FormEventFragments.ts b/frontend/src/form/FormEventFragments.ts index 92d356c7aaa..ca8d372390b 100644 --- a/frontend/src/form/FormEventFragments.ts +++ b/frontend/src/form/FormEventFragments.ts @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2021 Obeo. + * Copyright (c) 2021, 2022 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 @@ -105,6 +105,18 @@ export const formRefreshedEventPayloadFragment = gql` label url } + ... on TreeWidget { + label + expandedNodesIds + nodes { + id + parentId + label + kind + imageURL + selectable + } + } } } } diff --git a/frontend/src/form/FormEventFragments.types.ts b/frontend/src/form/FormEventFragments.types.ts index 56f8043d373..283bcfcd328 100644 --- a/frontend/src/form/FormEventFragments.types.ts +++ b/frontend/src/form/FormEventFragments.types.ts @@ -174,3 +174,18 @@ export interface GQLLink extends GQLWidget { label: string; url: string; } + +export interface GQLTree extends GQLWidget { + label: string; + nodes: GQLTreeNode[]; + expandedNodesIds: string[]; +} + +export interface GQLTreeNode { + id: string; + parentId: string; + label: string; + kind: string; + imageURL: string; + selectable: Boolean; +} diff --git a/frontend/src/properties/Group.tsx b/frontend/src/properties/Group.tsx index ffb9578fd4e..8ef422df24c 100644 --- a/frontend/src/properties/Group.tsx +++ b/frontend/src/properties/Group.tsx @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019, 2021 Obeo. + * Copyright (c) 2019, 2022 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 @@ -21,6 +21,7 @@ import { Select, Textarea, Textfield, + Tree, Widget, WidgetSubscription, } from 'form/Form.types'; @@ -34,6 +35,7 @@ import { SelectPropertySection } from 'properties/propertysections/SelectPropert import { TextfieldPropertySection } from 'properties/propertysections/TextfieldPropertySection'; import React from 'react'; import { Selection } from 'workbench/Workbench.types'; +import { TreePropertySection } from './propertysections/TreePropertySection'; const useGroupStyles = makeStyles((theme) => ({ group: { @@ -79,6 +81,7 @@ const isMultiSelect = (widget: Widget): widget is MultiSelect => widget.__typena const isRadio = (widget: Widget): widget is Radio => widget.__typename === 'Radio'; const isList = (widget: Widget): widget is List => widget.__typename === 'List'; const isLink = (widget: Widget): widget is Link => widget.__typename === 'Link'; +const isTree = (widget: Widget): widget is Tree => widget.__typename === 'TreeWidget'; const widgetToPropertySection = ( editingContextId: string, @@ -163,6 +166,16 @@ const widgetToPropertySection = ( ); } else if (isLink(widget)) { propertySection = ; + } else if (isTree(widget)) { + propertySection = ( + + ); } else { console.error(`Unsupported widget type ${widget.__typename}`); } diff --git a/frontend/src/properties/propertysections/TreePropertySection.tsx b/frontend/src/properties/propertysections/TreePropertySection.tsx new file mode 100644 index 00000000000..873441f041c --- /dev/null +++ b/frontend/src/properties/propertysections/TreePropertySection.tsx @@ -0,0 +1,81 @@ +/******************************************************************************* + * Copyright (c) 2022 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 { makeStyles } from '@material-ui/core'; +import { ClassNameMap } from '@material-ui/core/styles/withStyles'; +import Typography from '@material-ui/core/Typography'; +import ChevronRightIcon from '@material-ui/icons/ChevronRight'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import { TreeItem } from '@material-ui/lab'; +import TreeView from '@material-ui/lab/TreeView'; +import { httpOrigin } from 'common/URL'; +import { TreeNode } from 'form/Form.types'; +import React from 'react'; +import { SelectionEntry } from 'workbench/Workbench.types'; +import { TreePropertySectionProps } from './TreePropertySection.types'; + +const useTreeWidgetStyles = makeStyles((theme) => ({ + nodeLabel: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + columnGap: theme.spacing(1), + }, +})); + +const renderTree = (styles: ClassNameMap, nodes: TreeNode[], onClick): JSX.Element => { + const renderTreeItem = (node: TreeNode): JSX.Element => { + const children = nodes.filter((g) => g.parentId === node.id); + + const label = ( +
onClick(node)}> + {node.label} + {node.label} +
+ ); + + return ( + + {children.map(renderTreeItem)} + + ); + }; + + return <>{...nodes.filter((f) => f.parentId == null).map(renderTreeItem)}; +}; + +export const TreePropertySection = ({ widget, setSelection }: TreePropertySectionProps) => { + const styles: ClassNameMap = useTreeWidgetStyles(); + const updateSelection = (node: TreeNode) => { + if (node.selectable) { + const newSelection: SelectionEntry = { + id: node.id, + label: node.label, + kind: node.kind, + }; + setSelection({ entries: [newSelection] }); + } + }; + return ( +
+ {widget.label} + } + defaultExpanded={widget.expandedNodesIds} + defaultExpandIcon={} + > + {renderTree(styles, widget.nodes, updateSelection)} + +
+ ); +}; diff --git a/frontend/src/properties/propertysections/TreePropertySection.types.ts b/frontend/src/properties/propertysections/TreePropertySection.types.ts new file mode 100644 index 00000000000..698659a301b --- /dev/null +++ b/frontend/src/properties/propertysections/TreePropertySection.types.ts @@ -0,0 +1,22 @@ +/******************************************************************************* + * Copyright (c) 2022 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 { Tree } from 'form/Form.types'; +import { Selection } from 'workbench/Workbench.types'; + +export interface TreePropertySectionProps { + editingContextId: string; + formId: string; + widget: Tree; + setSelection: (selection: Selection) => void; +}