Add CRUD operations. (#368)
Extend the sample so it uses SharePoint data.
This commit is contained in:
parent
874f0ebd8f
commit
9c9807c4fd
|
@ -5,7 +5,7 @@
|
|||
Sample Todo web part demonstrating how you can utilize [Vue](https://vuejs.org/v2) (a progressive framework for building user interfaces) with SharePoint Framework using handy [single-file components](https://vuejs.org/v2/guide/single-file-components.html) approach.
|
||||
|
||||
## Used SharePoint Framework Version
|
||||
![drop](https://img.shields.io/badge/drop-1.3.0-green.svg)
|
||||
![drop](https://img.shields.io/badge/drop-ga-green.svg)
|
||||
|
||||
## Applies to
|
||||
|
||||
|
@ -16,7 +16,7 @@ Sample Todo web part demonstrating how you can utilize [Vue](https://vuejs.org/v
|
|||
|
||||
Solution|Author(s)
|
||||
--------|---------
|
||||
vuejs-todo-single-file-component|Sergei Sergeev ([@sergeev_srg](https://twitter.com/sergeev_srg))
|
||||
vuejs-todo-single-file-component|Sergei Sergeev ([@sergeev_srg](https://twitter.com/sergeev_srg)), Dimcho Tsanov ([Singens](http://singens.com))
|
||||
|
||||
## Version history
|
||||
|
||||
|
@ -26,6 +26,7 @@ Version|Date|Comments
|
|||
0.0.2|March 30, 2017|Updated to GA
|
||||
0.0.3|June 14, 2017|Fix webpack 2 issues
|
||||
0.0.4|October 7, 2017|Updated packages to latest versions, misc fixing
|
||||
1.0.0|November 15, 2017|Added data provider that demonstrats the CRUD operations
|
||||
|
||||
## 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.**
|
||||
|
|
|
@ -6,27 +6,30 @@
|
|||
"node": ">=0.10.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@microsoft/sp-core-library": "~1.3.0",
|
||||
"@microsoft/sp-lodash-subset": "~1.3.0",
|
||||
"@microsoft/sp-webpart-base": "~1.3.0",
|
||||
"@microsoft/sp-core-library": "~1.3.4",
|
||||
"@microsoft/sp-webpart-base": "~1.3.4",
|
||||
"@microsoft/sp-lodash-subset": "~1.3.4",
|
||||
"@microsoft/sp-office-ui-fabric-core": "~1.3.4",
|
||||
"@types/webpack-env": ">=1.12.1 <1.14.0",
|
||||
"vue": "^2.4.4",
|
||||
"vue-class-component": "^5.0.1",
|
||||
"vue-property-decorator": "^5.3.0",
|
||||
"vue-template-compiler": "^2.4.4"
|
||||
|
||||
"immutability-helper": "^2.5.0",
|
||||
"vue": "~2.4.4",
|
||||
"vue-class-component": "~5.0.1",
|
||||
"vue-property-decorator": "~5.3.0",
|
||||
"vue-template-compiler": "~2.4.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@microsoft/sp-build-web": "~1.3.0",
|
||||
"@microsoft/sp-module-interfaces": "~1.3.0",
|
||||
"@microsoft/sp-webpart-workbench": "~1.3.0",
|
||||
"@microsoft/sp-build-web": "~1.3.4",
|
||||
"@microsoft/sp-module-interfaces": "~1.3.4",
|
||||
"@microsoft/sp-webpart-workbench": "~1.3.4",
|
||||
"gulp": "~3.9.1",
|
||||
"@types/chai": ">=3.4.34 <3.6.0",
|
||||
"@types/mocha": ">=2.2.33 <2.6.0",
|
||||
"css-loader": "^0.28.4",
|
||||
"gulp": "~3.9.1",
|
||||
"node-sass": "^4.5.3",
|
||||
"sass-loader": "^6.0.6",
|
||||
"vue-loader": "^13.0.5",
|
||||
"webpack-merge": "^4.1.0"
|
||||
"ajv": "~5.2.2",
|
||||
"node-sass": "~4.5.3",
|
||||
"sass-loader": "~6.0.6",
|
||||
"vue-loader": "~13.0.5",
|
||||
"webpack-merge": "~4.1.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "gulp bundle",
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
import { ITaskList } from '../models/ICommonObjects';
|
||||
import {
|
||||
IPropertyPaneDropdownOption,
|
||||
} from '@microsoft/sp-webpart-base';
|
||||
|
||||
export class Utils {
|
||||
|
||||
public GetKeyFroDropdown(list: ITaskList): string {
|
||||
|
||||
return list.Id + "#" + list.ListItemEntityTypeFullName;
|
||||
}
|
||||
|
||||
public GetListId(option: IPropertyPaneDropdownOption): string {
|
||||
let result = "";
|
||||
let keyAsString = option.key.toString();
|
||||
if (keyAsString.indexOf("#") > 0) {
|
||||
result = keyAsString.split("#")[0];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public GetListItemEntityType(option: IPropertyPaneDropdownOption): string {
|
||||
let result = "";
|
||||
let keyAsString = option.key.toString();
|
||||
if (keyAsString.indexOf("#") > 0) {
|
||||
result = keyAsString.split("#")[1];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
import { IWebPartContext } from '@microsoft/sp-webpart-base';
|
||||
import { ITodoItem, ITaskList } from '../models/ICommonObjects';
|
||||
|
||||
interface ITodoDataProvider {
|
||||
|
||||
selectedList: ITaskList;
|
||||
|
||||
webPartContext: IWebPartContext;
|
||||
|
||||
getTaskLists(): Promise<ITaskList[]>;
|
||||
|
||||
getItems(): Promise<ITodoItem[]>;
|
||||
|
||||
createItem(title: string): Promise<ITodoItem[]>;
|
||||
|
||||
updateItem(itemUpdated: ITodoItem): Promise<ITodoItem[]>;
|
||||
|
||||
deleteItem(itemDeleted: ITodoItem): Promise<ITodoItem[]>;
|
||||
}
|
||||
|
||||
export default ITodoDataProvider;
|
|
@ -0,0 +1,153 @@
|
|||
import { IWebPartContext } from '@microsoft/sp-webpart-base';
|
||||
import * as lodash from '@microsoft/sp-lodash-subset';
|
||||
import ITodoDataProvider from './ITodoDataProvider';
|
||||
import { ITodoItem, ITaskList } from '../models/ICommonObjects';
|
||||
|
||||
export default class MockDataProvider implements ITodoDataProvider {
|
||||
|
||||
private _idCounter: number;
|
||||
private _taskLists: ITaskList[];
|
||||
private _items: { [listId: string]: ITodoItem[] };
|
||||
private _selectedList: ITaskList;
|
||||
private _webPartContext: IWebPartContext;
|
||||
|
||||
constructor() {
|
||||
this._idCounter = 0;
|
||||
|
||||
this._taskLists = [
|
||||
this._createMockTaskList('1', 'List One', "SP.Data.OneListItem"),
|
||||
this._createMockTaskList('2', 'List Two', 'SP.Data.TwoListItem'),
|
||||
this._createMockTaskList('3', 'List Three', 'SP.Data.ThreetListItem')
|
||||
];
|
||||
|
||||
let listOneItems = [
|
||||
this._createMockTodoItem('Sunt filet mignon', true),
|
||||
this._createMockTodoItem('Laborum flank ', false),
|
||||
this._createMockTodoItem('consectetur ex', false)
|
||||
];
|
||||
for (let i = 0; i < 2000; i++) {
|
||||
listOneItems.push(this._createMockTodoItem(this._getRandomString(i % 5), i % 2 == 0));
|
||||
}
|
||||
|
||||
this._items = {
|
||||
'1': listOneItems,
|
||||
'2': [
|
||||
this._createMockTodoItem('Ut custodiant te sermonem', false),
|
||||
this._createMockTodoItem('Dixi sunt implicatae', false),
|
||||
this._createMockTodoItem('Est, ante me factus singulis', true),
|
||||
this._createMockTodoItem('Tu omne quod ille voluit', false)
|
||||
],
|
||||
'3': [
|
||||
this._createMockTodoItem('Integer massa lectus ', true),
|
||||
this._createMockTodoItem('Phasellus sodales ', false),
|
||||
this._createMockTodoItem('finibus porttitor dolor', false),
|
||||
this._createMockTodoItem('Vestibulum at rutrum nisi', true)
|
||||
]
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
public set webPartContext(value: IWebPartContext) {
|
||||
this._webPartContext = value;
|
||||
}
|
||||
|
||||
public get webPartContext(): IWebPartContext {
|
||||
return this._webPartContext;
|
||||
}
|
||||
|
||||
public set selectedList(value: ITaskList) {
|
||||
this._selectedList = value;
|
||||
}
|
||||
|
||||
public get selectedList(): ITaskList {
|
||||
return this._selectedList;
|
||||
}
|
||||
|
||||
public getTaskLists(): Promise<ITaskList[]> {
|
||||
debugger;
|
||||
const taskLists: ITaskList[] = this._taskLists;
|
||||
|
||||
return new Promise<ITaskList[]>((resolve) => {
|
||||
setTimeout(() => resolve(taskLists), 500);
|
||||
});
|
||||
}
|
||||
|
||||
public getItems(): Promise<ITodoItem[]> {
|
||||
|
||||
const items: ITodoItem[] = lodash.clone(this._items[this._selectedList.Id]);
|
||||
|
||||
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.Id] = this._items[this._selectedList.Id].concat(newItem);
|
||||
|
||||
return this.getItems();
|
||||
}
|
||||
|
||||
public updateItem(itemUpdated: ITodoItem): Promise<ITodoItem[]> {
|
||||
const index: number =
|
||||
lodash.findIndex(
|
||||
this._items[this._selectedList.Id],
|
||||
(item: ITodoItem) => item.Id === itemUpdated.Id
|
||||
);
|
||||
|
||||
if (index !== -1) {
|
||||
this._items[this._selectedList.Id][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.Id] = this._items[this._selectedList.Id].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: this._getRandomNumber() * 0.1
|
||||
|
||||
};
|
||||
return mockTodoItem;
|
||||
}
|
||||
|
||||
private _createMockTaskList(id: string, title: string, listItemEntityType: string): ITaskList {
|
||||
const mockTaskList: ITaskList = {
|
||||
Id: id,
|
||||
Title: title,
|
||||
ListItemEntityTypeFullName: listItemEntityType,
|
||||
};
|
||||
return mockTaskList;
|
||||
}
|
||||
|
||||
private _getRandomString(wordCount: number): string {
|
||||
if (wordCount == 0) {
|
||||
wordCount = 2;
|
||||
}
|
||||
var text = "";
|
||||
var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
|
||||
for (var j = 0; j < wordCount; j++) {
|
||||
for (var i = 0; i < 5; i++) {
|
||||
text += possible.charAt(Math.floor(Math.random() * possible.length));
|
||||
}
|
||||
text += " ";
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
private _getRandomNumber(): number {
|
||||
return Math.floor(Math.random() * 9) + 1;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,224 @@
|
|||
import {
|
||||
SPHttpClient,
|
||||
SPHttpClientBatch,
|
||||
SPHttpClientResponse
|
||||
} from '@microsoft/sp-http';
|
||||
import { IWebPartContext } from '@microsoft/sp-webpart-base';
|
||||
import ITodoDataProvider from '../dataProviders/ITodoDataProvider';
|
||||
import { ITodoItem, ITaskList } from '../models/ICommonObjects';
|
||||
import { debounce } from '@microsoft/sp-lodash-subset';
|
||||
|
||||
export default class SharePointDataProvider implements ITodoDataProvider {
|
||||
|
||||
private _selectedList: ITaskList;
|
||||
private _webPartContext: IWebPartContext;
|
||||
|
||||
|
||||
public set webPartContext(value: IWebPartContext) {
|
||||
this._webPartContext = value;
|
||||
}
|
||||
|
||||
public get webPartContext(): IWebPartContext {
|
||||
return this._webPartContext;
|
||||
}
|
||||
|
||||
public set selectedList(value: ITaskList) {
|
||||
this._selectedList = value;
|
||||
}
|
||||
|
||||
public get selectedList(): ITaskList {
|
||||
return this._selectedList;
|
||||
}
|
||||
|
||||
public getTaskLists(): Promise<ITaskList[]> {
|
||||
|
||||
const listTemplateId: string = '171';
|
||||
const queryUrl: string = `${this._webPartContext.pageContext.web.absoluteUrl}/_api/web/lists?$filter=BaseTemplate eq ${listTemplateId}`;
|
||||
|
||||
return this._webPartContext.spHttpClient.get(queryUrl, SPHttpClient.configurations.v1)
|
||||
.then((response: SPHttpClientResponse) => {
|
||||
return response.json();
|
||||
})
|
||||
.then((json: { value: ITaskList[] }) => {
|
||||
return 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(itemToBeDeleted: ITodoItem): Promise<ITodoItem[]> {
|
||||
//Approach 1: it is not working here with DELETE
|
||||
// const batch: SPHttpClientBatch = this.webPartContext.spHttpClient.beginBatch();
|
||||
// const batchPromises: Promise<{}>[] = [
|
||||
// this._deleteItem(batch, itemToBeDeleted),
|
||||
// this._getItemsBatched(batch)
|
||||
// ];
|
||||
// return this._resolveBatch(batch, batchPromises);
|
||||
|
||||
|
||||
//Approach 2:
|
||||
return this._deleteItem2(this.webPartContext.spHttpClient, itemToBeDeleted);
|
||||
}
|
||||
|
||||
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[]> {
|
||||
debugger;
|
||||
const queryUrl: string = `${this._webPartContext.pageContext.web.absoluteUrl}` +
|
||||
`/_api/web/lists(guid'${this._selectedList.Id}')/items?$select=Id,Title,PercentComplete`;
|
||||
|
||||
return requester.get(queryUrl, SPHttpClient.configurations.v1)
|
||||
.then((response: SPHttpClientResponse) => {
|
||||
return response.json();
|
||||
})
|
||||
//Approach 1: Of the property names of ITodoItem are equal to the internal names of the list, this will work
|
||||
// .then((json: { value: ITodoItem[] }) => {
|
||||
// debugger;
|
||||
// return json.value;
|
||||
// // .map((task: ITodoItem) => { debugger; return task; });
|
||||
// });
|
||||
//Approach 2: manually create the ITodoItem object; useful when the properties are different form the internal names of the list
|
||||
.then((json: any) => {
|
||||
debugger;
|
||||
return json.value.map((item: any) => {
|
||||
debugger;
|
||||
let newItem: ITodoItem = {
|
||||
Id: item.Id,
|
||||
Title: item.Title,
|
||||
PercentComplete: item.PercentComplete
|
||||
};
|
||||
return newItem;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private _getItemsBatched(requester: SPHttpClientBatch): Promise<ITodoItem[]> {
|
||||
const queryUrl: string = `${this._webPartContext.pageContext.web.absoluteUrl}` +
|
||||
`/_api/web/lists(guid'${this._selectedList.Id}')/items?$select=Id,Title,PercentComplete`;
|
||||
|
||||
return requester.get(queryUrl, SPHttpClientBatch.configurations.v1)
|
||||
.then((response: SPHttpClientResponse) => {
|
||||
return response.json();
|
||||
})
|
||||
.then((json: { value: ITodoItem[] }) => {
|
||||
debugger;
|
||||
return json.value.map((task: ITodoItem) => {
|
||||
debugger;
|
||||
return task;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private _createItem(batch: SPHttpClientBatch, title: string): Promise<SPHttpClientResponse> {
|
||||
|
||||
const body: {} = {
|
||||
'@data.type': this._selectedList.ListItemEntityTypeFullName,
|
||||
'Title': title
|
||||
};
|
||||
|
||||
return batch.post(
|
||||
`${this._webPartContext.pageContext.web.absoluteUrl}/_api/web/lists(guid'${this._selectedList.Id}')/items`,
|
||||
SPHttpClientBatch.configurations.v1,
|
||||
{ body: JSON.stringify(body) }
|
||||
);
|
||||
}
|
||||
|
||||
private _deleteItem(batch: SPHttpClientBatch, item: ITodoItem): Promise<SPHttpClientResponse> {
|
||||
const itemDeletedUrl: string = `${this._webPartContext.pageContext.web.absoluteUrl}/_api/web/lists(guid'${this._selectedList.Id}')/items(${item.Id})`;
|
||||
|
||||
const headers: Headers = new Headers();
|
||||
headers.append('If-Match', '*');
|
||||
|
||||
return batch.fetch(itemDeletedUrl,
|
||||
SPHttpClientBatch.configurations.v1,
|
||||
{
|
||||
headers,
|
||||
method: 'DELETE'
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private _deleteItem2(requester: SPHttpClient, item: ITodoItem): Promise<ITodoItem[]> {
|
||||
const itemDeletedUrl: string = `${this._webPartContext.pageContext.web.absoluteUrl}/_api/web/lists(guid'${this._selectedList.Id}')/items(${item.Id})`;
|
||||
debugger;
|
||||
const headers: Headers = new Headers();
|
||||
headers.append('If-Match', '*');
|
||||
|
||||
return requester.fetch(itemDeletedUrl,
|
||||
SPHttpClient.configurations.v1,
|
||||
{
|
||||
headers,
|
||||
method: 'DELETE'
|
||||
}
|
||||
).then((response: any) => {
|
||||
debugger;
|
||||
if (response.status >= 200 && response.status < 300) {
|
||||
return response;
|
||||
} else {
|
||||
return Promise.reject(new Error(JSON.stringify(response)));
|
||||
}
|
||||
}).then(() => {
|
||||
return this._getItems(requester);
|
||||
});
|
||||
}
|
||||
|
||||
private _updateItem(batch: SPHttpClientBatch, item: ITodoItem): Promise<SPHttpClientResponse> {
|
||||
|
||||
const itemUpdatedUrl: string = `${this._webPartContext.pageContext.web.absoluteUrl}/_api/web/lists(guid'${this._selectedList.Id}')/items(${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(() => {
|
||||
return Promise.all(promises);
|
||||
}).then((values: any) => {
|
||||
debugger;
|
||||
return Promise.resolve(values[values.length - 1]);
|
||||
// return values[values.length - 1];
|
||||
}).catch((ex) => {
|
||||
debugger;
|
||||
throw ex;
|
||||
});
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
/**
|
||||
* This interface describes a list item in Task list.
|
||||
*/
|
||||
export interface ITodoItem {
|
||||
Id: number;
|
||||
Title: string;
|
||||
PercentComplete: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* This interface describes Task list.
|
||||
*/
|
||||
export interface ITaskList {
|
||||
Id?: string;
|
||||
Title?: string;
|
||||
ListItemEntityTypeFullName?: string;
|
||||
}
|
|
@ -1,4 +1,12 @@
|
|||
import { ITaskList } from '../../models/ICommonObjects';
|
||||
/**
|
||||
* This interface describes the serialized properties of the webpart
|
||||
*/
|
||||
export interface ITodoWebPartProps {
|
||||
message: string;
|
||||
todos: string[];
|
||||
|
||||
/**
|
||||
* Represents the selected Task list
|
||||
*/
|
||||
SelectedList: ITaskList;
|
||||
|
||||
}
|
||||
|
|
|
@ -10,12 +10,10 @@
|
|||
"preconfiguredEntries": [{
|
||||
"groupId": "a0e1eddd-ea67-4775-a52b-0141c5807146",
|
||||
"group": { "default": "Under Development" },
|
||||
"title": { "default": "ToDo" },
|
||||
"description": { "default": "My Todo's" },
|
||||
"officeFabricIconFontName": "Page",
|
||||
"title": { "default": "ToDo Vue.js" },
|
||||
"description": { "default": "My Todo's demo with Vue.js" },
|
||||
"officeFabricIconFontName": "TaskSolid",
|
||||
"properties": {
|
||||
"message": "todos",
|
||||
"todos": []
|
||||
}
|
||||
}]
|
||||
}
|
||||
|
|
|
@ -1,43 +1,104 @@
|
|||
import { Version } from '@microsoft/sp-core-library';
|
||||
import { Version, Environment, EnvironmentType } from '@microsoft/sp-core-library';
|
||||
import {
|
||||
BaseClientSideWebPart,
|
||||
IPropertyPaneConfiguration,
|
||||
PropertyPaneTextField
|
||||
PropertyPaneTextField,
|
||||
PropertyPaneDropdown,
|
||||
IPropertyPaneField,
|
||||
PropertyPaneLabel,
|
||||
IPropertyPaneDropdownOption
|
||||
} from '@microsoft/sp-webpart-base';
|
||||
import { escape } from '@microsoft/sp-lodash-subset';
|
||||
import { findIndex } from '@microsoft/sp-lodash-subset';
|
||||
|
||||
import * as Vue from 'vue';
|
||||
import TodoComponent from './components/todo/Todo.vue';
|
||||
import { ITodoProps } from './components/todo/ITodoProps';
|
||||
|
||||
import * as strings from 'toDoStrings';
|
||||
import { ITodoWebPartProps } from './ITodoWebPartProps';
|
||||
|
||||
import ITodoDataProvider from '../../dataProviders/ITodoDataProvider';
|
||||
import MockDataProvider from '../../dataProviders//MockDataProvider';
|
||||
import SharePointDataProvider from '../../dataProviders/SharePointDataProvider';
|
||||
import { ITaskList } from '../../models/ICommonObjects';
|
||||
import { Utils } from '../../common/Utils';
|
||||
export default class TodoWebPart extends BaseClientSideWebPart<ITodoWebPartProps> {
|
||||
|
||||
public data: ITodoWebPartProps;
|
||||
private _data: ITodoProps;
|
||||
private _dropdownOptions: IPropertyPaneDropdownOption[];
|
||||
private _dataProvider: ITodoDataProvider;
|
||||
private _selectedList: ITaskList;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/*
|
||||
If we have serialized list in the webpart properties, use it
|
||||
*/
|
||||
if (this.properties.SelectedList) {
|
||||
this._dataProvider.selectedList = this.properties.SelectedList;
|
||||
}
|
||||
|
||||
/*
|
||||
Approach 1:
|
||||
Get the list of tasks lists from the current site and store them in the variable _dropdownOptions
|
||||
_dropdownOptions will be used to populate the property pane dropdown field when pane opens.
|
||||
|
||||
Approach 2:
|
||||
Get the list of tasks lists from the current site and load them in the property pane only when the property pane is open.
|
||||
For this approach please review the sample: react-custompropertypanecontrols
|
||||
|
||||
*/
|
||||
this._loadTaskLists()
|
||||
.then(() => {
|
||||
debugger;
|
||||
/*
|
||||
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.SelectedList) {
|
||||
this._setSelectedList(this.properties.SelectedList.Id);
|
||||
this.context.statusRenderer.clearLoadingIndicator(this.domElement);
|
||||
}
|
||||
});
|
||||
|
||||
return super.onInit();
|
||||
}
|
||||
|
||||
public render(): void {
|
||||
|
||||
this.domElement.innerHTML = `
|
||||
<div id="app-${this.context.instanceId}">
|
||||
</div>`;
|
||||
|
||||
this.data = {
|
||||
message: this.properties.message,
|
||||
todos: this.properties.todos
|
||||
this._data = {
|
||||
dataProvider: this._dataProvider,
|
||||
webPartDisplayMode: this.displayMode
|
||||
};
|
||||
|
||||
new Vue({
|
||||
el: `#app-${this.context.instanceId}`,
|
||||
render: h => h(TodoComponent, {
|
||||
props: this.data
|
||||
props: this._data
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
public onBeforeSerialize(): any {
|
||||
this.properties.message = this.data.message;
|
||||
this.properties.todos = this.data.todos;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
protected get dataVersion(): Version {
|
||||
|
@ -55,8 +116,10 @@ export default class TodoWebPart extends BaseClientSideWebPart<ITodoWebPartProps
|
|||
{
|
||||
groupName: strings.BasicGroupName,
|
||||
groupFields: [
|
||||
PropertyPaneTextField('message', {
|
||||
label: strings.DescriptionFieldLabel
|
||||
PropertyPaneDropdown('selectedList', {
|
||||
label: "Select a list",
|
||||
disabled: this._disableDropdown,
|
||||
options: this._dropdownOptions
|
||||
})
|
||||
]
|
||||
}
|
||||
|
@ -65,4 +128,64 @@ export default class TodoWebPart extends BaseClientSideWebPart<ITodoWebPartProps
|
|||
]
|
||||
};
|
||||
}
|
||||
|
||||
protected onPropertyPaneFieldChanged(propertyPath: string, oldValue: any, newValue: any): void {
|
||||
debugger;
|
||||
/*
|
||||
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 === 'selectedList') {
|
||||
this._setSelectedList(newValue);
|
||||
}
|
||||
|
||||
/*
|
||||
Tell property pane to re-render the web part.
|
||||
This is valid for reactive property pane.
|
||||
*/
|
||||
super.onPropertyPaneFieldChanged(propertyPath, oldValue, newValue);
|
||||
}
|
||||
|
||||
private _loadTaskLists(): Promise<any> {
|
||||
debugger;
|
||||
return this._dataProvider.getTaskLists()
|
||||
.then((taskLists: ITaskList[]) => {
|
||||
debugger;
|
||||
// Disable dropdown field if there are no results from the server.
|
||||
this._disableDropdown = taskLists.length === 0;
|
||||
let utiility: Utils = new Utils();
|
||||
if (taskLists.length !== 0) {
|
||||
this._dropdownOptions = taskLists.map((list: ITaskList) => {
|
||||
return {
|
||||
key: utiility.GetKeyFroDropdown(list),
|
||||
text: list.Title
|
||||
};
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _setSelectedList(value: string) {
|
||||
debugger;
|
||||
|
||||
const selectedIndex: number = findIndex(this._dropdownOptions,
|
||||
(item: IPropertyPaneDropdownOption) => item.key === value
|
||||
);
|
||||
|
||||
const selectedDropDownOption: IPropertyPaneDropdownOption = this._dropdownOptions[selectedIndex];
|
||||
let utiility: Utils = new Utils();
|
||||
|
||||
if (selectedDropDownOption) {
|
||||
this._selectedList = {
|
||||
Title: selectedDropDownOption.text,
|
||||
Id: utiility.GetListId(selectedDropDownOption),
|
||||
ListItemEntityTypeFullName: utiility.GetListItemEntityType(selectedDropDownOption)
|
||||
};
|
||||
|
||||
this._dataProvider.selectedList = this._selectedList;
|
||||
|
||||
this.properties.SelectedList = this._selectedList;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
li {
|
||||
font-size: 30px;
|
||||
}
|
||||
.completed
|
||||
{
|
||||
text-decoration: line-through;
|
||||
}
|
|
@ -1,12 +1,34 @@
|
|||
import * as Vue from 'vue';
|
||||
import { Component, Prop } from 'vue-property-decorator';
|
||||
import { ITodoItem } from '../../../../models/ICommonObjects';
|
||||
|
||||
@Component
|
||||
export default class TodoItem extends Vue {
|
||||
@Prop()
|
||||
public todoText: string;
|
||||
|
||||
@Prop()
|
||||
public todoItem: ITodoItem;
|
||||
|
||||
/*
|
||||
This method is attached to the checkbox click
|
||||
*/
|
||||
public onComplete(): void {
|
||||
this.$emit('completed', this.todoText);
|
||||
|
||||
//// Approach 2: update the property PercentComplete when the checkbox is clicked
|
||||
// if (this.todoItem.PercentComplete == 1) {
|
||||
// this.todoItem.PercentComplete = 0;
|
||||
// }
|
||||
// else {
|
||||
// this.todoItem.PercentComplete = 1;
|
||||
// }
|
||||
|
||||
this.$emit('completed', this.todoItem);
|
||||
}
|
||||
|
||||
/*
|
||||
This is a system Vue.js hook. Added here only for demonstrations
|
||||
*/
|
||||
public created(): void {
|
||||
debugger;
|
||||
console.log(this.todoItem.Title + " | " + this.todoItem.Id);
|
||||
}
|
||||
}
|
|
@ -1,14 +1,24 @@
|
|||
<template>
|
||||
<li>
|
||||
<div class="view">
|
||||
<input type="checkbox" @click="onComplete">
|
||||
<label>{{todoText}}</label>
|
||||
<!-- Approach 1: delete item when the checkbox is clicked -->
|
||||
<!-- <input id="" type="checkbox" @click="onComplete" /> -->
|
||||
|
||||
<!-- Approach 2: update the property PercentComplete when the checkbox is clicked -->
|
||||
<span v-if="todoItem.PercentComplete === 1">
|
||||
<input type="checkbox" @click="onComplete" checked />
|
||||
</span>
|
||||
<span v-else>
|
||||
<input type="checkbox" @click="onComplete" />
|
||||
</span>
|
||||
<label v-bind:class="[ todoItem.PercentComplete == 1 ? 'completed':'' ]" >
|
||||
{{todoItem.Title}}</label>
|
||||
<label> | {{todoItem.PercentComplete * 100}} % </label>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
module.exports = require('./TodoItem');
|
||||
module.exports = require("./TodoItem");
|
||||
</script>
|
||||
|
||||
<style scoped src="./todoitem.scss" lang="scss"></style>
|
|
@ -0,0 +1,9 @@
|
|||
import ITodoDataProvider from '../../../../dataProviders/ITodoDataProvider';
|
||||
import { DisplayMode } from '@microsoft/sp-core-library';
|
||||
|
||||
|
||||
export interface ITodoProps {
|
||||
dataProvider: ITodoDataProvider;
|
||||
webPartDisplayMode: DisplayMode;
|
||||
|
||||
}
|
|
@ -18,3 +18,20 @@ ul{
|
|||
width: 300px;
|
||||
}
|
||||
}
|
||||
.notification{
|
||||
text-align: center;
|
||||
padding-top: 50px;
|
||||
}
|
||||
.notificationIcon{
|
||||
font-size: 20px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.notificationHeader
|
||||
{
|
||||
font-size: 20px;
|
||||
}
|
||||
.notificationDescription
|
||||
{
|
||||
font-size: 18px;
|
||||
margin-top: 20px;
|
||||
}
|
|
@ -1,35 +1,103 @@
|
|||
import * as Vue from 'vue';
|
||||
import { Component, Prop } from 'vue-property-decorator';
|
||||
import TodoItem from '../todo-item/Todoitem.vue';
|
||||
import { ITodoWebPartProps } from '../../ITodoWebPartProps';
|
||||
import { ITodoProps } from './ITodoProps';
|
||||
import ITodoDataProvider from '../../../../dataProviders/ITodoDataProvider';
|
||||
import { DisplayMode } from '@microsoft/sp-core-library';
|
||||
import { ITodoItem } from '../../../../models/ICommonObjects';
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
'todo-item': TodoItem
|
||||
}
|
||||
})
|
||||
export default class Todo extends Vue implements ITodoWebPartProps {
|
||||
export default class Todo extends Vue implements ITodoProps {
|
||||
@Prop()
|
||||
public message: string;
|
||||
public dataProvider: ITodoDataProvider;
|
||||
@Prop()
|
||||
public todos: string[];
|
||||
public webPartDisplayMode: DisplayMode;
|
||||
|
||||
public mytodos: ITodoItem[] = [];
|
||||
|
||||
public mytodos: string[] = this.todos;
|
||||
public todoTitle: string = '';
|
||||
|
||||
public isLoading = true;
|
||||
/*
|
||||
This method is attached to the textbox whne the Enter key is hitted
|
||||
*/
|
||||
public addTodo(): void {
|
||||
|
||||
debugger;
|
||||
if (!this.todoTitle) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.mytodos.push(this.todoTitle);
|
||||
if (this.dataProvider.selectedList) {
|
||||
|
||||
this.dataProvider.createItem(this.todoTitle).then(
|
||||
(allItems: ITodoItem[]) => {
|
||||
debugger;
|
||||
if (allItems && allItems.length > 0) {
|
||||
this.mytodos = allItems;
|
||||
}
|
||||
else {
|
||||
this.mytodos = [];
|
||||
}
|
||||
this.todoTitle = '';
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public completed(todo: string): void {
|
||||
const index: number = this.mytodos.indexOf(todo, 0);
|
||||
if (index > -1) {
|
||||
this.mytodos.splice(index, 1);
|
||||
/*
|
||||
This method is triggered from the child componenets 'todo-item'
|
||||
*/
|
||||
public completed(todo: ITodoItem): void {
|
||||
debugger;
|
||||
if (this.dataProvider.selectedList) {
|
||||
|
||||
//// Approach 1: delete item when the checkbox is clicked
|
||||
this.dataProvider.deleteItem(todo).then(
|
||||
(allItems: ITodoItem[]) => {
|
||||
debugger;
|
||||
if (allItems && allItems.length > 0) {
|
||||
this.mytodos = allItems;
|
||||
}
|
||||
else {
|
||||
this.mytodos = [];
|
||||
}
|
||||
this.todoTitle = '';
|
||||
});
|
||||
|
||||
//// Approach 2: update the property PercentComplete when the checkbox is clicked
|
||||
// this.dataProvider.updateItem(todo).then(
|
||||
// (allItems: ITodoItem[]) => {
|
||||
// debugger;
|
||||
// if (allItems && allItems.length > 0) {
|
||||
// this.mytodos = allItems;
|
||||
// this.todoTitle = '';
|
||||
// }
|
||||
// });
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
This is a system Vue.js hook. It is used here to communicate with SharePoint and get list items
|
||||
*/
|
||||
public created(): void {
|
||||
debugger;
|
||||
if (this.dataProvider.selectedList) {
|
||||
|
||||
this.dataProvider.getItems().then(
|
||||
(results: ITodoItem[]) => {
|
||||
debugger;
|
||||
if (results && results.length > 0) {
|
||||
this.mytodos = results;
|
||||
}
|
||||
this.isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -1,17 +1,53 @@
|
|||
<template>
|
||||
<div>
|
||||
<h1>{{message}}</h1>
|
||||
<div v-if="webPartDisplayMode === 1">
|
||||
<span v-if="dataProvider.selectedList!= null">
|
||||
<div id="new-todo">
|
||||
<input type="text" @keyup.enter="addTodo" v-model="todoTitle" placeholder="what needs to be done?">
|
||||
</div>
|
||||
<ul>
|
||||
<todo-item v-for="(todo, index) in mytodos" :key="index + todo" :todoText="todo" v-on:completed="completed"></todo-item>
|
||||
<todo-item v-for="item in mytodos" :key="item.Id"
|
||||
:todoText="item.Title"
|
||||
:todoId="item.Id"
|
||||
:todoItem="item"
|
||||
v-on:completed="completed">
|
||||
</todo-item>
|
||||
</ul>
|
||||
</span>
|
||||
<span v-else>
|
||||
<div class="notification">
|
||||
<div class="notificationIcon">
|
||||
<i class="ms-Icon ms-Icon--ErrorBadge notificationIcon" aria-hidden="true"></i>
|
||||
<span class="notificationHeader">
|
||||
WebPart is not configured
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="webPartDisplayMode === 2">
|
||||
<div class="notification">
|
||||
<div class="notificationIcon">
|
||||
<i class="ms-Icon ms-Icon--ErrorBadge notificationIcon" aria-hidden="true"></i>
|
||||
<span class="notificationHeader">
|
||||
Edit Mode
|
||||
</span>
|
||||
</div>
|
||||
<div class="notificationDescription">
|
||||
<span v-if="dataProvider.selectedList!= null">
|
||||
Selected List: {{dataProvider.selectedList.Title}}
|
||||
</span>
|
||||
<span v-else>
|
||||
WebPart is not configured. Please select Task list.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
module.exports = require('./Todo');
|
||||
module.exports = require("./Todo");
|
||||
</script>
|
||||
|
||||
<style scoped src="./todo.scss" lang="scss"></style>
|
|
@ -9,6 +9,8 @@
|
|||
"experimentalDecorators": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"types": [
|
||||
"es6-promise",
|
||||
"es6-collections",
|
||||
"webpack-env"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -6,3 +6,6 @@
|
|||
block will not be included in the final bundle when the
|
||||
--ship flag is specified */
|
||||
declare const UNIT_TEST: boolean;
|
||||
|
||||
/* Global defintion for SPO builds */
|
||||
declare const DATACENTER: boolean;
|
Loading…
Reference in New Issue