Skip to content

Commit

Permalink
Render sameInstance properites inline (iTwin#1195)
Browse files Browse the repository at this point in the history
* Render sameInstance properites inline

* Minor fixes

* Change

* extract-api

* PR nestedHierarchy and css fixes

* PR Fixes - Field API and DataProvider

* PR Fixes. Table update

* API and Changes update

* Render sameInstance properites inline

* PR nestedHierarchy and css fixes

* PR Fixes - Field API and DataProvider

* PR Fixes. Table update

* PR fixes

* PR Fixes. Full-stack tests

* PR Fixes. Linter and minor fixes

* PR Fixes. API and Changes

* PR fixes. Test fix

* lint fix

* Added minor style fixes and ui-test-app-example

* PR Fixes. classnames fix

* classNames fix

Co-authored-by: Grigas <[email protected]>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
  • Loading branch information
3 people authored May 13, 2021
1 parent a4ffd7d commit 7902272
Show file tree
Hide file tree
Showing 15 changed files with 627 additions and 40 deletions.
4 changes: 3 additions & 1 deletion common/api/ui-components.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -803,6 +803,8 @@ export interface CellItem {
alignment?: HorizontalAlignment;
isDisabled?: boolean;
key: string;
// @alpha
mergedCellsCount?: number;
record?: PropertyRecord;
style?: ItemStyle;
}
Expand Down Expand Up @@ -4431,7 +4433,7 @@ export class Table extends React.Component<TableProps, TableState> {
// @internal (undocumented)
componentDidMount(): void;
// @internal (undocumented)
componentDidUpdate(previousProps: TableProps): void;
componentDidUpdate(previousProps: TableProps, previousState: TableState): void;
// @internal (undocumented)
componentWillUnmount(): void;
// @internal
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"changes": [
{
"packageName": "@bentley/presentation-common",
"comment": "Added `relationshipMeaning` property to `NestedContentField`",
"type": "none"
}
],
"packageName": "@bentley/presentation-common",
"email": "[email protected]"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"changes": [
{
"packageName": "@bentley/presentation-components",
"comment": "Updated table/DataProvider so that it would find sameInstance nested properties and extract their values/fields when needed.",
"type": "none"
}
],
"packageName": "@bentley/presentation-components",
"email": "[email protected]"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"changes": [
{
"packageName": "@bentley/ui-components",
"comment": "Added `mergedCellsCount` property to `CellItem`. It is used for determining width of the cell.",
"type": "none"
},
{
"packageName": "@bentley/ui-components",
"comment": "Added `overflow` property for `react-grid-Cell__value` element. `zIndex` is set in `TableCellContent` style property. These updates are necessary for rendering merged cells in `Table` component.",
"type": "none"
},
{
"packageName": "@bentley/ui-components",
"comment": "Updated `Table` component so that it would be possible to merge cells in it",
"type": "none"
}
],
"packageName": "@bentley/ui-components",
"email": "[email protected]"
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,45 @@ import * as sinon from "sinon";
import { Id64 } from "@bentley/bentleyjs-core";
import { ModelProps } from "@bentley/imodeljs-common";
import { IModelConnection, SnapshotConnection } from "@bentley/imodeljs-frontend";
import { ContentSpecificationTypes, InstanceKey, KeySet, Ruleset, RuleTypes } from "@bentley/presentation-common";
import { ContentSpecificationTypes, InstanceKey, KeySet, RelationshipDirection, RelationshipMeaning, Ruleset, RuleTypes } from "@bentley/presentation-common";
import { PresentationTableDataProvider } from "@bentley/presentation-components";
import { Presentation } from "@bentley/presentation-frontend";
import { SortDirection } from "@bentley/ui-core";
import { initialize, terminate } from "../../IntegrationTests";

const RULESET_MODIFIER: Ruleset = {
id: "ruleset",
rules: [{
ruleType: RuleTypes.Content,
specifications: [{
specType: ContentSpecificationTypes.SelectedNodeInstances,
}],
}, {
ruleType: RuleTypes.ContentModifier,
class: {
schemaName: "BisCore",
className: "Model",
},
relatedProperties: [
{
propertiesSource: {
relationship: {
schemaName: "BisCore",
className: "ModelContainsElements",
},
direction: RelationshipDirection.Forward,
},
handleTargetClassPolymorphically: true,
relationshipMeaning: RelationshipMeaning.SameInstance,
properties: [
"UserLabel",
"CodeValue",
],
},
],
}],
};

const RULESET: Ruleset = {
id: "localization test",
rules: [{
Expand Down Expand Up @@ -72,6 +105,12 @@ describe("TableDataProvider", async () => {
expect(columns).to.matchSnapshot();
});

it("returns extracted columns from instances which have SameInstance relationshipMeaning", async () => {
provider.keys = new KeySet([{ className: "Generic:PhysicalObject", id: "0x74" } ]);
const columns = await provider.getColumns();
expect(columns.length).to.eq(32);
});

});

describe("getRowsCount", () => {
Expand Down Expand Up @@ -99,6 +138,26 @@ describe("TableDataProvider", async () => {
expect(row).to.matchSnapshot();
});

it("returns row with extracted cells from instances which have SameInstance relationshipMeaning", async () => {
provider.keys = new KeySet([{ className: "Generic:PhysicalObject", id: "0x74" }]);
const row = await provider.getRow(0);
expect(row.cells.length).to.eq(32);
});

it("returns row with merged cells from instances which have SameInstance relationshipMeaning and more than one value in it", async () => {
provider = new PresentationTableDataProvider({
imodel, ruleset: RULESET_MODIFIER, pageSize: 10,
});
provider.keys = new KeySet([instances.physicalModel]);

const row = await provider.getRow(0);
expect(row.cells[0].mergedCellsCount).to.be.undefined;
expect(row.cells[1].mergedCellsCount).to.eq(2);
expect(row.cells[2].mergedCellsCount).to.be.undefined;
expect(row.cells[3].mergedCellsCount).to.eq(2);
expect(row.cells[4].mergedCellsCount).to.be.undefined;
});

it("returns undefined when requesting row with invalid index", async () => {
provider.keys = new KeySet([instances.physicalModel]);
const row = await provider.getRow(1);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,7 @@ export class NestedContentField extends Field {
* @param nestedFields Contained nested fields
* @param editor Property editor used to edit values of this field
* @param autoExpand Flag specifying whether field should be expanded
* @param relationshipMeaning RelationshipMeaning of the field
* @param renderer Property renderer used to render values of this field
*/
public constructor(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ import { sort } from "fast-sort";
import memoize from "micro-memoize";
import { IModelConnection } from "@bentley/imodeljs-frontend";
import {
Content, DefaultContentDisplayTypes, Descriptor, DescriptorOverrides, Field, FieldDescriptorType, InstanceKey, Item, PresentationError,
PresentationStatus, Ruleset, SortDirection,
Content, DefaultContentDisplayTypes, Descriptor, DescriptorOverrides, Field, FieldDescriptorType, InstanceKey, Item,
NestedContentValue,
PresentationError, PresentationStatus, RelationshipMeaning, Ruleset, SortDirection, Value, ValuesDictionary,
} from "@bentley/presentation-common";
import { CellItem, ColumnDescription, TableDataProvider as ITableDataProvider, RowItem, TableDataChangeEvent } from "@bentley/ui-components";
import { SortDirection as UiSortDirection } from "@bentley/ui-core";
import { HorizontalAlignment, SortDirection as UiSortDirection } from "@bentley/ui-core";
import { ContentBuilder } from "../common/ContentBuilder";
import { CacheInvalidationProps, ContentDataProvider, IContentDataProvider } from "../common/ContentDataProvider";
import { DiagnosticsProps } from "../common/Diagnostics";
Expand Down Expand Up @@ -256,10 +257,107 @@ const createColumns = (descriptor: Readonly<Descriptor> | undefined): ColumnDesc
if (descriptor.displayType === DefaultContentDisplayTypes.List)
return [createLabelColumn()];

return sort(descriptor.fields).by([
{ desc: (f) => f.priority },
{ asc: (f) => f.label },
]).map((f) => createColumn(f));
// Return array of ColumnDescriptions created from fields which are sorted and with extracted SameInstanceFields
return getFieldsWithExtractedSameInstanceFields(
sort(descriptor.fields).by([
{ desc: (f) => f.priority },
{ asc: (f) => f.label },
])
).map((f) => createColumn(f));
};

const getFieldsWithExtractedSameInstanceFields = (fields: Field[]) => {
const updatedFields: Field[] = fields.map((f) => f.clone());
for (let i = 0; i < updatedFields.length; i++) {
const field = updatedFields[i];
if (field.isNestedContentField() && field.relationshipMeaning === RelationshipMeaning.SameInstance) {
const nestedFields = field.nestedFields.map((nestedField: Field): Field => {
nestedField.resetParentship();
return nestedField;
});

const updatedChildFields = getFieldsWithExtractedSameInstanceFields(nestedFields);
updatedFields.splice(i, 1, ...updatedChildFields);

// Skip inserted fields
i += updatedChildFields.length - 1;
}
}
return updatedFields;
};

const getFieldsWithExtractedSameInstanceFieldsAndCreateMap = (fields: Field[]) => {
let firstExtractedFieldNameToNestedFieldMap: { [fieldName: string]: Field } = {};
const updatedFields: Field[] = fields.map((f) => f.clone());

for (let i = 0; i < updatedFields.length; i++) {
const field = updatedFields[i];
if (field.isNestedContentField() && field.relationshipMeaning === RelationshipMeaning.SameInstance) {
// Reset parentship for nestedFields, so ContentBuilder.createPropertyRecord() wouldn't create an array record
const nestedFields = field.nestedFields.map((nestedField: Field): Field => {
nestedField.resetParentship();
return nestedField;
});
const { firstExtractedFieldNameToNestedFieldMap: childFirstExtractedFieldToNestedFieldMap, updatedFields: childUpdatedFields } = getFieldsWithExtractedSameInstanceFieldsAndCreateMap(nestedFields);
const deletedField = updatedFields.splice(i, 1, ...childUpdatedFields)[0];

// Map the first extracted field name to a nestedField from which the field was extracted and merge it with the map found in child fields.
firstExtractedFieldNameToNestedFieldMap = {
...firstExtractedFieldNameToNestedFieldMap,
...childFirstExtractedFieldToNestedFieldMap,
[field.nestedFields[0].name]: deletedField,
};

// Skip inserted fields
i += childUpdatedFields.length - 1;
}
}
return { firstExtractedFieldNameToNestedFieldMap, updatedFields };
};

const extractValues = (values: ValuesDictionary<Value>, sameInstanceNestedFieldNames: string[]) => {
// Map representing how many fields were merged in order to create a field specified by fieldName.
const mergedFieldsCounts: { [fieldName: string]: number } = {};
const updatedValues: ValuesDictionary<Value> = {...values};
for (const fieldName of sameInstanceNestedFieldNames) {
const value = values[fieldName];
if (!Value.isNestedContent(value))
continue;

extractNestedContentValue(updatedValues, value, mergedFieldsCounts, sameInstanceNestedFieldNames);

delete updatedValues[fieldName];
}
return { mergedFieldsCounts, updatedValues };
};

const extractNestedContentValue = (values: ValuesDictionary<Value>, nestedContentValues: NestedContentValue[], mergedFieldsCounts: { [field: string]: number }, sameInstanceNestedFieldNames: string[]) => {
// If nestedContentValue has only one value item, then all the values within the item will be extracted.
if (nestedContentValues.length === 1) {
/* Get value map from the only item. */
const nestedContentValueMap = nestedContentValues[0].values;
// eslint-disable-next-line guard-for-in
for (const valueMapKey in nestedContentValueMap) {
const childValue = nestedContentValueMap[valueMapKey];
// Check if child value in nested item itself is nested, if it is, try extracting values from it too,
// if not, then finish extracting values.
if (Value.isNestedContent(childValue) && sameInstanceNestedFieldNames.includes(valueMapKey))
extractNestedContentValue(values, childValue, mergedFieldsCounts, sameInstanceNestedFieldNames);
else
values[valueMapKey] = nestedContentValueMap[valueMapKey];
}
}
// If nestedContentValue has more than one value item, then cells that should have values from extracted item will be merged
// while leaving a link that opens a dialog item containing all the values in nestedContentValue.
if (nestedContentValues.length > 1) {
const keys = Object.keys(nestedContentValues[0].values);
const valueMapKey = keys[0];
// Set the nestedContentValue to be on the first cell that should contain extracted values.
values[valueMapKey] = nestedContentValues;
// Save information that describes how many cells will have to be merged in order to create this cell.
// Using this information, width of the merged cell will be calculated.
mergedFieldsCounts[valueMapKey] = keys.length;
}
};

const createColumn = (field: Readonly<Field>): ColumnDescription => {
Expand Down Expand Up @@ -287,14 +385,17 @@ const createLabelColumn = (): ColumnDescription => {
const createRows = (c: Readonly<Content> | undefined): RowItem[] => {
if (!c)
return [];
return c.contentSet.map((item) => createRow(c.descriptor, item));
const { firstExtractedFieldNameToNestedFieldMap: sameInstanceFieldsMap, updatedFields } = getFieldsWithExtractedSameInstanceFieldsAndCreateMap(c.descriptor.fields);
return c.contentSet.map((item) => createRow(c.descriptor, item, sameInstanceFieldsMap, updatedFields));
};

const createRow = (descriptor: Readonly<Descriptor>, item: Readonly<Item>): RowItem => {
const createRow = (descriptor: Readonly<Descriptor>, item: Readonly<Item>, sameInstanceFieldsMap: {[fieldName: string]: Field}, updatedFields: Field[]): RowItem => {
if (item.primaryKeys.length !== 1) {
// note: for table view we expect the record to always have only 1 primary key
throw new PresentationError(PresentationStatus.InvalidArgument, "item.primaryKeys");
}
const { mergedFieldsCounts: mergedCellsCounts, updatedValues } = extractValues(item.values, Object.values(sameInstanceFieldsMap).map((field) => (field.name)));
const updatedItem = new Item(item.primaryKeys, item.label, item.imageId, item.classInfo, updatedValues, item.displayValues, item.mergedFieldNames, item.extendedData);

const key = JSON.stringify(item.primaryKeys[0]);
if (descriptor.displayType === DefaultContentDisplayTypes.List) {
Expand All @@ -309,9 +410,22 @@ const createRow = (descriptor: Readonly<Descriptor>, item: Readonly<Item>): RowI

return {
key,
cells: descriptor.fields.map((field): CellItem => ({
key: field.name,
record: ContentBuilder.createPropertyRecord({ field }, item).record,
})),
cells: updatedFields.map((field: Field): CellItem => {
const itemProps: Partial<CellItem> = {};
const mergedCellsCount = mergedCellsCounts[field.name];
if (mergedCellsCount) {
itemProps.mergedCellsCount = mergedCellsCount;
itemProps.alignment = HorizontalAlignment.Center;

const nestedField = sameInstanceFieldsMap[field.name];
nestedField.name = field.name;
field = nestedField;
}
return {
key: field.name,
record: ContentBuilder.createPropertyRecord({ field }, updatedItem).record,
...itemProps,
};
}),
};
};
Loading

0 comments on commit 7902272

Please sign in to comment.