diff --git a/packages/core/src/data_sources/model/ComponentWithDataResolver.ts b/packages/core/src/data_sources/model/ComponentWithDataResolver.ts index 3eb144e41..89f720726 100644 --- a/packages/core/src/data_sources/model/ComponentWithDataResolver.ts +++ b/packages/core/src/data_sources/model/ComponentWithDataResolver.ts @@ -41,11 +41,11 @@ export abstract class ComponentWithDataResolver ext options: ComponentOptions & { collectionsStateMap: DataCollectionStateMap }, ): DataResolver; - getDataResolver() { + getDataResolver(): T { return this.get('dataResolver'); } - setDataResolver(props: any) { + setDataResolver(props: T) { return this.set('dataResolver', props); } diff --git a/packages/core/src/data_sources/model/DataVariable.ts b/packages/core/src/data_sources/model/DataVariable.ts index 3a60e4084..c7f195fcc 100644 --- a/packages/core/src/data_sources/model/DataVariable.ts +++ b/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 { path: '', collectionId: undefined, variableType: undefined, + asPlainText: undefined, }; } diff --git a/packages/core/src/data_sources/model/data_collection/ComponentDataCollection.ts b/packages/core/src/data_sources/model/data_collection/ComponentDataCollection.ts index 0c06a5372..0435b76f2 100644 --- a/packages/core/src/data_sources/model/data_collection/ComponentDataCollection.ts +++ b/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; + 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[] { diff --git a/packages/core/src/data_sources/model/data_collection/types.ts b/packages/core/src/data_sources/model/data_collection/types.ts index 7bbd91f9f..ea1bf324d 100644 --- a/packages/core/src/data_sources/model/data_collection/types.ts +++ b/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; diff --git a/packages/core/src/data_sources/view/ComponentDataVariableView.ts b/packages/core/src/data_sources/view/ComponentDataVariableView.ts index 5a940abd1..511746d4f 100644 --- a/packages/core/src/data_sources/view/ComponentDataVariableView.ts +++ b/packages/core/src/data_sources/view/ComponentDataVariableView.ts @@ -20,7 +20,16 @@ export default class ComponentDataVariableView extends ComponentView diff --git a/packages/core/test/specs/data_sources/model/ComponentDataVariable.ts b/packages/core/test/specs/data_sources/model/ComponentDataVariable.ts index 8844ad155..64b0cb543 100644 --- a/packages/core/test/specs/data_sources/model/ComponentDataVariable.ts +++ b/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 = '

Hello World!

'; + const plainTextContent = '<p>Hello <strong>World</strong>!</p>'; + 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!'); + }); }); diff --git a/packages/core/test/specs/data_sources/model/conditional_variables/ComponentDataCondition.ts b/packages/core/test/specs/data_sources/model/conditional_variables/ComponentDataCondition.ts index f3b1987b4..dc527d437 100644 --- a/packages/core/test/specs/data_sources/model/conditional_variables/ComponentDataCondition.ts +++ b/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) { diff --git a/packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollection.ts b/packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollection.ts index ffc214d74..7fd5f4c37 100644 --- a/packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollection.ts +++ b/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); }); }); });