From 9c9807c4fd158365d1d89ebe7ef1971795595e89 Mon Sep 17 00:00:00 2001 From: Dimcho Tsanov Date: Fri, 1 Dec 2017 16:43:13 +0200 Subject: [PATCH] Add CRUD operations. (#368) Extend the sample so it uses SharePoint data. --- .../README.md | 5 +- .../package.json | 35 +-- .../src/common/Utils.ts | 30 +++ .../src/dataProviders/ITodoDataProvider.ts | 21 ++ .../src/dataProviders/MockDataProvider.ts | 153 ++++++++++++ .../dataProviders/SharePointDataProvider.ts | 224 ++++++++++++++++++ .../src/models/ICommonObjects.ts | 17 ++ .../src/webparts/todo/ITodoWebPartProps.ts | 12 +- .../webparts/todo/TodoWebPart.manifest.json | 8 +- .../src/webparts/todo/TodoWebPart.ts | 153 ++++++++++-- .../todo/components/todo-item/TodoItem.scss | 4 + .../todo/components/todo-item/TodoItem.ts | 28 ++- .../todo/components/todo-item/Todoitem.vue | 18 +- .../todo/components/todo/ITodoProps.ts | 9 + .../webparts/todo/components/todo/Todo.scss | 19 +- .../src/webparts/todo/components/todo/Todo.ts | 94 +++++++- .../webparts/todo/components/todo/Todo.vue | 50 +++- .../tsconfig.json | 4 +- .../typings/@ms/odsp.d.ts | 5 +- 19 files changed, 819 insertions(+), 70 deletions(-) create mode 100644 samples/vuejs-todo-single-file-component/src/common/Utils.ts create mode 100644 samples/vuejs-todo-single-file-component/src/dataProviders/ITodoDataProvider.ts create mode 100644 samples/vuejs-todo-single-file-component/src/dataProviders/MockDataProvider.ts create mode 100644 samples/vuejs-todo-single-file-component/src/dataProviders/SharePointDataProvider.ts create mode 100644 samples/vuejs-todo-single-file-component/src/models/ICommonObjects.ts create mode 100644 samples/vuejs-todo-single-file-component/src/webparts/todo/components/todo/ITodoProps.ts diff --git a/samples/vuejs-todo-single-file-component/README.md b/samples/vuejs-todo-single-file-component/README.md index 879e853a7..ce835fc73 100644 --- a/samples/vuejs-todo-single-file-component/README.md +++ b/samples/vuejs-todo-single-file-component/README.md @@ -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.** diff --git a/samples/vuejs-todo-single-file-component/package.json b/samples/vuejs-todo-single-file-component/package.json index 2feea9056..0e98c7a7f 100644 --- a/samples/vuejs-todo-single-file-component/package.json +++ b/samples/vuejs-todo-single-file-component/package.json @@ -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", diff --git a/samples/vuejs-todo-single-file-component/src/common/Utils.ts b/samples/vuejs-todo-single-file-component/src/common/Utils.ts new file mode 100644 index 000000000..6244c60d5 --- /dev/null +++ b/samples/vuejs-todo-single-file-component/src/common/Utils.ts @@ -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; + } +} \ No newline at end of file diff --git a/samples/vuejs-todo-single-file-component/src/dataProviders/ITodoDataProvider.ts b/samples/vuejs-todo-single-file-component/src/dataProviders/ITodoDataProvider.ts new file mode 100644 index 000000000..733df89d2 --- /dev/null +++ b/samples/vuejs-todo-single-file-component/src/dataProviders/ITodoDataProvider.ts @@ -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; + + getItems(): Promise; + + createItem(title: string): Promise; + + updateItem(itemUpdated: ITodoItem): Promise; + + deleteItem(itemDeleted: ITodoItem): Promise; +} + +export default ITodoDataProvider; \ No newline at end of file diff --git a/samples/vuejs-todo-single-file-component/src/dataProviders/MockDataProvider.ts b/samples/vuejs-todo-single-file-component/src/dataProviders/MockDataProvider.ts new file mode 100644 index 000000000..606b5cd4f --- /dev/null +++ b/samples/vuejs-todo-single-file-component/src/dataProviders/MockDataProvider.ts @@ -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 { + debugger; + const taskLists: ITaskList[] = this._taskLists; + + return new Promise((resolve) => { + setTimeout(() => resolve(taskLists), 500); + }); + } + + public getItems(): Promise { + + const items: ITodoItem[] = lodash.clone(this._items[this._selectedList.Id]); + + return new Promise((resolve) => { + setTimeout(() => resolve(items), 500); + }); + } + + public createItem(title: string): Promise { + 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 { + 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 { + 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; + } +} diff --git a/samples/vuejs-todo-single-file-component/src/dataProviders/SharePointDataProvider.ts b/samples/vuejs-todo-single-file-component/src/dataProviders/SharePointDataProvider.ts new file mode 100644 index 000000000..4aad2dc7b --- /dev/null +++ b/samples/vuejs-todo-single-file-component/src/dataProviders/SharePointDataProvider.ts @@ -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 { + + 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 { + return this._getItems(this.webPartContext.spHttpClient); + } + + public createItem(title: string): Promise { + 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 { + //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 { + 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 { + 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 { + 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 { + + 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 { + 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 { + 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 { + + 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 { + 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; + }); + + } +} diff --git a/samples/vuejs-todo-single-file-component/src/models/ICommonObjects.ts b/samples/vuejs-todo-single-file-component/src/models/ICommonObjects.ts new file mode 100644 index 000000000..d7a7df840 --- /dev/null +++ b/samples/vuejs-todo-single-file-component/src/models/ICommonObjects.ts @@ -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; +} \ No newline at end of file diff --git a/samples/vuejs-todo-single-file-component/src/webparts/todo/ITodoWebPartProps.ts b/samples/vuejs-todo-single-file-component/src/webparts/todo/ITodoWebPartProps.ts index d6c233708..37293aae6 100644 --- a/samples/vuejs-todo-single-file-component/src/webparts/todo/ITodoWebPartProps.ts +++ b/samples/vuejs-todo-single-file-component/src/webparts/todo/ITodoWebPartProps.ts @@ -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; + } diff --git a/samples/vuejs-todo-single-file-component/src/webparts/todo/TodoWebPart.manifest.json b/samples/vuejs-todo-single-file-component/src/webparts/todo/TodoWebPart.manifest.json index 83c5600a2..c90fa999b 100644 --- a/samples/vuejs-todo-single-file-component/src/webparts/todo/TodoWebPart.manifest.json +++ b/samples/vuejs-todo-single-file-component/src/webparts/todo/TodoWebPart.manifest.json @@ -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": [] } }] } diff --git a/samples/vuejs-todo-single-file-component/src/webparts/todo/TodoWebPart.ts b/samples/vuejs-todo-single-file-component/src/webparts/todo/TodoWebPart.ts index 27c1577df..4125c2ec7 100644 --- a/samples/vuejs-todo-single-file-component/src/webparts/todo/TodoWebPart.ts +++ b/samples/vuejs-todo-single-file-component/src/webparts/todo/TodoWebPart.ts @@ -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 { - public data: ITodoWebPartProps; + private _data: ITodoProps; + private _dropdownOptions: IPropertyPaneDropdownOption[]; + private _dataProvider: ITodoDataProvider; + private _selectedList: ITaskList; + + private _disableDropdown: boolean; + + + protected onInit(): Promise { + + 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 = `
`; - 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 { + 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; + } + } + } diff --git a/samples/vuejs-todo-single-file-component/src/webparts/todo/components/todo-item/TodoItem.scss b/samples/vuejs-todo-single-file-component/src/webparts/todo/components/todo-item/TodoItem.scss index 378863bef..7caae4f93 100644 --- a/samples/vuejs-todo-single-file-component/src/webparts/todo/components/todo-item/TodoItem.scss +++ b/samples/vuejs-todo-single-file-component/src/webparts/todo/components/todo-item/TodoItem.scss @@ -1,3 +1,7 @@ li { font-size: 30px; + } + .completed + { + text-decoration: line-through; } \ No newline at end of file diff --git a/samples/vuejs-todo-single-file-component/src/webparts/todo/components/todo-item/TodoItem.ts b/samples/vuejs-todo-single-file-component/src/webparts/todo/components/todo-item/TodoItem.ts index 253319d7a..da5caf078 100644 --- a/samples/vuejs-todo-single-file-component/src/webparts/todo/components/todo-item/TodoItem.ts +++ b/samples/vuejs-todo-single-file-component/src/webparts/todo/components/todo-item/TodoItem.ts @@ -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); } } \ No newline at end of file diff --git a/samples/vuejs-todo-single-file-component/src/webparts/todo/components/todo-item/Todoitem.vue b/samples/vuejs-todo-single-file-component/src/webparts/todo/components/todo-item/Todoitem.vue index 2192c4797..c01f329e3 100644 --- a/samples/vuejs-todo-single-file-component/src/webparts/todo/components/todo-item/Todoitem.vue +++ b/samples/vuejs-todo-single-file-component/src/webparts/todo/components/todo-item/Todoitem.vue @@ -1,14 +1,24 @@ - \ No newline at end of file diff --git a/samples/vuejs-todo-single-file-component/src/webparts/todo/components/todo/ITodoProps.ts b/samples/vuejs-todo-single-file-component/src/webparts/todo/components/todo/ITodoProps.ts new file mode 100644 index 000000000..8e442b4b4 --- /dev/null +++ b/samples/vuejs-todo-single-file-component/src/webparts/todo/components/todo/ITodoProps.ts @@ -0,0 +1,9 @@ +import ITodoDataProvider from '../../../../dataProviders/ITodoDataProvider'; +import { DisplayMode } from '@microsoft/sp-core-library'; + + +export interface ITodoProps { + dataProvider: ITodoDataProvider; + webPartDisplayMode: DisplayMode; + +} \ No newline at end of file diff --git a/samples/vuejs-todo-single-file-component/src/webparts/todo/components/todo/Todo.scss b/samples/vuejs-todo-single-file-component/src/webparts/todo/components/todo/Todo.scss index 8c3132dae..ab61eefb2 100644 --- a/samples/vuejs-todo-single-file-component/src/webparts/todo/components/todo/Todo.scss +++ b/samples/vuejs-todo-single-file-component/src/webparts/todo/components/todo/Todo.scss @@ -17,4 +17,21 @@ ul{ border: 1px solid #d6d4d4; width: 300px; } -} \ No newline at end of file +} +.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; + } \ No newline at end of file diff --git a/samples/vuejs-todo-single-file-component/src/webparts/todo/components/todo/Todo.ts b/samples/vuejs-todo-single-file-component/src/webparts/todo/components/todo/Todo.ts index 89032f39e..6fce94d27 100644 --- a/samples/vuejs-todo-single-file-component/src/webparts/todo/components/todo/Todo.ts +++ b/samples/vuejs-todo-single-file-component/src/webparts/todo/components/todo/Todo.ts @@ -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; + }); + } + } + + } \ No newline at end of file diff --git a/samples/vuejs-todo-single-file-component/src/webparts/todo/components/todo/Todo.vue b/samples/vuejs-todo-single-file-component/src/webparts/todo/components/todo/Todo.vue index dc5f06cb6..91af34997 100644 --- a/samples/vuejs-todo-single-file-component/src/webparts/todo/components/todo/Todo.vue +++ b/samples/vuejs-todo-single-file-component/src/webparts/todo/components/todo/Todo.vue @@ -1,17 +1,53 @@ \ No newline at end of file diff --git a/samples/vuejs-todo-single-file-component/tsconfig.json b/samples/vuejs-todo-single-file-component/tsconfig.json index 63a68fc77..9a5b79001 100644 --- a/samples/vuejs-todo-single-file-component/tsconfig.json +++ b/samples/vuejs-todo-single-file-component/tsconfig.json @@ -9,7 +9,9 @@ "experimentalDecorators": true, "allowSyntheticDefaultImports": true, "types": [ + "es6-promise", + "es6-collections", "webpack-env" ] } -} \ No newline at end of file +} diff --git a/samples/vuejs-todo-single-file-component/typings/@ms/odsp.d.ts b/samples/vuejs-todo-single-file-component/typings/@ms/odsp.d.ts index 2d2913e53..5a2404000 100644 --- a/samples/vuejs-todo-single-file-component/typings/@ms/odsp.d.ts +++ b/samples/vuejs-todo-single-file-component/typings/@ms/odsp.d.ts @@ -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; \ No newline at end of file +declare const UNIT_TEST: boolean; + +/* Global defintion for SPO builds */ +declare const DATACENTER: boolean; \ No newline at end of file