diff --git a/__tests__/DataSetMock.test.ts b/__tests__/DataSetMock.test.ts index 6219ab3..fe93e62 100644 --- a/__tests__/DataSetMock.test.ts +++ b/__tests__/DataSetMock.test.ts @@ -150,6 +150,11 @@ describe('DataSetMock', () => { dataset.clearSelectedRecordIds(); expect(dataset.getSelectedRecordIds()).toEqual([]); }); + + it('new record should work', () => { + var newRecord = dataset.newRecord(); + expect(newRecord.getRecordId()).toEqual('Guid.NewGuid?'); + }); }); describe('without id', () => { diff --git a/__tests__/EntityRecordMock.test.ts b/__tests__/EntityRecordMock.test.ts index b6c2dfc..1ac843b 100644 --- a/__tests__/EntityRecordMock.test.ts +++ b/__tests__/EntityRecordMock.test.ts @@ -6,18 +6,21 @@ import type { ShkoOnline } from '../src/ShkoOnline'; import { it, expect, describe, beforeEach } from '@jest/globals'; import { EntityRecordMock, MetadataDB } from '../src'; +import { SinonStub, stub } from 'sinon'; describe('EntityRecordMock', () => { let db: MetadataDB; let entityRecord: EntityRecordMock; let LogicalName: string; let boundRow: string; + let updateView: SinonStub<[],void>; beforeEach(() => { + db = new MetadataDB(); LogicalName = '!!test'; boundRow = 'TheRowId'; - + updateView = stub(); db.initMetadata([ { LogicalName, @@ -46,7 +49,7 @@ describe('EntityRecordMock', () => { value: [{ id: boundRow, name: 'Betim Beja' }], }); - entityRecord = new EntityRecordMock(db, LogicalName, boundRow); + entityRecord = new EntityRecordMock(db, LogicalName, boundRow, updateView); }); it('getNamedReference should return an entity reference', () => { @@ -72,4 +75,9 @@ describe('EntityRecordMock', () => { it('getRecordId should return the bound row id', () => { expect(entityRecord.getRecordId()).toEqual(boundRow); }); + + it('setValue should call updateView', async () => { + await entityRecord.setValue('name', 'Asllan Makaj'); + expect(updateView.calledOnce).toEqual(true); + }); }); diff --git a/src/ComponentFramework-Mock-Generator/ComponentFramework-Mock-Generator-React.ts b/src/ComponentFramework-Mock-Generator/ComponentFramework-Mock-Generator-React.ts index 0e50494..6e62ee1 100644 --- a/src/ComponentFramework-Mock-Generator/ComponentFramework-Mock-Generator-React.ts +++ b/src/ComponentFramework-Mock-Generator/ComponentFramework-Mock-Generator-React.ts @@ -55,7 +55,6 @@ export class ComponentFrameworkMockGeneratorReact< mockSetControlState(this); } - ExecuteInit() { this.RefreshParameters(); const state = this.state === undefined ? this.state : { ...this.state }; diff --git a/src/ComponentFramework-Mock-Generator/ComponentFramework-Mock-Generator.ts b/src/ComponentFramework-Mock-Generator/ComponentFramework-Mock-Generator.ts index 924fec4..6dcbece 100644 --- a/src/ComponentFramework-Mock-Generator/ComponentFramework-Mock-Generator.ts +++ b/src/ComponentFramework-Mock-Generator/ComponentFramework-Mock-Generator.ts @@ -14,7 +14,7 @@ import { mockSetControlState } from './mockSetControlState'; import { mockSetControlResource } from './mockSetControlResource'; import { mockRefreshParameters } from './mockRefreshParameters'; import { mockNotifyOutputChanged } from './mockNotifyOutputChanged'; -import { ContextMock } from '../ComponentFramework-Mock'; +import { ContextMock, DataSetMock } from '../ComponentFramework-Mock'; import { showBanner } from '../utils'; import { MockGenerator } from './MockGenerator'; import { mockRefreshDatasets } from './mockRefreshDatasets'; @@ -59,6 +59,13 @@ export class ComponentFrameworkMockGenerator< else this.resizeObserver.unobserve(this.container); }); + Object.getOwnPropertyNames(this.context._parameters).forEach((p) => { + var parameter = this.context._parameters[p]; + if (parameter instanceof DataSetMock) { + parameter._updateView = this.ExecuteUpdateView.bind(this); + } + }); + mockGetEntityMetadata(this); this.notifyOutputChanged = stub(); mockNotifyOutputChanged(this, this.control.getOutputs?.bind(this.control), this.ExecuteUpdateView.bind(this)); diff --git a/src/ComponentFramework-Mock-Generator/ReactResizeObserver.ts b/src/ComponentFramework-Mock-Generator/ReactResizeObserver.ts index 72432ae..c9c2c00 100644 --- a/src/ComponentFramework-Mock-Generator/ReactResizeObserver.ts +++ b/src/ComponentFramework-Mock-Generator/ReactResizeObserver.ts @@ -6,10 +6,11 @@ import type { JSXElementConstructor, ReactElement } from 'react'; import type { ShkoOnline } from '../ShkoOnline'; -import { createElement, Fragment, useEffect, useRef, useState } from 'react'; +import { createElement, Fragment, useCallback, useEffect, useRef, useState } from 'react'; import { ComponentFrameworkMockGeneratorReact } from './ComponentFramework-Mock-Generator-React'; import { mockNotifyOutputChanged } from './mockNotifyOutputChanged'; import { mockRefreshDatasets } from './mockRefreshDatasets'; +import { DataSetMock } from '../ComponentFramework-Mock/PropertyTypes'; export interface ReactResizeObserverProps< TInputs extends ShkoOnline.PropertyTypes, @@ -30,18 +31,26 @@ export const ReactResizeObserver = < const [Component, setComponent] = useState>>( createElement(Fragment), ); + const updateView = useCallback(() => { + setComponent( + componentFrameworkMockGeneratorReact.control.updateView(componentFrameworkMockGeneratorReact.context), + ); + }, [setComponent, componentFrameworkMockGeneratorReact]); useEffect(() => { + Object.getOwnPropertyNames(componentFrameworkMockGeneratorReact.context._parameters).forEach((p) => { + var parameter = componentFrameworkMockGeneratorReact.context._parameters[p]; + if (parameter instanceof DataSetMock) { + parameter._updateView = updateView; + } + }); + mockNotifyOutputChanged( componentFrameworkMockGeneratorReact, componentFrameworkMockGeneratorReact.control.getOutputs?.bind(componentFrameworkMockGeneratorReact.control), () => { componentFrameworkMockGeneratorReact.RefreshParameters(); - setComponent( - componentFrameworkMockGeneratorReact.control.updateView( - componentFrameworkMockGeneratorReact.context, - ), - ); - componentFrameworkMockGeneratorReact.RefreshDatasets(); + updateView(); + componentFrameworkMockGeneratorReact.RefreshDatasets(); }, ); @@ -50,39 +59,29 @@ export const ReactResizeObserver = < componentFrameworkMockGeneratorReact.context.mode.allocatedHeight = size.contentRect.height; componentFrameworkMockGeneratorReact.context.mode.allocatedWidth = size.contentRect.width; componentFrameworkMockGeneratorReact.RefreshParameters(); - setComponent( - componentFrameworkMockGeneratorReact.control.updateView( - componentFrameworkMockGeneratorReact.context, - ), - ); + updateView(); }); - mockRefreshDatasets(componentFrameworkMockGeneratorReact, () => { - setComponent( - componentFrameworkMockGeneratorReact.control.updateView(componentFrameworkMockGeneratorReact.context), - ); - }); + mockRefreshDatasets(componentFrameworkMockGeneratorReact, updateView); componentFrameworkMockGeneratorReact.context.mode.trackContainerResize.callsFake((value) => { if (!containerRef.current) { console.error('Container Ref is null'); return; } - + if (value) componentFrameworkMockGeneratorReact.resizeObserver.observe(containerRef.current); else componentFrameworkMockGeneratorReact.resizeObserver.unobserve(containerRef.current); }); - if(componentFrameworkMockGeneratorReact.context.mode._TrackingContainerResize && containerRef.current){ + if (componentFrameworkMockGeneratorReact.context.mode._TrackingContainerResize && containerRef.current) { componentFrameworkMockGeneratorReact.resizeObserver.observe(containerRef.current); } }, []); useEffect(() => { componentFrameworkMockGeneratorReact.RefreshParameters(); - setComponent( - componentFrameworkMockGeneratorReact.control.updateView(componentFrameworkMockGeneratorReact.context), - ); + updateView(); componentFrameworkMockGeneratorReact.RefreshDatasets(); }, [circuitBreaker]); diff --git a/src/ComponentFramework-Mock/Context.mock.ts b/src/ComponentFramework-Mock/Context.mock.ts index 55fa0c5..d7de04c 100644 --- a/src/ComponentFramework-Mock/Context.mock.ts +++ b/src/ComponentFramework-Mock/Context.mock.ts @@ -7,7 +7,7 @@ import type { SinonStub } from 'sinon'; import type { ShkoOnline } from '../ShkoOnline'; import type { MockToRaw, PropertyMap, PropertyToMock } from './PropertyTypes'; -import Sinon, { stub } from 'sinon'; +import { stub } from 'sinon'; import { ClientMock } from './Client.mock'; import { DeviceMock } from './Device.mock'; import { FactoryMock } from './Factory.mocks'; diff --git a/src/ComponentFramework-Mock/PropertyTypes/DataSet.mock.ts b/src/ComponentFramework-Mock/PropertyTypes/DataSet.mock.ts index dbfd675..e9869f4 100644 --- a/src/ComponentFramework-Mock/PropertyTypes/DataSet.mock.ts +++ b/src/ComponentFramework-Mock/PropertyTypes/DataSet.mock.ts @@ -20,6 +20,7 @@ export class DataSetMock implements ComponentFramework.PropertyTypes.DataSet { _boundColumn: string; _boundTable: string; _boundRow?: string; + _dataSetCapabilities: ComponentFramework.PropertyTypes.DataProviderCapabilities; _db: MetadataDB; _Bind: SinonStub<[boundTable: string, boundColumn: string, boundRow?: string], void>; _Refresh: SinonStub<[], void>; @@ -29,8 +30,10 @@ export class DataSetMock implements ComponentFramework.PropertyTypes.DataSet { _onLoaded: SinonStub<[], void>; _delay: number; _SelectedRecordIds: string[]; + _updateView?: () => void; addColumn?: SinonStub<[name: string, entityAlias?: string], void>; columns: Column[]; + delete: SinonStub<[ids: string[]], Promise>; error: boolean; errorMessage: string; filtering: FilteringMock; @@ -43,19 +46,30 @@ export class DataSetMock implements ComponentFramework.PropertyTypes.DataSet { sortedRecordIds: string[]; sorting: ComponentFramework.PropertyHelper.DataSetApi.SortStatus[]; clearSelectedRecordIds: SinonStub<[], void>; + getDataSetCapabilities: SinonStub<[], ComponentFramework.PropertyTypes.DataProviderCapabilities>; getSelectedRecordIds: SinonStub<[], string[]>; getTargetEntityType: SinonStub<[], string>; getTitle: SinonStub<[], string>; getViewId: SinonStub<[], string>; + newRecord: SinonStub<[], ComponentFramework.PropertyHelper.DataSetApi.EntityRecord>; openDatasetItem: SinonStub<[entityReference: ComponentFramework.EntityReference], void>; refresh: SinonStub<[], void>; setSelectedRecordIds: SinonStub<[ids: string[]], void>; constructor(propertyName: string, db: MetadataDB) { this._boundTable = `!!${propertyName}`; this._boundColumn = propertyName; + this._dataSetCapabilities = { + canCreateNewRecords: true, + canPaginate: true, + hasCellImageInfo: true, + hasRecordNavigation: true, + isEditable: true, + isFilterable: true, + isSortable: true, + }; this._db = db; this._SelectedRecordIds = []; - this._onLoaded = stub(); + this._onLoaded = stub(); this.error = false; this.errorMessage = ''; this.linking = new LinkingMock(); @@ -147,6 +161,7 @@ export class DataSetMock implements ComponentFramework.PropertyTypes.DataSet { db, this._boundTable, item[rows.entityMetadata?.PrimaryIdAttribute || 'id'], + this._updateView, ); return row; }); @@ -188,6 +203,12 @@ export class DataSetMock implements ComponentFramework.PropertyTypes.DataSet { this.clearSelectedRecordIds.callsFake(() => { this._SelectedRecordIds = []; }); + this.delete = stub(); + this.delete.callsFake((ids) => { + return new Promise((resolve) => resolve()); + }); + this.getDataSetCapabilities = stub(); + this.getDataSetCapabilities.callsFake(() => ({ ...this._dataSetCapabilities })); this.getSelectedRecordIds = stub(); this.getSelectedRecordIds.callsFake(() => [...this._SelectedRecordIds]); this.addColumn = stub(); @@ -197,6 +218,16 @@ export class DataSetMock implements ComponentFramework.PropertyTypes.DataSet { }); this.getTitle = stub(); this.getViewId = stub(); + this.newRecord = stub(); + this.newRecord.callsFake(() => { + const tableMetadata = this._db.getTableMetadata(`${this._boundTable}`); + if (!tableMetadata) { + throw new Error('Please initialize the metadata for this functionality'); + } + + const row = new EntityRecordMock(db, this._boundTable, 'Guid.NewGuid?', this._updateView); + return row; + }); this.openDatasetItem = stub(); this.refresh = stub(); this.setSelectedRecordIds = stub(); diff --git a/src/ComponentFramework-Mock/PropertyTypes/DataSetApi/EntityRecord.mock.ts b/src/ComponentFramework-Mock/PropertyTypes/DataSetApi/EntityRecord.mock.ts index 22bb142..6487406 100644 --- a/src/ComponentFramework-Mock/PropertyTypes/DataSetApi/EntityRecord.mock.ts +++ b/src/ComponentFramework-Mock/PropertyTypes/DataSetApi/EntityRecord.mock.ts @@ -23,14 +23,24 @@ export class EntityRecordMock implements ComponentFramework.PropertyHelper.DataS _db: MetadataDB; _boundRow: string; _boundTable: string; + _isDirty: boolean; + _isValid: boolean; + _updateView?: ()=>void; getFormattedValue: SinonStub<[columnName: string], string>; getRecordId: SinonStub<[], string>; getValue: SinonStub<[columnName: string], ColumnReturnValue>; getNamedReference: SinonStub<[], ComponentFramework.EntityReference>; - constructor(db: MetadataDB, etn: string, id: string) { + isDirty: SinonStub<[],boolean>; + isValid: SinonStub<[], boolean>; + save: SinonStub<[],Promise>; + setValue: SinonStub<[columnName: string, value: ColumnReturnValue],Promise>; + constructor(db: MetadataDB, etn: string, id: string, updateView?: ()=>void) { + this._updateView = updateView; this._db = db; this._boundTable = etn; this._boundRow = id; + this._isDirty = false; + this._isValid = true; this.getFormattedValue = stub(); this.getFormattedValue.callsFake((columnName) => { const { value, attributeMetadata } = this._db.GetValueAndMetadata( @@ -64,5 +74,34 @@ export class EntityRecordMock implements ComponentFramework.PropertyHelper.DataS ); return value; }); + this.isDirty = stub(); + this.isDirty.callsFake(()=>this._isDirty); + this.isValid = stub(); + this.isValid.callsFake(()=>this._isValid); + this.save = stub(); + this.save.callsFake(()=>{ + return new Promise((resolve)=>{ + setTimeout(()=>{ + this._isDirty = false; + if(this._updateView){ + this._updateView(); + } + resolve(); + },10); + }); + }); + this.setValue = stub(); + this.setValue.callsFake((columnName, value)=>{ + return new Promise((resolve)=>{ + setTimeout(()=>{ + this._db.UpdateValue(value, this._boundTable, columnName, this._boundRow); + this._isDirty = true; + if(this._updateView){ + this._updateView(); + } + resolve(); + },10); + }); + }); } } diff --git a/src/global.d.ts b/src/global.d.ts index 8a96542..eb08d61 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -5,6 +5,116 @@ /// +declare namespace ComponentFramework { + namespace PropertyTypes { + /** + * The structure of a dataset property as it would be passed to a control + */ + interface DataSet { + /** + * Delete the records from data source. + * @param ids Array of IDs to be deleted. + * @todo Overloaded by Shko Online + */ + delete: (ids: string[]) => Promise; + + /** + * The capabilities for the dataset. + * @todo Overloaded by Shko Online + */ + getDataSetCapabilities: ()=> DataProviderCapabilities; + + /** + * Initialize a local record object for control to set the value. The control needs to invoke the {@link ComponentFramework.PropertyHelper.DataSetApi.EntityRecord.save save()} method on the newly created record to persist the change. + * @todo Overloaded by Shko Online + */ + newRecord: ()=> ComponentFramework.PropertyHelper.DataSetApi.EntityRecord; + } + + /** + * Provides access to all the properties of a file. + * @todo Overloaded by Shko Online + */ + interface DataProviderCapabilities { + /** + * Whether adding new records is supported or not. + */ + canCreateNewRecords: boolean; + /** + * If the dataset records can be paged. + */ + canPaginate: boolean; + /** + * Whether image info for record columns can be retrieved. + */ + hasCellImageInfo: boolean; + /** + * Whether the dataset supports record navigation for lookup and primary fields. + */ + hasRecordNavigation: boolean; + /** + * If the data provider has edit capabilities. + */ + isEditable: boolean; + /** + * If the dataset can be filtered. + */ + isFilterable: boolean; + /** + * If the dataset can be sorted. + */ + isSortable: boolean; + } + } + namespace PropertyHelper { + namespace DataSetApi { + /** + * Base interface for dataset record result. Supports value retrival by column name. + */ + interface EntityRecord { + /** + * Whether this record is dirty. Only applicable if the dataset is editable and this record has dirty values. + * @todo Overloaded by Shko Online + */ + isDirty(): boolean; + + /** + * Whether this record is valid. Only applicable if the dataset is editable and this record has invalid values. + * @todo Overloaded by Shko Online + */ + isValid(): boolean; + + /** + * Saves the record + * @throws You can get an error saying `Invalid snapshot with id undefined` when the incorrect column name parameter was used with {@link ComponentFramework.PropertyHelper.DataSetApi.EntityRecord.setValue setValue}. Make sure to use the logical name of the column. + * @todo Overloaded by Shko Online + */ + save(): Promise; + + /** + * Set value for the column. + * @param columnName The logical name of the column. + * @param value New value for the record. + * @todo Overloaded by Shko Online + */ + setValue( + columnName: string, + value: + | string + | Date + | number + | number[] + | boolean + | ComponentFramework.EntityReference + | ComponentFramework.EntityReference[] + | ComponentFramework.LookupValue + | ComponentFramework.LookupValue[], + ): Promise; + } + } + } +} + interface ObjectConstructor { getOwnPropertyNames(o: T): (keyof T)[]; }