Ui and Documentation

This commit is contained in:
petkir 2020-06-27 23:04:23 +02:00
parent 7b037ef047
commit 052513b08e
18 changed files with 305 additions and 499 deletions

View File

@ -2,7 +2,8 @@
## 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 which shows a Kanban board using Fluent UI components ([FluentUI](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/kanban-board.gif)
@ -26,7 +27,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 +35,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/))
## Version history ## Version history
@ -41,11 +43,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 FluentUI 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 PropertyPane)
* PNP Order PropertyPane Control (Change Position of Buckets)
* PNP List Selection PropertyPane Control (including Filter on BaseTemplateId)
* Usage of BucketEdit in Pane (Use an Component in PropertyPane (Custom Field))
* Fluent UI
* PNP JS DataConnection to SharePoint
Thanks form @petkir to:
Daniel Westerdale for Testing and inspiration (everytime again)
Hugo Bernier for Inspiration to use Office UI Fabric
Jean-Philippe CIVADE for Bug Report IE11 (initiator of rewrite of this Sample)
## 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.**
### only For Version 1
**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** **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 +82,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 an 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

View File

@ -3,7 +3,7 @@
"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.1.0", "version": "2.0.0.0",
"includeClientSideAssets": true, "includeClientSideAssets": true,
"skipFeatureDeployment": true, "skipFeatureDeployment": true,
"isDomainIsolated": false "isDomainIsolated": false

View File

@ -1,8 +1,7 @@
.bucket { .bucket {
display: table-cell;
border-right: 1px solid gray; // border-right: 1px solid gray;
.headline { .headline {
padding: 2px 10px; padding: 2px 10px;
line-height: 2em; line-height: 2em;
@ -19,11 +18,27 @@
right: 0px; right: 0px;
} }
} }
&.dragover{ .taskArea{
background-color: lightgray; padding: 5px;
} }
.placeholder{ .placeholder {
background-color: rgb(180, 180, 180); 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;
}
*/
} }

View File

@ -11,6 +11,7 @@ import classNames from 'classnames';
import * as strings from 'KanbanBoardStrings'; import * as strings from 'KanbanBoardStrings';
export interface IKanbanBucketProps extends IKanbanBucket { export interface IKanbanBucketProps extends IKanbanBucket {
buckettasks: IKanbanTask[]; buckettasks: IKanbanTask[];
tasksettings: IKanbanBoardTaskSettings; tasksettings: IKanbanBoardTaskSettings;
@ -19,12 +20,10 @@ export interface IKanbanBucketProps extends IKanbanBucket {
addTask?: (bucket: string) => void; addTask?: (bucket: string) => void;
onDragStart: (event, taskId: string, bucket: string) => void; onDragStart: (event, taskId: string, bucket: string) => void;
onDragOver: (event, targetbucket: string) => void;
onDragLeave: (event, targetbucket: string) => void;
onDrop: (event, targetbucket: string) => void;
onDragEnd: (event, taskId: string, bucket: string) => void; onDragEnd: (event, taskId: string, bucket: string) => void;
leavingTaskId?: string; leavingTaskId?: string;
leavingBucket?: string; leavingBucket?: string;
@ -52,16 +51,16 @@ export default class KanbanBucket extends React.Component<IKanbanBucketProps, IK
hope this will be translated hope this will be translated
*/ */
public render(): React.ReactElement<IKanbanBucketProps> { public render(): React.ReactElement<IKanbanBucketProps> {
const { bucket, bucketheadline, color, buckettasks, const { bucket, bucketheadline, color, buckettasks,
tasksettings, percentageComplete, tasksettings, percentageComplete,
allowAddTask, overBucket, leavingTaskId, leavingBucket } = this.props; allowAddTask, overBucket, leavingTaskId, leavingBucket } = this.props;
return ( return (
<div <div
className={classNames({ [styles.bucket]: true, [styles.dragover]: !!(overBucket && overBucket === bucket) })}
className={styles.bucket}
key={bucket} key={bucket}
onDragOver={(event) => this.props.onDragOver(event, bucket)}
onDragLeave={(event) => this.props.onDragLeave(event, bucket)}
onDrop={(event) => this.props.onDrop(event, bucket)}
> >
<div className={styles.headline} <div className={styles.headline}
@ -77,12 +76,15 @@ export default class KanbanBucket extends React.Component<IKanbanBucketProps, IK
> >
{strings.AddTask} {strings.AddTask}
</ActionButton>)} </ActionButton>)}
<div className={styles.taskArea}>
{ {
buckettasks.map((t) => { buckettasks.map((t) => {
const merge = { ...t, ...tasksettings, }; const merge = { ...t, ...tasksettings, };
const isMoving = (t.taskId === leavingTaskId && t.bucket === leavingBucket); const isMoving = (t.taskId === leavingTaskId && t.bucket === leavingBucket);
return (<div className={isMoving ? styles.placeholder : undefined} key={'' + t.taskId} > return (<div
className={styles.taskplaceholder + (isMoving ? styles.placeholder : '')}
key={'' + t.taskId} >
<KanbanTask <KanbanTask
key={'task' + t.taskId} key={'task' + t.taskId}
{...merge} {...merge}
@ -95,6 +97,7 @@ export default class KanbanBucket extends React.Component<IKanbanBucketProps, IK
); );
}) })
} }
</div>
</div > </div >
); );
} }

