Ui and Documentation
This commit is contained in:
parent
7b037ef047
commit
052513b08e
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 >
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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]";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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]";
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 |
|
@ -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(
|
||||||
|
|
|
@ -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 });
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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 {
|
||||||
|
|
||||||
|
|
|
@ -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[]> {
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue