Merge pull request #1366 from petkir/petkir-IE11Support-1288
This commit is contained in:
commit
d383a4dde8
|
@ -2,11 +2,11 @@
|
||||||
"@microsoft/generator-sharepoint": {
|
"@microsoft/generator-sharepoint": {
|
||||||
"isCreatingSolution": true,
|
"isCreatingSolution": true,
|
||||||
"environment": "spo",
|
"environment": "spo",
|
||||||
"version": "1.8.2",
|
"version": "1.10.0",
|
||||||
"libraryName": "react-kanban-board",
|
"libraryName": "react-kanban-board",
|
||||||
"libraryId": "cccbd72b-7b89-4128-9348-0a4850ded8fd",
|
"libraryId": "cccbd72b-7b89-4128-9348-0a4850ded8fd",
|
||||||
"packageManager": "npm",
|
"packageManager": "npm",
|
||||||
"isDomainIsolated": false,
|
"isDomainIsolated": false,
|
||||||
"componentType": "webpart"
|
"componentType": "webpart"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,32 @@
|
||||||
|
---
|
||||||
|
page_type: sample
|
||||||
|
products:
|
||||||
|
- office-sp
|
||||||
|
languages:
|
||||||
|
- javascript
|
||||||
|
- typescript
|
||||||
|
extensions:
|
||||||
|
contentType: samples
|
||||||
|
technologies:
|
||||||
|
- SharePoint Framework
|
||||||
|
platforms:
|
||||||
|
- React
|
||||||
|
createdDate: 7/17/2019 12:00:00 AM
|
||||||
|
---
|
||||||
|
|
||||||
# React Kanban Board Webpart
|
# React Kanban Board Webpart
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
This solution contains an SPFx webpart which shows a Kanban board using jqxKanban ReactJS component (from [JQWidgets](https://www.jqwidgets.com/jquery-widgets-documentation/documentation/jqxkanban/jquery-kanban-getting-started.htm?search=kanban)).
|
|
||||||
|
This solution contains an SPFx webpart that shows a kanban board using Office UI Fabric components ([Office UI Fabric](https://developer.microsoft.com/fluentui/)).
|
||||||
The webpart uses the default columns of the SharePoint Tasks list for showing the board's columns and the tasks.
|
The webpart uses the default columns of the SharePoint Tasks list for showing the board's columns and the tasks.
|
||||||
|
|
||||||
![picture of the web part in action](assets/kanban-board.gif)
|
![picture of the web part in action](assets/kanbanofficeUI.gif)
|
||||||
|
|
||||||
## Used SharePoint Framework Version
|
## Used SharePoint Framework Version
|
||||||
|
|
||||||
![drop](https://img.shields.io/badge/version-1.8.2-green.svg)
|
![1.10.0](https://img.shields.io/badge/version-1.10.0-green.svg)
|
||||||
|
|
||||||
## Applies to
|
## Applies to
|
||||||
|
|
||||||
|
@ -26,7 +43,7 @@ This webpart reads the information from a Tasks list and uses the following OOB
|
||||||
* Priority
|
* Priority
|
||||||
* Task Status
|
* Task Status
|
||||||
|
|
||||||
The Task list can be chosen using the webpart properties
|
The Task list can be chosen using the webpart properties (BaseTemplate 171 or 107)
|
||||||
|
|
||||||
## Solution
|
## Solution
|
||||||
|
|
||||||
|
@ -34,6 +51,7 @@ Solution|Author(s)
|
||||||
--------|---------
|
--------|---------
|
||||||
react-kanban-board | [Ram](https://twitter.com/ram_meenavalli)
|
react-kanban-board | [Ram](https://twitter.com/ram_meenavalli)
|
||||||
react-kanban-board | Daniel Westerdale ([Westerdale Solutions Ltd.](https://westerdale.blog), [@westerdaled](https://twitter.com/westerdaled?s=20))
|
react-kanban-board | Daniel Westerdale ([Westerdale Solutions Ltd.](https://westerdale.blog), [@westerdaled](https://twitter.com/westerdaled?s=20))
|
||||||
|
react-kanban-board | Peter Paul Kirschner ([@petkir_at](https://twitter.com/petkir_at))
|
||||||
|
|
||||||
## Version history
|
## Version history
|
||||||
|
|
||||||
|
@ -41,12 +59,31 @@ Version|Date|Comments
|
||||||
-------|----|--------
|
-------|----|--------
|
||||||
1.0.0.0|July 17, 2019|Initial release
|
1.0.0.0|July 17, 2019|Initial release
|
||||||
1.0.1.0|April 21, 2020|Added support for Teams hosts
|
1.0.1.0|April 21, 2020|Added support for Teams hosts
|
||||||
|
2.0.0.0|July 10, 2020| jqwidgets replaced with a custom Kanban Board based on Office UI Component and IE11 Support
|
||||||
|
|
||||||
|
[Read More about the implementation of this Board](./src/kanban/README.md)
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
* PNP Placeholder control if not Configured
|
||||||
|
* PNP WebpartTitle control (toggle Show/Hide in property pane)
|
||||||
|
* PNP OrderPropertyPane control (change position of buckets)
|
||||||
|
* PNP ListSelectionPropertyPane control (including filtering on BaseTemplateId)
|
||||||
|
* Usage of BucketEdit in Pane (Use a component in property pane (custom field))
|
||||||
|
* Office UI Fabric
|
||||||
|
* PNP JS DataConnection to SharePoint
|
||||||
|
|
||||||
|
|
||||||
|
<!---Thanks from @petkir to: -->
|
||||||
|
<!--- -->
|
||||||
|
<!---* [Daniel Westerdale](https://github.com/westerdaled) for Testing and inspiration (everytime again)-->
|
||||||
|
<!---* [Hugo Bernier](https://github.com/hugoabernier) for Inspiration to use Office UI Fabric -->
|
||||||
|
<!---* [Jean-Philippe CIVADE](https://github.com/ewidance) for Bug Report IE11 (initiator of rewrite of this sample)-->
|
||||||
|
<!---* [RamPrasadMeenavalli](https://github.com/RamPrasadMeenavalli) for the initial Idea-->
|
||||||
|
|
||||||
## Disclaimer
|
## 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.**
|
**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.**
|
||||||
|
|
||||||
**THIS WEBPART USES jQWidgets FOR SHOWING THE KANBAN BOARD. jQWidgets IS FREE TO USE UNDER THE CREATIVE COMMONS ATTRIBUTION-NONCOMMERCIAL 3.0 LICENSE. FOR COMMERCIAL USE, PLEASE CHECK THE [LICENSING TERMS](https://www.jqwidgets.com/license/) FOR jQWidgets**
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -60,8 +97,8 @@ Version|Date|Comments
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
This sample highlights the following concepts
|
This sample highlights the following concepts
|
||||||
* Binding SharePoint list data to jqxKanban React Component
|
* Binding SharePoint list data to a custom Kanban-Control
|
||||||
* Updating SharePoint List Items based on events from the jqxKanban board
|
* Updating SharePoint List Items based on events from the custom Kanban-Control
|
||||||
|
|
||||||
When a task is moved to different columns in the Kanban Board, the status of the respective SharePoint list item is updated using PnP JS
|
When a task is moved to different columns in the Kanban Board, the status of the respective SharePoint list item is updated using PnP JS
|
||||||
|
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 3.8 MiB |
Binary file not shown.
After Width: | Height: | Size: 2.1 MiB |
|
@ -13,6 +13,9 @@
|
||||||
},
|
},
|
||||||
"externals": {},
|
"externals": {},
|
||||||
"localizedResources": {
|
"localizedResources": {
|
||||||
"KanbanBoardWebPartStrings": "lib/webparts/kanbanBoard/loc/{locale}.js"
|
"KanbanBoardStrings": "lib/kanban/loc/{locale}.js",
|
||||||
|
"KanbanBoardWebPartStrings": "lib/webparts/kanbanBoard/loc/{locale}.js",
|
||||||
|
"ControlStrings": "node_modules/@pnp/spfx-controls-react/lib/loc/{locale}.js",
|
||||||
|
"PropertyControlStrings": "node_modules/@pnp/spfx-property-controls/lib/loc/{locale}.js"
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,14 +1,14 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
|
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
|
||||||
"solution": {
|
"solution": {
|
||||||
"name": "react-kanban-board-client-side-solution",
|
"name": "react-kanban-board-client-side-solution",
|
||||||
"id": "cccbd72b-7b89-4128-9348-0a4850ded8fd",
|
"id": "cccbd72b-7b89-4128-9348-0a4850ded8fd",
|
||||||
"version": "1.0.0.0",
|
"version": "2.0.0.0",
|
||||||
"includeClientSideAssets": true,
|
"includeClientSideAssets": true,
|
||||||
"skipFeatureDeployment": true,
|
"skipFeatureDeployment": true,
|
||||||
"isDomainIsolated": false
|
"isDomainIsolated": false
|
||||||
},
|
},
|
||||||
"paths": {
|
"paths": {
|
||||||
"zippedPackage": "solution/react-kanban-board.sppkg"
|
"zippedPackage": "solution/react-kanban-board.sppkg"
|
||||||
}
|
}
|
||||||
}
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -1,6 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "react-kanban-board",
|
"name": "react-kanban-board",
|
||||||
"version": "1.0.1",
|
"main": "lib/index.js",
|
||||||
|
"version": "2.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
|
@ -11,36 +12,41 @@
|
||||||
"test": "gulp test"
|
"test": "gulp test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@microsoft/sp-core-library": "1.8.2",
|
"@microsoft/sp-core-library": "1.10.0",
|
||||||
"@microsoft/sp-lodash-subset": "1.8.2",
|
"@microsoft/sp-lodash-subset": "1.10.0",
|
||||||
"@microsoft/sp-office-ui-fabric-core": "1.8.2",
|
"@microsoft/sp-office-ui-fabric-core": "1.10.0",
|
||||||
"@microsoft/sp-property-pane": "1.8.2",
|
"@microsoft/sp-property-pane": "1.10.0",
|
||||||
"@microsoft/sp-webpart-base": "1.8.2",
|
"@microsoft/sp-webpart-base": "1.10.0",
|
||||||
"@pnp/common": "^1.3.3",
|
"@pnp/common": "^1.3.3",
|
||||||
"@pnp/logging": "^1.3.3",
|
"@pnp/logging": "^1.3.3",
|
||||||
"@pnp/odata": "^1.3.3",
|
"@pnp/odata": "^1.3.3",
|
||||||
|
"@pnp/polyfill-ie11": "^2.0.2",
|
||||||
"@pnp/sp": "^1.3.3",
|
"@pnp/sp": "^1.3.3",
|
||||||
|
"@pnp/spfx-controls-react": "1.19.0",
|
||||||
|
"@pnp/spfx-property-controls": "1.19.0",
|
||||||
"@types/es6-promise": "0.0.33",
|
"@types/es6-promise": "0.0.33",
|
||||||
"@types/react": "16.7.22",
|
"@types/react": "16.8.8",
|
||||||
"@types/react-dom": "16.8.0",
|
"@types/react-dom": "16.8.3",
|
||||||
"@types/webpack-env": "1.13.1",
|
"@types/webpack-env": "1.13.1",
|
||||||
"jqwidgets-scripts": "^8.1.2",
|
"classnames": "^2.2.6",
|
||||||
"office-ui-fabric-react": "^6.143.0",
|
"office-ui-fabric-react": "6.214.0",
|
||||||
"react": "16.7.0",
|
"react": "16.8.5",
|
||||||
"react-dom": "16.7.0"
|
"react-dom": "16.8.5"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"@types/react": "16.7.22"
|
"@types/react": "16.8.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@microsoft/rush-stack-compiler-2.9": "0.7.7",
|
"@microsoft/rush-stack-compiler-2.9": "0.7.7",
|
||||||
"@microsoft/sp-build-web": "1.8.2",
|
"@microsoft/rush-stack-compiler-3.3": "0.3.5",
|
||||||
"@microsoft/sp-module-interfaces": "1.8.2",
|
"@microsoft/sp-build-web": "1.10.0",
|
||||||
"@microsoft/sp-tslint-rules": "1.8.2",
|
"@microsoft/sp-module-interfaces": "1.10.0",
|
||||||
"@microsoft/sp-webpart-workbench": "1.8.2",
|
"@microsoft/sp-tslint-rules": "1.10.0",
|
||||||
|
"@microsoft/sp-webpart-workbench": "1.10.0",
|
||||||
"@types/chai": "3.4.34",
|
"@types/chai": "3.4.34",
|
||||||
"@types/mocha": "2.2.38",
|
"@types/mocha": "2.2.38",
|
||||||
"ajv": "~5.2.2",
|
"ajv": "~5.2.2",
|
||||||
|
"autoprefixer": "^9.8.4",
|
||||||
"gulp": "~3.9.1",
|
"gulp": "~3.9.1",
|
||||||
"react-html-parser": "^2.0.2"
|
"react-html-parser": "^2.0.2"
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
import {IKanbanTask} from './IKanbanTask';
|
||||||
|
import { IKanbanBucket } from './IKanbanBucket';
|
||||||
|
|
||||||
|
export interface IKanbanBoardRenderers{
|
||||||
|
task?: (task:IKanbanTask) => JSX.Element ;
|
||||||
|
bucketHeadline?: (bucket:IKanbanBucket) => JSX.Element ;
|
||||||
|
taskDetail?: (task:IKanbanTask) => JSX.Element ;
|
||||||
|
/*
|
||||||
|
its an action not a renderer
|
||||||
|
taskEdit?: (task:IKanbanTask) => JSX.Element ;
|
||||||
|
taskAdd?: (bucket?:IKanbanBucket) => JSX.Element ;
|
||||||
|
*/
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { IKanbanBucket } from "./IKanbanBucket";
|
||||||
|
import { IKanbanTask } from "./IKanbanTask";
|
||||||
|
|
||||||
|
export interface IKanbanBoardTaskActions {
|
||||||
|
|
||||||
|
toggleCompleted?: (taskId: string) => void;
|
||||||
|
allowMove?: (taskId: string, prevBucket: IKanbanBucket, targetBucket: IKanbanBucket) => boolean;
|
||||||
|
moved?: (taskId: string, targetBucket: IKanbanBucket) => void;
|
||||||
|
/* think about Await???
|
||||||
|
*/
|
||||||
|
addTaskSaved?: (task: IKanbanTask) => void;
|
||||||
|
editTaskSaved?: (task: IKanbanTask) => void;
|
||||||
|
//deleteTask?: (task: IKanbanTask) => void;
|
||||||
|
|
||||||
|
taskEdit?: (task:IKanbanTask) => void ;
|
||||||
|
taskAdd?: (bucket?:IKanbanBucket) => void ;
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
export interface IKanbanBoardTaskSettings {
|
||||||
|
showPriority: boolean;
|
||||||
|
showAssignedTo: boolean;
|
||||||
|
showTaskDetailsButton: boolean;
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
export interface IKanbanBucket {
|
||||||
|
bucket:string;
|
||||||
|
bucketheadline:string;
|
||||||
|
percentageComplete: number;
|
||||||
|
color?:string;
|
||||||
|
allowAddTask?:boolean;
|
||||||
|
showPercentageHeadline?:boolean;
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { IPersonaProps } from "office-ui-fabric-react/lib/Persona";
|
||||||
|
|
||||||
|
export interface IKanbanTask {
|
||||||
|
taskId: string;
|
||||||
|
title: string;
|
||||||
|
isCompleted?: boolean;
|
||||||
|
assignedTo?: IPersonaProps;
|
||||||
|
htmlDescription?:string;
|
||||||
|
priority?:string;
|
||||||
|
bucket: string;
|
||||||
|
mamagedProperties?: IKanbanTaskManagedProps[];
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IKanbanTaskManagedProps {
|
||||||
|
name: string;
|
||||||
|
displayName?: string;
|
||||||
|
type: KanbanTaskMamagedPropertyType;
|
||||||
|
value: string | number | IPersonaProps | IPersonaProps[] | any;
|
||||||
|
renderer?: (name: string, value: object, type: KanbanTaskMamagedPropertyType) => JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 0 is bad because
|
||||||
|
const value = EnumType.xyz // = 0
|
||||||
|
if(value) {is false}
|
||||||
|
*/
|
||||||
|
export enum KanbanTaskMamagedPropertyType {
|
||||||
|
string = 1,
|
||||||
|
number = 2,
|
||||||
|
percent = 3,
|
||||||
|
html = 4,
|
||||||
|
person = 5,
|
||||||
|
persons = 6,
|
||||||
|
complex = 7
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
.bucket {
|
||||||
|
width: 100%;
|
||||||
|
// border-right: 1px solid gray;
|
||||||
|
.headline {
|
||||||
|
padding: 2px 10px;
|
||||||
|
line-height: 2em;
|
||||||
|
font-weight: 700;
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
text-align: center;
|
||||||
|
border-bottom: 1px solid gray;
|
||||||
|
.headlineText{
|
||||||
|
width: calc( 100% - 10px);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.colorindicator {
|
||||||
|
width: 10px;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0px;
|
||||||
|
}
|
||||||
|
.processIndicatorHeight {
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.taskArea {
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
.placeholder {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.taskplaceholder {
|
||||||
|
position: relative;
|
||||||
|
background-color: transparent;
|
||||||
|
cursor: move;
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
.placeholder::after {
|
||||||
|
content:'x';
|
||||||
|
position:absolute;
|
||||||
|
top:0;
|
||||||
|
width:100%;
|
||||||
|
height:100%;
|
||||||
|
background-color:blue;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
|
@ -0,0 +1,104 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import styles from './KanbanBucket.module.scss';
|
||||||
|
import { IKanbanBucket } from './IKanbanBucket';
|
||||||
|
import { IKanbanTask } from './IKanbanTask';
|
||||||
|
import { IKanbanBoardTaskSettings } from './IKanbanBoardTaskSettings';
|
||||||
|
import { IKanbanBoardTaskActions } from './IKanbanBoardTaskActions';
|
||||||
|
import { ProgressIndicator } from 'office-ui-fabric-react/lib/ProgressIndicator';
|
||||||
|
import { ActionButton } from 'office-ui-fabric-react';
|
||||||
|
import KanbanTask from './KanbanTask';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import * as strings from 'KanbanBoardStrings';
|
||||||
|
|
||||||
|
export interface IKanbanBucketProps extends IKanbanBucket {
|
||||||
|
|
||||||
|
buckettasks: IKanbanTask[];
|
||||||
|
tasksettings: IKanbanBoardTaskSettings;
|
||||||
|
|
||||||
|
|
||||||
|
toggleCompleted?: (taskId: string) => void;
|
||||||
|
addTask?: (bucket: string) => void;
|
||||||
|
|
||||||
|
onDragStart: (event, taskId: string, bucket: string) => void;
|
||||||
|
|
||||||
|
onDragEnd: (event, taskId: string, bucket: string) => void;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
leavingTaskId?: string;
|
||||||
|
leavingBucket?: string;
|
||||||
|
overBucket?: string;
|
||||||
|
hasOneProcessIndicator: boolean;
|
||||||
|
openDetails?: (taskId: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IKanbanBucketState { }
|
||||||
|
|
||||||
|
export default class KanbanBucket extends React.Component<IKanbanBucketProps, IKanbanBucketState> {
|
||||||
|
|
||||||
|
constructor(props: IKanbanBucketProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
nice to use a object merge
|
||||||
|
ECMAScript 2018 Standard Method
|
||||||
|
{...t, ...tasksettings, ...taskactions}
|
||||||
|
hope this will be translated in IE
|
||||||
|
*/
|
||||||
|
public render(): React.ReactElement<IKanbanBucketProps> {
|
||||||
|
const { bucket, bucketheadline, color, buckettasks,
|
||||||
|
tasksettings, percentageComplete,
|
||||||
|
allowAddTask, showPercentageHeadline, leavingTaskId, leavingBucket,hasOneProcessIndicator } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={styles.bucket}
|
||||||
|
key={bucket}>
|
||||||
|
<div className={styles.headline}>
|
||||||
|
<div className={styles.headlineText}>{bucketheadline}</div>
|
||||||
|
{color && <div style={{ backgroundColor: color }} className={styles.colorindicator}></div>}
|
||||||
|
{showPercentageHeadline ?
|
||||||
|
(<ProgressIndicator percentComplete={percentageComplete / 100} />):
|
||||||
|
(hasOneProcessIndicator?(<div className={styles.processIndicatorHeight}></div>):(<div></div>))}
|
||||||
|
</div>
|
||||||
|
{allowAddTask && (<ActionButton
|
||||||
|
iconProps={{ iconName: 'Add' }}
|
||||||
|
allowDisabledFocus={true}
|
||||||
|
onClick={() => this.props.addTask(bucket)}
|
||||||
|
>
|
||||||
|
{strings.AddTask}
|
||||||
|
</ActionButton>)}
|
||||||
|
<div className={styles.taskArea}>
|
||||||
|
{
|
||||||
|
buckettasks.map((t) => {
|
||||||
|
const merge = { ...t, ...tasksettings, };
|
||||||
|
const isMoving = (t.taskId === leavingTaskId && t.bucket === leavingBucket);
|
||||||
|
|
||||||
|
return (<div
|
||||||
|
className={styles.taskplaceholder + (isMoving ? styles.placeholder : '')}
|
||||||
|
key={'' + t.taskId} >
|
||||||
|
<KanbanTask
|
||||||
|
key={'task' + t.taskId}
|
||||||
|
{...merge}
|
||||||
|
toggleCompleted={this.props.toggleCompleted}
|
||||||
|
isMoving={isMoving}
|
||||||
|
openDetails={this.props.openDetails}
|
||||||
|
onDragStart={(event) => this.props.onDragStart(event, t.taskId, t.bucket)}
|
||||||
|
onDragEnd={(event) => this.props.onDragEnd(event, t.taskId, t.bucket)}
|
||||||
|
/></div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div >
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,162 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
//import styles from './KanbanBucketConfigurator.module.scss';
|
||||||
|
|
||||||
|
import * as strings from 'KanbanBoardStrings';
|
||||||
|
import { TextField, MaskedTextField } from 'office-ui-fabric-react/lib/TextField';
|
||||||
|
import { Stack, IStackProps, IStackStyles } from 'office-ui-fabric-react/lib/Stack';
|
||||||
|
import { Slider } from 'office-ui-fabric-react/lib/Slider';
|
||||||
|
import { Toggle } from 'office-ui-fabric-react/lib/Toggle';
|
||||||
|
import { cloneDeep, clone, isEqual } from '@microsoft/sp-lodash-subset';
|
||||||
|
import {
|
||||||
|
ColorPicker,
|
||||||
|
ChoiceGroup,
|
||||||
|
IChoiceGroupOption,
|
||||||
|
getColorFromString,
|
||||||
|
IColor,
|
||||||
|
IColorPickerStyles,
|
||||||
|
IColorPickerProps,
|
||||||
|
PrimaryButton,
|
||||||
|
DefaultButton,
|
||||||
|
ThemeSettingName,
|
||||||
|
} from 'office-ui-fabric-react/lib/index';
|
||||||
|
|
||||||
|
import { IKanbanBucket } from './IKanbanBucket';
|
||||||
|
|
||||||
|
|
||||||
|
export interface IKanbanBucketConfiguratorProps {
|
||||||
|
index: number;
|
||||||
|
bucket: IKanbanBucket;
|
||||||
|
update: (index: number, value: IKanbanBucket) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IKanbanBucketConfiguratorState {
|
||||||
|
bucket?: IKanbanBucket;
|
||||||
|
// showHeadline: boolean;
|
||||||
|
useColor: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class KanbanBucketConfigurator extends React.Component<IKanbanBucketConfiguratorProps, IKanbanBucketConfiguratorState> {
|
||||||
|
|
||||||
|
constructor(props: IKanbanBucketConfiguratorProps) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
// showHeadline: false,
|
||||||
|
useColor: false
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
public componentDidMount(): void {
|
||||||
|
this.resetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentDidUpdate(prevProps: IKanbanBucketConfiguratorProps): void {
|
||||||
|
if (prevProps.bucket && this.props.bucket && !isEqual(this.props.bucket, prevProps.bucket)) {
|
||||||
|
this.resetState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public render(): React.ReactElement<IKanbanBucketConfiguratorProps> {
|
||||||
|
/*
|
||||||
|
const columnProps: Partial<IStackProps> = {
|
||||||
|
gap: 15,
|
||||||
|
styles: { root: { width: 300 } },
|
||||||
|
};*/
|
||||||
|
/*
|
||||||
|
const colorPickerStyles: Partial<IColorPickerStyles> = {
|
||||||
|
panel: { padding: 12 },
|
||||||
|
root: {
|
||||||
|
maxWidth: 352,
|
||||||
|
minWidth: 352,
|
||||||
|
},
|
||||||
|
colorRectangle: { height: 268 },
|
||||||
|
};*/
|
||||||
|
const statebucket = this.state.bucket;
|
||||||
|
if (!statebucket) {
|
||||||
|
return (<div></div>);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Stack>
|
||||||
|
<TextField label={strings.BucketConfigInternalName} disabled defaultValue={statebucket.bucket} />
|
||||||
|
{/* <Toggle label={strings.BucketConfigUseCustomHeadline} onText="On" offText="Off" inlineLabel
|
||||||
|
checked={this.state.showHeadline}
|
||||||
|
onChange={(ev, checked) => { this.setState({ showHeadline: checked }); }} />
|
||||||
|
*/}
|
||||||
|
<TextField label={strings.BucketConfigHeadline} defaultValue={statebucket.bucketheadline}
|
||||||
|
onChange={(ev, value?: string) => {
|
||||||
|
const bucket = clone(this.state.bucket);
|
||||||
|
bucket.bucketheadline = value;
|
||||||
|
this.setState({ bucket: bucket });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Toggle label={strings.BucketConfigShowPercentage} onText={strings.BucketConfigShowPercentageShow} offText={strings.BucketConfigShowPercentageHide} inlineLabel
|
||||||
|
checked={statebucket.showPercentageHeadline}
|
||||||
|
onChange={(ev, checked) => {
|
||||||
|
const bucket = clone(this.state.bucket);
|
||||||
|
bucket.showPercentageHeadline = checked;
|
||||||
|
this.setState({ bucket: bucket });
|
||||||
|
}} />
|
||||||
|
{statebucket.showPercentageHeadline && <Slider
|
||||||
|
label={strings.BucketConfigPercentageComplete}
|
||||||
|
max={100}
|
||||||
|
value={statebucket.percentageComplete}
|
||||||
|
ariaValueText={(value: number) => `${value} ${strings.Percent}`}
|
||||||
|
valueFormat={(value: number) => `${value}%`}
|
||||||
|
showValue
|
||||||
|
onChange={(value: number) => {
|
||||||
|
const bucket = clone(this.state.bucket);
|
||||||
|
bucket.percentageComplete = value;
|
||||||
|
this.setState({ bucket: bucket });
|
||||||
|
}}
|
||||||
|
/>}
|
||||||
|
<Toggle label={strings.BucketConfigUseColor} onText="On" offText="Off" inlineLabel
|
||||||
|
checked={this.state.useColor}
|
||||||
|
onChange={(ev, checked) => { this.setState({ useColor: checked }); }} />
|
||||||
|
{this.state.useColor && (<ColorPicker
|
||||||
|
|
||||||
|
color={statebucket.color}
|
||||||
|
|
||||||
|
// alphaSliderHidden={false}
|
||||||
|
// showPreview={true}
|
||||||
|
onChange={(ev: any, colorObj: IColor) => {
|
||||||
|
const bucket = clone(this.state.bucket);
|
||||||
|
bucket.color = colorObj.str;
|
||||||
|
this.setState({ bucket: bucket });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Stack>
|
||||||
|
|
||||||
|
<PrimaryButton text={strings.BucketConfigSave} onClick={this.submitData.bind(this)} />
|
||||||
|
<DefaultButton text={strings.BucketConfigReset} onClick={this.resetState.bind(this)} />
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private resetState(): void {
|
||||||
|
const newbucket: IKanbanBucket = clone(this.props.bucket);
|
||||||
|
this.setState({
|
||||||
|
bucket: newbucket,
|
||||||
|
// showHeadline: newbucket.bucketheadline && newbucket.bucketheadline.length > 0,
|
||||||
|
useColor: newbucket.color && newbucket.color.length > 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
private submitData(): void {
|
||||||
|
const newbucket: IKanbanBucket = clone(this.state.bucket);
|
||||||
|
if (!this.state.useColor) {
|
||||||
|
newbucket.color = undefined;
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
if (!this.state.showHeadline) {
|
||||||
|
newbucket.color = undefined;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
if (this.props.update) {
|
||||||
|
this.props.update(this.props.index, newbucket);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
.kanbanBoard {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
.bucketwrapper {
|
||||||
|
border: 1px solid transparent;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
|
// min-width: 220px;
|
||||||
|
}
|
||||||
|
.dragover {
|
||||||
|
border-color: "[theme: themePrimary, default: #0078d7]";
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,432 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import styles from './KanbanComponent.module.scss';
|
||||||
|
import bucketstyles from './KanbanBucket.module.scss';
|
||||||
|
import * as strings from 'KanbanBoardStrings';
|
||||||
|
|
||||||
|
import { IKanbanTask, KanbanTaskMamagedPropertyType } from './IKanbanTask';
|
||||||
|
import { IKanbanBoardTaskSettings } from './IKanbanBoardTaskSettings';
|
||||||
|
import { IKanbanBoardTaskActions } from './IKanbanBoardTaskActions';
|
||||||
|
import { IKanbanBoardRenderers } from './IKanbanBoardRenderers';
|
||||||
|
import { IKanbanBucket } from './IKanbanBucket';
|
||||||
|
import KanbanBucket from './KanbanBucket';
|
||||||
|
import KanbanTaskManagedProp from './KanbanTaskManagedProp';
|
||||||
|
|
||||||
|
import { Dialog, DialogType, DialogFooter } from 'office-ui-fabric-react/lib/Dialog';
|
||||||
|
import { PrimaryButton, DefaultButton } from 'office-ui-fabric-react/lib/Button';
|
||||||
|
import { IStackStyles, Stack } from 'office-ui-fabric-react/lib/Stack';
|
||||||
|
import { clone } from '@microsoft/sp-lodash-subset';
|
||||||
|
|
||||||
|
import { CommandBar } from 'office-ui-fabric-react/lib/CommandBar';
|
||||||
|
|
||||||
|
import { TooltipHost, findIndex } from 'office-ui-fabric-react';
|
||||||
|
|
||||||
|
export interface IKanbanComponentProps {
|
||||||
|
buckets: IKanbanBucket[];
|
||||||
|
tasks: IKanbanTask[];
|
||||||
|
tasksettings: IKanbanBoardTaskSettings;
|
||||||
|
taskactions: IKanbanBoardTaskActions;
|
||||||
|
showCommandbar?: boolean;
|
||||||
|
renderers?: IKanbanBoardRenderers;
|
||||||
|
allowEdit?: boolean;
|
||||||
|
allowAdd?: boolean;
|
||||||
|
editSchema?: boolean;
|
||||||
|
/*
|
||||||
|
showCommandbarNew: boolean;
|
||||||
|
allowDialog: boolean; TODO im mock
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IKanbanComponentState {
|
||||||
|
leavingTaskId?: string;
|
||||||
|
leavingBucket?: string;
|
||||||
|
openDialog: boolean;
|
||||||
|
openTaskId?: string;
|
||||||
|
dialogState?: DialogState;
|
||||||
|
editTask?: IKanbanTask;
|
||||||
|
addBucket?: IKanbanBucket;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum DialogState {
|
||||||
|
New = 1,
|
||||||
|
Edit = 2,
|
||||||
|
Display = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
export class KanbanComponent extends React.Component<IKanbanComponentProps, IKanbanComponentState> {
|
||||||
|
private dragelement?: IKanbanTask;
|
||||||
|
private bucketsref: any[];
|
||||||
|
constructor(props: IKanbanComponentProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
openDialog: false,
|
||||||
|
leavingTaskId: null,
|
||||||
|
leavingBucket: null,
|
||||||
|
|
||||||
|
};
|
||||||
|
this.bucketsref = [];
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public render(): React.ReactElement<IKanbanComponentProps> {
|
||||||
|
const { buckets, tasks, tasksettings, taskactions, showCommandbar } = this.props;
|
||||||
|
const { openDialog } = this.state;
|
||||||
|
const bucketwidth: number = buckets.length > 0 ? 100 / buckets.length : 100;
|
||||||
|
const { leavingBucket, leavingTaskId } = this.state;
|
||||||
|
const hasprocessIndicator = buckets.filter((b)=> b.showPercentageHeadline).length >0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ overflowX: 'auto' }}>
|
||||||
|
{showCommandbar && <CommandBar
|
||||||
|
items={this.getItems()}
|
||||||
|
|
||||||
|
farItems={this.getFarItems()}
|
||||||
|
ariaLabel={'Use left and right arrow keys to navigate between commands'}
|
||||||
|
/>}
|
||||||
|
<div className={styles.kanbanBoard}>
|
||||||
|
{
|
||||||
|
|
||||||
|
buckets.map((b, i) => {
|
||||||
|
const merge = { ...b, ...this.state };
|
||||||
|
return (<div
|
||||||
|
|
||||||
|
style={{
|
||||||
|
flexBasis: bucketwidth ? bucketwidth + '%' : '100%' ,
|
||||||
|
maxWidth: bucketwidth ? bucketwidth + '%' : '100%'
|
||||||
|
}}
|
||||||
|
|
||||||
|
className={styles.bucketwrapper}
|
||||||
|
ref={bucketContent => this.bucketsref[i] = bucketContent}
|
||||||
|
key={'BucketWrapper' + b.bucket + i}
|
||||||
|
onDragOver={(event) => this.onDragOver(event, b.bucket)}
|
||||||
|
onDragLeave={(event) => this.onDragLeave(event, b.bucket)}
|
||||||
|
onDrop={(event) => this.onDrop(event, b.bucket)}
|
||||||
|
>
|
||||||
|
<KanbanBucket
|
||||||
|
key={b.bucket}
|
||||||
|
{...merge}
|
||||||
|
hasOneProcessIndicator={hasprocessIndicator}
|
||||||
|
buckettasks={tasks.filter((x) => x.bucket == b.bucket)}
|
||||||
|
tasksettings={tasksettings}
|
||||||
|
|
||||||
|
toggleCompleted={this.props.taskactions && this.props.taskactions.toggleCompleted ? this.props.taskactions.toggleCompleted : undefined}
|
||||||
|
|
||||||
|
addTask={this.internalAddTask.bind(this)}
|
||||||
|
openDetails={this.internalOpenDialog.bind(this)}
|
||||||
|
|
||||||
|
|
||||||
|
onDragStart={this.onDragStart.bind(this)}
|
||||||
|
onDragEnd={this.onDragEnd.bind(this)}
|
||||||
|
/>
|
||||||
|
</div>);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{openDialog && (this.renderDialog())}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
private getTaskByID(taskId: string): IKanbanTask {
|
||||||
|
const tasks = this.props.tasks.filter(t => t.taskId == this.state.openTaskId);
|
||||||
|
if (tasks.length == 1) {
|
||||||
|
return tasks[0];
|
||||||
|
}
|
||||||
|
throw "Error Taks not found by taskId";
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderDialog(): JSX.Element {
|
||||||
|
let renderer: (task?: IKanbanTask, bucket?: IKanbanBucket) => JSX.Element = () => (<div>Dialog Renderer Not Set</div>);
|
||||||
|
let task: IKanbanTask = undefined;
|
||||||
|
let bucket: IKanbanBucket = undefined;
|
||||||
|
let dialogheadline: string = '';
|
||||||
|
switch (this.state.dialogState) {
|
||||||
|
case DialogState.Edit:
|
||||||
|
task = this.getTaskByID(this.state.openTaskId);
|
||||||
|
renderer = this.internalTaskEditRenderer.bind(this);
|
||||||
|
dialogheadline = strings.EditTaskDlgHeadline;
|
||||||
|
break;
|
||||||
|
case DialogState.New:
|
||||||
|
renderer = this.internalTaskAddRenderer.bind(this);
|
||||||
|
dialogheadline = strings.AddTaskDlgHeadline;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
task = this.getTaskByID(this.state.openTaskId);
|
||||||
|
dialogheadline = task.title;
|
||||||
|
renderer = (this.props.renderers && this.props.renderers.taskDetail) ? this.props.renderers.taskDetail : this.internalTaskDetailRenderer.bind(this);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (<Dialog
|
||||||
|
minWidth={600}
|
||||||
|
hidden={!this.state.openDialog}
|
||||||
|
onDismiss={this.internalCloseDialog.bind(this)}
|
||||||
|
dialogContentProps={{
|
||||||
|
type: DialogType.largeHeader,
|
||||||
|
title: dialogheadline,
|
||||||
|
subText: ''
|
||||||
|
}}
|
||||||
|
modalProps={{
|
||||||
|
isBlocking: false,
|
||||||
|
styles: { main: { minWidth: 600 } }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{renderer(task, bucket)}
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
{(this.props.allowEdit && this.state.dialogState === DialogState.Display) &&
|
||||||
|
(<PrimaryButton onClick={this.clickEditTask.bind(this)} text={strings.EditTaskBtn} />)}
|
||||||
|
{(this.props.allowEdit && this.state.dialogState === DialogState.Edit) &&
|
||||||
|
(<PrimaryButton onClick={this.saveEditTask.bind(this)} text={strings.SaveTaskBtn} />)}
|
||||||
|
{(this.props.allowAdd && this.state.dialogState === DialogState.New) &&
|
||||||
|
(<PrimaryButton onClick={this.saveAddTask.bind(this)} text={strings.SaveAddTaskBtn} />)}
|
||||||
|
<DefaultButton onClick={this.internalCloseDialog.bind(this)} text={strings.CloseTaskDialog} />
|
||||||
|
</DialogFooter>
|
||||||
|
|
||||||
|
</Dialog>);
|
||||||
|
}
|
||||||
|
|
||||||
|
private clickEditTask(): void {
|
||||||
|
const task = this.getTaskByID(this.state.openTaskId);
|
||||||
|
if (this.props.taskactions.taskEdit) {
|
||||||
|
|
||||||
|
this.internalCloseDialog();
|
||||||
|
this.props.taskactions.taskEdit(clone(task));
|
||||||
|
} else {
|
||||||
|
this.setState({
|
||||||
|
dialogState: DialogState.Edit,
|
||||||
|
editTask: clone(task)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private saveEditTask() {
|
||||||
|
if (this.props.taskactions.editTaskSaved) {
|
||||||
|
const edittask = clone(this.state.editTask);
|
||||||
|
//check fist state and than event or in the other way
|
||||||
|
this.internalCloseDialog();
|
||||||
|
this.props.taskactions.editTaskSaved(edittask);
|
||||||
|
} else {
|
||||||
|
throw "allowEdit is Set but no handler is set";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private saveAddTask() {
|
||||||
|
|
||||||
|
if (this.props.taskactions.editTaskSaved) {
|
||||||
|
const edittask = clone(this.state.editTask);
|
||||||
|
//check fist state and than event or in the other way
|
||||||
|
this.internalCloseDialog();
|
||||||
|
this.props.taskactions.editTaskSaved(edittask);
|
||||||
|
} else {
|
||||||
|
throw "allowAdd is Set but no handler is set";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private internalTaskDetailRenderer(task: IKanbanTask): JSX.Element {
|
||||||
|
const { tasksettings } = this.props;
|
||||||
|
return (<Stack>
|
||||||
|
{tasksettings && tasksettings.showPriority && (
|
||||||
|
<KanbanTaskManagedProp
|
||||||
|
name="assignedTo"
|
||||||
|
displayName={strings.Priority}
|
||||||
|
type={KanbanTaskMamagedPropertyType.string}
|
||||||
|
value={task.priority}
|
||||||
|
key={'assignedToProp'} />
|
||||||
|
)}
|
||||||
|
{tasksettings && tasksettings.showAssignedTo && (<KanbanTaskManagedProp
|
||||||
|
name="assignedTo"
|
||||||
|
displayName={strings.AssignedTo}
|
||||||
|
type={KanbanTaskMamagedPropertyType.person}
|
||||||
|
value={task.assignedTo}
|
||||||
|
key={'assignedToProp'} />
|
||||||
|
)}
|
||||||
|
<KanbanTaskManagedProp
|
||||||
|
name="assignedTo"
|
||||||
|
displayName={strings.HtmlDescription}
|
||||||
|
type={KanbanTaskMamagedPropertyType.html}
|
||||||
|
value={task.htmlDescription}
|
||||||
|
key={'htmlDescriptionProp'} />
|
||||||
|
|
||||||
|
{task.mamagedProperties && (
|
||||||
|
task.mamagedProperties.map((p, i) => {
|
||||||
|
return (
|
||||||
|
<KanbanTaskManagedProp {...p} key={p.name + i} />
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private internalTaskEditRenderer(task: IKanbanTask): JSX.Element {
|
||||||
|
const schema = this.props.editSchema; //TODO
|
||||||
|
return (<div>Edit</div>);
|
||||||
|
}
|
||||||
|
private internalTaskAddRenderer(task?: IKanbanTask, bucket?: IKanbanBucket): JSX.Element {
|
||||||
|
const schema = this.props.editSchema; //TODO
|
||||||
|
return (<div>New</div>);
|
||||||
|
}
|
||||||
|
|
||||||
|
private internalCloseDialog(ev?: React.MouseEvent<HTMLButtonElement>) {
|
||||||
|
this.setState({
|
||||||
|
openDialog: false,
|
||||||
|
openTaskId: undefined,
|
||||||
|
dialogState: undefined,
|
||||||
|
editTask: undefined,
|
||||||
|
addBucket: undefined
|
||||||
|
});
|
||||||
|
}
|
||||||
|
private internalOpenDialog(taskid: string) {
|
||||||
|
this.setState({
|
||||||
|
openDialog: true,
|
||||||
|
openTaskId: taskid,
|
||||||
|
dialogState: DialogState.Display
|
||||||
|
});
|
||||||
|
}
|
||||||
|
private internalAddTask(targetbucket?: string) {
|
||||||
|
let bucket: IKanbanBucket = undefined;
|
||||||
|
if (bucket) {
|
||||||
|
const buckets = this.props.buckets.filter((p) => p.bucket === targetbucket);
|
||||||
|
if (buckets.length === 1) {
|
||||||
|
bucket = clone(buckets[0]);
|
||||||
|
} else {
|
||||||
|
throw "Bucket not Found in addDialog";
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.props.taskactions && this.props.taskactions.taskAdd) {
|
||||||
|
this.props.taskactions.taskAdd(bucket);
|
||||||
|
} else {
|
||||||
|
this.setState({
|
||||||
|
openDialog: true,
|
||||||
|
openTaskId: '',
|
||||||
|
dialogState: DialogState.New,
|
||||||
|
addBucket: bucket
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private onDragLeave(event, bucket): void {
|
||||||
|
const index = findIndex(this.props.buckets, element => element.bucket == bucket);
|
||||||
|
if (index != -1 && this.bucketsref.length > index) {
|
||||||
|
|
||||||
|
//&& this.bucketsref[index].classList.contains(styles.dragover)) {
|
||||||
|
this.bucketsref[index].classList.remove(styles.dragover);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private onDragEnd(event): void {
|
||||||
|
|
||||||
|
this.dragelement = undefined;
|
||||||
|
this.setState({
|
||||||
|
leavingTaskId: null,
|
||||||
|
leavingBucket: null,
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private onDragStart(event, taskId: string, bucket: string): void {
|
||||||
|
const taskitem = this.props.tasks.filter(p => p.taskId === taskId);
|
||||||
|
if (taskitem.length === 1) {
|
||||||
|
event.dataTransfer.setData("text", taskId);
|
||||||
|
event.dataTransfer.effectAllowed = 'copy';
|
||||||
|
//event.dataTransfer.setData("sourcebucket", bucket);
|
||||||
|
//set element because event.dataTransfer is empty by DragOver
|
||||||
|
this.dragelement = taskitem[0];
|
||||||
|
this.setState({
|
||||||
|
leavingTaskId: taskId,
|
||||||
|
leavingBucket: bucket,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Error not consitent
|
||||||
|
throw "TaskItem not found on DragStart";
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private onDragOver(event, targetbucket: string): void {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (this.dragelement.bucket !== targetbucket) {
|
||||||
|
const index = findIndex(this.props.buckets, element => element.bucket == targetbucket);
|
||||||
|
if (index > -1 && this.bucketsref.length > index) {
|
||||||
|
//&& this.bucketsref[index].classList.contains(styles.dragover)) {
|
||||||
|
this.bucketsref[index].classList.add(styles.dragover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private onDrop(event, targetbucket: string): void {
|
||||||
|
if (this.bucketsref && this.bucketsref.length > 0) {
|
||||||
|
this.bucketsref.forEach(x => { x.classList.remove(styles.dragover); });
|
||||||
|
}
|
||||||
|
if (this.dragelement.bucket !== targetbucket) {
|
||||||
|
//event.dataTransfer.getData("text");
|
||||||
|
const taskId = this.dragelement.taskId;
|
||||||
|
const source = this.props.buckets.filter(s => s.bucket == this.dragelement.bucket)[0];
|
||||||
|
const target = this.props.buckets.filter(s => s.bucket == targetbucket)[0];
|
||||||
|
|
||||||
|
if (this.props.taskactions) {
|
||||||
|
let allowMove = true;
|
||||||
|
if (this.props.taskactions.allowMove) {
|
||||||
|
allowMove = this.props.taskactions.allowMove(taskId,
|
||||||
|
source,
|
||||||
|
target
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (allowMove && this.props.taskactions.moved) {
|
||||||
|
this.props.taskactions.moved(taskId, target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.dragelement = null;
|
||||||
|
this.setState({
|
||||||
|
leavingTaskId: null,
|
||||||
|
leavingBucket: null,
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private getItems = () => {
|
||||||
|
if (this.props.allowAdd) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: 'newItem',
|
||||||
|
name: 'New',
|
||||||
|
cacheKey: 'myAddBtnKey',
|
||||||
|
iconProps: {
|
||||||
|
iconName: 'Add'
|
||||||
|
},
|
||||||
|
onClick: () => this.internalAddTask.bind(this)
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private getFarItems = () => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: 'info',
|
||||||
|
name: 'Info',
|
||||||
|
ariaLabel: 'Info',
|
||||||
|
iconProps: {
|
||||||
|
iconName: 'Info'
|
||||||
|
},
|
||||||
|
iconOnly: true,
|
||||||
|
onClick: () => console.log('Info')
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
|
||||||
|
.taskcard {
|
||||||
|
padding: 5px;
|
||||||
|
background-color:initial;
|
||||||
|
// box-shadow: $ms-depth-shadow-8;
|
||||||
|
box-shadow: 0 3.2px 7.2px 0 rgba(0,0,0,.132), 0 0.6px 1.8px 0 rgba(0,0,0,.108);
|
||||||
|
.titlerow {
|
||||||
|
display: table-row;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 0;
|
||||||
|
.isCompleted {
|
||||||
|
display: table-cell;
|
||||||
|
width: 32px;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
display: table-cell;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
vertical-align: middle;
|
||||||
|
white-space: nowrap;
|
||||||
|
max-width: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.details {
|
||||||
|
display: table-cell;
|
||||||
|
width: 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.membersAndLabels {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.labels,
|
||||||
|
.priority,
|
||||||
|
.assignedto {
|
||||||
|
padding: 7px 0px;
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.moving {
|
||||||
|
background-color:"[theme: neutralLight, default: #eaeaea]";
|
||||||
|
}
|
|
@ -0,0 +1,93 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import styles from './KanbanTask.module.scss';
|
||||||
|
import * as strings from 'KanbanBoardStrings';
|
||||||
|
import { IconButton } from 'office-ui-fabric-react/lib/Button';
|
||||||
|
import { IKanbanTask } from './IKanbanTask';
|
||||||
|
import { IKanbanBoardTaskSettings } from './IKanbanBoardTaskSettings';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { Persona, PersonaSize } from 'office-ui-fabric-react';
|
||||||
|
|
||||||
|
export interface IKanbanTaskProps extends IKanbanTask, IKanbanBoardTaskSettings {
|
||||||
|
|
||||||
|
toggleCompleted?: (taskId: string) => void;
|
||||||
|
openDetails: (taskId: string) => void;
|
||||||
|
onDragStart: (event) => void;
|
||||||
|
onDragEnd: (event) => void;
|
||||||
|
isMoving: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IKanbanTaskState { }
|
||||||
|
|
||||||
|
export default class KanbanTask extends React.Component<IKanbanTaskProps, IKanbanTaskState> {
|
||||||
|
constructor(props: IKanbanTaskProps) {
|
||||||
|
super(props);
|
||||||
|
this.state = {};
|
||||||
|
}
|
||||||
|
public render(): React.ReactElement<IKanbanTaskProps> {
|
||||||
|
|
||||||
|
const { title, showPriority, showAssignedTo, isCompleted, isMoving, showTaskDetailsButton } = this.props;
|
||||||
|
const showCompleted = !!this.props.toggleCompleted;
|
||||||
|
const iconCompleted = { iconName: isCompleted ? 'RadioBtnOn' : 'RadioBtnOff' };
|
||||||
|
/*
|
||||||
|
className={classNames({ [styles.taskcard]: true, [styles.moving]: isMoving })}
|
||||||
|
*/
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames({ [styles.taskcard]: true, [styles.moving]: isMoving })}
|
||||||
|
onDragStart={this.props.onDragStart}
|
||||||
|
onDragEnd={this.props.onDragEnd}
|
||||||
|
draggable
|
||||||
|
>
|
||||||
|
<div className={styles.titlerow}>
|
||||||
|
{showCompleted && (
|
||||||
|
<div className={styles.isCompleted} ><IconButton
|
||||||
|
iconProps={iconCompleted}
|
||||||
|
title={isCompleted ? strings.IsCompleted : strings.IsNotCompleted}
|
||||||
|
ariaLabel={isCompleted ? strings.IsCompleted : strings.IsNotCompleted}
|
||||||
|
onClick={this._toggleCompleted.bind(this)}
|
||||||
|
/></div>)
|
||||||
|
}
|
||||||
|
<div className={styles.title}>{title}</div>
|
||||||
|
{showTaskDetailsButton && (
|
||||||
|
<div className={styles.details}><IconButton
|
||||||
|
iconProps={{ iconName: 'More' }}
|
||||||
|
title={strings.OpenDetails}
|
||||||
|
ariaLabel={strings.OpenDetails}
|
||||||
|
onClick={this._openDetails.bind(this)}
|
||||||
|
/></div>)
|
||||||
|
}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div className={styles.membersAndLabels}>
|
||||||
|
{showPriority && this.props.priority && (<div className={styles.priority}>{this.props.priority}</div>)}
|
||||||
|
|
||||||
|
{showAssignedTo && this.props.assignedTo && (<div className={styles.assignedto}>
|
||||||
|
<Persona
|
||||||
|
key={'assingedto'}
|
||||||
|
{...this.props.assignedTo}
|
||||||
|
size={PersonaSize.size32}
|
||||||
|
hidePersonaDetails={false}
|
||||||
|
/>
|
||||||
|
</div>)}
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _toggleCompleted(): void {
|
||||||
|
if (this.props.toggleCompleted) {
|
||||||
|
this.props.toggleCompleted(this.props.taskId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _openDetails(): void {
|
||||||
|
if (this.props.openDetails) {
|
||||||
|
this.props.openDetails(this.props.taskId);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
.rowcol1{
|
||||||
|
flex-basis: 30%;
|
||||||
|
}
|
||||||
|
.rowcol2{
|
||||||
|
flex-basis: 70%;
|
||||||
|
}
|
|
@ -0,0 +1,84 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import styles from './KanbanTaskManagedProp.module.scss';
|
||||||
|
import { IKanbanTaskManagedProps, KanbanTaskMamagedPropertyType } from './IKanbanTask';
|
||||||
|
import { Stack } from 'office-ui-fabric-react/lib/Stack';
|
||||||
|
import ReactHtmlParser from 'react-html-parser';
|
||||||
|
import { Persona, PersonaSize, IPersonaProps } from 'office-ui-fabric-react';
|
||||||
|
|
||||||
|
export interface IKanbanTaskManagedPropProps extends IKanbanTaskManagedProps { }
|
||||||
|
|
||||||
|
export interface IKanbanTaskManagedPropState { }
|
||||||
|
|
||||||
|
export default class KanbanTaskManagedProp extends React.Component<IKanbanTaskManagedPropProps, IKanbanTaskManagedPropState> {
|
||||||
|
public render(): React.ReactElement<IKanbanTaskManagedPropProps> {
|
||||||
|
const { displayName, name } = this.props;
|
||||||
|
return (
|
||||||
|
<Stack horizontal horizontalAlign="stretch">
|
||||||
|
<Stack.Item align="auto" className={styles.rowcol1}>
|
||||||
|
<span>{displayName ? displayName : name}</span>
|
||||||
|
</Stack.Item>
|
||||||
|
<Stack.Item align="stretch" className={styles.rowcol2}>
|
||||||
|
{this.renderValue()}
|
||||||
|
</Stack.Item>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
private renderValue() {
|
||||||
|
const { name, type, value } = this.props;
|
||||||
|
if (this.props.renderer) {
|
||||||
|
return this.props.renderer(name, value, type);
|
||||||
|
}
|
||||||
|
switch (this.props.type) {
|
||||||
|
case KanbanTaskMamagedPropertyType.string:
|
||||||
|
return (<span>{value} </span>);
|
||||||
|
break;
|
||||||
|
case KanbanTaskMamagedPropertyType.number:
|
||||||
|
return (<span>{value} </span>);
|
||||||
|
//TODO maybe Formater
|
||||||
|
break;
|
||||||
|
case KanbanTaskMamagedPropertyType.percent:
|
||||||
|
return (<span>{`${(value as any) * 100}%`} </span>);
|
||||||
|
//TODO maybe better Formater
|
||||||
|
break;
|
||||||
|
case KanbanTaskMamagedPropertyType.html:
|
||||||
|
return (<span>{ReactHtmlParser(value)}</span>);
|
||||||
|
break;
|
||||||
|
case KanbanTaskMamagedPropertyType.person:
|
||||||
|
|
||||||
|
return (<span>
|
||||||
|
{value && (
|
||||||
|
<Persona
|
||||||
|
{...value}
|
||||||
|
size={PersonaSize.size32}
|
||||||
|
hidePersonaDetails={false} />
|
||||||
|
)}
|
||||||
|
</span>);
|
||||||
|
break;
|
||||||
|
case KanbanTaskMamagedPropertyType.persons:
|
||||||
|
return (<span>
|
||||||
|
{
|
||||||
|
(value && (
|
||||||
|
(value as IPersonaProps[]).map((p, i) => (<Persona
|
||||||
|
key={'persona' + i}
|
||||||
|
{...value}
|
||||||
|
size={PersonaSize.size32}
|
||||||
|
hidePersonaDetails={false}
|
||||||
|
/>))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</span>);
|
||||||
|
break;
|
||||||
|
case KanbanTaskMamagedPropertyType.complex:
|
||||||
|
return (<span>{JSON.stringify(value)}</span>);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw "Unknow KanbanTaskMamagedPropertyType";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,129 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import {KanbanComponent} from './KanbanComponent';
|
||||||
|
import {KanbanBucketConfigurator} from './KanbanBucketConfigurator';
|
||||||
|
import { IKanbanBucket } from './IKanbanBucket';
|
||||||
|
import { IKanbanTask, KanbanTaskMamagedPropertyType } from './IKanbanTask';
|
||||||
|
import { findIndex } from "lodash";
|
||||||
|
import { cloneDeep, clone } from '@microsoft/sp-lodash-subset';
|
||||||
|
|
||||||
|
export interface IMockKanbanProps { }
|
||||||
|
|
||||||
|
export interface IMockKanbanState {
|
||||||
|
buckets: IKanbanBucket[];
|
||||||
|
tasks: IKanbanTask[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MockKanban extends React.Component<IMockKanbanProps, IMockKanbanState> {
|
||||||
|
|
||||||
|
constructor(props: IMockKanbanProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
buckets: [
|
||||||
|
{ bucket: 'Not Started', bucketheadline: 'Not Started Head', percentageComplete: 0, color: 'yellow', allowAddTask: true },
|
||||||
|
{ bucket: 'Test1', bucketheadline: 'Test1 Head', percentageComplete: 10, color: 'orange', allowAddTask: true },
|
||||||
|
{ bucket: 'Test2', bucketheadline: 'Test2 Head', percentageComplete: 50, color: 'green' },
|
||||||
|
{ bucket: 'Test3', bucketheadline: 'Test3 Head', percentageComplete: 50, color: '#FF0000' },
|
||||||
|
{ bucket: 'Test4', bucketheadline: 'Test4 Head', percentageComplete: 0, allowAddTask: true }
|
||||||
|
],
|
||||||
|
tasks: [
|
||||||
|
{
|
||||||
|
taskId: '1', title: 'test1', bucket: 'Not Started',
|
||||||
|
mamagedProperties: [
|
||||||
|
{
|
||||||
|
name: 'Prop1',
|
||||||
|
displayName: 'Prop1 Display',
|
||||||
|
type: KanbanTaskMamagedPropertyType.html,
|
||||||
|
value: '<p>test<b>Bold</b></p>'
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'Prop2',
|
||||||
|
displayName: 'Prop2 Display',
|
||||||
|
type: KanbanTaskMamagedPropertyType.complex,
|
||||||
|
value: '<p>test<b>Bold</b></p>',
|
||||||
|
renderer: (name, value, type) => { return (<span>SampleRenderer</span>); }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Prop3',
|
||||||
|
displayName: 'String',
|
||||||
|
type: KanbanTaskMamagedPropertyType.string,
|
||||||
|
value: 'Hallo World'
|
||||||
|
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{ taskId: '2', title: 'test2', bucket: 'Not Started' },
|
||||||
|
{ taskId: '3', title: 'test3', bucket: 'Not Started' },
|
||||||
|
{ taskId: '4', title: 'test 4', bucket: 'Test4' },
|
||||||
|
{ taskId: '5', title: 'test 5', bucket: 'Test3' },
|
||||||
|
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public render(): React.ReactElement<IMockKanbanProps> {
|
||||||
|
const { buckets, tasks } = this.state;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<KanbanComponent
|
||||||
|
buckets={buckets}
|
||||||
|
tasks={tasks}
|
||||||
|
tasksettings={{
|
||||||
|
showPriority: true,
|
||||||
|
showAssignedTo: true,
|
||||||
|
showTaskDetailsButton: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
taskactions={{
|
||||||
|
toggleCompleted: this._toggleCompleted.bind(this),
|
||||||
|
allowMove: this._allowMove.bind(this),
|
||||||
|
moved: this._moved.bind(this),
|
||||||
|
}}
|
||||||
|
showCommandbar={true}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<h2>
|
||||||
|
Bucket Configuration sample
|
||||||
|
</h2>
|
||||||
|
{
|
||||||
|
this.state.buckets.map((b, i) =>
|
||||||
|
<KanbanBucketConfigurator
|
||||||
|
key={'BucketConfig' + i}
|
||||||
|
index={i}
|
||||||
|
bucket={b}
|
||||||
|
update={this.updateBucket.bind(this)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateBucket(index: number, value: IKanbanBucket) {
|
||||||
|
debugger;
|
||||||
|
const cstate = cloneDeep(this.state);
|
||||||
|
cstate.buckets[index] = clone(value);
|
||||||
|
this.setState(cstate);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _toggleCompleted(taskId: string): void {
|
||||||
|
//TODO
|
||||||
|
}
|
||||||
|
private _allowMove(taskId: string, prevBucket: IKanbanBucket, targetBucket: IKanbanBucket): boolean {
|
||||||
|
if (prevBucket.bucket === 'Test2' && targetBucket.bucket === 'Test3') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _moved(taskId: string, targetBucket: IKanbanBucket): void {
|
||||||
|
const elementsIndex = findIndex(this.state.tasks, element => element.taskId == taskId);
|
||||||
|
let newArray = [...this.state.tasks];
|
||||||
|
newArray[elementsIndex].bucket = targetBucket.bucket;
|
||||||
|
this.setState({ tasks: newArray });
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,209 @@
|
||||||
|
# KanbanComponent Control
|
||||||
|
|
||||||
|
This control renders a KanbanBoard which can be used to show Tasks and move it from one State to an Other.
|
||||||
|
|
||||||
|
**Control in Action**
|
||||||
|
|
||||||
|
![KanbanBoard control](../../assets/kanbanofficeUI.gif)
|
||||||
|
|
||||||
|
|
||||||
|
## How to use this control in your solutions
|
||||||
|
|
||||||
|
this component is not Extracted as an NPM Package
|
||||||
|
Copy this Folder
|
||||||
|
In the Files ```MockKanban.tsx``` you can find many Configuration Options
|
||||||
|
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
<KanbanComponent
|
||||||
|
buckets={buckets}
|
||||||
|
tasks={tasks}
|
||||||
|
tasksettings={{
|
||||||
|
showPriority: true,
|
||||||
|
showAssignedTo: true,
|
||||||
|
showTaskDetailsButton: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
taskactions={{
|
||||||
|
toggleCompleted: this._toggleCompleted.bind(this),
|
||||||
|
allowMove: this._allowMove.bind(this),
|
||||||
|
moved: this._moved.bind(this),
|
||||||
|
}}
|
||||||
|
showCommandbar={true}
|
||||||
|
/>
|
||||||
|
|
||||||
|
```
|
||||||
|
Buckets
|
||||||
|
```
|
||||||
|
[
|
||||||
|
{bucket:'Not Started', bucketheadline:'Not Started Head',percentageComplete:0, color:'yellow' ,allowAddTask:true},
|
||||||
|
{bucket:'Test1', bucketheadline:'Test1 Head',percentageComplete:10, color:'orange',allowAddTask:true },
|
||||||
|
{bucket:'Test2', bucketheadline:'Test2 Head',percentageComplete:50, color:'green' },
|
||||||
|
]
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Tasks
|
||||||
|
```
|
||||||
|
[
|
||||||
|
{taskId: '1', title:'test1',bucket:'Not Started'},
|
||||||
|
{taskId: '5', title:'test 5',bucket:'Test3'}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
The KanbanBoard control can be configured with the following properties:
|
||||||
|
|
||||||
|
### IKanbanBucket
|
||||||
|
| Property | Type | Required | Description | Default |
|
||||||
|
| ------------------ | ------ | -------- | ------------------------------------------------ | ----------------------- |
|
||||||
|
| bucket | string | * | internalname | |
|
||||||
|
| bucketheadline | string | | Optional Headline | name of property bucket |
|
||||||
|
| percentageComplete | number | | Percentage of Bucket shows in bar under Headline |
|
||||||
|
| color | string | | | |
|
||||||
|
|
||||||
|
### IKanbanTask
|
||||||
|
| Property | Type | Required | Description | Default |
|
||||||
|
| ----------------- | ----------------------- | -------- | ----------- | ------- |
|
||||||
|
| taskId | string | * | | |
|
||||||
|
| title | string | * | | |
|
||||||
|
| assignedTo | IPersonaProps | | | |
|
||||||
|
| htmlDescription | string | | | |
|
||||||
|
| priority | string | | | |
|
||||||
|
| bucket | string | * | | |
|
||||||
|
| mamagedProperties | IKanbanTaskManagedProps | | | |
|
||||||
|
|
||||||
|
|
||||||
|
### IKanbanComponentProps
|
||||||
|
|
||||||
|
| Property | Type | Required | Description | Default |
|
||||||
|
| -------------- | ------------------------ | -------- | ----------- | ------- |
|
||||||
|
| buckets | IKanbanBucket[] | * | | |
|
||||||
|
| tasks | IKanbanTask[] | * | | |
|
||||||
|
| tasksettings | IKanbanBoardTaskSettings | * | | |
|
||||||
|
| taskactions | IKanbanBoardTaskActions | * | | |
|
||||||
|
| showCommandbar | boolean | | | false |
|
||||||
|
| renderers | IKanbanBoardRenderers | | | |
|
||||||
|
| allowAdd? | boolean | | | false |
|
||||||
|
|
||||||
|
|
||||||
|
#### IKanbanBoardTaskSettings
|
||||||
|
| Property | Type | Required | Description | Default |
|
||||||
|
| --------------------- | ------- | -------- | ----------- | ------- |
|
||||||
|
| showPriority | boolean | * | | |
|
||||||
|
| showAssignedTo | boolean | * | | |
|
||||||
|
| showTaskDetailsButton | boolean | * | | |
|
||||||
|
#### IKanbanBoardTaskActions
|
||||||
|
| Property | Type | Required | Description | Default |
|
||||||
|
| --------------- | ----------------------------------------------------------------------------------- | -------- | ----------- | ------- |
|
||||||
|
| toggleCompleted | (taskId: string) => void | | | |
|
||||||
|
| allowMove | taskId: string, prevBucket: IKanbanBucket, targetBucket: IKanbanBucket) => boolean | | | |
|
||||||
|
| moved | (taskId: string, targetBucket: IKanbanBucket) => void; | | | |
|
||||||
|
|
||||||
|
|
||||||
|
#### IKanbanBoardRenderers
|
||||||
|
| Property | Type | Required | Description | Default |
|
||||||
|
| --------------- | ------------------------------------- | -------- | ----------- | ------- |
|
||||||
|
| task? | (task:IKanbanTask) => JSX.Element | | | |
|
||||||
|
| bucketHeadline? | (bucket:IKanbanBucket) => JSX.Element | | | |
|
||||||
|
| taskDetail? | (task:IKanbanTask) => JSX.Element | | |
|
||||||
|
|
||||||
|
#### IKanbanTaskManagedProps
|
||||||
|
| Property | Type | Required | Description | Default |
|
||||||
|
| ----------- | --------------------------------------------------------------------------------- | -------- | ----------- | ------- |
|
||||||
|
| name | string | * | | |
|
||||||
|
| displayName | string | | | |
|
||||||
|
| type: | KanbanTaskMamagedPropertyType | * | | |
|
||||||
|
| value | string /number/ IPersonaProps/ IPersonaProps[] / any | * | | |
|
||||||
|
| renderer | (name: string, value: object, type: KanbanTaskMamagedPropertyType) => JSX.Element | | | |
|
||||||
|
|
||||||
|
IPersonaProps reference to Office UI Fabric
|
||||||
|
|
||||||
|
|
||||||
|
#### KanbanTaskMamagedPropertyType
|
||||||
|
| Type | Value | Description |
|
||||||
|
| ------- | ----- | ------------------------------------------- |
|
||||||
|
| string | 1 | value |
|
||||||
|
| number | 2 | value |
|
||||||
|
| percent | 3 | value * 100 % |
|
||||||
|
| html | 4 | value with html string |
|
||||||
|
| person | 5 | Office Ui Persona (value:IPersonaProps) |
|
||||||
|
| persons | 6 | Office Ui Persona's (value:IPersonaProps[]) |
|
||||||
|
| complex | 7 | JSON.stringify(value) |
|
||||||
|
}
|
||||||
|
|
||||||
|
## Samples
|
||||||
|
|
||||||
|
### Custom Detail View Renderer
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
renderers.taskDetail?: (task:IKanbanTask) => JSX.Element ;
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
renderers.taskDetail?: (task:IKanbanTask) => JSX.Element ;
|
||||||
|
private _taskDetailRenderer(task:IKanbanTask): JSX.Element {
|
||||||
|
return (<div>My Cool Content!!!</div>)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Use SharePoint New and Edit Form
|
||||||
|
|
||||||
|
´´´typescript
|
||||||
|
taskactions.taskEdit?: (task: IKanbanTask) => void ;
|
||||||
|
taskactions.taskAdd?: (bucket?: IKanbanBucket) => void ;
|
||||||
|
´´´
|
||||||
|
Open in Dialog (Iframe) or new Tab (NewForm.aspx od EditForm.aspx) or make your custom Form
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
private _taskAdd(bucket?: IKanbanBucket): void {
|
||||||
|
window.open(this.listurl+'/NewForm.aspx)
|
||||||
|
}
|
||||||
|
private _taskEditd(task: IKanbanTask): void {
|
||||||
|
window.open(this.listurl+'/EditFrom.aspx?ID='+task.taskId')
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Disallow Move from One Bucket to an Other (allowMove)
|
||||||
|
|
||||||
|
´´´typescript
|
||||||
|
taskactions.allowMove: (taskId: string, prevBucket: IKanbanBucket, targetBucket: IKanbanBucket) => boolean;
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
private _allowMove(taskId: string, prevBucket: IKanbanBucket, targetBucket: IKanbanBucket): boolean {
|
||||||
|
if (prevBucket.bucket === 'Test2' && targetBucket.bucket === 'Test3') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Leanings
|
||||||
|
|
||||||
|
Read more about [Drag and Drop](https://petkir.wordpress.com/2020/07/01/drag-and-drop-in-react-spfx/)
|
||||||
|
```
|
||||||
|
IKanbanTask {
|
||||||
|
taskId: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
has to be a string because, i use event ```event.dataTransfer.setData``` and this accespts only strings in IE
|
||||||
|
```
|
||||||
|
event.dataTransfer.setData('text',1)
|
||||||
|
Invalid argument.
|
||||||
|
```
|
||||||
|
The second big thing is IE allows only to set the value 'text' event.dataTransfer.setData('text','1')
|
||||||
|
```
|
||||||
|
event.dataTransfer.setData('xyz','1')
|
||||||
|
Unexpected call to method or property access.
|
||||||
|
```
|
||||||
|
|
||||||
|
-------------------------------
|
||||||
|
## Future
|
||||||
|
|
||||||
|
* think about Promise Task Actions, because actions are async
|
||||||
|
* EditSchema To support Edit
|
|
@ -0,0 +1,4 @@
|
||||||
|
export { KanbanComponent } from './KanbanComponent';
|
||||||
|
export { KanbanBucketConfigurator ,IKanbanBucketConfiguratorProps } from './KanbanBucketConfigurator';
|
||||||
|
export { IKanbanBucket } from './IKanbanBucket';
|
||||||
|
export { IKanbanTask, KanbanTaskMamagedPropertyType } from './IKanbanTask';
|
|
@ -0,0 +1,30 @@
|
||||||
|
define([], function () {
|
||||||
|
return {
|
||||||
|
"CompleteButton": "Complete",
|
||||||
|
"IsCompleted": "Is Completed",
|
||||||
|
"IsNotCompleted": "Is Not Completed",
|
||||||
|
"AddTask": "Add Task",
|
||||||
|
"OpenDetails": "Open Details",
|
||||||
|
"EditTaskBtn": "Edit",
|
||||||
|
"SaveTaskBtn": "Save and Close",
|
||||||
|
"SaveAddTaskBtn": "Add and Close",
|
||||||
|
"CloseTaskDialog": "Close",
|
||||||
|
"AddTaskDlgHeadline": "Add Task",
|
||||||
|
"EditTaskDlgHeadline": "Edit Task",
|
||||||
|
"AssignedTo": "Assigned to",
|
||||||
|
"HtmlDescription": "Description",
|
||||||
|
"Priority": "Priority",
|
||||||
|
|
||||||
|
"BucketConfigInternalName": "Internal Name",
|
||||||
|
"BucketConfigHeadline": "Headline",
|
||||||
|
"BucketConfigPercentageComplete": "Bucket State Percentage Complete",
|
||||||
|
"BucketConfigAllowAddTask": "Add Task?",
|
||||||
|
"BucketConfigUseColor": "Use Color?",
|
||||||
|
"BucketConfigSave": "Save",
|
||||||
|
"BucketConfigReset": "Reset",
|
||||||
|
"Percent":"percent",
|
||||||
|
"BucketConfigShowPercentage": "Show Processindicator",
|
||||||
|
"BucketConfigShowPercentageShow": "Show",
|
||||||
|
"BucketConfigShowPercentageHide": "Hide"
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,34 @@
|
||||||
|
declare interface IKanbanBoardStrings {
|
||||||
|
CompleteButton: string;
|
||||||
|
IsCompleted: string;
|
||||||
|
IsNotCompleted: string;
|
||||||
|
AddTask: string;
|
||||||
|
OpenDetails: string;
|
||||||
|
EditTaskBtn: string;
|
||||||
|
SaveTaskBtn: string;
|
||||||
|
SaveAddTaskBtn: string;
|
||||||
|
CloseTaskDialog: string;
|
||||||
|
AddTaskDlgHeadline: string;
|
||||||
|
EditTaskDlgHeadline: string;
|
||||||
|
|
||||||
|
AssignedTo: string;
|
||||||
|
HtmlDescription: string;
|
||||||
|
Priority: string;
|
||||||
|
|
||||||
|
BucketConfigInternalName: string;
|
||||||
|
BucketConfigHeadline: string;
|
||||||
|
BucketConfigPercentageComplete: string;
|
||||||
|
BucketConfigAllowAddTask: string;
|
||||||
|
BucketConfigUseColor: string;
|
||||||
|
BucketConfigSave: string;
|
||||||
|
BucketConfigReset: string;
|
||||||
|
Percent: string;
|
||||||
|
BucketConfigShowPercentage: string;
|
||||||
|
BucketConfigShowPercentageShow: string;
|
||||||
|
BucketConfigShowPercentageHide: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'KanbanBoardStrings' {
|
||||||
|
const strings: IKanbanBoardStrings;
|
||||||
|
export = strings;
|
||||||
|
}
|
|
@ -1,49 +1,79 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import * as ReactDom from 'react-dom';
|
import * as ReactDom from 'react-dom';
|
||||||
import { Version } from '@microsoft/sp-core-library';
|
import { Version, Environment, EnvironmentType } from '@microsoft/sp-core-library';
|
||||||
import { BaseClientSideWebPart, PropertyPaneDropdown } from '@microsoft/sp-webpart-base';
|
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
|
||||||
import {
|
import {
|
||||||
IPropertyPaneConfiguration,
|
IPropertyPaneConfiguration,
|
||||||
PropertyPaneTextField
|
PropertyPaneToggle
|
||||||
} from '@microsoft/sp-property-pane';
|
} from '@microsoft/sp-property-pane';
|
||||||
import {sp} from '@pnp/sp';
|
import { cloneDeep } from '@microsoft/sp-lodash-subset';
|
||||||
|
|
||||||
|
import { PropertyFieldListPicker, PropertyFieldListPickerOrderBy } from '@pnp/spfx-property-controls/lib/PropertyFieldListPicker';
|
||||||
|
import { PropertyFieldOrder } from '@pnp/spfx-property-controls/lib/PropertyFieldOrder';
|
||||||
import * as strings from 'KanbanBoardWebPartStrings';
|
import * as strings from 'KanbanBoardWebPartStrings';
|
||||||
import KanbanBoard from './components/KanbanBoard';
|
import "@pnp/polyfill-ie11";
|
||||||
import { IKanbanBoardProps } from './components/IKanbanBoardProps';
|
import { sp } from '@pnp/sp';
|
||||||
|
|
||||||
|
import PropertyPaneBucketConfigComponent from './components/PropertyPaneBucketConfig';
|
||||||
|
import KanbanBoardV2, { IKanbanBoardV2Props } from './components/KanbanBoardV2';
|
||||||
|
import { bucketOrder } from './components/bucketOrder';
|
||||||
|
import { mergeBucketsWithChoices } from './components/helper';
|
||||||
|
|
||||||
|
import { IKanbanBucket } from '../../kanban';
|
||||||
|
|
||||||
|
import { ISPKanbanService } from './services/ISPKanbanService';
|
||||||
|
import SPKanbanService from './services/SPKanbanService';
|
||||||
|
import MockKanbanService from './services/MockKanbanService';
|
||||||
|
|
||||||
export interface IKanbanBoardWebPartProps {
|
export interface IKanbanBoardWebPartProps {
|
||||||
listTitle: string;
|
hideWPTitle: boolean;
|
||||||
lists: Array<any>;
|
title: string;
|
||||||
|
buckets: IKanbanBucket[];
|
||||||
|
listId: string;
|
||||||
|
listTitle: string; //was the name if upgrade support than (remap title to id)
|
||||||
loaded: boolean;
|
loaded: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class KanbanBoardWebPart extends BaseClientSideWebPart<IKanbanBoardWebPartProps> {
|
export default class KanbanBoardWebPart extends BaseClientSideWebPart<IKanbanBoardWebPartProps> {
|
||||||
|
private kanbanComponent = null;
|
||||||
|
private dataService: ISPKanbanService;
|
||||||
|
private statekey: string = Date.now().toString();
|
||||||
public onInit(): Promise<void> {
|
public onInit(): Promise<void> {
|
||||||
|
|
||||||
return super.onInit().then(_ => {
|
return super.onInit().then(_ => {
|
||||||
|
|
||||||
sp.setup({
|
sp.setup({
|
||||||
spfxContext: this.context
|
spfxContext: this.context
|
||||||
});
|
});
|
||||||
|
if (Environment.type == EnvironmentType.Local || Environment.type == EnvironmentType.Test) {
|
||||||
|
this.dataService = new MockKanbanService();
|
||||||
|
} else {
|
||||||
|
this.dataService = new SPKanbanService();
|
||||||
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public render(): void {
|
public render(): void {
|
||||||
|
const element: React.ReactElement<IKanbanBoardV2Props> = React.createElement(
|
||||||
|
KanbanBoardV2,
|
||||||
|
|
||||||
const element: React.ReactElement<IKanbanBoardProps > = React.createElement(
|
|
||||||
KanbanBoard,
|
|
||||||
{
|
{
|
||||||
listTitle: this.properties.listTitle,
|
hideWPTitle: this.properties.hideWPTitle,
|
||||||
webUrl: this.context.pageContext.web.absoluteUrl
|
title: this.properties.title,
|
||||||
|
displayMode: this.displayMode,
|
||||||
|
updateProperty: (value: string) => {
|
||||||
|
this.properties.title = value;
|
||||||
|
},
|
||||||
|
statekey: this.statekey,
|
||||||
|
context: this.context,
|
||||||
|
listId: this.properties.listId,
|
||||||
|
configuredBuckets: this.properties.buckets
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
ReactDom.render(element, this.domElement);
|
|
||||||
|
this.kanbanComponent = ReactDom.render(element, this.domElement);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected onDispose(): void {
|
protected onDispose(): void {
|
||||||
|
@ -55,35 +85,130 @@ export default class KanbanBoardWebPart extends BaseClientSideWebPart<IKanbanBoa
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
|
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
|
||||||
return {
|
const propertypages = [];
|
||||||
pages: [
|
|
||||||
{
|
const generalgroups = [];
|
||||||
groups: [
|
generalgroups.push(
|
||||||
{
|
{
|
||||||
groupName: strings.BasicGroupName,
|
groupName: strings.BasicGroupName,
|
||||||
groupFields: [
|
groupFields: [
|
||||||
PropertyPaneDropdown('listTitle',{
|
PropertyPaneToggle('hideWPTitle', {
|
||||||
label: strings.ListTitleFieldLabel,
|
label: strings.propertyPaneHideWPHeadline,
|
||||||
options: this.properties.lists
|
checked: this.properties.hideWPTitle,
|
||||||
})
|
onText: strings.propertyPaneHideWPHeadlineHide,
|
||||||
]
|
offText: strings.propertyPaneHideWPHeadlineShow
|
||||||
|
}),
|
||||||
|
PropertyFieldListPicker('listId', {
|
||||||
|
label: strings.propertyPaneSelectList,
|
||||||
|
selectedList: this.properties.listId,
|
||||||
|
includeHidden: false,
|
||||||
|
orderBy: PropertyFieldListPickerOrderBy.Title,
|
||||||
|
disabled: false,
|
||||||
|
onPropertyChange: this.listConfigurationChanged.bind(this),
|
||||||
|
properties: this.properties,
|
||||||
|
context: this.context,
|
||||||
|
onGetErrorMessage: null,
|
||||||
|
deferredValidationTime: 0,
|
||||||
|
key: 'listPickerFieldId',
|
||||||
|
onListsRetrieved: (lists) => {
|
||||||
|
//TODO Check from TS Definition it should be a string but i get a number
|
||||||
|
// with Typesafe equal it fails
|
||||||
|
if (Environment.type == EnvironmentType.Local || Environment.type == EnvironmentType.Test) {
|
||||||
|
return lists;
|
||||||
|
} else {
|
||||||
|
const alists = lists.filter((l: any) => {
|
||||||
|
return (l.BaseTemplate === 171 || l.BaseTemplate === 107);
|
||||||
|
});
|
||||||
|
return alists;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
]
|
||||||
|
});
|
||||||
|
if (this.properties.listId && this.properties.buckets && this.properties.buckets.length > 1) {
|
||||||
|
generalgroups.push(
|
||||||
|
{
|
||||||
|
groupName: strings.propertyPaneLabelOrderBuckets,
|
||||||
|
groupFields: [
|
||||||
|
PropertyFieldOrder("buckets", {
|
||||||
|
key: "orderedItems",
|
||||||
|
label: strings.propertyPaneLabelOrderBuckets,
|
||||||
|
items: this.properties.buckets,
|
||||||
|
properties: this.properties,
|
||||||
|
onPropertyChange: this.onPropertyPaneFieldChanged,
|
||||||
|
onRenderItem: bucketOrder,
|
||||||
|
})
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
);
|
||||||
|
}
|
||||||
|
propertypages.push({
|
||||||
|
groups: generalgroups
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.properties.buckets && this.properties.buckets.length > 0) {
|
||||||
|
this.properties.buckets.forEach((b, i) => {
|
||||||
|
propertypages.push({
|
||||||
|
key: { i },
|
||||||
|
header: {
|
||||||
|
description: strings.propertyPaneBucketConfiguration
|
||||||
|
},
|
||||||
|
groups: [{
|
||||||
|
groupName: b.bucketheadline ? b.bucketheadline : b.bucket,
|
||||||
|
groupFields: [
|
||||||
|
PropertyPaneBucketConfigComponent('bucket_' + i, {
|
||||||
|
key: 'bucket_' + i,
|
||||||
|
properties: cloneDeep(b),
|
||||||
|
onPropertyChange: this.bucketConfigurationChanged.bind(this)
|
||||||
|
})
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
pages: propertypages
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
protected onPropertyPaneConfigurationStart(){
|
private listConfigurationChanged(propertyPath: string, oldValue: any, newValue: any) {
|
||||||
// Use the list template ID to locate both the old style task lists (107) and newer task lists (171)
|
this.onPropertyPaneFieldChanged(propertyPath, oldValue, newValue);
|
||||||
sp.web.lists.filter("BaseTemplate eq 171 or BaseTemplate eq 107").select("Title").get().then(res => {
|
this.refreshBucket();
|
||||||
this.properties.lists = res.map((val,index) => {
|
|
||||||
return {
|
|
||||||
key: val.Title,
|
|
||||||
text: val.Title
|
|
||||||
};
|
|
||||||
});
|
|
||||||
this.context.propertyPane.refresh();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
private bucketConfigurationChanged(propertyPath: string, oldValue: any, newValue: any) {
|
||||||
|
//its an array part !!!!!
|
||||||
|
if (propertyPath.indexOf('bucket_') !== -1) {
|
||||||
|
const oribuckets: IKanbanBucket[] = cloneDeep(this.properties.buckets);
|
||||||
|
const newbuckets: IKanbanBucket[] = cloneDeep(this.properties.buckets);
|
||||||
|
const bucketindex: number = +propertyPath.split('_')[1];
|
||||||
|
newbuckets[bucketindex] = newValue;
|
||||||
|
//maybe better to make a array control (Update)
|
||||||
|
|
||||||
|
this.onPropertyPaneFieldChanged("buckets", oribuckets, newbuckets);
|
||||||
|
this.properties.buckets = newbuckets;
|
||||||
|
this.context.propertyPane.refresh();
|
||||||
|
this.render();
|
||||||
|
} else {
|
||||||
|
throw "propertypath is not a bucket";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private refreshBucket(): void {
|
||||||
|
const listId = this.properties.listId;
|
||||||
|
if (!listId || listId.length === 0) { return; }
|
||||||
|
this.dataService.getBuckets(listId).then((x) => {
|
||||||
|
const currentbuckets: IKanbanBucket[] = mergeBucketsWithChoices(this.properties.buckets, x);
|
||||||
|
if (!currentbuckets) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.properties.buckets = currentbuckets;
|
||||||
|
this.context.propertyPane.refresh();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
export interface IKanbanBoardProps {
|
|
||||||
listTitle: string;
|
|
||||||
webUrl: string;
|
|
||||||
}
|
|
|
@ -1,74 +0,0 @@
|
||||||
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
|
|
||||||
|
|
||||||
.kanbanBoard {
|
|
||||||
.container {
|
|
||||||
max-width: 700px;
|
|
||||||
margin: 0px auto;
|
|
||||||
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.row {
|
|
||||||
@include ms-Grid-row;
|
|
||||||
@include ms-fontColor-white;
|
|
||||||
background-color: $ms-color-themeDark;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.column {
|
|
||||||
@include ms-Grid-col;
|
|
||||||
@include ms-lg10;
|
|
||||||
@include ms-xl8;
|
|
||||||
@include ms-xlPush2;
|
|
||||||
@include ms-lgPush1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
@include ms-font-xl;
|
|
||||||
@include ms-fontColor-white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subTitle {
|
|
||||||
@include ms-font-l;
|
|
||||||
@include ms-fontColor-white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.description {
|
|
||||||
@include ms-font-l;
|
|
||||||
@include ms-fontColor-white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button {
|
|
||||||
// Our button
|
|
||||||
text-decoration: none;
|
|
||||||
height: 32px;
|
|
||||||
|
|
||||||
// Primary Button
|
|
||||||
min-width: 80px;
|
|
||||||
background-color: $ms-color-themePrimary;
|
|
||||||
border-color: $ms-color-themePrimary;
|
|
||||||
color: $ms-color-white;
|
|
||||||
|
|
||||||
// Basic Button
|
|
||||||
outline: transparent;
|
|
||||||
position: relative;
|
|
||||||
font-family: "Segoe UI WestEuropean","Segoe UI",-apple-system,BlinkMacSystemFont,Roboto,"Helvetica Neue",sans-serif;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
font-size: $ms-font-size-m;
|
|
||||||
font-weight: $ms-font-weight-regular;
|
|
||||||
border-width: 0;
|
|
||||||
text-align: center;
|
|
||||||
cursor: pointer;
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0 16px;
|
|
||||||
|
|
||||||
.label {
|
|
||||||
font-weight: $ms-font-weight-semibold;
|
|
||||||
font-size: $ms-font-size-m;
|
|
||||||
height: 32px;
|
|
||||||
line-height: 32px;
|
|
||||||
margin: 0 4px;
|
|
||||||
vertical-align: top;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,267 +0,0 @@
|
||||||
import * as React from 'react';
|
|
||||||
import styles from './KanbanBoard.module.scss';
|
|
||||||
import { IKanbanBoardProps } from './IKanbanBoardProps';
|
|
||||||
import {sp} from '@pnp/sp';
|
|
||||||
import { Spinner } from 'office-ui-fabric-react/lib/Spinner';
|
|
||||||
import { Dialog, DialogType, DialogFooter } from 'office-ui-fabric-react/lib/Dialog';
|
|
||||||
import { DefaultButton } from 'office-ui-fabric-react/lib/Button';
|
|
||||||
import { IStackStyles, Stack } from 'office-ui-fabric-react/lib/Stack';
|
|
||||||
|
|
||||||
import ReactHtmlParser, { processNodes, convertNodeToElement, htmlparser2 } from 'react-html-parser';
|
|
||||||
|
|
||||||
import 'jqwidgets-scripts/jqwidgets/styles/jqx.base.css';
|
|
||||||
import 'jqwidgets-scripts/jqwidgets/styles/jqx.metro.css';
|
|
||||||
import JqxKanban, { IKanbanProps, jqx, IKanbanSource } from 'jqwidgets-scripts/jqwidgets-react-tsx/jqxkanban';
|
|
||||||
|
|
||||||
const rowStyle1: IStackStyles = {
|
|
||||||
root:{
|
|
||||||
flexBasis:"30%"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const rowStyle2: IStackStyles = {
|
|
||||||
root:{
|
|
||||||
flexBasis:"70%"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface ICustomKanbanProps extends IKanbanProps{
|
|
||||||
listTitle: string;
|
|
||||||
showBoard: boolean;
|
|
||||||
noItems: boolean;
|
|
||||||
taskDetails: IKanbanSource;
|
|
||||||
hideDialog: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class KanbanBoard extends React.Component<IKanbanBoardProps, ICustomKanbanProps, {}> {
|
|
||||||
|
|
||||||
private sourceFields: any[] = [
|
|
||||||
{ name: 'id', map:'Id', type: 'string' },
|
|
||||||
{ name: 'status', map: 'Status', type: 'string' },
|
|
||||||
{ name: 'text', map: 'Title', type: 'string' },
|
|
||||||
{ name: 'tags',map:'Priority', type: 'string' },
|
|
||||||
{ name: 'color',map:'PercentComplete', type: 'string' },
|
|
||||||
{ name: 'resourceId', map: 'AssignedToId', type: 'number' },
|
|
||||||
{ name: 'content', map:'Body', type: 'string'},
|
|
||||||
{ name: 'percent', map:'PercentComplete', type: 'number'},
|
|
||||||
{ name: 'priority', map:'Priority', type: 'string'}
|
|
||||||
];
|
|
||||||
|
|
||||||
private resourceFields : any[] = [
|
|
||||||
{ name: 'id', map:'Id', type: 'number' },
|
|
||||||
{ name: 'name', map:'Title', type: 'string' },
|
|
||||||
{ name: 'image', type: 'string' },
|
|
||||||
{ name: 'common', type: 'boolean' },
|
|
||||||
{ name: 'email', type: 'string', map: 'Email' }
|
|
||||||
];
|
|
||||||
|
|
||||||
constructor(props: IKanbanBoardProps) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
const template: string =
|
|
||||||
'<div class="jqx-kanban-item" id="" style="border-radius:0px;">'
|
|
||||||
+ '<div class="jqx-kanban-item-color-status"></div>'
|
|
||||||
+ '<div class="jqx-kanban-item-avatar"></div>'
|
|
||||||
+ '<div class="jqx-kanban-item-text"></div>'
|
|
||||||
+ '<div class="jqx-kanban-item-footer">'
|
|
||||||
+ '<div style="float:right;"><div class="jqx-kanban-item-template-content"><i data-icon-name="More" role="presentation" aria-hidden="true" class="ms-Icon root-48" style="font-family: "FabricMDL2Icons";font-size: 1.25em;color: gray;"></i></div></div>'
|
|
||||||
+ '</div></div>';
|
|
||||||
const itemRenderer = (element: any, item: any, resource: any): void => {
|
|
||||||
let precentComplete = item.color as Number;
|
|
||||||
let style = "";
|
|
||||||
if(precentComplete <= .3)
|
|
||||||
{
|
|
||||||
style = "background-color:red";
|
|
||||||
}
|
|
||||||
else if(precentComplete <= .7)
|
|
||||||
{
|
|
||||||
style = "background-color:orange";
|
|
||||||
}
|
|
||||||
element[0].getElementsByClassName('jqx-kanban-item-color-status')[0].style = style;
|
|
||||||
if(!resource.common)
|
|
||||||
element[0].getElementsByClassName('jqx-kanban-item-avatar-image')[0].src = this.props.webUrl + "/_layouts/15/userphoto.aspx?size=M&username=" + resource.email;
|
|
||||||
};
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
template: template,
|
|
||||||
itemRenderer,
|
|
||||||
width: "100%",
|
|
||||||
listTitle: this.props.listTitle,
|
|
||||||
showBoard: false,
|
|
||||||
noItems: true,
|
|
||||||
taskDetails: {},
|
|
||||||
hideDialog:true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public render(): React.ReactElement<IKanbanBoardProps> {
|
|
||||||
const el = this.state.showBoard ?
|
|
||||||
!this.state.noItems ? <JqxKanban
|
|
||||||
width={this.state.width}
|
|
||||||
height={"100%"}
|
|
||||||
source={this.state.source}
|
|
||||||
columns={this.state.columns}
|
|
||||||
resources={this.state.resources}
|
|
||||||
onItemMoved={this._updateTask}
|
|
||||||
itemRenderer={this.state.itemRenderer}
|
|
||||||
template={this.state.template}
|
|
||||||
onItemAttrClicked={this._showTask}
|
|
||||||
/>: <div>No tasks found!</div> : <Spinner label="Loading tasks..." ariaLive="assertive" labelPosition="top" />;
|
|
||||||
const selectlist = !(this.state.listTitle && this.state.listTitle.length > 0) ? <div>Please choose a list</div> : null;
|
|
||||||
return (<>
|
|
||||||
{selectlist}
|
|
||||||
{el}
|
|
||||||
|
|
||||||
<Dialog
|
|
||||||
minWidth="600"
|
|
||||||
hidden={this.state.hideDialog}
|
|
||||||
onDismiss={this._closeDialog}
|
|
||||||
dialogContentProps={{
|
|
||||||
type: DialogType.largeHeader,
|
|
||||||
title: this.state.taskDetails.text,
|
|
||||||
subText: ''
|
|
||||||
}}
|
|
||||||
modalProps={{
|
|
||||||
isBlocking: false,
|
|
||||||
styles: { main: { minWidth:600 } }
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Stack>
|
|
||||||
<Stack horizontal horizontalAlign="stretch">
|
|
||||||
<Stack.Item align="auto" styles={rowStyle1}>
|
|
||||||
<span>% Complete</span>
|
|
||||||
</Stack.Item>
|
|
||||||
<Stack.Item align="stretch" styles={rowStyle2}>
|
|
||||||
<span>{ parseFloat(this.state.taskDetails.color) * 100 }</span>
|
|
||||||
</Stack.Item>
|
|
||||||
</Stack>
|
|
||||||
<Stack horizontal horizontalAlign="stretch">
|
|
||||||
<Stack.Item align="auto" styles={rowStyle1}>
|
|
||||||
<span>Description</span>
|
|
||||||
</Stack.Item>
|
|
||||||
<Stack.Item align="stretch" styles={rowStyle2}>
|
|
||||||
<span>{ ReactHtmlParser(this.state.taskDetails.content) }</span>
|
|
||||||
</Stack.Item>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
<Stack horizontal>
|
|
||||||
<Stack.Item align="auto" styles={rowStyle1}>
|
|
||||||
<span>Priority</span>
|
|
||||||
</Stack.Item>
|
|
||||||
<Stack.Item align="stretch" styles={rowStyle2}>
|
|
||||||
<span>{ this.state.taskDetails.tags }</span>
|
|
||||||
</Stack.Item>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
<Stack horizontal>
|
|
||||||
<Stack.Item align="auto" styles={rowStyle1}>
|
|
||||||
<span>Task Status</span>
|
|
||||||
</Stack.Item>
|
|
||||||
<Stack.Item align="stretch" styles={rowStyle2}>
|
|
||||||
<span>{ this.state.taskDetails.status }</span>
|
|
||||||
</Stack.Item>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
<DialogFooter>
|
|
||||||
<DefaultButton onClick={this._closeDialog} text="Close" />
|
|
||||||
</DialogFooter>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
</>);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static getDerivedStateFromProps(nextProps, prevState){
|
|
||||||
if(nextProps.listTitle!==prevState.listTitle){
|
|
||||||
return { listTitle: nextProps.listTitle};
|
|
||||||
}
|
|
||||||
else return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public componentDidUpdate(prevProps, prevState) {
|
|
||||||
if (prevState.listTitle !== this.state.listTitle) {
|
|
||||||
this.setState({ listTitle: this.props.listTitle,showBoard:false });
|
|
||||||
this._getData(this.props.listTitle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public componentDidMount(){
|
|
||||||
this._getData(this.props.listTitle);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _getData = (listTitle) => {
|
|
||||||
if(listTitle && listTitle.length > 0)
|
|
||||||
{
|
|
||||||
sp.web.lists.getByTitle(listTitle).fields.getByInternalNameOrTitle("Status").get()
|
|
||||||
.then(status => {
|
|
||||||
|
|
||||||
const cols = status.Choices.map((val,index) => {
|
|
||||||
return { text: val, dataField: val };
|
|
||||||
});
|
|
||||||
|
|
||||||
sp.web.lists.getByTitle(listTitle).items.getAll().then(res => {
|
|
||||||
|
|
||||||
const source = {
|
|
||||||
dataFields: this.sourceFields,
|
|
||||||
dataType: 'array',
|
|
||||||
localData: [ ...res ]
|
|
||||||
};
|
|
||||||
|
|
||||||
sp.web.siteUsers.get().then(users => {
|
|
||||||
const resourcesAdapterFunc = (): any => {
|
|
||||||
const resourcesSource = {
|
|
||||||
dataFields: this.resourceFields,
|
|
||||||
dataType: 'array',
|
|
||||||
localData: [...users]
|
|
||||||
};
|
|
||||||
const resourcesDataAdapter = new jqx.dataAdapter(resourcesSource);
|
|
||||||
return resourcesDataAdapter;
|
|
||||||
};
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
width: "100%",
|
|
||||||
columns: cols,
|
|
||||||
resources: resourcesAdapterFunc(),
|
|
||||||
source: new jqx.dataAdapter(source),
|
|
||||||
showBoard: true,
|
|
||||||
noItems: source.localData.length <= 0
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.setState({
|
|
||||||
showBoard:true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _updateTask = (event: any): void => {
|
|
||||||
let args = event.args;
|
|
||||||
// let itemId = args.itemId;
|
|
||||||
// let oldParentId = args.oldParentId;
|
|
||||||
// let newParentId = args.newParentId;
|
|
||||||
// let itemData = args.itemData;
|
|
||||||
// let oldColumn = args.oldColumn;
|
|
||||||
// let newColumn = args.newColumn;
|
|
||||||
sp.web.lists.getByTitle(this.props.listTitle).items.getById(args.itemId).update({
|
|
||||||
Status: args.newColumn.dataField
|
|
||||||
}).then(res => {
|
|
||||||
console.log("Task updated");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private _showTask = (event:any): void => {
|
|
||||||
if(event.args.attribute === "template")
|
|
||||||
{
|
|
||||||
this.setState({
|
|
||||||
taskDetails : {
|
|
||||||
...event.args.item
|
|
||||||
},
|
|
||||||
hideDialog: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _closeDialog = (): void => {
|
|
||||||
this.setState({ hideDialog: true });
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
.ordercolor {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
}
|
|
@ -0,0 +1,173 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import * as strings from 'KanbanBoardWebPartStrings';
|
||||||
|
|
||||||
|
import { DisplayMode, Guid, Environment, EnvironmentType } from '@microsoft/sp-core-library';
|
||||||
|
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||||
|
import { findIndex, isEqual, cloneDeep } from '@microsoft/sp-lodash-subset';
|
||||||
|
import { Spinner } from 'office-ui-fabric-react/lib/Spinner';
|
||||||
|
|
||||||
|
|
||||||
|
import { WebPartTitle } from "@pnp/spfx-controls-react/lib/WebPartTitle";
|
||||||
|
import { Placeholder } from "@pnp/spfx-controls-react/lib/Placeholder";
|
||||||
|
|
||||||
|
import {KanbanComponent,IKanbanBucket,IKanbanTask} from '../../../kanban';
|
||||||
|
|
||||||
|
import { mergeBucketsWithChoices } from './helper';
|
||||||
|
import { ISPKanbanService } from '../services/ISPKanbanService';
|
||||||
|
import SPKanbanService from '../services/SPKanbanService';
|
||||||
|
import MockKanbanService from '../services/MockKanbanService';
|
||||||
|
|
||||||
|
export interface IKanbanBoardV2Props {
|
||||||
|
hideWPTitle: boolean;
|
||||||
|
title: string;
|
||||||
|
displayMode: DisplayMode;
|
||||||
|
updateProperty: (value: string) => void;
|
||||||
|
context: WebPartContext;
|
||||||
|
listId: string;
|
||||||
|
configuredBuckets: IKanbanBucket[]; // need mearge with current readed
|
||||||
|
statekey: string; // force refresh ;)
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IKanbanBoardV2State {
|
||||||
|
loading: boolean;
|
||||||
|
isConfigured: boolean;
|
||||||
|
buckets: IKanbanBucket[];
|
||||||
|
tasks: IKanbanTask[];
|
||||||
|
errorMessage?: string;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class KanbanBoardV2 extends React.Component<IKanbanBoardV2Props, IKanbanBoardV2State> {
|
||||||
|
private choices: string[] = [];
|
||||||
|
private dataService: ISPKanbanService;
|
||||||
|
constructor(props: IKanbanBoardV2Props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
loading: false,
|
||||||
|
isConfigured: false,
|
||||||
|
buckets: [],
|
||||||
|
tasks: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
public componentDidMount(): void {
|
||||||
|
if (Environment.type == EnvironmentType.Local || Environment.type == EnvironmentType.Test) {
|
||||||
|
this.dataService= new MockKanbanService();
|
||||||
|
} else {
|
||||||
|
this.dataService = new SPKanbanService();
|
||||||
|
}
|
||||||
|
this._getData();
|
||||||
|
}
|
||||||
|
public shouldComponentUpdate(nextProps: IKanbanBoardV2Props, nextState: IKanbanBoardV2State): boolean {
|
||||||
|
if (!isEqual(this.state, nextState)) { return true; }
|
||||||
|
if (!isEqual(this.props, nextProps)) {
|
||||||
|
//stateKey
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
public componentDidUpdate(prevProps: IKanbanBoardV2Props) {
|
||||||
|
if (this.props.listId !== prevProps.listId) {
|
||||||
|
this._getData();
|
||||||
|
}
|
||||||
|
const currentPropBuckets: IKanbanBucket[] = mergeBucketsWithChoices(this.props.configuredBuckets, this.choices);
|
||||||
|
if (!isEqual(this.state.buckets, currentPropBuckets)) {
|
||||||
|
this.setState({ buckets: cloneDeep(currentPropBuckets) });
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public render(): React.ReactElement<IKanbanBoardV2Props> {
|
||||||
|
const { buckets, tasks, errorMessage } = this.state;
|
||||||
|
const { hideWPTitle, displayMode } = this.props;
|
||||||
|
const isConfigured: boolean = this.state.isConfigured;
|
||||||
|
const isLoading: boolean = this.state.loading;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{!hideWPTitle && (<WebPartTitle displayMode={displayMode}
|
||||||
|
title={this.props.title}
|
||||||
|
updateProperty={this.props.updateProperty} />
|
||||||
|
)}
|
||||||
|
{!isConfigured && !isLoading && (<Placeholder iconName='Edit'
|
||||||
|
iconText={strings.PlaceholderIconText}
|
||||||
|
description={strings.PlaceholderDescription}
|
||||||
|
buttonLabel={strings.PlaceholderButtonLabel}
|
||||||
|
hideButton={displayMode === DisplayMode.Read}
|
||||||
|
onConfigure={this._onConfigure} />
|
||||||
|
)}
|
||||||
|
{isConfigured && isLoading && (
|
||||||
|
<Spinner label={strings.SpinnerLabel} ariaLive="assertive" labelPosition="top" />
|
||||||
|
)}
|
||||||
|
{isConfigured && !isLoading && (
|
||||||
|
<KanbanComponent
|
||||||
|
buckets={buckets}
|
||||||
|
tasks={tasks}
|
||||||
|
tasksettings={{
|
||||||
|
showPriority: true,
|
||||||
|
showAssignedTo: true,
|
||||||
|
showTaskDetailsButton: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
taskactions={{
|
||||||
|
moved: this._moved.bind(this),
|
||||||
|
}}
|
||||||
|
showCommandbar={false}
|
||||||
|
/>)
|
||||||
|
}
|
||||||
|
{!!errorMessage && (<div>{errorMessage}</div>)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onConfigure = () => {
|
||||||
|
this.props.context.propertyPane.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private _moved(taskId: string, targetBucket: IKanbanBucket): void {
|
||||||
|
const elementsIndex = findIndex(this.state.tasks, element => element.taskId == taskId);
|
||||||
|
let newArray = [...this.state.tasks]; // same as Clone
|
||||||
|
newArray[elementsIndex].bucket = targetBucket.bucket;
|
||||||
|
this.dataService.updateTaskBucketMove(this.props.listId, +taskId, targetBucket.bucket)
|
||||||
|
.then(res => {
|
||||||
|
this.setState({ tasks: newArray });
|
||||||
|
}).catch(error => {
|
||||||
|
this.setState({ errorMessage: 'Error Update Task Item' });
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private _getData(): void {
|
||||||
|
if (!this.props.listId || this.props.listId.length == 0) {
|
||||||
|
this.setState({ isConfigured: false, loading: false });
|
||||||
|
} else {
|
||||||
|
const listId: string = this.props.listId;
|
||||||
|
this.dataService.getBuckets(listId).then((choices) => {
|
||||||
|
this.choices = choices;
|
||||||
|
const currentbuckets: IKanbanBucket[] = mergeBucketsWithChoices(this.props.configuredBuckets, this.choices);
|
||||||
|
if (!currentbuckets) {
|
||||||
|
this.setState({ isConfigured: false, loading: false, errorMessage: 'No Buckets found' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.dataService.getAllTasks(listId).then((tasks) => {
|
||||||
|
this.setState({
|
||||||
|
isConfigured: true,
|
||||||
|
loading: false,
|
||||||
|
errorMessage: undefined,
|
||||||
|
buckets: currentbuckets,
|
||||||
|
tasks: tasks
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
this.setState({ isConfigured: true, loading: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import * as ReactDom from 'react-dom';
|
||||||
|
import {
|
||||||
|
IPropertyPaneField,
|
||||||
|
PropertyPaneFieldType
|
||||||
|
} from '@microsoft/sp-property-pane';
|
||||||
|
|
||||||
|
import {KanbanBucketConfigurator, IKanbanBucketConfiguratorProps } from '../../../kanban';
|
||||||
|
import { IKanbanBucket } from '../../../kanban/IKanbanBucket';
|
||||||
|
|
||||||
|
export interface IPropertyPaneBucketConfig {
|
||||||
|
key: string;
|
||||||
|
properties: IKanbanBucket;
|
||||||
|
onPropertyChange(propertyPath: string, oldValue: any, newValue: any): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPropertyPaneBucketConfigInternal extends IPropertyPaneBucketConfig {
|
||||||
|
targetProperty: string;
|
||||||
|
onRender(elem: HTMLElement, ctx, changeCallback): void;
|
||||||
|
onDispose(elem: HTMLElement): void;
|
||||||
|
onChanged(targetProperty: string, value: IKanbanBucket): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class PropertyPaneBucketConfigBuilder implements IPropertyPaneField<IPropertyPaneBucketConfigInternal> {
|
||||||
|
// Properties defined by IPropertyPaneField
|
||||||
|
public type: PropertyPaneFieldType = PropertyPaneFieldType.Custom;
|
||||||
|
public targetProperty: string;
|
||||||
|
public properties: IPropertyPaneBucketConfigInternal;
|
||||||
|
private elem: HTMLElement;
|
||||||
|
// Custom properties
|
||||||
|
private customProperties: any;
|
||||||
|
private onPropertyChange: (propertyPath: string, oldValue: any, newValue: any) => void;
|
||||||
|
|
||||||
|
public constructor(_targetProperty: string, _properties: IPropertyPaneBucketConfigInternal) {
|
||||||
|
|
||||||
|
this.customProperties = _properties.properties;
|
||||||
|
this.targetProperty = _targetProperty;
|
||||||
|
this.onPropertyChange = _properties.onPropertyChange;
|
||||||
|
this.properties = _properties;
|
||||||
|
this.properties.onRender = this.render.bind(this);
|
||||||
|
this.properties.onDispose = this.dispose;
|
||||||
|
}
|
||||||
|
|
||||||
|
private render(elem: HTMLElement, ctx?, changeCallback?: (targetProperty: string, value: any) => void): void {
|
||||||
|
if (!this.elem) {
|
||||||
|
this.elem = elem;
|
||||||
|
}
|
||||||
|
const configprops: IKanbanBucketConfiguratorProps = {
|
||||||
|
index: 1,
|
||||||
|
bucket: this.customProperties,
|
||||||
|
update: this.saveBucketConfig.bind(this)
|
||||||
|
};
|
||||||
|
const element: React.ReactElement<IKanbanBucketConfiguratorProps> = React.createElement(KanbanBucketConfigurator,
|
||||||
|
{ ...configprops }
|
||||||
|
|
||||||
|
);
|
||||||
|
ReactDom.render(element, elem);
|
||||||
|
}
|
||||||
|
private saveBucketConfig(index: number, value: IKanbanBucket): void {
|
||||||
|
if (this.onPropertyChange) {
|
||||||
|
this.onPropertyChange(this.targetProperty, this.customProperties, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private dispose(elem: HTMLElement): void { }
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PropertyPaneBucketConfigComponent(targetProperty: string, properties: IPropertyPaneBucketConfig):
|
||||||
|
IPropertyPaneField<IPropertyPaneBucketConfigInternal> {
|
||||||
|
var newProperties: IPropertyPaneBucketConfigInternal = {
|
||||||
|
key: properties.key,
|
||||||
|
properties: properties.properties,
|
||||||
|
targetProperty: targetProperty,
|
||||||
|
onPropertyChange: properties.onPropertyChange,
|
||||||
|
onDispose: null,
|
||||||
|
onRender: null,
|
||||||
|
onChanged: null
|
||||||
|
};
|
||||||
|
return new PropertyPaneBucketConfigBuilder(targetProperty, newProperties);
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { IKanbanBucket } from '../../../kanban';
|
||||||
|
import styles from './KanbanBoardV2.module.scss';
|
||||||
|
|
||||||
|
export const bucketOrder = (item:IKanbanBucket, index:number): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
{<span className={styles.ordercolor} style={{ backgroundColor: item.color?item.color:'none' }}></span>}
|
||||||
|
{item.bucketheadline?item.bucketheadline:item.bucket}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { IKanbanBucket } from "../../../kanban";
|
||||||
|
|
||||||
|
export function mergeBucketsWithChoices(inB: IKanbanBucket[], choices: string[]): IKanbanBucket[] {
|
||||||
|
const currentbuckets: IKanbanBucket[] = [];
|
||||||
|
if (inB &&
|
||||||
|
inB.length > 0 &&
|
||||||
|
choices && choices.length > 0) {
|
||||||
|
inB.forEach((b) => {
|
||||||
|
if (choices.filter((c) => c === b.bucket).length === 1) {
|
||||||
|
currentbuckets.push(b);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return currentbuckets;
|
||||||
|
|
||||||
|
} else if (choices && choices.length) {
|
||||||
|
//Adding with default values
|
||||||
|
choices.forEach((x) => {
|
||||||
|
currentbuckets.push({
|
||||||
|
bucket: x,
|
||||||
|
bucketheadline: x,
|
||||||
|
percentageComplete: 0
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return currentbuckets;
|
||||||
|
} else {
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,22 @@
|
||||||
define([], function() {
|
define([], function () {
|
||||||
return {
|
return {
|
||||||
"PropertyPaneDescription": "Description",
|
"PropertyPaneDescription": "Description",
|
||||||
"BasicGroupName": "Basic Configuration",
|
"BasicGroupName": "Basic Configuration",
|
||||||
"ListTitleFieldLabel": "List Title"
|
|
||||||
|
|
||||||
|
"PercentComplete": "Percent Complete",
|
||||||
|
|
||||||
|
"propertyPaneHideWPHeadline": "Hide WebPart-Title?",
|
||||||
|
"propertyPaneHideWPHeadlineShow": "show",
|
||||||
|
"propertyPaneHideWPHeadlineHide": "hide",
|
||||||
|
"propertyPaneSelectList": "Select List",
|
||||||
|
"propertyPaneGroupNameOrderBuckets": "Order Buckets",
|
||||||
|
"propertyPaneLabelOrderBuckets": "",
|
||||||
|
"propertyPaneBucketConfiguration": "Bucket Configuration",
|
||||||
|
|
||||||
|
"PlaceholderIconText": "Configure your web part",
|
||||||
|
"PlaceholderDescription": "Please configure the web part.",
|
||||||
|
"PlaceholderButtonLabel": "Configure",
|
||||||
|
"SpinnerLabel": "Seriously, still loading..."
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,7 +1,24 @@
|
||||||
declare interface IKanbanBoardWebPartStrings {
|
declare interface IKanbanBoardWebPartStrings {
|
||||||
PropertyPaneDescription: string;
|
PropertyPaneDescription: string;
|
||||||
BasicGroupName: string;
|
BasicGroupName: string;
|
||||||
ListTitleFieldLabel: string;
|
|
||||||
|
//Resource is used for Translating percentage SP Field Title would be better
|
||||||
|
PercentComplete: string;
|
||||||
|
|
||||||
|
propertyPaneHideWPHeadline: string;
|
||||||
|
propertyPaneHideWPHeadlineShow: string;
|
||||||
|
propertyPaneHideWPHeadlineHide: string;
|
||||||
|
propertyPaneSelectList: string;
|
||||||
|
|
||||||
|
propertyPaneGroupNameOrderBuckets: string;
|
||||||
|
propertyPaneLabelOrderBuckets: string;
|
||||||
|
propertyPaneBucketConfiguration: string;
|
||||||
|
|
||||||
|
|
||||||
|
PlaceholderIconText: string;
|
||||||
|
PlaceholderDescription: string;
|
||||||
|
PlaceholderButtonLabel: string;
|
||||||
|
SpinnerLabel: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module 'KanbanBoardWebPartStrings' {
|
declare module 'KanbanBoardWebPartStrings' {
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { IKanbanTask } from "../../../kanban";
|
||||||
|
|
||||||
|
export interface ISPKanbanService {
|
||||||
|
|
||||||
|
updateTaskBucketMove(listId:string,taskId: number, bucket: string): Promise<boolean>;
|
||||||
|
getAllTasks(listId:string,): Promise<IKanbanTask[]>;
|
||||||
|
getBuckets(listId:string,): Promise<string[]>;
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
import { ISPKanbanService } from "./ISPKanbanService";
|
||||||
|
import "@pnp/polyfill-ie11";
|
||||||
|
import { sp } from '@pnp/sp';
|
||||||
|
import { IKanbanTask, KanbanTaskMamagedPropertyType } from "../../../kanban";
|
||||||
|
import * as strings from 'KanbanBoardWebPartStrings';
|
||||||
|
|
||||||
|
export default class MockKanbanService implements ISPKanbanService {
|
||||||
|
|
||||||
|
|
||||||
|
public updateTaskBucketMove(listid: string, taskId: number, bucket: string): Promise<boolean> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(() => resolve(true), 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
public getAllTasks(listId: string): Promise < IKanbanTask[] > {
|
||||||
|
const data=[1,2,3,4];
|
||||||
|
|
||||||
|
const tasks: IKanbanTask[] = data.map((x) => {
|
||||||
|
return {
|
||||||
|
taskId: 'tid' + x,
|
||||||
|
title: 'Title'+x,
|
||||||
|
htmlDescription: '<p>Body <b>Bold</b></p>',
|
||||||
|
assignedTo:
|
||||||
|
{
|
||||||
|
text: 'Person '+x
|
||||||
|
},
|
||||||
|
priority: 'Prio'+x,
|
||||||
|
bucket: 'Status'+x,
|
||||||
|
mamagedProperties: [
|
||||||
|
{
|
||||||
|
name: 'PercentComplete',
|
||||||
|
displayName: strings.PercentComplete,
|
||||||
|
type: KanbanTaskMamagedPropertyType.percent,
|
||||||
|
value: x/10 /* 10/20 30 .. percent */
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(() => resolve(tasks), 1000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public getBuckets(listId: string): Promise < string[] > {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(() => resolve([
|
||||||
|
'Status1',
|
||||||
|
'Status2',
|
||||||
|
'Status3',
|
||||||
|
'Status4',
|
||||||
|
'Status5',
|
||||||
|
]), 1000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
import { ISPKanbanService } from "./ISPKanbanService";
|
||||||
|
import "@pnp/polyfill-ie11";
|
||||||
|
import { sp } from '@pnp/sp';
|
||||||
|
import { IKanbanTask, KanbanTaskMamagedPropertyType } from "../../../kanban";
|
||||||
|
import * as strings from 'KanbanBoardWebPartStrings';
|
||||||
|
|
||||||
|
export default class SPKanbanService implements ISPKanbanService {
|
||||||
|
|
||||||
|
|
||||||
|
public updateTaskBucketMove(listid: string, taskId: number, bucket: string): Promise<boolean> {
|
||||||
|
return sp.web.lists.getById(listid).items.getById(+taskId).update({
|
||||||
|
Status: bucket
|
||||||
|
}).then(() => { return true; });
|
||||||
|
}
|
||||||
|
public getAllTasks(listId: string, ): Promise<IKanbanTask[]> {
|
||||||
|
|
||||||
|
const odatafiels: string[] = ['AssignedTo/Id', 'AssignedTo/Title', 'AssignedTo/Name', 'AssignedTo/EMail',
|
||||||
|
'ID', 'Title', 'Status', 'Priority', 'PercentComplete', 'Body'
|
||||||
|
];
|
||||||
|
|
||||||
|
return sp.web.lists.getById(listId).items
|
||||||
|
.select(odatafiels.join(','))
|
||||||
|
.expand('AssignedTo').getAll().then(res => {
|
||||||
|
const tasks: IKanbanTask[] = res.map((x) => {
|
||||||
|
return {
|
||||||
|
taskId: '' + x.ID,
|
||||||
|
title: x.Title,
|
||||||
|
htmlDescription: x.Body,
|
||||||
|
assignedTo: (x.AssignedTo && (x.AssignedTo).length === 1) ?
|
||||||
|
{
|
||||||
|
text: x.AssignedTo[0].Title
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
priority: x.Priority,
|
||||||
|
bucket: x.Status,
|
||||||
|
mamagedProperties: [
|
||||||
|
{
|
||||||
|
name: 'PercentComplete',
|
||||||
|
displayName: strings.PercentComplete,
|
||||||
|
type: KanbanTaskMamagedPropertyType.percent,
|
||||||
|
value: x.PercentComplete
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return tasks;
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
public getBuckets(listId: string, ): Promise<string[]> {
|
||||||
|
return sp.web.lists.getById(listId).fields.getByInternalNameOrTitle("Status").get()
|
||||||
|
.then(status => status.Choices.map((val, index) => {
|
||||||
|
return val;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"extends": "./node_modules/@microsoft/rush-stack-compiler-2.9/includes/tsconfig-web.json",
|
"extends": "node_modules/@microsoft/rush-stack-compiler-3.3/includes/tsconfig-web.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es5",
|
"target": "es5",
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
@ -29,7 +29,7 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src/**/*.ts"
|
"src/**/*.ts", "src/webparts/kanbanBoard/components/bucketOrder.tsx"
|
||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"node_modules",
|
"node_modules",
|
||||||
|
|
Loading…
Reference in New Issue