Updating react-todo-basic to GA level
This commit is contained in:
parent
89092b794b
commit
cf2dd21984
|
@ -0,0 +1,25 @@
|
|||
# EditorConfig helps developers define and maintain consistent
|
||||
# coding styles between different editors and IDEs
|
||||
# editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
|
||||
[*]
|
||||
|
||||
# change these settings to your own preference
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
# we recommend you to keep these unchanged
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[{package,bower}.json]
|
||||
indent_style = space
|
||||
indent_size = 2
|
|
@ -0,0 +1 @@
|
|||
* text=auto
|
|
@ -0,0 +1,32 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Dependency directories
|
||||
node_modules
|
||||
|
||||
# Build generated files
|
||||
dist
|
||||
lib
|
||||
solution
|
||||
temp
|
||||
*.sppkg
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# OSX
|
||||
.DS_Store
|
||||
|
||||
# Visual Studio files
|
||||
.ntvs_analysis.dat
|
||||
.vs
|
||||
bin
|
||||
obj
|
||||
|
||||
# Resx Generated Code
|
||||
*.resx.ts
|
||||
|
||||
# Styles Generated Code
|
||||
*.scss.ts
|
|
@ -0,0 +1,14 @@
|
|||
# Folders
|
||||
.vscode
|
||||
coverage
|
||||
node_modules
|
||||
sharepoint
|
||||
src
|
||||
temp
|
||||
|
||||
# Files
|
||||
*.csproj
|
||||
.git*
|
||||
.yo-rc.json
|
||||
gulpfile.js
|
||||
tsconfig.json
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"@microsoft/generator-sharepoint": {
|
||||
"libraryName": "react-todo-basic",
|
||||
"framework": "react",
|
||||
"version": "1.0.2",
|
||||
"libraryId": "40efb476-b506-409a-8848-5fbfb9456af6"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,348 @@
|
|||
## Summary
|
||||
A simple todo web part built using react to showcase some of the SharePoint Framework developer features, utilities and best practices in building react based web parts.
|
||||
|
||||
![Todo basic web part demo in SharePoint Workbench](./assets/todo-basic-demo.gif)
|
||||
|
||||
## Used SharePoint Framework Version
|
||||
![drop](https://img.shields.io/badge/drop-GA-green.svg)
|
||||
|
||||
## Applies to
|
||||
|
||||
* [SharePoint Framework Developer Documentation](http://dev.office.com/sharepoint/docs/spfx/sharepoint-framework-overview)
|
||||
|
||||
## Solution
|
||||
|
||||
Solution|Author(s)
|
||||
--------|---------
|
||||
react-todo-basic | Chakkaradeep Chandran (@chakkaradeep)
|
||||
|
||||
## Version history
|
||||
|
||||
Version|Date|Comments
|
||||
-------|----|--------
|
||||
1.0.2|May 4th, 2017|Updated to SPFx GA
|
||||
1.0.1|February 1st, 2017|Updated to SPFx drop RC0
|
||||
1.0.0|October 12th, 2016|Initial release
|
||||
|
||||
## Disclaimer
|
||||
**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
|
||||
|
||||
---
|
||||
|
||||
## Minimal Path to Awesome
|
||||
|
||||
- Clone this repository
|
||||
- Move to sample folder
|
||||
- in the command line run:
|
||||
- `npm install`
|
||||
- `gulp serve`
|
||||
|
||||
## Features
|
||||
This todo basic sample web part showcases some of the SharePoint Framework developer features which will help you build web parts with great user experiences along with good coding pattern and practices for react based web parts. Below are some resources if you are not familiar with react:
|
||||
|
||||
- [React Quick Start](https://facebook.github.io/react/docs/tutorial.html)
|
||||
- [TypeScript React Tutorials](https://www.typescriptlang.org/docs/handbook/react-&-webpack.html)
|
||||
|
||||
### React pattern
|
||||
|
||||
While there are many patterns that one can choose to build their react based app, this web part sample uses the [Container component](http://reactpatterns.com/#Container component) approach where there is a main contain component and then one or more dummy components that render the data.
|
||||
|
||||
In our case, [TodoContainer.tsx](./src/webparts/todo/components/TodoContainer/TodoContainer.tsx) is the container component while the rest of the components are sub components.
|
||||
|
||||
All of the components are placed under the [components](./src/webparts/todo/components) folder. Each component folder has its corresponding:
|
||||
|
||||
- Component file | .tsx file
|
||||
- Props interface, if applicable | .ts file
|
||||
- State interface, if applicable | .ts file
|
||||
- Sass file, if applicable | .module.scss file
|
||||
|
||||
The code ensures that the web part file, [TodoWebPart.ts](./src/webparts/todo/TodoWebPart.ts), only handles the key web part specific operations including property pane.
|
||||
|
||||
While you can choose from many patterns, this kind of an approach, to break into multiple components and handling only the web part specific code in the web part file, helps to keep your react based web part structured and well formed.
|
||||
|
||||
### Status Renderers
|
||||
|
||||
SharePoint Framework provides status renderers to use when the web part is loading information from SharePoint or display error if the web part run into issues that could prevent it from working properly. The following status renderers are available via the web part context property. Do note that the status renderers take up the entire web part UX.
|
||||
|
||||
- Loading indicator
|
||||
- Used to display the loading indicator. Useful when you are initializing or loading any content in your web part.
|
||||
- Error indicator
|
||||
- Used to display error messages.
|
||||
|
||||
![Todo basic web part loading progress](./assets/todo-basic-placeholder.gif)
|
||||
|
||||
Here is an example of using the loading indicator. You can find this code in the `onInit` method in the [TodoWebPart.ts](./src/webparts/todo/TodoWebPart.ts) file.
|
||||
|
||||
```ts
|
||||
this.context.statusRenderer.displayLoadingIndicator(this.domElement, "Todo");
|
||||
```
|
||||
The code above displays the default loading indicator for web parts. The `this.domElement` specifically instructs the loading indicator to be displayed in the web part's DOM element.
|
||||
|
||||
To clear the loading indicator when your operation is complete, you just call [`clearLoadingIndicator`](https://dev.office.com/sharepoint/reference/spfx/sp-webpart-base/iclientsidewebpartstatusrenderer):
|
||||
|
||||
```ts
|
||||
this.context.statusRenderer.clearLoadingIndicator(this.domElement);
|
||||
```
|
||||
|
||||
### Placeholders
|
||||
Placeholders are a great way to show default information when the web part is first run or needs to be configured. SharePoint Framework provides a default placeholder react component that you can use in your react based web parts.
|
||||
|
||||
![Todo basic web part placeholder](./assets/todo-basic-placeholder.gif)
|
||||
|
||||
To use this placeholder component, you will need to import the `Placeholder` component from `@microsoft/sp-webpart-base` module.
|
||||
|
||||
```ts
|
||||
import { Placeholder } from '@microsoft/sp-webpart-base';
|
||||
```
|
||||
Once imported, then you can simply create the component. You can find this code in the [TodoContainer.tsx](./src/webparts/todo/components/TodoContainer/TodoContainer.tsx) file.
|
||||
|
||||
```tsx
|
||||
<Placeholder
|
||||
icon={ 'ms-Icon--Edit' }
|
||||
iconText='Todos'
|
||||
description='Get things done. Organize and share your team\'s to-do items with your team. Edit this web part to start managing to-dos.' />
|
||||
```
|
||||
You can also include a button in the placeholder if you want to aid specific operation that helps end users.
|
||||
|
||||
```tsx
|
||||
<Placeholder
|
||||
icon={ 'ms-Icon--Edit' }
|
||||
iconText='Todos'
|
||||
description='Get things done. Organize and share your team\'s to-do items with your team.'
|
||||
buttonLabel='Configure'
|
||||
onAdd={ this._configureWebPart } />
|
||||
```
|
||||
### Lodash Utility Library
|
||||
[lodash](https://lodash.com/) is a great JavaScript utility library that you can use to perform operations on various objects like arrays, numbers, strings etc., SharePoint Framework includes [`lodash` utility library](https://www.npmjs.com/package/@microsoft/sp-lodash-subset) for use with SharePoint Framework out of the box so you need not install it separately. To improve runtime performance, it only includes a subset of the most essential lodash functions.
|
||||
|
||||
To use the `lodash` utility, you will need to first import the library from the `@microsoft/sp-lodash-subset` module:
|
||||
|
||||
```
|
||||
import * as lodash from '@microsoft/sp-lodash-subset';
|
||||
```
|
||||
Here is an example how the [MockDataProvider](./src/webparts/todo/tests/MockDataProvider.ts) uses `lodash`'s `findIndex` method to find the index of the todo item to update. You can find this code in the `updateItem` method:
|
||||
|
||||
```
|
||||
const index: number =
|
||||
lodash.findIndex(
|
||||
this._items[this._selectedList.Title],
|
||||
(item: ITodoItem) => item.Id === itemUpdated.Id
|
||||
);
|
||||
```
|
||||
### Page Display Modes
|
||||
SharePoint pages have display modes which indicates in which mode that page and/or its contents (e.g. text and web parts) are displayed. In the classic server-side SharePoint page, the web part needs to be in edit mode even though the page is already in the edit mode while in the modern client-side SharePoint page, both the page and/or its contents are in the same mode.
|
||||
|
||||
You can provide a tailored experience using the display modes to enhance the web part user experience. In this web part, we display different placeholder depending on the page display mode. This is well demonstrated in the classic server-side SharePoint page.
|
||||
|
||||
When the page is in edit mode, but the web part is not, the web part displays the following placeholder.
|
||||
|
||||
![Todo basic web part placeholder in page edit mode](./assets/todo-basic-placeholder-read-mode.png)
|
||||
|
||||
When the page in in edit mode and also the web part, the web part displays the following placeholder:
|
||||
|
||||
![Todo basic web part placeholder in web part edit mode](./assets/todo-basic-placeholder-edit-mode.png)
|
||||
|
||||
You can see this in action in the the [TodoContainer.tsx](./src/webparts/todo/components/TodoContainer/TodoContainer.tsx) component's `render` method:
|
||||
|
||||
```ts
|
||||
{ this._showPlaceHolder && this.props.webPartDisplayMode === DisplayMode.Edit &&
|
||||
<Placeholder
|
||||
icon={ 'ms-Icon--Edit' }
|
||||
iconText='Todos'
|
||||
description='Get things done. Organize and share your team\'s to-do items with your team.'
|
||||
buttonLabel='Configure'
|
||||
onAdd={ this._configureWebPart } />
|
||||
}
|
||||
{ this._showPlaceHolder && this.props.webPartDisplayMode === DisplayMode.Read &&
|
||||
<Placeholder
|
||||
icon={ 'ms-Icon--Edit' }
|
||||
iconText='Todos'
|
||||
description='Get things done. Organize and share your team\'s to-do items with your team. Edit this web part to start managing to-dos.' />
|
||||
}
|
||||
{ !this._showPlaceHolder &&
|
||||
<div className={ styles.todo }>
|
||||
<div className={ styles.topRow }>
|
||||
<h2 className={ styles.todoHeading }>Todo</h2>
|
||||
</div>
|
||||
<TodoForm onAddTodoItem={ this._createTodoItem} />
|
||||
<TodoList items={this.state.todoItems}
|
||||
onCompleteTodoItem={this._completeTodoItem}
|
||||
onDeleteTodoItem={this._deleteTodoItem} />
|
||||
</div>
|
||||
}
|
||||
```
|
||||
### Loading SharePoint data in property pane
|
||||
One of the things you may want to do in your web part is the ability to configure the data source of your web part. For example, selecting a SharePoint list to bind to. Usually, this is presented in the web part property pane. However, this requires you fetch the available lists from the SharePoint site.
|
||||
|
||||
[TodoWebPart.ts](./src/webparts/todo/TodoWebPart.ts) demonstrates an approach that will help you fetch data from SharePoint and populate a property pane field, in this case, a dropdown. This operation is performed in the `onInit` method where it calls the `_getTaskLists` method to query the data source and populate the corresponding property pane dropdown field property array:
|
||||
|
||||
```ts
|
||||
private _loadTaskLists(): Promise<any> {
|
||||
return this._dataProvider.getTaskLists()
|
||||
.then((taskLists: ITodoTaskList[]) => {
|
||||
this._disableDropdown = taskLists.length === 0;
|
||||
if (taskLists.length !== 0) {
|
||||
this._dropdownOptions = taskLists.map((list: ITodoTaskList) => {
|
||||
return {
|
||||
key: list.Id,
|
||||
text: list.Title
|
||||
};
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
As we do this operation in the `onInit` method, the dropdown values are initialized by the time property pane is initialized or invoked. Also, notice how we use the loading indicator in the `onInit` method during this operation to provide information to the end user:
|
||||
|
||||
```ts
|
||||
protected onInit(): Promise<void> {
|
||||
this.context.statusRenderer.displayLoadingIndicator(this.domElement, "Todo");
|
||||
|
||||
/* ...code removed for brevity... */
|
||||
|
||||
this._loadTaskLists()
|
||||
.then(() => {
|
||||
/*
|
||||
If a list is already selected, then we would have stored the list Id in the associated web part property.
|
||||
So, check to see if we do have a selected list for the web part. If we do, then we set that as the selected list
|
||||
in the property pane dropdown field.
|
||||
*/
|
||||
if (this.properties.spListIndex) {
|
||||
this._setSelectedList(this.properties.spListIndex.toString());
|
||||
this.context.statusRenderer.clearLoadingIndicator(this.domElement);
|
||||
}
|
||||
});
|
||||
|
||||
return super.onInit();
|
||||
}
|
||||
```
|
||||
### Handling empty data in property pane fields
|
||||
This todo sample talks to a SharePoint task list. In the property pane, page authors can select the task list they want to use for the web part. However, in cases where there are no task lists available, we should communicate that to the author. While there are more than one experience you can choose to tackle this problem, the todo sample takes the following approach:
|
||||
|
||||
- If there are no task lists available to choose from, the dropdown field is disabled and a meaningful message is displayed to the author in the property pane.
|
||||
|
||||
Below is the experience:
|
||||
|
||||
![No task lists available to choose in property pane](./assets/todo-basic-no-task-list.gif)
|
||||
|
||||
You can see the code to render this experience in the `_getGroupFields` method in [TodoWebPart.ts](./src/webparts/todo/TodoWebPart.ts):
|
||||
|
||||
```ts
|
||||
private _getGroupFields(): IPropertyPaneField<any>[] {
|
||||
const fields: IPropertyPaneField<any>[] = [];
|
||||
|
||||
fields.push(PropertyPaneDropdown('spListIndex', {
|
||||
label: "Select a list",
|
||||
disabled: this._disableDropdown,
|
||||
options: this._dropdownOptions
|
||||
}));
|
||||
|
||||
if (this._disableDropdown) {
|
||||
fields.push(PropertyPaneLabel(null, {
|
||||
text: 'Could not find tasks lists in your site. Create one or more tasks list and then try using the web part.'
|
||||
}));
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
```
|
||||
The method returns a set of property pane fields to render in the property pane. In our case, we check to see if the dropdown is disabled. If it is, then we add a label field with the appropriate message.
|
||||
|
||||
### Data providers
|
||||
This sample uses two data providers:
|
||||
- [MockDataProvider](./src/webparts/todo/tests/MockDataProvider.ts) - a light weight provider that mocks SharePoint API calls and returns mock data.
|
||||
- [SharePointDataProvider](./src/webparts/todo/dataProviders/SharePointDataProvider.ts) - the provider which talks to SharePoint and returns SharePoint data.
|
||||
|
||||
Depending on where you are web part is running, local environment or SharePoint environment, you use the respective data provider. You can see this in action in the [TodoWebPart.ts](./src/webparts/todo/TodoWebPart.ts) web part constructor. The [`Environment`](https://dev.office.com/sharepoint/reference/spfx/sp-core-library/environment) class and [`EnvironmentType`](https://dev.office.com/sharepoint/reference/spfx/sp-core-library/environmenttype) enum in the [`@microsoft/sp-core-library`](https://dev.office.com/sharepoint/reference/spfx/sp-core-library-module) module helps you determine where the web part is running. We use that to create the corresponding data provider instance in the `onInit` method:
|
||||
|
||||
```ts
|
||||
protected onInit(): Promise<void> {
|
||||
this.context.statusRenderer.displayLoadingIndicator(this.domElement, "Todo");
|
||||
|
||||
/*
|
||||
Create the appropriate data provider depending on where the web part is running.
|
||||
The DEBUG flag will ensure the mock data provider is not bundled with the web part when you package the solution for distribution, that is, using the --ship flag with the package-solution gulp command.
|
||||
*/
|
||||
if (DEBUG && Environment.type === EnvironmentType.Local) {
|
||||
this._dataProvider = new MockDataProvider();
|
||||
} else {
|
||||
this._dataProvider = new SharePointDataProvider();
|
||||
this._dataProvider.webPartContext = this.context;
|
||||
}
|
||||
|
||||
this._openPropertyPane = this._openPropertyPane.bind(this);
|
||||
|
||||
/* ...code removed for brevity... */
|
||||
}
|
||||
```
|
||||
### Using SPHttpClient to fetch SharePoint data
|
||||
|
||||
SharePoint Framework includes a [`SPHttpClient`](https://dev.office.com/sharepoint/reference/spfx/sp-http/sphttpclient) utility class that you can use to interact with SharePoint data using SharePoint REST APIs. It adds default headers, manages the digest needed for writes, and collects telemetry that helps the service to monitor the performance of an application. For communicating with non-SharePoint services, you can use the [`HttpClient`](https://dev.office.com/sharepoint/reference/spfx/sp-http/httpclient) utility class instead.
|
||||
|
||||
You can see this in action in the [SharePointDataProvider](./src/webparts/todo/dataProviders/SharePointDataProvider.ts). For example, here is what we do in the `createItem` method which creates a new todo item in the specific SharePoint list:
|
||||
|
||||
```ts
|
||||
public createItem(title: string): Promise<ITodoItem[]> {
|
||||
const batch: SPHttpClientBatch = this.webPartContext.spHttpClient.beginBatch();
|
||||
|
||||
const batchPromises: Promise<{}>[] = [
|
||||
this._createItem(batch, title),
|
||||
this._getItemsBatched(batch)
|
||||
];
|
||||
|
||||
return this._resolveBatch(batch, batchPromises);
|
||||
}
|
||||
```
|
||||
|
||||
And below is the code that retrieves todo items from the task list. We have a simple GET and a batched GET to accomodate for batch requests.
|
||||
|
||||
```ts
|
||||
private _getItems(requester: SPHttpClient): Promise<ITodoItem[]> {
|
||||
const queryString: string = `?$select=Id,Title,PercentComplete`;
|
||||
const queryUrl: string = this._listItemsUrl + queryString;
|
||||
|
||||
return requester.get(queryUrl, SPHttpClient.configurations.v1)
|
||||
.then((response: Response) => {
|
||||
return response.json();
|
||||
})
|
||||
.then((json: { value: ITodoItem[] }) => {
|
||||
return json.value.map((task: ITodoItem) => {
|
||||
return task;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private _getItemsBatched(requester: SPHttpClientBatch): Promise<ITodoItem[]> {
|
||||
const queryString: string = `?$select=Id,Title,PercentComplete`;
|
||||
const queryUrl: string = this._listItemsUrl + queryString;
|
||||
|
||||
return requester.get(queryUrl, SPHttpClientBatch.configurations.v1)
|
||||
.then((response: Response) => {
|
||||
return response.json();
|
||||
})
|
||||
.then((json: { value: ITodoItem[] }) => {
|
||||
return json.value.map((task: ITodoItem) => {
|
||||
return task;
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
To execute multiple API requests, we create a new batch that includes those requests, and then resolve it. In our code, we create the following two requests:
|
||||
|
||||
- `_createItem` | Creating a new item in the list
|
||||
- `_getItemsBatched` | Getting the new set of items
|
||||
|
||||
Each of the method will be executed in the order specified. To execute the batch requests, we call the `_resolveBatch` method
|
||||
|
||||
And finally the `_resolveBatch` method which executes and resolves the promises in the current batch:
|
||||
|
||||
```ts
|
||||
private _resolveBatch(batch: SPHttpClientBatch, promises: Promise<{}>[]): Promise<ITodoItem[]> {
|
||||
return batch.execute()
|
||||
.then(() => Promise.all(promises).then(values => values[values.length - 1]));
|
||||
}
|
||||
```
|
||||
|
||||
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-todo-basic" />
|
||||
|
||||
|
Binary file not shown.
After Width: | Height: | Size: 734 KiB |
Binary file not shown.
After Width: | Height: | Size: 369 KiB |
Binary file not shown.
After Width: | Height: | Size: 126 KiB |
Binary file not shown.
After Width: | Height: | Size: 109 KiB |
Binary file not shown.
After Width: | Height: | Size: 187 KiB |
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"entries": [
|
||||
{
|
||||
"entry": "./lib/webparts/todo/TodoWebPart.js",
|
||||
"manifest": "./src/webparts/todo/TodoWebPart.manifest.json",
|
||||
"outputPath": "./dist/todo.bundle.js"
|
||||
}
|
||||
],
|
||||
"externals": {},
|
||||
"localizedResources": {
|
||||
"todoStrings": "webparts/todo/loc/{locale}.js"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"deployCdnPath": "temp/deploy"
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"workingDir": "./temp/deploy/",
|
||||
"account": "<!-- STORAGE ACCOUNT NAME -->",
|
||||
"container": "react-todo-basic",
|
||||
"accessKey": "<!-- ACCESS KEY -->"
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"solution": {
|
||||
"name": "react-todo-basic-client-side-solution",
|
||||
"id": "40efb476-b506-409a-8848-5fbfb9456af6",
|
||||
"version": "1.0.0.0"
|
||||
},
|
||||
"paths": {
|
||||
"zippedPackage": "solution/react-todo-basic.sppkg"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"port": 4321,
|
||||
"initialPage": "https://localhost:5432/workbench",
|
||||
"https": true,
|
||||
"api": {
|
||||
"port": 5432,
|
||||
"entryPath": "node_modules/@microsoft/sp-webpart-workbench/lib/api/"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
{
|
||||
// Display errors as warnings
|
||||
"displayAsWarning": true,
|
||||
// The TSLint task may have been configured with several custom lint rules
|
||||
// before this config file is read (for example lint rules from the tslint-microsoft-contrib
|
||||
// project). If true, this flag will deactivate any of these rules.
|
||||
"removeExistingRules": true,
|
||||
// When true, the TSLint task is configured with some default TSLint "rules.":
|
||||
"useDefaultConfigAsBase": false,
|
||||
// Since removeExistingRules=true and useDefaultConfigAsBase=false, there will be no lint rules
|
||||
// which are active, other than the list of rules below.
|
||||
"lintConfig": {
|
||||
// Opt-in to Lint rules which help to eliminate bugs in JavaScript
|
||||
"rules": {
|
||||
"class-name": false,
|
||||
"export-name": false,
|
||||
"forin": false,
|
||||
"label-position": false,
|
||||
"member-access": true,
|
||||
"no-arg": false,
|
||||
"no-console": false,
|
||||
"no-construct": false,
|
||||
"no-duplicate-case": true,
|
||||
"no-duplicate-variable": true,
|
||||
"no-eval": false,
|
||||
"no-function-expression": true,
|
||||
"no-internal-module": true,
|
||||
"no-shadowed-variable": true,
|
||||
"no-switch-case-fall-through": true,
|
||||
"no-unnecessary-semicolons": true,
|
||||
"no-unused-expression": true,
|
||||
"no-unused-imports": true,
|
||||
"no-use-before-declare": true,
|
||||
"no-with-statement": true,
|
||||
"semicolon": true,
|
||||
"trailing-comma": false,
|
||||
"typedef": false,
|
||||
"typedef-whitespace": false,
|
||||
"use-named-parameter": true,
|
||||
"valid-typeof": true,
|
||||
"variable-name": false,
|
||||
"whitespace": false
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"cdnBasePath": "<!-- PATH TO CDN -->"
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
'use strict';
|
||||
|
||||
const gulp = require('gulp');
|
||||
const build = require('@microsoft/sp-build-web');
|
||||
|
||||
build.initialize(gulp);
|
|
@ -0,0 +1,36 @@
|
|||
{
|
||||
"name": "react-todo-basic",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@microsoft/sp-client-base": "~1.0.0",
|
||||
"@microsoft/sp-core-library": "~1.0.0",
|
||||
"@microsoft/sp-webpart-base": "~1.0.0",
|
||||
"@types/immutability-helper": "^2.0.15",
|
||||
"@types/react": "0.14.46",
|
||||
"@types/react-addons-shallow-compare": "0.14.17",
|
||||
"@types/react-addons-test-utils": "0.14.15",
|
||||
"@types/react-addons-update": "0.14.14",
|
||||
"@types/react-dom": "0.14.18",
|
||||
"@types/webpack-env": ">=1.12.1 <1.14.0",
|
||||
"immutability-helper": "^2.2.0",
|
||||
"react": "15.4.2",
|
||||
"react-dom": "15.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@microsoft/sp-build-web": "~1.0.1",
|
||||
"@microsoft/sp-module-interfaces": "~1.0.0",
|
||||
"@microsoft/sp-webpart-workbench": "~1.0.0",
|
||||
"gulp": "~3.9.1",
|
||||
"@types/chai": ">=3.4.34 <3.6.0",
|
||||
"@types/mocha": ">=2.2.33 <2.6.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "gulp bundle",
|
||||
"clean": "gulp clean",
|
||||
"test": "gulp test"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
interface ITodoWebPartProps {
|
||||
spListIndex: number;
|
||||
}
|
||||
|
||||
export default ITodoWebPartProps;
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"$schema": "../../../node_modules/@microsoft/sp-module-interfaces/lib/manifestSchemas/jsonSchemas/clientSideComponentManifestSchema.json",
|
||||
|
||||
"id": "acbc7f18-e38b-40c1-bf84-3e9eea368ca5",
|
||||
"alias": "TodoWebPart",
|
||||
"componentType": "WebPart",
|
||||
"version": "0.0.1",
|
||||
"manifestVersion": 2,
|
||||
|
||||
"preconfiguredEntries": [{
|
||||
"groupId": "acbc7f18-e38b-40c1-bf84-3e9eea368ca5",
|
||||
"group": { "default": "Under Development" },
|
||||
"title": { "default": "todo" },
|
||||
"description": { "default": "Webpart for managing team tasks" },
|
||||
"officeFabricIconFontName": "Page",
|
||||
"properties": {
|
||||
"description": "todo"
|
||||
}
|
||||
}]
|
||||
}
|
|
@ -0,0 +1,183 @@
|
|||
import * as React from 'react';
|
||||
import * as ReactDom from 'react-dom';
|
||||
import { Version } from '@microsoft/sp-core-library';
|
||||
import {
|
||||
BaseClientSideWebPart,
|
||||
IPropertyPaneConfiguration,
|
||||
PropertyPaneDropdown,
|
||||
IPropertyPaneField,
|
||||
PropertyPaneLabel,
|
||||
IPropertyPaneDropdownOption
|
||||
} from '@microsoft/sp-webpart-base';
|
||||
|
||||
import { Environment, EnvironmentType } from '@microsoft/sp-core-library';
|
||||
import * as lodash from '@microsoft/sp-lodash-subset';
|
||||
import * as strings from 'todoStrings';
|
||||
import TodoContainer from './components/TodoContainer/TodoContainer';
|
||||
import ITodoContainerProps from './components/TodoContainer/ITodoContainerProps';
|
||||
import ITodoWebPartProps from './ITodoWebPartProps';
|
||||
import ITodoDataProvider from './dataProviders/ITodoDataProvider';
|
||||
import MockDataProvider from './tests/MockDataProvider';
|
||||
import SharePointDataProvider from './dataProviders/SharePointDataProvider';
|
||||
import ITodoTaskList from './models/ITodoTaskList';
|
||||
|
||||
export default class TodoWebPart extends BaseClientSideWebPart<ITodoWebPartProps> {
|
||||
|
||||
private _dropdownOptions: IPropertyPaneDropdownOption[];
|
||||
private _dataProvider: ITodoDataProvider;
|
||||
private _selectedList: ITodoTaskList;
|
||||
private _todoContainerComponent: TodoContainer;
|
||||
private _disableDropdown: boolean;
|
||||
|
||||
protected onInit(): Promise<void> {
|
||||
this.context.statusRenderer.displayLoadingIndicator(this.domElement, "Todo");
|
||||
|
||||
/*
|
||||
Create the appropriate data provider depending on where the web part is running.
|
||||
The DEBUG flag will ensure the mock data provider is not bundled with the web part when you package the solution for distribution, that is, using the --ship flag with the package-solution gulp command.
|
||||
*/
|
||||
if (DEBUG && Environment.type === EnvironmentType.Local) {
|
||||
this._dataProvider = new MockDataProvider();
|
||||
} else {
|
||||
this._dataProvider = new SharePointDataProvider();
|
||||
this._dataProvider.webPartContext = this.context;
|
||||
}
|
||||
|
||||
this._openPropertyPane = this._openPropertyPane.bind(this);
|
||||
|
||||
/*
|
||||
Get the list of tasks lists from the current site and populate the property pane dropdown field with the values.
|
||||
*/
|
||||
this._loadTaskLists()
|
||||
.then(() => {
|
||||
/*
|
||||
If a list is already selected, then we would have stored the list Id in the associated web part property.
|
||||
So, check to see if we do have a selected list for the web part. If we do, then we set that as the selected list
|
||||
in the property pane dropdown field.
|
||||
*/
|
||||
if (this.properties.spListIndex) {
|
||||
this._setSelectedList(this.properties.spListIndex.toString());
|
||||
this.context.statusRenderer.clearLoadingIndicator(this.domElement);
|
||||
}
|
||||
});
|
||||
|
||||
return super.onInit();
|
||||
}
|
||||
|
||||
public render(): void {
|
||||
/*
|
||||
Create the react element we want to render in the web part DOM. Pass the required props to the react component.
|
||||
*/
|
||||
const element: React.ReactElement<ITodoContainerProps> = React.createElement(
|
||||
TodoContainer,
|
||||
{
|
||||
dataProvider: this._dataProvider,
|
||||
webPartDisplayMode: this.displayMode,
|
||||
configureStartCallback: this._openPropertyPane
|
||||
}
|
||||
);
|
||||
|
||||
this._todoContainerComponent = <TodoContainer>ReactDom.render(element, this.domElement);
|
||||
}
|
||||
|
||||
protected get dataVersion(): Version {
|
||||
return Version.parse('1.0');
|
||||
}
|
||||
|
||||
private _loadTaskLists(): Promise<any> {
|
||||
return this._dataProvider.getTaskLists()
|
||||
.then((taskLists: ITodoTaskList[]) => {
|
||||
// Disable dropdown field if there are no results from the server.
|
||||
this._disableDropdown = taskLists.length === 0;
|
||||
if (taskLists.length !== 0) {
|
||||
this._dropdownOptions = taskLists.map((list: ITodoTaskList) => {
|
||||
return {
|
||||
key: list.Id,
|
||||
text: list.Title
|
||||
};
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _setSelectedList(value: string) {
|
||||
const selectedIndex: number = lodash.findIndex(this._dropdownOptions,
|
||||
(item: IPropertyPaneDropdownOption) => item.key === value
|
||||
);
|
||||
|
||||
const selectedDropDownOption: IPropertyPaneDropdownOption = this._dropdownOptions[selectedIndex];
|
||||
|
||||
if (selectedDropDownOption) {
|
||||
this._selectedList = {
|
||||
Title: selectedDropDownOption.text,
|
||||
Id: selectedDropDownOption.key.toString()
|
||||
};
|
||||
|
||||
this._dataProvider.selectedList = this._selectedList;
|
||||
}
|
||||
}
|
||||
|
||||
private _openPropertyPane(): void {
|
||||
this.context.propertyPane.open();
|
||||
}
|
||||
|
||||
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
|
||||
return {
|
||||
pages: [
|
||||
{
|
||||
header: {
|
||||
description: strings.PropertyPaneDescription
|
||||
},
|
||||
groups: [
|
||||
{
|
||||
groupName: strings.BasicGroupName,
|
||||
/*
|
||||
Instead of creating the fields here, we call a method that will return the set of property fields to render.
|
||||
*/
|
||||
groupFields: this._getGroupFields()
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
protected onPropertyPaneFieldChanged(propertyPath: string, oldValue: any, newValue: any): void {
|
||||
/*
|
||||
Check the property path to see which property pane feld changed. If the property path matches the dropdown, then we set that list
|
||||
as the selected list for the web part.
|
||||
*/
|
||||
if (propertyPath === 'spListIndex') {
|
||||
this._setSelectedList(newValue);
|
||||
}
|
||||
|
||||
/*
|
||||
Finally, tell property pane to re-render the web part.
|
||||
This is valid for reactive property pane.
|
||||
*/
|
||||
super.onPropertyPaneFieldChanged(propertyPath, oldValue, newValue);
|
||||
}
|
||||
|
||||
private _getGroupFields(): IPropertyPaneField<any>[] {
|
||||
const fields: IPropertyPaneField<any>[] = [];
|
||||
|
||||
fields.push(PropertyPaneDropdown('spListIndex', {
|
||||
label: "Select a list",
|
||||
disabled: this._disableDropdown,
|
||||
options: this._dropdownOptions
|
||||
}));
|
||||
|
||||
/*
|
||||
When we do not have any lists returned from the server, we disable the dropdown. If that is the case,
|
||||
we also add a label field displaying the appropriate message.
|
||||
*/
|
||||
if (this._disableDropdown) {
|
||||
fields.push(PropertyPaneLabel(null, {
|
||||
text: 'Could not find tasks lists in your site. Create one or more tasks list and then try using the web part.'
|
||||
}));
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export interface ITodoProps {
|
||||
description: string;
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
.helloWorld {
|
||||
.container {
|
||||
max-width: 700px;
|
||||
margin: 0px auto;
|
||||
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.row {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.listItem {
|
||||
max-width: 715px;
|
||||
margin: 5px auto 5px auto;
|
||||
box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.button {
|
||||
// Our button
|
||||
text-decoration: none;
|
||||
height: 32px;
|
||||
|
||||
// Primary Button
|
||||
min-width: 80px;
|
||||
background-color: #0078d7;
|
||||
border-color: #0078d7;
|
||||
color: #ffffff;
|
||||
|
||||
// Basic Button
|
||||
outline: transparent;
|
||||
position: relative;
|
||||
font-family: "Segoe UI WestEuropean","Segoe UI",-apple-system,BlinkMacSystemFont,Roboto,"Helvetica Neue",sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
border-width: 0;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
padding: 0 16px;
|
||||
|
||||
.label {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
margin: 0 4px;
|
||||
vertical-align: top;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import * as React from 'react';
|
||||
import styles from './Todo.module.scss';
|
||||
import { ITodoProps } from './ITodoProps';
|
||||
import { escape } from '@microsoft/sp-lodash-subset';
|
||||
|
||||
export default class Todo extends React.Component<ITodoProps, void> {
|
||||
public render(): React.ReactElement<ITodoProps> {
|
||||
return (
|
||||
<div className={styles.helloWorld}>
|
||||
<div className={styles.container}>
|
||||
<div className={`ms-Grid-row ms-bgColor-themeDark ms-fontColor-white ${styles.row}`}>
|
||||
<div className="ms-Grid-col ms-u-lg10 ms-u-xl8 ms-u-xlPush2 ms-u-lgPush1">
|
||||
<span className="ms-font-xl ms-fontColor-white">Welcome to SharePoint!</span>
|
||||
<p className="ms-font-l ms-fontColor-white">Customize SharePoint experiences using Web Parts.</p>
|
||||
<p className="ms-font-l ms-fontColor-white">{escape(this.props.description)}</p>
|
||||
<a href="https://aka.ms/spfx" className={styles.button}>
|
||||
<span className={styles.label}>Learn more</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
import { DisplayMode } from '@microsoft/sp-core-library';
|
||||
import ITodoDataProvider from '../../dataProviders/ITodoDataProvider';
|
||||
|
||||
interface ITodoContainerProps {
|
||||
dataProvider: ITodoDataProvider;
|
||||
webPartDisplayMode: DisplayMode;
|
||||
configureStartCallback: () => void;
|
||||
}
|
||||
|
||||
export default ITodoContainerProps;
|
|
@ -0,0 +1,7 @@
|
|||
import ITodoItem from '../../models/ITodoItem';
|
||||
|
||||
interface ITodoContainerState {
|
||||
todoItems: ITodoItem[];
|
||||
}
|
||||
|
||||
export default ITodoContainerState;
|
|
@ -0,0 +1,15 @@
|
|||
.todo {
|
||||
padding: 28px 40px;
|
||||
|
||||
.topRow {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.todoHeading {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
padding: 0px;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,126 @@
|
|||
import * as React from 'react';
|
||||
import { DisplayMode } from '@microsoft/sp-core-library';
|
||||
import { Placeholder } from '@microsoft/sp-webpart-base';
|
||||
import { Fabric } from 'office-ui-fabric-react';
|
||||
import TodoForm from '../TodoForm/TodoForm';
|
||||
import styles from './TodoContainer.module.scss';
|
||||
import ITodoItem from '../../models/ITodoItem';
|
||||
import TodoList from '../TodoList/TodoList';
|
||||
import ITodoContainerProps from './ITodoContainerProps';
|
||||
import ITodoContainerState from './ITodoContainerState';
|
||||
import * as update from 'immutability-helper';
|
||||
|
||||
export default class Todo extends React.Component<ITodoContainerProps, ITodoContainerState> {
|
||||
private _showPlaceHolder: boolean = true;
|
||||
|
||||
constructor(props: ITodoContainerProps) {
|
||||
super(props);
|
||||
|
||||
if (this.props.dataProvider.selectedList) {
|
||||
if (this.props.dataProvider.selectedList.Id !== '0') {
|
||||
this._showPlaceHolder = false;
|
||||
}
|
||||
else if (this.props.dataProvider.selectedList.Id === '0') {
|
||||
this._showPlaceHolder = true;
|
||||
}
|
||||
} else {
|
||||
this._showPlaceHolder = true;
|
||||
}
|
||||
|
||||
this.state = {
|
||||
todoItems: []
|
||||
};
|
||||
|
||||
this._configureWebPart = this._configureWebPart.bind(this);
|
||||
this._createTodoItem = this._createTodoItem.bind(this);
|
||||
this._completeTodoItem = this._completeTodoItem.bind(this);
|
||||
this._deleteTodoItem = this._deleteTodoItem.bind(this);
|
||||
}
|
||||
|
||||
public componentWillReceiveProps(props: ITodoContainerProps) {
|
||||
if (this.props.dataProvider.selectedList) {
|
||||
if (this.props.dataProvider.selectedList.Id !== '0') {
|
||||
this._showPlaceHolder = false;
|
||||
this.props.dataProvider.getItems().then(
|
||||
(items: ITodoItem[]) => {
|
||||
const newItems = update(this.state.todoItems, { $set: items });
|
||||
this.setState({ todoItems: newItems });
|
||||
});
|
||||
}
|
||||
else if (this.props.dataProvider.selectedList.Id === '0') {
|
||||
this._showPlaceHolder = true;
|
||||
}
|
||||
} else {
|
||||
this._showPlaceHolder = true;
|
||||
}
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
if (!this._showPlaceHolder) {
|
||||
this.props.dataProvider.getItems().then(
|
||||
(items: ITodoItem[]) => {
|
||||
this.setState({ todoItems: items });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<Fabric>
|
||||
{ this._showPlaceHolder && this.props.webPartDisplayMode === DisplayMode.Edit &&
|
||||
<Placeholder
|
||||
icon={ 'ms-Icon--Edit' }
|
||||
iconText='Todos'
|
||||
description='Get things done. Organize and share your teams to-do items with your team.'
|
||||
buttonLabel='Configure'
|
||||
onAdd={ this._configureWebPart } />
|
||||
}
|
||||
{ this._showPlaceHolder && this.props.webPartDisplayMode === DisplayMode.Read &&
|
||||
<Placeholder
|
||||
icon={ 'ms-Icon--Edit' }
|
||||
iconText='Todos'
|
||||
description='Get things done. Organize and share your teams to-do items with your team. Edit this web part to start managing to-dos.' />
|
||||
}
|
||||
{ !this._showPlaceHolder &&
|
||||
<div className={ styles.todo }>
|
||||
<div className={ styles.topRow }>
|
||||
<h2 className={ styles.todoHeading }>Todo</h2>
|
||||
</div>
|
||||
<TodoForm onAddTodoItem={ this._createTodoItem} />
|
||||
<TodoList items={this.state.todoItems}
|
||||
onCompleteTodoItem={this._completeTodoItem}
|
||||
onDeleteTodoItem={this._deleteTodoItem} />
|
||||
</div>
|
||||
}
|
||||
</Fabric>
|
||||
);
|
||||
}
|
||||
|
||||
private _configureWebPart(): void {
|
||||
this.props.configureStartCallback();
|
||||
}
|
||||
|
||||
private _createTodoItem(inputValue: string): Promise<any> {
|
||||
return this.props.dataProvider.createItem(inputValue).then(
|
||||
(items: ITodoItem[]) => {
|
||||
const newItems = update(this.state.todoItems, { $set: items });
|
||||
this.setState({ todoItems: newItems });
|
||||
});
|
||||
}
|
||||
|
||||
private _completeTodoItem(todoItem: ITodoItem): Promise<any> {
|
||||
return this.props.dataProvider.updateItem(todoItem).then(
|
||||
(items: ITodoItem[]) => {
|
||||
const newItems = update(this.state.todoItems, { $set: items });
|
||||
this.setState({ todoItems: newItems });
|
||||
});
|
||||
}
|
||||
|
||||
private _deleteTodoItem(todoItem: ITodoItem): Promise<any> {
|
||||
return this.props.dataProvider.deleteItem(todoItem).then(
|
||||
(items: ITodoItem[]) => {
|
||||
const newItems = update(this.state.todoItems, { $set: items });
|
||||
this.setState({ todoItems: newItems });
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import ItemCreationCallback from '../../models/ItemCreationCallback';
|
||||
|
||||
interface ITodoFormProps {
|
||||
onAddTodoItem: ItemCreationCallback;
|
||||
}
|
||||
|
||||
export default ITodoFormProps;
|
|
@ -0,0 +1,5 @@
|
|||
interface ITodoFormState {
|
||||
inputValue: string;
|
||||
}
|
||||
|
||||
export default ITodoFormState;
|
|
@ -0,0 +1,23 @@
|
|||
.todoForm {
|
||||
display: table;
|
||||
|
||||
.textField {
|
||||
display: table-cell;
|
||||
width: 100%;
|
||||
|
||||
input {
|
||||
height: 32px;
|
||||
min-width: 80px;
|
||||
}
|
||||
}
|
||||
.addButtonCell {
|
||||
display: table-cell;
|
||||
vertical-align: top;
|
||||
|
||||
.addButton {
|
||||
margin-left: 10px;
|
||||
white-space: nowrap;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
import * as React from 'react';
|
||||
import {
|
||||
TextField,
|
||||
Button,
|
||||
ButtonType
|
||||
} from 'office-ui-fabric-react';
|
||||
import styles from './TodoForm.module.scss';
|
||||
import ITodoFormState from './ITodoFormState';
|
||||
import ITodoFormProps from './ITodoFormProps';
|
||||
|
||||
export default class TodoForm extends React.Component<ITodoFormProps, ITodoFormState>{
|
||||
|
||||
private _placeHolderText: string = 'Enter your todo';
|
||||
|
||||
constructor(props: ITodoFormProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
inputValue: ''
|
||||
};
|
||||
|
||||
this._handleInputChange = this._handleInputChange.bind(this);
|
||||
this._handleAddButtonClick = this._handleAddButtonClick.bind(this);
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<div className={ styles.todoForm }>
|
||||
<TextField
|
||||
className={ styles.textField }
|
||||
value={ this.state.inputValue }
|
||||
placeholder={ this._placeHolderText }
|
||||
autoComplete='off'
|
||||
onChanged={this._handleInputChange}/>
|
||||
<div className={ styles.addButtonCell }>
|
||||
<Button
|
||||
className={ styles.addButton }
|
||||
buttonType={ ButtonType.primary }
|
||||
ariaLabel='Add a todo task'
|
||||
onClick={this._handleAddButtonClick}>
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private _handleInputChange(newValue: string) {
|
||||
this.setState({
|
||||
inputValue: newValue
|
||||
});
|
||||
}
|
||||
|
||||
private _handleAddButtonClick(event?: React.MouseEvent<HTMLButtonElement>) {
|
||||
this.setState({
|
||||
inputValue: this._placeHolderText
|
||||
});
|
||||
this.props.onAddTodoItem(this.state.inputValue);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
import ITodoItem from '../../models/ITodoItem';
|
||||
import ItemOperationCallback from '../../models/ItemOperationCallback';
|
||||
|
||||
interface ITodoListProps {
|
||||
items: ITodoItem[];
|
||||
onCompleteTodoItem: ItemOperationCallback;
|
||||
onDeleteTodoItem: ItemOperationCallback;
|
||||
}
|
||||
|
||||
export default ITodoListProps;
|
|
@ -0,0 +1,6 @@
|
|||
@import "~office-ui-fabric-react/dist/sass/Fabric.Common";
|
||||
|
||||
.todoList {
|
||||
margin-top: 20px;
|
||||
border-top: 1px $ms-color-neutralTertiaryAlt solid;
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
import * as React from 'react';
|
||||
import { List, FocusZone, FocusZoneDirection, getRTLSafeKeyCode, KeyCodes } from 'office-ui-fabric-react';
|
||||
import ITodoListProps from './ITodoListProps';
|
||||
import TodoListItem from '../TodoListItem/TodoListItem';
|
||||
import ITodoItem from '../../models/ITodoItem';
|
||||
import styles from './TodoList.module.scss';
|
||||
|
||||
export default class TodoList extends React.Component<ITodoListProps, {}> {
|
||||
constructor(props: ITodoListProps) {
|
||||
super(props);
|
||||
|
||||
this._onRenderCell = this._onRenderCell.bind(this);
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<FocusZone
|
||||
direction={ FocusZoneDirection.vertical }
|
||||
isInnerZoneKeystroke={ (ev: React.KeyboardEvent<HTMLElement>) => ev.which === getRTLSafeKeyCode(KeyCodes.right) }
|
||||
>
|
||||
<List
|
||||
className={ styles.todoList }
|
||||
items={ this.props.items }
|
||||
onRenderCell={ this._onRenderCell }
|
||||
/>
|
||||
</FocusZone>
|
||||
);
|
||||
}
|
||||
|
||||
private _onRenderCell(item: ITodoItem, index: number) {
|
||||
return (
|
||||
<TodoListItem item= { item }
|
||||
isChecked={ item.PercentComplete >= 1 ? true : false }
|
||||
onCompleteListItem={this.props.onCompleteTodoItem}
|
||||
onDeleteListItem={this.props.onDeleteTodoItem} />
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import ITodoItem from '../../models/ITodoItem';
|
||||
import ItemOperationCallback from '../../models/ItemOperationCallback';
|
||||
|
||||
interface ITodoListItemProps {
|
||||
item: ITodoItem;
|
||||
isChecked?: boolean;
|
||||
onCompleteListItem: ItemOperationCallback;
|
||||
onDeleteListItem: ItemOperationCallback;
|
||||
}
|
||||
|
||||
export default ITodoListItemProps;
|
|
@ -0,0 +1,31 @@
|
|||
@import "~office-ui-fabric-react/dist/sass/Fabric.Common";
|
||||
|
||||
.todoListItem {
|
||||
border: 1px $ms-color-neutralTertiaryAlt solid;
|
||||
|
||||
.itemTaskRow {
|
||||
padding: 16px 20px 8px 20px;
|
||||
|
||||
@media (max-width: 600px) {
|
||||
padding: 8px 20px 8px 4px;
|
||||
}
|
||||
|
||||
.deleteButton {
|
||||
&:hover, &:focus {
|
||||
color: $ms-color-themePrimary;
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
:global .ms-Label {
|
||||
color: $ms-color-neutralPrimary;
|
||||
font-size: 18px;
|
||||
padding: 0 0 0 36px;
|
||||
}
|
||||
}
|
||||
|
||||
&.isCompleted {
|
||||
background-color: $ms-color-neutralLighter;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
import * as React from 'react';
|
||||
import {
|
||||
Checkbox,
|
||||
Button,
|
||||
ButtonType,
|
||||
FocusZone,
|
||||
FocusZoneDirection,
|
||||
css
|
||||
} from 'office-ui-fabric-react';
|
||||
import styles from './TodoListItem.module.scss';
|
||||
import ITodoItem from '../../models/ITodoItem';
|
||||
import ITodoListItemProps from './ITodoListItemProps';
|
||||
import * as update from 'immutability-helper';
|
||||
|
||||
export default class TodoListItem extends React.Component<ITodoListItemProps,{}> {
|
||||
|
||||
constructor(props: ITodoListItemProps) {
|
||||
super(props);
|
||||
|
||||
this._handleToggleChanged = this._handleToggleChanged.bind(this);
|
||||
this._handleDeleteClick = this._handleDeleteClick.bind(this);
|
||||
}
|
||||
|
||||
public shouldComponentUpdate(newProps: ITodoListItemProps): boolean {
|
||||
return (
|
||||
this.props.item !== newProps.item ||
|
||||
this.props.isChecked !== newProps.isChecked
|
||||
);
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
const classTodoItem: string = css(
|
||||
styles.todoListItem,
|
||||
'ms-Grid',
|
||||
'ms-u-slideDownIn20'
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
role='row'
|
||||
className={ classTodoItem }
|
||||
data-is-focusable={ true }
|
||||
>
|
||||
<FocusZone direction={ FocusZoneDirection.horizontal }>
|
||||
<div className={ css(styles.itemTaskRow, 'ms-Grid-row') }>
|
||||
<Checkbox
|
||||
className={ css(styles.checkbox, 'ms-Grid-col', 'ms-u-sm11') }
|
||||
label={this.props.item.Title}
|
||||
onChange={ this._handleToggleChanged }
|
||||
checked={ this.props.isChecked }
|
||||
/>
|
||||
<Button
|
||||
className={ css(styles.deleteButton, 'ms-Grid-col', 'ms-u-sm1') }
|
||||
buttonType={ ButtonType.icon }
|
||||
icon='Cancel'
|
||||
onClick={this._handleDeleteClick}
|
||||
/>
|
||||
</div>
|
||||
</FocusZone>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private _handleToggleChanged(ev: React.FormEvent<HTMLInputElement>, checked: boolean): void {
|
||||
const newItem: ITodoItem = update(this.props.item, {
|
||||
PercentComplete: { $set: this.props.item.PercentComplete >= 1 ? 0 : 1 }
|
||||
});
|
||||
|
||||
this.props.onCompleteListItem(newItem);
|
||||
}
|
||||
|
||||
private _handleDeleteClick(event: React.MouseEvent<HTMLButtonElement>) {
|
||||
this.props.onDeleteListItem(this.props.item);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
import { IWebPartContext } from '@microsoft/sp-webpart-base';
|
||||
import ITodoItem from '../models/ITodoItem';
|
||||
import ITodoTaskList from '../models/ITodoTaskList';
|
||||
|
||||
interface ITodoDataProvider {
|
||||
|
||||
selectedList: ITodoTaskList;
|
||||
|
||||
webPartContext: IWebPartContext;
|
||||
|
||||
getTaskLists(): Promise<ITodoTaskList[]>;
|
||||
|
||||
getItems(): Promise<ITodoItem[]>;
|
||||
|
||||
createItem(title: string): Promise<ITodoItem[]>;
|
||||
|
||||
updateItem(itemUpdated: ITodoItem): Promise<ITodoItem[]>;
|
||||
|
||||
deleteItem(itemDeleted: ITodoItem): Promise<ITodoItem[]>;
|
||||
}
|
||||
|
||||
export default ITodoDataProvider;
|
|
@ -0,0 +1,174 @@
|
|||
import {
|
||||
SPHttpClient,
|
||||
SPHttpClientBatch,
|
||||
SPHttpClientResponse
|
||||
} from '@microsoft/sp-http';
|
||||
import { IWebPartContext } from '@microsoft/sp-webpart-base';
|
||||
import ITodoDataProvider from '../dataProviders/ITodoDataProvider';
|
||||
import ITodoItem from '../models/ITodoItem';
|
||||
import ITodoTaskList from '../models/ITodoTaskList';
|
||||
|
||||
export default class SharePointDataProvider implements ITodoDataProvider {
|
||||
|
||||
private _selectedList: ITodoTaskList;
|
||||
private _taskLists: ITodoTaskList[];
|
||||
private _listsUrl: string;
|
||||
private _listItemsUrl: string;
|
||||
private _webPartContext: IWebPartContext;
|
||||
|
||||
public set selectedList(value: ITodoTaskList) {
|
||||
this._selectedList = value;
|
||||
this._listItemsUrl = `${this._listsUrl}(guid'${value.Id}')/items`;
|
||||
}
|
||||
|
||||
public get selectedList(): ITodoTaskList {
|
||||
return this._selectedList;
|
||||
}
|
||||
|
||||
public set webPartContext(value: IWebPartContext) {
|
||||
this._webPartContext = value;
|
||||
this._listsUrl = `${this._webPartContext.pageContext.web.absoluteUrl}/_api/web/lists`;
|
||||
}
|
||||
|
||||
public get webPartContext(): IWebPartContext {
|
||||
return this._webPartContext;
|
||||
}
|
||||
|
||||
public getTaskLists(): Promise<ITodoTaskList[]> {
|
||||
const listTemplateId: string = '171';
|
||||
const queryString: string = `?$filter=BaseTemplate eq ${listTemplateId}`;
|
||||
const queryUrl: string = this._listsUrl + queryString;
|
||||
|
||||
return this._webPartContext.spHttpClient.get(queryUrl, SPHttpClient.configurations.v1)
|
||||
.then((response: SPHttpClientResponse) => {
|
||||
return response.json();
|
||||
})
|
||||
.then((json: { value: ITodoTaskList[] }) => {
|
||||
return this._taskLists = json.value;
|
||||
});
|
||||
}
|
||||
|
||||
public getItems(): Promise<ITodoItem[]> {
|
||||
return this._getItems(this.webPartContext.spHttpClient);
|
||||
}
|
||||
|
||||
public createItem(title: string): Promise<ITodoItem[]> {
|
||||
const batch: SPHttpClientBatch = this.webPartContext.spHttpClient.beginBatch();
|
||||
|
||||
const batchPromises: Promise<{}>[] = [
|
||||
this._createItem(batch, title),
|
||||
this._getItemsBatched(batch)
|
||||
];
|
||||
|
||||
return this._resolveBatch(batch, batchPromises);
|
||||
}
|
||||
|
||||
public deleteItem(itemDeleted: ITodoItem): Promise<ITodoItem[]> {
|
||||
const batch: SPHttpClientBatch = this.webPartContext.spHttpClient.beginBatch();
|
||||
|
||||
const batchPromises: Promise<{}>[] = [
|
||||
this._deleteItem(batch, itemDeleted),
|
||||
this._getItemsBatched(batch)
|
||||
];
|
||||
|
||||
return this._resolveBatch(batch, batchPromises);
|
||||
}
|
||||
|
||||
public updateItem(itemUpdated: ITodoItem): Promise<ITodoItem[]> {
|
||||
const batch: SPHttpClientBatch = this.webPartContext.spHttpClient.beginBatch();
|
||||
|
||||
const batchPromises: Promise<{}>[] = [
|
||||
this._updateItem(batch, itemUpdated),
|
||||
this._getItemsBatched(batch)
|
||||
];
|
||||
|
||||
return this._resolveBatch(batch, batchPromises);
|
||||
}
|
||||
|
||||
private _getItems(requester: SPHttpClient): Promise<ITodoItem[]> {
|
||||
const queryString: string = `?$select=Id,Title,PercentComplete`;
|
||||
const queryUrl: string = this._listItemsUrl + queryString;
|
||||
|
||||
return requester.get(queryUrl, SPHttpClient.configurations.v1)
|
||||
.then((response: SPHttpClientResponse) => {
|
||||
return response.json();
|
||||
})
|
||||
.then((json: { value: ITodoItem[] }) => {
|
||||
return json.value.map((task: ITodoItem) => {
|
||||
return task;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private _getItemsBatched(requester: SPHttpClientBatch): Promise<ITodoItem[]> {
|
||||
const queryString: string = `?$select=Id,Title,PercentComplete`;
|
||||
const queryUrl: string = this._listItemsUrl + queryString;
|
||||
|
||||
return requester.get(queryUrl, SPHttpClientBatch.configurations.v1)
|
||||
.then((response: SPHttpClientResponse) => {
|
||||
return response.json();
|
||||
})
|
||||
.then((json: { value: ITodoItem[] }) => {
|
||||
return json.value.map((task: ITodoItem) => {
|
||||
return task;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private _createItem(batch: SPHttpClientBatch, title: string): Promise<SPHttpClientResponse> {
|
||||
const body: {} = {
|
||||
'@data.type': `${this._selectedList.ListItemEntityTypeFullName}`,
|
||||
'Title': title
|
||||
};
|
||||
|
||||
return batch.post(
|
||||
this._listItemsUrl,
|
||||
SPHttpClientBatch.configurations.v1,
|
||||
{ body: JSON.stringify(body) }
|
||||
);
|
||||
}
|
||||
|
||||
private _deleteItem(batch: SPHttpClientBatch, item: ITodoItem): Promise<SPHttpClientResponse> {
|
||||
const itemDeletedUrl: string = `${this._listItemsUrl}(${item.Id})`;
|
||||
|
||||
const headers: Headers = new Headers();
|
||||
headers.append('If-Match', '*');
|
||||
|
||||
return batch.fetch(itemDeletedUrl,
|
||||
SPHttpClientBatch.configurations.v1,
|
||||
{
|
||||
headers,
|
||||
method: 'DELETE'
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private _updateItem(batch: SPHttpClientBatch, item: ITodoItem): Promise<SPHttpClientResponse> {
|
||||
|
||||
const itemUpdatedUrl: string = `${this._listItemsUrl}(${item.Id})`;
|
||||
|
||||
const headers: Headers = new Headers();
|
||||
headers.append('If-Match', '*');
|
||||
|
||||
const body: {} = {
|
||||
'@data.type': `${this._selectedList.ListItemEntityTypeFullName}`,
|
||||
'PercentComplete': item.PercentComplete
|
||||
};
|
||||
|
||||
return batch.fetch(itemUpdatedUrl,
|
||||
SPHttpClientBatch.configurations.v1,
|
||||
{
|
||||
body: JSON.stringify(body),
|
||||
headers,
|
||||
method: 'PATCH'
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private _resolveBatch(batch: SPHttpClientBatch, promises: Promise<{}>[]): Promise<ITodoItem[]> {
|
||||
return batch.execute()
|
||||
.then(() => Promise.all(promises).then(values => values[values.length - 1]));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
define([], function() {
|
||||
return {
|
||||
"PropertyPaneDescription": "Configure todo web part",
|
||||
"BasicGroupName": "Basics"
|
||||
}
|
||||
});
|
|
@ -0,0 +1,10 @@
|
|||
declare interface ITodoStrings {
|
||||
PropertyPaneDescription: string;
|
||||
BasicGroupName: string;
|
||||
DescriptionFieldLabel: string;
|
||||
}
|
||||
|
||||
declare module 'todoStrings' {
|
||||
const strings: ITodoStrings;
|
||||
export = strings;
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
interface ITodoItem {
|
||||
Id: number;
|
||||
Title: string;
|
||||
PercentComplete: number;
|
||||
}
|
||||
|
||||
export default ITodoItem;
|
|
@ -0,0 +1,7 @@
|
|||
interface ITodoTaskList {
|
||||
Id?: string;
|
||||
ListItemEntityTypeFullName?: string;
|
||||
Title: string;
|
||||
}
|
||||
|
||||
export default ITodoTaskList;
|
|
@ -0,0 +1,3 @@
|
|||
type ItemCreationCallback = (inputValue: string) => void;
|
||||
|
||||
export default ItemCreationCallback;
|
|
@ -0,0 +1,5 @@
|
|||
import ITodoItem from './ITodoItem';
|
||||
|
||||
type ItemOperationCallback = (item: ITodoItem) => void;
|
||||
|
||||
export default ItemOperationCallback;
|
|
@ -0,0 +1,124 @@
|
|||
import { IWebPartContext } from '@microsoft/sp-webpart-base';
|
||||
import * as lodash from '@microsoft/sp-lodash-subset';
|
||||
import ITodoDataProvider from '../dataProviders/ITodoDataProvider';
|
||||
import ITodoItem from '../models/ITodoItem';
|
||||
import ITodoTaskList from '../models/ITodoTaskList';
|
||||
|
||||
export default class MockDataProvider implements ITodoDataProvider {
|
||||
|
||||
private _idCounter: number;
|
||||
private _taskLists: ITodoTaskList[];
|
||||
private _items: { [listName: string]: ITodoItem[] };
|
||||
private _selectedList: ITodoTaskList;
|
||||
private _webPartContext: IWebPartContext;
|
||||
|
||||
constructor() {
|
||||
this._idCounter = 0;
|
||||
|
||||
this._taskLists = [
|
||||
this._createMockTaskList('1','List One'),
|
||||
this._createMockTaskList('2', 'List Two'),
|
||||
this._createMockTaskList('3', 'List Three')
|
||||
];
|
||||
|
||||
this._items = {
|
||||
'List One': [
|
||||
this._createMockTodoItem('Sunt filet mignon ut ut porchetta', true),
|
||||
this._createMockTodoItem('Laborum flank brisket esse chuck t-bone', false),
|
||||
this._createMockTodoItem('consectetur ex meatloaf boudin beef laborum pastrami', false)
|
||||
],
|
||||
'List Two': [
|
||||
this._createMockTodoItem('Striga! Ut custodiant te sermonem dicens', false),
|
||||
this._createMockTodoItem('Dixi sunt implicatae', false),
|
||||
this._createMockTodoItem('Est, ante me factus singulis decem gradibus', true),
|
||||
this._createMockTodoItem('Tu omne quod ille voluit', false)
|
||||
],
|
||||
'List Three': [
|
||||
this._createMockTodoItem('Integer massa lectus ultricies at lacinia et', false),
|
||||
this._createMockTodoItem('Phasellus sodales diam at interdum vulputate', false),
|
||||
this._createMockTodoItem('finibus porttitor dolor', false),
|
||||
this._createMockTodoItem('Vestibulum at rutrum nisi', false)
|
||||
]
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
public set webPartContext(value: IWebPartContext) {
|
||||
this._webPartContext = value;
|
||||
}
|
||||
|
||||
public get webPartContext(): IWebPartContext {
|
||||
return this._webPartContext;
|
||||
}
|
||||
|
||||
public set selectedList(value: ITodoTaskList) {
|
||||
this._selectedList = value;
|
||||
}
|
||||
|
||||
public get selectedList(): ITodoTaskList {
|
||||
return this._selectedList;
|
||||
}
|
||||
|
||||
public getTaskLists(): Promise<ITodoTaskList[]> {
|
||||
const taskLists: ITodoTaskList[] = this._taskLists;
|
||||
|
||||
return new Promise<ITodoTaskList[]>((resolve) => {
|
||||
setTimeout(() => resolve(taskLists), 500);
|
||||
});
|
||||
}
|
||||
|
||||
public getItems(): Promise<ITodoItem[]> {
|
||||
const items: ITodoItem[] = lodash.clone(this._items[this.selectedList.Title]);
|
||||
|
||||
return new Promise<ITodoItem[]>((resolve) => {
|
||||
setTimeout(() => resolve(items), 500);
|
||||
});
|
||||
}
|
||||
|
||||
public createItem(title: string): Promise<ITodoItem[]> {
|
||||
const newItem = this._createMockTodoItem(title, false);
|
||||
|
||||
this._items[this.selectedList.Title]=this._items[this.selectedList.Title].concat(newItem);
|
||||
|
||||
return this.getItems();
|
||||
}
|
||||
|
||||
public updateItem(itemUpdated: ITodoItem): Promise<ITodoItem[]> {
|
||||
const index: number =
|
||||
lodash.findIndex(
|
||||
this._items[this._selectedList.Title],
|
||||
(item: ITodoItem) => item.Id === itemUpdated.Id
|
||||
);
|
||||
|
||||
if (index !== -1) {
|
||||
this._items[this._selectedList.Title][index] = itemUpdated;
|
||||
return this.getItems();
|
||||
}
|
||||
else {
|
||||
return Promise.reject(new Error(`Item to update doesn't exist.`));
|
||||
}
|
||||
}
|
||||
|
||||
public deleteItem(itemDeleted: ITodoItem): Promise<ITodoItem[]> {
|
||||
this._items[this.selectedList.Title] = this._items[this.selectedList.Title].filter((item: ITodoItem) => item.Id !== itemDeleted.Id);
|
||||
|
||||
return this.getItems();
|
||||
}
|
||||
|
||||
private _createMockTodoItem(title: string, isCompleted: boolean): ITodoItem {
|
||||
const mockTodoItem: ITodoItem = {
|
||||
Id: this._idCounter++,
|
||||
Title: title,
|
||||
PercentComplete: isCompleted ? 1 : 0
|
||||
};
|
||||
return mockTodoItem;
|
||||
}
|
||||
|
||||
private _createMockTaskList(id: string, title: string): ITodoTaskList {
|
||||
const mockTaskList: ITodoTaskList = {
|
||||
Id: id,
|
||||
Title: title
|
||||
};
|
||||
return mockTaskList;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
/// <reference types="mocha" />
|
||||
|
||||
import { assert } from 'chai';
|
||||
|
||||
describe('TodoWebPart', () => {
|
||||
it('should do something', () => {
|
||||
assert.ok(true);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "commonjs",
|
||||
"jsx": "react",
|
||||
"declaration": true,
|
||||
"sourceMap": true,
|
||||
"types": [
|
||||
"es6-promise",
|
||||
"es6-collections",
|
||||
"webpack-env"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
// Type definitions for Microsoft ODSP projects
|
||||
// Project: ODSP
|
||||
|
||||
/* Global definition for UNIT_TEST builds
|
||||
Code that is wrapped inside an if(UNIT_TEST) {...}
|
||||
block will not be included in the final bundle when the
|
||||
--ship flag is specified */
|
||||
declare const UNIT_TEST: boolean;
|
|
@ -0,0 +1 @@
|
|||
/// <reference path="@ms/odsp.d.ts" />
|
Loading…
Reference in New Issue