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:
commit
7536d8960e
|
@ -1 +1 @@
|
|||
v18.18.2
|
||||
v18.18.0
|
||||
|
|
|
@ -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`
|
||||
|
|
|
@ -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 |
|
@ -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
|
@ -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"
|
||||
|
|
|
@ -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'
|
||||
}),
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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>}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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"
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue