From cbf89aad90f1d89afb0510670aaaf5d210d346d4 Mon Sep 17 00:00:00 2001 From: MscrmTools Date: Mon, 19 Dec 2022 11:16:12 +0100 Subject: [PATCH] Added Mru,Favorites,Search + fixes --- .../ControlManifest.Input.xml | 10 +- .../LookupToPicklist/RecordSelector.tsx | 84 -------------- .../LookupToPicklist/SearchableDropdown.tsx | 106 ++++++++++++++++++ LookupToPicklist/LookupToPicklist/index.ts | 100 +++++++++++++---- .../strings/LookupToPicklist.1033.resx | 24 ++++ .../strings/LookupToPicklist.1036.resx | 27 +++++ 6 files changed, 245 insertions(+), 106 deletions(-) delete mode 100644 LookupToPicklist/LookupToPicklist/RecordSelector.tsx create mode 100644 LookupToPicklist/LookupToPicklist/SearchableDropdown.tsx diff --git a/LookupToPicklist/LookupToPicklist/ControlManifest.Input.xml b/LookupToPicklist/LookupToPicklist/ControlManifest.Input.xml index 77d8ce4..a40fe6b 100644 --- a/LookupToPicklist/LookupToPicklist/ControlManifest.Input.xml +++ b/LookupToPicklist/LookupToPicklist/ControlManifest.Input.xml @@ -1,14 +1,20 @@  - + + 1 0 - + + + 1 + 0 + + 1 0 diff --git a/LookupToPicklist/LookupToPicklist/RecordSelector.tsx b/LookupToPicklist/LookupToPicklist/RecordSelector.tsx deleted file mode 100644 index e8cd5ca..0000000 --- a/LookupToPicklist/LookupToPicklist/RecordSelector.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import * as React from 'react'; -import { Dropdown, IDropdownOption, IDropdownStyleProps, IDropdownStyles } from '@fluentui/react/lib/Dropdown'; -import { Icon } from '@fluentui/react/lib/Icon'; - -export interface IRecordSelectorProps { - selectedRecordId: string | undefined; - availableOptions: IDropdownOption[]; - isDisabled: boolean; - onChange: (selectedOption?: IDropdownOption) => void; -} - -export const RecordSelector: React.FunctionComponent = props => { - return ( - { - props.onChange(option); - }} - styles={DropdownStyle} - onRenderOption={(option): JSX.Element=>{ - if(option?.data && option.data.isMenu && option.data.icon){ - return ( -
-
- ); - } - else{ - return ( -
{option?.text} -
- ); - } - }} - />); -} -const iconStyles = { marginRight: '8px' }; -const italicStyle = { fontStyle: 'italic', align:'right' }; - -export const DropdownStyle = (props: IDropdownStyleProps): Partial => ({ - ...(props.disabled ? { - root: { - width: "100%" - }, - title: { - color: "rgb(50, 49, 48)", - borderColor: "transparent", - backgroundColor: "transparent", - fontWeight: 600, - ":hover": { - backgroundColor: "rgb(226, 226, 226)" - } - }, - caretDown: { - color: "transparent" - } - }: { - root: { - width: "100%" - }, - title: { - borderColor: "transparent", - fontWeight: 600, - ":hover": { - borderColor: "rgb(96, 94, 92)", - fontWeight: 400 - } - }, - caretDown: { - color: "transparent", - ":hover": { - color: "rgb(96, 94, 92)" - } - }, - dropdown: { - ":focus:after": { - borderColor: "transparent" - } - } - }) -}); \ No newline at end of file diff --git a/LookupToPicklist/LookupToPicklist/SearchableDropdown.tsx b/LookupToPicklist/LookupToPicklist/SearchableDropdown.tsx new file mode 100644 index 0000000..e5848da --- /dev/null +++ b/LookupToPicklist/LookupToPicklist/SearchableDropdown.tsx @@ -0,0 +1,106 @@ +import * as React from 'react'; +import { Dropdown, DropdownMenuItemType, IDropdownOption, IDropdownProps, IDropdownStyleProps, IDropdownStyles } from '@fluentui/react/lib/Dropdown'; +import { Icon } from '@fluentui/react/lib/Icon'; +import { SearchBox } from '@fluentui/react/lib/SearchBox'; + +export const SearchableDropdown: React.FunctionComponent = props => { + const [searchText, setSearchText] = React.useState(''); + + return ( + setSearchText('')} + selectedKey = {props.selectedKey} + options={[ + ...props.options.map(option => !option.disabled && option.text.toLowerCase().indexOf(searchText.toLowerCase()) > -1 || ["divider","new","FilterHeader","divider_filterHeader","mru","mru_divider1","mru_divider2","mru_divider3","records_header","favorite","mru_divider4","mru_divider5"].includes(option.key?.toString())? + option : { ...option, hidden: true } + ), + ]} + onRenderOption={(option): JSX.Element=>{ + if(option?.data && option.data.isMenu && option.data.icon){ + return ( +
+
+ ); + } + else if(option?.data && option.data.isMru){ + return ( +
+
+ ); + } + else if(option?.data && option.data.isFavorite){ + return ( +
+
+ ); + } + else{ + + if(option?.itemType === DropdownMenuItemType.Header && option.key === "FilterHeader") { + return( + setSearchText(newValue ?? "")} underlined={true} placeholder={option.data.label} /> + ); + } + else{ + return ( +
{option?.text} +
+ ); + } + } + }} + />); +} +const iconStyles = { marginRight: '8px' }; +const italicStyle = { fontStyle: 'italic', align:'right' }; + +export const DropdownStyle = (props: IDropdownStyleProps): Partial => ({ + ...(props.disabled ? { + root: { + width: "100%" + }, + title: { + color: "rgb(50, 49, 48)", + borderColor: "transparent", + backgroundColor: "transparent", + fontWeight: 600, + ":hover": { + backgroundColor: "rgb(226, 226, 226)" + } + }, + caretDown: { + color: "transparent" + } + }: { + root: { + width: "100%" + }, + title: { + borderColor: "transparent", + fontWeight: 600, + ":hover": { + borderColor: "rgb(96, 94, 92)", + fontWeight: 400 + } + }, + caretDown: { + color: "transparent", + ":hover": { + color: "rgb(96, 94, 92)" + } + }, + dropdown: { + ":focus:after": { + borderColor: "transparent" + } + } + }) +}); \ No newline at end of file diff --git a/LookupToPicklist/LookupToPicklist/index.ts b/LookupToPicklist/LookupToPicklist/index.ts index e00df3e..ccec56c 100644 --- a/LookupToPicklist/LookupToPicklist/index.ts +++ b/LookupToPicklist/LookupToPicklist/index.ts @@ -1,8 +1,8 @@ import { IInputs, IOutputs } from './generated/ManifestTypes' import * as React from 'react' import * as ReactDom from 'react-dom' -import { RecordSelector } from './RecordSelector' import { DropdownMenuItemType, IDropdownOption } from '@fluentui/react/lib/Dropdown' +import { SearchableDropdown } from './SearchableDropdown' export class LookupToPicklist implements ComponentFramework.StandardControl { @@ -18,6 +18,7 @@ export class LookupToPicklist implements ComponentFramework.StandardControl { @@ -61,7 +59,16 @@ console.log("Entité: " + this.entityName + " / Vue : " + this.viewId); private retrieveRecords(){ var thisCtrl = this; - thisCtrl._context.webAPI.retrieveRecord('savedquery', this.viewId, "?$select=fetchxml,returnedtypecode").then(view =>{ + let filter = ""; + if(this.viewId){ + filter = "?$top=1&$select=fetchxml,returnedtypecode&$filter=savedqueryid eq " + this.viewId; + } + else{ + filter = "?$top=1&$select=fetchxml,returnedtypecode&$filter=returnedtypecode eq '" + this.entityName + "' and querytype eq 64"; + } + + thisCtrl._context.webAPI.retrieveMultipleRecords('savedquery',filter).then(result =>{ + let view =result.entities[0]; var xml = view.fetchxml; if(thisCtrl._context.parameters.dependantLookup && thisCtrl._context.parameters.dependantLookup.raw && thisCtrl._context.parameters.dependantLookup.raw.length > 0){ var dependentId = thisCtrl._context.parameters.dependantLookup.raw[0].id; @@ -113,9 +120,15 @@ console.log("Entité: " + this.entityName + " / Vue : " + this.viewId); public updateView(context: ComponentFramework.Context): void { if(context.updatedProperties.includes("dependantLookup")){ - this.retrieveRecords(); + let newParentId = context.parameters.dependantLookup.raw.length > 0 ? context.parameters.dependantLookup.raw[0].id : null; + if(newParentId !== this.parentId){ + this.parentId = newParentId; + this.currentValue = undefined; + this.notifyOutputChanged(); + this.retrieveRecords(); + } } - else{ + else if(context.updatedProperties.includes("lookup")){ this.renderControl(context); } } @@ -144,8 +157,52 @@ console.log("Entité: " + this.entityName + " / Vue : " + this.viewId); return 0; }) } + + let searchOptions = thisCtrl._context.parameters.addSearch.raw === "1" ? [ + { key: 'FilterHeader', text: '-', itemType: DropdownMenuItemType.Header, data:{label: thisCtrl._context.resources.getString("searchPlaceHolder")} }, + { key: 'divider_filterHeader', text: '-', itemType: DropdownMenuItemType.Divider } + ] : []; - let options = [{key: '---', text:'---'},...this.availableOptions]; + let mruOptions = []; + // @ts-ignore + let mrus = thisCtrl._context.parameters.lookup.getRecentItems(); + // @ts-ignore + if(mrus?.length > 0 && context.parameters.lookup.getLookupConfiguration().isMruDisabled === false){ + let maxSize = thisCtrl._context.parameters.mruSize?.raw ?? 999; + + mruOptions.push({key:"mru", text:thisCtrl._context.resources.getString("recentItems"), itemType: DropdownMenuItemType.Header},{ key: 'mru_divider1', text: '-', itemType: DropdownMenuItemType.Divider }); + for(let i=0;i< mrus.length && i < maxSize; i++){ + mruOptions.push({key: mrus[i].objectId+"_mru", text:this.availableOptions.find(o => o.key===mrus[i].objectId)?.text ?? mrus[i].title, itemType: DropdownMenuItemType.Normal, data:{isMru:true}}); + } + + mruOptions.push({ key: 'mru_divider2', text: '-', itemType: DropdownMenuItemType.Divider }); + } + + let favoritesOptions = []; + if(thisCtrl._context.parameters.favorites.raw !== null && thisCtrl._context.parameters.favorites.raw.length > 0){ + favoritesOptions.push({key:"favorite", text:thisCtrl._context.resources.getString("favorites"), itemType: DropdownMenuItemType.Header},{ key: 'mru_divider4', text: '-', itemType: DropdownMenuItemType.Divider }); + let favorites = >JSON.parse(thisCtrl._context.parameters.favorites.raw); + + for(let i of favorites){ + let favOption = this.availableOptions.find(o => o.key===i); + if(favOption){ + favoritesOptions.push({key: i +"_fav", text:favOption.text, itemType: DropdownMenuItemType.Normal, data:{isFavorite:true}}); + } + } + + favoritesOptions.push({ key: 'mru_divider5', text: '-', itemType: DropdownMenuItemType.Divider }); + } + + if(favoritesOptions.length > 0){ + favoritesOptions.push({ key: 'records_header', text: thisCtrl.entityDisplayName, itemType: DropdownMenuItemType.Header }); + favoritesOptions.push({ key: 'mru_divider3', text: '-', itemType: DropdownMenuItemType.Divider }); + } + else if(mruOptions.length > 0){ + mruOptions.push({ key: 'records_header', text: thisCtrl.entityDisplayName, itemType: DropdownMenuItemType.Header }); + mruOptions.push({ key: 'mru_divider3', text: '-', itemType: DropdownMenuItemType.Divider }); + } + + let options = [...searchOptions,...mruOptions,...favoritesOptions,{key: '---', text:'---'},...this.availableOptions]; if(thisCtrl._context.parameters.addNew.raw === "1") { // If rights to read and create entity @@ -155,34 +212,37 @@ console.log("Entité: " + this.entityName + " / Vue : " + this.viewId); } } - const recordSelector = React.createElement(RecordSelector, { - selectedRecordId: recordId, - availableOptions: options, + const recordSelector = React.createElement(SearchableDropdown, { + selectedKey: recordId, + options: options, isDisabled: context.mode.isControlDisabled, - onChange: (selectedOption?: IDropdownOption) => { - if(selectedOption?.key === "new"){ + onChange: (event: React.FormEvent, option?: IDropdownOption, index?: number) => { + if(option?.key === "new"){ context.navigation.openForm({ entityName : this.entityName, useQuickCreateForm: true, windowPosition: 2 }).then((result)=>{ - if(result.savedEntityReference.length > 0){ + if(result.savedEntityReference?.length > 0){ thisCtrl.newlyCreatedId = result.savedEntityReference[0].id.replace('{','').replace('}','').toLowerCase(); } thisCtrl.retrieveRecords(); }); } - else if (typeof selectedOption === 'undefined' || selectedOption.key === '---') { + else if (typeof option === 'undefined' || option.key === '---') { this.currentValue = undefined + + this.notifyOutputChanged(); } else { + option.selected = true; this.currentValue = [{ - id: selectedOption.key, - name: selectedOption.text, + id: (option.key).split('_mru')[0].split('_fav')[0], + name: option.text, entityType: this.entityName - }] - } + }]; - this.notifyOutputChanged(); + this.notifyOutputChanged(); + } } }) diff --git a/LookupToPicklist/LookupToPicklist/strings/LookupToPicklist.1033.resx b/LookupToPicklist/LookupToPicklist/strings/LookupToPicklist.1033.resx index 4092992..be52fe0 100644 --- a/LookupToPicklist/LookupToPicklist/strings/LookupToPicklist.1033.resx +++ b/LookupToPicklist/LookupToPicklist/strings/LookupToPicklist.1033.resx @@ -109,4 +109,28 @@ Set the Lookup column to use to filter this control. Must be the same Lookup than the one defined in this control filter configuration + + Display Search bar + + + Recent items + + + Search + + + Favorites + + + Favorites + + + List of records unique identifiers to show as favorites + + + Number of recent items + + + Indicates the number of recent items to display + \ No newline at end of file diff --git a/LookupToPicklist/LookupToPicklist/strings/LookupToPicklist.1036.resx b/LookupToPicklist/LookupToPicklist/strings/LookupToPicklist.1036.resx index 012149b..af657d9 100644 --- a/LookupToPicklist/LookupToPicklist/strings/LookupToPicklist.1036.resx +++ b/LookupToPicklist/LookupToPicklist/strings/LookupToPicklist.1036.resx @@ -109,4 +109,31 @@ Indique la colonne de type Recherche qui doit permettre de filtrer le contrôle courant. Doit être la même valeur que la colonne indiquée dans le filtrage au niveau du formulaire + + Afficher recherche + + + Indique s'il faut afficher une barre de recherche des enregistrements + + + Rechercher + + + Eléments récents + + + Favoris + + + Favoris + + + Liste d'identifiants des enregistrements à proposer en favoris + + + Nombre d'éléments récents + + + Indique le nombre d'éléments récents à afficher + \ No newline at end of file