Items with features view
Introduction
The "Items with features" view and its siblings "Products with features" and "Variants with features" views show all items/variants/products of a structure group and display their attributes in the context of the structure group features. This means values will be converted in the datatype and unit of the structure group feature.
The old implementation of this view had some flaws due to its origin as a customer solution:
In the old view the decision whether an attribute is mapped to a feature has been done via the name. This could lead to free attributes being displayed in the view and other mapped attributes not being displayed because the name of the attribute was not the same as the name of the feature it was mapped to. In the new view the mapping is done through the AASGAM.
The old view would not work in a revision, the new one does.
We also added some new features to the view:
The field selection dialog lets you choose additional fields of the item/variant/product. These will be displayed to the left of the feature columns. In contrast to the feature columns, you can reorder and remove them by dragging them.
A second language for all feature value columns can be selected which will double the feature columns.
A header language can be selected. This will change the label in the column header of the feature columns.
An additional feature column filter has been added. By default, the number of columns is not limited but the number of displayed features is limited. This means if 10 features are configured you get 10 columns for the first language, 10 columns for the second language (if selected) plus additional second value columns in one or two languages for all features with datatype "range". At most you get 40 columns. You can adjust the number of features in the plugin_customization.ini as shown below.
The feature columns are ordered by the value of the features' field "sequence", not alphabetically. If there are more features than the preference says, those with the highest sequence numbers won't be displayed. If you need to edit them, use the feature column filter.preference max.feature.count in plugin_customization.ini# Defines the maximum count of features displayed in the items with features (specialized) view. If you
# increase
this
number be sure that your view still works correctely. There is a limitation on the character count
# of all column headers of about
3000
. If you exceed
this
limit it could be that the table will not be displayed
# properly.
# com.heiler.ppm.article.structure.core/max.feature.count =
50
Also good to know are these points:
When grouping or sorting, only the item/variant/product columns (sometimes called static columns) are taken into consideration.
Also when saving the layout only the item/variant/product columns, the selected languages and the feature filter configuration are saved. The feature columns will not be saved in the layout because they are context dependant and most likely a different structure group will be selected when you open a layout.The initial first language (in an empty workspace) is the client language. No second language and no header language is selected.
You need the article read action right to right see and open the view and the field rights for ArticleAttributeValue.Value and ArticleAttributeValue.SecondValue to see and edit attribute values.
The view
This is an overview of the view implementation. These are the core objects of the view and their interaction will be briefly described below. However, there are many many details which can't be described here.
ArticlesInFeatureContextTableView
creates FeaturesManager, FeatureFilterManager and FeatureLanguageManager and registers as listener
when notified by the FeatureManager that features have changed, it asks the FeatureFieldFactory to generate the featureFields, sets them in the tableContainer and fires columnsChanged
when notified by the FeatureLanguageManager of a
new content language:
asks the FeatureFieldFactory to generate the featureFields, sets them in the tableContainer and fires columnsChanged
updates the table layout config for being able to restore the view later
new header language:
updates the headers of the columns using the FeatureFieldNameFactory which asks the FeatureLanguageManager for the current headerLanguage.
updates the table layout config for being able to restore the view later
when notified by the FeatureFilterManager that the filter settings changed, it is only stored in the table layout.
The columns (and content) are adjusted by the notification of the FeatureManager
FeatureManager
knows about the structureGroup context
knows about the preference indicating the number of features
manages the list of features which is updated when
the filter changed (= when notified by the FeatureFilterManager)
when the structureGroup context changed
FeatureFilterManager
contains the filter model (containing the settings) which is manipulated by the FeatureFilterDialog directly. When confirming the dialog with OK, it fires filterChanged().
FeatureLanguageManager
manages primary, secondary and header language
notifies its listeners when one of the content languages or the header language is changed
can persist its settings
Data Access and fragments
The old implementation of the view mapped attributes and features via their name which might be wrong. An attribute can have the same name as a feature but no mapping to it and the other way around, they can have different names but the attribute has a mapping to the feature.
So the right way to load data for the view is to go over the AASGAM. This wasn't possible. In the view we want to get all attributes mapped to the structureGroupAttributes of a specific structureGroup. When loading a listModel with attributes, you need a fieldPath to the aasgam fields. One of the logical keys of these fieldPaths is the attribute name. However, we don't know the names as they can be different to the structureGroupAttribute names.
Therefore we needed a new fragment. The idea is to ignore the name of the attribute but provide enough additional information to be able to retrieve all attributes mapped to a structureGroupAttribute of a certain structureGroup. In the aasgam entity we only wanted to use proxies, so the combination of structureGroup and structureFeature needed to uniquely identify a mapped attribute. Therfore it is no longer allowed to map two attributes of the same article to the same StructureGroupAttribute.
There is no update script handling the case of two attributes being mapped to the same StructureGroupAttribute. If the data base contains such data constellation it is not defined what happens in the view.
The new fragments of type ArticleAttributeSGContextFragment are contributed in com.heiler.ppm.article.structure.server. They are HQLFragments and use contributed Parameter and ParameterBuilder which replace the generic HQL generation for the ArticleAttribute.LK.Name with our own implementation which takes the given StructureGroup and StructureAttribute from the FieldPath and adds a condition which matches these values against the AASGAM.StructureGroupId and AASGAM.StructureAttributeId of the Attribute.
There are four contributions by intent because there are cases in which you wouldn't get data because of the join condition, if handled by one fragment instance. For example you wouldn't get a value for english if there is no name in english. With the AASGAM fragment there is a different problem. The AASGAM has two parents, the ArticleAttribute and the ArticleStructureMap. It is undefined which parent is used by the fragment, so sometimes it would use ArticleAttribute and because we don't know the attribute name, the resulting data wasn't correct/values were missing.
Fragment changes in general
In addition to the substitution of the name by the StructureGroup and StructureFeature, the new fragment and the old fragment have to be able to run in parallel. To make that possible we had to make some changes in the FragmentManager and FragmentRegistry. Now, they handle a list of fragments for each entityType or fieldType.
For new fragments there are two things to be aware of:
The Fragment interface has an additional method boolean isResponsible( FieldPath fieldPath ). FragmentBaseImpl has a default implementation returning true.
If you have two fragments for the same fieldType or entityType you need to specify a priority. If only one fragment is used everything will work fine without a priority.
When loading a listmodel with fieldPaths, all fragments which were contributed for the entityType or fieldType of the fieldPath will be asked if they feel responsible for this fieldPath. They will be asked in the order of their priority where the fragment with the highest priority will be asked first. The first fragment returning true when isResponsible is called will handle the fieldPath.
Displayed values and read validations
The Attribute has three values, the first value, the second value and the different default value, but we only have at most two columns for each feature. This leads to the question which value is displayed?
If a different default value is available, the different default value will be displayed
If no different default value is available, the value for identifier "Default" will be displayed. In short: This view only displayes values with identifier "Default" even if the attribute is multivalue and has multiple values.
If the datatype of the feature is range always the first and second value should be displayed, never the different default value.
Read validations
We added a variety of validations which are displayed in a lightweight dialog when you click on the information, warning or error icon in a cell.
Validation |
Message |
Type |
Meaning |
Class |
Is an attribute mapped to the feature? |
The feature is not yet associated with an attribute |
Warning |
The item doesn't have an aasgam with the selected structureGroup and the feature of the corresponding column |
MappedAttributeExistsValueValidator |
If the feature is mandatory, is there a value? |
No value exists for this mandatory feature |
Error |
The feature is mandatory and the attribute neither has a value nor a different default value |
MandatoryValueValidator |
Are both, attribute and feature, multiValue or not? |
The "multiple values" property of the attribute does not match that of the feature |
Error |
Cell will be read only |
MultiValueFlagValueValidator |
Are datatypes matching? |
The data type check has detected incompatible data types |
Error |
see table below |
DataTypeValueValidator |
The data type check has detected different data types |
Warning |
|||
Are Units convertible? |
The attribute has a unit but the feature has no unit |
Warning |
UnitValueValidator |
|
The units of the feature and attribute cannot be converted into each other |
Error |
Cell will be read only |
||
Are datatype and unit of the feature matching? |
The attribute has a unit, but no data type |
Warning |
DatatypeUnitValueValidator |
|
The feature has the 'Date' or 'Logical Value' data type and a unit |
Error |
Cell will be read only |
||
The attribute has a unit, but has no data type |
Warning |
|||
The attribute has the 'Date' or 'Logical Value' data type and a unit |
Error |
Cell will be read only |
||
The attribute and feature have the "Character String" data type and different units |
Warning |
If feature and attribute both have datatype string and a unit and their units are convertible |
||
The attribute and feature have the "Character String" data type but only one of them has a unit |
Warning |
|||
Is the value one of the preset values? |
The divergent present value does not match any of the preset values |
Error |
Attribute is mapped, feature is not multiValue and different default value is present |
PresetValueValidator |
The value does not match any of the preset values |
Error |
Value is not checked if different default value is present (only if feature is multiValue) |
||
No Validation; |
No attribute value was maintained |
Info |
Different default value is displayed but there is no value. |
ConvertValueValidator |
The attribute value is <attributeValue> |
Info |
Shows the converted and formatted value as additional information if the different default value is displayed |
||
The saved attribute value is incompatible with the attribute data type but is compatible with the feature data type |
Warning |
|||
The saved attribute value is incompatible with both the attribute data type and the feature data type |
Error |
The unconverted db value is displayed. No unit conversion! |
||
The attribute value was converted into the unit of the feature |
Info |
|||
Converting the attribute value into the unit of the feature results in a loss of accuracy: the converted value would be <convertedValueWithFullPrecision> but, according to the feature data type, it should be <convertedValueInSgaDatatype> |
Error |
|||
The converted attribute value is incompatible with the feature data type |
Error |
|||
A problem occurred during the conversion |
Error |
Additional Information on DataTypeValueValidator
This is an overview which datatype combinations are allowed and which are not. If an error is detected, the cell will be read only.
Attribute datatype |
|||||||||
null |
String |
Boolean |
Date |
Integer |
Decimal |
Double |
Range |
||
Feature |
null |
OK |
OK |
OK |
OK |
OK |
OK |
OK |
OK |
String |
OK |
OK |
OK |
OK |
OK |
OK |
OK |
OK |
|
Boolean |
OK |
ERROR |
OK |
ERROR |
ERROR |
ERROR |
ERROR |
ERROR |
|
Date |
OK |
ERROR |
ERROR |
OK |
ERROR |
ERROR |
ERROR |
ERROR |
|
Integer |
OK |
WARNING |
WARNING |
ERROR |
OK |
WARNING |
WARNING |
ERROR |
|
Decimal |
OK |
WARNING |
WARNING |
ERROR |
OK |
OK |
WARNING |
ERROR |
|
Double |
OK |
WARNING |
WARNING |
ERROR |
OK |
OK |
OK |
ERROR |
|
Range |
OK |
ERROR |
ERROR |
ERROR |
ERROR |
ERROR |
ERROR |
OK |
Additional Information on ConvertValueValidator
The different default value is not converted at all. It is assumed that the value has a matching datatype and the value is already correct for the unit of the structure group attribute. This is because it was initially added to the feature groups list before it was assigned as different default value to the attribute. (Work will be done in this area in future versions.)
For a value the simplified conversion looks like this:
Adding your own validations
To add a validation to the "Items with features" view do the following:
Write your own validations by implementing ListModelValueValidator.
Extend the class FeatureContextValidationCreator and overwrite createValidators(). Add your own instances of ListModelValueValidator to the returned array.
Extend the view and overwrite the method protected FeatureContextValidationCreator createValidationCreator( StructureGroupProxy structureGroupProxy, Long attributeKeyLanguageId ). Return your own FeatureContextValidationCreator in this method.
The interface ListModelValueValidator:
public
interface
ListModelValueValidator
extends
Serializable
{
public
FieldPath getTargetFieldPath();
public
FieldPath[] getAdditionalFieldPaths();
public
void
validate( ValueValidationDataRow dataRow )
throws
CoreException;
}
The getTargetFieldPath() method returns a fieldPath. The value in the column of this fieldPath is the one which is validated. If this fieldPath is not one of the requested fieldPaths, the additional field paths won't be added to requested fieldPaths.
In the method getAdditionalFieldPaths() you can request more information by returning fieldPaths leading to the information you need. This information will be present in the listModel
The method validate() is the one which is actually doing the validation (or calculation). Also this method takes responsibility for writing the result back into the listModel for later use. The ValueValidationDataRow is a wrapper for ListEntry and ListEntryBody which allows you to simply use getValue(FieldPath fieldPath) and setValue(FieldPath fieldPath, Object value) in virtual and non-virtual list model loading.
We used a special object (FeatureValidationResult) which bundles the value, a list of validation results in form of StatusExt objects and additional information (also used when writing values later) and replaced the original value in the column of the according fieldPath with it.
Validation example (items with features view)
In this example an additional information - the article attribute value sequence - is displayed as validation result.
Write the validation
Write a validationpublic
class
AttributeDisplayOrderValidation
extends
FeatureValueValidatorBaseImpl
{
private
static
final
long
serialVersionUID = 1L;
private
FieldPath targetFieldPath;
private
FieldPath additionalFieldPath;
public
AttributeDisplayOrderValidation( FieldPath targetFieldPath )
{
this
.targetFieldPath = targetFieldPath;
this
.additionalFieldPath = createAdditionalFieldPath( targetFieldPath,
"ArticleAttributeValueType.DisplayOrder"
);
//$NON-NLS-1$
}
@Override
public
FieldPath getTargetFieldPath()
{
return
this
.targetFieldPath;
}
@Override
public
FieldPath[] getAdditionalFieldPaths()
{
return
new
FieldPath[] {
this
.additionalFieldPath };
}
@Override
public
void
validate( ValueValidationDataRow dataRow )
throws
CoreException
{
FeatureValidationResult validationResultObject = initValidationResultObject( dataRow );
if
( validationResultObject.getOriginalValue() !=
null
)
{
Object displayOrder = dataRow.getValue(
this
.additionalFieldPath );
String message =
"Attribute value sequence: "
+ displayOrder;
//$NON-NLS-1$ //$NON-NLS-2$
StatusExt status =
new
StatusExt( Categories.SUMMARY, Status.INFO, Activator.PLUGIN_ID,
0
, message,
null
);
validationResultObject.addResult( status );
}
}
}
The targetFieldPath is the fieldPath of the column the validation result is displayed in (usually a fieldPath to ArticleAttributeValueType.Value or ArticleAttributeValueType.SecondValue).
The additionalFieldPath is the one of the additional information you want so display as a validation result.
The validate method has to first call initValidationResultObject which replaces the original value by the newly created FeatureValidationResult. Then you just make a message with the value of the additionalFieldPath and add it to the list of validation results in the FeatureValidationResult. The ValueValidationDataRow is just a wrapper for the different interfaces used by virtual and non virtual listModel loading.Write your own ValidationCreator and add your own validation
Extend the ValidationCreatorpublic
class
CustomFeatureContextValidationCreator
extends
FeatureContextValidationCreator
{
public
CustomFeatureContextValidationCreator( StructureGroupProxy structureGroupProxy, Long attributeKeyLanguageId )
{
super
( structureGroupProxy, attributeKeyLanguageId );
}
@Override
protected
ListModelValueValidator[] createValidators( Set< FieldPath > valueFieldPaths,
Map< StructureFeatureProxy, StructureGroupAttributeDataForValidation > sgaDataForValidation,
RevisionToken revisionToken )
throws
CoreException
{
List< ListModelValueValidator > validators =
new
ArrayList< ListModelValueValidator >();
for
( FieldPath fieldPath : valueFieldPaths )
{
validators.add(
new
AttributeDisplayOrderValidation( fieldPath ) );
}
ListModelValueValidator[] otherValidators =
super
.createValidators( valueFieldPaths, sgaDataForValidation,
revisionToken );
validators.addAll( Arrays.asList( otherValidators ) );
return
validators.toArray(
new
ListModelValueValidator[
0
] );
}
}
Don't remove the standard validations! They write additional information in the FeatureValidationResult which is used when storing/saving a changed value.
Then extend the items with features view and add your validationCreator
Extend the viewpublic
class
ItemsWithFeaturesCustomTableView
extends
ArticlesInFeatureContextTableView
{
@Override
protected
FeatureContextValidationCreator createValidationCreator( StructureGroupProxy structureGroupProxy,
Long attributeKeyLanguageId )
{
return
new
CustomFeatureContextValidationCreator( structureGroupProxy, attributeKeyLanguageId );
}
}
Validations outside of the "Items with features" view
You can add an array of ListModelValueValidators to the ListModeLoadParams when loading a listModel via the ListModelLoadService.
Note that this is not a feature of its own but more a by-product of another feature. Therefore there is no way to add additional columns to the listModel for validation results. If there is a general need for adding columns, please talk to PM and add a feature request in Jira.
Validation example
In this Example a validator should add "20" to the second value if first and second value are initially equal.
Create two items.
The first one has different first and second values. The second value will not be changed by the validator.
The second value has the same value (60) for first and second value. The validator will change the second value to 80.
...
CatalogProxy catalog =
this
.articleUtils.createCatalog();
ArticleProxy article =
this
.articleUtils.createArticle( catalog,
"article_1"
);
this
.articleStructureUtils.mapArticleToStructureGroup( article, structureGroup );
this
.articleUtils.setArticleAttributeFirstAndSecondValue( article,
this
.featureName, Languages.GERMAN,
"30"
,
"50"
);
ArticleProxy secondArticle =
this
.articleUtils.createArticle( catalog,
"article_2"
);
this
.articleStructureUtils.mapArticleToStructureGroup( secondArticle, structureGroup );
this
.articleUtils.setArticleAttributeFirstAndSecondValue( secondArticle,
this
.featureName, Languages.GERMAN,
"60"
,
"60"
);
...
Implement the validator. The targetFieldPath which is the secondValue is handed to the validator in the constructor. The validator returns the fieldPath for the firstValue because it needs it for its validation. This fieldPath will be added to the resulting listModel. The method validate changes the value of the secondValue.
public
class
AdjustSecondValue
implements
ListModelValueValidator
{
private
FieldPath targetFieldPath;
private
FieldPath firstValueFieldPath;
public
AdjustSecondValue( FieldPath fieldPath )
{
this
.targetFieldPath = fieldPath;
}
@Override
public
FieldPath getTargetFieldPath()
{
return
this
.targetFieldPath;
}
@Override
public
FieldPath[] getAdditionalFieldPaths()
{
EntityPath entityPath =
this
.targetFieldPath.getEntityPath();
this
.firstValueFieldPath =
new
FieldPath( entityPath,
"ArticleAttributeValueType.FirstValue"
);
return
new
FieldPath[] {
this
.firstValueFieldPath };
}
@Override
public
void
validate( ValueValidationDataRow dataRow )
throws
CoreException
{
String firstValueString = ( String ) dataRow.getValue(
this
.firstValueFieldPath );
String secondValueString = ( String ) dataRow.getValue(
this
.targetFieldPath );
Integer firstValue = Integer.parseInt( firstValueString );
Integer secondValue = Integer.parseInt( secondValueString );
int
difference = secondValue.intValue() - firstValue.intValue();
if
( difference ==
0
)
{
dataRow.setValue(
this
.targetFieldPath, secondValue +
20
);
}
}
}
Add the validation to the ListModelLoadParams when loading the data.
private
ListModel loadListModel( EntityItemList articles, FieldPath[] fieldPaths )
throws
CoreException,
InterruptedException
{
Entity startEntity = RepositoryComponent.getRepositoryService()
.getEntityByIdentifier(
"Article"
);
//$NON-NLS-1$
FieldPath targetFieldPath = fieldPaths[
1
];
ListModelValueValidator[] valueValidators =
new
ListModelValueValidator[] {
new
AdjustSecondValue( targetFieldPath ) };
ListModelLoadService listModelLoadService = EntityManagementComponent.getListModelLoadService();
ListModelLoadParams loadParams =
new
ListModelLoadParams( articles, startEntity, fieldPaths );
loadParams.setListModelValueValidators( valueValidators );
ListModel listModel = listModelLoadService.loadListModel(
new
NullProgressMonitor(), loadParams );
return
listModel;
}
The resulting listModel contains the changed second value for article_2
3768
[|], article_1,
3768
,
1000
,
3770
[|],
50
,
30
3769
[|], article_2,
3769
,
1000
,
3770
[|],
80
,
60
Writing
The diagram below shows which value will be written, the ArticleAttribute.Value or the ArticleAttributeStructureGroupAttributeMap.DomainValue.
If a value is entered in the column of a second value (if available), it is written to ArticleAttributeValue.SecondValue. If the attribute has another datatype than "range", the value isn't written at all.
When a value is edited, it is edited in the context of the structureGroupAttribute, so it is first converted in the datatype of the feature, then converted into the attributes unit and then converted to the datatype of the attribute.
Extending the feature filter
In addition to the standard filter, the view provides the possibility to filter the displayed features. This is an important feature regarding the restriction of a maximum of 50 features which can be displayed. By default the features can be filtered for their attribute type, mandatory value and purpose but it is also possible to extend the filter for custom (reserved) fields.
The architecture is quite simple and based on a strict separation of model and UI. As you can see in the UML diagramm below the central class is the FeatureFilterManager which contains the extendable model.
To extend the filter you have to do the following steps:
Sublcass the FeatureFilterManager and overwrite the methods createCategoryNodes() and createValueNodesForCategory() as shown in the example below
Subclass the ArticleInFeatureContextTableView and overwrite the method getFeatureFilterManager() like in the example below
Changes for the UI are not necessary
Sublcass the FeatureFilterManager and overwrite the methods createCategoryNodes() and createValueNodesForCategory()
public
class
CustomFeatureFilterManager
extends
FeatureFilterManager
{
private
FilterCategoryNode customCategoryNode;
@Override
protected
List< FilterCategoryNode > createCategoryNodes()
{
List< FilterCategoryNode > categoryNodes =
super
.createCategoryNodes();
Field customField = RepositoryComponent.getRepositoryService()
.getFieldByIdentifier(
"StructureGroupAttribute.Grouping"
);
//$NON-NLS-1$
this
.customCategoryNode =
new
FilterCategoryNode(
"Grouping"
, customField );
//$NON-NLS-1$
categoryNodes.add(
this
.customCategoryNode );
return
categoryNodes;
}
@Override
protected
List< FilterValueNode > createValueNodesForCategory( FilterCategoryNode categoryNode )
{
if
( categoryNode.equals(
this
.customCategoryNode ) )
{
List< FilterValueNode > valueNodes =
new
ArrayList<>();
valueNodes.add(
new
FilterValueNode(
"ID_BASEDATA"
,
"Base data"
,
"Base data"
) );
//$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
valueNodes.add(
new
FilterValueNode(
"ID_INTERFACE"
,
"Interface"
,
"Interface"
) );
//$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
return
valueNodes;
}
return
super
.createValueNodesForCategory( categoryNode );
}
}
The Category nodes are the parent nodes and contain the field which has to be evaluated. In other words: Get this field from the structure group attribute and check its content vs. the selected values. Of course this field must be within the StructureGroupAttribute subentity. Don´t forget to call super.createCategoryNodes() so you will get the default filters, or ignore the call to remove them.
The Filtervalue nodes contain an id, a value and a displayname. The id is only used to persist and restore the state of the filter, so use an unique identifier. The value is is the real value which will be evaluated vs the value in the database.
If an enumprovider is contributed to your field, you can simply iterate over the entries and create a FilterValueNode for every entry. Use the label as displayName and the key as value.
For a better understanding see the screenshot of the filter below. The Category nodes are within a red frame while the Filtervalue nodes are within blue frames. Focusing on the "Grouping" filter it means "Get the value of the structure group attribute field "Grouping" and check if it is equal to the selected value "Base data" (Note that displayname and value are the same in this example). If the structure group attribute field is a List, it will be checked if this list contains the selected value.
Subclass the ArticleInFeatureContextTableView and overwrite the method getFeatureFilterManager()
public
class
CustomArticlesInFeatureContextView
extends
ArticlesInFeatureContextTableView
{
@Override
public
FeatureFilterManager getFeatureFilterManager()
{
if
(
this
.featureFilterManager ==
null
)
{
this
.featureFilterManager =
new
CustomFeatureFilterManager();
}
return
this
.featureFilterManager;
}
}
We try to prevent that views have to be subclassed in future. So we will see if we can provide a better solution for PIM 8 or later versions. Right now we see no other way to make the filter extendable since we don´t want to provide an extension point for such a small thing.
Changes for the UI are not necessary
The filter Dialog will be automatically generated from the model, so you don´t have to implement anything on UI side.
Questions and Answers
Why do I need the Article edit right to see the view?
Actually you only need the Article read right to see the view.Why can't I reorder and remove the feature columns?
The features are ordered by their order number and can be filtered additionally. Reordering or removing feature columns would not be saved and so get lost if another structure group was selected or the current structure group was changed. This makes it nearly useless.Are there similar read validations for detail models?
Not yet! Perhaps we will add such validations when we refactor other views like the ArticleAttributeTableView (and don't merge it with the Article Attributes cumulative View which would change the data access to listModel loading).