├── AssociationTableControl ├── .gitignore ├── AssociationTableControl │ ├── .gitignore │ ├── AssociationTableControl.pcfproj │ ├── AssociationTableControl │ │ ├── ControlManifest.Input.xml │ │ ├── components │ │ │ ├── DropDownComponent.tsx │ │ │ └── ToggleComponent.tsx │ │ ├── css │ │ │ ├── AssociationTableControl.css │ │ │ └── bootstrap.min.css │ │ ├── img │ │ │ └── preview.png │ │ ├── index.ts │ │ └── strings │ │ │ └── AssociationTableControl.1033.resx │ ├── package-lock.json │ ├── package.json │ ├── pcfconfig.json │ └── tsconfig.json ├── README.md ├── Solutions │ ├── Other │ │ ├── Customizations.xml │ │ ├── Relationships.xml │ │ └── Solution.xml │ └── Solutions.cdsproj ├── package-lock.json ├── package.json ├── pcfconfig.json └── tsconfig.json ├── LICENSE ├── README.md └── images ├── Association-Table-Control-Configuration-Form-Example.png ├── Association-Table-Control-ERD.svg └── Association-Table-Control-Form-Example.gif /AssociationTableControl/.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | **/Properties/launchSettings.json 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_i.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.iobj 68 | *.pch 69 | *.pdb 70 | *.ipdb 71 | *.pgc 72 | *.pgd 73 | *.rsp 74 | *.sbr 75 | *.tlb 76 | *.tli 77 | *.tlh 78 | *.tmp 79 | *.tmp_proj 80 | *.log 81 | *.vspscc 82 | *.vssscc 83 | .builds 84 | *.pidb 85 | *.svclog 86 | *.scc 87 | 88 | # Chutzpah Test files 89 | _Chutzpah* 90 | 91 | # Visual C++ cache files 92 | ipch/ 93 | *.aps 94 | *.ncb 95 | *.opendb 96 | *.opensdf 97 | *.sdf 98 | *.cachefile 99 | *.VC.db 100 | *.VC.VC.opendb 101 | 102 | # Visual Studio profiler 103 | *.psess 104 | *.vsp 105 | *.vspx 106 | *.sap 107 | 108 | # Visual Studio Trace Files 109 | *.e2e 110 | 111 | # TFS 2012 Local Workspace 112 | $tf/ 113 | 114 | # Guidance Automation Toolkit 115 | *.gpState 116 | 117 | # ReSharper is a .NET coding add-in 118 | _ReSharper*/ 119 | *.[Rr]e[Ss]harper 120 | *.DotSettings.user 121 | 122 | # JustCode is a .NET coding add-in 123 | .JustCode 124 | 125 | # TeamCity is a build add-in 126 | _TeamCity* 127 | 128 | # DotCover is a Code Coverage Tool 129 | *.dotCover 130 | 131 | # AxoCover is a Code Coverage Tool 132 | .axoCover/* 133 | !.axoCover/settings.json 134 | 135 | # Visual Studio code coverage results 136 | *.coverage 137 | *.coveragexml 138 | 139 | # NCrunch 140 | _NCrunch_* 141 | .*crunch*.local.xml 142 | nCrunchTemp_* 143 | 144 | # MightyMoose 145 | *.mm.* 146 | AutoTest.Net/ 147 | 148 | # Web workbench (sass) 149 | .sass-cache/ 150 | 151 | # Installshield output folder 152 | [Ee]xpress/ 153 | 154 | # DocProject is a documentation generator add-in 155 | DocProject/buildhelp/ 156 | DocProject/Help/*.HxT 157 | DocProject/Help/*.HxC 158 | DocProject/Help/*.hhc 159 | DocProject/Help/*.hhk 160 | DocProject/Help/*.hhp 161 | DocProject/Help/Html2 162 | DocProject/Help/html 163 | 164 | # Click-Once directory 165 | publish/ 166 | 167 | # Publish Web Output 168 | *.[Pp]ublish.xml 169 | *.azurePubxml 170 | # Note: Comment the next line if you want to checkin your web deploy settings, 171 | # but database connection strings (with potential passwords) will be unencrypted 172 | *.pubxml 173 | *.publishproj 174 | 175 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 176 | # checkin your Azure Web App publish settings, but sensitive information contained 177 | # in these scripts will be unencrypted 178 | PublishScripts/ 179 | 180 | # NuGet Packages 181 | *.nupkg 182 | # The packages folder can be ignored because of Package Restore 183 | **/[Pp]ackages/* 184 | # except build/, which is used as an MSBuild target. 185 | !**/[Pp]ackages/build/ 186 | # Uncomment if necessary however generally it will be regenerated when needed 187 | #!**/[Pp]ackages/repositories.config 188 | # NuGet v3's project.json files produces more ignorable files 189 | *.nuget.props 190 | *.nuget.targets 191 | 192 | # Microsoft Azure Build Output 193 | csx/ 194 | *.build.csdef 195 | 196 | # Microsoft Azure Emulator 197 | ecf/ 198 | rcf/ 199 | 200 | # Windows Store app package directories and files 201 | AppPackages/ 202 | BundleArtifacts/ 203 | Package.StoreAssociation.xml 204 | _pkginfo.txt 205 | *.appx 206 | 207 | # Visual Studio cache files 208 | # files ending in .cache can be ignored 209 | *.[Cc]ache 210 | # but keep track of directories ending in .cache 211 | !*.[Cc]ache/ 212 | 213 | # Others 214 | ClientBin/ 215 | ~$* 216 | *~ 217 | *.dbmdl 218 | *.dbproj.schemaview 219 | *.jfm 220 | *.pfx 221 | *.publishsettings 222 | orleans.codegen.cs 223 | 224 | # Including strong name files can present a security risk 225 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 226 | #*.snk 227 | 228 | # Since there are multiple workflows, uncomment next line to ignore bower_components 229 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 230 | #bower_components/ 231 | 232 | # RIA/Silverlight projects 233 | Generated_Code/ 234 | 235 | # Backup & report files from converting an old project file 236 | # to a newer Visual Studio version. Backup files are not needed, 237 | # because we have git ;-) 238 | _UpgradeReport_Files/ 239 | Backup*/ 240 | UpgradeLog*.XML 241 | UpgradeLog*.htm 242 | ServiceFabricBackup/ 243 | *.rptproj.bak 244 | 245 | # SQL Server files 246 | *.mdf 247 | *.ldf 248 | *.ndf 249 | 250 | # Business Intelligence projects 251 | *.rdl.data 252 | *.bim.layout 253 | *.bim_*.settings 254 | *.rptproj.rsuser 255 | 256 | # Microsoft Fakes 257 | FakesAssemblies/ 258 | 259 | # GhostDoc plugin setting file 260 | *.GhostDoc.xml 261 | 262 | # Node.js Tools for Visual Studio 263 | .ntvs_analysis.dat 264 | node_modules/ 265 | 266 | # Visual Studio 6 build log 267 | *.plg 268 | 269 | # Visual Studio 6 workspace options file 270 | *.opt 271 | 272 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 273 | *.vbw 274 | 275 | # Visual Studio LightSwitch build output 276 | **/*.HTMLClient/GeneratedArtifacts 277 | **/*.DesktopClient/GeneratedArtifacts 278 | **/*.DesktopClient/ModelManifest.xml 279 | **/*.Server/GeneratedArtifacts 280 | **/*.Server/ModelManifest.xml 281 | _Pvt_Extensions 282 | 283 | # Paket dependency manager 284 | .paket/paket.exe 285 | paket-files/ 286 | 287 | # FAKE - F# Make 288 | .fake/ 289 | 290 | # JetBrains Rider 291 | .idea/ 292 | *.sln.iml 293 | 294 | # CodeRush 295 | .cr/ 296 | 297 | # Python Tools for Visual Studio (PTVS) 298 | __pycache__/ 299 | *.pyc 300 | 301 | # Cake - Uncomment if you are using it 302 | # tools/** 303 | # !tools/packages.config 304 | 305 | # Tabs Studio 306 | *.tss 307 | 308 | # Telerik's JustMock configuration file 309 | *.jmconfig 310 | 311 | # BizTalk build output 312 | *.btp.cs 313 | *.btm.cs 314 | *.odx.cs 315 | *.xsd.cs 316 | 317 | # OpenCover UI analysis results 318 | OpenCover/ 319 | 320 | # Azure Stream Analytics local run output 321 | ASALocalRun/ 322 | 323 | # MSBuild Binary and Structured Log 324 | *.binlog 325 | 326 | # NVidia Nsight GPU debugger configuration file 327 | *.nvuser 328 | 329 | # MFractors (Xamarin productivity tool) working folder 330 | .mfractor/ 331 | /RelationshipSummary/Solution 332 | /JsonEditor 333 | -------------------------------------------------------------------------------- /AssociationTableControl/AssociationTableControl/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # generated directory 7 | **/generated 8 | 9 | # output directory 10 | /out 11 | 12 | # msbuild output directories 13 | /bin 14 | /obj -------------------------------------------------------------------------------- /AssociationTableControl/AssociationTableControl/AssociationTableControl.pcfproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(MSBuildExtensionsPath)\Microsoft\VisualStudio\v$(VisualStudioVersion)\PowerApps 5 | 6 | 7 | 8 | 9 | 10 | 11 | AssociationTableControl 12 | 691e0d62-802e-461d-96f5-215eb80ab0e8 13 | $(MSBuildThisFileDirectory)out\controls 14 | production 15 | 16 | 17 | 18 | v4.6.2 19 | 20 | net462 21 | PackageReference 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /AssociationTableControl/AssociationTableControl/AssociationTableControl/ControlManifest.Input.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 0 15 | 1 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /AssociationTableControl/AssociationTableControl/AssociationTableControl/components/DropDownComponent.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Dropdown, IDropdownStyles, IDropdownOption } from 'office-ui-fabric-react/lib/Dropdown'; 3 | import { ICalloutProps } from 'office-ui-fabric-react/lib/Callout'; 4 | 5 | interface IDropDownProps { 6 | label:string, 7 | placeholder:string, 8 | dropdownStyles: Partial, 9 | options : IDropdownOption [], 10 | defaultSelectedKeys?: string[] ; 11 | onChangeResult: (selectedValues:any) => void; 12 | calloutProps: Partial 13 | } 14 | 15 | export default class DropDownControl extends React.Component { 16 | constructor(props:IDropDownProps) { 17 | super(props); 18 | debugger; 19 | } 20 | 21 | render() { return ( 22 | 23 | { 32 | this.props.onChangeResult(selectedOption) 33 | }} 34 | /> 35 | ); 36 | } 37 | 38 | 39 | }; -------------------------------------------------------------------------------- /AssociationTableControl/AssociationTableControl/AssociationTableControl/components/ToggleComponent.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Toggle } from 'office-ui-fabric-react/lib/Toggle'; 3 | 4 | interface IToggleProps { 5 | visible:boolean, 6 | onChangeResult: (selectedValues:any) => void; 7 | 8 | } 9 | 10 | export default class ToogleControl extends React.Component { 11 | constructor(props:IToggleProps) { 12 | super(props); 13 | 14 | } 15 | render() { return ( 16 | { 22 | this.props.onChangeResult(selectedOption) 23 | }} 24 | role="checkbox" 25 | /> 26 | ); 27 | } 28 | } -------------------------------------------------------------------------------- /AssociationTableControl/AssociationTableControl/AssociationTableControl/css/AssociationTableControl.css: -------------------------------------------------------------------------------- 1 | 2 | body, 3 | html { 4 | margin: 0; 5 | padding: 0; 6 | } 7 | /*Body*/ 8 | body { 9 | display: flex; 10 | align-items: center; 11 | justify-content: center; 12 | height: 100vh; 13 | background: #FFEFBA; 14 | /* fallback for old browsers */ 15 | background: -webkit-linear-gradient(to right, #FFFFFF, #FFEFBA); 16 | /* Chrome 10-25, Safari 5.1-6 */ 17 | background: linear-gradient(to right, #FFFFFF, #FFEFBA); 18 | /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */ 19 | 20 | 21 | } 22 | 23 | .multiselect-container { 24 | max-width: 1000px; 25 | font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; 26 | font-size: 14px; 27 | } 28 | 29 | ul.ks-cboxtags { 30 | list-style: none; 31 | padding: 0px; 32 | } 33 | ul.ks-cboxtags li{ 34 | display: inline; 35 | } 36 | 37 | ul.ks-cboxtags li label{ 38 | display: inline-block; 39 | border: 1px solid #2266E3; 40 | background-color: #ffffff; 41 | color: #2266E3; 42 | border-radius: 0px; 43 | white-space: nowrap; 44 | margin: 3px 3px; 45 | -webkit-touch-callout: none; 46 | -webkit-user-select: none; 47 | -moz-user-select: none; 48 | -ms-user-select: none; 49 | user-select: none; 50 | -webkit-tap-highlight-color: transparent; 51 | transition: all .2s; 52 | } 53 | 54 | ul.ks-cboxtags li label { 55 | padding: 2px 10px; 56 | cursor: pointer; 57 | } 58 | 59 | ul.ks-cboxtags li label::before { 60 | display: inline-block; 61 | font-style: normal; 62 | font-variant: normal; 63 | text-rendering: auto; 64 | -webkit-font-smoothing: antialiased; 65 | font-family: "Font Awesome 5 Free"; 66 | font-weight: 900; 67 | font-size: 12px; 68 | padding: 2px 6px 2px 2px; 69 | transition: transform .3s ease-in-out; 70 | } 71 | 72 | ul.ks-cboxtags li input[type="checkbox"]:checked + label::before { 73 | transform: rotate(-360deg); 74 | transition: transform .3s ease-in-out; 75 | 76 | } 77 | 78 | ul.ks-cboxtags li input[type="checkbox"]:checked + label { 79 | border: 1px solid #2266E3; 80 | background-color: #2266E3; 81 | color: #ffffff; 82 | } 83 | 84 | ul.ks-cboxtags li input[type="checkbox"] { 85 | display: absolute; 86 | } 87 | ul.ks-cboxtags li input[type="checkbox"] { 88 | position: absolute; 89 | opacity: 0; 90 | } 91 | ul.ks-cboxtags li input[type="checkbox"]:focus + label { 92 | border: 1px solid #2266E3; 93 | } -------------------------------------------------------------------------------- /AssociationTableControl/AssociationTableControl/AssociationTableControl/img/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crmvet/AssociationTableControl/c035fa80795cec6c6cc002c249b375e18fa7360c/AssociationTableControl/AssociationTableControl/AssociationTableControl/img/preview.png -------------------------------------------------------------------------------- /AssociationTableControl/AssociationTableControl/AssociationTableControl/index.ts: -------------------------------------------------------------------------------- 1 | import * as ReactDOM from 'react-dom'; 2 | import * as React from 'react'; 3 | import swal from 'sweetalert2'; 4 | import { IInputs, IOutputs } from "./generated/ManifestTypes"; 5 | import ToggleComponent from "./components/ToggleComponent"; 6 | 7 | class detailItem { 8 | parent: string; 9 | key: string; 10 | value: string; 11 | } 12 | 13 | class selState { 14 | label: string; 15 | text: string; 16 | total: number; 17 | actual: number; 18 | } 19 | 20 | export class AssociationTableControl implements ComponentFramework.StandardControl { 21 | 22 | // Global Variables 23 | private _context: ComponentFramework.Context; 24 | private _container: HTMLDivElement; 25 | private _mainContainer: HTMLDivElement; 26 | private _unorderedList: HTMLUListElement; 27 | private _errorLabel: HTMLLabelElement; 28 | public _defaultFilter: string; 29 | public _filter: string; 30 | private _entityName: string; 31 | private _selectorLabel: string; 32 | public _selValues: detailItem[]; 33 | public _values: string[]; 34 | private _checkBoxChanged: EventListenerOrEventListenerObject; 35 | private _notifyOutputChanged: () => void; 36 | private _togglePanel: HTMLDivElement; 37 | private _itemList: detailItem[]; 38 | private _selStates: selState[]; 39 | private _showToggle = false; 40 | 41 | constructor() 42 | { 43 | 44 | } 45 | 46 | public async init(context: ComponentFramework.Context, notifyOutputChanged: () => void, state: ComponentFramework.Dictionary, container:HTMLDivElement) 47 | { 48 | this._context = context; 49 | this._container = container; 50 | this._mainContainer = document.createElement("div"); 51 | this._unorderedList = document.createElement("ul"); 52 | this._errorLabel = document.createElement("label"); 53 | this._unorderedList.classList.add("ks-cboxtags"); 54 | this._mainContainer.classList.add("multiselect-container"); 55 | this._itemList = []; 56 | 57 | if (this._context.parameters.visibilityToggle.raw != null) { 58 | this._showToggle = this._context.parameters.visibilityToggle.raw == "1" ? true : false; 59 | } 60 | 61 | if (this._context.parameters.defaultFilter.raw != null) { 62 | this._defaultFilter = this._context.parameters.defaultFilter.raw; 63 | } 64 | 65 | //Trigger function on check-box change. 66 | this._notifyOutputChanged = notifyOutputChanged; 67 | this._checkBoxChanged = this.checkBoxChanged.bind(this); 68 | this._selStates = []; 69 | 70 | // @ts-ignore 71 | if (Xrm.Page.ui.getFormType() !== 1) { 72 | await this.getRelatedRecords(); 73 | } 74 | } 75 | 76 | public async updateView(context: ComponentFramework.Context) 77 | { 78 | // Add code to update control view 79 | this._context = context; 80 | 81 | //Check that the entityId value has been updated before refreshing the control 82 | if (context.updatedProperties != null && context.updatedProperties.length != 0) { 83 | if (context.updatedProperties[context.updatedProperties.length - 1] == "entityId" || context.updatedProperties[context.updatedProperties.length - 1] == "IsControlDisabled") { 84 | await this.getRecords(); 85 | } 86 | } 87 | } 88 | 89 | /** 90 | * Called when the control is to be removed from the DOM tree. Controls should use this call for cleanup. 91 | * i.e. canceling any pending remote calls, removing listeners, etc. 92 | */ 93 | public destroy(): void 94 | { 95 | // Add code to cleanup control if necessary 96 | } 97 | 98 | public async getRelatedRecords() { 99 | try { 100 | var list = []; 101 | // @ts-ignore 102 | var contextInfo = this._context.mode.contextInfo; 103 | var recordId = contextInfo.entityId; 104 | var lookupTo = this._context.parameters.lookuptoAssociatedTable.raw!.toLowerCase(); 105 | var lookupFrom = this._context.parameters.lookuptoCurrentTable.raw!.toLowerCase(); 106 | var result = await this._context.webAPI.retrieveMultipleRecords(this._context.parameters.associationTable.raw!, '?$select= _' + lookupTo + '_value&$filter=_' + lookupFrom + '_value eq ' + recordId); 107 | for (var i = 0; i < result.entities.length; i++) { 108 | var temp = result.entities[i]["_" + lookupTo + "_value"]; 109 | list.push(temp); 110 | var cItem = this._itemList.find((e => e.key === temp)); 111 | var cState = this._selStates.findIndex(e => e.text === cItem?.parent); 112 | if (cState !== -1) { 113 | this._selStates[cState].actual++; 114 | } 115 | } 116 | this._values = list; 117 | await this.getRecords(); 118 | } catch(error) { 119 | swal.fire("getRelatedRecords", "Error:" + error.message, "error"); 120 | } 121 | } 122 | 123 | //Called to retrieve records to display, both on-load and on-change of lookup 124 | public async getRecords() { 125 | try { 126 | this._container.innerHTML = ""; 127 | this._mainContainer.innerHTML = ""; 128 | this._unorderedList.innerHTML = ""; 129 | if (this._showToggle) { 130 | this._togglePanel = document.createElement("div"); 131 | this._togglePanel.style.float = "right"; 132 | var toggleProps = { 133 | visible: true, 134 | onChangeResult: this.showHideControl.bind(this) 135 | } 136 | 137 | ReactDOM.render(React.createElement(ToggleComponent, toggleProps), this._togglePanel); 138 | this._mainContainer.appendChild(this._togglePanel); 139 | } 140 | //Check if table name variable contains data 141 | if (this._context.parameters.selectorTable.raw != null && this._context.parameters.selectorTable.raw != "") { 142 | this._entityName = this._context.parameters.selectorTable.raw; 143 | } 144 | //Check if field name contains data 145 | if (this._context.parameters.selectorLabel.raw != null && this._context.parameters.selectorLabel.raw != "") { 146 | this._selectorLabel = this._context.parameters.selectorLabel.raw; 147 | } 148 | this._filter = "?$select=" + this._selectorLabel + "," + this._entityName + "id" + "&$orderby=" + this._selectorLabel + " asc"; 149 | //Default Filter 150 | if (this._defaultFilter !== undefined && this._defaultFilter !== "") { 151 | this._filter += "&$filter=" + this._defaultFilter; 152 | } 153 | var records = await this._context.webAPI.retrieveMultipleRecords(this._entityName, this._filter); 154 | for (var i = 0; i < records.entities.length; i++) { 155 | var newChkBox = document.createElement("input"); 156 | var newLabel = document.createElement("label"); 157 | var newUList = document.createElement("li"); 158 | 159 | newChkBox.type = "checkbox"; 160 | newChkBox.id = records.entities[i][this._entityName + "id"]; 161 | newChkBox.name = records.entities[i][this._selectorLabel]; 162 | newChkBox.value = records.entities[i][this._entityName + "id"]; 163 | if (this._values != undefined) { 164 | if (this._values.includes(newChkBox.id)) { 165 | newChkBox.checked = true; 166 | } 167 | } 168 | newChkBox.addEventListener("change", this._checkBoxChanged); 169 | newLabel.innerHTML = records.entities[i][this._selectorLabel]; 170 | newLabel.htmlFor = records.entities[i][this._entityName + "id"]; 171 | newUList.appendChild(newChkBox); 172 | newUList.appendChild(newLabel); 173 | this._unorderedList.appendChild(newUList); 174 | } 175 | this._mainContainer.appendChild(this._unorderedList); 176 | this._mainContainer.appendChild(this._errorLabel); 177 | this._container.appendChild(this._mainContainer); 178 | 179 | } catch(error) { 180 | swal.fire("getRecords", "Error:" + error.message, "error"); 181 | } 182 | } 183 | 184 | public async checkBoxChanged(evnt: Event) { 185 | try { 186 | var targetInput = evnt.target; 187 | // @ts-ignore 188 | var contextInfo = this._context.mode.contextInfo; 189 | var recordId = contextInfo.entityId; 190 | var thisEntity = contextInfo.entityTypeName; 191 | var thatEntity = this.getEntityPluralName(this._entityName); 192 | var thisEntityPlural = this.getEntityPluralName(thisEntity); 193 | var associationTable = this._context.parameters.associationTable.raw!; 194 | var lookupFieldTo = this._context.parameters.lookuptoAssociatedTable.raw!; 195 | var lookupFieldFrom = this._context.parameters.lookuptoCurrentTable.raw!; 196 | var lookupToLower = lookupFieldTo.toLowerCase(); 197 | var lookupFromLower = lookupFieldFrom.toLowerCase(); 198 | var lookupDataTo = lookupFieldTo + "@odata.bind"; 199 | var lookupDataFrom = lookupFieldFrom + "@odata.bind"; 200 | var associationTableNameField = this._context.parameters.associationLable.raw!; 201 | 202 | var data = 203 | { 204 | [associationTableNameField]: targetInput.name, 205 | [lookupDataTo]: "/" + thatEntity + "(" + targetInput.id + ")", 206 | [lookupDataFrom]: "/" + thisEntityPlural + "(" + recordId + ")" 207 | } 208 | var actual = 0; 209 | var cState = this._selStates.findIndex(e => e.text === targetInput.value); 210 | if (cState !== -1) 211 | actual = this._selStates[cState].actual; 212 | 213 | if (targetInput.checked) { 214 | await this._context.webAPI.createRecord(associationTable, data); 215 | actual++; 216 | } 217 | else { 218 | await this.deleteRecord(associationTable, lookupToLower, targetInput.id, lookupFromLower, recordId); 219 | actual--; 220 | } 221 | 222 | this._notifyOutputChanged(); 223 | } catch (error) { 224 | swal.fire("checkBoxChanged", "Error:" + error.message , "error"); 225 | } 226 | } 227 | 228 | //Async delete record process called when a check-box is unchecked 229 | private async deleteRecord(associationTable: string, lookupToLower: string, targetInput: string, lookupFromLower: string, recordId: string) { 230 | let _this = this; 231 | try { 232 | var result = await this._context.webAPI.retrieveMultipleRecords(associationTable, '?$select=' + associationTable + 'id&$filter=_' + lookupToLower + '_value eq ' + targetInput + ' and _' + lookupFromLower + '_value eq ' + recordId) 233 | for (var i = 0; i < result.entities.length; i++) { 234 | var linkRecordId = result.entities[i][associationTable + 'id']; 235 | } 236 | _this._context.webAPI.deleteRecord(associationTable, linkRecordId) 237 | } catch(error) { 238 | swal.fire("deleteRecord", "Error:" + error.message, "error"); 239 | } 240 | } 241 | 242 | public async showHideControl(show: boolean) { 243 | try { 244 | var display = "inline"; 245 | if (show === false) { 246 | display = "none"; 247 | } 248 | this._unorderedList.style.display = display; 249 | } catch (error) { 250 | swal.fire("showHideControl", "Error:" + error.message, "error"); 251 | } 252 | } 253 | 254 | public async refreshItems() { 255 | try { 256 | await this.getRecords(); 257 | return true; 258 | } catch (error) { 259 | swal.fire("refreshItems", "Error:" + error.message, "error"); 260 | } 261 | } 262 | 263 | //Retrieve plural name of a table 264 | private getEntityPluralName(entityName: string): string { 265 | if (entityName.endsWith("s")) 266 | return entityName + "es"; 267 | else if (entityName.endsWith("y")) 268 | return entityName.slice(0, entityName.length - 1) + "ies"; 269 | else 270 | return entityName + "s"; 271 | } 272 | } -------------------------------------------------------------------------------- /AssociationTableControl/AssociationTableControl/AssociationTableControl/strings/AssociationTableControl.1033.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | text/microsoft-resx 51 | 52 | 53 | 2.0 54 | 55 | 56 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 57 | 58 | 59 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 60 | 61 | 62 | Control Name 63 | 64 | 65 | Control Description 66 | 67 | -------------------------------------------------------------------------------- /AssociationTableControl/AssociationTableControl/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pcf-project", 3 | "version": "1.0.0", 4 | "description": "Association Table Control (PCF).", 5 | "scripts": { 6 | "build": "pcf-scripts build", 7 | "clean": "pcf-scripts clean", 8 | "rebuild": "pcf-scripts rebuild", 9 | "start": "pcf-scripts start" 10 | }, 11 | "dependencies": { 12 | "@fluentui/react": "^8.6.1", 13 | "@types/node": "^10.12.18", 14 | "@types/powerapps-component-framework": "^1.2.0", 15 | "react-bootstrap": "^1.5.2", 16 | "sweetalert2": "^10.15.7" 17 | }, 18 | "devDependencies": { 19 | "pcf-scripts": "^1", 20 | "pcf-start": "^1" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /AssociationTableControl/AssociationTableControl/pcfconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "outDir": "./out/controls" 3 | } -------------------------------------------------------------------------------- /AssociationTableControl/AssociationTableControl/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/pcf-scripts/tsconfig_base.json", 3 | "compilerOptions": { 4 | "typeRoots": ["node_modules/@types"], 5 | } 6 | } -------------------------------------------------------------------------------- /AssociationTableControl/README.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /AssociationTableControl/Solutions/Other/Customizations.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 1033 17 | 18 | -------------------------------------------------------------------------------- /AssociationTableControl/Solutions/Other/Relationships.xml: -------------------------------------------------------------------------------- 1 |  2 | -------------------------------------------------------------------------------- /AssociationTableControl/Solutions/Other/Solution.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | crmvet_AssociationTableControl 6 | 7 | 8 | 9 | 10 | 11 | 1.0.0.3 12 | 13 | 2 14 | 15 | 16 | crmvet 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | crmvet 29 | 30 | 29305 31 | 32 | 33 |
34 | 1 35 | 1 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 1 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 |
61 |
62 | 2 63 | 1 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 1 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 |
89 |
90 |
91 | 92 | 93 |
94 |
-------------------------------------------------------------------------------- /AssociationTableControl/Solutions/Solutions.cdsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | $(MSBuildExtensionsPath)\Microsoft\VisualStudio\v$(VisualStudioVersion)\PowerApps 5 | 6 | 7 | 8 | 9 | 10 | 11 | cc0710f0-106c-4362-95bb-801bc7722666 12 | v4.6.2 13 | 14 | net462 15 | PackageReference 16 | 17 | 18 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /AssociationTableControl/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pcf-project", 3 | "version": "1.0.0", 4 | "description": "Association Table Control (PCF).", 5 | "scripts": { 6 | "build": "pcf-scripts build", 7 | "clean": "pcf-scripts clean", 8 | "rebuild": "pcf-scripts rebuild", 9 | "start": "pcf-scripts start" 10 | }, 11 | "dependencies": { 12 | "@fluentui/react": "^7.153.0", 13 | "@types/jquery": "^3.3.33", 14 | "@types/node": "^10.12.18", 15 | "@types/powerapps-component-framework": "^1.2.0", 16 | "office-ui-fabric-react": "^7.105.1", 17 | "react": "^16.13.1", 18 | "react-dom": "^16.13.1" 19 | }, 20 | "devDependencies": { 21 | "pcf-scripts": "^1", 22 | "pcf-start": "^1" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /AssociationTableControl/pcfconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "outDir": "./out/controls" 3 | } -------------------------------------------------------------------------------- /AssociationTableControl/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/pcf-scripts/tsconfig_base.json", 3 | "compilerOptions": { 4 | "typeRoots": ["node_modules/@types"], 5 | } 6 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 crmvet 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

