Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .vs/AssociationTableControl/v16/.suo
Binary file not shown.
6 changes: 6 additions & 0 deletions .vs/VSWorkspaceState.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"ExpandedNodes": [
""
],
"PreviewInSolutionExplorer": false
}
Binary file added .vs/slnx.sqlite
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest>
<control namespace="CRMVet" constructor="AssociationTableControl" version="0.0.31" display-name-key="Association Table Control" description-key="Control for Association (aka joint or N:N) tables" control-type="standard" preview-image="img/preview.png">
<control namespace="CRMVet" constructor="AssociationTableControl" version="0.0.34" display-name-key="Association Table Control" description-key="Control for Association (aka joint or N:N) tables" control-type="standard" preview-image="img/preview.png">
<!-- This property node defines configuration required for this PCF. Configuration is done as a mapping to Dataverse fields or hard-coded expressions -->
<property name="fieldValue" display-name-key="Field Logical Name" description-key="Property_Desc_Key" of-type="Multiple" usage="bound" required="true" />
<property name="selectorTable" display-name-key="Selector Table" description-key="Name for the Selector Table to show records to choose from in UI. Example: crmvet_table2" of-type="SingleLine.Text" usage="input" required="true" default-value="" />
Expand All @@ -12,6 +12,10 @@
<property name="visibilityToggle" display-name-key="Visibility Toggle" description-key="Show or Hide in UI Selector Values. Useful if due to the number of options PCF takes to much space on a form. Example: Yes/No" of-type="Enum" usage="input" required="false">
<value name="No" display-name-key="No">0</value>
<value name="Yes" display-name-key="Yes">1</value>
</property>
<property name="outputSelection" display-name-key="Output Selection" description-key="Decides whether to output the selection as a comma separated list" of-type="Enum" usage="input" required="true" default-value="No">
<value name="Yes" display-name-key="Yes" description-key="Yes">Yes</value>
<value name="No" display-name-key="No" description-key="No">No</value>
</property>
<resources>
<code path="index.ts" order="1" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,4 +90,11 @@ ul.ks-cboxtags li input[type="checkbox"] {
}
ul.ks-cboxtags li input[type="checkbox"]:focus + label {
border: 1px solid #2266E3;
}

/*Disabled*/
ul.ks-cboxtags li input[type="checkbox"]:checked:disabled + label {
border: 1px solid #c8c6c4;
background-color: #c8c6c4;
color: #ffffff;
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ export class AssociationTableControl implements ComponentFramework.StandardContr
private _itemList: detailItem[];
private _selStates: selState[];
private _showToggle = false;
private _controleDisabled = false;

private _outputSelection: boolean = false;
private _possibleValues: any[] = [];

constructor()
{
Expand All @@ -58,6 +62,10 @@ export class AssociationTableControl implements ComponentFramework.StandardContr
this._showToggle = this._context.parameters.visibilityToggle.raw == "1" ? true : false;
}

if(context.parameters.outputSelection!.raw == "Yes"){
this._outputSelection = true;
}

if (this._context.parameters.defaultFilter.raw != null) {
this._defaultFilter = this._context.parameters.defaultFilter.raw;
}
Expand All @@ -71,21 +79,53 @@ export class AssociationTableControl implements ComponentFramework.StandardContr
if (Xrm.Page.ui.getFormType() !== 1) {
await this.getRelatedRecords();
}

if((this._context.parameters.fieldValue.raw == null || this._context.parameters.fieldValue.raw == "") && this._values.length > 0){
this._notifyOutputChanged();
}
}

