Skip to content

Commit

Permalink
fix: Dataset should load with a delay
Browse files Browse the repository at this point in the history
  • Loading branch information
BetimBeja committed Feb 27, 2023
1 parent 188aec9 commit cee23dd
Show file tree
Hide file tree
Showing 13 changed files with 218 additions and 29 deletions.
11 changes: 11 additions & 0 deletions __sample-components__/GridControl/generated/ManifestTypes.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
*This is auto generated from the ControlManifest.Input.xml file
*/

// Define IInputs and IOutputs Type. They should match with ControlManifest.
export interface IInputs {
records: ComponentFramework.PropertyTypes.DataSet;
}
export interface IOutputs {
FilteredRecordCount?: number;
}
57 changes: 57 additions & 0 deletions __sample-components__/GridControl/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { IInputs, IOutputs } from './generated/ManifestTypes';

export class GridControl implements ComponentFramework.StandardControl<IInputs, IOutputs> {
notifyOutputChanged: () => void;
container: HTMLDivElement;
filteredRecordCount?: number;

/**
* Used to initialize the control instance. Controls can kick off remote server calls and other initialization actions here.
* Data-set values are not initialized here, use updateView.
* @param context The entire property bag available to control via Context Object; It contains values as set up by the customizer mapped to property names defined in the manifest, as well as utility functions.
* @param notifyOutputChanged A callback method to alert the framework that the control has new outputs ready to be retrieved asynchronously.
* @param state A piece of data that persists in one session for a single user. Can be set at any point in a controls life cycle by calling 'setControlState' in the Mode interface.
* @param container If a control is marked control-type='standard', it will receive an empty div element within which it can render its content.
*/
public init(
context: ComponentFramework.Context<IInputs>,
notifyOutputChanged: () => void,
state: ComponentFramework.Dictionary,
container: HTMLDivElement,
): void {
this.notifyOutputChanged = notifyOutputChanged;
this.container = container;
}

/**
* Called when any value in the property bag has changed. This includes field values, data-sets, global values such as container height and width, offline status, control metadata values such as label, visible, etc.
* @param context The entire property bag available to control via Context Object; It contains values as set up by the customizer mapped to names defined in the manifest, as well as utility functions
*/
public updateView(context: ComponentFramework.Context<IInputs>): void {
const dataset = context.parameters.records;
const datasetChanged = context.updatedProperties.indexOf('dataset') > -1;
if (datasetChanged) {
if (this.filteredRecordCount !== dataset.sortedRecordIds.length) {
this.filteredRecordCount = dataset.sortedRecordIds.length;
this.notifyOutputChanged();
}
}
this.container.innerHTML = `${this.filteredRecordCount}`;
}

/**
* It is called by the framework prior to a control receiving new data.
* @returns an object based on nomenclature defined in manifest, expecting object[s] for property marked as “bound” or “output”
*/
public getOutputs(): IOutputs {
return {
FilteredRecordCount: this.filteredRecordCount,
} as IOutputs;
}

/**
* Called when the control is to be removed from the DOM tree. Controls should use this call for cleanup.
* i.e. cancelling any pending remote calls, removing listeners, etc.
*/
destroy(): void {}
}
43 changes: 43 additions & 0 deletions __tests__/Components/GridControl.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
Copyright (c) 2022 Betim Beja and Shko Online LLC
Licensed under the MIT license.
*/

import { it, expect, describe, beforeEach } from '@jest/globals';
import { ComponentFrameworkMockGenerator, DataSetMock } from '../../src';
import { GridControl } from '../../__sample-components__/GridControl';
import { IInputs, IOutputs } from '../../__sample-components__/GridControl/generated/ManifestTypes';

describe('GridControl', () => {
let mockGenerator: ComponentFrameworkMockGenerator<IInputs, IOutputs>;
beforeEach(() => {
const container = document.createElement('div');
mockGenerator = new ComponentFrameworkMockGenerator(
GridControl,
{
records: DataSetMock,
},
container,
);
mockGenerator.context._parameters.records._InitItems([
{
name: 'Betim',
surname: 'Beja',
},
]);
document.body.appendChild(container);
});

afterEach(() => {
document.body.innerHTML = null;
});

it('Should render count', (done) => {
mockGenerator.context._parameters.records._onLoaded.callsFake(() => {
expect(mockGenerator.container).toMatchSnapshot();
done();
});
mockGenerator.ExecuteInit();
mockGenerator.ExecuteUpdateView();
});
});
7 changes: 7 additions & 0 deletions __tests__/Components/__snapshots__/GridControl.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`GridControl Should render count 1`] = `
<div>
1
</div>
`;
3 changes: 2 additions & 1 deletion __tests__/DataSetMock.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ describe('DataSetMock', () => {
});

