Browse Source

Datasources improvements (#6529)

* fixe ComponentDataVariable dataResolver type 'data-variable' issue

* Improve some types in data resolvers

* Add an options to add dynamic values as plain text 'asPlainText: boolean'

* Make collections loop over objects

* update getItemsCount method
fix-resizeobserver-recursion-in-framewrapview
mohamed yahia 1 year ago
committed by GitHub
parent
commit
63c459001e
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 4
      packages/core/src/data_sources/model/ComponentWithDataResolver.ts
  2. 2
      packages/core/src/data_sources/model/DataVariable.ts
  3. 73
      packages/core/src/data_sources/model/data_collection/ComponentDataCollection.ts
  4. 2
      packages/core/src/data_sources/model/data_collection/types.ts
  5. 11
      packages/core/src/data_sources/view/ComponentDataVariableView.ts
  6. 2
      packages/core/src/dom_components/model/ComponentDataResolverWatchers.ts
  7. 53
      packages/core/test/specs/data_sources/model/ComponentDataVariable.ts
  8. 18
      packages/core/test/specs/data_sources/model/conditional_variables/ComponentDataCondition.ts
  9. 173
      packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollection.ts

4
packages/core/src/data_sources/model/ComponentWithDataResolver.ts

@ -41,11 +41,11 @@ export abstract class ComponentWithDataResolver<T extends DataResolverProps> ext
options: ComponentOptions & { collectionsStateMap: DataCollectionStateMap },
): DataResolver;
getDataResolver() {
getDataResolver(): T {
return this.get('dataResolver');
}
setDataResolver(props: any) {
setDataResolver(props: T) {
return this.set('dataResolver', props);
}

2
packages/core/src/data_sources/model/DataVariable.ts

@ -11,6 +11,7 @@ export interface DataVariableProps {
defaultValue?: string;
collectionId?: string;
variableType?: DataCollectionStateType;
asPlainText?: boolean;
}
interface DataVariableOptions {
@ -29,6 +30,7 @@ export default class DataVariable extends Model<DataVariableProps> {
path: '',
collectionId: undefined,
variableType: undefined,
asPlainText: undefined,
};
}

73
packages/core/src/data_sources/model/data_collection/ComponentDataCollection.ts

@ -21,6 +21,8 @@ import { ModelDestroyOptions } from 'backbone';
import Components from '../../../dom_components/model/Components';
const AvoidStoreOptions = { avoidStore: true, partial: true };
type DataVariableMap = Record<string, DataVariableProps>;
export default class ComponentDataCollection extends Component {
dataSourceWatcher?: DataResolverListener;
@ -44,7 +46,6 @@ export default class ComponentDataCollection extends Component {
return super(props as any, opt) as unknown as ComponentDataCollection;
}
const em = opt.em;
const newProps = { ...props, droppable: false } as any;
const cmp: ComponentDataCollection = super(newProps, opt) as unknown as ComponentDataCollection;
this.rebuildChildrenFromCollection = this.rebuildChildrenFromCollection.bind(this);
@ -60,9 +61,11 @@ export default class ComponentDataCollection extends Component {
getItemsCount() {
const items = this.getDataSourceItems();
const itemsCount = getLength(items);
const startIndex = Math.max(0, this.getConfigStartIndex() ?? 0);
const configEndIndex = this.getConfigEndIndex() ?? Number.MAX_VALUE;
const endIndex = Math.min(items.length - 1, configEndIndex);
const endIndex = Math.min(itemsCount - 1, configEndIndex);
const count = endIndex - startIndex + 1;
return Math.max(0, count);
@ -132,7 +135,14 @@ export default class ComponentDataCollection extends Component {
}
private getDataSourceItems() {
return getDataSourceItems(this.dataResolver.dataSource, this.em);
const items = getDataSourceItems(this.dataResolver.dataSource, this.em);
if (isArray(items)) {
return items;
}
const clone = { ...items };
delete clone['__p'];
return clone;
}
private get dataResolver() {
@ -190,7 +200,8 @@ export default class ComponentDataCollection extends Component {
for (let index = startIndex; index <= endIndex; index++) {
const isFirstItem = index === startIndex;
const collectionsStateMap = this.getCollectionsStateMapForItem(items, index);
const key = isArray(items) ? index : Object.keys(items)[index];
const collectionsStateMap = this.getCollectionsStateMapForItem(items, key);
if (isFirstItem) {
getSymbolInstances(firstChild)?.forEach((cmp) => detachSymbolInstance(cmp));
@ -211,18 +222,20 @@ export default class ComponentDataCollection extends Component {
return components;
}
private getCollectionsStateMapForItem(items: DataVariableProps[], index: number) {
private getCollectionsStateMapForItem(items: DataVariableProps[] | DataVariableMap, key: number | string) {
const { startIndex, endIndex, totalItems } = this.resolveCollectionConfig(items);
const collectionId = this.collectionId;
const item = items[index];
let item: DataVariableProps = (items as any)[key];
const parentCollectionStateMap = this.collectionsStateMap;
const offset = index - startIndex;
const numericKey = typeof key === 'string' ? Object.keys(items).indexOf(key) : key;
const offset = numericKey - startIndex;
const remainingItems = totalItems - (1 + offset);
const collectionState = {
collectionId,
currentIndex: index,
currentIndex: numericKey,
currentItem: item,
currentKey: key,
startIndex,
endIndex,
totalItems,
@ -244,12 +257,20 @@ export default class ComponentDataCollection extends Component {
return !!parentCollectionStateMap[collectionId];
}
private resolveCollectionConfig(items: DataVariableProps[]) {
private resolveCollectionConfig(items: DataVariableProps[] | DataVariableMap) {
const isArray = Array.isArray(items);
const actualItemCount = isArray ? items.length : Object.keys(items).length;
const startIndex = this.getConfigStartIndex() ?? 0;
const configEndIndex = this.getConfigEndIndex() ?? Number.MAX_VALUE;
const endIndex = Math.min(items.length - 1, configEndIndex);
const totalItems = endIndex - startIndex + 1;
return { startIndex, endIndex, totalItems };
const endIndex = Math.min(actualItemCount - 1, configEndIndex);
let totalItems = 0;
if (actualItemCount > 0) {
totalItems = Math.max(0, endIndex - startIndex + 1);
}
return { startIndex, endIndex, totalItems, isArray };
}
private ensureFirstChild() {
@ -331,6 +352,10 @@ export default class ComponentDataCollection extends Component {
}
}
function getLength(items: DataVariableProps[] | object) {
return isArray(items) ? items.length : Object.keys(items).length;
}
function setCollectionStateMapAndPropagate(cmp: Component, collectionsStateMap: DataCollectionStateMap) {
cmp.setSymbolOverride(['locked', 'layerable']);
cmp.syncComponentsCollectionState();
@ -366,34 +391,28 @@ function validateCollectionDef(dataResolver: DataCollectionProps, em: EditorMode
return true;
}
function getDataSourceItems(dataSource: DataCollectionDataSource, em: EditorModel) {
let items: DataVariableProps[] = [];
function getDataSourceItems(
dataSource: DataCollectionDataSource,
em: EditorModel,
): DataVariableProps[] | DataVariableMap {
switch (true) {
case isArray(dataSource):
items = dataSource;
break;
case isObject(dataSource) && dataSource instanceof DataSource: {
const id = dataSource.get('id')!;
items = listDataSourceVariables(id, em);
break;
return listDataSourceVariables(id, em);
}
case isDataVariable(dataSource): {
const path = dataSource.path;
if (!path) break;
if (!path) return [];
const isDataSourceId = path.split('.').length === 1;
if (isDataSourceId) {
items = listDataSourceVariables(path, em);
return listDataSourceVariables(path, em);
} else {
items = em.DataSources.getValue(path, []);
return em.DataSources.getValue(path, []);
}
break;
}
default:
break;
return [];
}
return items;
}
function listDataSourceVariables(dataSource_id: string, em: EditorModel): DataVariableProps[] {

2
packages/core/src/data_sources/model/data_collection/types.ts

@ -8,6 +8,7 @@ export enum DataCollectionStateType {
currentIndex = 'currentIndex',
startIndex = 'startIndex',
currentItem = 'currentItem',
currentKey = 'currentKey',
endIndex = 'endIndex',
collectionId = 'collectionId',
totalItems = 'totalItems',
@ -18,6 +19,7 @@ export interface DataCollectionState {
[DataCollectionStateType.currentIndex]: number;
[DataCollectionStateType.startIndex]: number;
[DataCollectionStateType.currentItem]: DataVariableProps;
[DataCollectionStateType.currentKey]: string | number;
[DataCollectionStateType.endIndex]: number;
[DataCollectionStateType.collectionId]: string;
[DataCollectionStateType.totalItems]: number;

11
packages/core/src/data_sources/view/ComponentDataVariableView.ts

@ -20,7 +20,16 @@ export default class ComponentDataVariableView extends ComponentView<ComponentDa
}
postRender() {
this.el.innerHTML = this.model.getDataValue();
const model = this.model;
const dataResolver = model.getDataResolver();
const asPlainText = !!dataResolver.asPlainText;
if (asPlainText) {
this.el.textContent = model.getDataValue();
} else {
this.el.innerHTML = model.getDataValue();
}
super.postRender();
}
}

2
packages/core/src/dom_components/model/ComponentDataResolverWatchers.ts

@ -37,7 +37,7 @@ export class ComponentDataResolverWatchers {
}
addProps(props: ObjectAny, options: DynamicWatchersOptions = {}) {
const excludedFromEvaluation = ['components'];
const excludedFromEvaluation = ['components', 'dataResolver'];
const evaluatedProps = Object.fromEntries(
Object.entries(props).map(([key, value]) =>

53
packages/core/test/specs/data_sources/model/ComponentDataVariable.ts

@ -3,6 +3,7 @@ import ComponentWrapper from '../../../../src/dom_components/model/ComponentWrap
import { DataVariableType } from '../../../../src/data_sources/model/DataVariable';
import { setupTestEditor } from '../../../common';
import EditorModel from '../../../../src/editor/model/Editor';
import ComponentDataVariable from '../../../../src/data_sources/model/ComponentDataVariable';
describe('ComponentDataVariable', () => {
let em: EditorModel;
@ -282,4 +283,56 @@ describe('ComponentDataVariable', () => {
expect(updatedStyle).toHaveProperty('color', 'blue');
expect(cmp.getEl()?.innerHTML).toContain('Hello World UP');
});
test("fixes: ComponentDataVariable dataResolver type 'data-variable' issue", () => {
const dataSource = {
id: 'ds1',
records: [{ id: 'id1', name: 'Name1' }],
};
dsm.add(dataSource);
const dataResolver = { type: DataVariableType, defaultValue: 'default', path: 'ds1.id1.name' };
const cmp = cmpRoot.append({
type: DataVariableType,
dataResolver,
})[0] as ComponentDataVariable;
expect(cmp.getDataResolver()).toBe(dataResolver);
expect(cmp.getEl()?.innerHTML).toContain('Name1');
expect(cmp.getInnerHTML()).toContain('Name1');
});
test('renders content as plain text or HTML based on asPlainText option', () => {
const htmlContent = '<p>Hello <strong>World</strong>!</p>';
const plainTextContent = '&lt;p&gt;Hello &lt;strong&gt;World&lt;/strong&gt;!&lt;/p&gt;';
const dataSource = {
id: 'dsHtmlTest',
records: [{ id: 'r1', content: htmlContent }],
};
dsm.add(dataSource);
// Scenario 1: asPlainText is true
const cmpPlainText = cmpRoot.append({
type: DataVariableType,
dataResolver: { path: 'dsHtmlTest.r1.content', asPlainText: true },
})[0] as ComponentDataVariable;
expect(cmpPlainText.getEl()?.innerHTML).toBe(plainTextContent);
expect(cmpPlainText.getEl()?.textContent).toBe(htmlContent);
// Scenario 2: asPlainText is false
const cmpHtml = cmpRoot.append({
type: DataVariableType,
dataResolver: { path: 'dsHtmlTest.r1.content', asPlainText: false },
})[0] as ComponentDataVariable;
expect(cmpHtml.getEl()?.innerHTML).toBe(htmlContent);
expect(cmpHtml.getEl()?.textContent).toBe('Hello World!');
// Scenario 3: asPlainText is omitted (should default to HTML rendering)
const cmpDefaultHtml = cmpRoot.append({
type: DataVariableType,
dataResolver: { path: 'dsHtmlTest.r1.content' },
})[0] as ComponentDataVariable;
expect(cmpDefaultHtml.getEl()?.innerHTML).toBe(htmlContent);
expect(cmpDefaultHtml.getEl()?.textContent).toBe('Hello World!');
});
});

18
packages/core/test/specs/data_sources/model/conditional_variables/ComponentDataCondition.ts

@ -264,6 +264,24 @@ describe('ComponentDataCondition', () => {
expect(ifFalseEl.style.display).toBe('');
expect(ifFalseEl.textContent).toContain(ifFalseText);
});
test("fixes: ComponentDatacondition dataResolver type 'data-variable' issue", () => {
const dataResolver = {
type: DataConditionType,
condition: {
left: 1,
operator: NumberOperation.greaterThan,
right: 0,
},
};
const cmp = cmpRoot.append({
type: DataConditionType,
dataResolver,
components: [ifTrueComponentDef, ifFalseComponentDef],
})[0] as ComponentDataCondition;
expect(cmp.getDataResolver()).toBe(dataResolver);
});
});
function changeDataSourceValue(dsm: DataSourceManager, newValue: string | number) {

173
packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollection.ts

@ -21,17 +21,18 @@ describe('Collection component', () => {
let wrapper: Component;
let firstRecord: DataRecord;
let secondRecord: DataRecord;
const records = [
{ id: 'user1', user: 'user1', firstName: 'Name1', age: '12' },
{ id: 'user2', user: 'user2', firstName: 'Name2', age: '14' },
{ id: 'user3', user: 'user3', firstName: 'Name3', age: '16' },
];
beforeEach(() => {
({ em, editor, dsm } = setupTestEditor());
wrapper = em.getWrapper()!;
dataSource = dsm.add({
id: 'my_data_source_id',
records: [
{ id: 'user1', user: 'user1', firstName: 'Name1', age: '12' },
{ id: 'user2', user: 'user2', firstName: 'Name2', age: '14' },
{ id: 'user3', user: 'user3', firstName: 'Name3', age: '16' },
],
records,
});
firstRecord = dataSource.getRecord('user1')!;
@ -917,80 +918,126 @@ describe('Collection component', () => {
});
});
describe('Diffirent Collection variable types', () => {
const stateVariableTests = [
{ variableType: DataCollectionStateType.currentIndex, expectedValues: [0, 1, 2] },
{ variableType: DataCollectionStateType.startIndex, expectedValues: [0, 0, 0] },
{ variableType: DataCollectionStateType.endIndex, expectedValues: [2, 2, 2] },
{
variableType: DataCollectionStateType.collectionId,
describe('State Variable Comprehensive Tests', () => {
const stateVariableTests = {
[DataCollectionStateType.currentIndex]: {
expectedValues: [0, 1, 2],
expectedObjectPathValue: [0, 1, 2, 3],
},
[DataCollectionStateType.startIndex]: {
expectedValues: [0, 0, 0],
expectedObjectPathValue: [0, 0, 0, 0],
},
[DataCollectionStateType.endIndex]: {
expectedValues: [2, 2, 2],
expectedObjectPathValue: [3, 3, 3, 3],
},
[DataCollectionStateType.currentKey]: {
expectedValues: [0, 1, 2],
expectedObjectPathValue: ['id', 'user', 'firstName', 'age'],
},
[DataCollectionStateType.currentItem]: {
expectedValues: null,
expectedObjectPathValue: ['user1', 'user1', 'Name1', '12'],
},
[DataCollectionStateType.collectionId]: {
expectedValues: ['my_collection', 'my_collection', 'my_collection'],
expectedObjectPathValue: ['my_collection', 'my_collection', 'my_collection', 'my_collection'],
},
{ variableType: DataCollectionStateType.totalItems, expectedValues: [3, 3, 3] },
{ variableType: DataCollectionStateType.remainingItems, expectedValues: [2, 1, 0] },
];
[DataCollectionStateType.totalItems]: {
expectedValues: [3, 3, 3],
expectedObjectPathValue: [4, 4, 4, 4],
},
[DataCollectionStateType.remainingItems]: {
expectedValues: [2, 1, 0],
expectedObjectPathValue: [3, 2, 1, 0],
},
};
stateVariableTests.forEach(({ variableType, expectedValues }) => {
test(`Variable type: ${variableType}`, () => {
const cmpDef = {
type: DataCollectionType,
const createCollectionCmpDef = (variableType: string, collectionId: string, dataSourcePath: string) => {
return {
type: DataCollectionType,
components: {
type: DataCollectionItemType,
components: {
type: DataCollectionItemType,
components: {
type: 'default',
name: {
type: 'default',
name: {
type: DataVariableType,
variableType: variableType,
collectionId: collectionId,
},
attributes: {
custom_attribute: {
type: DataVariableType,
variableType: variableType,
collectionId: 'my_collection',
collectionId: collectionId,
},
attributes: {
custom_attribute: {
},
traits: [
{
name: 'attribute_trait',
value: {
type: DataVariableType,
variableType: variableType,
collectionId: 'my_collection',
collectionId: collectionId,
},
},
traits: [
{
name: 'attribute_trait',
value: {
type: DataVariableType,
variableType: variableType,
collectionId: 'my_collection',
},
},
{
name: 'property_trait',
changeProp: true,
value: {
type: DataVariableType,
variableType: variableType,
collectionId: 'my_collection',
},
{
name: 'property_trait',
changeProp: true,
value: {
type: DataVariableType,
variableType: variableType,
collectionId: collectionId,
},
],
},
},
],
},
dataResolver: {
collectionId: 'my_collection',
dataSource: {
type: DataVariableType,
path: 'my_data_source_id',
},
},
dataResolver: {
collectionId: collectionId,
dataSource: {
type: DataVariableType,
path: dataSourcePath,
},
} as ComponentDataCollectionProps;
},
};
};
const performStateVariableAssertions = (
cmp: ComponentDataCollection,
expectedAssertValues: (string | number)[] | null,
) => {
if (!expectedAssertValues) return;
const children = cmp.components();
children.each((child, index) => {
const content = child.components().at(0);
expect(content.get('name')).toBe(expectedAssertValues[index]);
expect(content.get('property_trait')).toBe(expectedAssertValues[index]);
expect(content.getAttributes()['custom_attribute']).toBe(expectedAssertValues[index]);
expect(content.getAttributes()['attribute_trait']).toBe(expectedAssertValues[index]);
});
};
Object.entries(stateVariableTests).forEach(([variableType, { expectedValues, expectedObjectPathValue }]) => {
test(`Variable type: ${variableType} - Standard Path`, () => {
const cmpDef = createCollectionCmpDef(
variableType,
'my_collection',
'my_data_source_id',
) as ComponentDataCollectionProps;
const cmp = wrapper.components(cmpDef)[0] as unknown as ComponentDataCollection;
performStateVariableAssertions(cmp, expectedValues);
});
expect(cmp.getItemsCount()).toBe(3);
const children = cmp.components();
children.each((child, index) => {
const content = child.components().at(0);
expect(content.get('name')).toBe(expectedValues[index]);
expect(content.get('property_trait')).toBe(expectedValues[index]);
expect(content.getAttributes()['custom_attribute']).toBe(expectedValues[index]);
expect(content.getAttributes()['attribute_trait']).toBe(expectedValues[index]);
});
test(`Variable type: ${variableType} - Object Path (my_data_source_id.user1)`, () => {
const cmpDef = createCollectionCmpDef(
variableType,
'my_collection',
'my_data_source_id.user1',
) as ComponentDataCollectionProps;
const cmp = wrapper.components(cmpDef)[0] as unknown as ComponentDataCollection;
performStateVariableAssertions(cmp, expectedObjectPathValue);
});
});
});

Loading…
Cancel
Save