public async updateView(context: ComponentFramework.Context<IInputs>)
{
// Add code to update control view
this._context = context;

//Might be more in the future.
this._controleDisabled = this._context.mode.isControlDisabled;

//Check that the entityId value has been updated before refreshing the control
if (context.updatedProperties != null && context.updatedProperties.length != 0) {
if (context.updatedProperties[context.updatedProperties.length - 1] == "entityId" || context.updatedProperties[context.updatedProperties.length - 1] == "IsControlDisabled") {
if (context.updatedProperties.indexOf("entityId") > -1 || context.updatedProperties.indexOf("IsControlDisabled") > -1) {
await this.getRecords();
}
}
}

public getOutputs(): IOutputs
{
if(this._outputSelection){
this._values = this._values.filter((c, index) => {
return this._values.indexOf(c) === index;
});

var tmpArray = this._values.map(item => {
var possibleValuesItem = this._possibleValues.find( i => i[this._entityName + "id"] == item);

return possibleValuesItem[this._selectorLabel];
});

tmpArray.sort((a,b) => a.localeCompare(b))

return {
fieldValue: tmpArray.join(', ')
};
}

return {
fieldValue: ""
};
}

/**
* Called when the control is to be removed from the DOM tree. Controls should use this call for cleanup.
* i.e. canceling any pending remote calls, removing listeners, etc.
Expand All @@ -96,130 +136,143 @@ export class AssociationTableControl implements ComponentFramework.StandardContr
}

public async getRelatedRecords() {
try {
var list = [];
// @ts-ignore
var contextInfo = this._context.mode.contextInfo;
var recordId = contextInfo.entityId;
var lookupTo = this._context.parameters.lookuptoAssociatedTable.raw!.toLowerCase();
var lookupFrom = this._context.parameters.lookuptoCurrentTable.raw!.toLowerCase();
var result = await this._context.webAPI.retrieveMultipleRecords(this._context.parameters.associationTable.raw!, '?$select= _' + lookupTo + '_value&$filter=_' + lookupFrom + '_value eq ' + recordId);
for (var i = 0; i < result.entities.length; i++) {
var temp = result.entities[i]["_" + lookupTo + "_value"];
list.push(temp);
var cItem = this._itemList.find((e => e.key === temp));
var cState = this._selStates.findIndex(e => e.text === cItem?.parent);
if (cState !== -1) {
this._selStates[cState].actual++;
try {
var list = [];
// @ts-ignore
var contextInfo = this._context.mode.contextInfo;
var recordId = contextInfo.entityId;
var lookupTo = this._context.parameters.lookuptoAssociatedTable.raw!.toLowerCase();
var lookupFrom = this._context.parameters.lookuptoCurrentTable.raw!.toLowerCase();
var result = await this._context.webAPI.retrieveMultipleRecords(this._context.parameters.associationTable.raw!, '?$select= _' + lookupTo + '_value&$filter=_' + lookupFrom + '_value eq ' + recordId);
for (var i = 0; i < result.entities.length; i++) {
var temp = result.entities[i]["_" + lookupTo + "_value"];
list.push(temp);
var cItem = this._itemList.find((e => e.key === temp));
var cState = this._selStates.findIndex(e => e.text === cItem?.parent);
if (cState !== -1) {
this._selStates[cState].actual++;
}
}
}
this._values = list;
await this.getRecords();
this._values = list;
await this.getRecords();
} catch(error) {
swal.fire("getRelatedRecords", "Error:" + error.message, "error");
}
}

//Called to retrieve records to display, both on-load and on-change of lookup
public async getRecords() {
try {
this._container.innerHTML = "";
this._mainContainer.innerHTML = "";
this._unorderedList.innerHTML = "";
if (this._showToggle) {
this._togglePanel = document.createElement("div");
this._togglePanel.style.float = "right";
var toggleProps = {
visible: true,
onChangeResult: this.showHideControl.bind(this)
try {
this._container.innerHTML = "";
this._mainContainer.innerHTML = "";
this._unorderedList.innerHTML = "";
if (!this._controleDisabled && this._showToggle) {
this._togglePanel = document.createElement("div");
this._togglePanel.style.float = "right";
var toggleProps = {
visible: true,
onChangeResult: this.showHideControl.bind(this)
}

ReactDOM.render(React.createElement(ToggleComponent, toggleProps), this._togglePanel);
this._mainContainer.appendChild(this._togglePanel);
}
//Check if table name variable contains data
if (this._context.parameters.selectorTable.raw != null && this._context.parameters.selectorTable.raw != "") {
this._entityName = this._context.parameters.selectorTable.raw;
}
//Check if field name contains data
if (this._context.parameters.selectorLabel.raw != null && this._context.parameters.selectorLabel.raw != "") {
this._selectorLabel = this._context.parameters.selectorLabel.raw;
}
this._filter = "?$select=" + this._selectorLabel + "," + this._entityName + "id" + "&$orderby=" + this._selectorLabel + " asc";
//Default Filter
if (this._defaultFilter !== undefined && this._defaultFilter !== "") {
this._filter += "&$filter=" + this._defaultFilter;
}
var records = await this._context.webAPI.retrieveMultipleRecords(this._entityName, this._filter);
for (var i = 0; i < records.entities.length; i++) {
var newChkBox = document.createElement("input");
var newLabel = document.createElement("label");
var newUList = document.createElement("li");

ReactDOM.render(React.createElement(ToggleComponent, toggleProps), this._togglePanel);
this._mainContainer.appendChild(this._togglePanel);
}
//Check if table name variable contains data
if (this._context.parameters.selectorTable.raw != null && this._context.parameters.selectorTable.raw != "") {
this._entityName = this._context.parameters.selectorTable.raw;
}
//Check if field name contains data
if (this._context.parameters.selectorLabel.raw != null && this._context.parameters.selectorLabel.raw != "") {
this._selectorLabel = this._context.parameters.selectorLabel.raw;
}
this._filter = "?$select=" + this._selectorLabel + "," + this._entityName + "id" + "&$orderby=" + this._selectorLabel + " asc";
//Default Filter
if (this._defaultFilter !== undefined && this._defaultFilter !== "") {
this._filter += "&$filter=" + this._defaultFilter;
}
var records = await this._context.webAPI.retrieveMultipleRecords(this._entityName, this._filter);
for (var i = 0; i < records.entities.length; i++) {
var newChkBox = document.createElement("input");
var newLabel = document.createElement("label");
var newUList = document.createElement("li");

newChkBox.type = "checkbox";
newChkBox.id = records.entities[i][this._entityName + "id"];
newChkBox.name = records.entities[i][this._selectorLabel];
newChkBox.value = records.entities[i][this._entityName + "id"];
if (this._values != undefined) {
if (this._values.includes(newChkBox.id)) {
newChkBox.checked = true;
this._possibleValues.push(records.entities[i]);

newChkBox.type = "checkbox";
newChkBox.id = records.entities[i][this._entityName + "id"];
newChkBox.name = records.entities[i][this._selectorLabel];
newChkBox.value = records.entities[i][this._entityName + "id"];
if (this._values != undefined) {
if (this._values.includes(newChkBox.id)) {
newChkBox.checked = true;
}
}
newChkBox.addEventListener("change", this._checkBoxChanged);
newChkBox.disabled = this._controleDisabled;
newLabel.innerHTML = records.entities[i][this._selectorLabel];
newLabel.htmlFor = records.entities[i][this._entityName + "id"];
newUList.appendChild(newChkBox);
newUList.appendChild(newLabel);
if(!this._controleDisabled || newChkBox.checked){
this._unorderedList.appendChild(newUList);
}
}
newChkBox.addEventListener("change", this._checkBoxChanged);
newLabel.innerHTML = records.entities[i][this._selectorLabel];
newLabel.htmlFor = records.entities[i][this._entityName + "id"];
newUList.appendChild(newChkBox);
newUList.appendChild(newLabel);
this._unorderedList.appendChild(newUList);
}
this._mainContainer.appendChild(this._unorderedList);
this._mainContainer.appendChild(this._errorLabel);
this._container.appendChild(this._mainContainer);

this._mainContainer.appendChild(this._unorderedList);
this._mainContainer.appendChild(this._errorLabel);
this._container.appendChild(this._mainContainer);
} catch(error) {
swal.fire("getRecords", "Error:" + error.message, "error");
}
}

public async checkBoxChanged(evnt: Event) {
try {
var targetInput = <HTMLInputElement>evnt.target;
// @ts-ignore
var contextInfo = this._context.mode.contextInfo;
var recordId = contextInfo.entityId;
var thisEntity = contextInfo.entityTypeName;
var thatEntity = this.getEntityPluralName(this._entityName);
var thisEntityPlural = this.getEntityPluralName(thisEntity);
var associationTable = this._context.parameters.associationTable.raw!;
var lookupFieldTo = this._context.parameters.lookuptoAssociatedTable.raw!;
var lookupFieldFrom = this._context.parameters.lookuptoCurrentTable.raw!;
var lookupToLower = lookupFieldTo.toLowerCase();
var lookupFromLower = lookupFieldFrom.toLowerCase();
var lookupDataTo = lookupFieldTo + "@odata.bind";
var lookupDataFrom = lookupFieldFrom + "@odata.bind";
var selectorLabel = this._context.parameters.selectorLabel.raw!;

var data =
{
[selectorLabel]: targetInput.name,
[lookupDataTo]: "/" + thatEntity + "(" + targetInput.id + ")",
[lookupDataFrom]: "/" + thisEntityPlural + "(" + recordId + ")"
}
var actual = 0;
var cState = this._selStates.findIndex(e => e.text === targetInput.value);
if (cState !== -1)
actual = this._selStates[cState].actual;

if (targetInput.checked) {
await this._context.webAPI.createRecord(associationTable, data);
actual++;
}
else {
await this.deleteRecord(associationTable, lookupToLower, targetInput.id, lookupFromLower, recordId);
actual--;
}

this._notifyOutputChanged();
try {
var targetInput = <HTMLInputElement>evnt.target;
// @ts-ignore
var contextInfo = this._context.mode.contextInfo;
var recordId = contextInfo.entityId;
var thisEntity = contextInfo.entityTypeName;
var thatEntity = this.getEntityPluralName(this._entityName);
var thisEntityPlural = this.getEntityPluralName(thisEntity);
var associationTable = this._context.parameters.associationTable.raw!;
var lookupFieldTo = this._context.parameters.lookuptoAssociatedTable.raw!;
var lookupFieldFrom = this._context.parameters.lookuptoCurrentTable.raw!;
var lookupToLower = lookupFieldTo.toLowerCase();
var lookupFromLower = lookupFieldFrom.toLowerCase();
var lookupDataTo = lookupFieldTo + "@odata.bind";
var lookupDataFrom = lookupFieldFrom + "@odata.bind";
var selectorLabel = this._context.parameters.selectorLabel.raw!;

var data =
{
[selectorLabel]: targetInput.name,
[lookupDataTo]: "/" + thatEntity + "(" + targetInput.id + ")",
[lookupDataFrom]: "/" + thisEntityPlural + "(" + recordId + ")"
}
var actual = 0;
var cState = this._selStates.findIndex(e => e.text === targetInput.value);
if (cState !== -1)
actual = this._selStates[cState].actual;

if (targetInput.checked) {
await this._context.webAPI.createRecord(associationTable, data);
actual++;

if(this._values === null || this._values === undefined){
this._values = [];
}

if(this._values.indexOf(targetInput.id) < 0){
this._values.push(targetInput.id);
}
}
else {
await this.deleteRecord(associationTable, lookupToLower, targetInput.id, lookupFromLower, recordId);
actual--;
this._values.splice(this._values.indexOf(targetInput.id), 1);
}

this._notifyOutputChanged();
} catch (error) {
swal.fire("checkBoxChanged", "Error:" + error.message , "error");
}
Expand All @@ -240,21 +293,21 @@ export class AssociationTableControl implements ComponentFramework.StandardContr
}

public async showHideControl(show: boolean) {
try {
var display = "inline";
if (show === false) {
display = "none";
}
this._unorderedList.style.display = display;
try {
var display = "inline";
if (show === false) {
display = "none";
}
this._unorderedList.style.display = display;
} catch (error) {
swal.fire("showHideControl", "Error:" + error.message, "error");
}
}

public async refreshItems() {
try {
await this.getRecords();
return true;
try {
await this.getRecords();
return true;
} catch (error) {
swal.fire("refreshItems", "Error:" + error.message, "error");
}
Expand Down
Loading