dataset = new DataSetMock('dataset', db);
dataset._loading = false;
});

describe('with id', () => {
Expand All @@ -79,7 +80,7 @@ describe('DataSetMock', () => {
expect(dataset.getSelectedRecordIds()).toEqual([]);
});

it('sortedRecordIds should contain the right data', () => {
it('sortedRecordIds should contain the right data', () => {
expect(dataset.sortedRecordIds).toEqual(['1', '2']);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@ import { mockGetEntityMetadata } from './mockGetEntityMetadata';
import { mockSetControlState } from './mockSetControlState';
import { mockSetControlResource } from './mockSetControlResource';
import { mockRefreshParameters } from './mockRefreshParameters';
import { mockNotifyOutputChanged } from './mockNotifyOutputChanged';

export class ComponentFrameworkMockGeneratorReact<
TInputs extends ShkoOnline.PropertyTypes<TInputs>,
TOutputs extends ShkoOnline.KnownTypes<TOutputs>,
> implements MockGenerator<TInputs, TOutputs>
{
RefreshParameters: SinonStub<[], void>;
RefreshDatasets: SinonStub<[], void>;
context: ContextMock<TInputs>;
control: SinonSpiedInstance<ComponentFramework.ReactControl<TInputs, TOutputs>>;
notifyOutputChanged: SinonStub<[], void>;
Expand All @@ -47,6 +47,7 @@ export class ComponentFrameworkMockGeneratorReact<
this.onOutputChanged = stub();
this.RefreshParameters = stub();
mockRefreshParameters(this);
this.RefreshDatasets = stub();
this.SetControlResource = stub();
mockSetControlResource(this);
mockSetControlState(this);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,21 @@ import { mockRefreshParameters } from './mockRefreshParameters';
import { mockNotifyOutputChanged } from './mockNotifyOutputChanged';
import { ContextMock } from '../ComponentFramework-Mock';
import { showBanner } from '../utils';
import { MockGenerator } from './MockGenerator';
import { mockRefreshDatasets } from './mockRefreshDatasets';

export class ComponentFrameworkMockGenerator<
TInputs extends ShkoOnline.PropertyTypes<TInputs>,
TOutputs extends ShkoOnline.KnownTypes<TOutputs>,
> {
> implements MockGenerator<TInputs, TOutputs>
{
RefreshParameters: SinonStub<[], void>;
RefreshDatasets: SinonStub<[], void>;
container: HTMLDivElement;
context: ContextMock<TInputs>;
control: SinonSpiedInstance<ComponentFramework.StandardControl<TInputs, TOutputs>>;
notifyOutputChanged: SinonStub<[], void>;
onOutputChanged: SinonStub<[],void>;
onOutputChanged: SinonStub<[], void>;
state: ComponentFramework.Dictionary;
SetControlResource: SinonStub<[resource: string], void>;
metadata: MetadataDB;
Expand Down Expand Up @@ -60,6 +64,8 @@ export class ComponentFrameworkMockGenerator<
this.onOutputChanged = stub();
this.RefreshParameters = stub();
mockRefreshParameters(this);
this.RefreshDatasets = stub();
mockRefreshDatasets(this, this.ExecuteUpdateView.bind(this));
this.SetControlResource = stub();
mockSetControlResource(this);
mockSetControlState(this);
Expand All @@ -74,5 +80,6 @@ export class ComponentFrameworkMockGenerator<
ExecuteUpdateView() {
this.RefreshParameters();
this.control.updateView(this.context);
this.RefreshDatasets();
}
}
7 changes: 7 additions & 0 deletions src/ComponentFramework-Mock-Generator/MockGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ export interface MockGenerator<
*/
RefreshParameters: SinonStub<[], void>;

/**
* Used to refresh the dataset parameters that might be still loading
*
* This is called internally by the framework at each UpdateView.
*/
RefreshDatasets: SinonStub<[], void>;

/**
* Mocked context that will be passed to the component in the init or update calls.
*/
Expand Down
27 changes: 15 additions & 12 deletions src/ComponentFramework-Mock-Generator/ReactResizeObserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type { ShkoOnline } from '../ShkoOnline';
import { createElement, Fragment, useEffect, useRef, useState } from 'react';
import { ComponentFrameworkMockGeneratorReact } from './ComponentFramework-Mock-Generator-React';
import { mockNotifyOutputChanged } from './mockNotifyOutputChanged';
import { mockRefreshDatasets } from './mockRefreshDatasets';

export interface ReactResizeObserverProps<
TInputs extends ShkoOnline.PropertyTypes<TInputs>,
Expand All @@ -34,18 +35,24 @@ export const ReactResizeObserver = <
componentFrameworkMockGeneratorReact,
componentFrameworkMockGeneratorReact.control.getOutputs?.bind(componentFrameworkMockGeneratorReact.control),
() => {
Object.getOwnPropertyNames<ShkoOnline.PropertyTypes<TInputs>>(
componentFrameworkMockGeneratorReact.context.parameters,
).forEach((propertyName) => {
componentFrameworkMockGeneratorReact.context._parameters[propertyName]._Refresh();
});
componentFrameworkMockGeneratorReact.RefreshParameters();
setComponent(
componentFrameworkMockGeneratorReact.control.updateView(
componentFrameworkMockGeneratorReact.context,
),
);
});


componentFrameworkMockGeneratorReact.RefreshDatasets();

},
);

mockRefreshDatasets(componentFrameworkMockGeneratorReact, () => {
setComponent(
componentFrameworkMockGeneratorReact.control.updateView(componentFrameworkMockGeneratorReact.context),
);
});

componentFrameworkMockGeneratorReact.context.mode.trackContainerResize.callsFake((value) => {
if (!containerRef.current) {
console.error('Container Ref is null');
Expand All @@ -55,11 +62,7 @@ export const ReactResizeObserver = <
const size = entries[0];
componentFrameworkMockGeneratorReact.context.mode.allocatedHeight = size.contentRect.height;
componentFrameworkMockGeneratorReact.context.mode.allocatedWidth = size.contentRect.width;
Object.getOwnPropertyNames<ShkoOnline.PropertyTypes<TInputs>>(
componentFrameworkMockGeneratorReact.context.parameters,
).forEach((propertyName) => {
componentFrameworkMockGeneratorReact.context._parameters[propertyName]._Refresh();
});
componentFrameworkMockGeneratorReact.RefreshParameters();
setComponent(
componentFrameworkMockGeneratorReact.control.updateView(
componentFrameworkMockGeneratorReact.context,
Expand Down
1 change: 1 addition & 0 deletions src/ComponentFramework-Mock-Generator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export { ComponentFrameworkMockGenerator } from './ComponentFramework-Mock-Gener
export { MetadataDB } from './Metadata.db';
export type { MockGenerator } from './MockGenerator';
export { mockGetEntityMetadata } from './mockGetEntityMetadata';
export { mockRefreshDatasets } from './mockRefreshDatasets';
export { mockRefreshParameters } from './mockRefreshParameters';
export { mockSetControlResource } from './mockSetControlResource';
export { mockSetControlState } from './mockSetControlState';
Expand Down
40 changes: 40 additions & 0 deletions src/ComponentFramework-Mock-Generator/mockRefreshDatasets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
Copyright (c) 2022 Betim Beja and Shko Online LLC
Licensed under the MIT license.
*/

/// <reference path="../global.d.ts" />

import type { PropertyToMock } from '../ComponentFramework-Mock';
import type { MockGenerator } from './MockGenerator';
import type { ShkoOnline } from '../ShkoOnline';

import { stub } from 'sinon';
import { DataSetMock } from '../ComponentFramework-Mock';

export const mockRefreshDatasets = <
TInputs extends ShkoOnline.PropertyTypes<TInputs>,
TOutputs extends ShkoOnline.KnownTypes<TOutputs>,
>(
mockGenerator: MockGenerator<TInputs, TOutputs>,
callback: () => void,
) => {
mockGenerator.RefreshDatasets = stub();
mockGenerator.RefreshDatasets.callsFake(() => {
Object.getOwnPropertyNames<PropertyToMock<TInputs>>(mockGenerator.context._parameters).forEach(
(propertyName) => {
const mock = mockGenerator.context._parameters[propertyName];
if (!(mock instanceof DataSetMock) || !mock._loading) {
return;
}
setTimeout(() => {
mock._loading = !mock._loading;
mockGenerator.RefreshParameters();
mockGenerator.context.updatedProperties = [propertyName as string, 'dataset'];
callback();
mock._onLoaded();
}, mock._delay);
},
);
});
};
Loading

0 comments on commit cee23dd

Please sign in to comment.