View File

@ -1,4 +1,11 @@
.kanbanBoard { .kanbanBoard {
display: table; display: table;
width: 100%; width: 100%;
} .bucketwrapper {
border: 1px solid transparent;
display: table-cell;
}
.dragover {
border-color: "[theme: themePrimary, default: #0078d7]";
}
}

View File

@ -17,7 +17,7 @@ import { clone } from '@microsoft/sp-lodash-subset';
import { CommandBar } from 'office-ui-fabric-react/lib/CommandBar'; import { CommandBar } from 'office-ui-fabric-react/lib/CommandBar';
import { TooltipHost } from 'office-ui-fabric-react'; import { TooltipHost, findIndex } from 'office-ui-fabric-react';
export interface IKanbanComponentProps { export interface IKanbanComponentProps {
buckets: IKanbanBucket[]; buckets: IKanbanBucket[];
@ -38,7 +38,6 @@ export interface IKanbanComponentProps {
export interface IKanbanComponentState { export interface IKanbanComponentState {
leavingTaskId?: string; leavingTaskId?: string;
leavingBucket?: string; leavingBucket?: string;
overBucket?: string;
openDialog: boolean; openDialog: boolean;
openTaskId?: string; openTaskId?: string;
dialogState?: DialogState; dialogState?: DialogState;
@ -54,6 +53,7 @@ export enum DialogState {
export default class KanbanComponent extends React.Component<IKanbanComponentProps, IKanbanComponentState> { export default class KanbanComponent extends React.Component<IKanbanComponentProps, IKanbanComponentState> {
private dragelement?: IKanbanTask; private dragelement?: IKanbanTask;
private bucketsref: any[];
constructor(props: IKanbanComponentProps) { constructor(props: IKanbanComponentProps) {
super(props); super(props);
@ -61,15 +61,16 @@ export default class KanbanComponent extends React.Component<IKanbanComponentPro
openDialog: false, openDialog: false,
leavingTaskId: null, leavingTaskId: null,
leavingBucket: null, leavingBucket: null,
overBucket: null
};
};
this.bucketsref = [];
} }
public render(): React.ReactElement<IKanbanComponentProps> { public render(): React.ReactElement<IKanbanComponentProps> {
const { buckets, tasks, tasksettings, taskactions, showCommandbar } = this.props; const { buckets, tasks, tasksettings, taskactions, showCommandbar } = this.props;
const { openDialog } = this.state; const { openDialog } = this.state;
const { leavingBucket, leavingTaskId, overBucket } = this.state; const bucketwidth: number = buckets.length > 0 ? 100 / buckets.length : 100;
const { leavingBucket, leavingTaskId } = this.state;
const wrappedTaskActions: IKanbanBoardTaskActions = { const wrappedTaskActions: IKanbanBoardTaskActions = {
}; };
@ -85,25 +86,34 @@ export default class KanbanComponent extends React.Component<IKanbanComponentPro
<div className={styles.kanbanBoard}> <div className={styles.kanbanBoard}>
{ {
buckets.map((b) => { buckets.map((b, i) => {
const merge = { ...b, ...this.state }; const merge = { ...b, ...this.state };
return (<KanbanBucket return (<div
key={b.bucket} style={{ width: bucketwidth ? bucketwidth + '%' : '100%' }}
{...merge} className={styles.bucketwrapper}
buckettasks={tasks.filter((x) => x.bucket == b.bucket)} ref={bucketContent => this.bucketsref[i] = bucketContent}
tasksettings={tasksettings} 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}
buckettasks={tasks.filter((x) => x.bucket == b.bucket)}
tasksettings={tasksettings}
toggleCompleted={this.props.taskactions && this.props.taskactions.toggleCompleted ? this.props.taskactions.toggleCompleted : undefined} toggleCompleted={this.props.taskactions && this.props.taskactions.toggleCompleted ? this.props.taskactions.toggleCompleted : undefined}
addTask={this.internalAddTask.bind(this)} addTask={this.internalAddTask.bind(this)}
openDetails={this.internalOpenDialog.bind(this)} openDetails={this.internalOpenDialog.bind(this)}
onDrop={this.onDrop.bind(this)}
onDragLeave={this.onDragLeave.bind(this)} onDragStart={this.onDragStart.bind(this)}
onDragOver={this.onDragOver.bind(this)} onDragEnd={this.onDragEnd.bind(this)}
onDragStart={this.onDragStart.bind(this)} />
onDragEnd={this.onDragEnd.bind(this)} </div>);
/>);
} }
@ -211,30 +221,30 @@ export default class KanbanComponent extends React.Component<IKanbanComponentPro
private internalTaskDetailRenderer(task: IKanbanTask): JSX.Element { private internalTaskDetailRenderer(task: IKanbanTask): JSX.Element {
const {tasksettings} = this.props; const { tasksettings } = this.props;
return (<Stack> return (<Stack>
{tasksettings && tasksettings.showPriority && ( {tasksettings && tasksettings.showPriority && (
<KanbanTaskManagedProp <KanbanTaskManagedProp
name="assignedTo" name="assignedTo"
displayName={strings.Priority} displayName={strings.Priority}
type={KanbanTaskMamagedPropertyType.string } type={KanbanTaskMamagedPropertyType.string}
value={task.priority} value={task.priority}
key={'assignedToProp'} /> key={'assignedToProp'} />
)} )}
{tasksettings && tasksettings.showAssignedTo && (<KanbanTaskManagedProp {tasksettings && tasksettings.showAssignedTo && (<KanbanTaskManagedProp
name="assignedTo" name="assignedTo"
displayName={strings.AssignedTo} displayName={strings.AssignedTo}
type={KanbanTaskMamagedPropertyType.person } type={KanbanTaskMamagedPropertyType.person}
value={task.assignedTo} value={task.assignedTo}
key={'assignedToProp'} /> key={'assignedToProp'} />
)} )}
<KanbanTaskManagedProp <KanbanTaskManagedProp
name="assignedTo" name="assignedTo"
displayName={strings.HtmlDescription} displayName={strings.HtmlDescription}
type={KanbanTaskMamagedPropertyType.html } type={KanbanTaskMamagedPropertyType.html}
value={task.htmlDescription} value={task.htmlDescription}
key={'htmlDescriptionProp'} /> key={'htmlDescriptionProp'} />
{task.mamagedProperties && ( {task.mamagedProperties && (
task.mamagedProperties.map((p, i) => { task.mamagedProperties.map((p, i) => {
return ( return (
@ -295,17 +305,24 @@ export default class KanbanComponent extends React.Component<IKanbanComponentPro
} }
} }
private onDragLeave(event): void { private onDragLeave(event,bucket): void {
console.log('onDragLeave'); const index = findIndex(this.props.buckets, element => element.bucket == bucket);
/* if (this.bucketRef.current.classList.contains(styles.dragover)) { if (index != -1 && this.bucketsref.length > index ){
this.bucketRef.current.classList.remove(styles.dragover)
}*/ //&& this.bucketsref[index].classList.contains(styles.dragover)) {
this.bucketsref[index].classList.remove(styles.dragover);
}
} }
private onDragEnd(event): void { private onDragEnd(event): void {
console.log('onDragEnd'); console.log('onDragEnd');
this.dragelement = undefined; this.dragelement = undefined;
this.setState({
leavingTaskId: null,
leavingBucket: null,
});
} }
private onDragStart(event, taskId: string, bucket: string): void { private onDragStart(event, taskId: string, bucket: string): void {
@ -316,6 +333,7 @@ export default class KanbanComponent extends React.Component<IKanbanComponentPro
if (taskitem.length === 1) { if (taskitem.length === 1) {
console.log('onDragStart taskitem check done'); console.log('onDragStart taskitem check done');
event.dataTransfer.setData("text", taskId); event.dataTransfer.setData("text", taskId);
event.dataTransfer.effectAllowed = 'copy';
//event.dataTransfer.setData("sourcebucket", bucket); //event.dataTransfer.setData("sourcebucket", bucket);
//set element because event.dataTransfer is empty by DragOver //set element because event.dataTransfer is empty by DragOver
console.log('set dragelement'); console.log('set dragelement');
@ -337,24 +355,24 @@ export default class KanbanComponent extends React.Component<IKanbanComponentPro
private onDragOver(event, targetbucket: string): void { private onDragOver(event, targetbucket: string): void {
event.preventDefault(); event.preventDefault();
console.log('onDragOver this.dragelement');
console.log(this.dragelement);
if (this.dragelement.bucket !== targetbucket) { if (this.dragelement.bucket !== targetbucket) {
/* if (!this.bucketRef.current.classList.contains(styles.dragover)) { const index = findIndex(this.props.buckets, element => element.bucket == targetbucket);
this.bucketRef.current.classList.add(styles.dragover) if (index > -1 && this.bucketsref.length > index ){
}*/ console.log(this.bucketsref[index]);
} else { console.log(this.bucketsref[index].classList);
//&& this.bucketsref[index].classList.contains(styles.dragover)) {
this.bucketsref[index].classList.add(styles.dragover);
}
} }
} }
private onDrop(event, targetbucket: string): void { private onDrop(event, targetbucket: string): void {
console.log('onDrop'); console.log('onDrop');
/* if (this.bucketRef.current.classList.contains(styles.dragover)) { if (this.bucketsref && this.bucketsref.length>0) {
this.bucketRef.current.classList.remove(styles.dragover) this.bucketsref.forEach(x=> {x.classList.remove(styles.dragover);});
}*/ }
if (this.dragelement.bucket !== targetbucket) { if (this.dragelement.bucket !== targetbucket) {
//event.dataTransfer.getData("text"); //event.dataTransfer.getData("text");
const taskId = this.dragelement.taskId; const taskId = this.dragelement.taskId;
@ -378,7 +396,7 @@ export default class KanbanComponent extends React.Component<IKanbanComponentPro
this.setState({ this.setState({
leavingTaskId: null, leavingTaskId: null,
leavingBucket: null, leavingBucket: null,
overBucket: null,
}); });
} }

View File

@ -1,20 +1,45 @@
.taskcard { .taskcard {
display: block; padding: 5px;
background-color: initial; background-color:initial;
.titlerow { // box-shadow: $ms-depth-shadow-8;
.title{ box-shadow: 0 3.2px 7.2px 0 rgba(0,0,0,.132), 0 0.6px 1.8px 0 rgba(0,0,0,.108);
display: inline; .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 { .membersAndLabels {
display: block;
width: 100%;
.labels, .labels,
.priority, .priority,
.assignedto { .assignedto {
display: inline; padding: 7px 0px;
display: block;
width: 100%;
overflow: hidden;
} }
} }
} }
.moving{ .moving {
background-color: rgb(180, 100, 100) !important; background-color:"[theme: neutralLight, default: #eaeaea]";
} }

View File

@ -6,7 +6,7 @@ import { IKanbanTask } from './IKanbanTask';
import { IKanbanBoardTaskSettings } from './IKanbanBoardTaskSettings'; import { IKanbanBoardTaskSettings } from './IKanbanBoardTaskSettings';
import { IKanbanBoardTaskActions } from './IKanbanBoardTaskActions'; import { IKanbanBoardTaskActions } from './IKanbanBoardTaskActions';
import classNames from 'classnames'; import classNames from 'classnames';
import { IconNames, Persona, PersonaSize } from 'office-ui-fabric-react'; import { IconNames, Persona, PersonaSize, Stack } from 'office-ui-fabric-react';
export interface IKanbanTaskProps extends IKanbanTask, IKanbanBoardTaskSettings { export interface IKanbanTaskProps extends IKanbanTask, IKanbanBoardTaskSettings {
@ -29,29 +29,33 @@ export default class KanbanTask extends React.Component<IKanbanTaskProps, IKanba
const { title, showPriority, showAssignedTo, isCompleted, isMoving, showTaskDetailsButton } = this.props; const { title, showPriority, showAssignedTo, isCompleted, isMoving, showTaskDetailsButton } = this.props;
const showCompleted = !!this.props.toggleCompleted; const showCompleted = !!this.props.toggleCompleted;
const iconCompleted = { iconName: isCompleted ? 'RadioBtnOn' : 'RadioBtnOff' }; const iconCompleted = { iconName: isCompleted ? 'RadioBtnOn' : 'RadioBtnOff' };
/*
className={classNames({ [styles.taskcard]: true, [styles.moving]: isMoving })}
*/
return ( return (
<div className={classNames({ [styles.taskcard]: true, [styles.moving]: isMoving })} <div
className={classNames({ [styles.taskcard]: true, [styles.moving]: isMoving })}
onDragStart={this.props.onDragStart} onDragStart={this.props.onDragStart}
onDragEnd={this.props.onDragEnd} onDragEnd={this.props.onDragEnd}
draggable draggable
> >
<div className={styles.titlerow}> <div className={styles.titlerow}>
{showCompleted && ( {showCompleted && (
<IconButton <div className={styles.isCompleted} ><IconButton
iconProps={iconCompleted} iconProps={iconCompleted}
title={isCompleted ? strings.IsCompleted : strings.IsNotCompleted} title={isCompleted ? strings.IsCompleted : strings.IsNotCompleted}
ariaLabel={isCompleted ? strings.IsCompleted : strings.IsNotCompleted} ariaLabel={isCompleted ? strings.IsCompleted : strings.IsNotCompleted}
onClick={this._toggleCompleted.bind(this)} onClick={this._toggleCompleted.bind(this)}
/>) /></div>)
} }
<div className={styles.title}>{title}</div> <div className={styles.title}>{title}</div>
{showTaskDetailsButton && ( {showTaskDetailsButton && (
<IconButton <div className={styles.details}><IconButton
iconProps={{ iconName: 'More' }} iconProps={{ iconName: 'More' }}
title={strings.OpenDetails} title={strings.OpenDetails}
ariaLabel={strings.OpenDetails} ariaLabel={strings.OpenDetails}
onClick={this._openDetails.bind(this)} onClick={this._openDetails.bind(this)}
/>) /></div>)
} }
</div> </div>

View File

@ -1,15 +1,43 @@
# its only Prototyping
Thinking about Kanban component with Fluent Ui Components # Next Steps Component:
* think about Promise Task Actions, because actions are async
* EditSchema To support Edit
-------------------------------
# KanbanComponent Control
## current This control renders a KanbanBoard which can be used to show Tasks and move it from one State to an Other.
allowMove from one Bucket to the Other tested
move task to other Bucket works
internalDislplayRenderer: Person / Persons
BucketEdit Component (can be used in CustomPropertyPane)
playing with drag visibility **Control in Action**
![KanbanBoard control](../assets/KanbanBoard.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}
/>
```
Bucket Bucket
``` ```
buckets:[ buckets:[
@ -21,10 +49,6 @@ Bucket
], ],
``` ```
with such a structure its possible to use
PropertyFieldOrder
PropertyFieldColorPicker
or a wrapper to warp PropertyFieldColorPicker with some other in a Custom Control
Task Task
``` ```
@ -34,14 +58,81 @@ Task
{taskId: '4', title:'test 4',bucket:'Test4'}, {taskId: '4', title:'test 4',bucket:'Test4'},
{taskId: '5', title:'test 5',bucket:'Test3'}, {taskId: '5', title:'test 5',bucket:'Test3'},
``` ```
Something like this sould come out, but styling is currently bad
![prototype](./img1.PNG "prototype")
Something like this sould come out, but styling is currently bad
![prototype](./sample.gif "prototype on 2nd day")
# IMPORTANT ## Implementation
The KanbanBoard control can be configured with the following properties:
### IKanbanBucket
| Property | Type | Required | Description | Default |
| ---- | ---- | ---- | ---- | ---- |
bucket:string;
bucketheadline:string;
percentageComplete: number;
color?:string;
allowAddTask?:boolean;
### IKanbanTask
| Property | Type | Required | Description | Default |
| ---- | ---- | ---- | ---- | ---- |
taskId: string;
title: string;
isCompleted?: boolean;
assignedTo?: IPersonaProps;
htmlDescription?:string;
priority?:string;
bucket: string;
mamagedProperties?: IKanbanTaskManagedProps[];
### IKanbanComponentProps
| Property | Type | Required | Description | Default |
| ---- | ---- | ---- | ---- | ---- |
#### IKanbanBoardTaskSettings
| Property | Type | Required | Description | Default |
| ---- | ---- | ---- | ---- | ---- |
#### IKanbanBoardTaskActions
| Property | Type | Required | Description | Default |
| ---- | ---- | ---- | ---- | ---- |
#### IKanbanBoardRenderers
| Property | Type | Required | Description | Default |
| ---- | ---- | ---- | ---- | ---- |
#### IKanbanTaskManagedProps
| Property | Type | Required | Description | Default |
| ---- | ---- | ---- | ---- | ---- |
IPersonaProps reference to Fluent UI
## Samples
### Custom Detail View Renderer
```typescript
```
### Use SharePoint New and Edit Form
this is the Classic Form on the ListItems
```typescript
```
### Use EditSchema to Configure New and Edit Form in this Component
this functionality supports only some Field Types
```typescript
```
### Disallow Move from One Bucket to an Other
this functionality supports only some Field Types
```typescript
```
## Leanings
``` ```
IKanbanTask { IKanbanTask {
taskId: string; taskId: string;
@ -56,25 +147,4 @@ The second big thing is IE allows only to set the value 'text' event.dataTransfe
``` ```
event.dataTransfer.setData('xyz','1') event.dataTransfer.setData('xyz','1')
Unexpected call to method or property access. Unexpected call to method or property access.
``` ```
# Next Steps Component:
* think about Promise Task Actions, because actions are async
* EditSchema To support Edit and New PNP Controls :)
# Webpart Steps
* DataConnection
** Task missing
* BucketEdit Does not refresh Component
# Webpart Steps Done
* PNP Placeholder Control for Config
* PNP WebpartTitle Control
* Usage of BucketEdit in pane
* PNP Order pane Control

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 219 KiB

View File

@ -58,19 +58,6 @@ export default class KanbanBoardWebPart extends BaseClientSideWebPart<IKanbanBoa
} }
public render(): void { public render(): void {
/*
const element: React.ReactElement<IKanbanBoardProps > = React.createElement(
KanbanBoard,
{
listTitle: this.properties.listTitle,
webUrl: this.context.pageContext.web.absoluteUrl
}
);
*/
/*
const element: React.ReactElement<IMockKanbanProps > = React.createElement(
MockKanban,{});
*/
console.log('bucket render webpart'); console.log('bucket render webpart');
console.log(this.properties.buckets); console.log(this.properties.buckets);
const element: React.ReactElement<IKanbanBoardV2Props> = React.createElement( const element: React.ReactElement<IKanbanBoardV2Props> = React.createElement(

View File

@ -1,4 +0,0 @@
export interface IKanbanBoardProps {
listTitle: string;
webUrl: string;
}

View File

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

View File

@ -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: &quot;FabricMDL2Icons&quot;;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 });
}
}

View File

@ -2,7 +2,7 @@ import { ISPKanbanService } from "./ISPKanbanService";
import "@pnp/polyfill-ie11"; import "@pnp/polyfill-ie11";
import { sp } from '@pnp/sp'; import { sp } from '@pnp/sp';
import { IKanbanTask, KanbanTaskMamagedPropertyType } from "../../../kanban/IKanbanTask"; import { IKanbanTask, KanbanTaskMamagedPropertyType } from "../../../kanban/IKanbanTask";
import * as strings from 'KanbanBoardWebPartStrings' import * as strings from 'KanbanBoardWebPartStrings';
export default class MockKanbanService implements ISPKanbanService { export default class MockKanbanService implements ISPKanbanService {

View File

@ -2,7 +2,7 @@ import { ISPKanbanService } from "./ISPKanbanService";
import "@pnp/polyfill-ie11"; import "@pnp/polyfill-ie11";
import { sp } from '@pnp/sp'; import { sp } from '@pnp/sp';
import { IKanbanTask, KanbanTaskMamagedPropertyType } from "../../../kanban/IKanbanTask"; import { IKanbanTask, KanbanTaskMamagedPropertyType } from "../../../kanban/IKanbanTask";
import * as strings from 'KanbanBoardWebPartStrings' import * as strings from 'KanbanBoardWebPartStrings';
export default class SPKanbanService implements ISPKanbanService { export default class SPKanbanService implements ISPKanbanService {
@ -10,7 +10,7 @@ export default class SPKanbanService implements ISPKanbanService {
public updateTaskBucketMove(listid: string, taskId: number, bucket: string): Promise<boolean> { public updateTaskBucketMove(listid: string, taskId: number, bucket: string): Promise<boolean> {
return sp.web.lists.getById(listid).items.getById(+taskId).update({ return sp.web.lists.getById(listid).items.getById(+taskId).update({
Status: bucket Status: bucket
}).then(() => { return true; }) }).then(() => { return true; });
} }
public getAllTasks(listId: string, ): Promise<IKanbanTask[]> { public getAllTasks(listId: string, ): Promise<IKanbanTask[]> {