Skip to content

Commit 7e1c661

Browse files
committed
Support lookup as user input
1 parent 0ffc048 commit 7e1c661

File tree

10 files changed

+150
-29
lines changed

10 files changed

+150
-29
lines changed

README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ My colleague [MrAutomate33](https://github.com/mrautomate33) and I have been lon
1717
<br />
1818
This SPFx (SharePoint Framework) command set expands on the existing 'trigger a flow' menu button in SharePoint, and allows you to configure one or more HTTP request triggered flows and serve the user with a choice on which flow to execute when selecting one or more items by injecting a menu lint and context button upon loading the page. A sppkg for both SharePoint document libaries and custom lists are available.
1919

20-
**[<img src="https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fwww.iconsdb.com%2Ficons%2Fpreview%2Froyal-blue%2Fdata-transfer-download-xxl.png&f=1&nofb=1" alt="Download .sppkg file" style="width:15px;margin-right:10px;"/><u>Download the .sppkg file for custom lists here!</u>](https://github.com/cupo365/enhanced-power-automate-command-set/releases/tag/v1.3.0)**
20+
**[<img src="https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fwww.iconsdb.com%2Ficons%2Fpreview%2Froyal-blue%2Fdata-transfer-download-xxl.png&f=1&nofb=1" alt="Download .sppkg file" style="width:15px;margin-right:10px;"/><u>Download the .sppkg file for custom lists here!</u>](https://github.com/cupo365/enhanced-power-automate-command-set/releases/tag/v1.4.0)**
2121

22-
**[<img src="https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fwww.iconsdb.com%2Ficons%2Fpreview%2Froyal-blue%2Fdata-transfer-download-xxl.png&f=1&nofb=1" alt="Download .sppkg file" style="width:15px;margin-right:10px;"/><u>Download the .sppkg file for document libraries here!</u>](https://github.com/cupo365/enhanced-power-automate-command-set/releases/tag/v1.3.0)**
22+
**[<img src="https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fwww.iconsdb.com%2Ficons%2Fpreview%2Froyal-blue%2Fdata-transfer-download-xxl.png&f=1&nofb=1" alt="Download .sppkg file" style="width:15px;margin-right:10px;"/><u>Download the .sppkg file for document libraries here!</u>](https://github.com/cupo365/enhanced-power-automate-command-set/releases/tag/v1.4.0)**
2323

2424
## Compatibility
2525

@@ -72,6 +72,7 @@ This SPFx (SharePoint Framework) command set expands on the existing 'trigger a
7272
| [1.1.0](https://github.com/cupo365/enhanced-power-automate-command-set/releases/tag/v1.1.0) | July 24, 2022 | Migration to SPFx 1.15.0 and lots of optimalizations |
7373
| [1.2.0](https://github.com/cupo365/enhanced-power-automate-command-set/releases/tag/v1.2.1) | August 1, 2022 | Customizable list and folder whitelisting and content type and file extension blacklisting |
7474
| [1.3.0](https://github.com/cupo365/enhanced-power-automate-command-set/releases/tag/v1.3.0) | October 2, 2022 | Dynamic user input form |
75+
| [1.4.0](https://github.com/cupo365/enhanced-power-automate-command-set/releases/tag/v1.4.0) | October 4, 2022 | Support lookup as user input |
7576

7677
## Supported languages
7778
- English
@@ -275,6 +276,6 @@ If a message is present, it will be displayed in the dialog that is shown to the
275276

276277
## Download the web part packages
277278

278-
**[<img src="https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fwww.iconsdb.com%2Ficons%2Fpreview%2Froyal-blue%2Fdata-transfer-download-xxl.png&f=1&nofb=1" alt="Download .sppkg file" style="width:15px;margin-right:10px;"/><u>Download the .sppkg file for custom lists here!</u>](https://github.com/cupo365/enhanced-power-automate-command-set/releases/tag/v1.3.0)**
279+
**[<img src="https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fwww.iconsdb.com%2Ficons%2Fpreview%2Froyal-blue%2Fdata-transfer-download-xxl.png&f=1&nofb=1" alt="Download .sppkg file" style="width:15px;margin-right:10px;"/><u>Download the .sppkg file for custom lists here!</u>](https://github.com/cupo365/enhanced-power-automate-command-set/releases/tag/v1.4.0)**
279280

280-
**[<img src="https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fwww.iconsdb.com%2Ficons%2Fpreview%2Froyal-blue%2Fdata-transfer-download-xxl.png&f=1&nofb=1" alt="Download .sppkg file" style="width:15px;margin-right:10px;"/><u>Download the .sppkg file for document libraries here!</u>](https://github.com/cupo365/enhanced-power-automate-command-set/releases/tag/v1.3.0)**
281+
**[<img src="https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fwww.iconsdb.com%2Ficons%2Fpreview%2Froyal-blue%2Fdata-transfer-download-xxl.png&f=1&nofb=1" alt="Download .sppkg file" style="width:15px;margin-right:10px;"/><u>Download the .sppkg file for document libraries here!</u>](https://github.com/cupo365/enhanced-power-automate-command-set/releases/tag/v1.4.0)**

SPFx/config/package-solution.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"solution": {
44
"name": "doc-enhanced-power-automate-trigger-client-side-solution",
55
"id": "5DA1DC8B-F9A4-43D4-9A0B-C2E89E2D5FBE",
6-
"version": "1.3.0.0",
6+
"version": "1.4.0.0",
77
"includeClientSideAssets": true,
88
"skipFeatureDeployment": false,
99
"isDomainIsolated": false,

SPFx/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "enhanced-power-automate-trigger",
3-
"version": "1.3.0",
3+
"version": "1.4.0",
44
"private": true,
55
"main": "lib/index.js",
66
"description": "Trigger a Power Automate flow from SharePoint while selecting one or more items.",
Binary file not shown.
Binary file not shown.

SPFx/src/extensions/enhancedPowerAutomateTrigger/components/UserInputForm.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,13 @@ export const UserInputForm: React.FC<IUserInputFormProps> = (
3939
const numberRegex: RegExp = new RegExp(/^\d+$/);;
4040
const emailRegex: RegExp = new RegExp(/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/);
4141

42-
42+
/**
43+
* Generic function that handles the value change of a form input field and revalidates the form input validity.
44+
* @param inputFieldName The name of the input field with a changed value.
45+
* @param event The input field change event.
46+
* @param newValue The new value of the input field after the change.
47+
* @param newValueIsEmpty Whether the new value of the input field is empty.
48+
*/
4349
const handleOnChangeInputFieldValue = (inputFieldName: string, event, newValue, newValueIsEmpty: boolean): void => {
4450
try {
4551
// Set input field error message
@@ -84,6 +90,10 @@ export const UserInputForm: React.FC<IUserInputFormProps> = (
8490
}
8591
}
8692

93+
/**
94+
* Renders a form input field.
95+
* @param formInputField The requested user input object to generate a form input field for.
96+
*/
8797
const renderFormInputField = (formInputField: IRequestedUserInput): JSX.Element => {
8898
try {
8999
switch (formInputField.type) {
@@ -145,6 +155,7 @@ export const UserInputForm: React.FC<IUserInputFormProps> = (
145155
/>
146156
);
147157
case SupportedInputTypes.Choice:
158+
case SupportedInputTypes.Lookup:
148159
return (
149160
<Dropdown
150161
label={formInputField.label}
@@ -203,6 +214,7 @@ export const UserInputForm: React.FC<IUserInputFormProps> = (
203214
resolveDelay={1000} />
204215
);
205216
case SupportedInputTypes.ComboBox:
217+
case SupportedInputTypes.MultiLookup:
206218
return (
207219
<ComboBox
208220
label={formInputField.label}
@@ -237,6 +249,9 @@ export const UserInputForm: React.FC<IUserInputFormProps> = (
237249
}
238250
}
239251

252+
/**
253+
* Parses the form input object state from a Map to a JSON object.
254+
*/
240255
const parseFormInput = (): object => {
241256
try {
242257
const formInputObject: object = {};

SPFx/src/models/IRequestedUserInput.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ export interface IRequestedUserInput {
1212
selectionLimit: number | undefined;
1313
groupName: string | undefined;
1414
options: IDropdownOption[] | undefined;
15+
lookupListName: string | undefined;
16+
lookupDisplayColumn: string | undefined;
1517
}
1618

1719
export enum SupportedInputTypes {
@@ -23,8 +25,15 @@ export enum SupportedInputTypes {
2325
Date = "Date",
2426
PeoplePicker = "People picker",
2527
ComboBox = "Combo box",
28+
Lookup = "Lookup",
29+
MultiLookup = "Multi lookup"
2630
}
2731

32+
/**
33+
* Validates a requested user input object.
34+
* @param requestedUserInput The requested user input object to validate.
35+
* @param triggerConfigTitle The configured title of the trigger.
36+
*/
2837
export const isRequestedUserInputValid = (requestedUserInput: IRequestedUserInput, triggerConfigTitle: string): boolean => {
2938
try {
3039
if (requestedUserInput && !stringIsNullOrEmpty(requestedUserInput?.name) && !stringIsNullOrEmpty(requestedUserInput?.label)
@@ -58,6 +67,10 @@ export const isRequestedUserInputValid = (requestedUserInput: IRequestedUserInpu
5867
case SupportedInputTypes.PeoplePicker:
5968
isValid = !isNaN(requestedUserInput?.selectionLimit) && requestedUserInput?.selectionLimit > 0;
6069
break;
70+
case SupportedInputTypes.Lookup:
71+
case SupportedInputTypes.MultiLookup:
72+
isValid = !stringIsNullOrEmpty(requestedUserInput?.lookupListName) && !stringIsNullOrEmpty(requestedUserInput?.lookupDisplayColumn);
73+
break;
6174
default:
6275
isValid = true;
6376
break;

SPFx/src/models/ITriggerConfig.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ export interface ITriggerConfig {
1414
requestedUserInput: Array<IRequestedUserInput> | undefined;
1515
}
1616

17+
/**
18+
* Validates a trigger configuration object.
19+
* @param triggerConfig The trigger configuration object to validate.
20+
*/
1721
export const isTriggerConfigValid = (triggerConfig: ITriggerConfig): boolean => {
1822
try {
1923
if (triggerConfig && !stringIsNullOrEmpty(triggerConfig?.title) && !stringIsNullOrEmpty(triggerConfig?.triggerUrl)

SPFx/src/services/SPOService.ts

Lines changed: 88 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
/* eslint-disable @microsoft/spfx/no-async-await */
2+
import { IDropdownOption } from "@fluentui/react";
23
import { ServiceKey } from "@microsoft/sp-core-library";
34
import { Logger } from "@pnp/logging";
45
import { SPFI } from "@pnp/sp";
56
import * as AppSettings from "AppSettings";
67
import { getSP } from "../middleware";
7-
import { isTriggerConfigValid, ITriggerConfig } from "../models";
8+
import { IRequestedUserInput, isTriggerConfigValid, ITriggerConfig, SupportedInputTypes } from "../models";
89

910
export interface ISPOService {
1011
getTriggerConfig(): Promise<ITriggerConfig[]>;
@@ -47,39 +48,106 @@ export class SPOService implements ISPOService {
4748
.getByTitle(this._configListTitle)
4849
.items.top(flowLimit)()
4950
.then((response): Promise<ITriggerConfig[]> => {
51+
return Promise.all(response.map(async (triggerConfigListItem) => {
52+
try {
53+
const flowConfig: ITriggerConfig = {
54+
title: triggerConfigListItem?.Title,
55+
triggerUrl: triggerConfigListItem?.TriggerURL,
56+
httpMethod: triggerConfigListItem?.HTTPType,
57+
originSecret: triggerConfigListItem?.OriginSecret,
58+
listWhitelist: triggerConfigListItem?.ListWhitelist,
59+
folderWhitelist: triggerConfigListItem?.FolderWhitelist,
60+
contentTypeBlacklist: triggerConfigListItem?.ContentTypeBlacklist,
61+
fileExtensionBlacklist: triggerConfigListItem?.FileExtensionBlacklist,
62+
selectionLimit: triggerConfigListItem?.SelectionLimit,
63+
requestedUserInput: triggerConfigListItem?.RequestedUserInput ? JSON.parse(triggerConfigListItem?.RequestedUserInput) : undefined
64+
};
65+
66+
if (!isTriggerConfigValid(flowConfig)) {
67+
throw new Error(`Flow configuration for '${flowConfig.title}' is invalid.`);
68+
} else if (flowConfig.requestedUserInput) {
69+
return await this._processRequestedUserInput(flowConfig.requestedUserInput).then((processedRequestedInput: IRequestedUserInput[]) => {
70+
flowConfig.requestedUserInput = processedRequestedInput;
71+
return Promise.resolve(flowConfig);
72+
});
73+
} else {
74+
return Promise.resolve(flowConfig);
75+
}
76+
}
77+
catch (err) {
78+
Logger.error(err);
79+
return Promise.reject();
80+
}
81+
}));
82+
});
83+
} catch (err) {
84+
Logger.error(err);
85+
return Promise.reject(null);
86+
}
87+
}
88+
89+
/**
90+
* Processes the requested user input fields for the flow trigger.
91+
* @param requestedUserInput The requested user input fields for the flow trigger.
92+
*/
93+
private _processRequestedUserInput = async (requestedUserInput: IRequestedUserInput[]): Promise<IRequestedUserInput[]> => {
94+
try {
95+
return await Promise.all(requestedUserInput.map(async (requestedInput: IRequestedUserInput): Promise<IRequestedUserInput> => {
96+
if (requestedInput.type === SupportedInputTypes.Lookup || requestedInput.type === SupportedInputTypes.MultiLookup) {
97+
return await this._getLookupOptions(requestedInput.lookupListName, requestedInput.lookupDisplayColumn).then((options: IDropdownOption[]) => {
98+
const processedRequestedInput: IRequestedUserInput = requestedInput;
99+
processedRequestedInput.options = options;
100+
return Promise.resolve(processedRequestedInput);
101+
});
102+
} else {
103+
return Promise.resolve(requestedInput);
104+
}
105+
}));
106+
} catch (err) {
107+
Logger.error(err);
108+
return Promise.reject(requestedUserInput);
109+
}
110+
}
111+
112+
/**
113+
* Fetches the lookup options for the requested user input for the flow trigger.
114+
* @param list The list to fetch the lookup options from.
115+
* @param displayField The column of the lookup list to display in the option set.
116+
*/
117+
private _getLookupOptions = async (list: string, displayField: string): Promise<IDropdownOption[]> => {
118+
try {
119+
if (!this._sp) {
120+
throw new Error("Context is invalid.");
121+
}
122+
123+
return await this._sp.web.lists
124+
.getByTitle(list)
125+
.select(`Id, ${displayField}`)
126+
.items()
127+
.then((response): Promise<IDropdownOption[]> => {
50128
return new Promise((resolve, reject): void => {
51-
const flowConfigs: ITriggerConfig[] = [];
129+
const lookupOptions: IDropdownOption[] = [];
52130

53-
response.forEach((triggerConfigListItem): void => {
131+
response.forEach((item): void => {
54132
try {
55-
const flowConfig: ITriggerConfig = {
56-
title: triggerConfigListItem?.Title,
57-
triggerUrl: triggerConfigListItem?.TriggerURL,
58-
httpMethod: triggerConfigListItem?.HTTPType,
59-
originSecret: triggerConfigListItem?.OriginSecret,
60-
listWhitelist: triggerConfigListItem?.ListWhitelist,
61-
folderWhitelist: triggerConfigListItem?.FolderWhitelist,
62-
contentTypeBlacklist: triggerConfigListItem?.ContentTypeBlacklist,
63-
fileExtensionBlacklist: triggerConfigListItem?.FileExtensionBlacklist,
64-
selectionLimit: triggerConfigListItem?.SelectionLimit,
65-
requestedUserInput: triggerConfigListItem?.RequestedUserInput ? JSON.parse(triggerConfigListItem?.RequestedUserInput) : undefined
133+
const lookupOption: IDropdownOption = {
134+
key: item.Id,
135+
text: item[displayField].toString()
66136
};
67137

68-
if (!isTriggerConfigValid(flowConfig)) {
69-
throw new Error(`Flow configuration for '${flowConfig.title}' is invalid.`);
70-
} else {
71-
flowConfigs.push(flowConfig);
72-
}
138+
lookupOptions.push(lookupOption);
73139
}
74140
catch (err) {
75141
Logger.error(err);
76142
}
77143
});
78-
resolve(flowConfigs);
144+
resolve(lookupOptions);
79145
});
80146
});
147+
81148
} catch (err) {
82149
Logger.error(err);
150+
console.log(`EnhancedPowerAutomateTrigger -> Could not fetch lookup options for list '${list}'.`);
83151
return null
84152
}
85153
}

resources/request-user-input-example.jsonc

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@
44
"name": "", // Internal name of the input field. Value must be JSON key proof, so spaces or special characters are not allowed! The value of the input field will be passed to the flow in the user.input object with the key of this value
55
"label": "", // Label for input field
66
"placeholder": "", // Placeholder for input field (optional)
7-
"type": "Single line text | Multi line text | Number | Email | Choice | Date | People picker | Combo box", // Input field type
7+
"type": "Single line text | Multi line text | Number | Email | Choice | Date | People picker | Combo box | Lookup | Multi lookup", // Input field type
88
"required": true, // Whether the input field is required
99
"minDate": "", // Only required if type equals date. Minimum allowed date for date input field
1010
"maxDate": "", // Only required if type equals date. Maximum allowed date for date input field
1111
"selectionLimit": 1, // Only required if type equals people picker. Maximum number of people that can be selected for this input field
1212
"groupName": "", // Optional and only used if type equals people picker. Name of the SharePoint group out of which people can be selected in the input field
13-
"options": [{ "key": "", "text": "" }] // Only required if type equals choice or combo box. The choices displayed in the dropdown input field
13+
"options": [{ "key": "", "text": "" }], // Only required if type equals choice or combo box. The choices displayed in the dropdown input field
14+
"lookupListName": "", // Only required if type equals lookup or multi lookup. Name of the SharePoint list out of which items can be selected in the input field
15+
"lookupDisplayColumn": "" // Only required if type equals lookup or multi lookup. Name of the column in the SharePoint list that should be displayed in the input field
1416
},
1517
/* EXAMPLES */
1618
{
@@ -77,6 +79,24 @@
7779
{ "key": "E", "text": "Tag E" }
7880
]
7981
},
82+
{
83+
"name": "customer",
84+
"label": "Customer",
85+
"placeholder": "Select an option",
86+
"type": "Lookup",
87+
"required": false,
88+
"lookupListName": "Customers",
89+
"lookupDisplayColumn": "Name"
90+
},
91+
{
92+
"name": "departments",
93+
"label": "Departments",
94+
"placeholder": "Select one or more options",
95+
"type": "Multi lookup",
96+
"required": false,
97+
"lookupListName": "Departments",
98+
"lookupDisplayColumn": "Title"
99+
},
80100
{
81101
"name": "comments",
82102
"label": "Comments",

0 commit comments

Comments
 (0)