Add CRUD operations. (#368)

Extend the sample so it uses SharePoint data.
This commit is contained in:
Dimcho Tsanov 2017-12-01 16:43:13 +02:00 committed by Vesa Juvonen
parent 874f0ebd8f
commit 9c9807c4fd
19 changed files with 819 additions and 70 deletions

View File

@ -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.**

View File

@ -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",

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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;
});
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -10,12 +10,10 @@
"preconfiguredEntries": [{
"groupId": "a0e1eddd-ea67-4775-a52b-0141c5807146",
"group": { "default": "Under Development" },
"title": { "default": "ToDo" },
"description": { "default": "My Todo&#39;s" },
"officeFabricIconFontName": "Page",
"title": { "default": "ToDo Vue.js" },
"description": { "default": "My Todo&#39;s demo with Vue.js" },
"officeFabricIconFontName": "TaskSolid",
"properties": {
"message": "todos",
"todos": []
}
}]
}

View File

@ -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;
}
}
}

View File

@ -1,3 +1,7 @@
li {
font-size: 30px;
}
.completed
{
text-decoration: line-through;
}

View File

@ -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);
}
}

View File

@ -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>

View File

@ -0,0 +1,9 @@
import ITodoDataProvider from '../../../../dataProviders/ITodoDataProvider';
import { DisplayMode } from '@microsoft/sp-core-library';
export interface ITodoProps {
dataProvider: ITodoDataProvider;
webPartDisplayMode: DisplayMode;
}

View File

@ -17,4 +17,21 @@ ul{
border: 1px solid #d6d4d4;
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;
}

View File

@ -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 {
if(!this.todoTitle){
debugger;
if (!this.todoTitle) {
return;
}
this.mytodos.push(this.todoTitle);
this.todoTitle = '';
}
if (this.dataProvider.selectedList) {
public completed(todo: string): void {
const index: number = this.mytodos.indexOf(todo, 0);
if (index > -1) {
this.mytodos.splice(index, 1);
this.dataProvider.createItem(this.todoTitle).then(
(allItems: ITodoItem[]) => {
debugger;
if (allItems && allItems.length > 0) {
this.mytodos = allItems;
}
else {
this.mytodos = [];
}
this.todoTitle = '';
});
}
}
/*
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;
});
}
}
}

View File

@ -1,17 +1,53 @@
<template>
<div>
<h1>{{message}}</h1>
<div id="new-todo">
<input type="text" @keyup.enter="addTodo" v-model="todoTitle" placeholder="what needs to be done?">
<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="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>
<ul>
<todo-item v-for="(todo, index) in mytodos" :key="index + todo" :todoText="todo" v-on:completed="completed"></todo-item>
</ul>
<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>

View File

@ -9,7 +9,9 @@
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"types": [
"es6-promise",
"es6-collections",
"webpack-env"
]
}
}
}

View File

@ -5,4 +5,7 @@
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;
declare const UNIT_TEST: boolean;
/* Global defintion for SPO builds */
declare const DATACENTER: boolean;