diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 5c568ff57..67b987f8d 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,5 +1,5 @@ # Contribute to SharePoint developer documentation -Below instructions explain how you can prepare your environment to contribute to SharePoint Developer Documentation. +These instructions explain how you can prepare your environment to contribute to SharePoint Developer Documentation. ## Docs repo structure @@ -7,8 +7,10 @@ Before you decide to contribute, it is important to understand the `sp-dev-docs` ``` sp-dev-docs ---images +--assets --docs +--general-development +--images ``` The `docs` folder contains the key developer topics: @@ -30,20 +32,20 @@ The `spfx` folder contains documentation for SharePoint Framework. It is further ``` docs --spfx ----webparts +---web-parts -----basics -----get-started ------developer-guide +-----guidance ``` #### Component folder Every component folder contains the following folders: * basics - * `basics` folder contains concept docs that are helpful in building that particular component, for example: `web parts`. + * `basics` folder contains concept docs that are helpful in building that particular component, for example: `web-parts`. * get-started - * `get-started` folder contains walkthroughs and tutorials on how to get started building that particular component, for example: `web parts`. -* developer-guide - * `developer-guide` folder contains guides, best practices and reference implementations for that particular component, for example: `web parts`. + * `get-started` folder contains walkthroughs and tutorials on how to get started building that particular component, for example: `web-parts`. +* guidance + * `guidance` folder contains guides, best practices and reference implementations for that particular component, for example: `web-parts`. * Any images associated with the docs should be uploaded into the `sp-dev-docs\images` folder. @@ -80,9 +82,10 @@ Now that you have forked the docs repository, to sync this forked repository to ![Clone the forked SharePoint developer docs repository](../images/contribute-docs-clone-options.png) -If you have any Git Desktop or any git source control installed, you can click the `Open is Desktop`, else follow the steps below: +If you have any Git Desktop or any git source control installed, you can click the `Open in Desktop` or follow the steps below: -In the Clone with HTTPs section, click to copy the clone URL for the repository. +In the `Clone with HTTPs` section, click ![](../images/contribute-clone-icon.png) + to copy the clone URL for the repository. * Open your favorite console terminal. @@ -105,14 +108,14 @@ remove: Total 10 (delta 1), reused 10 (delta 1) Unpacking objects: 100% (10/10), done. ``` -## Switch to `master` branch +## Switch to `main` branch -In order to add your changes, you will need to do those in the `master` branch. +In order to add your changes, you will need to do those in the `main` branch. -Type the following command in the console to switch to `master` branch: +Type the following command in the console to switch to `main` branch: ``` -git checkout master +git checkout main ``` Now, you can update existing docs or add new docs to the docs repo. @@ -128,20 +131,22 @@ Depending on the doc's intent, you can choose to add your doc into `basics` or ## Submit a pull request -Once you have completed adding your changes, you can submit a pull request. +Once you have completed adding your changes, you can submit a pull request. -Navigate to the forked sp-dev-docs repo in your account. Make sure your current branch is `master` branch. +Navigate to the forked sp-dev-docs repo in your account. Make sure your current branch is `main` branch. -Once you are in the `master` branch, you should see a message to `Compare & pull request` +Once you are in the `main` branch, you should see a message stating `This branch is 1 commit ahead of Sharepoint:main` and next to it will be a `Pull request` link. ![Submit a pull request to sp-dev-docs repo](../images/contribute-docs-submit-pr.png) -This will start a new pull request. Make sure you use the following [template]() to fill in your changes. Make sure you are creating this pull request against the `master` branch. +Click the `Pull request` link to start a new pull request. Make sure you use this [template](PULL_REQUEST_TEMPLATE.md) to fill in your changes. Make sure you are creating this pull request against the `main` branch. + +Once you have all the information, click the `Create pull request` button to submit your pull request. -Once you have all the information, click the `Create pull request` to submit your pull request. +![Create pull request](../images/contribute-docs-create-pr.png) ## Syncing your forked repository to keep it up-to-date with the upstream repository In order to keep your forked sp-dev-docs repo up-to-date with the parent repository, you will need to first [configure a remote that points to upstream repository](https://help.github.com/articles/configuring-a-remote-for-a-fork). -Once you have configured the upstream repository, follow the steps [here](https://help.github.com/articles/configuring-a-remote-for-a-fork) to sync your fork to keep it up-to-date with the upstream repository. +Once you have configured the upstream repository, follow the steps [here](https://help.github.com/en/articles/syncing-a-fork) to sync your fork to keep it up-to-date with the upstream repository. diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index b8b4b8f27..000000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,28 +0,0 @@ -Thank you for reporting an issue or suggesting an enhancement. We appreciate your feedback - to help the team to understand your needs, please complete the below template to ensure we have the necessary details to assist you. - -#### Category -- [ ] Question -- [ ] Typo -- [ ] Bug -- [ ] Additional article idea - -> If you are planning to share a new feature request (enhancement / suggestion), please use SP Dev UserVoice at http://aka.ms/sp-dev-uservoice. - -#### Expected or Desired Behavior -_If you are reporting a bug, please describe the expected behavior. If you are suggesting an enhancement please describe thoroughly the enhancement, how it can be achieved, and expected benefit._ - -#### Observed Behavior -_If you are reporting a bug, please describe the behavior you expected to occur when performing the action. If you are making a suggestion, you can delete this section._ - -#### Steps to Reproduce -_If you are reporting a bug please describe the steps to reproduce the bug in sufficient detail to allow testing. Only way to fix things properly, is to have sufficient details to reproduce it. If you are making a suggestion, you can delete this section._ - -#### Submission Guidelines -_Delete this section after reading_ -- All suggestions or bugs are welcome, please let us know what's on your mind. -- If you are reporting an issue around any of the documents or articles, please ensure that you have clear reference on the specific file or URL, which should be fixed. -- If you have technical questions about the framework, we’ll be monitoring #spfx, #spfx-webparts, and #spfx-tooling on (SharePoint StackExchange)[http://sharepoint.stackexchange.com/]. You can also alternatively submit your question to (SharePoint Developer group)[https://network.office.com/t5/SharePoint-Developer/bd-p/SharePointDev] at Office Network. -- Remember to include sufficient details and context. -- If you have multiple suggestions or bugs please submit them in separate bugs so we can track resolution. - -Thanks for your contribution! Sharing is caring. diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml new file mode 100644 index 000000000..6d3825f0c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -0,0 +1,116 @@ +name: 🐞 Bug or Error Report +description: Submit a bug or error report. +labels: ['Needs: Triage', 'type:bug-suspected'] + +body: +- type: markdown + attributes: + value: | + - [x] Bug + + This is for SharePoint development bugs. If your submission is not about SharePoint development such as out-of-the-box capabilities, SharePoint configuration, please refer to other support options listed on the [new issue chooser page](https://github.com/SharePoint/sp-dev-docs/issues/new/choose). Please provide as much information as possible so we can best address your submission. Thanks! + + - Follow our guidance on [How To Create Good Issues](https://github.com/sharepoint/sp-dev-docs/wiki/How-to-Create-Good-Issues). + - Remember to include sufficient details and context. + - If you have multiple questions, suggestions, or bugs, please submit them in separate issues. + + Please provide the following details about your environment. 🚨 *If this section is ignored, your submission will be flagged as **incomplete** & automatically closed.* + +- type: dropdown + attributes: + label: Target SharePoint environment + options: + - SharePoint Online + - SharePoint Server 2019 (on-premise) + - SharePoint Server 2016 (on-premise) + - other (enter in the "Additional environment details" area below) + validations: + required: true + +- type: dropdown + attributes: + label: What SharePoint development model, framework, SDK or API is this about? + description: | + What tooling, frameworks, SDKs, or official libraries is this related to? Please include the version details in the *"Additional environment details"* field below. + + **This form is only for officially supported Microsoft products**. + + *If your question is about a third-party or another library/SDK/tooling that is not officially supported by Microsoft, please submit your issue to that project's relevant forum.* + + **NOTE**:💥 If you select SharePoint Framework, you must include the following version numbers in the **Additional environment details** section below: 1️⃣ SharePoint Framework & 2️⃣ Node.js (`node -v`). + options: + - 💥 SharePoint Framework + - SharePoint Add-ins + - SharePoint CSOM + - SharePoint REST API + - Site designs & site scripts + - Declarative list formatting + - not applicable + - other (enter in the "Additional environment details" area below) + validations: + required: true + +- type: dropdown + attributes: + label: Developer environment + options: + - Windows + - macOS + - Linux + +- type: checkboxes + attributes: + label: What browser(s) / client(s) have you tested + description: | + Select the browser(s)/clients this submission is relevant to. + + **NOTE**:💥 If you select an item with this icon, you must include the version number of the selection in the **Additional environment details** section below. + options: + - label: 💥 Internet Explorer + - label: 💥 Microsoft Edge + - label: 💥 Google Chrome + - label: 💥 FireFox + - label: 💥 Safari + - label: mobile (iOS/iPadOS) + - label: mobile (Android) + - label: not applicable + - label: other (enter in the "Additional environment details" area below) + +- type: textarea + attributes: + label: Additional environment details + description: Include as much detail about the environment you're targetting. This is required if "other (enter below)" is selected in the previous field. + value: | + - browser version + - SPFx version + - Node.js version + - etc + +- type: markdown + attributes: + value: | + Provide a clear & concise description of what the bug is. Please follow our guidance on [How To Create Good Issues](https://github.com/sharepoint/sp-dev-docs/wiki/How-to-Create-Good-Issues) which explains how to apply formatting, adding references & resources, screenshots, etc. **Do not attach ZIP files** of your code or compiled projects - instead, please publish your code to a public GitHub repo & post a link to it. + +- type: textarea + attributes: + label: Describe the bug / error + validations: + required: true + +- type: textarea + attributes: + label: Steps to reproduce + description: How do you reproduce this? Please provide as much step-by-step detail as possible. + value: | + 1. + 2. + 3. + validations: + required: true + +- type: textarea + attributes: + label: Expected behavior + description: What did you expect to happen when the reproduce steps are followed? + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..419103265 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,11 @@ +blank_issues_enabled: true +contact_links: + - name: Feature Request / Change Request + url: https://aka.ms/sp-dev-uservoice + about: Feature requests should be submitted to the SharePoint Dev UserVoice site. + - name: SharePoint Developer Documentation + url: https://docs.microsoft.com/sharepoint/dev + about: All developer documentation for SharePoint can be found here. + - name: SharePoint TechCommunity + url: https://techcommunity.microsoft.com/t5/SharePoint/ct-p/SharePoint + about: Please submit non-developer questions/reports to the SharePoint TechCommunity site. This includes anything related to administrator, end-user, or user experience capabilities, questions, and errors. diff --git a/.github/ISSUE_TEMPLATE/question.yml b/.github/ISSUE_TEMPLATE/question.yml new file mode 100644 index 000000000..dbb8092af --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.yml @@ -0,0 +1,93 @@ +name: 🤔 Question or generic issue +description: Do you have a question? Or is it something else that doesn't fit one of the links below? Select this option! +labels: 'Needs: Triage' + +body: +- type: markdown + attributes: + value: | + This is for SharePoint development topics. If your submission is now about SharePoint development such as out-of-the-box capabilities, SharePoint configuration, please use refer to other support options listed on the [new issue chooser page](https://github.com/SharePoint/sp-dev-docs/issues/new/choose). Please provide as much information as possible so we can best address your submission. Thanks! + + - Follow our guidance on [How To Create Good Issues](https://github.com/sharepoint/sp-dev-docs/wiki/How-to-Create-Good-Issues). + - Remember to include sufficient details and context. + - If you have multiple questions, suggestions, or bugs, please submit them in separate issues. + + Please provide the following details about your environment. 🚨 *If this section is ignored, your submission will be flagged as **incomplete** & automatically closed.* + +- type: dropdown + attributes: + label: What type of issue is this? + options: + - Question + - Documentation issue / typo + - other + validations: + required: true + +- type: dropdown + attributes: + label: What SharePoint development model, framework, SDK or API is this about? + description: | + What tooling, frameworks, SDKs, or official libraries is this related to? Please include the version details in the *"Additional environment details"* field below. + + **This form is only for officially supported Microsoft products**. + + *If your question is about a third-party or another library/SDK/tooling that is not officially supported by Microsoft, please submit your issue to that project's relevant forum.* + + **NOTE**:💥 If you select SharePoint Framework, you must include the following version numbers in the **Additional environment details** section below: 1️⃣ SharePoint Framework & 2️⃣ Node.js (`node -v`). + options: + - 💥 SharePoint Framework + - SharePoint Add-ins + - SharePoint CSOM + - SharePoint REST API + - Site designs & site scripts + - Declarative list formatting + - not applicable + - other (enter in the "Additional environment details" area below) + validations: + required: true + +- type: dropdown + attributes: + label: Target SharePoint environment + options: + - SharePoint Online + - SharePoint Server 2019 (on-premise) + - SharePoint Server 2016 (on-premise) + - other (enter in the "Additional environment details" area below) + validations: + required: true + +- type: checkboxes + attributes: + label: What browser(s) / client(s) have you tested + description: | + Select the browser(s)/clients this submission is relevant to. + + **NOTE**:💥 If you select an item with this icon, you must include the version number of the selection in the **Additional environment details** section below. + options: + - label: 💥 Internet Explorer + - label: 💥 Microsoft Edge + - label: 💥 Google Chrome + - label: 💥 FireFox + - label: 💥 Safari + - label: mobile (iOS/iPadOS) + - label: mobile (Android) + - label: not applicable + - label: other (enter in the "Additional environment details" area below) + +- type: textarea + attributes: + label: Additional environment details + description: Include as much detail about the environment you're targetting. This is required if "other (enter below)" is selected in the previous field. + value: | + - browser version + - SPFx version + - Node.js version + - etc + +- type: textarea + attributes: + label: Issue description + validations: + required: true diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 1b29cb055..99aeec9af 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,15 +1,30 @@ -| Q | A -| --------------- | --- -| content fix? | no - yes? -| New article? | no - yes? -| Related issues? | fixes #X, partially #Y, mentioned in #Z +## Category -#### What's in this Pull Request? +- [ ] Content fix +- [ ] New article +- [x] Example checked item (*delete this line*) -Please describe the changes in this PR. Sample description or details around bugs which are being fixed. +> **DELETE THIS LINE BEFORE SUBMITTING** | *For the above list, an empty checkbox is [ ] as in [SPACE]. A checked checkbox is [x] with no space between the brackets. Use the `PREVIEW` tab at the top right to preview the rendering before submitting your issue.* +## Related issues -#### Guidance -*You can delete this section when you are submitting the pull request.* -* *Please update this PR information accordingly. We'll use this as part of our release notes in monthly communications.* -* *Please target your PR to 'master' branch. Released documents are in live branch.* \ No newline at end of file +- fixes #issuenumber +- partially #issuenumber +- mentioned in #issuenumber + +> **DELETE THIS LINE BEFORE SUBMITTING** | *If this fixes (aka: closes) or references an issue, please reference it here. This helps maintaining the issue list as it will (1) link the PR to the issue & (2) automatically close the issue when this PR is merged in.* + +## What's in this Pull Request? + +> **DELETE THIS LINE BEFORE SUBMITTING** | *Please describe the changes in this PR. Sample description or details around bugs which are being fixed.* + +## Submission guidelines + +> - **!!IMPORTANT!!** - All submissions must complete the baseline sections included in this template. Ignoring or deleting this template may result in closing the issue with the label **type:incomplete-submission**. +> - Follow our guidance on [How To Create Good Pull Requests](https://github.com/SharePoint/sp-dev-docs/wiki/How-to-Create-Good-Pull-Requests). +> - Target the `main` branch of this repo. +> - When changing a page, ensure you update the `ms.date` front matter wih the current date in the format `MM/DD/YYYY`. +> - Review all build checks and address the automated errors, warnings, and suggestions. +> - *NOTE: The live site is based on the `live` branch. Site owners periodically refresh `live` branch from the `main` branch so merged PRs won't immediately appear on the live site. Please be patient to see your changes appear on the live site.* +> +> **DELETE THIS SECTION BEFORE SUBMITTING** diff --git a/.github/fabricbot.json b/.github/fabricbot.json new file mode 100644 index 000000000..6dcaf2f39 --- /dev/null +++ b/.github/fabricbot.json @@ -0,0 +1,1149 @@ +{ + "version": "1.0", + "tasks": [ + { + "taskType": "trigger", + "capabilityId": "IssueResponder", + "subCapability": "IssueResponder", + "version": "1.0", + "config": { + "conditions": { + "operator": "and", + "operands": [ + { + "name": "isEvent", + "parameters": { + "eventName": "issues" + } + }, + { + "name": "isAction", + "parameters": { + "action": "opened" + } + }, + { + "operator": "not", + "operands": [ + { + "name": "isAssignedToSomeone", + "parameters": {} + } + ] + } + ] + }, + "taskName": "Auto-label incoming issues as Needs Triage", + "actions": [ + { + "name": "addReply", + "parameters": { + "comment": "Thank you for reporting this issue. We will be triaging your incoming issue as soon as possible." + } + }, + { + "name": "addLabels", + "parameters": { + "labels": [ + "Needs: Triage :mag:" + ] + } + } + ] + }, + "id": "DhSdUvTfU" + }, + { + "taskType": "trigger", + "capabilityId": "IssueResponder", + "subCapability": "IssueResponder", + "version": "1.0", + "config": { + "conditions": { + "operator": "and", + "operands": [ + { + "operator": "not", + "operands": [ + { + "name": "isActivitySender", + "parameters": { + "user": "msft-github-bot" + } + } + ] + }, + { + "operator": "not", + "operands": [ + { + "name": "isAction", + "parameters": { + "action": "closed" + } + } + ] + }, + { + "name": "hasLabel", + "parameters": { + "label": "no-recent-activity" + } + } + ] + }, + "taskName": "Remove no recent activity label", + "actions": [ + { + "name": "removeLabel", + "parameters": { + "label": "no-recent-activity" + } + } + ] + }, + "id": "EuTNKsOAX" + }, + { + "taskType": "trigger", + "capabilityId": "IssueResponder", + "subCapability": "IssueResponder", + "version": "1.0", + "config": { + "conditions": { + "operator": "and", + "operands": [ + { + "name": "isEvent", + "parameters": { + "eventName": "issue_comment" + } + }, + { + "name": "isIssue", + "parameters": {} + }, + { + "name": "isActivitySender", + "parameters": { + "user": { + "type": "author" + } + } + }, + { + "name": "hasLabel", + "parameters": { + "label": "Needs: Author Feedback" + } + } + ] + }, + "taskName": "Add needs attention label to issues", + "actions": [ + { + "name": "addLabels", + "parameters": { + "labels": [ + "Needs: Attention :wave:" + ] + } + } + ] + }, + "id": "4g_ssp7c7" + }, + { + "taskType": "trigger", + "capabilityId": "IssueResponder", + "subCapability": "IssueResponder", + "version": "1.0", + "config": { + "conditions": { + "operator": "and", + "operands": [ + { + "name": "isActivitySender", + "parameters": { + "user": { + "type": "author" + } + } + }, + { + "operator": "not", + "operands": [ + { + "name": "isAction", + "parameters": { + "action": "closed" + } + } + ] + }, + { + "name": "hasLabel", + "parameters": { + "label": "Needs: Author Feedback" + } + } + ] + }, + "taskName": "Remove needs author feedback label from issues and pull requests", + "actions": [ + { + "name": "removeLabel", + "parameters": { + "label": "Needs: Author Feedback" + } + } + ] + }, + "id": "LSpcATOkS" + }, + { + "taskType": "scheduled", + "capabilityId": "ScheduledSearch", + "subCapability": "ScheduledSearch", + "version": "1.1", + "id": "R2LaDi6Kz", + "config": { + "taskName": "Closed answered issues in 3 days", + "frequency": [ + { + "weekDay": 0, + "hours": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23 + ] + }, + { + "weekDay": 1, + "hours": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23 + ] + }, + { + "weekDay": 2, + "hours": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23 + ] + }, + { + "weekDay": 3, + "hours": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23 + ] + }, + { + "weekDay": 4, + "hours": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23 + ] + }, + { + "weekDay": 5, + "hours": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23 + ] + }, + { + "weekDay": 6, + "hours": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23 + ] + } + ], + "searchTerms": [ + { + "name": "isIssue", + "parameters": {} + }, + { + "name": "isOpen", + "parameters": {} + }, + { + "name": "hasLabel", + "parameters": { + "label": "status:answered" + } + }, + { + "name": "noActivitySince", + "parameters": { + "days": 3 + } + } + ], + "actions": [ + { + "name": "addReply", + "parameters": { + "comment": "Closing this issue as \"answered\". If you encounter a similar issue(s), please open up a new issue. See our wiki for more details: [Issue-List: Our approach to closed issues](https://github.com/SharePoint/sp-dev-docs/wiki/Issue-List#our-approach-to-closed-issues)" + } + }, + { + "name": "closeIssue", + "parameters": {} + }, + { + "name": "lockIssue", + "parameters": { + "reason": "resolved" + } + } + ] + } + }, + { + "taskType": "scheduled", + "capabilityId": "ScheduledSearch", + "subCapability": "ScheduledSearch", + "version": "1.1", + "id": "ejaaeLe6G", + "config": { + "frequency": [ + { + "weekDay": 0, + "hours": [ + 1, + 5, + 9, + 13, + 17, + 21 + ], + "timezoneOffset": -5 + }, + { + "weekDay": 1, + "hours": [ + 1, + 5, + 9, + 13, + 17, + 21 + ], + "timezoneOffset": -5 + }, + { + "weekDay": 2, + "hours": [ + 1, + 5, + 9, + 13, + 17, + 21 + ], + "timezoneOffset": -5 + }, + { + "weekDay": 3, + "hours": [ + 1, + 5, + 9, + 13, + 17, + 21 + ], + "timezoneOffset": -5 + }, + { + "weekDay": 4, + "hours": [ + 1, + 5, + 9, + 13, + 17, + 21 + ], + "timezoneOffset": -5 + }, + { + "weekDay": 5, + "hours": [ + 1, + 5, + 9, + 13, + 17, + 21 + ], + "timezoneOffset": -5 + }, + { + "weekDay": 6, + "hours": [ + 1, + 5, + 9, + 13, + 17, + 21 + ], + "timezoneOffset": -5 + } + ], + "searchTerms": [ + { + "name": "isIssue", + "parameters": {} + }, + { + "name": "isOpen", + "parameters": {} + }, + { + "name": "hasLabel", + "parameters": { + "label": "Needs: Author Feedback" + } + }, + { + "name": "hasLabel", + "parameters": { + "label": "no-recent-activity" + } + }, + { + "name": "noActivitySince", + "parameters": { + "days": 7 + } + } + ], + "taskName": "Close stale issues", + "actions": [ + { + "name": "closeIssue", + "parameters": {} + }, + { + "name": "addReply", + "parameters": { + "comment": "Closing issue due to no response from the original author. Please refer to our wiki for more details, including how to remediate this action if you feel this was done prematurely or in error: [No response from the original issue author](https://github.com/SharePoint/sp-dev-docs/wiki/Issue-List#no-response-from-the-original-issue-author)" + } + }, + { + "name": "lockIssue", + "parameters": {} + } + ] + }, + "disabled": false + }, + { + "taskType": "trigger", + "capabilityId": "IssueResponder", + "subCapability": "IssuesOnlyResponder", + "version": "1.0", + "id": "JN4EianUp", + "config": { + "conditions": { + "operator": "or", + "operands": [ + { + "name": "labelAdded", + "parameters": { + "label": "type:invalid-not-dev-issue" + } + }, + { + "name": "labelAdded", + "parameters": { + "label": "type:invalid" + } + } + ] + }, + "eventType": "issue", + "eventNames": [ + "issues", + "project_card" + ], + "taskName": "Reply & close issues tagged \"type:invalid-not-dev-issue\"", + "actions": [ + { + "name": "addReply", + "parameters": { + "comment": "Thank you for your submission. As explained in our wiki ([Issue List: What doesn't belong in the issue list](https://github.com/SharePoint/sp-dev-docs/wiki/Issue-List#what-doesnt-belong-in-the-issue-list)), this issue list is for SharePoint developer/development issues. All capability question/discussion questions, or topics related to SharePoint administration & end-user topics should be reported through the support user interface available in the tenant admin settings. You can also have a discussion and ask questions at the [SharePoint TechCommunity](https://techcommunity.microsoft.com/t5/SharePoint/ct-p/SharePoint) forum. You can learn more about this in our wiki: [type:invalid-not-dev-issue](https://github.com/SharePoint/sp-dev-docs/wiki/Issue-List-Labels#typeinvalid-not-dev-issue)" + } + }, + { + "name": "closeIssue", + "parameters": {} + }, + { + "name": "lockIssue", + "parameters": { + "reason": "off-topic" + } + } + ] + } + }, + { + "taskType": "trigger", + "capabilityId": "IssueResponder", + "subCapability": "IssuesOnlyResponder", + "version": "1.0", + "id": "dTcNyMD5a", + "config": { + "conditions": { + "operator": "and", + "operands": [ + { + "name": "labelAdded", + "parameters": { + "label": "type:uservoice-request" + } + } + ] + }, + "eventType": "issue", + "eventNames": [ + "issues", + "project_card" + ], + "actions": [ + { + "name": "addReply", + "parameters": { + "comment": "Thank you for your submission. As explained in our wiki ([Issue List: What doesn't belong in the issue list](https://github.com/SharePoint/sp-dev-docs/wiki/Issue-List#what-doesnt-belong-in-the-issue-list)), all new feature requests and change requests to existing features should be posted to the [SP Dev UserVoice](https://aka.ms/sp-dev-uservoice) site. You can learn more about this in our wiki: [type:uservoice-request](https://github.com/SharePoint/sp-dev-docs/wiki/Issue-List-Labels#typeuservoice-request)" + } + }, + { + "name": "closeIssue", + "parameters": {} + } + ], + "taskName": "Reply & close issues tagged \"type:uservoice-request\"" + } + }, + { + "taskType": "trigger", + "capabilityId": "IssueResponder", + "subCapability": "IssuesOnlyResponder", + "version": "1.0", + "id": "TXAA0OOon", + "config": { + "conditions": { + "operator": "and", + "operands": [ + { + "name": "labelAdded", + "parameters": { + "label": "Needs: Context Detail :question:" + } + } + ] + }, + "eventType": "issue", + "eventNames": [ + "issues", + "project_card" + ], + "taskName": "Reply issues tagged \"Needs: Context Detail\"", + "actions": [ + { + "name": "addReply", + "parameters": { + "comment": "The more context details you can provide, the easier it is to help assist on issues. Any code you can provide and/or screenshots of the issue also help. The easier you can make it to reproduce the issue, the easier and quicker it is for someone to help you. Please refer to [How to Create Good Issues](https://github.com/SharePoint/sp-dev-docs/wiki/How-to-Create-Good-Issues), specifically [How to Create Good Issues: Include context](https://github.com/SharePoint/sp-dev-docs/wiki/How-to-Create-Good-Issues#include-context), in our wiki for more details." + } + }, + { + "name": "addLabel", + "parameters": { + "label": "Needs: Author Feedback" + } + } + ] + } + }, + { + "taskType": "trigger", + "capabilityId": "IssueResponder", + "subCapability": "IssuesOnlyResponder", + "version": "1.0", + "id": "vAHQpj0AT", + "config": { + "conditions": { + "operator": "and", + "operands": [ + { + "name": "labelAdded", + "parameters": { + "label": "status:duplicate" + } + } + ] + }, + "eventType": "issue", + "eventNames": [ + "issues", + "project_card" + ], + "taskName": "Close issues tagged \"status:duplicate\"", + "actions": [ + { + "name": "addReply", + "parameters": { + "comment": "Closing this issue as a dupe. Please refer to our wiki for more details: [Issue List Labels: status:duplicate](https://github.com/SharePoint/sp-dev-docs/wiki/Issue-List-Labels#statusduplicate)" + } + }, + { + "name": "closeIssue", + "parameters": {} + }, + { + "name": "lockIssue", + "parameters": { + "reason": "resolved" + } + } + ] + } + }, + { + "taskType": "scheduled", + "capabilityId": "ScheduledSearch", + "subCapability": "ScheduledSearch", + "version": "1.1", + "id": "Lzyb5Csy_", + "config": { + "frequency": [ + { + "weekDay": 0, + "hours": [ + 0, + 6, + 12, + 18 + ] + }, + { + "weekDay": 1, + "hours": [ + 0, + 6, + 12, + 18 + ] + }, + { + "weekDay": 2, + "hours": [ + 0, + 6, + 12, + 18 + ] + }, + { + "weekDay": 3, + "hours": [ + 0, + 6, + 12, + 18 + ] + }, + { + "weekDay": 4, + "hours": [ + 0, + 6, + 12, + 18 + ] + }, + { + "weekDay": 5, + "hours": [ + 0, + 6, + 12, + 18 + ] + }, + { + "weekDay": 6, + "hours": [ + 0, + 6, + 12, + 18 + ] + } + ], + "searchTerms": [ + { + "name": "isIssue", + "parameters": {} + }, + { + "name": "isClosed", + "parameters": {} + }, + { + "name": "noActivitySince", + "parameters": { + "days": 7 + } + }, + { + "name": "isUnlocked", + "parameters": {} + } + ], + "taskName": "Lock issues if inactive 7d after closing", + "actions": [ + { + "name": "addReply", + "parameters": { + "comment": "Issues that have been closed & had no follow-up activity for at least 7 days are automatically locked. Please refer to our wiki for more details, including how to remediate this action if you feel this was done prematurely or in error: [Issue List: Our approach to locked issues](https://github.com/SharePoint/sp-dev-docs/wiki/Issue-List#our-approach-to-locked-issues)" + } + }, + { + "name": "lockIssue", + "parameters": { + "reason": "resolved" + } + } + ] + } + }, + { + "taskType": "trigger", + "capabilityId": "IssueResponder", + "subCapability": "IssuesOnlyResponder", + "version": "1.0", + "id": "k84udcNf_", + "config": { + "conditions": { + "operator": "and", + "operands": [ + { + "name": "bodyContains", + "parameters": { + "bodyPattern": "Do not edit this section. It is required for docs.microsoft.com ➟ GitHub issue linking." + } + } + ] + }, + "eventType": "issue", + "eventNames": [ + "issues", + "project_card" + ], + "actions": [ + { + "name": "addLabel", + "parameters": { + "label": "area:docs-comment" + } + } + ], + "taskName": "Label new issues created as comment on docs with \"area:docs-comment\"" + } + }, + { + "taskType": "trigger", + "capabilityId": "IssueResponder", + "subCapability": "IssuesOnlyResponder", + "version": "1.0", + "id": "NbeJ2zWgh", + "config": { + "conditions": { + "operator": "and", + "operands": [ + { + "name": "labelAdded", + "parameters": { + "label": "type:incomplete-submission" + } + } + ] + }, + "eventType": "issue", + "eventNames": [ + "issues", + "project_card" + ], + "taskName": "Reply & tag issues tagged with type:incomplete-submission", + "actions": [ + { + "name": "addReply", + "parameters": { + "comment": "Thank you for your submission, but there isn't enough detail in the issue for us to review & move forward. The new issue template includes sections for you to fill out. Please resubmit your issue and complete the provided sections in the new item template so we can move forward on it refer to our wiki for more information: [How to Create Good Issues](https://github.com/SharePoint/sp-dev-docs/wiki/How-to-Create-Good-Issues)" + } + }, + { + "name": "removeLabel", + "parameters": { + "label": "Needs: Triage :mag:" + } + }, + { + "name": "closeIssue", + "parameters": {} + } + ] + } + }, + { + "taskType": "trigger", + "capabilityId": "IssueResponder", + "subCapability": "PullRequestResponder", + "version": "1.0", + "id": "SgmbtMnlk", + "config": { + "conditions": { + "operator": "and", + "operands": [ + { + "name": "labelAdded", + "parameters": { + "label": "type:incomplete-submission" + } + } + ] + }, + "eventType": "pull_request", + "eventNames": [ + "pull_request", + "issues", + "project_card" + ], + "taskName": "Reply & tag PRs tagged with type:incomplete-submission", + "actions": [ + { + "name": "addReply", + "parameters": { + "comment": "Thank you for your submission, but there isn't enough detail in the pull request for us to review & move forward. The new PR template includes sections for you to fill out. Please resubmit your PR and complete the provided sections in the new item template so we can move forward on it refer to our wiki for more information: [How to Create Good Pull Requests]https://github.com/SharePoint/sp-dev-docs/wiki/How-to-Create-Good-Pull-Requests)" + } + }, + { + "name": "closeIssue", + "parameters": {} + } + ] + } + }, + { + "taskType": "scheduled", + "capabilityId": "ScheduledSearch", + "subCapability": "ScheduledSearch", + "version": "1.1", + "id": "ECBdzA7w3Y7R-jcBx0icn", + "config": { + "frequency": [ + { + "weekDay": 0, + "hours": [ + 0, + 6, + 12, + 18 + ] + }, + { + "weekDay": 1, + "hours": [ + 0, + 6, + 12, + 18 + ] + }, + { + "weekDay": 2, + "hours": [ + 0, + 6, + 12, + 18 + ] + }, + { + "weekDay": 3, + "hours": [ + 0, + 6, + 12, + 18 + ] + }, + { + "weekDay": 4, + "hours": [ + 0, + 6, + 12, + 18 + ] + }, + { + "weekDay": 5, + "hours": [ + 0, + 6, + 12, + 18 + ] + }, + { + "weekDay": 6, + "hours": [ + 0, + 6, + 12, + 18 + ] + } + ], + "searchTerms": [ + { + "name": "isIssue", + "parameters": {} + }, + { + "name": "isOpen", + "parameters": {} + }, + { + "name": "hasLabel", + "parameters": { + "label": "Needs: Author Feedback" + } + }, + { + "name": "noActivitySince", + "parameters": { + "days": 7 + } + } + ], + "actions": [ + { + "name": "addLabel", + "parameters": { + "label": "no-recent-activity" + } + }, + { + "name": "addReply", + "parameters": { + "comment": "This issue has been automatically marked as stale because it has marked as requiring author feedback but has not had any activity for **7 days**. It will be closed if no further activity occurs **within the next 7 days of this comment**. Please see our wiki for more information: [Issue List Labels: Needs Author Feedback](https://github.com/SharePoint/sp-dev-docs/wiki/Issue-List-Labels#needs-author-feedback) & [Issue List: No response from the original issue author](https://github.com/SharePoint/sp-dev-docs/wiki/Issue-List#no-response-from-the-original-issue-author)" + } + } + ], + "taskName": "Mark issue with no-recent-activity label if there's no actions in 7 days" + } + }, + { + "taskType": "trigger", + "capabilityId": "IssueResponder", + "subCapability": "IssuesOnlyResponder", + "version": "1.0", + "id": "MdPj2K40N73yDGFnu9REz", + "config": { + "conditions": { + "operator": "and", + "operands": [ + { + "name": "labelAdded", + "parameters": { + "label": "listmaintenance-oldissues" + } + } + ] + }, + "eventType": "issue", + "eventNames": [ + "issues", + "project_card" + ], + "actions": [ + { + "name": "addReply", + "parameters": { + "comment": "This issue is being closed as part of an issue list cleanup project. Issues with no activity in the past 6 months that aren't tracked by engineering as bugs were closed as part of this inititive. If this is still an issue, please [follow the steps outlined to re-open or submit a new issue](https://github.com/sharepoint/sp-dev-docs/wiki/Issue-List#our-approach-to-closed-issues)." + } + }, + { + "name": "closeIssue", + "parameters": {} + } + ], + "taskName": "Close inactive issues based on list maintenance label" + } + } + ], + "userGroups": [] +} diff --git a/.github/label-actions.yml b/.github/label-actions.yml new file mode 100644 index 000000000..80a5bf7aa --- /dev/null +++ b/.github/label-actions.yml @@ -0,0 +1,14 @@ +# Configuration for Label Actions - https://github.com/dessant/label-actions + +# Actions taken when the `type:archive-old-issue` label is added to issues that are being archived. +type:archive-old-issue: + # Post a comment + comment: |+ + Thank you for taking the time to file an issue. We periodically **archive** older or inactive issues as part of our issue management process, which automatically closes them once they are archived. + + If you’d like to understand more about why and how we handle archived (closed) issues, please see [Our approach to closed issues](https://github.com/SharePoint/sp-dev-docs/wiki/Issue-List#our-approach-to-closed-issues). + + We appreciate your contribution and if this is still an active issue with the latest SPFx versions, please do resubmit the details. We needed to perform a cleanup, so that we can start with a **clean table** with a new process. We apologize for the inconvenience this might cause. + + # Close the issue + close: true diff --git a/.github/policies/resourceManagement.yml b/.github/policies/resourceManagement.yml new file mode 100644 index 000000000..43263aa44 --- /dev/null +++ b/.github/policies/resourceManagement.yml @@ -0,0 +1,115 @@ +id: bot-issue-management +name: Issue Management +description: Enable tracking & monitoring of issues +resource: repository +disabled: false +configuration: + resourceManagementConfiguration: + scheduledSearches: + - description: Close answered issues after 3 days of inactivity + frequencies: + - hourly: { hour: 0 } + filters: + - isIssue + - isOpen + - hasLabel: { label: status:answered } + - noActivitySince: { days: 3 } + actions: + - addReply: + reply: > + Closing this issue as "answered". If you encounter a similar issue(s), please open up a new issue. See our wiki for more details: [Issue-List: Our approach to closed issues](https://github.com/SharePoint/sp-dev-docs/wiki/Issue-List#our-approach-to-closed-issues) + - closeIssue + - lockIssue: + reason: resolved + + - description: Close stale issues with no recent author activity after 7 days + frequencies: + - hourly: { hour: 6 } + filters: + - isIssue + - isOpen + - hasLabel: { label: 'Needs: Author Feedback' } + - hasLabel: { label: no-recent-activity } + - noActivitySince: { days: 7 } + actions: + - addReply: + reply: > + Closing issue due to no response from the original author. Please refer to our wiki for more details, including how to remediate this action if you feel this was done prematurely or in error: [No response from the original issue author](https://github.com/SharePoint/sp-dev-docs/wiki/Issue-List#no-response-from-the-original-issue-author) + - closeIssue + - lockIssue + + - description: Mark issues as no recent activity after 7 days + frequencies: + - hourly: { hour: 6 } + filters: + - isIssue + - isOpen + - hasLabel: { label: 'Needs: Author Feedback' } + - noActivitySince: { days: 7 } + actions: + - addLabel: { label: no-recent-activity } + - addReply: + reply: > + This issue has been automatically marked as stale because it has marked as requiring author feedback but has not had any activity for **7 days**. It will be closed if no further activity occurs **within the next 7 days of this comment**. Please see our wiki for more information: [Issue List Labels: Needs Author Feedback](https://github.com/SharePoint/sp-dev-docs/wiki/Issue-List-Labels#needs-author-feedback) & [Issue List: No response from the original issue author](https://github.com/SharePoint/sp-dev-docs/wiki/Issue-List#no-response-from-the-original-issue-author) + + - description: Lock issues inactive 7 days after closing + frequencies: + - hourly: { hour: 6 } + filters: + - isIssue + - isClosed + - noActivitySince: { days: 7 } + - isUnlocked + actions: + - addReply: + reply: > + Issues that have been closed & had no follow-up activity for at least 7 days are automatically locked. Please refer to our wiki for more details, including how to remediate this action if you feel this was done prematurely or in error: [Issue List: Our approach to locked issues](https://github.com/SharePoint/sp-dev-docs/wiki/Issue-List#our-approach-to-locked-issues) + - lockIssue: + reason: resolved + + eventResponderTasks: + - if: + - payloadType: Issues + - isAction: { action: opened } + - not: + isAssignedToSomeone: true + then: + - addReply: + reply: > + Thank you for reporting this issue. We will be triaging your incoming issue as soon as possible. + - addLabel: + label: 'Needs: Triage :mag:' + + - if: + - payloadType: Issue_Comment + - isActivitySender: { issueAuthor: true } + - hasLabel: { label: 'Needs: Author Feedback' } + - isOpen + then: + - addLabel: + label: 'Needs: Attention :wave:' + + - if: + - payloadType: Issues + - isActivitySender: { issueAuthor: true } + - not: + isAction: { action: closed } + - hasLabel: { label: 'Needs: Author Feedback' } + then: + - removeLabel: { label: 'Needs: Author Feedback' } + + - if: + - payloadType: Issues + - not: + isActivitySender: { user: microsoft-github-policy-service } + - not: + isAction: { action: closed } + - hasLabel: { label: no-recent-activity } + then: + - removeLabel: { label: no-recent-activity } + + - if: + - payloadType: Issue_Comment + - hasLabel: { label: no-recent-activity } + then: + - removeLabel: { label: no-recent-activity } diff --git a/.github/workflows/label-actions.yml b/.github/workflows/label-actions.yml new file mode 100644 index 000000000..d2dd0ff59 --- /dev/null +++ b/.github/workflows/label-actions.yml @@ -0,0 +1,18 @@ +name: 'Check for Incomplete Issues' + +on: + issues: + types: [labeled, unlabeled] + +permissions: + issues: write + pull-requests: write + +jobs: + reaction: + runs-on: ubuntu-latest + steps: + - uses: dessant/label-actions@v2 + with: + github-token: ${{ github.token }} + process-only: 'issues' diff --git a/.openpublishing.build.ps1 b/.openpublishing.build.ps1 deleted file mode 100644 index aadef7620..000000000 --- a/.openpublishing.build.ps1 +++ /dev/null @@ -1,17 +0,0 @@ -param( - [string]$buildCorePowershellUrl = "https://opbuildstorageprod.blob.core.windows.net/opps1container/.openpublishing.buildcore.ps1", - [string]$parameters -) -# Main -$errorActionPreference = 'Stop' - -# Step-1: Download buildcore script to local -echo "download build core script to local with source url: $buildCorePowershellUrl" -$repositoryRoot = Split-Path -Parent $MyInvocation.MyCommand.Definition -$buildCorePowershellDestination = "$repositoryRoot\.openpublishing.buildcore.ps1" -Invoke-WebRequest $buildCorePowershellUrl -OutFile "$buildCorePowershellDestination" - -# Step-2: Run build core -echo "run build core script with parameters: $parameters" -& "$buildCorePowershellDestination" "$parameters" -exit $LASTEXITCODE diff --git a/.openpublishing.publish.config.json b/.openpublishing.publish.config.json index dc4791fa2..f3522487f 100644 --- a/.openpublishing.publish.config.json +++ b/.openpublishing.publish.config.json @@ -23,23 +23,48 @@ } ], "notification_subscribers": [ - "vesaj@microsoft.com" + "vesaj@microsoft.com", + "bjansen@microsoft.com" ], + "sync_notification_subscribers": null, "branches_to_filter": [], + "git_repository_url_open_to_public_contributors": "https://github.com/SharePoint/sp-dev-docs", + "git_repository_branch_open_to_public_contributors": "main", "skip_source_output_uploading": false, - "need_preview_pull_request": false, + "need_preview_pull_request": true, "contribution_branch_mappings": {}, - "git_repository_branch_open_to_public_contributors": "master", "dependent_repositories": [ { "path_to_root": "_themes", "url": "https://github.com/Microsoft/templates.docs.msft", - "branch": "master", + "branch": "main", + "branch_mapping": {} + }, + { + "path_to_root": "_themes.pdf", + "url": "https://github.com/Microsoft/templates.docs.msft.pdf", + "branch": "main", + "branch_mapping": {} + }, + { + "path_to_root": "sp-dev-modernization", + "url": "https://github.com/SharePoint/sp-dev-modernization.git", + "branch": "dev", "branch_mapping": {} } ], - "branch_target_mapping": {}, - "need_generate_pdf_url_template": false, + "branch_target_mapping": { + "live": [ + "Publish", + "Pdf" + ] + }, + "need_generate_pdf_url_template": true, + "Targets": { + "Pdf": { + "template_folder": "_themes.pdf" + } + }, "JoinTOCPlugin": [ { "ConceptualTOC": "/docs/toc.yml", diff --git a/.openpublishing.redirection.json b/.openpublishing.redirection.json new file mode 100644 index 000000000..78222f726 --- /dev/null +++ b/.openpublishing.redirection.json @@ -0,0 +1,274 @@ +{ + "redirections": [ + { + "source_path": "docs/apis/rest/complete-basic-operations-using-sharepoint-rest-endpoints.md", + "redirect_url": "/sharepoint/dev/sp-add-ins/complete-basic-operations-using-sharepoint-rest-endpoints", + "redirect_document_id": false + }, + { + "source_path": "docs/apis/rest/determine-sharepoint-rest-service-endpoint-uris.md", + "redirect_url": "/sharepoint/dev/sp-add-ins/determine-sharepoint-rest-service-endpoint-uris", + "redirect_document_id": false + }, + { + "source_path": "docs/apis/rest/get-to-know-the-sharepoint-rest-service.md", + "redirect_url": "/sharepoint/dev/sp-add-ins/get-to-know-the-sharepoint-rest-service", + "redirect_document_id": false + }, + { + "source_path": "docs/apis/rest/make-batch-requests-with-the-rest-apis.md", + "redirect_url": "/sharepoint/dev/sp-add-ins/make-batch-requests-with-the-rest-apis", + "redirect_document_id": false + }, + { + "source_path": "docs/apis/rest/navigate-the-sharepoint-data-structure-represented-in-the-rest-service.md", + "redirect_url": "/sharepoint/dev/sp-add-ins/navigate-the-sharepoint-data-structure-represented-in-the-rest-service", + "redirect_document_id": false + }, + { + "source_path": "docs/apis/rest/set-custom-permissions-on-a-list-by-using-the-rest-interface.md", + "redirect_url": "/sharepoint/dev/sp-add-ins/set-custom-permissions-on-a-list-by-using-the-rest-interface", + "redirect_document_id": false + }, + { + "source_path": "docs/apis/rest/synchronize-sharepoint-items-using-the-rest-service.md", + "redirect_url": "/sharepoint/dev/sp-add-ins/synchronize-sharepoint-items-using-the-rest-service", + "redirect_document_id": false + }, + { + "source_path": "docs/apis/rest/upload-a-file-by-using-the-rest-api-and-jquery.md", + "redirect_url": "/sharepoint/dev/sp-add-ins/upload-a-file-by-using-the-rest-api-and-jquery", + "redirect_document_id": false + }, + { + "source_path": "docs/apis/rest/use-odata-query-operations-in-sharepoint-rest-requests.md", + "redirect_url": "/sharepoint/dev/sp-add-ins/use-odata-query-operations-in-sharepoint-rest-requests", + "redirect_document_id": false + }, + { + "source_path": "docs/apis/rest/working-with-folders-and-files-with-rest.md", + "redirect_url": "/sharepoint/dev/sp-add-ins/working-with-folders-and-files-with-rest", + "redirect_document_id": false + }, + { + "source_path": "docs/apis/rest/working-with-lists-and-list-items-with-rest.md", + "redirect_url": "/sharepoint/dev/sp-add-ins/working-with-lists-and-list-items-with-rest", + "redirect_document_id": false + }, + { + "source_path": "docs/schema/index.md", + "redirect_url": "/sharepoint/dev/schema/upgrade-definition-schema", + "redirect_document_id": false + }, + { + "source_path": "docs/sp-add-ins/add-a-custom-content-type-to-a-sharepoint-hostedsharepoint-add-in.md", + "redirect_url": "/sharepoint/dev/sp-add-ins/add-a-custom-content-type-to-a-sharepoint-hosted-sharepoint-add-in", + "redirect_document_id": false + }, + { + "source_path": "docs/sp-add-ins/add-custom-columns-to-a-sharepoint-hostedsharepoint-add-in.md", + "redirect_url": "/sharepoint/dev/sp-add-ins/add-custom-columns-to-a-sharepoint-hosted-sharepoint-add-in", + "redirect_document_id": false + }, + { + "source_path": "docs/spfx/web-parts/guidance/creating-team-manifest-manually-for-webpart.md", + "redirect_url": "/sharepoint/dev/spfx/deployment-spfx-teams-solutions", + "redirect_document_id": false + }, + { + "source_path": "docs/general-development/office-365-cdn.md", + "redirect_url": "/sharepoint/dev/office365/enterprise/use-office-365-cdn-with-spo", + "redirect_document_id": false + }, + { + "source_path": "docs/spfx/toolchain/scaffolding-projects-using-yeoman-sharepoint-generator.md", + "redirect_url": "/sharepoint/dev/spfx/yeoman-generator-for-spfx-intro", + "redirect_document_id": false + }, + { + "source_path": "docs/spfx/office-addins-create.md", + "redirect_url": "/sharepoint/dev/spfx/release-1.14", + "redirect_document_id": false + }, + { + "source_path": "docs/spfx/web-parts/get-started/office-addins-tutorial.md", + "redirect_url": "/sharepoint/dev/spfx/release-1.14", + "redirect_document_id": false + }, + { + "source_path": "docs/spfx/sharepoint-2019-support.md", + "redirect_url": "/sharepoint/dev/spfx/sharepoint-2019-and-subscription-edition-support", + "redirect_document_id": false + }, + { + "source_path": "docs/embedded/mslearn/m01-01-intro.md", + "redirect_url": "/training/modules/sharepoint-embedded-setup/", + "redirect_document_id": false + }, + { + "source_path": "docs/embedded/mslearn/m02-01-intro.md", + "redirect_url": "/training/modules/sharepoint-embedded-create-app/", + "redirect_document_id": false + }, + { + "source_path": "docs/embedded/adoptions-and-use.md", + "redirect_url": "/sharepoint/dev/embedded/scenarios-and-use-cases", + "redirect_document_id": false + }, + { + "source_path": "docs/embedded/concepts/admin-exp/billing.md", + "redirect_url": "/sharepoint/dev/embedded/concepts/admin-exp/billing/billing", + "redirect_document_id": false + }, + { + "source_path": "docs/embedded/concepts/admin-exp/cta.md", + "redirect_url": "/sharepoint/dev/embedded/concepts/admin-exp/consuming-tenant-admin/cta", + "redirect_document_id": false + }, + { + "source_path": "docs/embedded/concepts/admin-exp/dev-admin.md", + "redirect_url": "/sharepoint/dev/embedded/concepts/admin-exp/developer-admin/dev-admin", + "redirect_document_id": false + }, + { + "source_path": "docs/embedded/concepts/app-concepts/terms-and-def.md", + "redirect_url": "/sharepoint/dev/embedded/overview", + "redirect_document_id": false + }, + { + "source_path": "docs/embedded/getting-started/enable-sharepoint-embedded.md", + "redirect_url": "/sharepoint/dev/embedded/overview", + "redirect_document_id": false + }, + { + "source_path": "docs/embedded/terms-of-service.md", + "redirect_url": "/sharepoint/dev/embedded/overview", + "redirect_document_id": false + }, + { + "source_path": "docs/embedded/concepts/admin-exp/adminrole.md", + "redirect_url": "/sharepoint/dev/embedded/administration/adminrole", + "redirect_document_id": false + }, + { + "source_path": "docs/embedded/concepts/admin-exp/billing/billing.md", + "redirect_url": "/sharepoint/dev/embedded/administration/billing/billing", + "redirect_document_id": false + }, + { + "source_path": "docs/embedded/concepts/admin-exp/billing/billingmanagement.md", + "redirect_url": "/sharepoint/dev/embedded/administration/billing/billingmanagement", + "redirect_document_id": false + }, + { + "source_path": "docs/embedded/concepts/admin-exp/billing/meters.md", + "redirect_url": "/sharepoint/dev/embedded/administration/billing/meters", + "redirect_document_id": false + }, + { + "source_path": "docs/embedded/concepts/admin-exp/consuming-tenant-admin/cta.md", + "redirect_url": "/sharepoint/dev/embedded/administration/consuming-tenant-admin/cta", + "redirect_document_id": false + }, + { + "source_path": "docs/embedded/concepts/admin-exp/consuming-tenant-admin/ctaUX.md", + "redirect_url": "/sharepoint/dev/embedded/administration/consuming-tenant-admin/ctaUX", + "redirect_document_id": false + }, + { + "source_path": "docs/embedded/concepts/admin-exp/consuming-tenant-admin/ctapowershell.md ", + "redirect_url": "/sharepoint/dev/embedded/administration/consuming-tenant-admin/ctapowershell", + "redirect_document_id": false + }, + { + "source_path": "docs/embedded/concepts/admin-exp/developer-admin/dev-admin.md", + "redirect_url": "/sharepoint/dev/embedded/administration/developer-admin/dev-admin", + "redirect_document_id": false + }, + { + "source_path": "docs/embedded/concepts/security-and-compliance.md", + "redirect_url": "/sharepoint/dev/embedded/compliance/security-and-compliance", + "redirect_document_id": false + }, + { + "source_path": "docs/embedded/concepts/app-concepts/app-architecture.md", + "redirect_url": "/sharepoint/dev/embedded/development/app-architecture", + "redirect_document_id": false + }, + { + "source_path": "docs/embedded/concepts/app-concepts/auth.md", + "redirect_url": "/sharepoint/dev/embedded/development/auth", + "redirect_document_id": false + }, + { + "source_path": "docs/embedded/concepts/content-experiences/office-experience.md", + "redirect_url": "/sharepoint/dev/embedded/development/content-experiences/office-experience", + "redirect_document_id": false + }, + { + "source_path": "docs/embedded/concepts/content-experiences/search-content.md", + "redirect_url": "/sharepoint/dev/embedded/development/content-experiences/search-content", + "redirect_document_id": false + }, + { + "source_path": "docs/embedded/concepts/content-experiences/user-experiences-overview.md", + "redirect_url": "/sharepoint/dev/embedded/development/content-experiences/user-experiences-overview", + "redirect_document_id": false + }, + { + "source_path": "docs/embedded/concepts/fluid.md", + "redirect_url": "/sharepoint/dev/embedded/development/fluid", + "redirect_document_id": false + }, + { + "source_path": "docs/embedded/concepts/app-concepts/limits-calling.md", + "redirect_url": "/sharepoint/dev/embedded/development/limits-calling", + "redirect_document_id": false + }, + { + "source_path": "docs/embedded/concepts/app-concepts/sharing-and-perm.md", + "redirect_url": "/sharepoint/dev/embedded/development/sharing-and-perm", + "redirect_document_id": false + }, + { + "source_path": "docs/embedded/tutorials/doc-processing-acs.md", + "redirect_url": "/sharepoint/dev/embedded/development/tutorials/doc-processing-acs", + "redirect_document_id": false + }, + { + "source_path": "docs/embedded/tutorials/launch-experience.md", + "redirect_url": "/sharepoint/dev/embedded/development/tutorials/launch-experience", + "redirect_document_id": false + }, + { + "source_path": "docs/embedded/tutorials/metadata.md", + "redirect_url": "/sharepoint/dev/embedded/development/tutorials/metadata", + "redirect_document_id": false + }, + { + "source_path": "docs/embedded/tutorials/migrate-abs-to-spe.md", + "redirect_url": "/sharepoint/dev/embedded/development/tutorials/migrate-abs-to-spe", + "redirect_document_id": false + }, + { + "source_path": "docs/embedded/tutorials/using-file-preview.md", + "redirect_url": "/sharepoint/dev/embedded/development/tutorials/using-file-preview", + "redirect_document_id": false + }, + { + "source_path": "docs/embedded/tutorials/using-webhooks.md", + "redirect_url": "/sharepoint/dev/embedded/development/tutorials/using-webhooks", + "redirect_document_id": false + }, + { + "source_path": "docs/embedded/concepts/app-concepts/containertypes.md", + "redirect_url": "/sharepoint/dev/embedded/getting-started/containertypes", + "redirect_document_id": false + }, + { + "source_path": "docs/embedded/concepts/app-concepts/register-api-documentation.md", + "redirect_url": "/sharepoint/dev/embedded/getting-started/register-api-documentation", + "redirect_document_id": false + } + ] +} diff --git a/.vs/ProjectSettings.json b/.vs/ProjectSettings.json new file mode 100644 index 000000000..f8b488856 --- /dev/null +++ b/.vs/ProjectSettings.json @@ -0,0 +1,3 @@ +{ + "CurrentProjectSetting": null +} \ No newline at end of file diff --git a/.vs/VSWorkspaceState.json b/.vs/VSWorkspaceState.json new file mode 100644 index 000000000..6b6114114 --- /dev/null +++ b/.vs/VSWorkspaceState.json @@ -0,0 +1,6 @@ +{ + "ExpandedNodes": [ + "" + ], + "PreviewInSolutionExplorer": false +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..ed9462b7e --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "markdownlint.config": { + "MD028": false, + "MD025": { + "front_matter_title": "" + } + } +} \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md index d5b4108b7..cc73ce409 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -4,6 +4,6 @@ Copyright (c) Microsoft Corporation. Distributed under the following terms: 1. Microsoft and any contributors to this project each grants you a license, under its respective copyrights, to the Microsoft SharePoint Documentation under the [Creative Commons Attribution-NonCommercial-NoDerivs 3.0 United States License](https://creativecommons.org/licenses/by-nc-nd/3.0/us/legalcode). In addition, with respect to any sample code contained in the documentation, Microsoft and any such contributors grants you an additional license, under its respective intellectual property rights, to use the code to develop or design your software for Microsoft SharePoint and/or Office 365. -2. Microsoft, Microsoft Office, Microsoft SharePoint, Office 365, Windows, Windows Azure and/or other Microsoft products and services referenced in the documentation may be either trademarks or registered trademarks of Microsoft in the United States and/or other countries. This license does not grant you rights to use any names, logos, or trademarks. For Microsoft’s general trademark guidelines, go to [1](http://go.microsoft.com/fwlink/?LinkID=254653). +2. Microsoft, Microsoft Office, Microsoft SharePoint, Office 365, Windows, Windows Azure and/or other Microsoft products and services referenced in the documentation may be either trademarks or registered trademarks of Microsoft in the United States and/or other countries. This license does not grant you rights to use any names, logos, or trademarks. For Microsoft’s general trademark guidelines, go to [1](https://go.microsoft.com/fwlink/?LinkID=254653). 3. Microsoft and any contributors reserves all others rights, whether under copyrights, patents, or trademarks, or by implication, estoppel or otherwise. diff --git a/README.md b/README.md index 6e6df8b0f..4e77f8fcb 100644 --- a/README.md +++ b/README.md @@ -1,70 +1,55 @@ -# Welcome to the SharePoint Framework ! +# Welcome to the SharePoint Framework! The SharePoint Framework (SPFx) is a page and part model that enables client-side development for building SharePoint experiences. It facilitates easy integration with the SharePoint data, and provides support for open source tooling development. -* [Official SharePoint Framework Documentation](http://aka.ms/spfx) +* [Official SharePoint Framework Documentation](https://aka.ms/spfx) -This repository contains the raw documents published to docs.microsoft.com site. Feel free to use [Issues]((https://github.com/SharePoint/sp-dev-docs/issues)) list to report us findings or also submit pull requests around the existing documentation. +This repository contains the raw documents published to Microsoft Docs. -The SharePoint Framework now generally available. Please check more release details either from the [GA release notes](https://github.com/SharePoint/sp-dev-docs/wiki/Release-Notes-GA) or from the [Office Blog post](https://blogs.office.com/2017/02/23/sharepoint-framework-reaches-general-availability-build-and-deploy-engaging-web-parts-today/) around the GA release. If you find issues or have new ideas and suggestions for SharePoint Framework, make sure you submit them [here](https://github.com/SharePoint/sp-dev-docs/issues). We’ll also be monitoring [#spfx](http://sharepoint.stackexchange.com/tags/spfx/), [#spfx-webparts](http://sharepoint.stackexchange.com/tags/spfx-webparts/), and [#spfx-tooling](http://sharepoint.stackexchange.com/tags/spfx-tooling/) at [SharePoint StackExchange](http://sharepoint.stackexchange.com/) as well. +## Questions & Help -## SharePoint Framework Releases - -* **September 25, 2017** - * **GA of Extensions and SPFx v1.3**. +Feel free to use [Issues]((https://github.com/SharePoint/sp-dev-docs/issues)) list to report us potential issues around the SharePoint Framework or gaps in our documentation. You can also submit directly pull requests towards our documentation. -* **June 6, 2017** - * **Dev Preview of extensions is available**. [See the release notes here](https://github.com/SharePoint/sp-dev-docs/wiki/Release-Notes---Extensions-Dev-Preview-Drop-1) +You can also tweet / follow [@Microsoft365Dev](https://twitter.com/Microsoft365Dev) or [@m365pnp](https://twitter.com/m365pnp). -* **Feb 22, 2017** - * **GA is available**. [See the release notes here](https://github.com/SharePoint/sp-dev-docs/wiki/Release-Notes-GA) +## SharePoint Framework Releases -* **Jan 9, 2017** - * **RC0 is available**. [See the release notes here](https://github.com/SharePoint/sp-dev-docs/wiki/Release-Notes-RC0) +Review all the SPFx releases here from the [initial GA release in February 2017](https://learn.microsoft.com/sharepoint/dev/spfx/roadmap) -* **Aug 17, 2016** - * **Drop 1 is available**. [See the release notes here](https://github.com/SharePoint/sp-dev-docs/wiki/Drop-1) - ## Get Started -* [Setup your Office 365 Developer Tenant](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-developer-tenant) -* [Setup your Machine](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-development-environment) -* [Go build your first web part](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/get-started/build-a-hello-world-web-part) - -## Reference -* [sp-component-base](reference/spfx/sp-component-base-module.md) -* [sp-core-library](reference/spfx/sp-core-library-module.md) -* [sp-http](reference/spfx/sp-http-module.md) -* [sp-loader](reference/spfx/sp-loader-module.md) -* [sp-odata-types](reference/spfx/sp-odata-types-module.md) -* [sp-page-context](reference/spfx/sp-page-context-module.md) -* [sp-webpart-base](reference/spfx/sp-webpart-base-module.md) +* [Setup your Office 365 Developer Tenant](https://learn.microsoft.com/sharepoint/dev/spfx/set-up-your-developer-tenant) +* [Setup your Machine](https://learn.microsoft.com/sharepoint/dev/spfx/set-up-your-development-environment) +* [Go build your first web part](https://learn.microsoft.com/sharepoint/dev/spfx/web-parts/get-started/build-a-hello-world-web-part) ## Learn More -* [Background and Philosophy](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/sharepoint-framework-overview) -* [Design Great Web Parts](https://docs.microsoft.com/en-us/sharepoint/dev/design/design-guidance-overview) -* [API Docs](https://docs.microsoft.com/en-us/javascript/api/sp-application-base) +* [Background and Philosophy](https://learn.microsoft.com/sharepoint/dev/spfx/sharepoint-framework-overview) +* [Design Great Web Parts](https://learn.microsoft.com/sharepoint/dev/design/design-guidance-overview) +* [API Docs](https://learn.microsoft.com/javascript/api/sp-application-base) ## Updates & Feedback To keep track of improvements to the Office 365 Framework, please take a look at: -* [@SharePoint](https://twitter.com/sharepoint), [@OfficeDev](https://twitter.com/officedev) and [@OfficeDevPnP](https://twitter.com/officedevpnp) on Twitter -* [Office Developer Blog](http://dev.office.com/blogs) +* [@SharePoint](https://twitter.com/sharepoint), [@Microsoft365Dev](https://twitter.com/Microsoft365Dev) and [@m365pnp](https://twitter.com/m365pnp) on Twitter +* [Office Developer Blog](https://developer.microsoft.com/office/blogs/) Provide Feedback: * If you find issues or have new ideas and suggestions for SharePoint Framework, make sure you submit them [here](https://github.com/SharePoint/sp-dev-docs/issues). -* [SharePoint StackExchange](http://sharepoint.stackexchange.com/) (please use [#spfx](http://sharepoint.stackexchange.com/tags/spfx/), [#spfx-webparts](http://sharepoint.stackexchange.com/tags/spfx-webparts/), and [#spfx-tooling](http://sharepoint.stackexchange.com/tags/spfx-tooling/) tags) * [SharePoint Developer](https://techcommunity.microsoft.com/t5/SharePoint-Developer/bd-p/SharePointDev) group at Microsoft Tech Community * [SharePoint Developer UserVoice](https://sharepoint.uservoice.com/forums/329220-sharepoint-dev-platform) -## Deployment Status -The SharePoint Framework is now generally available at Office 365. -- [SharePoint Framework reaches general availability—build and deploy engaging web parts today](https://blogs.office.com/2017/02/23/sharepoint-framework-reaches-general-availability-build-and-deploy-engaging-web-parts-today/) +## Contribute on the SharePoint Dev Docs + +Please see following guidance if you are planning to submit changes on the SharePoint developer documentation. We do welcome your pull requests! + +* [Contribution guidance](https://github.com/SharePoint/sp-dev-docs/blob/master/.github/CONTRIBUTING.md) +* [How to Create Good Pull Requests](https://github.com/SharePoint/sp-dev-docs/wiki/How-to-Create-Good-Pull-Requests) +* If you are a Microsoft contributor, please review official guidance from our [internal documentation](https://review.learn.microsoft.com/help/contribute/?branch=main) for Microsoft Docs contributors ## Have Fun -We look forward to seeing what you build! Please tweet us at @OfficeDev or @SharePoint with the #SPFx tag! +We look forward to seeing what you build! Please tweet us at @Microsoft365Dev, @m365pnp or @SharePoint with the #SPFx tag! diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..e138ec5d6 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,41 @@ + + +## Security + +Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). + +If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. + +## Reporting Security Issues + +**Please do not report security vulnerabilities through public GitHub issues.** + +Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). + +If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). + +You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). + +Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: + + * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) + * Full paths of source file(s) related to the manifestation of the issue + * The location of the affected source code (tag/branch/commit or direct URL) + * Any special configuration required to reproduce the issue + * Step-by-step instructions to reproduce the issue + * Proof-of-concept or exploit code (if possible) + * Impact of the issue, including how an attacker might exploit the issue + +This information will help us triage your report more quickly. + +If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. + +## Preferred Languages + +We prefer all communications to be in English. + +## Policy + +Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). + + diff --git a/ThirdPartyNotices b/ThirdPartyNotices index a0bd09d68..25ca6dcd6 100644 --- a/ThirdPartyNotices +++ b/ThirdPartyNotices @@ -9,7 +9,7 @@ may be either trademarks or registered trademarks of Microsoft in the United Sta The licenses for this project do not grant you rights to use any Microsoft names, logos, or trademarks. Microsoft's general trademark guidelines can be found at http://go.microsoft.com/fwlink/?LinkID=254653. -Privacy information can be found at https://privacy.microsoft.com/en-us/ +Privacy information can be found at https://privacy.microsoft.com/ Microsoft and any contributors reserve all others rights, whether under their respective copyrights, patents, or trademarks, whether by implication, estoppel or otherwise. \ No newline at end of file diff --git a/assets/MicrosoftSharePointClientComponentsEULA.docx b/assets/MicrosoftSharePointClientComponentsEULA.docx new file mode 100644 index 000000000..d7e06041f Binary files /dev/null and b/assets/MicrosoftSharePointClientComponentsEULA.docx differ diff --git a/assets/ace/URL.txt b/assets/ace/URL.txt new file mode 100644 index 000000000..7d3d66ec3 --- /dev/null +++ b/assets/ace/URL.txt @@ -0,0 +1 @@ +me/events?$select=subject,body,bodyPreview,organizer,attendees,start,end,location \ No newline at end of file diff --git a/assets/ace/calendar-top.png b/assets/ace/calendar-top.png new file mode 100644 index 000000000..d8cb719da Binary files /dev/null and b/assets/ace/calendar-top.png differ diff --git a/assets/ace/email-top.png b/assets/ace/email-top.png new file mode 100644 index 000000000..d9170e95a Binary files /dev/null and b/assets/ace/email-top.png differ diff --git a/assets/ace/events-quick-view.json b/assets/ace/events-quick-view.json new file mode 100644 index 000000000..fb16cb82b --- /dev/null +++ b/assets/ace/events-quick-view.json @@ -0,0 +1,85 @@ +{ + "type": "AdaptiveCard", + "version": "1.5", + "@odata.type": "#microsoft.graph.message", + "body": [ + { + "type": "Container", + "items": [ + { + "type": "Image", + "url": "https://raw.githubusercontent.com/SharePoint/sp-dev-docs/main/assets/ace/calendar-top.png" + }, + { + "type": "TextBlock", + "text": "This control displays the latest calendar events. You can open the event in Outlook or, if it's a meeting, you can join it simply clicking on the button next to the event.", + "wrap": true + } + ] + }, + { + "type": "Container", + "$data": "${value}", + "items": [ + { + "type": "ColumnSet", + "columns": [ + { + "type": "Column", + "width": "stretch", + "items": [ + { + "type": "TextBlock", + "text": "${subject}", + "size": "Medium" + }, + { + "type": "TextBlock", + "text": "${location.displayName}", + "spacing": "None" + }, + { + "type": "TextBlock", + "text": "${formatDateTime(substring(start.dateTime,0,19), 'dd/MM/yyyy hh:mm')}-${formatDateTime(substring(end.dateTime,0,19), 'hh:mm')}", + "spacing": "None", + "size": "Small" + } + ] + }, + { + "type": "Column", + "width": "auto", + "items": [ + { + "type": "Image", + "url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAvBJREFUSEvtVUtIalEUXdeKiDDIiIY1i7CIBkqRRrNCAqFM6AsRQhMLx5VSVvTBoqwIIqTIoCYV1aBZ/4EDk4gkmgg16iP9cFQaez+89N4zrz148AZvw+XCufectddeZ68tRCKRCP5iCP8MgM/nQ39/P9bX15Gfnw+bzYb6+nokJyfH5Z8Qg0AggObmZqjVarS2tsLr9WJ4eBhjY2PQ6/UMEA6H8fT0BJlMhoyMDAiCwOsJAayuroKehYUFZGZmgmSbm5vD+fk5HA4H3t/f0d3djcnJScjlcvT29sJisTC7hADcbjd2d3cxMzPDB1AsLS3h5OQEExMTODo64kNnZ2dxe3uLnp4eLC4uQqlUQgiHwxGPx4PDw0POJFbc3NzwhunpaTQ0NODy8hKdnZ1cHnqvra2JDB8fH7mcxKy0tBTC/f19pKmpiTP8TnR1dWFwcBDp6emIahQKhUCPRqPB+Pj4Dy0+A9DNqKys/A1nb28PfX190Gq1XIqioiLk5OSIQtKGh4cHbG1tMaBOp+M3i/wZYHl5GcTm1yANiLZCoUBxcTFSUlIkydbW1qK9vf1ngJKSEmRnZ6OmpgZmsxlOpxPb29u4u7vD6ekpCgoKYDQakZaWJgIEg0Fsbm6yHpQABWl2dXUFSiymBtRQVAq73Q6r1SoeVlVVxZuysrLEtajgU1NT3IAUx8fHvPc/AJfjj0pkMpnQ2NiIlZUVzM/Pf1uDg4MDDA0Nfa3BV3cwEZHf3t7YaV9fXzE6OhrfKigTioqKCiQlJXGjkaNGnZK+nZ2dwWAw8A0qKysD2TpZD92g8vLy+GZH7rmzsyO6aCxm9I/L5YJKpcLFxQUKCwu5h/Ly8qTtOuoxpElLSwt7/efw+/1oa2vjA2M5QELzYH9/HwRAndrR0YHc3Fy8vLxgY2MDIyMjqK6uFk0vFkPJeUDDhWxiYGCAx2U0qObU5XV1dUhNTf3SmyQBojsJ6Pn5GdfX1+xXZBdS8zihEknapsQPH6Mh/FGBh+hUAAAAAElFTkSuQmCC", + "selectAction": { + "type": "Action.OpenUrl", + "url": "${onlineMeeting.joinUrl}" + }, + "$when": "${isOnlineMeeting}" + } + ] + }, + { + "type": "Column", + "width": "auto", + "items": [ + { + "type": "Image", + "url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAR5JREFUSEvtljGuRFAUhv8rIdGQSHQahc4ORG8PbMICVBZgE+xBL3agU2h0RIJCQsLkSt4kM2/e3Fu8ea+hPef4zvnOjYscx3Hggw95BizLgjAM4boufN/nQmdZhqIokCQJZFl+qCHzPB9pmmIcxzOwbRvyPIdpmrBtmwtQVRWapoHneRBF8axRVRVBEID0fX/QTilAURTs+466rqFpGnRd5wJ0XYdhGGBZFgRBwDRNJ4BOdgdEUQTHcfAbisqyRBzHrwF057QDSZK++fxpHNrUuq6nAUIIXgLoeIZhcClhJbVte2p+UPS1A1YxT/ztDnhewMp5uwNWMU/8AjAtXYouRUwDzIT/OUUf+9g9X5nM+TkT7lfmn/9VcDbInXYDn/BFftWPiLMAAAAASUVORK5CYII=", + "selectAction": { + "type": "Action.OpenUrl", + "url": "${webLink}" + } + } + ] + } + ] + } + ], + "separator": true + } + ], + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json" +} \ No newline at end of file diff --git a/assets/ace/messages-quick-view.json b/assets/ace/messages-quick-view.json new file mode 100644 index 000000000..74e7f40a1 --- /dev/null +++ b/assets/ace/messages-quick-view.json @@ -0,0 +1,84 @@ +{ + "type": "AdaptiveCard", + "version": "1.5", + "@odata.type": "#microsoft.graph.message", + "body": [ + { + "type": "Container", + "items": [ + { + "type": "Image", + "url": "https://raw.githubusercontent.com/SharePoint/sp-dev-docs/main/assets/ace/email-top.png" + }, + { + "type": "TextBlock", + "text": "This control displays the last email message received in your inbox. To view the message, simply click on the button. The message will open directly in Outlook, allowing you to read and respond to it as needed.", + "wrap": true + } + ] + }, + { + "type": "Container", + "$data": "${value}", + "items": [ + { + "type": "ColumnSet", + "columns": [ + { + "type": "Column", + "width": "stretch", + "items": [ + { + "type": "TextBlock", + "text": "${from.emailAddress.name}", + "size": "Medium", + "weight": "${if(isRead, 'normal', 'bolder')}" + }, + { + "type": "TextBlock", + "text": "${subject}", + "spacing": "None", + "weight": "${if(isRead, 'normal', 'bolder')}" + } + ] + }, + { + "type": "Column", + "width": "auto", + "items": [ + { + "type": "TextBlock", + "text": "${if(hasAttachments, '📎', '')} ${if(importance == 'normal', '', '❗')} ${if(flag.flagStatus == 'flagged', '🚩', '')}", + "horizontalAlignment": "Right" + }, + { + "type": "TextBlock", + "text": "{{DATE(${sentDateTime}, COMPACT)}} {{TIME(${sentDateTime})}}", + "spacing": "None", + "size": "Small" + } + ], + "verticalContentAlignment": "Center" + }, + { + "type": "Column", + "width": "auto", + "items": [ + { + "type": "Image", + "url": "${if(isRead, 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAY5JREFUSEvt1aGLwlAcB/DvQ2FNg6BFMPg3WDRZRayGLWkxKViUgUkYWgRNKy5psG52kxb/BoOwMtEwTaLwjudxntObb3onHMe9uvH97Pd9vDdCKaV44SK/Btjv9zAMAz6fD5lMBoIgeJqbOwELHo/HaLfbME3zFBqNRlGr1ZDNZrmQK3AZvFqtUKlUUCwWT0C/30e320U4HOZCN4BbcCAQcFSy3W49QWfAa/B18TyIHA4Hqus6Wq0WLqu4/mLejl5D9XoduVwOZL1eU1EUEYlE0Ol0EAqFeFl3n282G1SrVViWheFw+AnMZjMoioJSqQS/3/8UcjweoaoqZFlGMpl0AolEAqPRCOVy+SnkI7zX6yGfz2M+nzuBRqMBttFsgkeRy3A2ATuEzWbzFmBjTSYTFAoFLJfLh2qKxWLQNA3pdBqs7i+BVCoFdvctFovzqfWqsNMdj8dBCMF0OnUHvAbee+8f4Lb4RyuybRuPXnBuXbGLLxgMvh+03W5HB4MBGPCTiwGSJIH7y/wu+nLgDSewZ7NrDNFAAAAAAElFTkSuQmCC', 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAARBJREFUSEvtlS2qhUAYhl+LRbALYhMsrsAF2MXmLhRsJpugu7CJ3QW4AotgE8EiBsFimYNevJxz8Th6YZpThpnv55nvnT+OEELAsHEPgKYue4nGcSS+76NtW9pibtkVRUEUReCGYSCO46Cua6y9KIq3Ev11nqYJaZpC07St/wUURQFd1+F5HmzbhiAIt0DzPCPLMsRxjKqqYJrmJ8B1XfR9vzms7SroPfEeJ0kSkiT5BARBAMMwcBRwVNGZX1mWCMPwGLDr8i3Bat+l+FbpJcARaFmWbZrn+VMJbwHeQXmeb0PLsk4Pwb8Ad47UA6CqdSiRqqqQZZkafMWh6zo0TfNzD5g/ds+PRtsT9j8a6z14AbwnH7bn+xIdAAAAAElFTkSuQmCC')}", + "selectAction": { + "type": "Action.OpenUrl", + "url": "${webLink}" + } + } + ] + } + ] + } + ], + "separator": true + } + ], + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json" +} \ No newline at end of file diff --git a/assets/bot-powered/Media/Collect-Feedback.png b/assets/bot-powered/Media/Collect-Feedback.png new file mode 100644 index 000000000..16aecc8a0 Binary files /dev/null and b/assets/bot-powered/Media/Collect-Feedback.png differ diff --git a/assets/bot-powered/Media/Ok-Feedback.png b/assets/bot-powered/Media/Ok-Feedback.png new file mode 100644 index 000000000..ea5aa3a3b Binary files /dev/null and b/assets/bot-powered/Media/Ok-Feedback.png differ diff --git a/assets/bot-powered/TeamsAppManifest/icon-color.png b/assets/bot-powered/TeamsAppManifest/icon-color.png new file mode 100644 index 000000000..b8cf81afb Binary files /dev/null and b/assets/bot-powered/TeamsAppManifest/icon-color.png differ diff --git a/assets/bot-powered/TeamsAppManifest/icon-outline.png b/assets/bot-powered/TeamsAppManifest/icon-outline.png new file mode 100644 index 000000000..2c3bf6fa6 Binary files /dev/null and b/assets/bot-powered/TeamsAppManifest/icon-outline.png differ diff --git a/assets/bot-powered/TeamsAppManifest/manifest.json b/assets/bot-powered/TeamsAppManifest/manifest.json new file mode 100644 index 000000000..7a502770d --- /dev/null +++ b/assets/bot-powered/TeamsAppManifest/manifest.json @@ -0,0 +1,65 @@ +{ + "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.14/MicrosoftTeams.schema.json", + "manifestVersion": "1.16", + "version": "1.0.0", + "id": "", + "packageName": "collectfeedback.botpoweredace", + "developer": { + "name": "", + "websiteUrl": "", + "privacyUrl": "", + "termsOfUseUrl": "", + "mpnId": "" + }, + "name": { + "short": "Collect Feedaback Bot Powered ACE", + "full": "This is a basic sample of a Bot Powered ACE for Microsoft Viva Connections Dashboard to collect user's feedback" + }, + "description": { + "short": "Basic sample of a Bot Powered ACE for Microsoft Viva Connections Dashboard to collect user's feedback", + "full": "Basic sample of how to use the latest release of the Bot Framework SDK to build a Bot Powered ACE for Microsoft Viva Connections Dashboard to collect user's feedback" + }, + "icons": { + "outline": "icon-outline.png", + "color": "icon-color.png" + }, + "accentColor": "#FFFFFF", + "bots": [ + { + "botId": "", + "needsChannelSelector": false, + "isNotificationOnly": false, + "supportsCalling": false, + "supportsVideo": false, + "supportsFiles": false, + "scopes": [ + "team", + "personal", + "groupchat" + ] + } + ], + "dashboardCards": [ + { + "id": "", + "displayName": "Collect Feedaback", + "description": "Bot Powered ACE to collect user's feedback", + "icon": { + "officeUIFabricIconName": "Feedback" + }, + "contentSource": { + "sourceType": "bot", + "botConfiguration": { + "botId": "" + } + }, + "defaultSize": "medium" + } + ], + "permissions": [ + "identity" + ], + "validDomains": [ + ".ngrok.io" + ] +} \ No newline at end of file diff --git a/assets/sharepoint-pnp-general-dev-community-call.ics b/assets/sharepoint-pnp-general-dev-community-call.ics new file mode 100644 index 000000000..fe57ff55f --- /dev/null +++ b/assets/sharepoint-pnp-general-dev-community-call.ics @@ -0,0 +1,594 @@ +BEGIN:VCALENDAR +PRODID:-//Microsoft Corporation//Outlook 16.0 MIMEDIR//EN +VERSION:2.0 +METHOD:REQUEST +X-MS-OLK-FORCEINSPECTOROPEN:TRUE +BEGIN:VTIMEZONE +TZID:FLE Standard Time +BEGIN:STANDARD +DTSTART:16011028T040000 +RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 +TZOFFSETFROM:+0300 +TZOFFSETTO:+0200 +END:STANDARD +BEGIN:DAYLIGHT +DTSTART:16010325T030000 +RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3 +TZOFFSETFROM:+0200 +TZOFFSETTO:+0300 +END:DAYLIGHT +END:VTIMEZONE +BEGIN:VEVENT +CLASS:PUBLIC +CREATED:20190625T112317Z +DESCRIPTION:Welcome to SharePoint PnP Bi-weekly Special Interest Group call + around SharePoint General dev. Topics covered in this call are solution d + esigns\, provisioning\, PnP PowerShell\, CSOM\, PnP CSOM\, PowerApps\, Flo + w etc. \n \nLink to join the call – https://aka.ms/spdev-sig-call-join \ + n \n-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --\nShareP + oint Dev Community – https://aka.ms/sppnp \nSharePoint Dev Videos (includ + ing recorded community calls) – https://aka.ms/spdev-videos \nSharePoint + Dev Documentation - https://aka.ms/spdev-docs \nSharePoint Dev issues – h + ttp://aka.ms/spdev-issues \nSharePoint Dev UserVoice – https://aka.ms/spd + ev-uservoice \n \n \n +DTEND;TZID="FLE Standard Time":20190808T180000 +DTSTAMP:20190625T112319Z +DTSTART;TZID="FLE Standard Time":20190808T170000 +LAST-MODIFIED:20190625T112317Z +LOCATION:Microsoft Teams Meeting +ORGANIZER;CN="Vesa Juvonen":mailto:Vesa.Juvonen@microsoft.com +PRIORITY:5 +RECURRENCE-ID;TZID="FLE Standard Time":20190808T170000 +SEQUENCE:0 +SUMMARY;LANGUAGE=en-us:SharePoint Dev Ecosystem (PnP) - General Dev\, Micro + soft Flow\, PowerApps\, CSOM\, PnP Core\, PnP PowerShell SIG Bi-Weekly Cal + l +TRANSP:OPAQUE +UID:040000008200E00074C5B7101A82E00800000000D0AFD045D62AD501000000000000000 + 01000000090435FC9105F97488EB25A958F88D87C +X-ALT-DESC;FMTTYPE=text/html:

Welcome to SharePoint PnP Bi-weekly Special Interest G + roup call around SharePoint General dev. Topics covered in this call are s + olution designs\, provisioning\, PnP PowerShell\, CSOM\, PnP CSOM\, PowerA + pps\, Flow etc.

 \;

Link to join + the call –\; https://aka + .ms/spdev-sig-call-join

 \;

- + - -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --

SharePoint D + ev Community –\; https://aka.ms/sppnp

SharePoint Dev Videos (including re + corded community calls) –\; http + ://aka.ms/spdev-videos

SharePoint + Dev Documentation - https://aka.ms/spde + v-docs

SharePoint Dev issues R + 11\; https://aka.ms/spdev-issues < + o:p>

SharePoint Dev UserVoice –\; https://aka.ms/spdev-uservoice

 \;

 \;

< + /html> +X-MICROSOFT-CDO-BUSYSTATUS:BUSY +X-MICROSOFT-CDO-IMPORTANCE:1 +X-MICROSOFT-DISALLOW-COUNTER:FALSE +X-MS-OLK-AUTOFILLLOCATION:FALSE +X-MS-OLK-CONFTYPE:0 +BEGIN:VALARM +TRIGGER:-PT15M +ACTION:DISPLAY +DESCRIPTION:Reminder +END:VALARM +END:VEVENT +END:VCALENDAR diff --git a/assets/sharepoint-pnp-monthly-community-call.ics b/assets/sharepoint-pnp-monthly-community-call.ics new file mode 100644 index 000000000..a7e1eb018 --- /dev/null +++ b/assets/sharepoint-pnp-monthly-community-call.ics @@ -0,0 +1,593 @@ +BEGIN:VCALENDAR +PRODID:-//Microsoft Corporation//Outlook 16.0 MIMEDIR//EN +VERSION:2.0 +METHOD:REQUEST +X-MS-OLK-FORCEINSPECTOROPEN:TRUE +BEGIN:VTIMEZONE +TZID:FLE Standard Time +BEGIN:STANDARD +DTSTART:16011028T040000 +RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 +TZOFFSETFROM:+0300 +TZOFFSETTO:+0200 +END:STANDARD +BEGIN:DAYLIGHT +DTSTART:16010325T030000 +RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3 +TZOFFSETFROM:+0200 +TZOFFSETTO:+0300 +END:DAYLIGHT +END:VTIMEZONE +BEGIN:VEVENT +CLASS:PUBLIC +CREATED:20190624T184213Z +DESCRIPTION:Welcome to SharePoint Dev Ecosystem (PnP) – Monthly Community + Call. In this call we cover the latest changes around the SharePoint deve + lopment from SharePoint engineering and community perspective. We’ll go + through the latest contributions and status in the UserVoice. \n \nLink t + o join the call – https://aka.ms/spdev-call-join \n \n-- -- -- -- -- -- + -- -- -- -- -- -- -- -- -- -- -- -- -- -- --\nSharePoint Dev Community – + https://aka.ms/sppnp \nSharePoint Dev Videos (including recorded community + calls) – https://aka.ms/spdev-videos \nSharePoint Dev Documentation - ht + tp://aka.ms/spdev-docs \nSharePoint Dev issues – https://aka.ms/spdev-iss + ues \nSharePoint Dev UserVoice – https://aka.ms/spdev-uservoice \n \n \n +DTEND;TZID="FLE Standard Time":20190813T190000 +DTSTAMP:20190624T184214Z +DTSTART;TZID="FLE Standard Time":20190813T180000 +LAST-MODIFIED:20190624T184213Z +LOCATION:Microsoft Teams Meeting +ORGANIZER;CN="Vesa Juvonen":mailto:Vesa.Juvonen@microsoft.com +PRIORITY:5 +RRULE:FREQ=MONTHLY;BYDAY=TU;BYSETPOS=2 +SEQUENCE:0 +SUMMARY;LANGUAGE=en-us:SharePoint Dev Ecosystem (PnP) - Monthly Community C + all +TRANSP:OPAQUE +UID:040000008200E00074C5B7101A82E008000000001067215BC42AD501000000000000000 + 010000000FAF0FD97CF317A488F8BED917B68BC08 +X-ALT-DESC;FMTTYPE=text/html:

Welcome to SharePoint Dev Ecos + ystem (PnP) –\; Monthly Community Call. In this call we cover the lat + est changes around the SharePoint development from SharePoint engineering + and community perspective. We’\;ll go through the latest contribution + s and status in the UserVoice. \;

&nbs + p\;

Link to join the call ̵ + 1\; https://aka.ms/spdev-call-joi + n

+  \;

-- -- -- -- -- -- -- -- + -- -- -- -- -- -- -- -- -- -- -- -- --

SharePoint Dev Community –\; + https://aka.ms/sppnp

SharePoint Dev Videos (including recorded community calls + ) –\; https://aka.ms/spdev-videos +

SharePoint Dev Documentation - < + a href="https://aka.ms/spdev-docs">https://aka.ms/spdev-docs +

SharePoint Dev issues –\; https://aka.ms/spdev-issues

SharePoint Dev UserVoice –\; https://aka.ms/spdev-uservoice

 \;

 \;

+X-MICROSOFT-CDO-BUSYSTATUS:BUSY +X-MICROSOFT-CDO-IMPORTANCE:1 +X-MICROSOFT-DISALLOW-COUNTER:FALSE +X-MS-OLK-AUTOFILLLOCATION:FALSE +X-MS-OLK-CONFTYPE:0 +BEGIN:VALARM +TRIGGER:-PT15M +ACTION:DISPLAY +DESCRIPTION:Reminder +END:VALARM +END:VEVENT +END:VCALENDAR diff --git a/assets/sharepoint-pnp-spfx-pnpjs-community-call.ics b/assets/sharepoint-pnp-spfx-pnpjs-community-call.ics new file mode 100644 index 000000000..3f92769fa --- /dev/null +++ b/assets/sharepoint-pnp-spfx-pnpjs-community-call.ics @@ -0,0 +1,105 @@ +BEGIN:VCALENDAR +PRODID:-//Microsoft Corporation//Outlook 16.0 MIMEDIR//EN +VERSION:2.0 +METHOD:REQUEST +X-MS-OLK-FORCEINSPECTOROPEN:TRUE +BEGIN:VTIMEZONE +TZID:Eastern Standard Time +BEGIN:STANDARD +DTSTART:16011104T020000 +RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +END:STANDARD +BEGIN:DAYLIGHT +DTSTART:16010311T020000 +RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +END:DAYLIGHT +END:VTIMEZONE +BEGIN:VEVENT +CLASS:PUBLIC +CREATED:20190624T182257Z +DESCRIPTION:Welcome to SharePoint PnP Bi-weekly Special Interest Group call + around Client-side development. Topics covered in this call are SharePoin + t Framework latest updates\, PnPjs\, Office365Cli\, Reusable Controls\, an + d community demos.\n\n \n\nLink to join the call – https://aka.ms/spdev- + spfx-call-join\n\n \n\n-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- + -- -- -- --\n\n \n\nSharePoint Dev Community – https://aka.ms/sppnp Share + Point Dev Videos (including recorded community calls) – https://aka.ms/sp + dev-videos SharePoint Dev Documentation - https://aka.ms/spdev-docs SharePo + int Dev issues – https://aka.ms/spdev-issues SharePoint Dev UserVoice – + https://aka.ms/spdev-uservoice\n\n +DTEND;TZID="Eastern Standard Time":20190801T110000 +DTSTAMP:20190624T182203Z +DTSTART;TZID="Eastern Standard Time":20190801T100000 +LAST-MODIFIED:20190624T182257Z +LOCATION:Microsoft Teams Meeting +ORGANIZER;CN="Patrick Rodgers":invalid:nomail +PRIORITY:5 +RRULE:FREQ=WEEKLY;INTERVAL=2;BYDAY=TH;WKST=SU +SEQUENCE:2 +SUMMARY;LANGUAGE=en-us:SharePoint Patterns and Practices Client-side develo + pment SIG +TRANSP:OPAQUE +UID:040000008200E00074C5B7101A82E00800000000C0BD1A15972AD501000000000000000 + 01000000044FD58F0AA2A1F4790EC41C4BE20606E +X-ALT-DESC;FMTTYPE=text/html: - - - - - - - -``` - -If you want your users to deep link into portions of your add-in, your apphost page and the contents of the **IFrame** can collaborate to make that possible. One alternative is to use **IFrame** post-message communication and individual URLs per page in the remote add-in. To have individual URLs per page, you can create individual pages in the add-in web or use query string parameters on one page. - - - - -### Alternative approach: Add the sites to the same security zone in Internet Explorer - -If an add-in was not designed following the apphost pattern, you can still allow it to work by adding the following domains into the same security zone: - - - - -- The domain of your SharePoint site (for example, `https://contoso.sharepoint.com`). - - -- The domain of the cloud-hosted add-in ( `http://remoteserver`). - - -- The domain of Microsoft-hosted sign-in pages and services ( `*.microsoftonline.com`). - - -Administrators can use Active Directory policies to push changes to all computers in the organization. - - - - -## Security implications of using the apphost pattern - - -It is important to point out that the apphost pattern effectively puts your remote page in the same security zone as the add-in web. Make you sure you understand the implications of adding a site to a security zone. For more information, see [How to use security zones in Internet Explorer](http://support.microsoft.com/kb/174360). - - - - -## Working in other browsers: Chrome, Firefox, and Safari - - -Other browsers, such as Google Chrome, Mozilla Firefox, and Apple Safari, do not implement the concept of security zone. If a browser does not isolate the cookies in separated storage, it probably will not encounter the difficulties described in this article. We recommend that you follow the apphost pattern in your add-ins. Using the apphost pattern ensures that your add-in works in the mentioned browsers and Internet Explorer, regardless of which security zone SharePoint is in. - - - - -## Additional resources - - - -- [Secure data access and client object models for SharePoint Add-ins](secure-data-access-and-client-object-models-for-sharepoint-add-ins.md) - - -- [Access SharePoint data from add-ins using the cross-domain library](access-sharepoint-data-from-add-ins-using-the-cross-domain-library.md) - - -- [SharePoint Add-ins](sharepoint-add-ins.md) - - -- [Authorization and authentication of SharePoint Add-ins](authorization-and-authentication-of-sharepoint-add-ins.md) - - -- [Three ways to think about design options for SharePoint Add-ins](three-ways-to-think-about-design-options-for-sharepoint-add-ins.md) - - -- [Important aspects of the SharePoint Add-in architecture and development landscape](important-aspects-of-the-sharepoint-add-in-architecture-and-development-landscap.md) - - -- [Host webs, add-in webs, and SharePoint components in SharePoint](host-webs-add-in-webs-and-sharepoint-components-in-sharepoint.md) - - -- [Data storage in SharePoint Add-ins](important-aspects-of-the-sharepoint-add-in-architecture-and-development-landscap.md#Data) - - -- [Create a custom proxy page for the cross-domain library in SharePoint](create-a-custom-proxy-page-for-the-cross-domain-library-in-sharepoint.md) - - -- [Client-side Cross-domain Security](http://msdn.microsoft.com/en-us/library/cc709423%28v=vs.85%29.aspx) - - - +--- +title: Work with the cross-domain library across different Internet Explorer security zones in SharePoint Add-ins +description: Use the cross-domain library in SharePoint when the host web and add-in pages are in different security zones in Internet Explorer. +ms.date: 09/26/2023 +ms.localizationpriority: high +ms.service: sharepoint +--- + + +# Work with the cross-domain library across different Internet Explorer security zones in SharePoint Add-ins + +[!INCLUDE [sp-add-in-deprecation](../../includes/snippets/sp-add-in-deprecation.md)] + +If you are using the SharePoint cross-domain library for your add-ins, you should be aware of how security zones work in Internet Explorer. Your add-in may encounter some communication issues if the SharePoint website and the add-in are in different zones. This article explains what happens when you use the cross-domain library in different Internet Explorer security zones. + + + +## Cross-zone scenarios in Internet Explorer using the SharePoint cross-domain library + +For security reasons, Internet Explorer prevents pages that are on different integrity levels (also known as security zones) to share cookies because each integrity level has its own cookie store. The integrity level of a page is determined by its top-most page, and any frame within that page shares the same integrity level. For more information, see [Beware Cookie Sharing in Cross-Zone Scenarios](https://blogs.msdn.microsoft.com/ieinternals/2011/03/10/beware-cookie-sharing-in-cross-zone-scenarios/). + +The SharePoint cross-domain library uses a hidden **IFrame** and a client-side proxy page hosted on SharePoint to enable client-side communication by using JavaScript. The cross-domain library is available when you reference the sp.requestexecutor.js file in your pages. For more information, see [Access SharePoint data from add-ins using the cross-domain library](access-sharepoint-data-from-add-ins-using-the-cross-domain-library.md). + +When the remote add-in page and SharePoint website are in different security zones, the authorization cookies cannot be sent. If there are no authorization cookies, and the **IFrame** tries to load the proxy page, it is redirected to the SharePoint sign-in page. The SharePoint sign-in page cannot be contained in an **IFrame** for security reasons. In these scenarios, the library cannot load the proxy page, and communication with SharePoint is not possible. + +The following diagram shows a cross-zone scenario in which the proxy page cannot be loaded. The top page puts the frame in the same security zone as `http://remoteserver/remotepage.html`. The proxy page does not load. + +**Cross-zone scenario where the proxy page cannot be loaded** + +![Cross-zone scenario, proxy page cannot be loaded](../images/Crosszone_loaderror.png) + +The following are some examples in which the cross-domain library may not be able to load the proxy page: + +- Your customers are using SharePoint Online, and your remote add-in page is hosted on an intranet server. This scenario is prone to the proxy page loading issue because the SharePoint Online URL is not usually in the local intranet zone. This is a very common scenario during initial development of an add-in because you may be using IIS Express or another local server to host your page without a fully qualified internet domain. + +- Your customers are using SharePoint on-premises with forms-based authentication, and your remote page is hosted on a cloud service (for example, Microsoft Azure). + + + + + +## Handling cross-zone scenarios in SharePoint Add-ins + +There are a few ways to solve this problem during both add-in development (strongly recommended) and add-in run time. + +### Best practice: Use the apphost pattern + +To handle a cross-zone scenario, we recommend that you have an apphost page in SharePoint. The apphost page is a SharePoint page that contains the remote page in an **IFrame**. Everything inside the **IFrame** on the apphost page exists in the same security zone as the add-in web. The cross-domain library on the remote page can receive the authorization cookies and loads the proxy page successfully. + +The following diagram shows a cross-zone scenario being handled by using the apphost page pattern. + +**Cross-zone scenario handling by using the apphost page pattern** + +![Cross-zone scenario handling by using the apphost](../images/Crosszone_loadsuccess.png) + +The code required for the apphost page is simple. The main portion of the apphost page is an **SPAppIFrame** element. You must use CSS to make the **IFrame** invisible so that it doesn't interfere with your add-in. + +The following markup is an example of a simple apphost page. The markup performs the following tasks: + +- Declares directives needed when using SharePoint components. + +- Declares styles to make the **IFrame** invisible. + +- Declares the **SPAppIFrame** and sets the target to the add-in start page. + + +```HTML +<%@ Page + Inherits="Microsoft.SharePoint.WebPartPages.WebPartPage, Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" + language="C#" %> +<%@ Register + Tagprefix="SharePoint" + Namespace="Microsoft.SharePoint.WebControls" + Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %> +<%@ Register + Tagprefix="Utilities" + Namespace="Microsoft.SharePoint.Utilities" + Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %> +<%@ Register + Tagprefix="WebPartPages" + Namespace="Microsoft.SharePoint.WebPartPages" + Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %> + + + + Your add-in page title + + + + + + + + +``` + +
+ +If you want your users to deep link into portions of your add-in, your apphost page and the contents of the **IFrame** can collaborate to make that possible. One alternative is to use **IFrame** post-message communication and individual URLs per page in the remote add-in. To have individual URLs per page, you can create individual pages in the add-in web or use query string parameters on one page. + +### Alternative approach: Add the sites to the same security zone in Internet Explorer + +If an add-in was not designed following the apphost pattern, you can still allow it to work by adding the following domains into the same security zone: + +- The domain of your SharePoint site (for example, `https://contoso.sharepoint.com`). + +- The domain of the cloud-hosted add-in (`http://remoteserver`). + +- The domain of Microsoft-hosted sign-in pages and services (`*.microsoftonline.com`). + +Administrators can use Active Directory policies to push changes to all computers in the organization. + + + + +## Security implications of using the apphost pattern + +It is important to point out that the apphost pattern effectively puts your remote page in the same security zone as the add-in web. Make you sure you understand the implications of adding a site to a security zone. + + + +## Working in other browsers: Chrome, Firefox, and Safari + +Other browsers, such as Google Chrome, Mozilla Firefox, and Apple Safari, do not implement the concept of security zones. If a browser does not isolate the cookies in separated storage, it probably will not encounter the difficulties described in this article. We recommend that you follow the apphost pattern in your add-ins. Using the apphost pattern ensures that your add-in works in the mentioned browsers and Internet Explorer, regardless of which security zone SharePoint is in. + +## See also + +- [Secure data access and client object models for SharePoint Add-ins](secure-data-access-and-client-object-models-for-sharepoint-add-ins.md) +- [Create a custom proxy page for the cross-domain library in SharePoint](create-a-custom-proxy-page-for-the-cross-domain-library-in-sharepoint.md) +- [Client-side Cross-domain Security](https://msdn.microsoft.com/library/cc709423%28v=vs.85%29.aspx) +- [Creating SharePoint Add-ins that use the cross-domain library](creating-sharepoint-add-ins-that-use-the-cross-domain-library.md) +- [Authorization and authentication of SharePoint Add-ins](authorization-and-authentication-of-sharepoint-add-ins.md) + + + diff --git a/docs/sp-add-ins/working-with-folders-and-files-with-rest.md b/docs/sp-add-ins/working-with-folders-and-files-with-rest.md index c9a04b3d0..cf4666d49 100644 --- a/docs/sp-add-ins/working-with-folders-and-files-with-rest.md +++ b/docs/sp-add-ins/working-with-folders-and-files-with-rest.md @@ -1,437 +1,356 @@ --- title: Working with folders and files with REST -ms.date: 09/25/2017 -ms.prod: sharepoint +description: Perform basic create, read, update, and delete (CRUD) operations on folders and files with the SharePoint REST interface. +ms.date: 01/12/2023 +ms.localizationpriority: high +ms.service: sharepoint --- - # Working with folders and files with REST -Learn how to perform basic create, read, update, and delete (CRUD) operations on folders and files with the SharePoint REST interface. - - - **Note** The name "apps for SharePoint" is changing to "SharePoint Add-ins". During the transition, the documentation and the UI of some SharePoint products and Visual Studio tools might still use the term "apps for SharePoint". For details, see [New name for apps for Office and SharePoint](new-name-for-apps-for-sharepoint.md#bk_newname). - - - **Tip** The SharePoint Online (and on-premise SharePoint 2016 and later) REST service supports combining multiple requests into a single call to the service by using the OData `$batch` query option. For details and links to code samples, see [Make batch requests with the REST APIs](make-batch-requests-with-the-rest-apis.md). - +> [!NOTE] +> The examples on this page do not support the % and # characters. [Support % and # in files and folders with ResourcePath API](../solution-guidance/supporting-and-in-file-and-folder-with-the-resourcepath-api.md) +> [!TIP] +> The SharePoint Online (and on-premises SharePoint 2016 and later) REST service supports combining multiple requests into a single call to the service by using the OData `$batch` query option. For details and links to code samples, see [Make batch requests with the REST APIs](make-batch-requests-with-the-rest-apis.md). ## Working with folders by using REST - -You can retrieve a folder inside a document library when you know its URL. For example, you can retrieve the root folder of your Shared Documents library by using the endpoint in the following example. - - - +You can retrieve a folder inside a document library when you know its URL. For example, you can **retrieve the root folder of your Shared Documents library** by using the endpoint in the following example. +```http +GET https://{site_url}/_api/web/GetFolderByServerRelativeUrl('Shared Documents') +Authorization: "Bearer " + accessToken +Accept: "application/json;odata=verbose" ``` -url: http://site url/_api/web/GetFolderByServerRelativeUrl('/Shared Documents') -method: GET -headers: - Authorization: "Bearer " + accessToken - accept: "application/json;odata=verbose" or "application/atom+xml" -``` +The following XML shows an example of **folder properties that are returned when you request the XML content type**. -The following XML shows an example of folder properties that are returned when you request the XML content type. - - - - - - -```XML +```xml - -0 -Shared Documents -/Shared Documents - - + + 0 + Shared Documents + /Shared Documents + + ``` -The following example shows how to **create** a folder. - - - +The following example shows how to **create a folder**. +```http +POST https://{site_url}/_api/web/folders +Authorization: "Bearer " + accessToken +Accept: "application/json;odata=verbose" +Content-Type: "application/json" +Content-Length: {length of request body as integer} +X-RequestDigest: "{form_digest_value}" - -``` -url: http://site url/_api/web/folders -method: POST -body: { '__metadata': { 'type': 'SP.Folder' }, 'ServerRelativeUrl': '/document library relative url/folder name'} -Headers: - Authorization: "Bearer " + accessToken - X-RequestDigest: form digest value - accept: "application/json;odata=verbose" - content-type: "application/json;odata=verbose" - content-length:length of post body +{ + "__metadata": { + "type": "SP.Folder" + }, + "ServerRelativeUrl": "/document library relative url/folder name" +} ``` -The following example shows how to **update** a folder by using the **MERGE** method. - - - - +The following example shows how to **rename a folder by using the MERGE method**. +First, obtain the folder's OData type with a GET request. +```http +GET https://{site_url}/_api/web/GetFolderByServerRelativeUrl('Folder Name')/ListItemAllFields +Authorization: "Bearer " + accessToken +Accept: "application/json;odata=verbose" ``` -url: http://site url/_api/web/GetFolderByServerRelativeUrl('/Folder Name') -method: POST -body: { '__metadata': { 'type': 'SP.Folder' }, 'Name': 'New name' } -Headers: - Authorization: "Bearer " + accessToken - X-RequestDigest: form digest value - "IF-MATCH": etag or "*" - "X-HTTP-Method":"MERGE", - accept: "application/json;odata=verbose" - content-type: "application/json;odata=verbose" - content-length:length of post body -``` - -The following example shows how to **delete** a folder. - - - +From the result, obtain the `odata.type` value, such as `SP.Data.Shared_x0020_DocumentsItem` (the value may be different depending on your library configuration). Then submit a MERGE request: +```http +POST https://{site_url}/_api/web/GetFolderByServerRelativeUrl('Folder Name')/ListItemAllFields +Authorization: "Bearer " + accessToken +Accept: "application/json;odata=verbose" +Content-Type: "application/json" +Content-Length: {length of request body as integer} +If-Match: "{etag or *}" +X-HTTP-Method: "MERGE" +X-RequestDigest: "{form_digest_value}" +{ + "__metadata": { + "type": "{odata.type from previous call}" + }, + "Title": "New name", + "FileLeafRef": "New name" +} ``` -url: http://site url/_api/web/GetFolderByServerRelativeUrl('/Folder Name') -method: POST -Headers: - Authorization: "Bearer " + accessToken - X-RequestDigest: form digest value - "IF-MATCH": etag or "*" - "X-HTTP-Method":"DELETE" - -``` - - -## Working with files by using REST - - -The following example shows how to **retrieve** all of the files in a folder. - - +The following example shows how to **delete a folder**. +```http +POST https://{site_url}/_api/web/GetFolderByServerRelativeUrl('Folder Name') +Authorization: "Bearer " + accessToken +If-Match: "{etag or *}" +X-HTTP-Method: "DELETE" +X-RequestDigest: "{form_digest_value}" ``` -url: http://site url/_api/web/GetFolderByServerRelativeUrl('/Folder Name')/Files -method: GET -headers: - Authorization: "Bearer " + accessToken - accept: "application/json;odata=verbose" or "application/atom+xml" - -``` - -The following example shows how to **retrieve** a specific file. - - - +## Working with files by using REST +The following example shows how to **retrieve all of the files in a folder**. -``` -url: http://site url/_api/web/GetFolderByServerRelativeUrl('/Folder Name')/Files('file name')/$value +```http +GET https://{site_url}/_api/web/GetFolderByServerRelativeUrl('Folder Name')/Files method: GET -headers: - Authorization: "Bearer " + accessToken -``` +Authorization: "Bearer " + accessToken +Accept: "application/json;odata=verbose" +``` + +The following example shows how to **retrieve a specific file**. + +```http +GET https://{site_url}/_api/web/GetFolderByServerRelativeUrl('Folder Name')/Files('{file_name}')/$value +Authorization: "Bearer " + accessToken +``` + +You can also **retrieve a file when you know its URL**, as in the following example. + +```http +GET https://{site_url}/_api/web/GetFileByServerRelativeUrl('/Folder Name/{file_name}')/$value +Authorization: "Bearer " + accessToken +``` + +The following code sample shows how to **retrieve a file when you know its URL by using the REST endpoint above and C#**. + +```csharp +/// +/// Download File Via Rest API +/// +/// https://xxxxx/sites/DevSite +/// +/// MyDocumentLibrary +/// test.docx +/// C:\\ +public static void DownloadFileViaRestAPI(string webUrl, ICredentials credentials, string documentLibName, string fileName, string path) +{ + webUrl = webUrl.EndsWith("/") ? webUrl.Substring(0, webUrl.Length - 1) : webUrl; + string webRelativeUrl = null; + if (webUrl.Split('/').Length > 3) + { + webRelativeUrl = "/" + webUrl.Split(new char[] { '/' }, 4)[3]; + } + else + { + webRelativeUrl = ""; + } + + using (WebClient client = new WebClient()) + { + client.Headers.Add("X-FORMS_BASED_AUTH_ACCEPTED", "f"); + client.Credentials = credentials; + Uri endpointUri = new Uri(webUrl + "/_api/web/GetFileByServerRelativeUrl('" + webRelativeUrl + "/" + documentLibName + "/" + fileName + "')/$value"); + + byte[] data = client.DownloadData(endpointUri); + FileStream outputStream = new FileStream(path + fileName, FileMode.OpenOrCreate | FileMode.Append, FileAccess.Write, FileShare.None); + outputStream.Write(data, 0, data.Length); + outputStream.Flush(true); + outputStream.Close(); + } +} -You can also **retrieve** a file when you know its URL, as in the following example. - +static void Main(string[] args) +{ + string siteURL = "https://xxxxx/sites/DevSite"; - + //set credential of SharePoint online + SecureString secureString = new SecureString(); + foreach (char c in "Password".ToCharArray()) + { + secureString.AppendChar(c); + } + ICredentials credentials = new SharePointOnlineCredentials("xxxxxx.onmicrosoft.com", secureString); + //set credential of SharePoint 2013(On-Premises) + //string userName = "Administrator"; + //string password = "xxxxxxx"; + //string domain = "CONTOSO"; + //ICredentials credentials = new NetworkCredential(userName, password, domain); + DownloadFileViaRestAPI(siteURL, credentials, "MyDocumentLib", "test.docx", "c:\\"); + Console.WriteLine("success"); + Console.ReadLine(); +} ``` -url: http://site url/_api/web/GetFileByServerRelativeUrl('/Folder Name/file name')/$value -method: GET -headers: - Authorization: "Bearer " + accessToken -``` - -The following example shows how to **create** a file and add it to a folder. - - - +The following example shows how to **create a file and add it to a folder**. +```http +POST https://{site_url}/_api/web/GetFolderByServerRelativeUrl('Folder Name')/Files/add(url='a.txt',overwrite=true) +Authorization: "Bearer " + accessToken +Content-Length: {length of request body as integer} +X-RequestDigest: "{form_digest_value}" +"Contents of file" ``` -url: http://site url/_api/web/GetFolderByServerRelativeUrl('/Folder Name')/Files/add(url='a.txt',overwrite=true) -method: POST -body: "Contents of file" -Headers: - Authorization: "Bearer " + accessToken - X-RequestDigest: form digest value - content-length:length of post body -``` - -The following example shows how to **update** a file by using the **PUT** method. - - +The following example shows how to **update a file by using the PUT method**. - **Note** **PUT** is the only method that you can use to update a file. The **MERGE** method is not allowed. - +> [!NOTE] +> **PUT** is the only method that you can use to update a file. The **MERGE** method is not allowed. +```http +POST https://{site_url}/_api/web/GetFileByServerRelativeUrl('/Folder Name/{file_name}')/$value +Authorization: "Bearer " + accessToken +Content-Length: {length of request body as integer} +X-HTTP-Method: "PUT" +X-RequestDigest: "{form_digest_value}" - - -``` -url: http://site url/_api/web/GetFileByServerRelativeUrl('/Folder Name/file name')/$value -method: POST -body: "Contents of file." -Headers: - Authorization: "Bearer " + accessToken - X-RequestDigest: form digest value - X-HTTP-Method:"PUT" - content-length:length of post body +"Contents of file" ``` -If you want to update a file's metadata, you'll have to construct an endpoint that reaches the file as a list item. You can do this because each folder is also a list, and each file is also a list item. Construct an endpoint that looks like this: `https:///_api/web/lists/getbytitle('Documents')/items()`. [Working with lists and list items with REST](working-with-lists-and-list-items-with-rest.md) explains how to update a list item's metadata. - +If you want to update a file's metadata, you have to construct an endpoint that reaches the file as a list item. You can do this because each folder is also a list, and each file is also a list item. Construct an endpoint that looks like this: `https://{site_url}/_api/web/lists/getbytitle('Documents')/items({item_id})`. For information about how to update a list item's metadata, see [Working with lists and list items with REST](working-with-lists-and-list-items-with-rest.md). - -You may want to check out a file in order to make sure that no one changes it before you update it. After your update, you should also may want to check the file back in so that others can work with it. The following example shows you how to **check a file out**. - +You may want to check out a file to make sure that no one changes it before you update it. After your update, you should check the file back in so that others can work with it. - +The following example shows how to **check out a file**. - - -``` -url: http://site url/_api/web/GetFileByServerRelativeUrl('/Folder Name/file name')/CheckOut(), -method: POST -headers: - Authorization: "Bearer " + accessToken - X-RequestDigest: form digest value +```http +POST https://{site_url}/_api/web/GetFileByServerRelativeUrl('/Folder Name/{file_name}')/CheckOut(), +Authorization: "Bearer " + accessToken +X-RequestDigest: "{form_digest_value}" ``` -The following example shows you how to **check a file in**. - - - - - +The following example shows how to **check in a file**. +```http +POST https://{site_url}/_api/web/GetFileByServerRelativeUrl('/Folder Name/{file_name}')/CheckIn(comment='Comment',checkintype=0) +Authorization: "Bearer " + accessToken +X-RequestDigest: "{form_digest_value}" ``` -url: http://site url/_api/web/GetFileByServerRelativeUrl('/Folder Name/file name')/CheckIn(comment='Comment',checkintype=0) -method: POST -headers: - Authorization: "Bearer " + accessToken - X-RequestDigest: form digest value -``` - -The following example shows how to **delete** a file. - - - - +The following example shows how to **delete a file**. +```http +POST https://{site_url}/_api/web/GetFileByServerRelativeUrl('/Folder Name/{file_name}') +Authorization: "Bearer " + accessToken +If-Match: "{etag or *}" +X-HTTP-Method: "DELETE" +X-RequestDigest: "{form_digest_value}" ``` -url: http://site url/_api/web/GetFileByServerRelativeUrl('/Folder Name/file name') -method: POST -headers: - Authorization: "Bearer " + accessToken - X-RequestDigest: form digest value - IF-MATCH: etag or "*" - X-HTTP-Method:"DELETE" - -``` - ## Working with large files by using REST - -When you need to upload a binary file that is larger than 1.5 megabytes (MB), the REST interface is your only option. See [Complete basic operations using JavaScript library code in SharePoint](complete-basic-operations-using-javascript-library-code-in-sharepoint.md) for a code example that shows you how to upload a binary file that is smaller than 1.5 MB by using the SharePoint Javascript object model. The maximum size of a binary file that you can create with REST is 2 gigabytes (GB.md). The following example shows how to **create** a large binary file. - +When you need to upload a binary file that is larger than 1.5 megabytes (MB), the REST interface is your only option. For a code example that shows you how to upload a binary file that is smaller than 1.5 MB by using the SharePoint JavaScript object model, see [Complete basic operations using JavaScript library code in SharePoint](complete-basic-operations-using-javascript-library-code-in-sharepoint.md). The maximum size of a binary file that you can create with REST is 2 gigabytes (GB). - +The following example shows how to **create a large binary file**. - **Caution** This approach will work only with Internet Explorer 10 and the latest versions of other browsers. - +> [!WARNING] +> This approach works only with Internet Explorer 10 and the latest versions of other browsers. +```http +POST https://{site_url}/_api/web/GetFolderByServerRelativeUrl('Folder Name')/Files/Add(url='{file_name}', overwrite=true) +Authorization: "Bearer " + accessToken +Content-Length: {length of request body as integer} +X-RequestDigest: "{form_digest_value}" +Contents of binary file ``` -url: http://site url/_api/web/GetFolderByServerRelativeUrl('/Folder Name')/Files/Add(url='file name', overwrite=true) -method: POST -body: contents of binary file -headers: - Authorization: "Bearer " + accessToken - X-RequestDigest: form digest value - content-type: "application/json;odata=verbose" - content-length:length of post body -``` - -The following code sample shows how to create a file by using this REST endpoint and the cross-domain library. - - - +The following code sample shows how to **create a file by using this REST endpoint and the JSOM cross-domain library**. - -``` +```javascript function uploadFileBinary() { -XDomainTestHelper.clearLog(); -var ro; -if (document.getElementById("TxtViaUrl").value.length > 0) { -ro = new SP.RequestExecutor(document.getElementById("TxtWebUrl").value, document.getElementById("TxtViaUrl").value); -} -else { -ro = new SP.RequestExecutor(document.getElementById("TxtWebUrl").value); -} -var body = ""; -for (var i = 0; i < 1000; i++) { -var ch = i % 256; -body = body + String.fromCharCode(ch); -} -var info = { -url: "_api/web/lists/getByTitle('Shared Documents')/RootFolder/Files/Add(url='a.dat', overwrite=true)", -method: "POST", -binaryStringRequestBody: true, -body: body, -success: success, -error: fail, -state: "Update" -}; -ro.executeAsync(info); + XDomainTestHelper.clearLog(); + var requestExecutor; + if (document.getElementById("TxtViaUrl").value.length > 0) { + requestExecutor = new SP.RequestExecutor(document.getElementById("TxtWebUrl").value, document.getElementById("TxtViaUrl").value); + } + else { + requestExecutor = new SP.RequestExecutor(document.getElementById("TxtWebUrl").value); + } + + var body = ""; + for (var i = 0; i < 1000; i++) { + var ch = i % 256; + body = body + String.fromCharCode(ch); + } + + var info = { + url: "_api/web/lists/getByTitle('Shared Documents')/RootFolder/Files/Add(url='a.dat', overwrite=true)", + method: "POST", + binaryStringRequestBody: true, + body: body, + success: success, + error: fail, + state: "Update" + }; + + requestExecutor.executeAsync(info); } ``` - ## Working with files attached to list items by using REST - - -The following example shows how to **retrieve** all of the files that are attached to a list item. - - +The following example shows how to **retrieve all of the files that are attached to a list item**. +```http +GET https://{site_url}/_api/web/lists/getbytitle('{list_title}')/items({item_id})/AttachmentFiles/ +Authorization: "Bearer " + accessToken +Accept: "application/json;odata=verbose" ``` -url: http://site url/_api/web/lists/getbytitle('list title')/items(item id)/AttachmentFiles/ -method: GET -headers: - Authorization: "Bearer " + accessToken - accept: "application/json;odata=verbose" or "application/atom+xml" - -``` - -The following example shows how to **retrieve** a file that is attached to a list item. - - - - +The following example shows how to **retrieve a file that is attached to a list item**. +```http +GET https://{site_url}/_api/web/lists/getbytitle('{list_title}')/items({item_id})/AttachmentFiles('{file_name}')/$value +Authorization: "Bearer " + accessToken +Accept: "application/json;odata=verbose" ``` -url: http://site url/_api/web/lists/getbytitle('list title')/items(item id)/AttachmentFiles('file name')/$value -method: GET -headers: - Authorization: "Bearer " + accessToken - accept: "application/json;odata=verbose" or "application/atom+xml" - -``` - -The following example shows how to **create** a file attachment to a list item. - - - +The following example shows how to **create a file attachment to a list item**. +```http +POST https://{site_url}/_api/web/lists/getbytitle('{list_title}')/items({item_id})/AttachmentFiles/ add(FileName='{file_name}') +Authorization: "Bearer " + accessToken +Content-Length: {length of request body as integer} +X-RequestDigest: "{form_digest_value}" +"Contents of file" ``` -url: http://site url/_api/web/lists/getbytitle('list title')/items(item id)/AttachmentFiles/ add(FileName='file name') -method: POST -headers: - Authorization: "Bearer " + accessToken - body: "Contents of file." - X-RequestDigest: form digest value - content-length:length of post body -``` - -The following example shows how to **update** a file attachment to a list item by using the **PUT** method. - - +The following example shows how to **update a file attachment to a list item by using the PUT method**. - **Note** **PUT** is the only method that you can use to update a file. The **MERGE** method is not allowed. - +> [!NOTE] +> **PUT** is the only method that you can use to update a file. The **MERGE** method is not allowed. +```http +POST https://{site_url}/_api/web/lists/getbytitle('{list_title}')/items({item_id})/AttachmentFiles('{file_name}')/$value +Authorization: "Bearer " + accessToken +Content-Length: {length of request body as integer} +X-HTTP-Method: "PUT" +X-RequestDigest: "{form_digest_value}" +"Contents of file" +``` +The following example shows how to **delete a file that is attached to a list item**. +```http +DELETE https://{site_url}/_api/web/lists/getbytitle('{list_title}')/items({item_id})/AttachmentFiles('{file_name}') +Authorization: "Bearer " + accessToken +Accept: "application/json;odata=verbose" ``` -url: http://site url/_api/web/lists/getbytitle('list title')/items(item id)/AttachmentFiles('file name')/$value -method: POST -body: "Contents of file." -headers: - Authorization: "Bearer " + accessToken - "X-HTTP-Method":"PUT" - X-RequestDigest: form digest value - content-length:length of post body -``` - -## Additional resources - - - -- [Complete basic operations using SharePoint REST endpoints](complete-basic-operations-using-sharepoint-rest-endpoints.md) - - -- [Files and folders REST API reference](http://msdn.microsoft.com/library/files-and-folders-rest-api-reference%28Office.15%29.aspx) - - -- [Upload a file by using the REST API and jQuery](upload-a-file-by-using-the-rest-api-and-jquery.md) - - -- [Working with lists and list items with REST](working-with-lists-and-list-items-with-rest.md) - - -- [SharePoint-Add-in-REST-OData-BasicDataOperations](https://github.com/OfficeDev/SharePoint-Add-in-REST-OData-BasicDataOperations) - - -- [SharePoint: Perform basic data access operations on files and folders by using REST](http://code.msdn.microsoft.com/SharePoint-Perform-ab9c4ae5) - - -- [Making REST calls with C# and JavaScript for SharePoint](http://www.microsoft.com/resources/msdn/en-us/office/media/video/videol?cid=sdc&from=mscomsdc&VideoID=4e4cc094-ff69-405b-852f-2ac7c41293c5) - - -- [Making REST calls with C# and JavaScript for SharePoint demo](http://www.microsoft.com/resources/msdn/en-us/office/media/video/videol?cid=sdc&from=mscomsdc&VideoID=b1e7c9c5-0f62-4a78-bb7b-8e283c86145c) - - -- [Complete basic operations using SharePoint client library code](complete-basic-operations-using-sharepoint-client-library-code.md) - - -- [Complete basic operations using JavaScript library code in SharePoint](complete-basic-operations-using-javascript-library-code-in-sharepoint.md) - - -- [Develop SharePoint Add-ins](develop-sharepoint-add-ins.md) - - -- [Secure data access and client object models for SharePoint Add-ins](secure-data-access-and-client-object-models-for-sharepoint-add-ins.md) - - -- [Work with external data in SharePoint](work-with-external-data-in-sharepoint.md) - - -- [Open Data Protocol](http://www.odata.org/) - - -- [OData: JavaScript Object Notation (JSON) Format](http://www.odata.org/documentation/odata-version-2-0/JSON-format/) - - - - - - +## See also +- [Get to know the SharePoint REST service](get-to-know-the-sharepoint-rest-service.md) +- [Complete basic operations using SharePoint client library code](complete-basic-operations-using-sharepoint-client-library-code.md) +- [Upload a file by using the REST API and jQuery](upload-a-file-by-using-the-rest-api-and-jquery.md) +- [SharePoint-Add-in-REST-OData-BasicDataOperations](https://github.com/OfficeDev/SharePoint-Add-in-REST-OData-BasicDataOperations) +- [Secure data access and client object models for SharePoint Add-ins](secure-data-access-and-client-object-models-for-sharepoint-add-ins.md) +- [Work with external data in SharePoint](work-with-external-data-in-sharepoint.md) +- [OData resources](get-to-know-the-sharepoint-rest-service.md#odata-resources) +- [Develop SharePoint Add-ins](develop-sharepoint-add-ins.md) diff --git a/docs/sp-add-ins/working-with-lists-and-list-items-with-rest.md b/docs/sp-add-ins/working-with-lists-and-list-items-with-rest.md index 644d8d218..7a4d93509 100644 --- a/docs/sp-add-ins/working-with-lists-and-list-items-with-rest.md +++ b/docs/sp-add-ins/working-with-lists-and-list-items-with-rest.md @@ -1,399 +1,638 @@ ---- -title: Working with lists and list items with REST -ms.date: 09/25/2017 -ms.prod: sharepoint ---- - - -# Working with lists and list items with REST -Learn how to perform basic create, read, update, and delete (CRUD) operations on lists and list items with the SharePoint REST interface. - - - **Note** The name "apps for SharePoint" is changing to "SharePoint Add-ins". During the transition, the documentation and the UI of some SharePoint products and Visual Studio tools might still use the term "apps for SharePoint". For details, see [New name for apps for Office and SharePoint](new-name-for-apps-for-sharepoint.md#bk_newname). - - - - **Tip** The SharePoint Online (and on-premise SharePoint 2016 and later) REST service supports combining multiple requests into a single call to the service by using the OData `$batch` query option. For details and links to code samples, see [Make batch requests with the REST APIs](make-batch-requests-with-the-rest-apis.md). - - - -## Prerequisites - -This topic assumes that you are already familiar with the topics [Get to know the SharePoint REST service](get-to-know-the-sharepoint-rest-service.md) and [Complete basic operations using SharePoint REST endpoints](complete-basic-operations-using-sharepoint-rest-endpoints.md). It does not provide code snippets. - - - - -## Retrieving lists and list properties with REST - - -The following example shows how to **retrieve** a specific list if you know its GUID. - - - - -``` -url: http://site url/_api/web/lists(guid'list GUID'), -method: GET -Headers: - Authorization: "Bearer " + accessToken - accept: "application/json;odata=verbose" or "application/atom+xml" - -``` - - - **Note** Use `application/json;odata=verbose` in the `accept` header if you want the response in JSON. Use `application/atom+xml` in the `accept` header if you want the response in Atom format. - - -The following example shows how to **retrieve** a specific list if you know its title. - - - - - - -``` -url: http://site url/_api/web/lists/GetByTitle('Test') -method: GET -Headers: - Authorization: "Bearer " + accessToken - accept: "application/json;odata=verbose" or "application/atom+xml" - -``` - -The following XML shows an example of the list properties that are returned when you request the XML content type. - - - - - - -```XML - - - true - 100 - 0 - false - 2012-06-26T23:15:58Z - 00000000-0000-0000-0000-000000000000 - A list created by Project Based Retention used to store Project Policy Items. - none - - 0 - true - false - false - false - false - ProjectPolicyItemList - false - false - true - 74de3ff3-029c-42f9-bd2a-1e9463def69d - /_layouts/15/images/itgen.gif - false - false - false - false - false - false - 0 - 2012-06-26T23:15:58Z - 2012-06-26T23:15:59Z - SP.Data.ProjectPolicyItemListItem - false - true - / - true - 00bfea71-de22-43b2-a848-c05709900100 - Project Policy Item List - - -``` - - - **Note** The **ListItemEntityTypeFullName** property ( **SP.Data.ProjectPolicyItemListItem** in the previous example) is especially important if you want to create and update list items. This value must be passed as the **type** property in the metadata that you pass in the body of the HTTP request whenever you create and update list items. - - - -## Working with lists by using REST - - -The following example shows how to **create** a list. - - - - -``` -url: http://site url/_api/web/lists -method: POST -body: { '__metadata': { 'type': 'SP.List' }, 'AllowContentTypes': true, 'BaseTemplate': 100, - 'ContentTypesEnabled': true, 'Description': 'My list description', 'Title': 'Test' } -Headers: - Authorization: "Bearer " + accessToken - X-RequestDigest: form digest value - accept: "application/json;odata=verbose" - content-type: "application/json;odata=verbose" - content-length:length of post body -``` - -The following example shows how to **update** a list by using the **MERGE** method. - - - - - - -``` -url: http://site url/_api/web/lists(guid'list GUID') -method: POST -body: { '__metadata': { 'type': 'SP.List' }, 'Title': 'New title' } -Headers: - Authorization: "Bearer " + accessToken - X-RequestDigest: form digest value - IF-MATCH": etag or "*" - X-HTTP-Method: MERGE, - accept: "application/json;odata=verbose" - content-type: "application/json;odata=verbose" - content-length:length of post body -``` - -The following example shows how to **create** a **custom field** for a list. - - - - - - -``` -Url: url: http://site url/_api/web/lists(guid'list GUID')/Fields -Method:POST -Body: { '__metadata': { 'type': 'SP.Field' }, 'Title': 'field title', 'FieldTypeKind': FieldType value,'Required': 'true/false', 'EnforceUniqueValues': 'true/false','StaticName': 'field name'} -Headers: - Authorization: "Bearer " + accessToken - X-RequestDigest: form digest value - content-type: "application/json;odata=verbose" - content-length:length of post body -``` - -The following example shows how to **delete** a list. - - - - - - -``` -url: http://site url/_api/web/lists(guid'list GUID') -method: POST -Headers: - Authorization: "Bearer " + accessToken - X-RequestDigest: form digest value - IF-MATCH: etag or "*" - X-HTTP-Method: DELETE - -``` - - -## Working with list items by using REST - - -The following example shows how to **retrieve** all of a list's items. - - - - - **Note** The OData $skip query option does not work when querying list items. In may situations, you can use the [$skiptoken](http://msdn.microsoft.com/library/4dda9434-c2c5-4577-8e01-7bf9e822d90a.aspx) option instead. - - - -``` -url: http://site url/_api/web/lists/GetByTitle('Test')/items -method: GET -headers: - Authorization: "Bearer " + accessToken - accept: "application/json;odata=verbose" or "application/atom+xml" - -``` - -The following example shows how to **retrieve** a specific list item. - - - - - - -``` -url: http://site url/_api/web/lists/GetByTitle('Test')/items(item id) -method: GET -headers: - Authorization: "Bearer " + accessToken - accept: "application/json;odata=verbose" or "application/atom+xml" - -``` - -The following XML shows an example of the list item properties that are returned when you request the XML content type. - - - - - - -```XML - - -0 -1 -1 -0x010049564F321A0F0543BA8C6303316C8C0F -an item -2012-07-24T22:47:26Z -2012-07-24T22:47:26Z -11 -11 -1.0 -false -eb6850c5-9a30-4636-b282-234eda8b1057 - - -``` - -The following example shows how to **create** a list item. - - - - - **Note** To do this operation, you must know the **ListItemEntityTypeFullName** property of the list and pass that as the value of **type** in the HTTP request body. - - - - - -``` -url: http://site url/_api/web/lists/GetByTitle('Test')/items -method: POST -body: { '__metadata': { 'type': 'SP.Data.TestListItem' }, 'Title': 'Test'} -headers: - Authorization: "Bearer " + accessToken - X-RequestDigest: form digest value - accept: "application/json;odata=verbose" - content-type: "application/json;odata=verbose" - content-length:length of post body -``` - -The following example shows how to **update** a list item. - - - - - **Note** To do this operation, you must know the **ListItemEntityTypeFullName** property of the list and pass that as the value of **type** in the HTTP request body. - - - - - -``` -url: http://site url/_api/web/lists/GetByTitle('Test')/items(item id) -method: POST -body: { '__metadata': { 'type': 'SP.Data.TestListItem' }, 'Title': 'TestUpdated'} -headers: - Authorization: "Bearer " + accessToken - X-RequestDigest: form digest value - "IF-MATCH": etag or "*" - "X-HTTP-Method":"MERGE", - accept: "application/json;odata=verbose" - content-type: "application/json;odata=verbose" - content-length:length of post body -``` - -The following example shows how to **delete** a list item. - - - - - - -``` -url: http://site url/_api/web/lists/GetByTitle('Test')/items(item id) -method: POST -headers: - Authorization: "Bearer " + accessToken - X-RequestDigest: form digest value - "IF-MATCH": etag or "*" - "X-HTTP-Method":"DELETE" - -``` - - -## Using ETag values to determine document and list item versioning - - -The SharePoint REST service, which follows the [OData standard](http://www.odata.org/developers/protocols/operations), uses [HTML ETags for concurrency control](http://www.odata.org/developers/protocols/operations#ConcurrencycontrolandETags) of SharePoint lists and list items. To check on an item's version when you perform a **PUT**, **MERGE**, or **DELETE** request, specify an **ETag** in the **If-Match** HTTP request header. - - - -If the **ETag** you specify in your request does not match the **ETag** of the document or list item on the server, the REST service returns a 412 exception, per the OData specification. - - - - -- To force an overwrite of the item regardless of version, set the **ETag** value to **"*"**. - - -- If you do not specify an **ETag**, SharePoint overwrites the item regardless of version. - - -Within SharePoint, ETags apply only to SharePoint lists and list items. - - - - -## Additional resources - - - -- [Complete basic operations using SharePoint REST endpoints](complete-basic-operations-using-sharepoint-rest-endpoints.md) - - -- [Working with folders and files with REST](working-with-folders-and-files-with-rest.md) - - -- [SharePoint-Add-in-REST-OData-BasicDataOperations](https://github.com/OfficeDev/SharePoint-Add-in-REST-OData-BasicDataOperations) - - -- [SharePoint: Perform basic data access operations on files and folders by using REST](http://code.msdn.microsoft.com/SharePoint-Perform-ab9c4ae5) - - -- [Making REST calls with C# and JavaScript for SharePoint](http://www.microsoft.com/resources/msdn/en-us/office/media/video/videol?cid=sdc&from=mscomsdc&VideoID=4e4cc094-ff69-405b-852f-2ac7c41293c5) - - -- [Making REST calls with C# and JavaScript for SharePoint demo](http://www.microsoft.com/resources/msdn/en-us/office/media/video/videol?cid=sdc&from=mscomsdc&VideoID=b1e7c9c5-0f62-4a78-bb7b-8e283c86145c) - - -- [Complete basic operations using SharePoint client library code](complete-basic-operations-using-sharepoint-client-library-code.md) - - -- [Complete basic operations using JavaScript library code in SharePoint](complete-basic-operations-using-javascript-library-code-in-sharepoint.md) - - -- [Develop SharePoint Add-ins](develop-sharepoint-add-ins.md) - - -- [Secure data access and client object models for SharePoint Add-ins](secure-data-access-and-client-object-models-for-sharepoint-add-ins.md) - - -- [Work with external data in SharePoint](work-with-external-data-in-sharepoint.md) - - -- [Open Data Protocol](http://www.odata.org/) - - -- [OData: JavaScript Object Notation (JSON) Format](http://www.odata.org/documentation/odata-version-2-0/json-format/) - - - - - - - +--- +title: Working with lists and list items with REST +description: Perform basic create, read, update, and delete (CRUD) operations on lists and list items with the SharePoint REST interface. +ms.date: 06/13/2022 +ms.localizationpriority: high +ms.service: sharepoint +--- + +# Working with lists and list items with REST + +> [!TIP] +> The SharePoint Online (and on-premises SharePoint 2016 and later) REST service supports combining multiple requests into a single call to the service by using the OData `$batch` query option. For details and links to code samples, see [Make batch requests with the REST APIs](make-batch-requests-with-the-rest-apis.md). + +## Prerequisites + +This topic assumes that you're already familiar with the topics [Get to know the SharePoint REST service](get-to-know-the-sharepoint-rest-service.md) and [Complete basic operations using SharePoint REST endpoints](complete-basic-operations-using-sharepoint-rest-endpoints.md). It doesn't provide code snippets. + +## Retrieving lists and list properties with REST + +The following example shows how to **retrieve a specific list if you know its GUID**. + +```http +GET https://{site_url}/_api/web/lists(guid'{list_guid}') +Authorization: "Bearer " + accessToken +Accept: "application/json;odata=verbose" +``` + +> [!NOTE] +> If you want the response in JSON, use `application/json;odata=verbose` in the `Accept` header. +> +> If you want the response in Atom format, use `application/atom+xml` in the `Accept` header. + +The following example shows how to **retrieve a specific list if you know its title**. + +```http +GET https://{site_url}/_api/web/lists/GetByTitle('List Title') +Authorization: "Bearer " + accessToken +Accept: "application/json;odata=verbose" +``` + +The following XML shows an example of the **list properties that are returned when you request the XML content type**. + +```xml + + + true + 100 + 0 + false + 2012-06-26T23:15:58Z + 00000000-0000-0000-0000-000000000000 + A list created by Project Based Retention used to store Project Policy Items. + none + + 0 + true + false + false + false + false + ProjectPolicyItemList + false + false + true + 74de3ff3-029c-42f9-bd2a-1e9463def69d + /_layouts/15/images/itgen.gif + false + false + false + false + false + false + 0 + 2012-06-26T23:15:58Z + 2012-06-26T23:15:59Z + SP.Data.ProjectPolicyItemListItem + false + true + / + true + 00bfea71-de22-43b2-a848-c05709900100 + Project Policy Item List + + +``` + +> [!NOTE] +> The **ListItemEntityTypeFullName** property (**SP.Data.ProjectPolicyItemListItem** in the previous example) is especially important if you want to create and update list items. This value must be passed as the **type** property in the metadata that you pass in the body of the HTTP request whenever you create and update list items. + +## Working with lists by using REST + +The following example shows how to **create a list**. + +```http +POST https://{site_url}/_api/web/lists +Authorization: "Bearer " + accessToken +Accept: "application/json;odata=verbose" +Content-Type: "application/json" +Content-Length: {length of request body as integer} +X-RequestDigest: "{form_digest_value}" + +{ + "__metadata": { + "type": "SP.List" + }, + "AllowContentTypes": true, + "BaseTemplate": 100, + "ContentTypesEnabled": true, + "Description": "My list description", + "Title": "Test" +} +``` + +The following example shows how to **update a list by using the MERGE method**. + +```http +POST https://{site_url}/_api/web/lists(guid'{list_guid}') +Authorization: "Bearer " + accessToken +Accept: "application/json;odata=verbose" +Content-Type: "application/json" +Content-Length: {length of request body as integer} +If-Match: "{etag or *}" +X-HTTP-Method: "MERGE" +X-RequestDigest: "{form_digest_value}" + +{ + "__metadata": { + "type": "SP.List" + }, + "Title": "New title" +} +``` + +The following example shows how to **create a custom field for a list**. + +```http +POST https://{site_url}/_api/web/lists(guid'{list_guid}')/Fields +Authorization: "Bearer " + accessToken +Accept: "application/json;odata=verbose" +Content-Type: "application/json" +Content-Length: {length of request body as integer} +X-RequestDigest: "{form_digest_value}" + +{ + "__metadata": { + "type": "SP.Field" + }, + "Title": "field title", + "FieldTypeKind": FieldType value, + "Required": "true/false", + "EnforceUniqueValues": "true/false", + "StaticName": "field name" +} +``` + +The following example shows how to **delete a list**. + +```http +POST https://{site_url}/_api/web/lists(guid'{list_guid}') +Authorization: "Bearer " + accessToken +Accept: "application/json;odata=verbose" +If-Match: "{etag or *}" +X-HTTP-Method: "DELETE" +X-RequestDigest: "{form_digest_value}" +``` + +### Lookup column changes + +When referring to a lookup column inside a list using REST API, use the display name of the lookup column instead of the internal name. + +```http +GET https://{site_url}/_api/web/lists/getbytitle('ListName')/Items?&$filter=LookupColumnId eq 1 +Authorization: "Bearer " + accessToken +Accept: "application/json;odata=verbose" +``` + +## Working with list items by using REST + +### Retrieve all list items + +The following example shows how to retrieve all of a list's items. + +> [!NOTE] +> +> - The OData `$skip` query parameter doesn't work when querying list items. In many situations, you can use the [$skiptoken](/openspecs/windows_protocols/ms-odata/4dda9434-c2c5-4577-8e01-7bf9e822d90a) option instead. +> - By default, this will return the first 100 items. More information on controlling the number of items, paging, etc. see the documentation on [OData Query operations](use-odata-query-operations-in-sharepoint-rest-requests.md) + +```http +GET https://{site_url}/_api/web/lists/GetByTitle('Test')/items +Authorization: "Bearer " + accessToken +Accept: "application/json;odata=verbose" +``` + +### Retrieve specific list item + +The following example shows how to retrieve a specific list item. + +```http +GET https://{site_url}/_api/web/lists/GetByTitle('Test')/items({item_id}) +Authorization: "Bearer " + accessToken +Accept: "application/json;odata=verbose" +``` + +The following XML shows an example of the list item properties that are returned when you request the XML content type. + +```xml + + + 0 + 1 + 1 + 0x010049564F321A0F0543BA8C6303316C8C0F + an item + 2012-07-24T22:47:26Z + 2012-07-24T22:47:26Z + 11 + 11 + 1.0 + false + eb6850c5-9a30-4636-b282-234eda8b1057 + + +``` + +### Retrieve items as a stream + +Retrieves information about the list and its data. Using this API you can retrieve list items in case they use complex fields such as lookups or managed metadata. + +```http +POST https://{site_url}/_api/web/GetList(@listUrl)/RenderListDataAsStream?@listUrl=%27%2Fsites%2Fteam-a%2Flists%2FList%27 +Authorization: "Bearer " + accessToken +Accept: "application/json;odata=nometadata" +Content-Type: "application/json;odata=nometadata" + +{ + "parameters": { + "AddRequiredFields": "true", + "DatesInUtc": "true", + "RenderOptions": 17 + } +} +``` + +#### RenderListDataAsStream URI parameters + +Following properties can be added as query string parameters to manipulate the returned data. + +Property | Description | Type | Example +-------- | ----------- | ---- | ------- +`CascDelWarnMessage`| Specifies if a message should be displayed if there's a cascade deletion warning | number | `1` +`DrillDown`| Specifies that some groups in a grouped view are expanded. Used with `GroupString`.| string | +`GroupString` | Group identifier used for drill-down feature. | string | +`HasOverrideSelectCommand` | Used to ensure that certain fields are present for proper functioning of the SharePoint ListView control. | string | +`Field`| Specifies a special field that should be included.| string | +`FieldInternalName`| Used to identify a field when a list has an external data source. Also used when filtering on a custom field.| string | +`Filter`| Specifies whether the requested view should have a filter applied.| string | +`FilterData`| Data specified by a particular filter.| string | +`FilterData1` | Data specified by a particular filter.| string | +`FilterData2` | Data specified by a particular filter.| string | +`FilterData3` | Data specified by a particular filter.| string | +`FilterData4` | Data specified by a particular filter.| string | +`FilterData5` | Data specified by a particular filter.| string | +`FilterData6` | Data specified by a particular filter.| string | +`FilterData7` | Data specified by a particular filter.| string | +`FilterData8` | Data specified by a particular filter.| string | +`FilterData9` | Data specified by a particular filter.| string | +`FilterData10` | Data specified by a particular filter.| string | +`FilterField` | A filter field name for a specific filter that is applied to the view.| string | +`FilterField1` | A filter field name for a specific filter that is applied to the view.| string | `ID` +`FilterField2` | A filter field name for a specific filter that is applied to the view.| string | `ID` +`FilterField3` | A filter field name for a specific filter that is applied to the view.| string | `ID` +`FilterField4` | A filter field name for a specific filter that is applied to the view.| string | `ID` +`FilterField5` | A filter field name for a specific filter that is applied to the view.| string | `ID` +`FilterField6` | A filter field name for a specific filter that is applied to the view.| string | `ID` +`FilterField7` | A filter field name for a specific filter that is applied to the view.| string | `ID` +`FilterField8` | A filter field name for a specific filter that is applied to the view.| string | `ID` +`FilterField9` | A filter field name for a specific filter that is applied to the view.| string | `ID` +`FilterField10`| A filter field name for a specific filter that is applied to the view.| string | `ID` +`FilterFields` | Specifies multiple fields that are being filtered on for a multiplier filter. | string | +`FilterFields1`| Specifies multiple fields that are being filtered on for a multiplier filter. | string | +`FilterFields2`| Specifies multiple fields that are being filtered on for a multiplier filter. | string | +`FilterFields3`| Specifies multiple fields that are being filtered on for a multiplier filter. | string | +`FilterFields4`| Specifies multiple fields that are being filtered on for a multiplier filter. | string | +`FilterFields5`| Specifies multiple fields that are being filtered on for a multiplier filter. | string | +`FilterFields6`| Specifies multiple fields that are being filtered on for a multiplier filter. | string | +`FilterFields7`| Specifies multiple fields that are being filtered on for a multiplier filter. | string | +`FilterFields8`| Specifies multiple fields that are being filtered on for a multiplier filter. | string | +`FilterFields9`| Specifies multiple fields that are being filtered on for a multiplier filter. | string | +`FilterFields10`| Specifies multiple fields that are being filtered on for a multiplier filter. | string | +`FilterValue` | The filter value associated with a particular filter. For example, FilterField3 goes with FilterValue3, and so forth. | string | +`FilterValue1` | The filter value associated with a particular filter. For example, FilterField3 goes with FilterValue3, and so forth. | string | `1` +`FilterValue2` | The filter value associated with a particular filter. For example, FilterField3 goes with FilterValue3, and so forth. | string | `1` +`FilterValue3` | The filter value associated with a particular filter. For example, FilterField3 goes with FilterValue3, and so forth. | string | `1` +`FilterValue4` | The filter value associated with a particular filter. For example, FilterField3 goes with FilterValue3, and so forth. | string | `1` +`FilterValue5` | The filter value associated with a particular filter. For example, FilterField3 goes with FilterValue3, and so forth. | string | `1` +`FilterValue6` | The filter value associated with a particular filter. For example, FilterField3 goes with FilterValue3, and so forth. | string | `1` +`FilterValue7` | The filter value associated with a particular filter. For example, FilterField3 goes with FilterValue3, and so forth. | string | `1` +`FilterValue8` | The filter value associated with a particular filter. For example, FilterField3 goes with FilterValue3, and so forth. | string | `1` +`FilterValue9` | The filter value associated with a particular filter. For example, FilterField3 goes with FilterValue3, and so forth. | string | `1` +`FilterValue10`| The filter value associated with a particular filter. For example, FilterField3 goes with FilterValue3, and so forth. | string | `1` +`FilterValues` | Used with FilterFields for multiplier filter. For example, FilterFields3 would go with FilterValues3, and so forth.| string | +`FilterValues1`| Used with FilterFields for multiplier filter. For example, FilterFields3 would go with FilterValues3, and so forth.| string | +`FilterValues2`| Used with FilterFields for multiplier filter. For example, FilterFields3 would go with FilterValues3, and so forth.| string | +`FilterValues3`| Used with FilterFields for multiplier filter. For example, FilterFields3 would go with FilterValues3, and so forth.| string | +`FilterValues4`| Used with FilterFields for multiplier filter. For example, FilterFields3 would go with FilterValues3, and so forth.| string | +`FilterValues5`| Used with FilterFields for multiplier filter. For example, FilterFields3 would go with FilterValues3, and so forth.| string | +`FilterValues6`| Used with FilterFields for multiplier filter. For example, FilterFields3 would go with FilterValues3, and so forth.| string | +`FilterValues7`| Used with FilterFields for multiplier filter. For example, FilterFields3 would go with FilterValues3, and so forth.| string | +`FilterValues8`| Used with FilterFields for multiplier filter. For example, FilterFields3 would go with FilterValues3, and so forth.| string | +`FilterValues9`| Used with FilterFields for multiplier filter. For example, FilterFields3 would go with FilterValues3, and so forth.| string | +`FilterValues10`| Used with FilterFields for multiplier filter. For example, FilterFields3 would go with FilterValues3, and so forth.| string | +`FilterLookupId`| Used when filtering on a lookup field. This is the item id in the foreign list that has a value that is being filtered on.| string | +`FilterLookupId1` | Used when filtering on a lookup field. This is the item id in the foreign list that has a value that is being filtered on.| string | +`FilterLookupId2` | Used when filtering on a lookup field. This is the item id in the foreign list that has a value that is being filtered on.| string | +`FilterLookupId3` | Used when filtering on a lookup field. This is the item id in the foreign list that has a value that is being filtered on.| string | +`FilterLookupId4` | Used when filtering on a lookup field. This is the item id in the foreign list that has a value that is being filtered on.| string | +`FilterLookupId5` | Used when filtering on a lookup field. This is the item id in the foreign list that has a value that is being filtered on.| string | +`FilterLookupId6` | Used when filtering on a lookup field. This is the item id in the foreign list that has a value that is being filtered on.| string | +`FilterLookupId7` | Used when filtering on a lookup field. This is the item id in the foreign list that has a value that is being filtered on.| string | +`FilterLookupId8` | Used when filtering on a lookup field. This is the item id in the foreign list that has a value that is being filtered on.| string | +`FilterLookupId9` | Used when filtering on a lookup field. This is the item id in the foreign list that has a value that is being filtered on.| string | +`FilterLookupId10` | Used when filtering on a lookup field. This is the item id in the foreign list that has a value that is being filtered on.| string | +`FilterOnly`| | string | +`FilterOp` | Filter operator. Used when filtering with other operators than Eq (`Geq`, `Leq` etc.) | string | `Geq` +`FilterOp1`| Filter operator. Used when filtering with other operators than Eq (`Geq`, `Leq` etc.) | string | `Geq` +`FilterOp2`| Filter operator. Used when filtering with other operators than Eq (`Geq`, `Leq` etc.) | string | `Geq` +`FilterOp3`| Filter operator. Used when filtering with other operators than Eq (`Geq`, `Leq` etc.) | string | `Geq` +`FilterOp4`| Filter operator. Used when filtering with other operators than Eq (`Geq`, `Leq` etc.) | string | `Geq` +`FilterOp5`| Filter operator. Used when filtering with other operators than Eq (`Geq`, `Leq` etc.) | string | `Geq` +`FilterOp6`| Filter operator. Used when filtering with other operators than Eq (`Geq`, `Leq` etc.) | string | `Geq` +`FilterOp7`| Filter operator. Used when filtering with other operators than Eq (`Geq`, `Leq` etc.) | string | `Geq` +`FilterOp8`| Filter operator. Used when filtering with other operators than Eq (`Geq`, `Leq` etc.) | string | `Geq` +`FilterOp9`| Filter operator. Used when filtering with other operators than Eq (`Geq`, `Leq` etc.) | string | `Geq` +`FilterOp10`| Filter operator. Used when filtering with other operators than Eq (`Geq`, `Leq` etc.) | string | `Geq` +`ID`| The item id of the item whose information is being sought.| number | +`InplaceSearchQuery`| Search term for a full list search.| string | +`InplaceFullListSearch`| A boolean that specifies whether there's a full list search. | string | +`IsCSR`| Whether this view is a client side rendered view. | string | +`CustomAction` | | string | +`IsGroupRender`| Used to set the IsGroupRender property of the SPView. | string | +`IsRibbon` | | string | +`IsXslView`| Whether this view is an xslt list view.| string | +`List` | | string | +`ListId`| | string | +`ListViewPageUrl` | | string | +`OverrideScope`| Used to override a scope on the rendered view: SPView.Scope| string | +`OverrideSelectCommand`| Used to make sure that certain fields are present in the query regardless of whether they're explicitly included in the view. | string | +`PageFirstRow` | Paging information about the first row that is requested. Used for paging list views. | string | +`PageLastRow` | Paging information about the last row that is requested. Used for paging list views. | string | +`RootFolder`| The folder that the view is displaying.| string | +`SortField`| A field that the view should be sorted on.| string | `ID` +`SortField1`| A field that the view should be sorted on.| string | `ID` +`SortField2`| A field that the view should be sorted on.| string | `ID` +`SortField3`| A field that the view should be sorted on.| string | `ID` +`SortField4`| A field that the view should be sorted on.| string | `ID` +`SortField5`| A field that the view should be sorted on.| string | `ID` +`SortField6`| A field that the view should be sorted on.| string | `ID` +`SortField7`| A field that the view should be sorted on.| string | `ID` +`SortField8`| A field that the view should be sorted on.| string | `ID` +`SortField9`| A field that the view should be sorted on.| string | `ID` +`SortField10` | A field that the view should be sorted on.| string | `ID` +`SortFields`| Specifies the name of the first field to sort by | string | +`SortFieldValues` | Specifies the name of the first field to sort by | string | +`SortDir` | The sort direction of an ad hoc sort that is being applied to the view.| string | `Desc` +`SortDir1` | The sort direction of an ad hoc sort that is being applied to the view.| string | `Desc` +`SortDir2` | The sort direction of an ad hoc sort that is being applied to the view.| string | `Desc` +`SortDir3` | The sort direction of an ad hoc sort that is being applied to the view.| string | `Desc` +`SortDir4` | The sort direction of an ad hoc sort that is being applied to the view.| string | `Desc` +`SortDir5` | The sort direction of an ad hoc sort that is being applied to the view.| string | `Desc` +`SortDir6` | The sort direction of an ad hoc sort that is being applied to the view.| string | `Desc` +`SortDir7` | The sort direction of an ad hoc sort that is being applied to the view.| string | `Desc` +`SortDir8` | The sort direction of an ad hoc sort that is being applied to the view.| string | `Desc` +`SortDir9` | The sort direction of an ad hoc sort that is being applied to the view.| string | `Desc` +`SortDir10`| The sort direction of an ad hoc sort that is being applied to the view.| string | `Desc` +`View` | Specifies the base view that will be used to render the list. | GUID| `3d13559e-3071-5000-76b8-8f1ca6b835f0` +`ViewPath` | Specifies the path of the view that will be used to render the list. If `ViewId` is given, then the `ViewId` will be used and this parameter will be ignored. | string | +`ViewCount`| When multiple list views are on a page, this identifies one of them. | string | +`ViewId`| Specifies the base view that will be used to render the list. ad-hoc parameters will be applied on top of this view. If both `ViewXml` and `BaseViewId` are given, then the `ViewXml` will be used and the ad-hoc parameters will be ignored. | string | +`WebPartId`| The id of the list view web part that is showing this view.| string | + +#### RenderListDataAsStream body parameter properties + +Property | Description | Type | Example +-------- | ----------- | ---- | ------- +`AddRequiredFields` | Specifies if required fields should be returned or not | bool| `true` +`AllowMultipleValueFilterForTaxonomyFields` | Specifies if multi value filtering is allowed for taxonomy fields or not | bool| `true` +`DatesInUtc`| Specifies if we return DateTime field in UTC or local time.| bool| `true` +`ExpandGroups`| Specifies if the grouping should be expanded or not. | bool| `true` +`FirstGroupOnly`| Specifies if only the first group should be returned or not (regardless of view schema). | bool| `true` +`FolderServerRelativeUrl` | Specifies the url to the folder from which to return items.| string| `/sites/team-a/lists/Orders/Europe` +`ImageFieldsToTryRewriteToCdnUrls`| Comma-separated list of field names whose values should be rewritten to CDN URLs | string| `ArticleImage,SecondaryImage` +`OverrideViewXml` | Specifies the override XML to be combined with the View CAML. Applies only to the `Query/Where` part of the View CAML. | string| `3` +`Paging`| Specifies the paging information.| string +`RenderOptions` | Specifies the type of output to return.| SPRenderListDataOptions | See the next section for possible values. You can specify multiple values by adding their values together +`ReplaceGroup`| Specifies if the grouping should be replaced or not to deal with GroupBy throttling. | bool| `true` +`ViewXml` | Specifies the CAML view XML. | string + +##### SPRenderListDataOptions options + +Label | Description | Value +----- | ----------- | ----- +`None`| Return default output| `0` +`ContextInfo` | Return list context information| `1` +`ListData`| Return list data (same as `None`)| `2` +`ListSchema`| Return list schema | `4` +`MenuView`| Return HTML for the list menu| `8` +`ListContentType` | Returns information about list content types. Must be combined with the `ContextInfo` flag | `16` +`FileSystemItemId`| The returned list will have a FileSystemItemId field on each item if possible. Must be combined with the `ListData` flag | `32` +`ClientFormSchema`| Returns the client form schema to add and edit items | `64` +`QuickLaunch` | Returns QuickLaunch navigation nodes | `128` +`Spotlight` | Returns Spotlight rendering information| `256` +`Visualization` | Returns Visualization rendering information| `512` +`ViewMetadata`| Returns view XML and other information about the current view| `1024` +`DisableAutoHyperlink`| Prevents AutoHyperlink from being run on text fields in this query | `2048` +`EnableMediaTAUrls` | Enables URLs pointing to Media TA service, such as `.thumbnailUrl`, `.videoManifestUrl`, `.pdfConversionUrls`| `4096` +`ParentInfo`| Returns parent folder information| `8192` +`PageContextInfo` | Returns page context info for the current list being rendered| `16384` +`ClientSideComponentManifest` | Return client-side component manifest information associated with the list (reserved for future use) | `32768` + +#### Examples + +##### Retrieve item with specific ID + +```http +POST https://{site_url}/sites/team-a/_api/web/GetList(@listUrl)/RenderListDataAsStream?@listUrl=%27%2Fsites%2Fteam-a%2Flists%2FList%27&FilterField1=ID&FilterValue1=1 +Authorization: "Bearer " + accessToken +Accept: "application/json;odata=nometadata" +... +``` + +##### Sort items descending by ID + +```http +POST https://{site_url}/sites/team-a/_api/web/GetList(@listUrl)/RenderListDataAsStream?@listUrl=%27%2Fsites%2Fteam-a%2Flists%2FList%27&SortField=ID&SortDir=Desc +Authorization: "Bearer " + accessToken +Accept: "application/json;odata=nometadata" +... +``` + +##### Retrieve items from the specified folder + +```http +POST https://{site_url}/sites/team-a/_api/web/GetList(@listUrl)/RenderListDataAsStream?@listUrl=%27%2Fsites%2Fteam-a%2Flists%2FOrders%27 +Authorization: "Bearer " + accessToken +Accept: "application/json;odata=nometadata" +Content-Type: "application/json" + +{ + "parameters": { + "FolderServerRelativeUrl": "/sites/team-a/lists/Orders/Europe" + } +} +``` + +##### Retrieve list schema + +```http +POST https://{site_url}/sites/team-a/_api/web/GetList(@listUrl)/RenderListDataAsStream?@listUrl=%27%2Fsites%2Fteam-a%2Flists%2FList%27 +Authorization: "Bearer " + accessToken +Accept: "application/json;odata=nometadata" +Content-Type: "application/json" + +{ + "parameters": { + "RenderOptions": 4 + } +} +``` + +##### Retrieve information about list content types + +```http +POST https://{site_url}/sites/team-a/_api/web/GetList(@listUrl)/RenderListDataAsStream?@listUrl=%27%2Fsites%2Fteam-a%2Flists%2FList%27 +Authorization: "Bearer " + accessToken +Accept: "application/json;odata=nometadata" +Content-Type: "application/json" + +{ + "parameters": { + "RenderOptions": 17 + } +} +``` + +### Create list item + +The following example shows how to create a list item. + +> [!NOTE] +> To do this operation, you must know the **ListItemEntityTypeFullName** property of the list and pass that as the value of **type** in the HTTP request body. Following is a sample rest call to get the ListItemEntityTypeFullName +> +> ```http +> GET https://{site_url}/_api/web/lists/GetByTitle('Test')?$select=ListItemEntityTypeFullName +> Authorization: "Bearer " + accessToken +> Accept: "application/json;odata=nometadata" +> ``` + +```http +POST https://{site_url}/_api/web/lists/GetByTitle('Test')/items +Authorization: "Bearer " + accessToken +Accept: "application/json;odata=verbose" +Content-Type: "application/json;odata=verbose" +Content-Length: {length of request body as integer} +X-RequestDigest: "{form_digest_value}" + +{ + "__metadata": { + "type": "SP.Data.TestListItem" + }, + "Title": "Test" +} +``` + +### Create list item in a folder + +The following example shows how to create a list item in a folder. + +```http +POST https://{site_url}/_api/web/lists/GetByTitle('Test')/AddValidateUpdateItemUsingPath +Authorization: "Bearer " + accessToken +Accept "application/json;odata=nometadata" +Content-Type "application/json;odata=nometadata" +X-RequestDigest "The appropriate digest for current site" + +{ + "listItemCreateInfo": { + "FolderPath": { + "DecodedUrl": "https://{site_url}/lists/Test/Folder/SubFolder" + }, + "UnderlyingObjectType": 0 + }, + "formValues": [ + { + "FieldName": "Title", + "FieldValue": "Item" + } + ], + "bNewDocumentUpdate": false +} +``` + +#### Request property details + +Property | Description +-------- | ----------- +`listItemCreateInfo` | Information about the list and folder where the item should be created +`listItemCreateInfo.FolderPath.DecodedUrl` | Absolute URL of the folder where the item should be created +`listItemCreateInfo.UnderlyingObjectType`| Type of item to create. For more information, see [FileSystemObjectType](/previous-versions/office/developer/sharepoint-2010/ee537053(v=office.14)) +`formValues` | Array of field names and values to set on the newly created item +`bNewDocumentUpdate` | Set to `false` to create a list item + +#### Responses + +| Name | Type |Description| +|--------|---------|-----------| +|200 OK | Boolean |Success | + +```json +{ + "value": [ + { + "ErrorMessage": null, + "FieldName": "Title", + "FieldValue": "Item", + "HasException": false, + "ItemId": 0 + }, + { + "ErrorMessage": null, + "FieldName": "Id", + "FieldValue": "1", + "HasException": false, + "ItemId": 0 + } + ] +} +``` + +The `value` property contains the list of properties that have been set when creating the list item. + +### Update list item + +The following example shows how to update a list item. + +> [!NOTE] +> To do this operation, you must know the **ListItemEntityTypeFullName** property of the list and pass that as the value of **type** in the HTTP request body. + +```http +POST https://{site_url}/_api/web/lists/GetByTitle('Test')/items({item_id}) +Authorization: "Bearer " + accessToken +Accept: "application/json;odata=verbose" +Content-Type: "application/json" +Content-Length: {length of request body as integer} +If-Match: "{etag or *}" +X-HTTP-Method: "MERGE" +X-RequestDigest: "{form_digest_value}" + +{ + "__metadata": { + "type": "SP.Data.TestListItem" + }, + "Title": "TestUpdated" +} +``` + +### Delete list item + +The following example shows how to delete a list item. + +```http +POST https://{site_url}/_api/web/lists/GetByTitle('Test')/items({item_id}) +Authorization: "Bearer " + accessToken +Accept: "application/json;odata=verbose" +Content-Type: "application/json" +If-Match: "{etag or *}" +X-HTTP-Method: "DELETE" +``` + +## Using ETag values to determine document and list item versioning + +The SharePoint REST service, which follows the [OData standard](https://www.odata.org/developers/protocols/operations), uses [Header ETags](https://docs.oasis-open.org/odata/odata/v4.01/cs01/part1-protocol/odata-v4.01-cs01-part1-protocol.html#sec_HeaderETag) of SharePoint lists and list items. To check on an item's version when you perform a **PUT**, **MERGE**, or **DELETE** request, specify an **ETag** in the **If-Match** HTTP request header. + +If the **ETag** you specify in your request doesn't match the **ETag** of the document or list item on the server, the REST service returns a 412 exception, per the OData specification. + +- To force an overwrite of the item regardless of version, set the **ETag** value to **"*"**. +- If you do not specify an **ETag**, SharePoint overwrites the item regardless of version. + +Within SharePoint, ETags apply only to SharePoint lists and list items. + +## See also + +- [Get to know the SharePoint REST service](get-to-know-the-sharepoint-rest-service.md) +- [SharePoint-Add-in-REST-OData-BasicDataOperations](https://github.com/OfficeDev/SharePoint-Add-in-REST-OData-BasicDataOperations) +- [SharePoint: Perform basic data access operations on files and folders by using REST](/sharepoint/dev/sp-add-ins/working-with-folders-and-files-with-rest) +- [Secure data access and client object models for SharePoint Add-ins](secure-data-access-and-client-object-models-for-sharepoint-add-ins.md) +- [Work with external data in SharePoint](work-with-external-data-in-sharepoint.md) +- [OData resources](get-to-know-the-sharepoint-rest-service.md#odata-resources) +- [Develop SharePoint Add-ins](develop-sharepoint-add-ins.md) diff --git a/docs/spfx/build-for-teams-configure-in-teams.md b/docs/spfx/build-for-teams-configure-in-teams.md new file mode 100644 index 000000000..cd3dae93c --- /dev/null +++ b/docs/spfx/build-for-teams-configure-in-teams.md @@ -0,0 +1,68 @@ +--- +title: Configure SharePoint Framework web parts in Microsoft Teams +description: To accommodate your users' preferences, you can let them configure your web parts when used in Microsoft Teams. +ms.date: 06/15/2020 +ms.localizationpriority: medium +--- + +# Configure SharePoint Framework web parts in Microsoft Teams + +To accommodate your users' preferences, you can let them configure your web parts when used in Microsoft Teams. Depending if you exposed your web parts as Teams tabs or personal apps, there are different ways to implement configuration capabilities in your web part. + +> [!TIP] +> To see how to use the different concepts described in this article, see the sample [Leads application](https://github.com/pnp/sp-dev-solutions/tree/master/solutions/LeadsLOBSolution) on GitHub. + +## Configure Microsoft Teams tabs built using SharePoint Framework + +Typically, when building Microsoft Teams tab, you need to [build custom UI to allow users to configure your tab](/microsoftteams/platform/tabs/how-to/create-tab-pages/configuration-page). Additionally, you need to write code to store and load configuration values as selected by the user. + +When building tabs using SharePoint Framework, the generated tab uses the [web part property pane](web-parts/guidance/integrate-web-part-properties-with-sharepoint.md) to let users configure the tab. This saves you a lot of effort. Not only you don’t need to build and maintain a separate configuration UI but you also don’t need to implement any code responsible for storing and managing the settings. All of that is handled automatically for you by SharePoint Framework. + +## Configure Microsoft Teams personal apps built using SharePoint Framework + +Microsoft Teams personal apps don’t offer any infrastructure for implementing configuration. Instead, following the pattern recommended by Microsoft Teams, personal app’s [settings should be exposed in a separate tab](/microsoftteams/platform/concepts/design/personal-apps). + +![Personal app with multiple tabs including settings built using SharePoint Framework](../images/build-for-teams/build-for-teams-personal-app.png) + +Translating this to your SharePoint Framework solution, it means building a separate web part that contains the configuration UI and which will be used only in the context of the personal app, defining a storage for user’s configuration and extending the personal app definition to contain multiple tabs. + +### Personal app configuration UI web part + +Each tab in a personal app is mapped to a URL. The easiest way to build UI for configuring your SharePoint Framework web part exposed as a personal app, is by building a separate web part with the configuration UI. This will allow you to optimize the UI for use in the context of the personal app and keep this code separate from your web part. + +Since the personal app configuration UI web part is not meant to be used outside of the personal app, in its manifest, you should set the `supportedHosts` property to an empty array. + +![The supportedHosts property of a SharePoint Framework web part used as a personal app configuration UI](../images/build-for-teams/build-for-teams-manifest-settings-webpart.png) + +The reason you want the `supportedHosts` property to be empty is to prevent the web part from being used in SharePoint but also not include it in the autogenerated Teams manifest. By default, each web part that contains the `TeamsTab` or `TeamsPersonalApp` value in the manifest is included in the generated Teams manifest as a separate Microsoft Teams app. In this case however, you want the personal app to consist of multiple tabs, each pointing to a different web part. This can be done only by manually updating the manifest yourself. + +To add a tab to your personal app and have it point to another web part, in the Teams manifest defined in the **teams/manifest.json** file, navigate to the `staticTabs` section and copy the existing entry. In the copied entry, update values of the `entityId` and `name` properties. In the `contentUrl` property, update the value of the `componentId` query string parameter so that it matches the ID of your settings web part as defined in its manifest. + +![Microsoft Teams app's manifest defining a personal app with multiple tabs pointing to SharePoint Framework web parts](../images/build-for-teams/build-for-teams-teams-manifest-personalapp-multipletabs.png) + +### Choose the location to store user’s configuration + +By default, web parts’ configuration is shared and the same for all users. Personal Teams apps are however meant to be installed, configured and used by individuals. As such, you need to have a way to store their preferences. + +#### Store user configuration in the User Profile Service + +In the past, a common way to store user-specific information in SharePoint, was by adding a custom property to the user profile service and storing the configuration as a serialized string in there. The problem with using the user profile service for this purpose is that you can’t automatically generate new user profile properties which complicates the deployment of your application. + +#### Store user configuration in a custom list + +Alternatively, you could store user settings in a list. You could create a hidden list in the root SharePoint site and configure it so that users can see only their items. The downside of this approach is that each time your web part starts you need to check if the list and the settings for the current user exist and gracefully handle errors in case they don’t. Additionally, when loading the configuration UI you would need to check if the list exists and provision it, along with all its settings if necessary. + +#### Store user configuration in application’s personal folder + +One of the less-known options for persisting application- and user-specific configuration is using the [application’s personal folder](/graph/api/drive-get-specialfolder?tabs=http). Application’s specific folder is located in the user’s OneDrive for Business site. Each application gets a designated folder in which it can store any number of files. + +![Application’s personal folder created for SharePoint Framework applications](../images/build-for-teams/build-for-teams-application-personal-folder.png) + +You can think of the application’s personal folder as a configuration folder of a desktop application on your disk, but then stored in OneDrive for Business and available on every device you use. + +> [!TIP] +> Application’s personal folders are created per Azure AD application, so in the context of SharePoint Framework, all SharePoint Framework solutions will share the root application folder. To avoid collisions with other solutions, you should consider creating a subfolder for your application. + +From the technical point of view, the application’s personal folder is a folder in a SharePoint document library, and you can store any number of files and folders inside. When persisting your application’s configuration to the application’s personal folder, you would serialize your settings as configured by the user and write the serialized data to a file in your folder. + +What’s convenient about using the application’s personal folder is that it’s automatically created if it doesn’t exist, data for each user is stored in their own OneDrive for Business meaning other users cannot see or tamper with their settings and it doesn’t require you to know any specific URLs because you can conveniently access it using Microsoft Graph. Should anything go wrong, user can navigate to their OneDrive for Business site and delete the application’s configuration to reset its state. diff --git a/docs/spfx/build-for-teams-considerations.md b/docs/spfx/build-for-teams-considerations.md new file mode 100644 index 000000000..5cdece1f9 --- /dev/null +++ b/docs/spfx/build-for-teams-considerations.md @@ -0,0 +1,43 @@ +--- +title: Considerations for building for Microsoft Teams using SharePoint Framework +description: There are a number of things that you should take into account when building for Microsoft Teams using SharePoint Framework +ms.date: 03/08/2023 +ms.localizationpriority: medium +--- + +# Considerations for building for Microsoft Teams using SharePoint Framework + +While using SharePoint Framework to build for Microsoft Teams offers you benefits, there are some considerations that you should take into account before building your next application. + +> [!TIP] +> To see how to use the different concepts described in this article, see the sample [Leads application](https://github.com/pnp/sp-dev-solutions/tree/master/solutions/LeadsLOBSolution) on GitHub. + +## Globally deploy the SharePoint Framework solution package + +When using SharePoint Framework to build web parts that will be exposed in Microsoft Teams you should allow the solution to be globally deployed. This setting is controlled when creating the project but can also be adjusted later in the **package-solution.json** file by setting the `skipFeatureDeployment` property to `true`. + +When the solution is globally deployed in your tenant, users can add tabs to any channel and install personal apps. + +## Expose existing application in Microsoft Teams + +If you have an existing web application, most likely you will not migrate it to SharePoint Framework. Since the application is already working, the easiest way to expose it in Microsoft Teams is by [creating a manifest for it](/microsoftteams/platform/tabs/what-are-tabs). + +Depending how your application is built, you might need to ensure that users can correctly sign into your application and that the application can securely access its APIs. When users work with your application in Microsoft Teams, the application loads inside an ` -
Recent orders
-
- - - - - - - - - - - - - - - - - `; - - this.context.statusRenderer.displayLoadingIndicator( - this.domElement.querySelector(".loading"), "orders"); - - this.domElement.querySelector("iframe").addEventListener("load", (): void => { - this.remotePartyLoaded = true; - }); - - this.executeOrDelayUntilRemotePartyLoaded((): void => { - // retrieve and render data - }); - } - - private executeOrDelayUntilRemotePartyLoaded(func: Function): void { - if (this.remotePartyLoaded) { - func(); - } else { - setTimeout((): void => { this.executeOrDelayUntilRemotePartyLoaded(func); }, 100); - } - } - - // ... -} -``` - -When executing AJAX requests in your web parts, you have to specify, that the authentication cookie should be sent cross-domain. You can enable this, by setting the **credentials** property of the web request to **include**. Without this, the request will be blocked in browser and you will not be able to call the API. - -```ts -// ... - -export default class LatestOrdersWebPart extends BaseClientSideWebPart { - // ... - - private retrieveAndRenderData(): void { - this.context.httpClient.get("https://contoso.azurewebsites.net/api/orders", - HttpClient.configurations.v1, { - credentials: "include" - }) - .then((response: HttpClientResponse): Promise => { - // ... - }); - } - - // ... -} -``` - -To support this method, the custom API requires some specific configuration as well. First of all it requires to support receiving credentials from cross-domain calls. This is done by setting the **Access-Control-Allow-Credentials** response header to **true**. -Next, it needs to specify what origin is allowed to call the API. This is configured in the **Access-Control-Allow-Origin** response header. - -> **Important:** When using the **Access-Control-Allow-Credentials** you are allowed to specify only one origin. - -How these headers should be configured exactly, depends on the implementation of your API. If you use an Azure Function to build the API using Node.js for example, you would set these headers on the response object: - -```js -context.res = { - body: "response", - headers: { - "Access-Control-Allow-Credentials" : "true", - "Access-Control-Allow-Origin" : "https://contoso.sharepoint.com" - } -}; -``` - -When using ASP.NET Web API, you would install the **Microsoft.AspNet.WebApi.Cors** NuGet package, call the `config.EnableCors()` method and use the **EnableCors** attribute to set the header values: - -```cs -[EnableCors("origins": "*", "headers": "*", "methods": "*", SupportsCredentials = true)] -public string Get() { - return "response"; -} -``` - -#### Benefits of using the SharePoint Online authentication cookie for seamless authentication - -The most important benefit for using the SharePoint Online authentication cookie to connect to custom APIs secured with AAD is the fact that in this approach you don't need to register an AAD application for every web part. This frees you from having to manage reply URLs of pages where each web part is used and doesn't have the limitation of maximum 10 reply URLs per AAD application. - -The authentication flow is handled seamlessly and there is no user interaction required to complete. In comparison, when using ADAL JS, each web part is based on a different AAD application and requires user to explicitly sign in to it. - -#### Considerations when using the SharePoint Online authentication cookie for seamless authentication - -From the functional point of view, using both ADAL JS and the SharePoint Online authentication cookie allows you to connect to APIs secured with AAD. There are however a few important differences between the two approaches that you should be aware of. - -When using ADAL JS, before the client-side application retrieves an access token, it retrieves the identity token for the current user. This token contains information about the current user retrieved from AAD such as the user name or password. When using the authentication cookie, there is no identity token. As you're working with SharePoint, this isn't really a limitation, since you can retrieve the same information about the current user from SharePoint. - -ADAL JS allows you to connect to any API secured with Azure AD. When using the authentication cookie, the API must explicitly support receiving credentials from cross-domain calls. When designing APIs you should take this requirement into account to ensure that you will be able to use these APIs in SharePoint Framework solutions. - -With both ADAL JS and the SharePoint Online authentication cookie you can access APIs secured with Azure Active Directory. But not all APIs support using of both methods. For example, in order to access the Microsoft Graph, you need to have a valid OAuth access token with specific Microsoft Graph permissions. You can obtain this token using ADAL JS but not using a SharePoint Online authentication cookie. - -When using the SharePoint Online authentication cookie to access APIs secured with AAD no additional authorization information is being sent along with the request. This means, that by default, every user with a valid organizational account in the Azure Active Directory associated with the API can access the API. When building the API you must take care of authorization, in order to ensure that all API operations are performed by users with sufficient privileges. - -Custom APIs are hosted outside of SharePoint Online and can be accessed using cross-domain web requests. By default, web browsers don't include credentials when performing cross-domain AJAX requests. In order to connect to these secured APIs, you have to enable sending credentials with cross-domains explicitly for each outgoing web request. - -### General considerations - -Both ADAL JS and the method using the SharePoint Online authentication cookie use iframes for communicating with Azure Active Directory. The reason for that are the redirects that are a part of the OAuth flow, and which cannot be automatically followed by AJAX requests. Microsoft Internet Explorer uses security zones to apply security policies to websites depending on the associated zone. For the scripts to be able to access information from an iframe, the resource in the iframe and the page hosting the iframe must be located in the same security zone. To ensure correct configuration, organization can use group policies to distribute the settings consistently to their users. - -## Build an API secured with Azure Active Directory - -Securing the access to an API with Azure Active Directory isn't complex and requires just a few steps. The exact process varies depending on the implementation of your API. If you choose to use Azure Functions, you will be able to configure the security through the Azure Portal. If you built your API as using ASP.NET WebAPI and want to host it somewhere else than in the Azure App Service, you will need to extend the WebAPI's code to add authentication to it. Following is a step-by-step description of how you would build and configure an API secured with AAD using both Azure Functions and ASP.NET WebAPI. - -### Build the API using an Azure Function - -Building APIs using Azure Functions offers you a number of benefits. First and foremost, it significantly simplifies the development and deployment process of the API. Azure Functions offer rich set of configuration options. The only thing that you need to take care of is the actual API code. For everything else, from authentication to support CORS and documenting the API, you can use the Azure Portal. - -Azure Functions are hosted in Azure App Service and benefit of many capabilities available in the underlying service. On top of securing the API using a function or admin key, you can choose to enable Azure App Service security and protect your API using Azure Active Directory or one of the other available authentication providers. App Service authentication can be configured via the Azure Portal and doesn't require any changes in the API code. - -Following is how you would use Azure Functions to create an API secured with Azure Active Directory and capable of being called from a cross-domain origin in a secured way. - -#### Create new Azure Function - -In the Azure Portal go to your Resource Group and add a Function App. - -![Function App highlighted in the list of available services that can be added to a Resource Group](../../../images/api-aad-create-new-function-app.png) - -Once the Function App has been provisioned, open the newly created Function App and add a new function by clicking the plus icon next to the Functions label. - -![The plus icon next to the Functions label highlighted on the Function App blade](../../../images/api-aad-add-function.png) - -On the quick start screen, scroll down to the **Get started on your own** section and choose the **Custom function** option. - -![The Custom function link highlighted on the Add new function screen](../../../images/api-aad-custom-function.png) - -From the list of templates choose **HttpTrigger-JavaScript**. As the function name, use **Orders** and set the function Authorization level to **Anonymous** as you will use Azure AD to secure the access to the Azure Function. Confirm your selection by clicking the **Create** button. - -![New Azure Function configuration](../../../images/api-aad-add-function-parameters.png) - -#### Implement API code - -Replace the function's code with the following snippet: - -```js -module.exports = function (context, req) { - context.res = { - body: [ - { - id: 1, - orderDate: new Date(2016, 0, 6), - region: "east", - rep: "Jones", - item: "Pencil", - units: 95, - unitCost: 1.99, - total: 189.05 - }, - { - id: 2, - orderDate: new Date(2016, 0, 23), - region: "central", - rep: "Kivell", - item: "Binder", - units: 50, - unitCost: 19.99, - total: 999.50 - }, - { - id: 3, - orderDate: new Date(2016, 1, 9), - region: "central", - rep: "Jardine", - item: "Pencil", - units: 36, - unitCost: 4.99, - total: 179.64 - }, - { - id: 4, - orderDate: new Date(2016, 1, 26), - region: "central", - rep: "Gill", - item: "Pen", - units: 27, - unitCost: 19.99, - total: 539.73 - }, - { - id: 5, - orderDate: new Date(2016, 2, 15), - region: "west", - rep: "Sorvino", - item: "Pencil", - units: 56, - unitCost: 2.99, - total: 167.44 - }], - headers: { - "Access-Control-Allow-Credentials" : "true", - "Access-Control-Allow-Origin" : "https://contoso.sharepoint.com" - } - }; - context.done(); -}; -``` - -Change the URL specified in the **Access-Control-Allow-Origin** header to match the URL of your SharePoint Online tenant from which you will be calling this API. - -Save the changes to the function's code by clicking the **Save** button. - -![Save button highlighted on the Azure Function code screen](../../../images/api-aad-function-code.png) - -#### Change CORS settings - -Azure Functions are hosted in Azure App Service which allows you to configure its Cross-Origin Resource Sharing (CORS) settings through the Azure Portal. While this is convenient, if configured through the portal, it cannot be used in combination with the **Access-Control-Allow-Credentials** header, which is required by the API to accept authentication cookie coming from another origin. For the client-sie authentication to work correctly, CORS settings of the Azure App Service must be cleared. - -In the Function App, select your Azure Function and navigate to the **Platform features** blade. - -![The Platform features link highlighted in the Azure Function settings](../../../images/api-aad-platform-features.png) - -From the **API** section, choose the **CORS** option. - -![The CORS option highlighted on the Azure Function Platform features blade](../../../images/api-aad-function-cors.png) - -On the CORS settings blade, delete all entries so that the CORS configuration is empty. - -![The Delete option highlighted on the first CORS entry](../../../images/api-aad-function-cors-delete.png) - -Confirm the deletion by clicking the **Save** button. - -![The Save button highlighted on the CORS settings blade](../../../images/api-aad-function-cors-save.png) - -#### Enable App Service Authentication - -In the Function App settings, go back to the Platform settings blade. From the **Networking** section, select the **Authentication / Authorization** option. - -![The Authentication / Authorization option highlighted on the Function App Platform settings blade](../../../images/api-aad-function-authentication.png) - -Enable the App Service Authentication by setting the **App Service Authentication** toggle to **On**. - -![The App Service Authentication toggle set to on](../../../images/api-aad-function-authentication-on.png) - -To disallow anonymous access to the API and force authentication using Azure AD, set the value of the **Action to take when request is not authenticated** drop-down to **Login with Azure Active Directory**. - -![The 'Login with Azure Active Directory' option selected in the 'Action to take when request is not authenticated' drop-down](../../../images/api-aad-function-authentication-login-aad.png) - -Next, in the list of authentication providers, select Azure Active Directory to configure it. - -![Azure Active Directory highlighted in the list of authentication providers](../../../images/api-aad-function-authentication-aad.png) - -On the Active Directory Authentication blade, set the **Management mode** to **Express** and create a new AAD app. - -> **Important:** when using the express configuration mode, the Azure Portal will create a new AAD application from the same directory as where the Function App is located. If the Function App is hosted in a different Azure subscription with a different directory, you should use the advanced mode instead, and specify the ID of the directory and application that should be used to secure the access to the API. -> -> When using existing AAD applications you should configure the application to accept credentials from a single tenant only. Configuring the application as multi-tenant would allow any user with a valid organization or personal account to connect to your API. -> -> Using an AAD application to secure the access to your API only accounts for authentication. When building your API, you should also authorize requests in your API's code, to ensure that only users with sufficient privileges are using the API. - -Because the app is only meant to secure the access to the Azure Function, it doesn't require any additional permissions. Confirm the selection by clicking the **OK** button. - -![Azure Active Directory authentication settings](../../../images/api-aad-function-authentication-aad-app.png) - -When the Azure Active Directory blade closes, back on the **Authentication / Authorization** blade, click the **Save** button to confirm all changes to authentication settings. - -![The Save button highlighted on the Authentication / Authorization blade](../../../images/api-aad-function-authentication-save.png) - -If you try to navigate to your API URL in a new private window, you should be prompted to sign in using your Azure AD account. - -![Azure AD sign in page](../../../images/api-aad-sign-in.png) - -At this point, the API is ready to be called securely from a SharePoint Framework client-side web part using the authentication cookie. - -### Build the API using ASP.NET Web API - -Another way to implement the API is by using ASP.NET WebAPI. Comparing to using Azure Functions to build the API, ASP.NET WebAPI requires significantly more work. Not only you have to setup a complete project for it, but also you have to think about where the API will be deployed to. Using ASP.NET WebAPI offers you on the other hand more flexibility and allows you to deploy the API to different to different platforms such as Azure App Service, Docker containers, other cloud providers or even on your infrastructure. - -Following is how you would build an API using ASP.NET WebAPI, deploy it to Azure App Service and secure it using Azure App Service Authentication. Later, you will extend the API to perform the authentication by itself, so that it can be deployed to other platforms as well. - -#### Create new ASP.NET WebAPI project - -In Visual Studio, from the **File** menu, choose the **New / Project** option. In the **New Project** dialog, select C# Web templates and from the list of available templates, select the **ASP.NET Web Application** template. - -![The 'ASP.NET Web Application' project template selected in the New Project dialog](../../../images/api-aad-webapi-vs-web-application.png) - -As the type of ASP.NET Web Application project, select **Web API**. - -![Web API selected as the type of ASP.NET Web Application project to create](../../../images/api-aad-webapi-vs-webapi.png) - -Because you will use Azure App Service Authentication to secure the access to the API, click the **Change Authentication** button and select the **No Authentication** option. - -![The 'No Authentication' option selected as the authentication option for the newly created ASP.NET Web Application](../../../images/api-aad-webapi-vs-no-authentication.png) - -Confirm your choice by clicking the **OK** button. - -Visual Studio allows you to easily deploy your WebAPI to Azure App Service. To benefit of this capability, in the **New ASP.NET Web Application** dialog, in the **Microsoft Azure** section, select the **Host in the cloud** section and in the drop-down select the **App Service** option. - -![App Service selected as the hosting platform for the Web Application](../../../images/api-aad-webapi-vs-host-app-service.png) - -In the **Create App Service** dialog, specify the name for the web app to be created and select your Azure subscription, Resource Group and App Service Plan that you want to use for this application. - -![App Service creation settings dialog](../../../images/api-aad-webapi-vs-create-app-service.png) - -Confirm your choice by clicking the **Create** button. At this point, Visual Studio will create a new Azure Web App to host your web application. - -#### Add support for CORS - -By default, APIs created using the ASP.NET Web Application project template don't support CORS and cannot be called by client-applications hosted on different domains. To add support for CORS to your WebAPI, click right on the project, and from the context menu choose the **Manage NuGet Packages...** option. - -![The 'Manage NuGet Packages' option highlighted in the project context menu in Visual Studio](../../../images/api-aad-webapi-vs-manage-nuget.png) - -On the **Manage NuGet Packages** tab, search for a package named **Microsoft.AspNet.WebApi.Cors** and install it in your project. - -![The 'Microsoft.AspNet.WebApi.Cors' package highlighted on the 'Manage NuGet Packages' tab](../../../images/api-aad-webapi-vs-cors-nuget.png) - -#### Add data model - -In the project, define a model that will represent the data returned by the API. In the **Models** folder add a new class and name it **Order**. Paste the following code into the newly crated file: - -```cs -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using System; - -namespace PnP.Aad.Api.Models { - public class Order { - [JsonProperty(PropertyName = "id")] - public int Id { get; set; } - [JsonProperty(PropertyName = "orderDate")] - public DateTime OrderDate { get; set; } - [JsonConverter(typeof(StringEnumConverter))] - [JsonProperty(PropertyName = "region")] - public Region Region { get; set; } - [JsonProperty(PropertyName = "rep")] - public string Rep { get; set; } - [JsonProperty(PropertyName = "item")] - public string Item { get; set; } - [JsonProperty(PropertyName = "units")] - public uint Units { get; set; } - [JsonProperty(PropertyName = "unitCost")] - public double UnitCost { get; set; } - [JsonProperty(PropertyName = "total")] - public double Total { get; set; } - } - - public enum Region { - East, - Central, - West - } -} -``` - -#### Add Orders API - -Next, add an API that will return the information about the latest orders. In the **Controllers** folder create a new class and name it **OrdersController**. Paste the following code into the newly created file: - -```cs -using PnP.Aad.Api.Models; -using System; -using System.Collections.Generic; -using System.Web.Http; - -namespace PnP.Aad.Api.Controllers { - public class OrdersController : ApiController { - private List orders = new List { - new Order { - Id = 1, - OrderDate = new DateTime(2016, 1, 6), - Region = Region.East, - Rep = "Jones", - Item = "Pencil", - Units = 95, - UnitCost = 1.99, - Total = 189.05 - }, - new Order { - Id = 2, - OrderDate = new DateTime(2016, 1, 23), - Region = Region.Central, - Rep = "Kivell", - Item = "Binder", - Units = 50, - UnitCost = 19.99, - Total = 999.50 - }, - new Order { - Id = 3, - OrderDate = new DateTime(2016, 2, 9), - Region = Region.Central, - Rep = "Jardine", - Item = "Pencil", - Units = 36, - UnitCost = 4.99, - Total = 179.64 - }, - new Order { - Id = 4, - OrderDate = new DateTime(2016, 2, 26), - Region = Region.Central, - Rep = "Gill", - Item = "Pen", - Units = 27, - UnitCost = 19.99, - Total = 539.73 - }, - new Order { - Id = 5, - OrderDate = new DateTime(2016, 3, 15), - Region = Region.West, - Rep = "Sorvino", - Item = "Pencil", - Units = 56, - UnitCost = 2.99, - Total = 167.44 - } - }; - - public IEnumerable Get() { - return orders; - } - } -} -``` - -#### Extend the API with support for CORS - -Even though you have installed support CORS in your project, it's not being used actively yet. If you would call the newly created Orders API from a client application hosted on another domain, you would get a CORS error and the request would fail. For an API to support CORS, it has to be decorated with the **EnableCors** attribute. - -```cs -using PnP.Aad.Api.Models; -using System; -using System.Collections.Generic; -using System.Web.Http; -using System.Web.Http.Cors; - -namespace PnP.Aad.Api.Controllers { - public class OrdersController : ApiController { - private List orders = new List { - // ... - }; - - [EnableCors("*", "*", "GET", SupportsCredentials = true)] - public IEnumerable Get() { - return orders; - } - } -} -``` - -Next, open the **.\App_Start\WebApiConfig.cs** file and paste the following code: - -```cs -using System.Web.Http; - -namespace PnP.Aad.Api { - public static class WebApiConfig { - public static void Register(HttpConfiguration config) { - // Web API configuration and services - - // Web API routes - config.MapHttpAttributeRoutes(); - - config.EnableCors(); - - config.Routes.MapHttpRoute( - name: "DefaultApi", - routeTemplate: "api/{controller}/{id}", - defaults: new { id = RouteParameter.Optional } - ); - } - } -} -``` - -At this point, the API is code complete and can be published to the Azure Web App. - -#### Publish the API to Azure Web App - -In Visual Studio, right click on the project, and from the context menu choose the **Publish...** option. - -![The Publish option highlighted in the project context menu](../../../images/api-aad-webapi-vs-publish.png) - -In the Publish dialog, verify that all information is correct and click the **Publish** button to start the publishing process. - -![Publish dialog with publish information](../../../images/api-aad-webapi-vs-publish-settings.png) - -Once the publishing process completes, navigate in your web browser to the API URL, eg. _http://pnp-aad-api.azurewebsites.net/api/orders_. At this point the API is not secured and can be accessed by anonymous users. - -![API response displayed in web browser for an anonymous user](../../../images/api-aad-webapi-response-anonymous.png) - -#### Secure the API using Azure App Service - -To secure the API using Azure AD go to the Azure Portal and open the Web App hosting your API. From the **Settings** group, select the **Authentication / Authorization** option. - -![Azure App Service Authentication / Authorization page displayed in the Azure Portal](../../../images/api-aad-webapi-authentication.png) - -To enable authentication for your Web App, set the **App Service Authentication** toggle to **On**. - -![The 'App Service Authentication' toggle set to On for the Web App hosting the WebAPI](../../../images/api-aad-webapi-authentication-on.png) - -To disallow anonymous access to the API, in the **Action to take when request is not authenticated** drop-down, select the **Login with Azure Active Directory** option. - -![The 'Login with Azure Active Directory' option selected in the 'Action to take when request is not authenticated' drop-down in Web App Authentication settings](../../../images/api-aad-webapi-authentication-aad-login.png) - -Finally, configure Azure Active Directory authentication by from the list of authentication providers selecting **Azure Active Directory**. - -![Azure Active Directory highlighted in the list of authentication providers](../../../images/api-aad-webapi-authentication-aad.png) - -On the Active Directory Authentication blade, set the **Management mode** to **Express** and create a new AAD app. - -> **Important:** when using the express configuration mode, the Azure Portal will create a new AAD application from the same directory as where the Function App is located. If the Function App is hosted in a different Azure subscription with a different directory, you should use the advanced mode instead, and specify the ID of the directory and application that should be used to secure the access to the API. -> -> When using existing AAD applications you should configure the application to accept credentials from a single tenant only. Configuring the application as multi-tenant would allow any user with a valid organization or personal account to connect to your API. -> -> Using an AAD application to secure the access to your API only accounts for authentication. When building your API, you should also authorize requests in your API's code, to ensure that only users with sufficient privileges are using the API. - -Because the app is only meant to secure the access to the Azure Function, it doesn't require any additional permissions. Confirm the selection by clicking the **OK** button. - -![Azure Active Directory authentication settings](../../../images/api-aad-webapi-authentication-aad-ok.png) - -When the Azure Active Directory blade closes, back on the **Authentication / Authorization** blade, click the **Save** button to confirm all changes to authentication settings. - -![The Save button highlighted on the Authentication / Authorization blade](../../../images/api-aad-webapi-authentication-save.png) - -If you try to navigate to your API URL in a new private window, you should be prompted to sign in using your Azure AD account. - -![Azure AD sign in page](../../../images/api-aad-webapi-azure-sign-in.png) - -At this point, the API is ready to be called securely from a SharePoint Framework client-side web part using the authentication cookie. - -#### Secure the API using OpenID - -If you want to deploy your ASP.NET WebAPI project to other location than Azure App Service and want it to be secured with AAD, you can't rely on App Service Authentication. Instead, you have to extend the Web Application to require itself its users to authenticate before they can use the API. - -##### Disable anonymous access to all resources - -Assuming you want all resources secured, open the **.\App_Start\FilterConfig.cs** file and paste the following code: - -```cs -using System.Web.Mvc; - -namespace PnP.Aad.Api { - public class FilterConfig { - public static void RegisterGlobalFilters(GlobalFilterCollection filters) { - filters.Add(new HandleErrorAttribute()); - filters.Add(new AuthorizeAttribute()); - } - } -} -``` - -To require authentication for all APIs, open the **.\App_Start\WebApiConfig.cs** file and paste the following code: - -```cs -using System.Web.Http; - -namespace PnP.Aad.Api { - public static class WebApiConfig { - public static void Register(HttpConfiguration config) { - // Web API configuration and services - - // Web API routes - config.MapHttpAttributeRoutes(); - - config.EnableCors(); - config.Filters.Add(new AuthorizeAttribute()); - - config.Routes.MapHttpRoute( - name: "DefaultApi", - routeTemplate: "api/{controller}/{id}", - defaults: new { id = RouteParameter.Optional } - ); - } - } -} -``` - -If you would try to access either the API or any other resource in your web application, you would get a 401 Unauthorized response. - -![Unauthorized response when trying to access the API](../../../images/api-aad-webapi-anonymous-unauthorized.png) - -At this point, the web application requires that all requests to its resources are authenticated, but it doesn't start the AAD login flow. In the following steps you will extend the web application so that it would redirect the user to the Azure AD login page, if she wasn't previously authenticated. - -##### Register Azure AD application - -In order to secure an API with Azure Active Directory, you need to register an Azure AD application. This application is then referenced in the web application project and used by the OWIN middleware to secure the access to your API with Azure AD. - -If you don't have an existing AAD application yet, you can create one in the Azure Portal, by navigating to the Azure Active Directory blade. - -> **Important:** the Azure AD application, used to secure the API, should be created in the same Azure Active Directory which is used by your organization to access Office 365. - -![Azure Active Directory blade open in the Azure Portal](../../../images/api-aad-webapi-azure-aad.png) - -On the Azure Active Directory blade, navigate to the **App registrations** blade. - -![App registrations section highlighted on the Azure Active Directory blade](../../../images/api-aad-webapi-azure-app-registrations.png) - -On the **App registrations** blade, click the **New application registration** button to register a new Azure AD application. - -![The 'New application registration' button highlighted on the App registrations blade](../../../images/api-aad-webapi-azure-new-app-registration.png) - -On the **Create** blade, provide the information about your application and confirm the creation by clicking the **Create** button. - -![The Create button highlighted on the Create new application registration blade](../../../images/api-aad-webapi-azure-new-app-registration-create.png) - -After the application registration is successfully created, select it in the list to view its details. - -![Application registration information displayed in the Azure Portal](../../../images/api-aad-webapi-azure-app-details.png) - -From the application registration information, copy the **Application ID** and store it as you will need it when configure Azure AD authentication for your web application. - -##### Redirect anonymous requests to Azure AD login page - -In Visual Studio, right click on the project and from the context menu, choose the **Manage NuGet Packages...** option. In the **Manage NuGet Packages** window, add the following packages to your project: - -- Microsoft.Owin.Host.SystemWeb -- Microsoft.Owin.Security.Cookies -- Microsoft.Owin.Security.OpenIdConnect - -In the root directory of your project, add a new class named **Startup** and paste the following code: - -```cs -using Owin; - -namespace PnP.Aad.Api { - public partial class Startup - { - public void Configuration(IAppBuilder app) - { - ConfigureAuth(app); - } - } -} -``` - -Then, in the **App_Start** folder, create a new class named **Startup.Auth** and paste the following code: - -```cs -using Microsoft.Owin.Security; -using Microsoft.Owin.Security.Cookies; -using Microsoft.Owin.Security.OpenIdConnect; -using Owin; -using System.Configuration; - -namespace PnP.Aad.Api { - public partial class Startup { - // For more information on configuring authentication, please visit http://go.microsoft.com/fwlink/?LinkId=301864 - public void ConfigureAuth(IAppBuilder app) { - app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType); - - app.UseCookieAuthentication(new CookieAuthenticationOptions()); - - app.UseOpenIdConnectAuthentication( - new OpenIdConnectAuthenticationOptions { - ClientId = ConfigurationManager.AppSettings["ida:ClientId"], - Authority = $"https://login.microsoftonline.com/{(ConfigurationManager.AppSettings["ida:Tenant"])}", - PostLogoutRedirectUri = ConfigurationManager.AppSettings["ida:PostLogoutRedirectUri"], - }); - } - } -} -``` - -In Visual Studio, open the **Web.config** file and in the **appSettings** section add the following elements: - -```xml - - - -``` - -The value of the **ida:Tenant** key, is the name of the Azure AD where the Azure AD app used to secure the API is defined. **ida:ClientId** specifies the ID of the Azure AD application used to secure the API. The URL specified in the **ida:PostLogoutRedirectUri** property, is where AAD would redirect to after signing out of your application, which isn't used in this case. - -This concludes the configuration process. If you start your web application, before you will be able to access any of its resources, you will be prompted to sign in with your Azure AD account. To ensure, that only authorized users are accessing the particular API, you should implement authorization in your custom APIs. You can do that by retrieving the user name from the `RequestContext.Principal.Identity` property and verifying it against your security matrix. - -## See also - -- [ -Call custom APIs secured with Azure Active Directory without ADAL JS (code sample)](https://github.com/SharePoint/sp-dev-fx-webparts/tree/master/samples/aad-api-spo-cookie) diff --git a/docs/spfx/web-parts/guidance/connect-to-sharepoint-using-jsom.md b/docs/spfx/web-parts/guidance/connect-to-sharepoint-using-jsom.md index a0a97ea31..c41336b0d 100644 --- a/docs/spfx/web-parts/guidance/connect-to-sharepoint-using-jsom.md +++ b/docs/spfx/web-parts/guidance/connect-to-sharepoint-using-jsom.md @@ -1,594 +1,606 @@ ---- -title: Connect to SharePoint using the JavaScript Object Model (JSOM) -ms.date: 09/25/2017 -ms.prod: sharepoint ---- - - -# Connect to SharePoint using the JavaScript Object Model (JSOM) - -In the past, when building SharePoint customizations you might have used the SharePoint JavaScript Object Model (JSOM) to communicate with SharePoint. This is no longer the recommended path (see **Considerations** below), but there are still valid use cases such as code migration. This article demonstrates how to use SharePoint JSOM when building solutions on the SharePoint Framework. - -> **Note:** Before following the steps in this article, be sure to [set up your SharePoint Framework development environment](../../set-up-your-development-environment.md). - -## Create a New Project - -From the console, create a new folder for your project: - -```sh -md react-sharepointlists -``` - -Go to the project folder: - -```sh -cd react-sharepointlists -``` - -In the project folder, run the SharePoint Framework Yeoman generator to scaffold a new SharePoint Framework project: - -```sh -yo @microsoft/sharepoint -``` - -When prompted, enter the following values: - -- **react-sharepointlists** as your solution name. -- Choose **Webpart** as the client-side component type to be created. -- **Use the current folder** for the location to place the files. -- **React** as the starting framework to build the web part. -- **SharePoint lists** as your web part name. -- **Shows names of lists in the current site** as your web part description. - -![The SharePoint Framework Yeoman generator with the default choices](../../../images/tutorial-spjsom-yo-sharepoint.png) - -Once the scaffolding completes, open your project folder in your code editor. This article uses Visual Studio Code in the steps and screenshots but you can use any editor you prefer. - -![The SharePoint Framework project open in Visual Studio Code](../../../images/tutorial-spjsom-vscode.png) - -To open the directory in Visual Studio Code, from the console type: -```sh -code . -``` - -## Referencing JSOM -In order to use SharePoint JSOM in your SharePoint Framework component, you must first reference it. In the past it was already available on the page for you to use. In the SharePoint Framework, it has to be explicitly loaded. - -There are two ways to reference SharePoint JSOM in the SharePoint Framework: -- **Declarative** - through configuration -- **Imperative** - through code - -Each of these approaches have advantages and disadvantages and it's important for you to understand each of them. - -## Reference JSOM Declaratively - -When referencing JSOM declaratively, the first step is to register the SharePoint JSOM API as external scripts within your SharePoint Framework project. In your code editor, open the **./config/config.json** file and add the following to the **externals** section: - -```json -{ - // ... - "externals": { - "sp-init": { - "path": "https://contoso.sharepoint.com/_layouts/15/init.js", - "globalName": "$_global_init" - }, - "microsoft-ajax": { - "path": "https://contoso.sharepoint.com/_layouts/15/MicrosoftAjax.js", - "globalName": "Sys", - "globalDependencies": [ - "sp-init" - ] - }, - "sp-runtime": { - "path": "https://contoso.sharepoint.com/_layouts/15/SP.Runtime.js", - "globalName": "SP", - "globalDependencies": [ - "microsoft-ajax" - ] - }, - "sharepoint": { - "path": "https://contoso.sharepoint.com/_layouts/15/SP.js", - "globalName": "SP", - "globalDependencies": [ - "sp-runtime" - ] - } - } - // ... -} -``` - -Each of the entries points to different script files that together allows you to use SharePoint JSOM in your SPFx component. All of these scripts are distributed as non-module scripts. This is why each registration entry requires a URL, (specified using the `path` property) and the name used by the script (provided in the `globalName` property). To ensure that these scripts load in the right order, the dependencies between these scripts are specified using the `globalDependencies` property. - -Additional scripts may need to be added depending on the JSOM functionality you are using (e.g. sp.taxonomy.js). - -### Install TypeScript Typings for SharePoint JSOM - -The next step is to install and configure TypeScript typings for SharePoint JSOM. This allows you to benefit from TypeScript's type safety features when working with SharePoint JSOM. - -From the console, execute the following command within your project directory: - -```sh -npm install @types/microsoft-ajax @types/sharepoint --save-dev -``` - -SharePoint JSOM is not distributed as a module and so you cannot import it directly in your code. Instead, you need to register its TypeScript typings globally. In the code editor, open the **./tsconfig.json** file, and in the `types` property, right after the **webpack-env** entry, add references to **microsoft-ajax** and **sharepoint**: - -```json -{ - "compilerOptions": { - // ... - "types": [ - "es6-promise", - "es6-collections", - "webpack-env", - "microsoft-ajax", - "sharepoint" - ] - } -} -``` - -### Reference SharePoint JSOM Scripts in a React Component - -To load the SharePoint JSOM scripts in your SPFx component, you have to reference them in the component's code. In this example, you will add the references in a React component where JSOM will be used to communicate with SharePoint. - -In the code editor, open the **./src/webparts/sharePointLists/components/SharePointLists.tsx** file. After the last `import` statement add the following code: - -```ts -require('sp-init'); -require('microsoft-ajax'); -require('sp-runtime'); -require('sharepoint'); -``` - -These names correspond to the external references you added previously so SharePoint Framework will load these scripts from the specified URLs. - -### Show Titles of SharePoint Lists in the Current Site Using JSOM - -To demonstrate using SharePoint JSOM for communicating with SharePoint, you will retrieve and render the titles of all SharePoint lists located in the current site. - -#### Add _siteUrl_ to the React Component's Properties - -In order to connect to SharePoint, the React component must know the URL of the current site. That URL is available in the parent web part and can be passed into the component through its properties. - -In the code editor, open the **./src/webparts/sharePointLists/components/ISharePointListsProps.ts** file and to the `ISharePointListsProps` interface add the `siteUrl` property: - -```ts -export interface ISharePointListsProps { - description: string; - siteUrl: string; -} -``` - -To pass the URL of the current site into the component, open the **./src/webparts/sharePointLists/SharePointListsWebPart.ts** file in the code editor and change the `render` method to: - -```ts -export default class SharePointListsWebPart extends BaseClientSideWebPart { - public render(): void { - const element: React.ReactElement = React.createElement( - SharePointLists, - { - description: this.properties.description, - siteUrl: this.context.pageContext.web.absoluteUrl - } - ); - - ReactDom.render(element, this.domElement); - } - - // ... -} -``` - -#### Define the React Component's State - -The React component will load data from SharePoint and render it to the user. The current state of the React component is modeled using a state interface we will add. - -In the code editor, in the **./src/webparts/sharePointLists/components** folder, create a new file named **ISharePointListsState.ts** and paste the following contents: - -```ts -export interface ISharePointListsState { - listTitles: string[]; - loadingLists: boolean; - error: string; -} -``` - -#### Add State to the React component - -Having defined the interface describing the shape of the component's state, the next step is to have the React component use that state interface. - -In the code editor, open the **./src/webparts/sharePointLists/components/SharePointLists.tsx** file. Beneath the existing `import` statements add: - -```ts -import { ISharePointListsState } from './ISharePointListsState'; -``` - -Next, change the signature of the `SharePointLists` class to: - -```ts -export default class SharePointLists extends React.Component { - // ... -} -``` - -In the `SharePointLists` class, add a constructor with the default state value: - -```ts -export default class SharePointLists extends React.Component { - constructor(props?: ISharePointListsProps, context?: any) { - super(); - - this.state = { - listTitles: [], - loadingLists: false, - error: null - }; - } - - // ... -} -``` - -#### Load Information About SharePoint Lists From the Current Site Using JSOM - -The sample client-side web part used in this article loads information about SharePoint lists in the current site after clicking a button. - -![SharePoint Framework client-side web part showing titles of SharePoint lists in the current site](../../../images/tutorial-spjsom-web-part-list-titles.png) - -In the code editor, open the **./src/webparts/sharePointLists/components/SharePointLists.tsx** file. In the `SharePointLists` class add a new method named `getListsTitles`: - -```ts -export default class SharePointLists extends React.Component { - constructor(props?: ISharePointListsProps, context?: any) { - super(); - - this.state = { - listTitles: [], - loadingLists: false, - error: null - }; - - this.getListsTitles = this.getListsTitles.bind(this); - } - - // ... - - private getListsTitles(): void { - } -} -``` - -To ensure the correct scoping of the method, we bind it to the web part in the constructor. - -In the `getListsTitles` method, use SharePoint JSOM to load the titles of SharePoint lists in the current site: - -```ts -export default class SharePointLists extends React.Component { - // ... - private getListsTitles(): void { - this.setState({ - loadingLists: true, - listTitles: [], - error: null - }); - - const context: SP.ClientContext = new SP.ClientContext(this.props.siteUrl); - const lists: SP.ListCollection = context.get_web().get_lists(); - context.load(lists, 'Include(Title)'); - context.executeQueryAsync((sender: any, args: SP.ClientRequestSucceededEventArgs): void => { - const listEnumerator: IEnumerator = lists.getEnumerator(); - - const titles: string[] = []; - while (listEnumerator.moveNext()) { - const list: SP.List = listEnumerator.get_current(); - titles.push(list.get_title()); - } - - this.setState((prevState: ISharePointListsState, props: ISharePointListsProps): ISharePointListsState => { - prevState.listTitles = titles; - prevState.loadingLists = false; - return prevState; - }); - }, (sender: any, args: SP.ClientRequestFailedEventArgs): void => { - this.setState({ - loadingLists: false, - listTitles: [], - error: args.get_message() - }); - }); - } -} -``` - -We start with resetting the component's state to communicate to the user that the component will be loading information from SharePoint. Then, using the URL of the current site passed to the component through its properties, we instantiate a new SharePoint context. Using SharePoint JSOM, we load lists from the current site. To optimize the request for performance, we specify that only the `Title` property should be loaded. Next, we execute the query by calling the `executeQueryAsync` method and passing two callback functions. Once the query is completed, we enumerate through the collection of retrieved lists, store their titles in an array, and update the component's state. - -#### Render the Titles of SharePoint Lists in the Current Site - -Having loaded the titles of SharePoint lists in the current site, the final step is to render them in the component. In the code editor, open the **./src/webparts/sharePointLists/components/SharePointLists.tsx** file and update the `render` method: - -```tsx -export default class SharePointLists extends React.Component { - // ... - public render(): React.ReactElement { - const titles: JSX.Element[] = this.state.listTitles.map((listTitle: string, index: number, listTitles: string[]): JSX.Element => { - return
  • {listTitle}
  • ; - }); - - return ( -
    -
    -
    -
    - Welcome to SharePoint! -

    Customize SharePoint experiences using Web Parts.

    -

    {escape(this.props.description)}

    - - Get lists titles -
    - {this.state.loadingLists && - Loading lists...} - {this.state.error && - An error has occurred while loading lists: {this.state.error}} - {this.state.error === null && titles && -
      - {titles} -
    } -
    -
    -
    -
    - ); - } - // ... -} -``` - -At this point, you should be able to add your web part to the page and see the titles of SharePoint lists in the current site. To verify that the project is working correctly, run the following command from the console: - -```sh -gulp serve --nobrowser -``` - -As you are using SharePoint JSOM to communicate with SharePoint, you have to test the web part using the hosted version of the SharePoint workbench (which is why the `--nobrowser` parameter is specified to prevent the automatic loading of the local workbench). - -![SharePoint Framework client-side web part showing titles of SharePoint lists in the current site](../../../images/tutorial-spjsom-web-part-list-titles.png) - -Referencing SharePoint JSOM scripts declaratively as external scripts is convenient and allows you to keep your code clean. One disadvantage, however, is that it requires specifying absolute URLs to the location from which SharePoint JSOM scripts should be loaded. If you're using separate SharePoint tenants for development, testing, and production, then it will require some additional work to change these URLs for the different environments accordingly. In such cases, you may consider referencing JSOM imperatively by using the [SPComponentLoader](https://dev.office.com/sharepoint/reference/spfx/sp-loader/spcomponentloader) to load the scripts in the SPFx component's code. - -## Reference JSOM imperatively - -Another way to load JavaScript libraries in SharePoint Framework projects, is by using the `SPComponentLoader`. `SPComponentLoader` is a utility class provided with the SharePoint Framework designed to help you load scripts and other resources in your components. One benefit of using the `SPComponentLoader` over loading scripts declaratively is that it allows you to use server-relative URLs which is more convenient when using different SharePoint tenants for the different stages of your development process. - -> For this portion of the tutorial, we'll be adjusting the code we created previously in the Declarative section above. - -### Declarative Reference Cleanup - -If you followed the steps in the declarative reference sections above, you'll need to remove those references. - -First, remove the existing external script references. In the code editor, open the **./config/config.json** file and from the **externals** property, remove all entries: - -```json -{ - "$schema": "https://dev.office.com/json-schemas/spfx-build/config.2.0.schema.json", - "version": "2.0", - "bundles": { - "share-point-lists-web-part": { - "components": [ - { - "entrypoint": "./lib/webparts/sharePointLists/SharePointListsWebPart.js", - "manifest": "./src/webparts/sharePointLists/SharePointListsWebPart.manifest.json" - } - ] - } - }, - "externals": {}, - "localizedResources": { - "SharePointListsWebPartStrings": "lib/webparts/sharePointLists/loc/{locale}.js" - } -} -``` - -With the SharePoint JSOM scripts no longer being registered as external scripts, you cannot reference them directly in your code. - -In the code editor, open the **./src/webparts/sharePointLists/components/SharePointLists.tsx** file and remove the `require` statements pointing to the different SharePoint JSOM scripts. - -### Wait to Load Data Until the JSOM Scripts are Loaded - -The primary functionality of the client-side web part that we are building in this tutorial depends on SharePoint JSOM. Depending on a number of factors, loading these scripts could take a few moments. When building SPFx components that utilize JSOM, you should take that into account. When added to the page, the web part should communicate to the user that it's loading its prerequisites and should make it clear when it's ready to be used. To support this, extend the React component's state with an additional property to track the status of loading the JSOM scripts. - -In the code editor, open the **./src/webparts/sharePointLists/components/ISharePointListsState.ts** file and paste the following code: - -```ts -export interface ISharePointListsState { - listTitles: string[]; - loadingLists: boolean; - error: string; - loadingScripts: boolean; -} -``` - -Next, add the newly added property to the state definitions in the React component. In the code editor, open the **./src/webparts/sharePointLists/components/SharePointLists.tsx** file. Update the constructor to the following code: - -```ts -export default class SharePointLists extends React.Component { - constructor(props?: ISharePointListsProps, context?: any) { - super(); - - this.state = { - listTitles: [], - loadingLists: false, - error: null, - loadingScripts: true - }; - - this.getListsTitles = this.getListsTitles.bind(this); - } - // ... -} -``` - -In the same file, update the `getListsTitles` method to the following code: - -```ts -export default class SharePointLists extends React.Component { - // ... - private getListsTitles(): void { - this.setState({ - loadingLists: true, - listTitles: [], - error: null, - loadingScripts: false - }); - - const context: SP.ClientContext = new SP.ClientContext(this.props.siteUrl); - const lists: SP.ListCollection = context.get_web().get_lists(); - context.load(lists, 'Include(Title)'); - context.executeQueryAsync((sender: any, args: SP.ClientRequestSucceededEventArgs): void => { - const listEnumerator: IEnumerator = lists.getEnumerator(); - - const titles: string[] = []; - while (listEnumerator.moveNext()) { - const list: SP.List = listEnumerator.get_current(); - titles.push(list.get_title()); - } - - this.setState((prevState: ISharePointListsState, props: ISharePointListsProps): ISharePointListsState => { - prevState.listTitles = titles; - prevState.loadingLists = false; - return prevState; - }); - }, (sender: any, args: SP.ClientRequestFailedEventArgs): void => { - this.setState({ - loadingLists: false, - listTitles: [], - error: args.get_message(), - loadingScripts: false - }); - }); - } -} -``` - -To communicate the status of loading the SharePoint JSOM scripts to the user, update the `render` method to the following code: - -```tsx -export default class SharePointLists extends React.Component { - // ... - public render(): React.ReactElement { - const titles: JSX.Element[] = this.state.listTitles.map((listTitle: string, index: number, listTitles: string[]): JSX.Element => { - return
  • {listTitle}
  • ; - }); - - return ( -
    -
    - {this.state.loadingScripts && -
    -
    -
    -
    - SharePoint lists -
    -
    -
    -
    - Loading SharePoint JSOM scripts... -
    -
    -
    } - {this.state.loadingScripts === false && -
    -
    - Welcome to SharePoint! -

    Customize SharePoint experiences using Web Parts.

    -

    {escape(this.props.description)}

    - - Get lists titles -
    - {this.state.loadingLists && - Loading lists...} - {this.state.error && - An error has occurred while loading lists: {this.state.error}} - {this.state.error === null && titles && -
      - {titles} -
    } -
    -
    - } -
    -
    - ); - } - // ... -} -``` - -When the React component's state indicates that the SharePoint JSOM scripts are being loaded, it will display a placeholder. Once the scripts have been loaded, the web part will display the expected content with the button allowing users to load the information about SharePoint lists in the current site. - -### Load SharePoint JSOM Scripts Using SPComponentLoader - -SPFx components should load SharePoint JSOM scripts only once. In this example, given that the web part consists of a single React component, the best place to load SharePoint JSOM scripts is inside the React component's `componentDidMount` method, which executes only once after the component has been instantiated. - -In the code editor, open the **./src/webparts/sharePointLists/components/SharePointLists.tsx** file. In the top section of the file, add an `import` statement referencing the `SPComponentLoader`. Then, in the `SharePointLists` class, add the `componentDidMount` method: - -```ts -import { SPComponentLoader } from '@microsoft/sp-loader'; - -export default class SharePointLists extends React.Component { - // ... - public componentDidMount(): void { - SPComponentLoader.loadScript('/_layouts/15/init.js', { - globalExportsName: '$_global_init' - }) - .then((): Promise<{}> => { - return SPComponentLoader.loadScript('/_layouts/15/MicrosoftAjax.js', { - globalExportsName: 'Sys' - }); - }) - .then((): Promise<{}> => { - return SPComponentLoader.loadScript('/_layouts/15/SP.Runtime.js', { - globalExportsName: 'SP' - }); - }) - .then((): Promise<{}> => { - return SPComponentLoader.loadScript('/_layouts/15/SP.js', { - globalExportsName: 'SP' - }); - }) - .then((): void => { - this.setState((prevState: ISharePointListsState, props: ISharePointListsProps): ISharePointListsState => { - prevState.loadingScripts = false; - return prevState; - }); - }); - } - // ... -} -``` - -Using a series of chained promises, we load the different scripts that together enable using SharePoint JSOM in your SharePoint Framework component. Note, how by using the `SPComponentLoader` you can use server-relative URLs that will load the scripts from the current SharePoint tenant. Once all scripts have been loaded, you update the React component's state confirming that all prerequisites have been loaded and the web part is ready to use. - -Confirm that the web part is working as expected by running the following command from the console: - -```sh -gulp serve --nobrowser -``` - -Just as before, the web part should show the titles of SharePoint lists in the current site. - -![SharePoint Framework client-side web part showing titles of SharePoint lists in the current site](../../../images/tutorial-spjsom-web-part-list-titles.png) - -While using the `SPComponentLoader` requires some additional effort, it allows you to use server-relative URLs which is beneficial in scenarios when you're using different tenants for development, testing, and production. - -## Considerations - -In the past, when building client-side customizations on SharePoint you might have used SharePoint JSOM to communicate with SharePoint. However, the recommended approach is to use the SharePoint REST API either directly or through the [PnP JavaScript Core Library](https://github.com/SharePoint/PnP-JS-Core). - -When SharePoint JSOM was introduced, it was the first step towards supporting client-side solutions on SharePoint. However, it is no longer being actively maintained and might not offer access to all capabilities available through the REST API. Additionally, whether using the SharePoint REST API directly or through the PnP JavaScript Core Library, you can use promises which significantly simplify writing asynchronous code (a common problem when utilizing JSOM). - -Although there are still a limited number of cases where SharePoint JSOM provides access to data and methods not yet covered by the SharePoint REST API, where possible, the REST API should be preferred. - -If you have existing customizations using SharePoint JSOM and are considering migrating them to the SharePoint Framework, this article should provide you with the necessary information about using SharePoint JSOM in SharePoint Framework solutions. Longer term, however, you should consider changing how you communicate with SharePoint to either using the SharePoint REST API directly or through the PnP JavaScript Core Library. +--- +title: Connect to SharePoint using the JavaScript Object Model (JSOM) +description: How to use SharePoint JSOM when building solutions on the SharePoint Framework. +ms.date: 19/12/2022 +ms.localizationpriority: high +--- + +# Connect to SharePoint using the JavaScript Object Model (JSOM) + +> [!IMPORTANT] +> JSOM is considered as a legacy option to operate the data in the SharePoint Online. You should be looking into using the SharePoint REST API or Microsoft Graph APIs either directly or through the [Microsoft Graph SDKs](https://learn.microsoft.com/en-us/graph/sdks/sdk-installation) or with the [PnPjs JavaScript Library](https://github.com/pnp/pnpjs). + + +When building SharePoint customizations, you might have used the SharePoint [JavaScript Object Model (JSOM)](../../../sp-add-ins/complete-basic-operations-using-javascript-library-code-in-sharepoint.md) to communicate with SharePoint. This is no longer the recommended path (see [Considerations](#considerations) later in this article). However, there are still valid use cases for using the JSOM API such as code migration. + +To use SharePoint JSOM in your SharePoint Framework component, you must include it in your project. This wasn't previously required because it was already available on SharePoint pages for you to use. In the SharePoint Framework, you must explicitly load it into your component. + +There are two ways to reference SharePoint JSOM in the SharePoint Framework: + +- **Declarative**: through configuration +- **Imperative**: through code + +Each of these approaches has advantages and disadvantages, and it's important for you to understand each of them. + +## Create a new project + +1. From the console, create a new folder for your project: + + ```console + md react-sharepointlists + ``` + +1. Go to the project folder: + + ```console + cd react-sharepointlists + ``` + +1. In the project folder, run the SharePoint Framework Yeoman generator to scaffold a new SharePoint Framework project: + + ```console + yo @microsoft/sharepoint + ``` + +1. When prompted, enter the following values (*select the default option for all prompts omitted below*): + + - **What is your solution name?**: react-sharepointlists + - **Which type of client-side component to create?**: WebPart + - **What is your Web part name?**: SharePoint lists + - **Which template would you like to use?**: React + +1. Open your project folder in your code editor. This article uses Visual Studio Code in the steps and screenshots, but you can use any editor you prefer. + +1. To open the directory in Visual Studio Code, from the console enter: + + ```console + code . + ``` + +## Reference JSOM declaratively + +### Register the SharePoint JSOM API as external scripts + +When referencing JSOM declaratively, the first step is to register the SharePoint JSOM API as external scripts within your SharePoint Framework project. + +1. In your code editor, open the **./config/config.json** file, and add the following to the `externals` section: + + ```json + { + // ... + "externals": { + "sp-init": { + "path": "https://contoso.sharepoint.com/_layouts/15/init.js", + "globalName": "$_global_init" + }, + "microsoft-ajax": { + "path": "https://contoso.sharepoint.com/_layouts/15/MicrosoftAjax.js", + "globalName": "Sys", + "globalDependencies": [ + "sp-init" + ] + }, + "sp-runtime": { + "path": "https://contoso.sharepoint.com/_layouts/15/SP.Runtime.js", + "globalName": "SP", + "globalDependencies": [ + "microsoft-ajax" + ] + }, + "sharepoint": { + "path": "https://contoso.sharepoint.com/_layouts/15/SP.js", + "globalName": "SP", + "globalDependencies": [ + "sp-runtime" + ] + } + } + // ... + } + ``` + + Each of the entries points to different script files that together allow you to use SharePoint JSOM in your SPFx component. All of these scripts are distributed as non-module scripts. This is why each registration entry requires a URL (specified using the `path` property) and the name used by the script (provided in the `globalName` property). To ensure that these scripts load in the right order, the dependencies between these scripts are specified by using the `globalDependencies` property. + + Additional scripts may need to be added depending on the JSOM functionality that you're using (for example: **sp.taxonomy.js**). + +### Install TypeScript typings for SharePoint JSOM + +The next step is to install and configure TypeScript type declarations for SharePoint JSOM, which allows you to benefit from TypeScript's type safety features when working with the SharePoint JSOM. + +1. From the console, execute the following command within your project directory: + + ```console + npm install @types/microsoft-ajax @types/sharepoint --save-dev + ``` + + The SharePoint JSOM isn't distributed as a module, so you can't import it directly in your code. Instead, you need to register its TypeScript global type declaration globally. + +1. In the code editor, open the **./tsconfig.json** file, and in the `types` property, after the `webpack-env` entry, add references to `microsoft-ajax` and `sharepoint`: + + ```json + { + "compilerOptions": { + // ... + "types": [ + "es6-promise", + "es6-collections", + "webpack-env", + "microsoft-ajax", + "sharepoint" + ] + } + } + ``` + +### Reference SharePoint JSOM scripts in a React component + +To load the SharePoint JSOM libraries in your SPFx component, reference them in the component's code. In this example, you add the references in a React component where the SharePoint JSOM is used to communicate with SharePoint. + +In the code editor, open the **./src/webparts/sharePointLists/components/SharePointLists.tsx** file. After the last `import` statement, add the following code: + +```typescript +require('sp-init'); +require('microsoft-ajax'); +require('sp-runtime'); +require('sharepoint'); +``` + +These names correspond to the external references you previously added. The SharePoint Framework will load these scripts from the specified URLs. + +To demonstrate using the SharePoint JSOM for communicating with SharePoint, retrieve and render the titles of all SharePoint lists located in the current site. + +### Add `siteUrl` to the React component's properties + +To connect to SharePoint, the React component must know the URL of the current site. That URL is available in the parent web part and can be passed into the component through its properties. + +1. In the code editor, open the **./src/webparts/sharePointLists/components/ISharePointListsProps.ts** file, and add the `siteUrl` property to the `ISharePointListsProps` interface: + + ```typescript + export interface ISharePointListsProps { + description: string; + siteUrl: string; + } + ``` + +1. To pass the URL of the current site into the component, open the **./src/webparts/sharePointLists/SharePointListsWebPart.ts** file in the code editor, and change the `render()` method to: + + ```typescript + export default class SharePointListsWebPart extends BaseClientSideWebPart { + public render(): void { + const element: React.ReactElement = React.createElement( + SharePointLists, + { + description: this.properties.description, + siteUrl: this.context.pageContext.web.absoluteUrl + } + ); + + ReactDom.render(element, this.domElement); + } + + // ... + } + ``` + +### Define the React component's state + +The React component loads data from SharePoint and renders it to the user. The current state of the React component is modeled by using a state interface that we add. + +In the code editor, in the **./src/webparts/sharePointLists/components** folder, create a new file named **ISharePointListsState.ts** and paste in the following contents: + +```typescript +export interface ISharePointListsState { + listTitles: string[]; + loadingLists: boolean; + error: string; +} +``` + +### Add state to the React component + +Having defined the interface describing the shape of the component's state, the next step is to have the React component use that state interface. + +1. In the code editor, open the **./src/webparts/sharePointLists/components/SharePointLists.tsx** file. Under the existing `import` statements, add: + + ```typescript + import { ISharePointListsState } from './ISharePointListsState'; + ``` + +1. Change the signature of the `SharePointLists` class to: + + ```typescript + export default class SharePointLists extends React.Component { + // ... + } + ``` + +1. In the `SharePointLists` class, add a constructor with the default state value: + + ```typescript + export default class SharePointLists extends React.Component { + constructor(props?: ISharePointListsProps, context?: any) { + super(); + + this.state = { + listTitles: [], + loadingLists: false, + error: null + }; + } + + // ... + } + ``` + +### Load information about SharePoint lists from the current site using JSOM + +The sample client-side web part used in this article loads information about SharePoint lists in the current site after selecting a button. + + ![SharePoint Framework client-side web part showing titles of SharePoint lists in the current site](../../../images/tutorial-spjsom-web-part-list-titles.png) + +1. In the code editor, open the **./src/webparts/sharePointLists/components/SharePointLists.tsx** file. In the `SharePointLists` class, add a new method named `getListsTitles()`: + + ```typescript + export default class SharePointLists extends React.Component { + constructor(props?: ISharePointListsProps, context?: any) { + super(); + + this.state = { + listTitles: [], + loadingLists: false, + error: null + }; + + this.getListsTitles = this.getListsTitles.bind(this); + } + + // ... + private getListsTitles(): void { } + } + ``` + +1. Ensure the correct scoping of the method; bind it to the web part in the constructor. In the `getListsTitles()` method, use SharePoint JSOM to load the titles of SharePoint lists in the current site: + + ```typescript + export default class SharePointLists extends React.Component { + // ... + private getListsTitles(): void { + this.setState({ + loadingLists: true, + listTitles: [], + error: null + }); + + const context: SP.ClientContext = new SP.ClientContext(this.props.siteUrl); + const lists: SP.ListCollection = context.get_web().get_lists(); + context.load(lists, 'Include(Title)'); + context.executeQueryAsync((sender: any, args: SP.ClientRequestSucceededEventArgs): void => { + const listEnumerator: IEnumerator = lists.getEnumerator(); + + const titles: string[] = []; + while (listEnumerator.moveNext()) { + const list: SP.List = listEnumerator.get_current(); + titles.push(list.get_title()); + } + + this.setState((prevState: ISharePointListsState, props: ISharePointListsProps): ISharePointListsState => { + prevState.listTitles = titles; + prevState.loadingLists = false; + return prevState; + }); + }, (sender: any, args: SP.ClientRequestFailedEventArgs): void => { + this.setState({ + loadingLists: false, + listTitles: [], + error: args.get_message() + }); + }); + } + } + ``` + +Start by resetting the component's state to communicate to the user that the component is loading information from SharePoint. Using the URL of the current site passed to the component through its properties, we instantiate a new SharePoint context. Using the SharePoint JSOM, we load lists from the current site. To optimize the request for performance, we specify that only the `Title` property should be loaded. + +Next, we execute the query by calling the `executeQueryAsync()` method and passing two callback functions. After the query is completed, we enumerate through the collection of retrieved lists, store their titles in an array, and update the component's state. + +### Render the titles of SharePoint lists in the current site + +Having loaded the titles of SharePoint lists in the current site, the final step is to render them in the component. + +1. In the code editor, open the **./src/webparts/sharePointLists/components/SharePointLists.tsx** file, and update the `render()` method: + + ```tsx + export default class SharePointLists extends React.Component { + // ... + public render(): React.ReactElement { + const titles: JSX.Element[] = this.state.listTitles.map((listTitle: string, index: number, listTitles: string[]): JSX.Element => { + return
  • {listTitle}
  • ; + }); + + return ( +
    +
    +
    +
    + Welcome to SharePoint! +

    Customize SharePoint experiences using web parts.

    +

    {escape(this.props.description)}

    + + Get lists titles +
    + {this.state.loadingLists && + Loading lists...} + {this.state.error && + An error has occurred while loading lists: {this.state.error}} + {this.state.error === null && titles && +
      + {titles} +
    } +
    +
    +
    +
    + ); + } + // ... + } + ``` + +1. At this point, you can add your web part to the page and see the titles of SharePoint lists in the current site. To verify that the project is working correctly, run the following command from the console: + + ```console + gulp serve --nobrowser + ``` + +1. As you're using SharePoint JSOM to communicate with SharePoint, you have to test the web part by using the hosted version of the SharePoint Workbench (which is why the `--nobrowser` parameter is specified to prevent the automatic loading of the local Workbench). + + ![SharePoint Framework client-side web part showing titles of SharePoint lists in the current site](../../../images/tutorial-spjsom-web-part-list-titles.png) + + Referencing the SharePoint JSOM scripts declaratively as external scripts is convenient and allows you to keep your code clean. One disadvantage, however, is that it requires specifying absolute URLs to the location from which SharePoint JSOM scripts should be loaded. If you're using separate SharePoint tenants for development, testing, and production, it requires some additional work to change these URLs for the different environments. In such cases, you may consider referencing JSOM imperatively by using the [SPComponentLoader](/javascript/api/sp-loader/spcomponentloader) to load the scripts in the SPFx component's code. + +## Reference JSOM imperatively + +Another way to load JavaScript libraries in SharePoint Framework projects is to use the `SPComponentLoader`, a utility class provided with the SharePoint Framework designed to help you load scripts and other resources in your components. One benefit of using the `SPComponentLoader` over loading scripts declaratively is that it allows you to use server-relative URLs, which are more convenient when using different SharePoint tenants for the different stages of your development process. + +For this portion of the tutorial, we'll be adjusting the code we created previously in the Declarative section. + +### Install @microsoft/sp-loader Module + +`SPComponentLoader` is declared in a separate module **\@microsoft/sp-loader** that should be installed to your project. + +1. From the console, execute the following command within your project directory: + + ```console + npm install @microsoft/sp-loader --save + ``` + +### Declarative reference cleanup + +If you followed the steps in the declarative reference sections earlier, you need to remove those references. + +1. Remove the existing external script references. In the code editor, open the **./config/config.json** file, and from the `externals` property, remove all entries: + + ```json + { + "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json", + "version": "2.0", + "bundles": { + "share-point-lists-web-part": { + "components": [ + { + "entrypoint": "./lib/webparts/sharePointLists/SharePointListsWebPart.js", + "manifest": "./src/webparts/sharePointLists/SharePointListsWebPart.manifest.json" + } + ] + } + }, + "externals": {}, + "localizedResources": { + "SharePointListsWebPartStrings": "lib/webparts/sharePointLists/loc/{locale}.js" + } + } + ``` + + With the SharePoint JSOM scripts no longer being registered as external scripts, you can't reference them directly in your code. + +1. In the code editor, open the **./src/webparts/sharePointLists/components/SharePointLists.tsx** file, and remove the `require()` statements pointing to the different SharePoint JSOM scripts. + +### Wait to load data until the SharePoint JSOM scripts are loaded + +The primary functionality of the client-side web part that we're building in this tutorial depends on SharePoint JSOM. Depending on a number of factors, loading these scripts could take a few moments. When building SPFx components that use the SharePoint JSOM, you should take that into account. When added to the page, the web part should communicate to the user that it's loading its prerequisites and should make it clear when it's ready to be used. + +To support this, extend the React component's state with an additional property to track the status of loading the JSOM scripts. + +1. In the code editor, open the **./src/webparts/sharePointLists/components/ISharePointListsState.ts** file, and paste the following code: + + ```typescript + export interface ISharePointListsState { + listTitles: string[]; + loadingLists: boolean; + error: string; + loadingScripts: boolean; + } + ``` + +1. Add the newly added property to the state definitions in the React component. In the code editor, open the **./src/webparts/sharePointLists/components/SharePointLists.tsx** file. Update the constructor to the following code: + + ```typescript + export default class SharePointLists extends React.Component { + constructor(props?: ISharePointListsProps, context?: any) { + super(); + + this.state = { + listTitles: [], + loadingLists: false, + error: null, + loadingScripts: true + }; + + this.getListsTitles = this.getListsTitles.bind(this); + } + // ... + } + ``` + +1. In the same file, update the `getListsTitles()` method to the following code: + + ```typescript + export default class SharePointLists extends React.Component { + // ... + private getListsTitles(): void { + this.setState({ + loadingLists: true, + listTitles: [], + error: null, + loadingScripts: false + }); + + const context: SP.ClientContext = new SP.ClientContext(this.props.siteUrl); + const lists: SP.ListCollection = context.get_web().get_lists(); + context.load(lists, 'Include(Title)'); + context.executeQueryAsync((sender: any, args: SP.ClientRequestSucceededEventArgs): void => { + const listEnumerator: IEnumerator = lists.getEnumerator(); + + const titles: string[] = []; + while (listEnumerator.moveNext()) { + const list: SP.List = listEnumerator.get_current(); + titles.push(list.get_title()); + } + + this.setState((prevState: ISharePointListsState, props: ISharePointListsProps): ISharePointListsState => { + prevState.listTitles = titles; + prevState.loadingLists = false; + return prevState; + }); + }, (sender: any, args: SP.ClientRequestFailedEventArgs): void => { + this.setState({ + loadingLists: false, + listTitles: [], + error: args.get_message(), + loadingScripts: false + }); + }); + } + } + ``` + +1. To communicate the status of loading the SharePoint JSOM scripts to the user, update the `render()` method to the following code: + + ```tsx + export default class SharePointLists extends React.Component { + // ... + public render(): React.ReactElement { + const titles: JSX.Element[] = this.state.listTitles.map((listTitle: string, index: number, listTitles: string[]): JSX.Element => { + return
  • {listTitle}
  • ; + }); + + return ( +
    +
    + {this.state.loadingScripts && +
    +
    +
    +
    + SharePoint lists +
    +
    +
    +
    + Loading SharePoint JSOM scripts... +
    +
    +
    } + {this.state.loadingScripts === false && +
    +
    + Welcome to SharePoint! +

    Customize SharePoint experiences using web parts.

    +

    {escape(this.props.description)}

    + + Get lists titles +
    + {this.state.loadingLists && + Loading lists...} + {this.state.error && + An error has occurred while loading lists: {this.state.error}} + {this.state.error === null && titles && +
      + {titles} +
    } +
    +
    + } +
    +
    + ); + } + // ... + } + ``` + +1. When the React component's state indicates that the SharePoint JSOM scripts are being loaded, it displays a placeholder. After the scripts have been loaded, the web part displays the expected content with the button allowing users to load the information about SharePoint lists in the current site. + +### Load the SharePoint JSOM scripts with the SPComponentLoader + +SPFx components should load SharePoint JSOM scripts only once. In this example, given that the web part consists of a single React component, the best place to load SharePoint JSOM scripts is inside the React component's `componentDidMount()` method, which executes only once after the component has been instantiated. + +1. In the code editor, open the **./src/webparts/sharePointLists/components/SharePointLists.tsx** file. In the top section of the file, add an `import` statement referencing the `SPComponentLoader`. In the `SharePointLists` class, add the `componentDidMount()` method: + + ```tsx + import { SPComponentLoader } from '@microsoft/sp-loader'; + + export default class SharePointLists extends React.Component { + // ... + public componentDidMount(): void { + SPComponentLoader.loadScript('/_layouts/15/init.js', { + globalExportsName: '$_global_init' + }) + .then((): Promise<{}> => { + return SPComponentLoader.loadScript('/_layouts/15/MicrosoftAjax.js', { + globalExportsName: 'Sys' + }); + }) + .then((): Promise<{}> => { + return SPComponentLoader.loadScript('/_layouts/15/SP.Runtime.js', { + globalExportsName: 'SP' + }); + }) + .then((): Promise<{}> => { + return SPComponentLoader.loadScript('/_layouts/15/SP.js', { + globalExportsName: 'SP' + }); + }) + .then((): void => { + this.setState((prevState: ISharePointListsState, props: ISharePointListsProps): ISharePointListsState => { + prevState.loadingScripts = false; + return prevState; + }); + }); + } + // ... + } + ``` + +1. Using a series of chained promises, we load the different scripts that together enable using SharePoint JSOM in your SharePoint Framework component. By using the `SPComponentLoader`, you can use server-relative URLs that load the scripts from the current SharePoint tenant. After all scripts have been loaded, you update the React component's state confirming that all prerequisites have been loaded and the web part is ready to use. +1. Confirm that the web part is working as expected by running the following command from the console: + + ```console + gulp serve --nobrowser + ``` + + As before, the web part should show the titles of SharePoint lists in the current site. + + ![SharePoint Framework client-side web part showing titles of SharePoint lists in the current site](../../../images/tutorial-spjsom-web-part-list-titles.png) + +While using the `SPComponentLoader` requires some additional effort, it allows you to use server-relative URLs, which is beneficial in scenarios when you're using different tenants for development, testing, and production. + +## Considerations + +In the past, when building client-side customizations on SharePoint, you might have used SharePoint JSOM to communicate with SharePoint. However, the recommended approach is to use the SharePoint REST API either directly or through the [PnP JavaScript Core Library](https://github.com/pnp/pnpjs). + +When the SharePoint JSOM was introduced, it was the first step towards supporting client-side solutions on SharePoint. However, it is no longer being actively maintained and might not offer access to all capabilities available through the REST API. Additionally, whether using the SharePoint REST API directly or through the PnP JavaScript Core Library, you can use promises that significantly simplify writing asynchronous code (a common problem when utilizing JSOM). + +Although there are still a limited number of cases where SharePoint JSOM provides access to data and methods not yet covered by the SharePoint REST API, where possible, the REST API should be preferred. + +If you have existing customizations using SharePoint JSOM and are considering migrating them to the SharePoint Framework, this article should provide you with the necessary information about using the SharePoint JSOM in SharePoint Framework solutions. Longer term, however, you should consider changing how you communicate with SharePoint to either using the SharePoint REST API directly or through the PnP JavaScript Core Library. diff --git a/docs/spfx/web-parts/guidance/getting-started-with-top-actions.md b/docs/spfx/web-parts/guidance/getting-started-with-top-actions.md new file mode 100644 index 000000000..5473106ec --- /dev/null +++ b/docs/spfx/web-parts/guidance/getting-started-with-top-actions.md @@ -0,0 +1,183 @@ +--- +title: Adding support for web part Top Actions +description: Top Actions is a SharePoint Framework feature that allows web part developers to add commands to a web part's toolbar. +ms.date: 04/12/2023 +ms.localizationpriority: high +--- +# Adding support for web part Top Actions + +Top Actions provide an alternative and more end user friendlier way to expose the different options and configuration capabilities for web parts in edit mode. You can use Top Actions to surface most common configurations from the web part property panel directly in web part toolbar, which is exposed when the page is in edit mode. + +![Top Actions Example](../../../images/webpart-top-actions.png) + +> [!IMPORTANT] +> Web part Top Actions were introduced in the [SharePoint Framework (SPFx) v1.17 release](../../release-1.17.md). + +## Getting started + +To add Top Actions to a web part, you'll implement two things: + +- return a collection of Top Actions to display at the top of the web part when the page is in edit mode +- implement a handler that's called by SPFx when the Top Action is selected + +Both of these steps are accomplished by providing a configuration object of type [ITopActions](/javascript/api/sp-top-actions/itopactions) to the SPFx. + +> [!TIP] +> These instructions assume you know [how to create a hello world web part](../get-started/build-a-hello-world-web-part.md). + +## Define a Top Action configuration + +Adding Top Actions to a web part follows a similar pattern to configuring the web part property pane. + +To add Top Actions to a web part, implement the `getTopActionsConfiguration()` member that returns an object of type ITopActions: + +```typescript +// existing imports omitted for brevity +import { + ITopActions, + ITopActionsField +} from '@microsoft/sp-top-actions'; + +export default class HelloWorldWebPart extends BaseClientSideWebPart { + + // existing web part members omitted for brevity + + public getTopActionsConfiguration(): ITopActions | undefined { + return { + topActions: [], + onExecute: (actionName: string, newValue: any): void { } + }; + } + +} +``` + +Notice the returned object has two properties: + +- `topActions`: this contains an array of supported Top Action controls that are rendered in the web part's toolbar when the page is in edit mode +- `onExecute`: this handler is called when one of the Top Actions is selected + +## Define the Top Actions user interface + +The `topActions` array is an ordered list of controls, or *fields* of type [ITopActionsField](/javascript/api/sp-top-actions/itopactionsfield), to render in the web part toolbar. You can choose from two types of user interface elements: + +- button +- dropdown list + +All controls must have the following properties defined: + +- `type`: this is the type of the control - *button* (`TopActionsFieldType.Button`) or *dropdown list* (`TopActionsFieldType.Dropdown) +- `targetProperty`: this is the name of the Top Action +- `properties`: properties unique to the type of Top Action control + +You can optionally specify a `title` which is used as the tooltip & value for an `aria-label` property, and the Boolean `shouldFocus` flag to indicate if the control should be focused. + +## Define the Top Actions button field + +The following code defines a button Top Action control: + +```typescript +import { + ITopActions, + TopActionsFieldType +} from '@microsoft/sp-top-actions'; + +return { + topActions: [ + { + targetProperty: 'button', + type: TopActionsFieldType.Button, + title: 'Button', + properties: { + text: 'Button', + icon: 'SharePointLogo' + } + } + ], + onExecute: (actionName: string, newValue: any): void { } +} +``` + +The `properties` for a button control are defined in the [ITopActionsButtonProps](/javascript/api/sp-top-actions/itopactionsbuttonprops) interface. + +## Define the Top Actions dropdown field + +The following code defines a dropdown Top Action control: + +```typescript +import { + ITopActions, + TopActionsFieldType +} from '@microsoft/sp-top-actions'; + +return { + topActions: [ + { + targetProperty: 'dropdown', + type: TopActionsFieldType.Dropdown, + title: 'Dropdown', + properties: { + options: [{ + key: '1', + text: 'Option 1' + }, { + key: '2', + text: 'Option 2' + }] + } + } + ], + onExecute: (actionName: string, newValue: any): void { } +} +``` + +The `properties` for a dropdown control are defined in the [ITopActionsButtonProps](/javascript/api/sp-top-actions/itopactionsdropdownoption) interface. + +## Define the handler when controls are selected + +The last step is to implement the handler when Top Action controls are selected. This is done by implementing the `onExecute()` method on the ITopActions interface. + +The `onExecute()` handler passes two arguments in that you can handle: + +- `actionName`: maps to the `targetProperty` of the Top Action control that triggered this handler to be called +- `updatedValue`: when the Top Action is of type dropdown, this is the `key` property of the selected dropdown option; otherwise when the Top Action is of type button, the value of this property is `true` + +```typescript +public getTopActionsConfiguration(): ITopActions | undefined { + return { + topActions: [{ + type: TopActionsFieldType.Button, + title: 'Button', + targetProperty: 'button', + properties: { + text: 'Button', + icon: 'SharePointLogo' + } + }, { + type: TopActionsFieldType.Dropdown, + title: 'Dropdown', + targetProperty: 'dropdown', + properties: { + options: [{ + key: '1', + text: 'Option 1' + }, { + key: '2', + text: 'Option 2' + }] + } + }], + onExecute(actionName, updatedValue) { + console.log('onExecute', actionName, updatedValue); + } + } +} +``` + +## Testing & debugging Top Actions + +Similar to the different types of SPFx extensions, Top Actions must be tested in a live SharePoint modern page. They won't render on the SharePoint hosted workbench. + +### See more + +- [Top Actions API](/javascript/api/sp-top-actions) diff --git a/docs/spfx/web-parts/guidance/governance-considerations.md b/docs/spfx/web-parts/guidance/governance-considerations.md index 4971cc771..61dbca6d0 100644 --- a/docs/spfx/web-parts/guidance/governance-considerations.md +++ b/docs/spfx/web-parts/guidance/governance-considerations.md @@ -1,64 +1,66 @@ ---- -title: SharePoint Framework solutions governance considerations -ms.date: 09/25/2017 -ms.prod: sharepoint ---- - - -# SharePoint Framework solutions governance considerations - -With the SharePoint Framework, your organization can easily build solutions that easily integrate the capabilities available in SharePoint and Office 365. SharePoint Framework solutions work across modern web technologies and different mobile devices so you can create productive experiences and apps that are responsive and mobile-ready from day one. In order to get the moust benefit from SharePoint Framework solutions, your organization should have an actionable governance plan covering the most important project management considerations. - -## Anatomy of SharePoint Framework solutions - -![Diagram illustrating the composition of SharePoint Framework solutions](../../../images/guidance-governance-spfx-structure-schema.png) - -SharePoint Framework solutions consist of two parts: code (often referred to as web part bundle), deployed to a URL, and an .sppkg file that contains web part manifest with a URL pointing to the location where the web part code is deployed. There are no particular restrictions to where the code is deployed, as long as users working with the web part can access the web part code. Organizations can choose for example to have their web parts deployed to the [Office 365 public CDN](https://dev.office.com/blogs/office-365-public-cdn-developer-preview-release), [Azure storage](../get-started/deploy-web-part-to-cdn.md) or a privately owned web server. - -## Web part code hosting location considerations - -The most important thing that organizations should know, before deploying SharePoint Framework solutions, is where the code of the solution is deployed. SharePoint Framework solutions are executed as a part of the page in the context of the current user. As a result, whatever the user can do, the web part's code can do as well. In contrast to SharePoint add-ins, there is no separate permission scope applied to SharePoint Framework solutions. This is why SharePoint administrators should treat SharePoint Framework solutions as high-trust solutions--the same way they treat farm solutions on-premises. The location where the web part's code is deployed is important for a number of reasons. - -Consider the following location issues: - -### Is the code hosting location supported by the organization? - -SharePoint Framework doesn't impose any restrictions regarding where the solution's code is deployed. As a result, developers and vendors can deploy the code to a range of locations within or outside of the organization's IT department. Different organizations may have different server requirements ranging from access policies to SLAs. Before deploying a SharePoint Framework solution package, organizations should ensure that the server used to host the code is a known server approved to be used by the organization. - -### Who manages the code hosting location? - -SharePoint Framework solutions execute as a part of the page in the context of the current user. While an organization could perform a code review before deploying a solution package, in order to verify that the code can be trusted, it also should ensure the integrity of the code as long as it's deployed to the tenant. Organizations should have a clear understanding of who manages the hosting location, who and under what circumstances they can modify the files, and what the update approval process looks like. Establishing this information upfront not only helps organizations control the update process, but also lowers the risk of deploying malicious code. - -### What is the SLA for the hosting location? - -When organizations use Office 365 and SharePoint Online, they rely on the SLA provided by Microsoft. SharePoint Framework solutions that extend the standard capabilities of SharePoint and Office 365 should be deployed to servers that meet or exceed the SLA provided by Microsoft. That way, organizations can ensure that they are able to truly benefit from the added values of their customizations. - -### Is the hosting location optimized for performance? - -Loading existing libraries from a URL instead of embedding them in the web part bundle is the first step to speed up the loading time of SharePoint Framework solutions. To get the most out of it, you want to ensure that the server hosting the different scripts is correctly configured for optimal performance. It should serve all files compressed and the longer it allows proxies and clients to cache the files, the longer users will be able to load these scripts from their local cache, significantly speeding up loading SharePoint pages containing web parts. - -## Tools and libraries - -When building client-side solutions, developers can choose from a variety of libraries such as React, Angular, jQuery or Knockout. Using an existing JavaScript library makes it easier for developers to build rich solutions. There are big differences between how the different libraries work, and often specific knowledge is required to fully understand how to build a solution using the particular library. - -Once released to your production tenant, you should ensure that your support organization (either your own IT department or a contracted third party) is capable of supporting the solution. To do this, the support organization should have at least a basic understanding of the library used to build that solution. Also, as you increase the number libraries you use across your tenant, the harder it will be to support the different solutions. Selecting one or two libraries to use in your organization helps you lower the operational costs. Before deploying a solution to your production tenant, you should ensure that the solution is using only libraries supported in your organization. - -## Using external scripts - -When using existing JavaScript libraries, developers can either choose to include them in the web part code bundle or load them from a URL. Loading libraries from URLs allows developers to optimize SharePoint Framework solutions for performance. Because libraries are loaded from a URL, they don't need to be included in the web part bundle which decreases its size making it load faster. Additionally, by referencing the same libraries across the whole tenant, SharePoint Framework solutions will load faster by reusing the previously downloaded scripts from the local cache. - -There are no restrictions to where the existing libraries can be loaded from and it's important to know from which servers the external scripts are loaded. Together with the web part code, these scripts run in the context of the current user and can do whatever the current user is capable of doing. Therefore, it's important that you trust these scripts and their integrity. Some organizations have strict policies relating to resources sourced from public CDNs and you should ensure that the solution and its resources meet your organizational policies. - -## Approving SharePoint Framework solutions for deployment - -SharePoint Framework solutions are deployed to a tenant centrally through the App Catalog. Your organization should have a plan in place describing who is allowed to deploy and approve SharePoint Framework packages. This is important, because this plan should include who is responsible to verify that the packages that are deployed are secure and meet the organizational policies. SharePoint Framework solutions run in browser in the context of the current user and, unlike SharePoint add-ins, always have the same permissions as the currently signed-in user. Before deploying and approving a SharePoint Framework solution for use in your organization, its origin and other criteria mentioned previously in this article should be carefully examined. - -In order to verify that your SharePoint Framework solution meets your organization's policies, you should review the contents of the .sppkg package that you want to deploy and closely examine the contents of the referenced scripts and the location where they are hosted. This step can be performed manually or it can be automated by using third party tooling. [SharePoint Customization Analysis Framework](https://rencore.com/products/#spcaf) (SPCAF) is an example of third party solution that significantly simplifies the process of analyzing the contents of SharePoint Framework solutions and verifying that they meet your organizational security and governance requirements. - -## SharePoint Framework solutions and no-script sites - -In Office 365, organizations can use the no-script setting to disable script-based customizations in SharePoint Online. Organizations can configure the no-script setting either for the whole tenant or for a particular site collections. Based on the criteria from the organizational policies, administrators can use the no-script setting to disable customizations built for example using the script editor web part or a user custom action. - -The no-script setting is meant for organizations to apply an additional layer of control and security to either the whole tenant or specific site collections. Customizing SharePoint using script embedding and injecting is not without risks and particularly on sites containing sensitive information should be thoroughly evaluated. - -In the past, developers used script embedding and injecting techniques for building powerful SharePoint customizations. In some cases, these customizations relied on specific page structure and when the particular customization changed, it would stop working correctly. To guide developers to build more robust solutions, the SharePoint engineering team decided that all modern sites should have the no-script setting enabled. This means that embedding and injecting scripts on these sites is not possible and using the SharePoint Framework is currently the only option to customize these sites. In the future, all modern sites will use the no-script setting and alternatives to script embedding and injecting will become available for developers to support the different scenarios. +--- +title: SharePoint Framework solutions governance considerations +description: To get the most benefit from SharePoint Framework solutions, your organization should have an actionable governance plan covering the most important project management considerations. +ms.date: 07/01/2020 +ms.localizationpriority: medium +--- +# SharePoint Framework solutions governance considerations + +With the SharePoint Framework, your organization can easily build solutions that easily integrate the capabilities available in SharePoint and Microsoft 365. SharePoint Framework solutions work across modern web technologies and different mobile devices so you can create productive experiences and apps that are responsive and mobile-ready from day one. To get the most benefit from SharePoint Framework solutions, your organization should have an actionable governance plan covering the most important project management considerations. + +> [!NOTE] +> Throughout this article, the word **component** is used to refer to all types of things developers can build with the SharePoint Framework. This includes client-side web parts, extensions, and library components. + +## Anatomy of SharePoint Framework solutions + +![Diagram illustrating the composition of SharePoint Framework solutions](../../../images/guidance-governance-spfx-structure-schema.png) + +SharePoint Framework solutions consist of two parts: code (often referred to as a component's bundle) deployed to a URL, and an **\*.sppkg** file that contains a component's manifest with a URL pointing to the location where the component's code is deployed. The component's code can be deployed anywhere, as long as users working with the component can access the component's code. Organizations can choose, for example, to have their components deployed to the [Microsoft 365 public CDN](https://developer.microsoft.com/office/blogs/office-365-public-cdn-developer-preview-release), [Azure storage](../get-started/deploy-web-part-to-cdn.md), or a privately owned web server. + +## Web part code-hosting location considerations + +The most important thing that organizations should know before deploying SharePoint Framework solutions is where the code of the solution is deployed. SharePoint Framework solutions are executed as a part of the page in the context of the current user. As a result, whatever the user can do, the component's code can do as well. In contrast to SharePoint Add-ins, there's no separate permission scope applied to SharePoint Framework solutions. This is why SharePoint administrators should treat SharePoint Framework solutions as high-trust solutions, the same way they treat farm solutions on-premises. The location where the component's code is deployed is important for a number of reasons. + +### Is the code-hosting location supported by the organization? + +SharePoint Framework doesn't impose any restrictions where the solution's code is deployed. As a result, developers and vendors can deploy the code to a range of locations within or outside the organization's IT department. Different organizations may have different server requirements ranging from access policies to SLAs. + +Before deploying a SharePoint Framework solution package, organizations should ensure that the server used to host the code is a known server approved to be used by the organization. + +### Who manages the code-hosting location? + +SharePoint Framework solutions execute as a part of the page in the context of the current user. While an organization could do a code review before deploying a solution package, to verify that the code can be trusted, it also should ensure the integrity of the code as long as it's deployed to the tenant. Organizations should have a clear understanding of who manages the hosting location, who and under what circumstances they can modify the files, and what the update approval process looks like. Establishing this information upfront not only helps organizations control the update process, but also lowers the risk of deploying malicious code. + +### What is the SLA for the hosting location? + +When organizations use Microsoft 365 and SharePoint Online, they rely on the SLA provided by Microsoft. SharePoint Framework solutions that extend the standard capabilities of SharePoint and Microsoft 365 should be deployed to servers that meet or exceed the SLA provided by Microsoft. That way, organizations can ensure that they're able to truly benefit from the added values of their customizations. + +### Is the hosting location optimized for performance? + +Loading existing libraries from a URL instead of embedding them in the component bundle is the first step to speed up the loading time of SharePoint Framework solutions. To get the most out of it, you want to ensure that the server hosting the different scripts is correctly configured for optimal performance. It should serve all files compressed, and the longer it allows proxies and clients to cache the files, the longer users will load these scripts from their local cache, significantly speeding up loading SharePoint pages containing components. + +## Tools and libraries + +When building client-side solutions, developers can choose from various libraries such as React, Angular, jQuery, or Knockout. Using an existing JavaScript library makes it easier for developers to build rich solutions. There are differences between how the different libraries work, and often specific knowledge is required to fully understand how to build a solution using the particular library. + +Once released to your production tenant, you should ensure that your organization (either your own IT department or a contracted third party) can support the solution. To do this, the support organization should have at least a basic understanding of the library used to build that solution. Also, as you increase the number of libraries that you use across your tenant, the harder it's to support the different solutions. Selecting one or two libraries to use in your organization helps you lower the operational costs. Before deploying a solution to your production tenant, you should ensure that the solution is using only libraries supported in your organization. + +## Using external scripts + +When using existing JavaScript libraries, developers can either choose to include them in the component code bundle or load them from a URL. Loading libraries from URLs allows developers to optimize SharePoint Framework solutions for performance. Because libraries are loaded from a URL, they don't need to be included in the component bundle, which reduces its size making it load faster. Additionally, by referencing the same libraries across the whole tenant, SharePoint Framework solutions load faster by reusing the previously downloaded scripts from the local cache. + +There are no restrictions to where the existing libraries can be loaded from, and it's important to know from which servers the external scripts are loaded. Together with the component code, these scripts run in the context of the current user and can do whatever the current user can do. It's important that you trust these scripts and their integrity. Some organizations have strict policies relating to resources sourced from public CDNs, and you should ensure that the solution and its resources meet your organizational policies. + +## Approving SharePoint Framework solutions for deployment + +SharePoint Framework solutions are deployed to a tenant centrally through the app catalog. They can also be deployed to select sites using the site collection scoped app catalog. Your organization should have a plan in place describing who is allowed to deploy and approve SharePoint Framework packages. This is important, because this plan should include who is responsible to verify that the packages that are deployed are secure and meet the organizational policies. SharePoint Framework solutions run in the browser in the context of the current user and, unlike SharePoint Add-ins, always have the same permissions as the currently signed-in user. Before deploying and approving a SharePoint Framework solution for use in your organization, its origin and other criteria mentioned previously in this article should be carefully examined. + +To verify that your SharePoint Framework solution meets your organization's policies, you should review the contents of the **\*.sppkg** package that you want to deploy and closely examine the contents of the referenced scripts and the location where they're hosted. This step can be performed manually, or it can be automated by using third-party tooling. [SharePoint Customization Analysis Framework](https://rencore.com/products/#spcaf) (SPCAF) is an example of a third-party solution that significantly simplifies the process of analyzing the contents of SharePoint Framework solutions and verifying that they meet your organizational security and governance requirements. + +## SharePoint Framework solutions and no-script sites + +In Microsoft 365, organizations can use the no-script setting to disable script-based customizations in SharePoint Online. Organizations can configure the no-script setting either for the whole tenant or for a particular site collection. Based on the criteria from the organizational policies, administrators can use the no-script setting to disable customizations built, for example, by using the Script Editor component or a user custom action. + +The no-script setting is meant for organizations to apply an additional layer of control and security to either the whole tenant or specific site collections. Customizing SharePoint using script embedding and injecting isn't without risks, and particularly on sites containing sensitive information, should be thoroughly evaluated. + +In the past, developers used script embedding and injecting techniques for building powerful SharePoint customizations. In some cases, these customizations relied on specific page structure, and when the particular customization changed, it would stop working correctly. To guide developers to build more robust solutions, the SharePoint engineering team decided that all modern sites should have the no-script setting enabled. This means that embedding and injecting scripts on these sites isn't possible, and using the SharePoint Framework is currently the only option to customize these sites. In the future, all modern sites will use the no-script setting, and alternatives to script embedding and injecting will become available for developers to support the different scenarios. diff --git a/docs/spfx/web-parts/guidance/integrate-web-part-properties-with-sharepoint.md b/docs/spfx/web-parts/guidance/integrate-web-part-properties-with-sharepoint.md index 9045788d5..0031f2dd9 100644 --- a/docs/spfx/web-parts/guidance/integrate-web-part-properties-with-sharepoint.md +++ b/docs/spfx/web-parts/guidance/integrate-web-part-properties-with-sharepoint.md @@ -1,76 +1,84 @@ ---- -title: Integrate web part properties with SharePoint -ms.date: 09/25/2017 -ms.prod: sharepoint ---- - - -# Integrate web part properties with SharePoint - -When building classic web parts, web part properties were isolated from SharePoint and their values were managed by end-users. SharePoint Frameworks offers you a new set of capabilities that simplify managing web part properties' values and integrate them with SharePoint Search. This article explains how you can use these capabilities when building SharePoint Framework client-side web parts. - -> **Important:** The following guide applies only to SharePoint Framework client-side web parts placed on modern SharePoint pages. Capabilities described in this article don't apply to classic web parts or SharePoint Framework client-side web parts placed on classic pages. - -## Client-side web part properties - -When building SharePoint Framework client-side web parts, you can define properties that can be configured by users. By using properties instead of fixed values, web parts are more flexible and suitable for many different scenarios. - -Comparing to classic web parts, there are some differences in how the SharePoint Framework handles web parts properties. The following schema illustrates how web part properties values flow through the different layers of SharePoint. - -![Schema illustrating how the SharePoint Framework handles web part properties](../../../images/integrate-webpart-properties-schema.png) - -Before accepting values for web part properties from the end-users you should always [validate them](./validate-web-part-property-values.md). This will not only allow you to ensure that your web parts are user-friendly, but will also help you prevent storing invalid data in the web part's configuration. Additionally, you should consider that the SharePoint Framework doesn't support personalization and all users see the same configuration of the particular web part. - -## Specify web part property value type - -In classic SharePoint web parts, web part property values were isolated from SharePoint. If you had a web part property with a URL of a file stored in SharePoint, you had to manually ensure that this URL was valid and pointing to a correct document in case it was moved or renamed. Also, if you allowed users to enter some text to be displayed in the web part, that text wouldn't be indexed by SharePoint Search. - -When building web parts, SharePoint Framework allows you to specify what kind of value the particular web part property holds. This configuration determines how SharePoint handles the value. Depending on the specified configuration, SharePoint can include the value of the particular property in the Search index, remove unsafe HTML and even keep links to documents stored in SharePoint up to date in case a file gets moved or renamed. - -To specify the configuration for your web part properties, in the web part class, override the **propertiesMetadata** getter: - -```ts -import { - BaseClientSideWebPart, - IPropertyPaneConfiguration, - PropertyPaneTextField, - IWebPartPropertiesMetadata -} from '@microsoft/sp-webpart-base'; - -// ... - -export default class ArticleLinkWebPart extends BaseClientSideWebPart { - // ... - protected get propertiesMetadata(): IWebPartPropertiesMetadata { - return { - 'title': { isSearchablePlainText: true }, - 'intro': { isHtmlString: true }, - 'image': { isImageSource: true }, - 'url': { isLink: true } - }; - } - // ... -} -``` - -The **propertiesMetadata** method returns an object, where the property is a string and specifies the name of the web part property and the value is an object specifying how SharePoint should handle that particular property. When overriding the **propertiesMetadata** method, you don't have to list all web part properties. By default, web part properties' values are not processed by SharePoint and you should include only those properties that you want to be processed. - -Following is the list of possible values that can be set in the properties metadata and their impact on how the value of the web part property is processed by SharePoint. - -Metadata value|Searchable|Link fixup|Remove unsafe HTML ---------------|:--------:|:--------:|:----------------: -none (default)|no|no|no -`isSearchablePlainText`|yes|no|no -`isHtmlString`|yes|yes|yes -`isImageSource`|yes|yes|no -`isLink`|yes|yes|no - -> **Important:** when defining the configuration for your web part properties, you should use only one of the properties mentioned above for each web part property. Setting multiple properties will most likely lead to undesirable results and you should avoid it. - -By default the value of a web part property is not indexed by SharePoint Search and it's not processed by SharePoint in any way. It's passed to the web part exactly how it's been entered by the user configuring the web part. - -If you specify the web part property as `isSearchablePlainText`, it will be included in the full-text Search index. Whenever users search for any keywords included in the value of that property, SharePoint Search will return the page with the web part in the search results. If the value contains a link to a document stored in SharePoint, that link won't be updated if the referenced document is moved or renamed. Also, any HTML entered by users, will be kept intact. When working with the value of such property, you should treat it as plain-text and escape HTML that might be entered by users before rendering it on the page to avoid script injection. - -When a web part property is defined as `isHtmlString`, SharePoint will first of all remove any unsafe HTML, such as `script` tags, from the property value. The HTML that remains can be considered safe to render on a page. If the value contains any URLs pointing to files stored in SharePoint, as soon as one of these files is renamed or moved, SharePoint will automatically update the URL stored in the web part property. This significantly simplifies managing URLs across all web parts and pages in your tenant. HTML web part properties are also searchable, so users can look for any keywords included in the property value. - -Property value types `isImageSource` and `isLink` are meant to be used for web part properties than include nothing else but a link to an image or a file stored in SharePoint. In both cases, SharePoint Search includes the content in the full-text index and keeps the specified URL up-to-date in case the referenced file is renamed or moved. Additionally, image sources may get additional processing to help images download faster. If the page has a title image and the image is amongst the first 5 images on the page or the image is in the first two rows on the page, the image will be preloaded. +--- +title: Integrate web part properties with SharePoint +description: Use capabilities that simplify managing web part properties' values and integrate them with SharePoint Search when building SharePoint Framework client-side web parts. +ms.date: 06/16/2020 +ms.localizationpriority: high +--- + +# Integrate web part properties with SharePoint + +When building classic web parts, web part properties were isolated from SharePoint, and their values were managed by end users. SharePoint Framework offers you a new set of capabilities that simplify managing web part properties' values and integrate them with SharePoint Search. This article explains how you can use these capabilities when building SharePoint Framework client-side web parts. + +> [!IMPORTANT] +> The following guide applies only to SharePoint Framework client-side web parts placed on modern SharePoint pages. Capabilities described in this article don't apply to classic web parts or SharePoint Framework client-side web parts placed on classic pages. + +## Client-side web part properties + +When building SharePoint Framework client-side web parts, you can define properties that can be configured by users. By using properties instead of fixed values, web parts are more flexible and suitable for many different scenarios. + +Compared to classic web parts, there are some differences in how the SharePoint Framework handles web part properties. The following schema illustrates how web part property values flow through the different layers of SharePoint. + +![Schema illustrating how the SharePoint Framework handles web part properties](../../../images/integrate-webpart-properties-schema.png) + +Before accepting values for web part properties from end users, you should always [validate them](./validate-web-part-property-values.md). This not only allows you to ensure that your web parts are user-friendly, but also helps you prevent storing invalid data in the web part's configuration. + +Additionally, you should consider that the SharePoint Framework doesn't support personalization, and all users see the same configuration of the particular web part. + +## Specify web part property value type + +In classic SharePoint web parts, web part property values were isolated from SharePoint. If you had a web part property with a URL of a file stored in SharePoint, you had to manually ensure that this URL was valid and pointing to a correct document in case it was moved or renamed. Also, if you allowed users to enter some text to be displayed in the web part, that text wouldn't be indexed by SharePoint Search. + +When building web parts, SharePoint Framework allows you to specify what kind of value the particular web part property holds. This configuration determines how SharePoint handles the value. Depending on the specified configuration, SharePoint can include the value of the particular property in the Search index, remove unsafe HTML, and even keep links to documents stored in SharePoint up to date in case a file gets moved or renamed. + +To specify the configuration for your web part properties, in the web part class, override the `propertiesMetadata` getter: + +```typescript +import { + BaseClientSideWebPart, + IWebPartPropertiesMetadata +} from '@microsoft/sp-webpart-base'; +import { + IPropertyPaneConfiguration, + PropertyPaneTextField, +} from '@microsoft/sp-property-base'; + +// ... + +export default class ArticleLinkWebPart extends BaseClientSideWebPart { + // ... + protected get propertiesMetadata(): IWebPartPropertiesMetadata { + return { + 'title': { isSearchablePlainText: true }, + 'intro': { isHtmlString: true }, + 'image': { isImageSource: true }, + 'url': { isLink: true } + }; + } + // ... +} +``` + +The `propertiesMetadata` getter returns an object, where the property is a string and specifies the name of the web part property, and the value is an object specifying how SharePoint should handle that particular property. + +When overriding the `propertiesMetadata` getter, you don't have to list all the web part properties. By default, web part properties' values are not processed by SharePoint, and you should include only those properties that you want to be processed. + +Following is the list of possible values that can be set in the properties metadata and their impact on how the value of the web part property is processed by SharePoint. + +Metadata value|Searchable|Link fixup|Remove unsafe HTML +--------------|:--------:|:--------:|:----------------: +none (default)|no|no|no +`isSearchablePlainText`|yes|no|no +`isHtmlString`|yes|yes|yes +`isImageSource`|yes|yes|no +`isLink`|yes|yes|no + +> [!IMPORTANT] +> When defining the configuration for your web part properties, you should use only one of the properties mentioned in the table for each web part property. Setting multiple properties will most likely lead to undesirable results, and you should avoid it. + +By default the value of a web part property is not indexed by SharePoint Search and it's not processed by SharePoint in any way. It's passed to the web part exactly how it's been entered by the user configuring the web part. + +If you specify the web part property as `isSearchablePlainText`, it's included in the full-text Search index. Whenever users search for any keywords included in the value of that property, SharePoint Search returns the page with the web part in the search results. If the value contains a link to a document stored in SharePoint, that link won't be updated if the referenced document is moved or renamed. Also, any HTML entered by users, is kept intact. When working with the value of such a property, you should treat it as plain-text and escape HTML that might be entered by users before rendering it on the page to avoid script injection. + +When a web part property is defined as `isHtmlString`, SharePoint first of all removes any unsafe HTML, such as ` - - - - - - - - - - - - - - -
    IDBusiness unitCategoryStatusDue dateAssigned to
    - - -``` - -First, the customization loads the libraries it uses: jQuery, DataTables and Moment.js (lines 1-4). Next, it specifies the structure of the table used to present the data (lines 5-16). After creating the table, it wraps Moment.js into a DataTables plugin so that dates displayed in the table can be formatted (first script block on lines 17-70). Finally, the customization uses DataTables to load and present the list of IT support requests. The data is loaded using AJAX from a SharePoint list (lines 71-96). - -Thanks to using DataTables, end-users get a powerful solution where they can easily filter, sort and page through the results without any additional development effort. - -![The list of IT support requests displayed using DataTables filtered by requests assigned to Lidia sorted descending by the due date](../../../images/datatables-sewp-filter.png) - -## Migrate the IT requests overview solution from the Script Editor Web Part to the SharePoint Framework - -> **Note:** Before following the steps in this article, be sure to [set up your development environment](../../set-up-your-development-environment.md) for building SharePoint Framework solutions. - -Transforming this customization to the SharePoint Framework offers a number of benefits such as more user-friendly configuration and centralized management of the solution. Following is a step-by-step description of how you would migrate the solution to the SharePoint Framework. First, you will migrate the solution to the SharePoint Framework with as few changes to the original code as possible. Later, you will transform the solution's code to TypeScript to benefit of its development-time type safety features. - -> The source code of the project in the different stages of migration is available at [https://github.com/SharePoint/sp-dev-fx-webparts/tree/master/tutorials/tutorial-migrate-datatables](https://github.com/SharePoint/sp-dev-fx-webparts/tree/master/tutorials/tutorial-migrate-datatables). - -### Create new SharePoint Framework project - -Start by creating a new folder for your project - -```sh -md datatables-itrequests -``` - -Navigate to the project folder: - -```sh -cd datatables-itrequests -``` - -In the project folder run the SharePoint Framework Yeoman generator to scaffold a new SharePoint Framework project: - -```sh -yo @microsoft/sharepoint -``` - -When prompted, define values as follows: -- **datatables-itrequests** as your solution name -- **Use the current folder** for the location to place the files -- **No javaScript web framework** as the starting point to build the web part -- **IT requests** as your web part name -- **Shows overview of IT support requests** as your web part description - -![SharePoint Framework Yeoman generator with the default choices](../../../images/datatables-yeoman.png) - -Once the scaffolding completes, open your project folder in your code editor. In this tutorial, you will use Visual Studio Code. - -![SharePoint Framework project open in Visual Studio Code](../../../images/datatables-vscode.png) - -### Load JavaScript libraries - -Similarly to the original solution built using the Script Editor Web Part, first you need to load the JavaScript libraries required by the solution. In SharePoint Framework this usually consists of two steps: specifying the URL from which the library should be loaded, and referencing the library in the code. - -Start, with specifying the URLs from which libraries should be loaded. In the code editor, open the **./config/config.json** file and change the **externals** section to: - -```json -{ - "externals": { - "jquery": "https://code.jquery.com/jquery-1.12.4.min.js", - "datatables.net": "https://cdn.datatables.net/1.10.15/js/jquery.dataTables.min.js", - "moment": "https://momentjs.com/downloads/moment.min.js" - } -} -``` - -Next, open the **./src/webparts/itRequests/ItRequestsWebPart.ts** file, and after the last **import** statement add: - -```ts -import 'jquery'; -import 'datatables.net'; -import 'moment'; -``` - -### Define data table - -Just as in the original solution, the next step is to define the structure of the table used to display the data. In the code editor, open the **./src/webparts/itRequests/ItRequestsWebPart.ts** file and change the **render** method to: - -```ts -export default class ItRequestsWebPart extends BaseClientSideWebPart { - public render(): void { - this.domElement.innerHTML = ` - - - - - - - - - - - - -
    IDBusiness unitCategoryStatusDue dateAssigned to
    `; - } - // ... -} -``` - -### Register Moment.js plugin for DataTables - -The next step is to define the Moment.js plugin for DataTables so that dates in the table can be formatted. In the **./src/webparts/itRequests** folder, create a new file named **moment-plugin.js** and paste the following code: - -```js -// UMD -(function (factory) { - "use strict"; - - if (typeof define === 'function' && define.amd) { - // AMD - define(['jquery'], function ($) { - return factory($, window, document); - }); - } - else if (typeof exports === 'object') { - // CommonJS - module.exports = function (root, $) { - if (!root) { - root = window; - } - - if (!$) { - $ = typeof window !== 'undefined' ? - require('jquery') : - require('jquery')(root); - } - - return factory($, root, root.document); - }; - } - else { - // Browser - factory(jQuery, window, document); - } -} - -(function ($, window, document) { - $.fn.dataTable.render.moment = function (from, to, locale) { - // Argument shifting - if (arguments.length === 1) { - locale = 'en'; - to = from; - from = 'YYYY-MM-DD'; - } - else if (arguments.length === 2) { - locale = 'en'; - } - - return function (d, type, row) { - var moment = require('moment'); - var m = moment(d, from, locale, true); - - // Order and type get a number value from Moment, everything else - // sees the rendered value - return m.format(type === 'sort' || type === 'type' ? 'x' : to); - }; - }; -})); -``` - -For the web part to load the plugin, it has to reference the newly created **moment-plugin.js** file. In the code editor, open the **./src/webparts/itRequests/ItRequestsWebPart.ts** file and after the last **import** statement add: - -```ts -import './moment-plugin'; -``` - -> **Note:** you don't need to include the **.js** extension when referencing other files. SharePoint Framework will automatically resolve the extension for you. - -### Initiate DataTables and load data - -The last step is to include the code that initiates the data table and loads the data from SharePoint. In the **./src/webparts/itRequests** folder, create a new file named **script.js** and paste the following code: - -```js -$(document).ready(function () { - $('#requests').DataTable({ - 'ajax': { - 'url': "../../_api/web/lists/getbytitle('IT Requests')/items?$select=ID,BusinessUnit,Category,Status,DueDate,AssignedTo/Title&$expand=AssignedTo/Title", - 'headers': { 'Accept': 'application/json;odata=nometadata' }, - 'dataSrc': function (data) { - return data.value.map(function (item) { - return [ - item.ID, - item.BusinessUnit, - item.Category, - item.Status, - new Date(item.DueDate), - item.AssignedTo.Title - ]; - }); - } - }, - columnDefs: [{ - targets: 4, - render: $.fn.dataTable.render.moment('YYYY/MM/DD') - }] - }); -}); -``` - -In order to reference this file in the web part, in the code editor, open the **./src/webparts/itRequests/ItRequestsWebPart.ts** file and change the **render** method to: - -```ts -export default class ItRequestsWebPart extends BaseClientSideWebPart { - public render(): void { - this.domElement.innerHTML = ` - - - - - - - - - - - - -
    IDBusiness unitCategoryStatusDue dateAssigned to
    `; - - require('./script'); - } - // ... -} -``` - -Verify, that the web part is working as expected by in the command line executing: - -```sh -gulp serve --nobrowser -``` - -Because the web part loads its data from SharePoint, you have to test the web part using the hosted SharePoint Framework workbench. Navigate to **https://yourtenant.sharepoint.com/_layouts/workbench.aspx** and add the web part to the canvas. You should now see the IT requests displayed using the DataTables jQuery plugin. - -![IT requests displayed in a SharePoint Framework client-side web part](../../../images/datatables-spfx.png) - -## Add support for configuring the web part through web part properties - -In the previous steps you migrated the IT requests solutions from Script Editor Web Part to the SharePoint Framework. While the solution already works as expected, it doesn't use any of the SharePoint Framework benefits. The name of the list from which IT requests are loaded is included in the code and the code itself is plain JavaScript which is harder to refactor than TypeScript. The following steps illustrate how to extend the existing solution to allow users to specify the name of the list to load the data from. Later, you will transform the code to TypeScript to benefit of its type safety features. - -### Define web part property for storing the name of the list - -Start with defining a web part property to store the name of the list from which IT requests should be loaded. In the code editor, open the **./src/webparts/itRequests/ItRequestsWebPart.manifest.json** file and rename the default **description** property to **listName** and clear its value. - -![The listName property in the web part manifest highlighted in Visual Studio Code](../../../images/datatables-spfx-listname-property.png) - -Next, update the web part properties interface to reflect the changes in the manifest. In the code editor, open the **./src/webparts/itRequests/IItRequestsWebPartProps.ts** file and change its contents to: - -```ts -export interface IItRequestsWebPartProps { - listName: string; -} -``` - -Then, update the display labels for the **listName** property. Open the **./src/webparts/itRequests/loc/mystrings.d.ts** file and change its contents to: - -```ts -declare interface IItRequestsStrings { - PropertyPaneDescription: string; - BasicGroupName: string; - ListNameFieldLabel: string; -} - -declare module 'itRequestsStrings' { - const strings: IItRequestsStrings; - export = strings; -} -``` - -Next, open the **./src/webparts/itRequests/loc/en-us.js** file and change its contents to: - -```js -define([], function() { - return { - "PropertyPaneDescription": "IT Requests settings", - "BasicGroupName": "Data", - "ListNameFieldLabel": "List name" - } -}); -``` - -Finally, update the web part to use the newly defined property. In the code editor, open the **./src/webparts/itRequests/ItRequestsWebPart.ts** file and change the **getPropertyPaneConfiguration** method to: - -```ts -export default class ItRequestsWebPart extends BaseClientSideWebPart { - // ... - protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration { - return { - pages: [ - { - header: { - description: strings.PropertyPaneDescription - }, - groups: [ - { - groupName: strings.BasicGroupName, - groupFields: [ - PropertyPaneTextField('listName', { - label: strings.ListNameFieldLabel - }) - ] - } - ] - } - ] - }; - } - - protected get disableReactivePropertyChanges(): boolean { - return true; - } -} -``` - -To prevent the web part from reloading as users type the name of the list, you've also configured the web part to use the non-reactive property pane by adding the **disableReactivePropertyChanges** method and settings its return value to **true**. - -### Use the configured name of the list to load the data from - -Initially, the name of the list from which the data should be loaded was embedded in the REST query. Now that users can configure this name, the configured value should be injected into the REST query before loading the data. The easiest way to do that, is by moving the contents of the **script.js** file to the main web part file. - -In the code editor, open the **./src/webparts/itRequests/ItRequestsWebPart.ts** file and change the **render** method to: - -```ts -var $: any = (window as any).$; - -export default class ItRequestsWebPart extends BaseClientSideWebPart { - public render(): void { - this.domElement.innerHTML = ` - - - - - - - - - - - - -
    IDBusiness unitCategoryStatusDue dateAssigned to
    `; - - $(document).ready(() => { - $('table', this.domElement).DataTable({ - 'ajax': { - 'url': `../../_api/web/lists/getbytitle('${escape(this.properties.listName)}')/items?$select=ID,BusinessUnit,Category,Status,DueDate,AssignedTo/Title&$expand=AssignedTo/Title`, - 'headers': { 'Accept': 'application/json;odata=nometadata' }, - 'dataSrc': function (data) { - return data.value.map(function (item) { - return [ - item.ID, - item.BusinessUnit, - item.Category, - item.Status, - new Date(item.DueDate), - item.AssignedTo.Title - ]; - }); - } - }, - columnDefs: [{ - targets: 4, - render: $.fn.dataTable.render.moment('YYYY/MM/DD') - }] - }); - }); - } - - // ... -} -``` - -Instead of referencing the code from the **script.js** file, all of its contents are a part of the web part's **render** method. In the REST query, in line 40, you can now replace the fixed name of the list with the value of the **listName** property which holds the name of the list as configured by the user. Before using the value, it's being escaped using the lodash's **escape** function to disallow script injection. - -At this point, the bulk of the code is still written using plain JavaScript. To avoid build issues with the **$** jQuery variable, you had to define it as **any** type in line 18. Later, when transforming the code to TypeScript you will replace it with a proper type definition. - -As you have just moved the contents of the **script.js** file into the main web part file, the **script.js** is no longer necessary and you can delete it from the project. - -To verify that the web part is working as expected, run in the command line: - -```sh -gulp serve --nobrowser -``` - -Navigate to the hosted workbench and add the web part to the canvas. Open the web part property pane, specify the name of the list with IT requests and click the **Apply** button to confirm the changes. You should now see IT requests displayed in the web part. - -![IT requests loaded from the configured list and displayed in a SharePoint Framework client-side web part](../../../images/datatables-spfx-list-configured.png) - -## Transform the plain JavaScript code to TypeScript - -Using TypeScript over plain JavaScript offers a number of benefits. Not only is TypeScript easier to maintain and refactor but it also allows you to catch errors earlier. Following steps describe how you would transform the original JavaScript code to TypeScript. - -### Add type definitions for used libraries - -To function properly, TypeScript requires type definitions for the different libraries used in the project. Type definitions are often distributed as npm packages in the @types namespace. - -Start by installing type definitions for jQuery and DataTables by executing in the command line: - -```sh -npm install --save-dev @types/jquery @types/jquery.datatables -``` - -Type definitions for Moment.js are distributed together with the Moment.js package. Even though, you're loading Moment.js from a URL, in order to use its typings, you still need to install the Moment.js package in the project. - -Install the Moment.js package by executing in the command line: - -```sh -npm install --save moment -``` - -### Update package references - -In order to use types from the installed type definitions, you have to change how you reference libraries. In the code editor, open the **./src/webparts/itRequests/ItRequestsWebPart.ts** file and change `import 'jquery';` statement to: - -```ts -import * as $ from 'jquery'; -``` - -Having defined **$** as jQuery you can now remove the local definition of **$** that you've added previously: - -```ts -var $: any = (window as any).$; -``` - -Because DataTables is a jQuery plugin that attaches itself to jQuery you cannot load its type definition directly. Instead, you have to add it to the list of types loaded globally. In the code editor, open the **./tsconfig.json** file and to the **types** array add **jquery.datatables**: - -```json -{ - "compilerOptions": { - "target": "es5", - "forceConsistentCasingInFileNames": true, - "module": "commonjs", - "jsx": "react", - "declaration": true, - "sourceMap": true, - "types": [ - "es6-promise", - "es6-collections", - "jquery.datatables", - "webpack-env" - ] - } -} -``` - -### Update main web part files to TypeScript - -Now that you have type definitions for all libraries installed in the project, you can start transforming the plain JavaScript code to TypeScript. - -Start, with defining an interface for the IT request information that you retrieve from the SharePoint list. In the code editor, open the **./src/webparts/itRequests/ItRequestsWebPart.ts** file and just above the web part class, add the following code snippet: - -```ts -interface IRequestItem { - ID: number; - BusinessUnit: string; - Category: string; - Status: string; - DueDate: string; - AssignedTo: { Title: string; }; -} -``` - -Next, in the web part class, change the **render** method to: - -```ts -export default class ItRequestsWebPart extends BaseClientSideWebPart { - public render(): void { - this.domElement.innerHTML = ` - - - - - - - - - - - - -
    IDBusiness unitCategoryStatusDue dateAssigned to
    `; - - $('table', this.domElement).DataTable({ - 'ajax': { - 'url': `../../_api/web/lists/getbytitle('${escape(this.properties.listName)}')/items?$select=ID,BusinessUnit,Category,Status,DueDate,AssignedTo/Title&$expand=AssignedTo/Title`, - 'headers': { 'Accept': 'application/json;odata=nometadata' }, - 'dataSrc': (data: { value: IRequestItem[] }): any[][] => { - return data.value.map((item: IRequestItem): any[] => { - return [ - item.ID, - item.BusinessUnit, - item.Category, - item.Status, - new Date(item.DueDate), - item.AssignedTo.Title - ]; - }); - } - }, - columnDefs: [{ - targets: 4, - render: $.fn.dataTable.render.moment('YYYY/MM/DD') - }] - }); - } - - // ... -} -``` - -Notice, how the AJAX request, to retrieve the data from the SharePoint list, is now typed and helps you ensure you're referring to correct properties when passing them into an array to DataTables. The data structure used by DataTables to represent a row in the table, is an array of mixed types so for simplicity it was defined as **any[]**. Using the **any** type in this context is not bad, because the data returned inside the **dataSrc** property is used internally by DataTables. - -As you're updating the **render** method, you have also added two more changes. First, you removed the **id** attribute from the table. This allows you to place multiple instances of the same web part on the page. Also, you removed the reference to the `$(document).ready()` function which isn't necessary as the DOM of the element, where the data table is rendered, is set before the DataTables initiation code. - -### Update the Moment.js DataTables plugin to TypeScript - -The last piece of the solution that needs to be transformed to TypeScript is the Moment.js DataTables plugin. First, rename the **./src/webparts/itRequests/moment-plugin.js** file to **./src/webparts/itRequests/moment-plugin.ts** so that it will be processed by the TypeScript compiler. Next, open the **moment-plugin.ts** file in the code editor, and replace its contents with: - -```ts -import * as $ from 'jquery'; -import * as moment from 'moment'; - -/* tslint:disable:no-function-expression */ -$.fn.dataTable.render.moment = function (from: string, to: string, locale: string): (d: any, type: string, row: any) => string { -/* tslint:enable */ - // Argument shifting - if (arguments.length === 1) { - locale = 'en'; - to = from; - from = 'YYYY-MM-DD'; - } - else if (arguments.length === 2) { - locale = 'en'; - } - - return (d: any, type: string, row: any): string => { - let m: moment.Moment = moment(d, from, locale, true); - - // Order and type get a number value from Moment, everything else - // sees the rendered value - return m.format(type === 'sort' || type === 'type' ? 'x' : to); - }; -}; -``` - -You start with loading references to jQuery and Moment.js to let TypeScript know what the corresponding variables refer to. Next, you define the plugin function. Usually in TypeScript you use the arrow notation for functions (`=>`). In this case however, because you need the access to the **arguments** property, you have to use the regular function definition. To prevent tslint from reporting warning about not using the arrow notation, you can explicitly disable the **no-function-expression** rule around the function definition. - -To confirm that everything is working as expected, in the command line execute: - -```sh -gulp serve --nobrowser -``` - -Navigate to the hosted workbench and add the web part to the canvas. Although visually nothing has changed, the new code base uses TypeScript and its type definitions to help you maintain the solution. +--- +title: Migrate jQuery and DataTables solution built using Script Editor web part to SharePoint Framework +description: Migrate a SharePoint customization using DataTables to build powerful data overviews of data coming from SharePoint and external APIs. +ms.date: 08/19/2020 +ms.localizationpriority: high +--- + +# Migrate jQuery and DataTables solution built using Script Editor web part to SharePoint Framework + +One of the frequently used jQuery plug-ins is [DataTables](https://datatables.net/). With DataTables, you can easily build powerful data overviews of data coming from both SharePoint and external APIs. + +## List of IT requests built using the Script Editor web part + +To illustrate the process of migrating a SharePoint customization using DataTables to the SharePoint Framework, use the following solution that shows an overview of IT support requests retrieved from a SharePoint list. + +![Overview of IT support requests displayed on a SharePoint page](../../../images/datatables-sewp.png) + +The solution is built by using the standard SharePoint Script Editor web part. Following is the code used by the customization. + +```html + + + + + + + + + + + + + + + +
    IDBusiness unitCategoryStatusDue dateAssigned to
    + + +``` + +First, the customization loads the libraries it uses: jQuery, DataTables, and Moment.js. + +Next, it specifies the structure of the table used to present the data. + +After creating the table, it wraps Moment.js into a DataTables plug-in so that dates displayed in the table can be formatted. + +Finally, the customization uses DataTables to load and present the list of IT support requests. The data is loaded by using AJAX from a SharePoint list. + +Thanks to using DataTables, end users get a powerful solution where they can easily filter, sort, and page through the results without any additional development effort. + +![The list of IT support requests displayed using DataTables filtered by requests assigned to Lidia sorted descending by the due date](../../../images/datatables-sewp-filter.png) + +## Migrate the IT requests overview solution from the Script Editor web part to the SharePoint Framework + +Transforming this customization to the SharePoint Framework offers a number of benefits such as more user-friendly configuration and centralized management of the solution. Following is a step-by-step description of how you would migrate the solution to the SharePoint Framework. First, you'll migrate the solution to the SharePoint Framework with as few changes to the original code as possible. Later, you'll transform the solution's code to TypeScript to benefit from its development-time type safety features. + +> [!NOTE] +> The source code of the project in the different stages of migration is available at [Tutorial: Migrate jQuery and DataTables solution built using Script Editor web part to SharePoint Framework](https://github.com/SharePoint/sp-dev-fx-webparts/tree/master/tutorials/tutorial-migrate-datatables). + +### Create new SharePoint Framework project + +1. Start by creating a new folder for your project: + + ```console + md datatables-itrequests + ``` + +1. Navigate to the project folder: + + ```console + cd datatables-itrequests + ``` + +1. In the project folder, run the SharePoint Framework Yeoman generator to scaffold a new SharePoint Framework project: + + ```console + yo @microsoft/sharepoint + ``` + +1. When prompted, enter the following values (*select the default option for all prompts omitted below*): + + - **What is your solution name?**: datatables-itrequests + - **Which type of client-side component to create?**: Web Part + - **What is your Web part name?**: IT requests + - **What is your Web part description?**: Shows overview of IT support requests + - **Which framework would you like to use?**: No JavaScript framework + +1. Open your project folder in your code editor. In this tutorial, you'll use Visual Studio Code. + +### Load JavaScript libraries + +Similar to the original solution built using the Script Editor web part, first you need to load the JavaScript libraries required by the solution. In SharePoint Framework this usually consists of two steps: specifying the URL from which the library should be loaded, and referencing the library in the code. + +1. Specify the URLs from which libraries should be loaded. In the code editor, open the **./config/config.json** file, and change the `externals` section to: + + ```json + { + // .. + "externals": { + "jquery": { + "path": "https://code.jquery.com/jquery-1.12.4.min.js", + "globalName": "jQuery" + }, + "datatables.net": { + "path": "https://cdn.datatables.net/1.10.15/js/jquery.dataTables.min.js", + "globalName": "jQuery", + "globalDependencies": [ + "jquery" + ] + }, + "moment": "https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.27.0/moment.min.js" + }, + // .. + } + ``` + + This serves two purposes: + + 1. When the SharePoint Framework build toolchain creates the bundle for the web part, it will ignore any `import` or `require` statements for these three packages and not include their source in the bundle. Without these, webpack (the tool used to create the bundle) would import these JavaScript libraries in the resulting SPFx component bundle. + 1. The SharePoint Framework build toolchain will add these three packages as dependencies in the component's manifest. This tells the SharePoint Framework's module loader to ensure these libraries have been loaded on the page before loading the component's bundle. + + > [!NOTE] + > For more information on referencing external libraries in SharePoint Framework projects, see [Add an external library to your SharePoint client-side web part](../basics/add-an-external-library.md). + +1. Open the **./src/webparts/itRequests/ItRequestsWebPart.ts** file, and after the last `import` statement add: + + ```typescript + import 'jquery'; + import 'datatables.net'; + import 'moment'; + ``` + +### Define data table + +As in the original solution, the next step is to define the structure of the table used to display the data. + +In the code editor, open the **./src/webparts/itRequests/ItRequestsWebPart.ts** file, and change the `render()` method to: + +```typescript +export default class ItRequestsWebPart extends BaseClientSideWebPart { + public render(): void { + this.domElement.innerHTML = ` + + + + + + + + + + + + +
    IDBusiness unitCategoryStatusDue dateAssigned to
    `; + } + // ... +} +``` + +### Register Moment.js plugin for DataTables + +The next step is to define the Moment.js plug-in for DataTables so that dates in the table can be formatted. + +1. In the **./src/webparts/itRequests** folder, create a new file named **moment-plugin.js**, and paste the following code: + + ```javascript + // UMD + ( + function (factory) { + "use strict"; + + if (typeof define === 'function' && define.amd) { + // AMD + define(['jquery'], function ($) { + return factory($, window, document); + }); + } else if (typeof exports === 'object') { + // CommonJS + module.exports = function (root, $) { + if (!root) { + root = window; + } + + if (!$) { + $ = typeof window !== 'undefined' + ? require('jquery') + : require('jquery')(root); + } + + return factory($, root, root.document); + }; + } else { + // Browser + factory(jQuery, window, document); + } + } + + (function ($, window, document) { + $.fn.dataTable.render.moment = function (from, to, locale) { + // Argument shifting + if (arguments.length === 1) { + locale = 'en'; + to = from; + from = 'YYYY-MM-DD'; + } else if (arguments.length === 2) { + locale = 'en'; + } + + return function (d, type, row) { + var moment = require('moment'); + var m = moment(d, from, locale, true); + + // Order and type get a number value from Moment, everything else + // sees the rendered value + return m.format(type === 'sort' || type === 'type' ? 'x' : to); + }; + }; + }) + ); + ``` + +1. For the web part to load the plug-in, it has to reference the newly created **moment-plugin.js** file. In the code editor, open the **./src/webparts/itRequests/ItRequestsWebPart.ts** file, and after the last `import` statement add: + + ```typescript + import './moment-plugin'; + ``` + +### Initialize DataTables and load data + +The last step is to include the code that initializes the data table and loads the data from SharePoint. + +1. In the **./src/webparts/itRequests** folder, create a new file named **script.js**, and paste the following code: + + ```javascript + $(document).ready(function () { + $('#requests').DataTable({ + 'ajax': { + 'url': "../../_api/web/lists/getbytitle('IT Requests')/items?$select=ID,BusinessUnit,Category,Status,DueDate,AssignedTo/Title&$expand=AssignedTo/Title", + 'headers': { 'Accept': 'application/json;odata=nometadata' }, + 'dataSrc': function (data) { + return data.value.map(function (item) { + return [ + item.ID, + item.BusinessUnit, + item.Category, + item.Status, + new Date(item.DueDate), + item.AssignedTo.Title + ]; + }); + } + }, + columnDefs: [{ + targets: 4, + render: $.fn.dataTable.render.moment('YYYY/MM/DD') + }] + }); + }); + ``` + +> [!NOTE] +> Make sure to use internal name (or static name) of columns in `$select` and `$expend` parameters. + +1. To reference this file in the web part, in the code editor, open the **./src/webparts/itRequests/ItRequestsWebPart.ts** file, and add `require('./script');` to the end of the `render()` method. The `render()` method should look like the following: + + ```typescript + export default class ItRequestsWebPart extends BaseClientSideWebPart { + public render(): void { + this.domElement.innerHTML = ` + + + + + + + + + + + + +
    IDBusiness unitCategoryStatusDue dateAssigned to
    `; + + require('./script'); + } + // ... + } + ``` + +1. Verify that the web part is working as expected in the command line by executing: + + ```console + gulp serve --nobrowser + ``` + +Because the web part loads its data from SharePoint, you've to test the web part by using the hosted SharePoint Framework Workbench. Navigate to **https://{your-tenant-name}.sharepoint.com/_layouts/workbench.aspx** and add the web part to the canvas. You should now see the IT requests displayed by using the DataTables jQuery plug-in. + +![IT requests displayed in a SharePoint Framework client-side web part](../../../images/datatables-spfx.png) + +## Add support for configuring the web part through web part properties + +In the previous steps, you migrated the IT requests solutions from the Script Editor web part to the SharePoint Framework. While the solution already works as expected, it doesn't use any of the SharePoint Framework benefits. The name of the list from which IT requests are loaded is included in the code, and the code itself is plain JavaScript, which is harder to refactor than TypeScript. + +The following steps illustrate how to extend the existing solution to allow users to specify the name of the list to load the data from. Later, you transform the code to TypeScript to benefit from its type safety features. + +### Define web part property for storing the name of the list + +1. Define a web part property to store the name of the list from which IT requests should be loaded. In the code editor, open the **./src/webparts/itRequests/ItRequestsWebPart.manifest.json** file, and rename the default `description` property to `listName` and clear its value. + + ![The listName property in the web part manifest highlighted in Visual Studio Code](../../../images/datatables-spfx-listname-property.png) + +1. Update the web part properties interface to reflect the changes in the manifest. In the code editor, open the **./src/webparts/itRequests/IItRequestsWebPartProps.ts** file, and change its contents to: + + ```typescript + export interface IItRequestsWebPartProps { + listName: string; + } + ``` + +1. Update the display labels for the `listName` property. Open the **./src/webparts/itRequests/loc/mystrings.d.ts** file, and change its contents to: + + ```typescript + declare interface IItRequestsStrings { + PropertyPaneDescription: string; + BasicGroupName: string; + ListNameFieldLabel: string; + } + + declare module 'itRequestsStrings' { + const strings: IItRequestsStrings; + export = strings; + } + ``` + +1. Open the **./src/webparts/itRequests/loc/en-us.js** file, and change its contents to: + + ```javascript + define([], function() { + return { + "PropertyPaneDescription": "IT Requests settings", + "BasicGroupName": "Data", + "ListNameFieldLabel": "List name" + } + }); + ``` + +1. Update the web part to use the newly defined property. In the code editor, open the **./src/webparts/itRequests/ItRequestsWebPart.ts** file, and change the `getPropertyPaneConfiguration()` method to: + + ```typescript + export default class ItRequestsWebPart extends BaseClientSideWebPart { + // ... + protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration { + return { + pages: [{ + header: { + description: strings.PropertyPaneDescription + }, + groups: [{ + groupName: strings.BasicGroupName, + groupFields: [ + PropertyPaneTextField('listName', { + label: strings.ListNameFieldLabel + }) + ] + }] + }] + }; + } + + protected get disableReactivePropertyChanges(): boolean { + return true; + } + } + ``` + +To prevent the web part from reloading as users type the name of the list, you also configured the web part to use the non-reactive property pane by adding the `disableReactivePropertyChanges()` method and setting its return value to `true`. + +### Use the configured name of the list to load the data from + +Initially, the name of the list from which the data should be loaded was embedded in the REST query. Now that users can configure this name, the configured value should be injected into the REST query before loading the data. The easiest way to do that is by moving the contents of the **script.js** file to the main web part file. + +1. In the code editor, open the **./src/webparts/itRequests/ItRequestsWebPart.ts** file, and change the `render()` method to: + + ```typescript + var $: any = (window as any).$; + + export default class ItRequestsWebPart extends BaseClientSideWebPart { + public render(): void { + this.domElement.innerHTML = ` + + + + + + + + + + + + +
    IDBusiness unitCategoryStatusDue dateAssigned to
    `; + + $(document).ready(() => { + $('table', this.domElement).DataTable({ + 'ajax': { + 'url': `../../_api/web/lists/getbytitle('${escape(this.properties.listName)}')/items?$select=ID,BusinessUnit,Category,Status,DueDate,AssignedTo/Title&$expand=AssignedTo/Title`, + 'headers': { 'Accept': 'application/json;odata=nometadata' }, + 'dataSrc': function (data) { + return data.value.map(function (item) { + return [ + item.ID, + item.BusinessUnit, + item.Category, + item.Status, + new Date(item.DueDate), + item.AssignedTo.Title + ]; + }); + } + }, + columnDefs: [{ + targets: 4, + render: $.fn.dataTable.render.moment('YYYY/MM/DD') + }] + }); + }); + } + + // ... + } + ``` + +1. Instead of referencing the code from the **script.js** file, all of its contents are a part of the web part's `render` method. In the REST query, you can now replace the fixed name of the list with the value of the `listName` property that holds the name of the list as configured by the user. Before using the value, it's being escaped by using the lodash's `escape` function to disallow script injection. + + At this point, the bulk of the code is still written using plain JavaScript. To avoid build issues with the `$` jQuery variable, you had to define it as `any` type before the class definition. Later, when transforming the code to TypeScript, you replace it with a proper type definition. + + As you've moved the contents of the **script.js** file into the main web part file, the **script.js** is no longer necessary, and you can delete it from the project. + +1. To verify that the web part is working as expected, run the following in the command line: + + ```console + gulp serve --nobrowser + ``` + +1. Navigate to the hosted Workbench and add the web part to the canvas. Open the web part property pane, specify the name of the list with IT requests, and select the **Apply** button to confirm the changes. + + You should now see IT requests displayed in the web part. + + ![IT requests loaded from the configured list and displayed in a SharePoint Framework client-side web part](../../../images/datatables-spfx-list-configured.png) + +## Transform the plain JavaScript code to TypeScript + +Using TypeScript over plain JavaScript offers a number of benefits. Not only is TypeScript easier to maintain and refactor, but it also allows you to catch errors earlier. The following steps describe how you would transform the original JavaScript code to TypeScript. + +### Add type definitions for used libraries + +To function properly, TypeScript requires type definitions for the different libraries used in the project. Type definitions are often distributed as npm packages in the @types namespace. + +1. Install type definitions for jQuery and DataTables by executing the following in the command line: + + ```console + npm install @types/jquery@1.10.34 @types/datatables.net@1.10.15 --save-dev --save-exact + ``` + + > [!TIP] + > In this example, we are specifying the exact version of the NPM package we want to install. This will ensure that NPM installs a type declaration package that matches the version of jQuery and the datatables library we are using in our project. + > + > The `--save-dev` argument tells NPM to save the references to these two packages in the `devDependencies` collection in the **package.json** file. TypeScript declarations are only needed in development, which is why we don't want them in the `dependencies` collection. + > + > The `--save-exact` argument tells NPM to add references to the specific version in the **package.json** file and not add the notation to enable automatic upgrades to a more recent version. + + Type definitions for Moment.js are distributed together with the Moment.js package. Even though you're loading Moment.js from a URL, to use its typings, you still need to install the Moment.js package in the project. + +1. Install the Moment.js package by executing the following in the command line: + + ```console + npm install moment@2.27.0 --save-exact + ``` + +### Update package references + +To use types from the installed type definitions, you've to change how you reference libraries. + +1. In the code editor, open the **./src/webparts/itRequests/ItRequestsWebPart.ts** file, and change the `import 'jquery';` statement to: + + ```typescript + import * as $ from 'jquery'; + ``` + +1. Having defined `$` as jQuery, you can now remove the local definition of `$` that you added previously: + + ```typescript + var $: any = (window as any).$; + ``` + +### Update main web part files to TypeScript + +Now that you've type definitions for all libraries installed in the project, you can start transforming the plain JavaScript code to TypeScript. + +1. Define an interface for the IT request information that you retrieve from the SharePoint list. In the code editor, open the **./src/webparts/itRequests/ItRequestsWebPart.ts** file, and just above the web part class, add the following code snippet: + + ```typescript + interface IRequestItem { + ID: number; + BusinessUnit: string; + Category: string; + Status: string; + DueDate: string; + AssignedTo: { Title: string; }; + } + ``` + +1. Next, in the web part class, change the `render()` method to: + + ```typescript + export default class ItRequestsWebPart extends BaseClientSideWebPart { + public render(): void { + this.domElement.innerHTML = ` + + + + + + + + + + + + +
    IDBusiness unitCategoryStatusDue dateAssigned to
    `; + + $('table', this.domElement).DataTable({ + 'ajax': { + 'url': `../../_api/web/lists/getbytitle('${escape(this.properties.listName)}')/items?$select=ID,BusinessUnit,Category,Status,DueDate,AssignedTo/Title&$expand=AssignedTo/Title`, + 'headers': { 'Accept': 'application/json;odata=nometadata' }, + 'dataSrc': (data: { value: IRequestItem[] }): any[][] => { + return data.value.map((item: IRequestItem): any[] => { + return [ + item.ID, + item.BusinessUnit, + item.Category, + item.Status, + new Date(item.DueDate), + item.AssignedTo.Title + ]; + }); + } + }, + columnDefs: [{ + targets: 4, + render: ($.fn.dataTable.render as any).moment('YYYY/MM/DD') + }] + }); + } + + // ... + } + ``` + +1. Notice how the AJAX request, to retrieve the data from the SharePoint list, is now typed and helps you ensure you're referring to correct properties when passing them into an array to DataTables. The data structure used by DataTables to represent a row in the table is an array of mixed types, so for simplicity it was defined as `any[]`. Using the `any` type in this context isn't bad, because the data returned inside the `dataSrc` property is used internally by DataTables. + + As you're updating the `render()` method, you've also added two more changes. First, you removed the `id` attribute from the table. This allows you to place multiple instances of the same web part on the page. Also, you removed the reference to the `$(document).ready()` function, which isn't necessary because the DOM of the element where the data table is rendered is set before the DataTables initiation code. + +### Update the Moment.js DataTables plugin to TypeScript + +The last piece of the solution that needs to be transformed to TypeScript is the Moment.js DataTables plug-in. + +1. Rename the **./src/webparts/itRequests/moment-plugin.js** file to **./src/webparts/itRequests/moment-plugin.ts** so that it's processed by the TypeScript compiler. +1. Open the **moment-plugin.ts** file in the code editor, and replace its contents with: + + ```typescript + import * as $ from 'jquery'; + import * as moment from 'moment'; + + /* tslint:disable:no-function-expression */ + ($.fn.dataTable.render as any).moment = function (from: string, to: string, locale: string): (d: any, type: string, row: any) => string { + /* tslint:enable */ + // Argument shifting + if (arguments.length === 1) { + locale = 'en'; + to = from; + from = 'YYYY-MM-DD'; + } else if (arguments.length === 2) { + locale = 'en'; + } + + return (d: any, type: string, row: any): string => { + let m: moment.Moment = moment(d, from, locale, true); + + // Order and type get a number value from Moment, everything else + // sees the rendered value + return m.format(type === 'sort' || type === 'type' ? 'x' : to); + }; + }; + ``` + +1. You start with loading references to jQuery and Moment.js to let TypeScript know what the corresponding variables refer to. Next, you define the plug-in function. Usually in TypeScript you use the arrow notation for functions (`=>`). In this case, however, because you need access to the `arguments` property, you've to use the regular function definition. To prevent tslint from reporting a warning about not using the arrow notation, you can explicitly disable the `no-function-expression` rule around the function definition. +1. To confirm that everything is working as expected, in the command line, execute: + + ```console + gulp serve --nobrowser + ``` + +1. Navigate to the hosted Workbench and add the web part to the canvas. Although visually nothing has changed, the new code base uses TypeScript and its type definitions to help you maintain the solution. diff --git a/docs/spfx/web-parts/guidance/migrate-jquery-fullcalendar-script-to-spfx.md b/docs/spfx/web-parts/guidance/migrate-jquery-fullcalendar-script-to-spfx.md index 8412a103c..d18d98525 100644 --- a/docs/spfx/web-parts/guidance/migrate-jquery-fullcalendar-script-to-spfx.md +++ b/docs/spfx/web-parts/guidance/migrate-jquery-fullcalendar-script-to-spfx.md @@ -1,1064 +1,1099 @@ ---- -title: Migrate jQuery and FullCalendar solution built using Script Editor Web Part to SharePoint Framework -ms.date: 09/25/2017 -ms.prod: sharepoint ---- - - -# Migrate jQuery and FullCalendar solution built using Script Editor Web Part to SharePoint Framework - -When building SharePoint solutions, SharePoint developers often use the [FullCalendar](https://fullcalendar.io) jQuery plugin to display data in calendar view. FullCalendar is a great alternative to the standard SharePoint calendar view, as it allows you to render as calendar data from multiple calendar lists, data from non-calendar lists or even data from outside of SharePoint. This article illustrates how you would migrate a SharePoint customization using FullCalendar built with the Script Editor Web Part to the SharePoint Framework. - -## List of tasks displayed as a calendar built using the Script Editor Web Part - -To illustrate the process of migrating a SharePoint customization using FullCalendar to the SharePoint Framework you will use the following solution that shows a calendar view of tasks retrieved from a SharePoint list. - -![Calendar view of tasks displayed on a SharePoint page](../../../images/fullcalendar-sewp.png) - -The solution is built using the standard SharePoint Script Editor Web Part. Following is the code used by the customization. - -```html - - - - -
    - - -``` - -> This solution is based on the work of Mark Rackley, Office Servers and Services MVP and Chief Strategy Officer at PAIT Group. For more information about the original solution visit [http://www.markrackley.net/2017/06/07/using-fullcalendar-io-to-create-custom-calendars-in-sharepoint/](http://www.markrackley.net/2017/06/07/using-fullcalendar-io-to-create-custom-calendars-in-sharepoint/). - -First, the customization loads the libraries it uses: jQuery, Moment.js and FullCalendar (lines 1-4). Next, it defines the div into which the generated calendar view will be injected (line 5). Then, it defines two functions: **displayTasks** - used to display tasks in the calendar view, and **updateTask** which is triggered after dragging and dropping a task to a different date and which updates the dates on the underlying list item. Each function defines its own REST query which is then used to communicate with the SharePoint List REST API to retrieve or update list items. - -Using the FullCalendar jQuery plugin, with little effort users get rich solutions capable of things such as using different colors to mark different events or using drag and drop to reorganize events. - -![Dragging events in FullCalendar to reschedule underlying tasks](../../../images/fullcalendar-sewp-draganddrop.png) - -## Migrate the Tasks calendar solution from the Script Editor Web Part to the SharePoint Framework - -> **Note:** Before following the steps in this article, be sure to [set up your development environment](../../set-up-your-development-environment.md) for building SharePoint Framework solutions. - -Transforming a Script Editor Web Part-based customization to the SharePoint Framework offers a number of benefits such as more user-friendly configuration and centralized management of the solution. Following is a step-by-step description of how you would migrate the solution to the SharePoint Framework. First, you will migrate the solution to the SharePoint Framework with as few changes to the original code as possible. Later, you will transform the solution's code to TypeScript to benefit of its development-time type safety features and replace some of the code with the SharePoint Framework API to fully benefit of its capabilities and simplify the solution even further. - -> The source code of the project in the different stages of migration is available at [https://github.com/SharePoint/sp-dev-fx-webparts/tree/master/tutorials/tutorial-migrate-fullcalendar](https://github.com/SharePoint/sp-dev-fx-webparts/tree/master/tutorials/tutorial-migrate-fullcalendar). - -### Create new SharePoint Framework project - -Start by creating a new folder for your project - -```sh -md fullcalendar-taskscalendar -``` - -Navigate to the project folder: - -```sh -cd fullcalendar-taskscalendar -``` - -In the project folder run the SharePoint Framework Yeoman generator to scaffold a new SharePoint Framework project: - -```sh -yo @microsoft/sharepoint -``` - -When prompted, define values as follows: -- **fullcalendar-taskscalendar** as your solution name -- **Use the current folder** for the location to place the files -- **WebPart** as the client-side component to create -- **Tasks calendar** as your web part name -- **Shows tasks in the calendar view** as your web part description -- **No javaScript web framework** as the starting point to build the web part - -![SharePoint Framework Yeoman generator with the default choices](../../../images/fullcalendar-yeoman.png) - -Once the scaffolding completes, open your project folder in your code editor. In this tutorial, you will use Visual Studio Code. - -![SharePoint Framework project open in Visual Studio Code](../../../images/fullcalendar-vscode.png) - -### Load JavaScript libraries - -Similarly to the original solution built using the Script Editor Web Part, first you need to load the JavaScript libraries required by the solution. In SharePoint Framework this usually consists of two steps: specifying the URL from which the library should be loaded, and referencing the library in the code. - -Start, with specifying the URLs from which libraries should be loaded. In the code editor, open the **./config/config.json** file and change the **externals** section to: - -```json -{ - "externals": { - "jquery": "https://code.jquery.com/jquery-1.11.1.min.js", - "moment": "https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.10.6/moment.min.js", - "fullcalendar": "https://cdnjs.cloudflare.com/ajax/libs/fullcalendar/3.4.0/fullcalendar.min.js" - } -} -``` - -Next, open the **./src/webparts/tasksCalendar/TasksCalendarWebPart.ts** file, and after the last **import** statement add: - -```ts -import 'jquery'; -import 'moment'; -import 'fullcalendar'; -``` - -### Define container div - -Just as in the original solution, the next step is to define the location where the calendar should be rendered. In the code editor, open the **./src/webparts/tasksCalendar/TasksCalendarWebPart.ts** file and change the **render** method to: - -```ts -export default class ItRequestsWebPart extends BaseClientSideWebPart { - public render(): void { - this.domElement.innerHTML = ` -
    - -
    -
    `; - } - // ... -} -``` - -### Initiate FullCalendar and load data - -The last step is to include the code that initiates the FullCalendar jQuery plugin and loads the data from SharePoint. In the **./src/webparts/tasksCalendar** folder, create a new file named **script.js** and paste the following code: - -```js -var moment = require('moment'); - -var PATH_TO_DISPFORM = window.webAbsoluteUrl + "/Lists/Tasks/DispForm.aspx"; -var TASK_LIST = "Tasks"; -var COLORS = ['#466365', '#B49A67', '#93B7BE', '#E07A5F', '#849483', '#084C61', '#DB3A34']; - -displayTasks(); - -function displayTasks() { - $('#calendar').fullCalendar('destroy'); - $('#calendar').fullCalendar({ - weekends: false, - header: { - left: 'prev,next today', - center: 'title', - right: 'month,basicWeek,basicDay' - }, - displayEventTime: false, - // open up the display form when a user clicks on an event - eventClick: function (calEvent, jsEvent, view) { - window.location = PATH_TO_DISPFORM + "?ID=" + calEvent.id; - }, - editable: true, - timezone: "UTC", - droppable: true, // this allows things to be dropped onto the calendar - // update the end date when a user drags and drops an event - eventDrop: function (event, delta, revertFunc) { - updateTask(event.id, event.start, event.end); - }, - // put the events on the calendar - events: function (start, end, timezone, callback) { - var startDate = start.format('YYYY-MM-DD'); - var endDate = end.format('YYYY-MM-DD'); - - var restQuery = "/_api/Web/Lists/GetByTitle('" + TASK_LIST + "')/items?$select=ID,Title,\ -Status,StartDate,DueDate,AssignedTo/Title&$expand=AssignedTo&\ -$filter=((DueDate ge '" + startDate + "' and DueDate le '" + endDate + "')or(StartDate ge '" + startDate + "' and StartDate le '" + endDate + "'))"; - - $.ajax({ - url: window.webAbsoluteUrl + restQuery, - type: "GET", - dataType: "json", - headers: { - Accept: "application/json;odata=nometadata" - } - }) - .done(function (data, textStatus, jqXHR) { - var personColors = {}; - var colorNo = 0; - - var events = data.value.map(function (task) { - var assignedTo = task.AssignedTo.map(function (person) { - return person.Title; - }).join(', '); - - var color = personColors[assignedTo]; - if (!color) { - color = COLORS[colorNo++]; - personColors[assignedTo] = color; - } - if (colorNo >= COLORS.length) { - colorNo = 0; - } - - return { - title: task.Title + " - " + assignedTo, - id: task.ID, - color: color, // specify the background color and border color can also create a class and use className parameter. - start: moment.utc(task.StartDate).add("1", "days"), - end: moment.utc(task.DueDate).add("1", "days") // add one day to end date so that calendar properly shows event ending on that day - }; - }); - - callback(events); - }); - } - }); -} - -function updateTask(id, startDate, dueDate) { - // subtract the previously added day to the date to store correct date - var sDate = moment.utc(startDate).add("-1", "days").format('YYYY-MM-DD') + "T" + - startDate.format("hh:mm") + ":00Z"; - if (!dueDate) { - dueDate = startDate; - } - var dDate = moment.utc(dueDate).add("-1", "days").format('YYYY-MM-DD') + "T" + - dueDate.format("hh:mm") + ":00Z"; - - $.ajax({ - url: window.webAbsoluteUrl + '/_api/contextinfo', - type: 'POST', - headers: { - 'Accept': 'application/json;odata=nometadata' - } - }) - .then(function (data, textStatus, jqXHR) { - return $.ajax({ - url: window.webAbsoluteUrl + - "/_api/Web/Lists/getByTitle('" + TASK_LIST + "')/Items(" + id + ")", - type: 'POST', - data: JSON.stringify({ - StartDate: sDate, - DueDate: dDate, - }), - headers: { - Accept: "application/json;odata=nometadata", - "Content-Type": "application/json;odata=nometadata", - "X-RequestDigest": data.FormDigestValue, - "IF-MATCH": "*", - "X-Http-Method": "PATCH" - } - }); - }) - .done(function (data, textStatus, jqXHR) { - alert("Update Successful"); - }) - .fail(function (jqXHR, textStatus, errorThrown) { - alert("Update Failed"); - }) - .always(function () { - displayTasks(); - }); -} -``` - -This code is almost identical with the original code of the Script Editor Web Part customization. The only difference is that where the original code retrieved the URL of the current web from the global **\_spPageContextInfo** variable set by SharePoint (lines 8, 45, 96 and 104), the code in the SharePoint Framework uses a custom variable that you will have to set in the web part. SharePoint Framework client-side web parts can be used both on classic and modern pages. While the **_spPageContextInfo** variable is present on classic pages, it's not available on modern pages which is why you can't rely on it and need a custom property that you can control yourself instead. - -In order to reference this file in the web part, in the code editor, open the **./src/webparts/tasksCalendar/TasksCalendarWebPart.ts** file and change the **render** method to: - -```ts -export default class ItRequestsWebPart extends BaseClientSideWebPart { - public render(): void { - this.domElement.innerHTML = ` -
    - -
    -
    `; - - (window as any).webAbsoluteUrl = this.context.pageContext.web.absoluteUrl; - require('./script'); - } - // ... -} -``` - -Verify, that the web part is working as expected by in the command line executing: - -```sh -gulp serve --nobrowser -``` - -Because the web part loads its data from SharePoint, you have to test the web part using the hosted SharePoint Framework workbench. Navigate to **https://yourtenant.sharepoint.com/_layouts/workbench.aspx** and add the web part to the canvas. You should now see the tasks displayed in a calendar view using the FullCalendar jQuery plugin. - -![Tasks displayed in a calendar view in a SharePoint Framework client-side web part](../../../images/fullcalendar-spfx.png) - -## Add support for configuring the web part through web part properties - -In the previous steps you migrated the Tasks calendar solutions from Script Editor Web Part to the SharePoint Framework. While the solution already works as expected, it doesn't use any of the SharePoint Framework benefits. The name of the list from which tasks are loaded is included in the code and the code itself is plain JavaScript which is harder to refactor than TypeScript. The following steps illustrate how to extend the existing solution to allow users to specify the name of the list to load the data from. Later, you will transform the code to TypeScript to benefit of its type safety features. - -### Define web part property for storing the name of the list - -Start with defining a web part property to store the name of the list from which tasks should be loaded. In the code editor, open the **./src/webparts/tasksCalendar/TasksCalendarWebPart.manifest.json** file and rename the default **description** property to **listName** and clear its value. - -![The listName property in the web part manifest highlighted in Visual Studio Code](../../../images/fullcalendar-spfx-listname-property.png) - -Next, update the web part properties interface to reflect the changes in the manifest. In the code editor, open the **./src/webparts/tasksCalendar/ITasksCalendarWebPartProps.ts** file and change its contents to: - -```ts -export interface ITasksCalendarWebPartProps { - listName: string; -} -``` - -Then, update the display labels for the **listName** property. Open the **./src/webparts/tasksCalendar/loc/mystrings.d.ts** file and change its contents to: - -```ts -declare interface ITasksCalendarStrings { - PropertyPaneDescription: string; - BasicGroupName: string; - ListNameFieldLabel: string; -} - -declare module 'tasksCalendarStrings' { - const strings: ITasksCalendarStrings; - export = strings; -} -``` - -Next, open the **./src/webparts/tasksCalendar/loc/en-us.js** file and change its contents to: - -```js -define([], function() { - return { - "PropertyPaneDescription": "Tasks calendar settings", - "BasicGroupName": "Data", - "ListNameFieldLabel": "List name" - } -}); -``` - -Finally, update the web part to use the newly defined property. In the code editor, open the **./src/webparts/tasksCalendar/TasksCalendarWebPart.ts** file and change the **getPropertyPaneConfiguration** method to: - -```ts -export default class TasksCalendarWebPart extends BaseClientSideWebPart { - // ... - protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration { - return { - pages: [ - { - header: { - description: strings.PropertyPaneDescription - }, - groups: [ - { - groupName: strings.BasicGroupName, - groupFields: [ - PropertyPaneTextField('listName', { - label: strings.ListNameFieldLabel - }) - ] - } - ] - } - ] - }; - } - - protected get disableReactivePropertyChanges(): boolean { - return true; - } -} -``` - -To prevent the web part from reloading as users type the name of the list, you've also configured the web part to use the non-reactive property pane by adding the **disableReactivePropertyChanges** method and settings its return value to **true**. - -### Use the configured name of the list to load the data from - -Initially, the name of the list from which the data should be loaded was embedded in the REST queries. Now that users can configure this name, the configured value should be injected into the REST queries before executing them. The easiest way to do that, is by moving the contents of the **script.js** file to the main web part file. - -In the code editor, open the **./src/webparts/tasksCalendar/TasksCalendarWebPart.ts** file. - -Start, by changing the import statement to load the required libraries to: - -```ts -var $: any = require('jquery'); -var moment: any = require('moment'); - -import 'fullcalendar'; - -var COLORS = ['#466365', '#B49A67', '#93B7BE', '#E07A5F', '#849483', '#084C61', '#DB3A34']; -``` - -Because Moment.js is referenced in the code that you will be using later, its name must be known to TypeScript or building the project will fail. The same applies to jQuery. Because FullCalendar is a jQuery plugin that attaches itself to the jQuery object, it can be imported the same way as previously. -The last part, includes copying the list of colors to use for marking the different events. - -Next, copy the **displayTasks** and **updateTask** functions from the **script.js** file and paste them as follows inside the **TasksCalendarWebPart** class: - -```ts -export default class TasksCalendarWebPart extends BaseClientSideWebPart { - // ... - - private displayTasks() { - $('#calendar').fullCalendar('destroy'); - $('#calendar').fullCalendar({ - weekends: false, - header: { - left: 'prev,next today', - center: 'title', - right: 'month,basicWeek,basicDay' - }, - displayEventTime: false, - // open up the display form when a user clicks on an event - eventClick: (calEvent, jsEvent, view) => { - (window as any).location = this.context.pageContext.web.absoluteUrl + - "/Lists/" + escape(this.properties.listName) + "/DispForm.aspx?ID=" + calEvent.id; - }, - editable: true, - timezone: "UTC", - droppable: true, // this allows things to be dropped onto the calendar - // update the end date when a user drags and drops an event - eventDrop: (event, delta, revertFunc) => { - this.updateTask(event.id, event.start, event.end); - }, - // put the events on the calendar - events: (start, end, timezone, callback) => { - var startDate = start.format('YYYY-MM-DD'); - var endDate = end.format('YYYY-MM-DD'); - - var restQuery = "/_api/Web/Lists/GetByTitle('" + escape(this.properties.listName) + "')/items?$select=ID,Title,\ -Status,StartDate,DueDate,AssignedTo/Title&$expand=AssignedTo&\ -$filter=((DueDate ge '" + startDate + "' and DueDate le '" + endDate + "')or(StartDate ge '" + startDate + "' and StartDate le '" + endDate + "'))"; - - $.ajax({ - url: this.context.pageContext.web.absoluteUrl + restQuery, - type: "GET", - dataType: "json", - headers: { - Accept: "application/json;odata=nometadata" - } - }) - .done((data, textStatus, jqXHR) => { - var personColors = {}; - var colorNo = 0; - - var events = data.value.map((task) => { - var assignedTo = task.AssignedTo.map((person) => { - return person.Title; - }).join(', '); - - var color = personColors[assignedTo]; - if (!color) { - color = COLORS[colorNo++]; - personColors[assignedTo] = color; - } - if (colorNo >= COLORS.length) { - colorNo = 0; - } - - return { - title: task.Title + " - " + assignedTo, - id: task.ID, - color: color, // specify the background color and border color can also create a class and use className parameter. - start: moment.utc(task.StartDate).add("1", "days"), - end: moment.utc(task.DueDate).add("1", "days") // add one day to end date so that calendar properly shows event ending on that day - }; - }); - - callback(events); - }); - } - }); - } - - private updateTask(id, startDate, dueDate) { - // subtract the previously added day to the date to store correct date - var sDate = moment.utc(startDate).add("-1", "days").format('YYYY-MM-DD') + "T" + - startDate.format("hh:mm") + ":00Z"; - if (!dueDate) { - dueDate = startDate; - } - var dDate = moment.utc(dueDate).add("-1", "days").format('YYYY-MM-DD') + "T" + - dueDate.format("hh:mm") + ":00Z"; - - $.ajax({ - url: this.context.pageContext.web.absoluteUrl + '/_api/contextinfo', - type: 'POST', - headers: { - 'Accept': 'application/json;odata=nometadata' - } - }) - .then((data, textStatus, jqXHR) => { - return $.ajax({ - url: this.context.pageContext.web.absoluteUrl + - "/_api/Web/Lists/getByTitle('" + escape(this.properties.listName) + "')/Items(" + id + ")", - type: 'POST', - data: JSON.stringify({ - StartDate: sDate, - DueDate: dDate, - }), - headers: { - Accept: "application/json;odata=nometadata", - "Content-Type": "application/json;odata=nometadata", - "X-RequestDigest": data.FormDigestValue, - "IF-MATCH": "*", - "X-Http-Method": "PATCH" - } - }); - }) - .done((data, textStatus, jqXHR) => { - alert("Update Successful"); - }) - .fail((jqXHR, textStatus, errorThrown) => { - alert("Update Failed"); - }) - .always(() => { - this.displayTasks(); - }); - } - - // ... -} -``` - -There are a few changes in the code comparing to the previous situation. Plain JavaScript functions are now changed into TypeScript methods by replacing the **function** keyword with the **private** modifier. This is required to be able to add them to the **TaskCalendarWebPart** class. Because both methods are now in the same file as the web part, instead of defining a global variable to hold the URL of the current site, you can access it directly from the web part context using the `this.context.pageContext.web.absoluteUrl` property. Additionally, in all REST queries, the fixed list name is replaced with the value of the **listName** property which holds the name of the list as configured by the user. Before using the value, it's being escaped using the lodash's **escape** function to disallow script injection. - -As the last step, change the **render** method to call the newly added **displayTasks** method: - -```ts -export default class TasksCalendarWebPart extends BaseClientSideWebPart { - public render(): void { - this.domElement.innerHTML = ` -
    - -
    -
    `; - - this.displayTasks(); - } - // ... -} -``` - -As you have just moved the contents of the **script.js** file into the main web part file, the **script.js** is no longer necessary and you can delete it from the project. - -To verify that the web part is working as expected, run in the command line: - -```sh -gulp serve --nobrowser -``` - -Navigate to the hosted workbench and add the web part to the canvas. Open the web part property pane, specify the name of the list with tasks and click the **Apply** button to confirm the changes. You should now see tasks displayed in a calendar view in the web part. - -![Tasks loaded from the configured list and displayed in a SharePoint Framework client-side web part](../../../images/fullcalendar-spfx-list-configured.png) - -## Transform the plain JavaScript code to TypeScript - -Using TypeScript over plain JavaScript offers a number of benefits. Not only is TypeScript easier to maintain and refactor but it also allows you to catch errors earlier. Following steps describe how you would transform the original JavaScript code to TypeScript. - -### Add type definitions for used libraries - -To function properly, TypeScript requires type definitions for the different libraries used in the project. Type definitions are often distributed as npm packages in the @types namespace. - -Start by installing type definitions for jQuery and FullCalendar by executing in the command line: - -```sh -npm install --save-dev @types/jquery@1 @types/fullcalendar -``` - -Type definitions for Moment.js are distributed together with the Moment.js package. Even though, you're loading Moment.js from a URL, in order to use its typings, you still need to install the Moment.js package in the project. - -Install the Moment.js package by executing in the command line: - -```sh -npm install --save moment -``` - -### Update package references - -In order to use types from the installed type definitions, you have to change how you reference libraries. In the code editor, open the **./src/webparts/tasksCalendar/TasksCalendarWebPart.ts** file and change the import statements to: - -```ts -import * as $ from 'jquery'; -import 'fullcalendar'; -import * as moment from 'moment'; -``` - -### Update main web part files to TypeScript - -Now that you have type definitions for all libraries installed in the project, you can start transforming the plain JavaScript code to TypeScript. - -Start, with defining an interface for a task that you retrieve from the SharePoint list. In the code editor, open the **./src/webparts/tasksCalendar/TasksCalendarWebPart.ts** file and just above the web part class, add the following code snippet: - -```ts -interface ITask { - ID: number; - Title: string; - StartDate: string; - DueDate: string; - AssignedTo: [{ Title: string }]; -} -``` - -Next, in the web part class, change the **displayTasks** and **updateTask** methods to: - -```ts -export default class TasksCalendarWebPart extends BaseClientSideWebPart { - private readonly colors: string[] = ['#466365', '#B49A67', '#93B7BE', '#E07A5F', '#849483', '#084C61', '#DB3A34']; - - // ... - - private displayTasks(): void { - $('#calendar').fullCalendar('destroy'); - $('#calendar').fullCalendar({ - weekends: false, - header: { - left: 'prev,next today', - center: 'title', - right: 'month,basicWeek,basicDay' - }, - displayEventTime: false, - // open up the display form when a user clicks on an event - eventClick: (calEvent: FC.EventObject, jsEvent: MouseEvent, view: FC.ViewObject): void => { - (window as any).location = `${this.context.pageContext.web.absoluteUrl}\ -/Lists/${escape(this.properties.listName)}/DispForm.aspx?ID=${calEvent.id}`; - }, - editable: true, - timezone: "UTC", - droppable: true, // this allows things to be dropped onto the calendar - // update the end date when a user drags and drops an event - eventDrop: (event: FC.EventObject, delta: moment.Duration, revertFunc: Function): void => { - this.updateTask(event.id, event.start, event.end); - }, - // put the events on the calendar - events: (start: moment.Moment, end: moment.Moment, timezone: string, callback: Function): void => { - const startDate: string = start.format('YYYY-MM-DD'); - const endDate: string = end.format('YYYY-MM-DD'); - - const restQuery: string = `/_api/Web/Lists/GetByTitle('${escape(this.properties.listName)}')/items?$select=ID,Title,\ -Status,StartDate,DueDate,AssignedTo/Title&$expand=AssignedTo&\ -$filter=((DueDate ge '${startDate}' and DueDate le '${endDate}')or(StartDate ge '${startDate}' and StartDate le '${endDate}'))`; - - $.ajax({ - url: this.context.pageContext.web.absoluteUrl + restQuery, - type: "GET", - dataType: "json", - headers: { - Accept: "application/json;odata=nometadata" - } - }) - .done((data: { value: ITask[] }, textStatus: string, jqXHR: JQueryXHR): void => { - let personColors: { [person: string]: string; } = {}; - let colorNo: number = 0; - - const events: FC.EventObject[] = data.value.map((task: ITask): FC.EventObject => { - const assignedTo: string = task.AssignedTo.map((person: { Title: string }): string => { - return person.Title; - }).join(', '); - - let color: string = personColors[assignedTo]; - if (!color) { - color = this.colors[colorNo++]; - personColors[assignedTo] = color; - } - if (colorNo >= this.colors.length) { - colorNo = 0; - } - - return { - title: `${task.Title} - ${assignedTo}`, - id: task.ID, - // specify the background color and border color can also create a class and use className parameter - color: color, - start: moment.utc(task.StartDate).add("1", "days"), - // add one day to end date so that calendar properly shows event ending on that day - end: moment.utc(task.DueDate).add("1", "days") - }; - }); - - callback(events); - }); - } - }); - } - - private updateTask(id: number, startDate: moment.Moment, dueDate: moment.Moment): void { - // subtract the previously added day to the date to store correct date - const sDate: string = moment.utc(startDate).add("-1", "days").format('YYYY-MM-DD') + "T" + - startDate.format("hh:mm") + ":00Z"; - if (!dueDate) { - dueDate = startDate; - } - const dDate: string = moment.utc(dueDate).add("-1", "days").format('YYYY-MM-DD') + "T" + - dueDate.format("hh:mm") + ":00Z"; - - $.ajax({ - url: this.context.pageContext.web.absoluteUrl + '/_api/contextinfo', - type: 'POST', - headers: { - 'Accept': 'application/json;odata=nometadata' - } - }) - .then((data: { FormDigestValue: string }, textStatus: string, jqXHR: JQueryXHR): JQueryXHR => { - return $.ajax({ - url: `${this.context.pageContext.web.absoluteUrl}\ -/_api/Web/Lists/getByTitle('${escape(this.properties.listName)}')/Items(${id})`, - type: 'POST', - data: JSON.stringify({ - StartDate: sDate, - DueDate: dDate, - }), - headers: { - Accept: "application/json;odata=nometadata", - "Content-Type": "application/json;odata=nometadata", - "X-RequestDigest": data.FormDigestValue, - "IF-MATCH": "*", - "X-Http-Method": "PATCH" - } - }); - }) - .done((data: {}, textStatus: string, jqXHR: JQueryXHR): void => { - alert("Update Successful"); - }) - .fail((jqXHR: JQueryXHR, textStatus: string, errorThrown: string): void => { - alert("Update Failed"); - }) - .always((): void => { - this.displayTasks(); - }); - } - - // ... -} -``` - -First, and the most obvious, change when transforming plain JavaScript to TypeScript are explicit types. While they are not required, they make it clear which type of data is expected where. Any deviation from the specified contract is immediately caught by TypeScript helping you find possible issues as soon as possible during the development process. This is particularly useful when working with AJAX responses and their data. - -Another change, that you might have noticed already, is TypeScript string interpolation. Using string interpolation simplifies dynamic string composition and increases the readability of your code. Compare plain JavaScript: - -```js -var restQuery = "/_api/Web/Lists/GetByTitle('" + TASK_LIST + "')/items?$select=ID,Title,\ -Status,StartDate,DueDate,AssignedTo/Title&$expand=AssignedTo&\ -$filter=((DueDate ge '" + startDate + "' and DueDate le '" + endDate + "')or(StartDate ge '" + startDate + "' and StartDate le '" + endDate + "'))"; -``` - -to: - -```ts -const restQuery: string = `/_api/Web/Lists/GetByTitle('${escape(this.properties.listName)}')/items?$select=ID,Title,\ -Status,StartDate,DueDate,AssignedTo/Title&$expand=AssignedTo&\ -$filter=((DueDate ge '${startDate}' and DueDate le '${endDate}')or(StartDate ge '${startDate}' and StartDate le '${endDate}'))`; -``` - -Additional benefit of using TypeScript string interpolation is, that you don't need to escape quotes, which also simplifies composing REST queries. - -To confirm that everything is working as expected, in the command line execute: - -```sh -gulp serve --nobrowser -``` - -Navigate to the hosted workbench and add the web part to the canvas. Although visually nothing has changed, the new code base uses TypeScript and its type definitions to help you maintain the solution. - -### Replace jQuery AJAX calls with SharePoint Framework API - -At this moment, the solution uses jQuery AJAX calls to communicate with the SharePoint REST API. For regular GET requests, the jQuery AJAX API is just as convenient as using the SharePoint Framework SPHttpClient. The real difference is when performing POST requests such as the one for updating the event: - -```ts -$.ajax({ - url: this.context.pageContext.web.absoluteUrl + '/_api/contextinfo', - type: 'POST', - headers: { - 'Accept': 'application/json;odata=nometadata' - } -}) - .then((data: { FormDigestValue: string }, textStatus: string, jqXHR: JQueryXHR): JQueryXHR => { - return $.ajax({ - url: `${this.context.pageContext.web.absoluteUrl}\ -/_api/Web/Lists/getByTitle('${escape(this.properties.listName)}')/Items(${id})`, - type: 'POST', - data: JSON.stringify({ - StartDate: sDate, - DueDate: dDate, - }), - headers: { - Accept: "application/json;odata=nometadata", - "Content-Type": "application/json;odata=nometadata", - "X-RequestDigest": data.FormDigestValue, - "IF-MATCH": "*", - "X-Http-Method": "PATCH" - } - }); - }) - .done((data: {}, textStatus: string, jqXHR: JQueryXHR): void => { - alert("Update Successful"); - }) - // ... -``` - -Because you want to update a list item, you need to provide SharePoint with a valid request digest token. While it's available on classic pages, it's valid for 3 minutes, so it's always the safest to retrieve a valid token yourself before performing an update operation. Once you obtained the request digest, you have to add it to request headers of the update request. If you don't, the request will fail. - -SharePoint Framework SPHttpClient simplifies communicating with SharePoint as it automatically detects if the request is a POST request and needs a valid request digest. If it does, the SPHttpClient automatically retrieves it from SharePoint and adds it to the request. By comparison, the same request issued using the SPHttpClient would look like this: - -```ts -this.context.spHttpClient.post(`${this.context.pageContext.web.absoluteUrl}\ -/_api/Web/Lists/getByTitle('${escape(this.properties.listName)}')/Items(${id})`, SPHttpClient.configurations.v1, { - body: JSON.stringify({ - StartDate: sDate, - DueDate: dDate, - }), - headers: { - Accept: "application/json;odata=nometadata", - "Content-Type": "application/json;odata=nometadata", - "IF-MATCH": "*", - "X-Http-Method": "PATCH" - } -}) -.then((response: SPHttpClientResponse): void => { - // ... -}); -``` - -To replace the original jQuery AJAX calls with the SharePoint Framework SPHttpClient API, in the code editor open the **./src/webparts/tasksCalendar/TasksCalendarWebPart.ts** file. To the list of imports add: - -```ts -import { SPHttpClient, SPHttpClientResponse } from '@microsoft/sp-http'; -``` - -In the **TasksCalendarWebPart** class replace the **displayTasks** and **updateTask** methods with the following code: - -```ts -export default class TasksCalendarWebPart extends BaseClientSideWebPart { - // ... - - private displayTasks(): void { - $('#calendar').fullCalendar('destroy'); - $('#calendar').fullCalendar({ - weekends: false, - header: { - left: 'prev,next today', - center: 'title', - right: 'month,basicWeek,basicDay' - }, - displayEventTime: false, - // open up the display form when a user clicks on an event - eventClick: (calEvent: FC.EventObject, jsEvent: MouseEvent, view: FC.ViewObject): void => { - (window as any).location = `${this.context.pageContext.web.absoluteUrl}\ -/Lists/${escape(this.properties.listName)}/DispForm.aspx?ID=${calEvent.id}`; - }, - editable: true, - timezone: "UTC", - droppable: true, // this allows things to be dropped onto the calendar - // update the end date when a user drags and drops an event - eventDrop: (event: FC.EventObject, delta: moment.Duration, revertFunc: Function): void => { - this.updateTask(event.id, event.start, event.end); - }, - // put the events on the calendar - events: (start: moment.Moment, end: moment.Moment, timezone: string, callback: Function): void => { - const startDate: string = start.format('YYYY-MM-DD'); - const endDate: string = end.format('YYYY-MM-DD'); - - const restQuery: string = `/_api/Web/Lists/GetByTitle('${escape(this.properties.listName)}')/items?$select=ID,Title,\ -Status,StartDate,DueDate,AssignedTo/Title&$expand=AssignedTo&\ -$filter=((DueDate ge '${startDate}' and DueDate le '${endDate}')or(StartDate ge '${startDate}' and StartDate le '${endDate}'))`; - - this.context.spHttpClient.get(this.context.pageContext.web.absoluteUrl + restQuery, SPHttpClient.configurations.v1, { - headers: { - 'Accept': "application/json;odata.metadata=none" - } - }) - .then((response: SPHttpClientResponse): Promise<{ value: ITask[] }> => { - return response.json(); - }) - .then((data: { value: ITask[] }): void => { - let personColors: { [person: string]: string; } = {}; - let colorNo: number = 0; - - const events: FC.EventObject[] = data.value.map((task: ITask): FC.EventObject => { - const assignedTo: string = task.AssignedTo.map((person: { Title: string }): string => { - return person.Title; - }).join(', '); - - let color: string = personColors[assignedTo]; - if (!color) { - color = this.colors[colorNo++]; - personColors[assignedTo] = color; - } - if (colorNo >= this.colors.length) { - colorNo = 0; - } - - return { - title: `${task.Title} - ${assignedTo}`, - id: task.ID, - // specify the background color and border color can also create a class and use className paramter - color: color, - start: moment.utc(task.StartDate).add("1", "days"), - // add one day to end date so that calendar properly shows event ending on that day - end: moment.utc(task.DueDate).add("1", "days") - }; - }); - - callback(events); - }); - } - }); - } - - private updateTask(id: number, startDate: moment.Moment, dueDate: moment.Moment): void { - // subtract the previously added day to the date to store correct date - const sDate: string = moment.utc(startDate).add("-1", "days").format('YYYY-MM-DD') + "T" + - startDate.format("hh:mm") + ":00Z"; - if (!dueDate) { - dueDate = startDate; - } - const dDate: string = moment.utc(dueDate).add("-1", "days").format('YYYY-MM-DD') + "T" + - dueDate.format("hh:mm") + ":00Z"; - - this.context.spHttpClient.post(`${this.context.pageContext.web.absoluteUrl}\ -/_api/Web/Lists/getByTitle('${escape(this.properties.listName)}')/Items(${id})`, SPHttpClient.configurations.v1, { - body: JSON.stringify({ - StartDate: sDate, - DueDate: dDate, - }), - headers: { - Accept: "application/json;odata=nometadata", - "Content-Type": "application/json;odata=nometadata", - "IF-MATCH": "*", - "X-Http-Method": "PATCH" - } - }) - .then((response: SPHttpClientResponse): void => { - if (response.ok) { - alert("Update Successful"); - } - else { - alert("Update Failed"); - } - - this.displayTasks(); - }); - } - - // ... -} -``` - -> **Important:** If you're suppressing metadata in the responses of the SharePoint REST API, when using the SharePoint Framework SPHttpClient you have to ensure that you're using `application/json;odata.metadata=none` and not `application/json;odata=nometadata` as the value of the **Accept** header. SPHttpClient uses OData 4.0 and requires the first value. If you use the latter instead, the request will fail with a **406 Not Acceptable** response. - -To confirm that everything is working as expected, in the command line execute: - -```sh -gulp serve --nobrowser -``` - -Navigate to the hosted workbench and add the web part to the canvas. Although there are still no visual changes, the new code uses the SharePoint Framework SPHttpClient which simplifies your code and maintaining your solution. +--- +title: Migrate jQuery and FullCalendar solution built using Script Editor web part to SharePoint Framework +description: Migrate a SharePoint customization by using FullCalendar built with the Script Editor web part to the SharePoint Framework. +ms.date: 08/19/2020 +ms.localizationpriority: high +--- +# Migrate jQuery and FullCalendar solution built using Script Editor web part to SharePoint Framework + +When building SharePoint solutions, SharePoint developers often use the [FullCalendar](https://fullcalendar.io) jQuery plug-in to display data in calendar view. FullCalendar is a great alternative to the standard SharePoint calendar view, as it allows you to render as calendar data from multiple calendar lists, data from non-calendar lists, or even data from outside SharePoint. This article illustrates how you would migrate a SharePoint customization by using FullCalendar built with the Script Editor web part to the SharePoint Framework. + +## List of tasks displayed as a calendar built using the Script Editor web part + +To illustrate the process of migrating a SharePoint customization using FullCalendar to the SharePoint Framework, you'll use the following solution that shows a calendar view of tasks retrieved from a SharePoint list. + +![Calendar view of tasks displayed on a SharePoint page](../../../images/fullcalendar-sewp.png) + +The solution is built using the standard SharePoint Script Editor web part. Following is the code used by the customization. + +```html + + + + +
    + + +``` + +> [!NOTE] +> This solution is based on the work of [Mark Rackley](http://www.markrackley.net), Office Servers and Services MVP and Chief Strategy Officer at PAIT Group. For more information about the original solution, see [Using FullCalendar.io to Create Custom Calendars in SharePoint](http://www.markrackley.net/2017/06/07/using-fullcalendar-io-to-create-custom-calendars-in-sharepoint/). + +First, the customization loads the libraries it uses: jQuery, Moment.js, and FullCalendar in the first few `