Overview

3 | 4 | 5 | 6 |

This control provides a nice UX to work with custom Microsoft Dataverse association (aka joint or many-to-many) 7 | tables on model-driven Power Apps (D365) forms.

8 | 9 | 10 | 11 |
12 |
13 | 14 | 15 | 16 |

There are several advantages to using custom Microsoft Dataverse tables to implement many-to-many relationships in 17 | Microsoft Dataverse. For more information and examples please read this post. 19 |

20 | 21 | 22 | 23 |

Data structure model for this control:

24 | 25 | 26 | 27 |
29 | 30 | 31 | 32 |

Installation

33 | 34 | 35 | 36 |

You can either take source code and embed it to your DevOps process (if planning to modify the control) or install a 37 | ready-to-use solution. You can find it in Releases Folder.

39 | 40 | 41 | 42 |

Configuration

43 | 44 | 45 | 46 |

On the string (Single Text or Multiline Text) you can go to the Field Properties > Controls > Add Control.

47 | 48 | 49 | 50 |
52 | 53 | 54 | 55 |

Configuration Parameters Description

56 | 57 | 58 | 59 |
60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 106 | 107 | 108 | 109 |
ParameterDescriptionExample
Selector TableName for the Selector Table to show records to choose from in UIcrmvet_table2
Selector LabelLogical Name for the Selector Table field to be used as a Label in UI for selector itemscrmvet_uilabel
Association TableName for the Association Table (aka N:N Table) where joint/association records are storedcrmvet_table1_table2_association
Association LableLogical Name for the Association Table field to be used as a Label in UI for associated itemscrmvet_uiassociatedlabel
Lookup to Associated TableSchema Name of the lookup to the associated/target table in the joint/association table (N:N)crmvet_Table2Id
Lookup to Current TableSchema Name of the lookup to the current table in the joint/association table (N:N)crmvet_Table1Id
Default FilterDefault Filter for Selector Table to define what records would be visible in UIstatuscode eq 1
Visibility ToggleShow or Hide in UI Selector Values. Useful if due to the number of options PCF takes too much space 105 | on a formYes/No
110 |
111 | -------------------------------------------------------------------------------- /images/Association-Table-Control-Configuration-Form-Example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crmvet/AssociationTableControl/c035fa80795cec6c6cc002c249b375e18fa7360c/images/Association-Table-Control-Configuration-Form-Example.png -------------------------------------------------------------------------------- /images/Association-Table-Control-ERD.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml 46 | 49 | 50 | 53 | 54 | 55 | 56 | 75 | 76 | 78 | 85 | 90 | 95 | 100 | 101 | 108 | 113 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 157 | 159 | 163 | 164 | 174 | 182 | 183 | 193 | 201 | 202 | 204 | 208 | 209 | 217 | 225 | 226 | 228 | 232 | 233 | 241 | 249 | 250 | 251 | 257 | 258 | 262 | 266 | 270 | 274 | 278 | 282 | 286 | 290 | 294 | 298 | 302 | 306 | 310 | 314 | 318 | 322 | 326 | 330 | 334 | 338 | 342 | 346 | 350 | 354 | 358 | 362 | 366 | 370 | 374 | 375 | Plain.1005 377 | 382 | 383 | 387 | 391 | 395 | 399 | 403 | 407 | 411 | 415 | 419 | 423 | 427 | 431 | 435 | 439 | 443 | 444 | Sheet.1043 446 | 452 | Sheet.1044 454 | 455 | 459 | 463 | 467 | 471 | 475 | 479 | 483 | 487 | 491 | 495 | 499 | 503 | 507 | 511 | 512 | 513 | 518 | 522 | 523 | 524 | 530 | 531 | 535 | 539 | 543 | 547 | 551 | 555 | 559 | 563 | 567 | 571 | 575 | 579 | 583 | 587 | 591 | 595 | 599 | 603 | 607 | 611 | 615 | 619 | 623 | 627 | 631 | 635 | 639 | 643 | 647 | 651 | 655 | 659 | 663 | 667 | 671 | 675 | 679 | 680 | Sheet.1045 682 | OOTB Dataverse Many to Many relationship 684 | 689 | Sheet.1046 691 | 692 | 696 | 700 | 704 | 708 | 712 | 716 | 720 | 724 | 728 | 732 | 736 | 740 | 744 | 748 | 752 | 753 | 754 | 759 | 761 | 766 | OOTB Dataverse Many to Many relationship 772 | 773 | 779 | 780 | 784 | 788 | 792 | 796 | 800 | 804 | 808 | 812 | 816 | 820 | 824 | 828 | 832 | 836 | 840 | 844 | 848 | 852 | 856 | 860 | 864 | 868 | 872 | 876 | 880 | 884 | 888 | 892 | 896 | 900 | 901 | Plain.1010 903 | 908 | 909 | 913 | 917 | 921 | 925 | 929 | 933 | 937 | 941 | 945 | 949 | 953 | 957 | 961 | 965 | 969 | 973 | 977 | 981 | 982 | Sheet.1048 984 | 990 | Sheet.1049 992 | 993 | 997 | 1001 | 1005 | 1009 | 1013 | 1017 | 1021 | 1025 | 1029 | 1033 | 1037 | 1041 | 1045 | 1049 | 1050 | 1051 | 1056 | 1060 | 1061 | 1062 | 1068 | 1069 | 1073 | 1077 | 1081 | 1085 | 1089 | 1093 | 1097 | 1101 | 1105 | 1109 | 1113 | 1117 | 1121 | 1125 | 1129 | 1133 | 1137 | 1141 | 1145 | 1149 | 1153 | 1157 | 1161 | 1165 | 1169 | 1173 | 1177 | 1181 | 1185 | 1189 | 1193 | 1197 | 1201 | 1205 | 1209 | 1213 | 1217 | 1221 | 1225 | 1229 | 1233 | 1234 | Sheet.1050 1236 | Custom Association Table in Dataverse to implement custom Man... 1238 | 1243 | Sheet.1051 1245 | 1246 | 1250 | 1254 | 1258 | 1262 | 1266 | 1270 | 1274 | 1278 | 1282 | 1286 | 1290 | 1294 | 1298 | 1302 | 1306 | 1307 | 1308 | 1313 | 1315 | 1320 | Custom Association Table in Dataverse to implement custom Many to Many relationship 1326 | 1327 | 1332 | Sheet.1062 1334 | Table 1 1336 | 1338 | 1343 | 1350 | Table 1 1362 | Dynamic connector 1364 | N:N 1366 | 1368 | 1373 | 1377 | 1385 | N:N 1396 | Sheet.1064 1398 | Table 2 1400 | 1402 | 1407 | 1414 | Table 2 1425 | Sheet.1065 1427 | Table 1 1429 | 1431 | 1436 | 1443 | Table 1 1455 | Dynamic connector.1001 1457 | 1461 | 1466 | Sheet.1067 1468 | Table 2 1470 | 1472 | 1477 | 1484 | Table 2 1495 | Sheet.1068 1497 | Table 1 to Table 2 Association 1499 | 1501 | 1506 | 1513 | Table 1 to Table 2 Association 1529 | Dynamic connector.1004 1531 | 1535 | 1536 | 1537 | -------------------------------------------------------------------------------- /images/Association-Table-Control-Form-Example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crmvet/AssociationTableControl/c035fa80795cec6c6cc002c249b375e18fa7360c/images/Association-Table-Control-Form-Example.gif --------------------------------------------------------------------------------