Tree widget: Customize model tree to add child nodes of specific elements
Is it possible currently to customize Models Tree hierarchy to add related nodes from different schema than the parent?
By default they go to a separate parent node even though a relationship exists between these created items and their parent.
For example: if slicing one element (of class BisCore.GeometricElement3d) into 2 parts, in the tree the sliced element should get 2 children as 2 elements get created (created elements are of class Construction.ConstructionDetailingElement). Instead they go to separate folder at the top of the tree that contains all construction only nodes.
Would like to effectively merge the construction data into models tree using relationships.
From what i found, can group nodes in pre and post processors for specific node, but only if they represent instance of same class, but since they both are of different classes, i assume pre-processor can't find the ConstructionDetailingElement or is only handling nodes that would exist in that specific tree?
Is it possible to somehow add nodes to models tree based on relationships?
Is it possible currently to customize Models Tree hierarchy to add related nodes from different schema than the parent?
You don't need to customize the component for that - all Models tree queries are polymorphic, meaning we take elements of specified class (e.g. bis.GeometricElement3d) and all its subclasses, including the ones from other schemas.
By default they go to a separate parent node even though a relationship exists between these created items and their parent.
For example: if slicing one element (of class BisCore.GeometricElement3d) into 2 parts, in the tree the sliced element should get 2 children as 2 elements get created (created elements are of class Construction.ConstructionDetailingElement). Instead they go to separate folder at the top of the tree that contains all construction only nodes.
I believe that would only be happening if the sliced element and its parts were in separate models - is that the case? On the other hand, that means the relationship between sliced elements and parts is not parent-child, because, according to BIS rules, children have to be in the same model as their parent.
its not a direct parent-child relation i suppose, rather a ECRelationshipClass is used in the middle from Construction schema between ConstructionDetailingElement and bis:GeometricElement3d. And yes they seem to be in deferent models. So i guess, would need to pass that relationship class somewhere
So basically what you're asking is to move an element from its rightful place in the Models tree to somewhere it doesn't really belong, according to Models tree hierarchy rules. I don't think that's a good idea for multiple reasons:
- The hierarchy would be lying about the data:
- ModelX - CategoryX - SlicedElement (in ModelX) - PartElement (not in ModelX, but under ModelX ancestor node). - Visibility states would become even more confusing than they already are - visibility of "PartElement" in the above example would NOT be affected by changes to ModelX, SlicedElement, and likely - CategoryX. Similarly, changing visibility of "PartElement" would NOT affect ancestor nodes' visibility states.
Due to above reasons, I think making the requested changes to Models tree is not a great idea. You can still make your own hierarchy using publicly available building blocks.
This is not for changing models tree behaviour by default, rather to have ability to add nodes via relationship classes. The idea would be to have separate tree - not replacing models tree - for displaying this construction data, where this kind of hierarchy is desired - as the split elements are together the one element, and going to a separate tree is rather cumbersome.
For visibility, could it not propagate using the provided relationship class for child nodes as well?
The problem with building a complete custom tree hierarchy would probably involve just copying over entire models tree with some additional logic, since for the most part would be same behaviour.
This is not for changing models tree behaviour by default, rather to have ability to add nodes via relationship classes.
I understand. But there are some customizations that we don't want to allow making, because with them Models tree becomes something other than Models tree. Especially in this case, where, IMO, the resulting hierarchy would be lying about the data.
For visibility, could it not propagate using the provided relationship class for child nodes as well?
Visibility works independently from hierarchy to make sure changes in one tree are properly reflected in all others. E.g. changing Category node visibility in Categories tree affects Model nodes' visibility in Models tree. Making the hierarchy changes that you suggested would make visibility controls work unpredictably.
The problem with building a complete custom tree hierarchy would probably involve just copying over entire models tree with some additional logic, since for the most part would be same behaviour.
That's bad, because the Models tree with those changes makes little sense. I suggest thinking about how to properly represent the data to your users, and that, most likely, won't be that similar to Models tree.
The user requested such hierarchy in order to easily locate "sliced elements" within Models Tree hierarchy. The original issue, in which we discussed such changes, was this #711
As a workaround, we were able to modify Models Tree hierachy look by adding additional rules to process on leaf nodes (based on tree-widget react 2.3.2):
diff --git a/packages/itwin/tree-widget/src/components/trees/models-tree/ModelsTree.tsx b/packages/itwin/tree-widget/src/components/trees/models-tree/ModelsTree.tsx
index 39045ad3..f32dd370 100644
--- a/packages/itwin/tree-widget/src/components/trees/models-tree/ModelsTree.tsx
+++ b/packages/itwin/tree-widget/src/components/trees/models-tree/ModelsTree.tsx
@@ -53,6 +53,8 @@ export interface ModelsTreeHierarchyConfiguration {
* @public
*/
export interface ModelsTreeProps extends BaseFilterableTreeProps {
+ processRules?: (r: Ruleset) => Ruleset;
+ processSearchRules?: (r: Ruleset) => Ruleset;
/**
* Predicate which indicates whether node can be selected or no
*/
@@ -181,24 +183,31 @@ interface UseModelsTreeStateProps extends Omit<ModelsTreeProps, "onFeatureUsed">
reportUsage: (props: { featureId?: UsageTrackedFeatures; reportInteraction: boolean }) => void;
}
+const useEvent = <F extends (...a: any[]) => any>(fn: F): F => {
+ const { current } = useRef({ flex: fn, fix: fn });
+ return current.flex = fn, current.fix !== fn ? current.fix : current.fix = ((...a) => current.flex(...a)) as F;
+};
+
function useModelsTreeState({ filterInfo, onFilterApplied, ...props }: UseModelsTreeStateProps) {
+ const processRules = useEvent(props.processRules ?? ((x: Ruleset) => x));
+ const processSearchRules = useEvent(props.processSearchRules ?? ((x: Ruleset) => x));
const rulesets = {
general: useMemo(
() =>
- createRuleset({
+ processRules(createRuleset({
enableElementsClassGrouping: !!props.hierarchyConfig?.enableElementsClassGrouping,
elementClassSpecification: props.hierarchyConfig?.elementClassSpecification,
showEmptyModels: props.hierarchyConfig?.showEmptyModels,
- }),
- [props.hierarchyConfig?.enableElementsClassGrouping, props.hierarchyConfig?.elementClassSpecification, props.hierarchyConfig?.showEmptyModels],
+ })),
+ [props.hierarchyConfig?.enableElementsClassGrouping, props.hierarchyConfig?.elementClassSpecification, props.hierarchyConfig?.showEmptyModels, processRules],
),
search: useMemo(
() =>
- createSearchRuleset({
+ processSearchRules(createSearchRuleset({
elementClassSpecification: props.hierarchyConfig?.elementClassSpecification,
showEmptyModels: props.hierarchyConfig?.showEmptyModels,
- }),
- [props.hierarchyConfig?.elementClassSpecification, props.hierarchyConfig?.showEmptyModels],
+ })),
+ [props.hierarchyConfig?.elementClassSpecification, props.hierarchyConfig?.showEmptyModels, processSearchRules],
),
};
Having this extension, we did this
<ModelsTree key="Construction" treeType="Construction" enablePreloading
enableElementsClassGrouping={ClassGroupingOption.Yes}
processRules={ModelsTreeUtils.patchConstructionTree}
processSearchRules={ModelsTreeUtils.patchConstructionTreeSearch}
/>
-------
const constructionInstanceFilter = {
find: "json_extract(this.JsonProperties, \"$.Subject.Job.Bridge\") = NULL AND " +
"ifnull(json_extract(this.JsonProperties, \"$.Subject.Model.Type\"), \"\") <> \"Hierarchy\"",
append: " AND this.CodeValue <> \"Construction data\"",
};
const slicesRule = {
ruleType: "ChildNodes" as const,
condition: "ParentNode.IsOfClass(\"WorkAreaDetailingElement\", \"Construction\")",
specifications: [
{
specType: "RelatedInstanceNodes" as const,
relationshipPaths: [
{
relationship: {
schemaName: "Construction",
className: "ConstructionDetailingElementSplitsGeometricElement3d",
},
direction: "Backward" as const,
targetClass: {
schemaName: "Construction",
className: "ConstructionDetailingElement",
},
},
],
groupByClass: false,
groupByLabel: false,
},
],
customizationRules: [
{
ruleType: "ExtendedData" as const,
items: {
modelId: "this.Model.Id",
categoryId: "this.Category.Id",
icon: "\"icon-item\"",
groupIcon: "\"icon-ec-class\"",
},
},
],
};
const constructionRulesAppend: ChildNodeRule[] = [
slicesRule,
produce(slicesRule, s => {
s.condition = "ParentNode.IsOfClass(\"GeometricElement3d\", \"BisCore\")" +
" AND NOT ParentNode.IsOfClass(\"WorkAreaDetailingElement\", \"Construction\")";
const i: RelatedInstanceNodesSpecification = s.specifications[0];
i.instanceFilter = "NOT this.HasRelatedInstance(\"Construction:ConstructionDetailingElementSplitsGeometricElement3d\", \"Forward\"," +
" \"Construction:ConstructionDetailingElement\")";
return s;
}),
];
export namespace ModelsTreeUtils {
export const constructionRulesId = "ConstructionTree";
export const patchConstructionTree = (ruleset: Ruleset): Ruleset => produce(ruleset, rules => {
if (rules.id !== "tree-widget-react/ModelsTree")
throw new ProgrammerError(`Wrong id: "${rules.id}"`);
rules.id = constructionRulesId;
if (!rules.rules.some(x => x.ruleType === "ChildNodes" &&
x.specifications?.some(s => s.specType === "RelatedInstanceNodes" && s.instanceFilter === constructionInstanceFilter.find &&
(s.instanceFilter += constructionInstanceFilter.append))))
throw new ProgrammerError("Construction Rules - Instance Filter Not Found");
rules.rules.push(...constructionRulesAppend);
return rules;
});
export const patchConstructionTreeSearch = (ruleset: Ruleset): Ruleset => produce(ruleset, rules => {
if (rules.id !== "tree-widget-react/ModelsTreeSearch")
throw new ProgrammerError(`Wrong id: "${rules.id}"`);
rules.id = `${constructionRulesId}Search`;
if (!rules.rules.some(x => x.ruleType === "ChildNodes" &&
x.specifications?.some(s => s.specType === "RelatedInstanceNodes" && s.instanceFilter === constructionInstanceFilter.find &&
(s.instanceFilter += constructionInstanceFilter.append))))
throw new ProgrammerError("Construction Search Rules - Instance Filter Not Found");
return rules;
});
}
We have rechecked and this is still relevant for the user, hence we ask for guidance to provide such tree to them
Hi @GintV , few comments
- Just because a single user is requesting a highly specific workflow / feature, doesnt mean we will add support/expose functionality for them. We need more details.
- You should not be presenting a misrepresentation of the data in your app via the trees, which will be very different from the trees users will see in other applications like Infrastructure Cloud.
- Tagging @diegoalexdiaz and @ColinKerr if they have additional comments to the data being presented here
- You should not be manually patching our code and going to PROD, this is very brittle and prone to break you long term.
- What other alternatives have you explored? Sounds like a tree is not the best fit here
- Have you spoken to your PM and UX for alternatives?
Hi @GintV, the model-tree as implemented is a viewer that exposes the state of the Subject-hierarchy in an iModel, being aware of the most fundamental whole-part relationships in BisCore (i.e. parent-child, submodels).
Since its original implementation was driven by Connector-workflows, it unfortunately had to embrace some functionality that we regret now-days (e.g. hiding of certain Subjects, merging of Subject-branches in some cases, etc), while only showing GeometricElement3d instances. Due to these dependencies with Connector-workflows, the model-tree widget is very hard to change.
Furthermore, since the Subject-hierarchy evolved to become the way data-writers (e.g. Connectors or iTwin Native apps) want to "physically"-organize their data into Channels in an iModel, it can't really be edited by users.
Having said that, different personas in an iTwin want to have control over how data is hierarchically presented to them. This is a need that is gradually being addressed by every data-writer, according to their workflows and personas involved. For example:
- OpenSite+ (authoring iTwin Native app) introduced its own parallel hierarchy based on the patterns of the ClassificationSystems schema. The app understands and controls the first few layers of nodes in that tree, while users can customize nodes under them.
- PlantSight introduced a parallel hierarchy based on classes on their application that is hydrated during the running of their data-aggregation & curation workflow. That is, users don't directly modify such hierarchy, but they indirectly lay it out based on input during some of the steps in such workflow.
- This topic hasn't evolved much for Connectorized data. Users today need to become familiar with the "Subject-hierarchy" that a particular Connector produces based on the way their external data is laid out in order to, by controlling the external lay-out, produce a "Subject-hierarchy" that reflects the hierarchy they want to work with through the model-tree widget. Certainly, far from ideal.
- These user-oriented hierarchies don't always have to be persisted in the iModel though. There has been experiments by BIC enabling a user-controlled tree-view from PresentationRules provided by power-users. One can visually produce any tree-view following classes/queries/associations from data that is located anywhere in the iModel.
I'm mentioning these experiences for inspiration. It sounds you're facing the need of a hierarchy that is initially identical to the Subject-hierarchy in your channel, but can be later edited by users. It could be addressed with:
- A full hierarchy persisted in parallel, or ...
- a hybrid (not sure if it is possible to identify the cases when the user modifies the hierarchy separately from the initial state, without to persist a whole parallel hierarchy)
- Or, if it is safe for users to physically reorganize Subjects/models in your channel based on pure BisCore fundamentals, the model-tree widget might still be an option.
The first two options require a new tree-view control in your app, different from the model-tree. It could be based on/copied from the model-tree implementation though (as you showed us in this very issue).
Please let us know which option fits better according to your needs. There might be overlapping with the needs behind other similar hierarchies so we'd like to make sure there's coordination across these efforts.
@diegoalexdiaz covered options in more detail so I will give give my on the models tree and this case in particular.
The models tree has not proven to be the most loved tree, but it does show where data lives ... not that is very useful for most end users given how we shuffle data around in locations that make most sense from a development point of view.
In this case they are splitting an element by creating N split elements. This is done in another model because 'connectors'. So logically these elements belong with the element they split. I could imagine also being able to see all splits grouped together, but that would only be useful for some review of splits workflow.
I think we need to break out of the binds of the model tree but I will admit I do not have a generic replacement to offer up.
IMO, at the very least, if anyone makes the tree similar to Models tree + what this is proposed in this issue (show elements from different model as child nodes for geometric element), the tree should not be a "visibility tree", meaning it should not have visibility controls. I just can't see how it could be made to work predictably...