Merge pull request #4746 from tmaestrini/react-personal-tools-list-MultiSiteHandling

React personal tools list – enhanced web part properties for list handling
This commit is contained in:
Hugo Bernier 2024-03-10 13:22:47 -04:00 committed by GitHub
commit 7536d8960e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 1510 additions and 61 deletions

View File

@ -1 +1 @@
v18.18.2
v18.18.0

View File

@ -9,6 +9,9 @@ This web part has the fundamental functionality - a great starting point to buil
<source src="./assets/video-demo1.mp4" type="video/mp4">
</video>
### Usage
* The user can select from this list what link(s) he/she wants to be displayed for them.
![](./assets/mytoold.png)
@ -19,9 +22,15 @@ This web part has the fundamental functionality - a great starting point to buil
* The tools will be displayed like this:
![](./assets/savedtools.png)
* The web part title can be changed from the property pane, here you can also select to display the tools in two columns (defaults to 1 column if this is not selected)
### Configuration
* The web part title can be changed from the property pane, here you can also select to display the tools in two columns (defaults to 1 column if this is not selected):
![](./assets/settings.png)
* Make sure you set the site that contains the two relevant lists ("Available Tools" and "Personal Tools") and set the reference to the lists accordingly:
![](./assets/settings-siteAndLists.png)
### In the background
The available tools are added to a list to show up in the web part
@ -57,26 +66,28 @@ This sample is optimally compatible with the following environment configuration
## Contributors
* [Eli Schei](https://github.com/Eli-Schei/)
* [Tobias Maestrini](https://github.com/tmaestrini)
## Version history
Version|Date|Comments
-------|----|--------
| 1.0 | February 08, 2024 | Initial release |
| 1.1 | February 25, 2024 | Dynamic lists selection |
## Prerequisites
You need to run the script in the env-setup folder to create the content types and lists used in the code. If you create them manually you might need to change the code to use the correct list and field names.
You need to run the script in the `env-setup` folder to create the content types and lists used in the code. If you create them manually you might need to change the code to use the correct list and field names.
For manual creation here is what you need:
* List named "AvailableTools", needs to have fields "tool_name" and "tool_url" (both text fields);
* List named:"PersonalTools", needs to have fields "tool_username" (text) and "tool_usertools" (note / multi line text field)
* List named "AvailableTools", needs to have fields `tool_name` and `tool_url` (both text fields);
* List named "PersonalTools", needs to have fields `tool_username` (text) and `tool_usertools` (note / multi line text field)
## Minimal path to awesome
* Clone this repository
* Run the script in the "env-setup" folder ([you need pnp-poweshell to run this](https://pnp.github.io/powershell/))
* Run the script `CreateLists.ps1` in the [`env-setup` folder](./src/webparts/myTools/env-setup/CreateLists.ps1) ([you need pnp-powershell to run this](https://pnp.github.io/powershell/))
* In your CLI navigate to the solution folder (the folder where this readme file lives)
* in the command-line run:
* `npm install`

View File

@ -10,7 +10,7 @@
"This web part has the fundamental functionality - a great starting point to build upon if you need something more advanced."
],
"creationDateTime": "2024-02-08",
"updateDateTime": "2024-02-08",
"updateDateTime": "2024-02-25",
"products": [
"SharePoint"
],

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

View File

@ -13,6 +13,7 @@
},
"externals": {},
"localizedResources": {
"MyToolsWebPartStrings": "lib/webparts/myTools/loc/{locale}.js"
"MyToolsWebPartStrings": "lib/webparts/myTools/loc/{locale}.js",
"PropertyControlStrings": "node_modules/@pnp/spfx-property-controls/lib/loc/{locale}.js"
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -26,6 +26,7 @@
"@pnp/graph": "^3.22.0",
"@pnp/logging": "^3.22.0",
"@pnp/sp": "^3.22.0",
"@pnp/spfx-property-controls": "^3.16.0",
"react": "17.0.1",
"react-dom": "17.0.1",
"tslib": "2.3.1"

View File

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
@ -11,10 +12,13 @@ import { IReadonlyTheme } from '@microsoft/sp-component-base';
import * as strings from 'MyToolsWebPartStrings';
import MyTools from './components/MyTools';
import { IMyToolsProps } from './models';
import { IPropertyFieldSite, PropertyFieldListPicker, PropertyFieldListPickerOrderBy, PropertyFieldSitePicker } from '@pnp/spfx-property-controls';
export interface IMyToolsWebPartProps {
wpTitle: string;
wpSites: IPropertyFieldSite[];
wpLists: { personalToolsList: { id: string, title: string, url: string }, availableToolsList: { id: string, title: string, url: string } };
twoColumns: boolean;
}
@ -28,6 +32,8 @@ export default class MyToolsWebPart extends BaseClientSideWebPart<IMyToolsWebPar
MyTools,
{
wpTitle: this.properties.wpTitle,
wpSite: (this.properties.wpSites?.length > 0) ? this.properties.wpSites[0] : undefined,
wpLists: this.properties.wpLists,
isDarkTheme: this._isDarkTheme,
context: this.context,
environmentMessage: this._environmentMessage,
@ -46,8 +52,6 @@ export default class MyToolsWebPart extends BaseClientSideWebPart<IMyToolsWebPar
});
}
private _getEnvironmentMessage(): Promise<string> {
if (!!this.context.sdks.microsoftTeams) { // running in Teams, office.com or Outlook
return this.context.sdks.microsoftTeams.teamsJs.app.getContext()
@ -115,14 +119,61 @@ export default class MyToolsWebPart extends BaseClientSideWebPart<IMyToolsWebPar
PropertyPaneTextField('wpTitle', {
label: "Title",
description:
"If this is not set the title will be shown as 'My tools'",
"If this is not set the title will be shown as 'My tools'",
}),
PropertyPaneCheckbox('twoColumns', {
checked: false,
disabled: false,
text: "Show links in two columns? (Defaults to 1column if this is not checked)"
text: "Show links in two columns? (defaults to 1 column if this is not checked)"
})
]
}, {
groupName: "Lists settings",
groupFields: [
PropertyFieldSitePicker('wpSites', {
label: 'Select site that contains the tools lists',
// initialSites: this.properties.wpSites?.length > 0 ? this.properties.wpSites : [{ url: this.context.pageContext.web.serverRelativeUrl, title: this.context.pageContext.web.title }],
initialSites: this.properties.wpSites,
context: this.context as any,
deferredValidationTime: 500,
multiSelect: false,
onPropertyChange: this.onPropertyPaneFieldChanged,
properties: this.properties,
key: 'wpSites'
}),
PropertyFieldListPicker('wpLists.personalToolsList', {
label: "Select the 'Personal tools' list",
selectedList: this.properties.wpLists?.personalToolsList,
includeHidden: false,
baseTemplate: 100,
orderBy: PropertyFieldListPickerOrderBy.Title,
includeListTitleAndUrl: true,
disabled: (this.properties.wpSites && this.properties.wpSites.length > 0) ? false : true,
onPropertyChange: this.onPropertyPaneFieldChanged.bind(this),
properties: this.properties,
context: this.context as any,
multiSelect: false,
webAbsoluteUrl: (this.properties.wpSites && this.properties.wpSites.length > 0) ? this.properties.wpSites[0].url : this.context.pageContext.web.absoluteUrl,
deferredValidationTime: 0,
key: 'wpLists.personalToolsList'
}),
PropertyFieldListPicker('wpLists.availableToolsList', {
label: "Select the 'Available tools' list",
selectedList: this.properties.wpLists?.availableToolsList,
includeHidden: false,
baseTemplate: 100,
orderBy: PropertyFieldListPickerOrderBy.Title,
includeListTitleAndUrl: true,
disabled: (this.properties.wpSites && this.properties.wpSites.length > 0) ? false : true,
onPropertyChange: this.onPropertyPaneFieldChanged.bind(this),
properties: this.properties,
context: this.context as any,
multiSelect: false,
webAbsoluteUrl: (this.properties.wpSites && this.properties.wpSites.length > 0) ? this.properties.wpSites[0].url : this.context.pageContext.web.absoluteUrl,
deferredValidationTime: 0,
key: 'wpLists.availableToolsList'
}),
]
}
]
}

View File

@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-floating-promises */
import * as React from "react";
import styles from "../styles/PersonalToolsListWebpart.module.scss";
import type {IMyToolsProps, ITool } from "../models";
import type { IMyToolsProps, ITool } from "../models";
import DialogTitle from "@mui/material/DialogTitle";
import DialogContent from "@mui/material/DialogContent";
import Button from "@mui/material/Button";
@ -14,7 +14,7 @@ import Dialog from "@mui/material/Dialog";
import { getSelectableTools, getUsersTools, updateUsersTools } from "../data/apiHelper";
const MyTools: React.FC<
IMyToolsProps
IMyToolsProps
> = (props) => {
/** === USE STATE HOOKS === */
const [open, setOpen] = React.useState(false);
@ -33,7 +33,25 @@ IMyToolsProps
/** === USE EFFECT HOOKS === */
React.useEffect(() => {
(async () => {
const tmpTools = await getUsersTools(props.context, props.userEmail);
await initListData();
})();
}, [props]);
React.useEffect(() => {
if (myTools.length > 0 && errorMessage) {
setErrorMessage(undefined);
}
if (myTools.length === 0) {
setErrorMessage(
errorMsgNotFound
);
}
}, [myTools]);
/** === FUNCTIONS === */
async function initListData(): Promise<void> {
if (props.wpLists?.personalToolsList && props.wpSite?.url) {
const tmpTools = await getUsersTools(props.context, props.userEmail, { list: props.wpLists.personalToolsList, siteUrl: props.wpSite.url });
if (tmpTools) {
setMyTools(tmpTools);
} else {
@ -41,28 +59,19 @@ IMyToolsProps
errorMsgNotFound
);
}
const tmpSelectTools = await getSelectableTools(props.context);
}
if (props.wpLists?.availableToolsList && props.wpSite?.url) {
const tmpSelectTools = await getSelectableTools(props.context, { list: props.wpLists.availableToolsList, siteUrl: props.wpSite.url });
if (tmpSelectTools) {
setSelectableTools(tmpSelectTools);
}
})();
}, []);
React.useEffect(() => {
if (myTools.length > 0 && errorMessage) {
setErrorMessage(undefined);
}
if(myTools.length === 0){
setErrorMessage(
errorMsgNotFound
);
}
}, [myTools]);
}
/** === FUNCTIONS === */
const handleClickOpen = (): void => {
setOpen(true);
};
const handleClose = (): void => {
setOpen(false);
};
@ -73,12 +82,13 @@ IMyToolsProps
const updateSucess = await updateUsersTools(
props.context,
checked,
props.userEmail
props.userEmail,
{ list: props.wpLists?.personalToolsList, siteUrl: props.wpSite?.url }
);
if (updateSucess) {
const tmpTools = await getUsersTools(props.context, props.userEmail);
if (tmpTools) {
setMyTools(tmpTools);
const userTools = await getUsersTools(props.context, props.userEmail, { list: props.wpLists?.personalToolsList, siteUrl: props.wpSite?.url });
if (userTools) {
setMyTools(userTools);
} else {
setErrorMessage(
errorMsgNotFound
@ -95,9 +105,8 @@ IMyToolsProps
/** === TSX === */
return (
<section
className={`${styles.personalToolsListWebpart} ${
props.hasTeamsContext ? styles.teams : ""
}`}
className={`${styles.personalToolsListWebpart} ${props.hasTeamsContext ? styles.teams : ""
}`}
>
<Grid style={{ width: "100%", borderBottom: "1px solid #333" }} container>
<Grid item xs={12} md={8}>

View File

@ -53,18 +53,18 @@ const SelectToolList: React.FC<ISelectToolList> = (props) => {
return (
<>
<List sx={{ width: "100%", maxWidth: 360, bgcolor: "background.paper" }}>
{tools.length > 0 ? tools : "Fant ingen verktøy. Kontakt support."}
{tools.length > 0 ? tools : "No tools found. Please contact support."}
</List>
<DialogActions>
{tools.length > 0 && <DialogActions>
<Button
autoFocus
onClick={() => {
props.handleSave(checked);
}}
>
Save changes
Save changes
</Button>
</DialogActions>
</DialogActions>}
</>
);
};

View File

@ -1,16 +1,29 @@
import { WebPartContext } from "@microsoft/sp-webpart-base";
import { SPFx, spfi } from "@pnp/sp";
import { ITool } from "../models";
import { getSP } from "./pnpjs-config";
import { IWeb, Web } from '@pnp/sp/presets/all';
type ListDefintion = {
siteUrl?: string;
list?: { id: string, title: string, url: string };
}
const getSourceWeb = async (context: WebPartContext, siteUrl: string): Promise<IWeb> => {
const sp = getSP(context);
const { WebFullUrl } = await sp.web.getContextInfo(siteUrl);
const sourceWeb = Web([sp.web, decodeURI(WebFullUrl)]);
return sourceWeb;
}
export const getUsersTools = async (
context: WebPartContext,
currentUserMail: string
currentUserMail: string,
personalToolsList: ListDefintion,
): Promise<Array<ITool> | undefined> => {
const sp = getSP(context);
const requestRes = await sp.web.lists
.getByTitle("PersonalTools")
.items();
const sourceWeb = await getSourceWeb(context, personalToolsList?.siteUrl ?? '');
const sourceList = sourceWeb.lists.getById(personalToolsList?.list?.id ?? '');
const requestRes = await sourceList.items();
const userTools = requestRes.filter(
(userTools) => userTools.tool_username === currentUserMail
);
@ -24,10 +37,13 @@ export const getUsersTools = async (
};
export const getSelectableTools = async (
context: WebPartContext
context: WebPartContext,
availableToolsList: ListDefintion,
): Promise<Array<ITool>> => {
const sp = spfi().using(SPFx(context));
const requestRes = await sp.web.lists.getByTitle("AvailableTools").items();
const sourceWeb = await getSourceWeb(context, availableToolsList?.siteUrl ?? '');
const sourceList = sourceWeb.lists.getById(availableToolsList?.list?.id ?? '');
const requestRes = await sourceList.items();
const tools = requestRes.map((tool) => {
return {
toolName: tool.tool_name,
@ -41,13 +57,13 @@ export const getSelectableTools = async (
export const updateUsersTools = async (
context: WebPartContext,
userTools: Array<ITool>,
currentUserMail: string
currentUserMail: string,
personalToolsList?: ListDefintion,
): Promise<boolean> => {
const sourceWeb = await getSourceWeb(context, personalToolsList?.siteUrl ?? '');
const sourceList = sourceWeb.lists.getById(personalToolsList?.list?.id ?? '');
const requestRes = await sourceList.items();
const sp = spfi().using(SPFx(context));
const requestRes = await sp.web.lists
.getByTitle("PersonalTools")
.items();
const tmpTools = requestRes.filter(
(userTools) => userTools.tool_username === currentUserMail
);
@ -59,8 +75,7 @@ export const updateUsersTools = async (
tool_username: currentUserMail,
};
if (tmpTools.length === 1) {
const update = await sp.web.lists
.getByTitle("PersonalTools")
const update = await sourceList
.items.getById(tmpTools[0].ID)
.update(userToolsObject)
.then((res) => {
@ -72,8 +87,7 @@ export const updateUsersTools = async (
});
return update;
} else if (tmpTools.length === 0) {
const addItem = await sp.web.lists
.getByTitle("PersonalTools")
const addItem = await sourceList
.items.add(userToolsObject)
.then((res) => {
return true;
@ -82,7 +96,7 @@ export const updateUsersTools = async (
console.log(error);
return false;
});
return addItem;
return addItem;
}
return false;
};

View File

@ -27,8 +27,10 @@ Add-PnPFieldToContentType -Field "tool_usertools" -ContentType "PersonalTools"
#Create list and add CT
New-PnPList -Title "AvailableTools" -Url "lists/availabletools" -Template GenericList
Add-PnPContentTypeToList -List "AvailableTools" -ContentType "ToolItem"
Remove-PnPContentTypeFromList -List "AvailableTools" -ContentType "Item"
#Create list and add CT
New-PnPList -Title "PersonalTools" -Url "lists/personaltools" -Template GenericList
Add-PnPContentTypeToList -List "PersonalTools" -ContentType "PersonalTools"
Add-PnPContentTypeToList -List "PersonalTools" -ContentType "PersonalTools"
Remove-PnPContentTypeFromList -List "PersonalTools" -ContentType "Item"

View File

@ -1,7 +1,10 @@
import { WebPartContext } from "@microsoft/sp-webpart-base";
import { IPropertyFieldSite } from "@pnp/spfx-property-controls";
export interface IMyToolsProps {
wpTitle: string;
wpSite?: IPropertyFieldSite;
wpLists?: { personalToolsList: { id: string, title: string, url: string }, availableToolsList: { id: string, title: string, url: string } };
isDarkTheme: boolean;
context: WebPartContext;
environmentMessage: string;