Display Planner MyTasks using spfx webpart

Hi,

 

The requirement was

 

  1. To display MyTasks from the planner in Outlook 365.
  2. By default in the webpart, tasks should be displayed for today that are assigned to me
  3. In the webpart, if date is selected, my tasks should be displayed as per the selected date.

 

Below is the source code that has been written to achieve the outcome

 

//IPlannerSpProps.ts

 

import { WebPartContext } from "@microsoft/sp-webpart-base";

 

export interface IPlannerSpProps {

  description: string;

  isDarkTheme: boolean;

  environmentMessage: string;

  hasTeamsContext: boolean;

  userDisplayName: string;

  context:WebPartContext

}

 

//PlannerSpWebPart.ts

import * as React from 'react';

import * as ReactDom from 'react-dom';

import { Version } from '@microsoft/sp-core-library';

import {

  type IPropertyPaneConfiguration,

  PropertyPaneTextField

} from '@microsoft/sp-property-pane';

import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';

import { IReadonlyTheme } from '@microsoft/sp-component-base';

 

import * as strings from 'PlannerSpWebPartStrings';

import { IPlannerSpProps } from './components/IPlannerSpProps';

import TaskDashboard from './components/PlannerSPTask';

 

export interface IPlannerSpWebPartProps {

  description: string;

}

 

export default class PlannerSpWebPart extends BaseClientSideWebPart<IPlannerSpWebPartProps> {

 

  private _isDarkTheme: boolean = false;

  private _environmentMessage: string = '';

 

  public render(): void {

    const element: React.ReactElement<IPlannerSpProps> = React.createElement(

      TaskDashboard,

      {

        description: this.properties.description,

        isDarkTheme: this._isDarkTheme,

        context:this.context,

        environmentMessage: this._environmentMessage,

        hasTeamsContext: !!this.context.sdks.microsoftTeams,

        userDisplayName: this.context.pageContext.user.displayName

      }

    );

 

    ReactDom.render(element, this.domElement);

  }

 

  protected onInit(): Promise<void> {

    return this._getEnvironmentMessage().then(message => {

      this._environmentMessage = message;

    });

  }



  private _getEnvironmentMessage(): Promise<string> {

    if (!!this.context.sdks.microsoftTeams) { // running in Teams, office.com or Outlook

      return this.context.sdks.microsoftTeams.teamsJs.app.getContext()

        .then(context => {

          let environmentMessage: string = '';

          switch (context.app.host.name) {

            case 'Office': // running in Office

              environmentMessage = this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentOffice : strings.AppOfficeEnvironment;

              break;

            case 'Outlook': // running in Outlook

              environmentMessage = this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentOutlook : strings.AppOutlookEnvironment;

              break;

            case 'Teams': // running in Teams

            case 'TeamsModern':

              environmentMessage = this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentTeams : strings.AppTeamsTabEnvironment;

              break;

            default:

              environmentMessage = strings.UnknownEnvironment;

          }

 

          return environmentMessage;

        });

    }

 

    return Promise.resolve(this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentSharePoint : strings.AppSharePointEnvironment);

  }

 

  protected onThemeChanged(currentTheme: IReadonlyTheme | undefined): void {

    if (!currentTheme) {

      return;

    }

 

    this._isDarkTheme = !!currentTheme.isInverted;

    const {

      semanticColors

    } = currentTheme;

 

    if (semanticColors) {

      this.domElement.style.setProperty('--bodyText', semanticColors.bodyText || null);

      this.domElement.style.setProperty('--link', semanticColors.link || null);

      this.domElement.style.setProperty('--linkHovered', semanticColors.linkHovered || null);

    }

 

  }

 

  protected onDispose(): void {

    ReactDom.unmountComponentAtNode(this.domElement);

  }

 

  protected get dataVersion(): Version {

    return Version.parse('1.0');

  }

 

  protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {

    return {

      pages: [

        {

          header: {

            description: strings.PropertyPaneDescription

          },

          groups: [

            {

              groupName: strings.BasicGroupName,

              groupFields: [

                PropertyPaneTextField('description', {

                  label: strings.DescriptionFieldLabel

                })

              ]

            }

          ]

        }

      ]

    };

  }

}

 

//PlannerSPTask.tsx

import * as React from 'react';

import { Stack, StackItem } from '@fluentui/react/lib/Stack';

import { Text } from '@fluentui/react/lib/Text';

import { DocumentCard } from '@fluentui/react/lib/DocumentCard';

import { Clock, CheckCircle, AlertCircle, BugIcon } from 'lucide-react';

import { IPlannerSpProps } from './IPlannerSpProps';

import styles from './PlannerSp.module.scss';

import { DatePicker, DetailsList, Dialog, DialogFooter, IColumn, IconButton, PrimaryButton, SelectionMode, Spinner } from '@fluentui/react';

import * as MicrosoftGraph from "@microsoft/microsoft-graph-types";

 

interface ITaskDashboardState {

    showTaskModal: boolean;

    selectedTask: IPlannerTask | null;

    selectedDate: Date | undefined;

    isSortedDescending: boolean;

    tasks: IPlannerTask[];

    filteredTasks: IPlannerTask[];

    buckets: { [key: string]: string };

    loading: boolean;

    noRecordsFound: boolean;

    message: any;

    error: string | null;

    taskStats: {

        notStarted: number;

        inProgress: number;

        completed: number;

        overdue: number;

 

    };

}

interface IPlannerTask {

    id: string;

    title: string;

    bucketId: string;

    startDateTime: string | any | null;

    percentComplete: number;

    createdDateTime: string;

    dueDateTime: string | null;

    priority: number;

    description?: string;

    status: 'notStarted' | 'inProgress' | 'completed';

    assignments: {

        [key: string]: {

            assignedDateTime: string;

            assignedBy: {

                user: {

                    id: string;

                }

            }

        }

    };

    checklistItemCount?: number;

    activeChecklistItemCount?: number;

    checklist?: { // Add checklist property

        [key: string]: {

            isChecked: boolean;

            title: string;

            orderHint: string;

            lastModifiedDateTime: string;

            lastModifiedBy: {

                user: {

                    displayName: string | null;

                    id: string;

                }

            }

        }

    };

}

let tenantId: any

 

export default class TaskDashboard extends React.Component<IPlannerSpProps, ITaskDashboardState> {

    private columns: IColumn[];

    constructor(props: IPlannerSpProps) {

        super(props);

        this.state = {

            showTaskModal: false,

            selectedTask: null,

            selectedDate: new Date(),

            isSortedDescending: false,

            tasks: [],

            filteredTasks: [],

            buckets: {},

            loading: true,

            error: null,

            taskStats: {

                notStarted: 0,

                inProgress: 0,

                completed: 0,

                overdue: 0,

            },

            noRecordsFound: false,

            message: '',

        };

        this.columns = this.getColumns();

    }

 

    componentDidMount() {

        this.fetchPlannerData();

        const today = new Date();

        this.setState({ selectedDate: today });

        this.onDateChange(today);

    }

 

    private getColumns(): IColumn[] {

        return [

            {

                key: 'title',

                name: 'Task Title',

                fieldName: 'title',

                minWidth: 150,

                maxWidth: 200,

                onRender: (item: IPlannerTask) => (

                    <div>

                        <IconButton

                            iconProps={{ iconName: 'view' }}

                            title="Open in Planner"

                            onClick={() => this.openTaskInPlanner(item.id)}

                        />

                        <a href={`#task-${item.id}`}

                            className={styles.taskLink}

                            onClick={() => this.onTaskClick(item)}>

                            {item.title}

                        </a>

                    </div>

                )

            },

            {

                key: 'bucketName',

                name: 'Bucket',

                minWidth: 100,

                maxWidth: 150,

                onRender: (item: IPlannerTask) => this.state.buckets[item.bucketId] || 'Unknown'

            },

            {

                key: 'dueDateTime',

                name: 'Due Date',

                minWidth: 100,

                maxWidth: 120,

                onRender: (item: IPlannerTask) => item.dueDateTime ? new Date(item.dueDateTime).toLocaleDateString() : 'No due date'

            },

            {

                key: 'Progress',

                name: 'Progress',

                minWidth: 100,

                maxWidth: 120,

                onRender: (item: IPlannerTask) => this.getStatusDisplay(item)

            },

            {

                key: 'priority',

                name: 'Priority',

                minWidth: 90,

                maxWidth: 100,

                onRender: (item: IPlannerTask) => this.getPriorityDisplay(item.priority)

            },

            {

                key: 'percentage',

                name: 'percentage',

                minWidth: 90,

                maxWidth: 100,

                onRender: (item: IPlannerTask) => `${item.percentComplete}%`

            },

            {

                key: 'description',

                name: 'Description',

                minWidth: 200,

                maxWidth: 300,

                onRender: (item: IPlannerTask) => (

                    <Text variant="medium"

                        className={styles.description}

                    >

                        {item.description || 'No description'}

                    </Text>

                )

            },

            // New Column: Checklist

            {

                key: 'checklist',

                name: 'Checklist',

                // width: 'max-content',

                minWidth: 400, // Minimum width

                maxWidth: 800, // Maximum width

                onRender: (item: IPlannerTask) => (

                    <div>

                        {item.checklist && Object.values(item.checklist).map((checklistItem, index) => (

                            <div key={index}

                                className={styles.checklistItem}

                            >

                                <input

                                    type="checkbox"

                                    checked={checklistItem.isChecked}

                                    readOnly

                                />

                                <Text variant="medium">{checklistItem.title}</Text>

                            </div>

                        ))}

                    </div>

                ),

            }

        ];

    }

 

    private openTaskInPlanner = (taskId: string) => {

        const url = `https://planner.cloud.microsoft.com/webui/mytasks/assignedtome/view/grid/task/${taskId}?tid=${tenantId}`;

        window.open(url, '_blank');

    };

 

    private getStatusDisplay(task: IPlannerTask): JSX.Element {

 

        const status = this.calculateTaskStatus(task);

        const statusColors = {

            notStarted: 'gray',

            inProgress: 'orange',

            completed: 'green',

            overdue: 'red'

        };

 

        return (

            <span style={{ color: statusColors[status] }}>

                {status.charAt(0).toUpperCase() + status.slice(1).replace(/([A-Z])/g, ' $1')}

            </span>

        );

    }

 

    private getPriorityDisplay(priority: number): string {

        if (priority === 0 || priority === 1) {

            return 'Urgent';

        } else if (priority >= 2 && priority <= 4) {

            return 'Important';

        } else if (priority >= 5 && priority <= 7) {

            return 'Medium';

        } else if (priority >= 8 && priority <= 10) {

            return 'Low';

        } else {

            return 'Normal';

        }

    }

 

    private calculateTaskStatus(task: IPlannerTask): 'notStarted' | 'inProgress' | 'completed' | 'overdue' {

        // Calculate percentage completion based on checklist items

        const percentComplete: any = task.percentComplete;

        if (percentComplete === 100) return 'completed';

        // if (task.dueDateTime && new Date(task.dueDateTime) < new Date(task.startDateTime)) return 'overdue';

        if (percentComplete > 0) return 'inProgress';

        return 'notStarted';

    }

 

    private async fetchPlannerData() {

        try {

            const client = await this.props.context.msGraphClientFactory.getClient('3');

            const tenantIdInfo = await client.api('/organization').get();

            tenantId = tenantIdInfo.value[0].id;

 

            const tasksResponse = await client.api('/me/planner/tasks')

                .select('id,title,bucketId,percentComplete,startDateTime,createdDateTime,dueDateTime,priority,assignments,checklistItemCount,activeChecklistItemCount,hasDescription,appliedCategories')

                .get();

            const tasks: IPlannerTask[] = tasksResponse.value;

            // Filter tasks with a due date of today

            const today = new Date();

            const filteredTasks = tasks.filter(task =>

                task.dueDateTime && new Date(task.dueDateTime).toDateString() === today.toDateString()

            );

            // Fetch task details (description and checklist) for each task

            const tasksWithDetails = await Promise.all(

                filteredTasks.map(async (task) => {

                    const taskDetails = await this.fetchTaskDetails(task.id);

                    return {

                        ...task,

                        description: taskDetails.description,

                        checklist: taskDetails.checklist,

                    };

                })

            );

            // Get unique bucket IDs from tasks

            const bucketIds = tasksResponse.value

                .map((task: IPlannerTask) => task.bucketId)

                .filter((id, index, self) => self.indexOf(id) === index);

            // Fetch bucket names

            const bucketNames: { [key: string]: string } = {};

            await Promise.all(bucketIds.map(async (bucketId) => {

                try {

                    const bucketResponse = await client.api(`/planner/buckets/${bucketId}`)

                        .select('id,name')

                        .get();

                    bucketNames[bucketId] = bucketResponse.name;

                } catch (error) {

                    console.error(`Error fetching bucket ${bucketId}:`, error);

                    bucketNames[bucketId] = 'Unknown Bucket';

                }

            }));

            // Calculate task statistics

            const taskStats = this.calculateTaskStats(tasksWithDetails);

            if (tasksWithDetails.length === 0) {

                this.setState({

                    tasks,

                    filteredTasks: [],

                    buckets: bucketNames,

                    taskStats,

                    loading: false,

                    noRecordsFound: true,

                    selectedDate: new Date(),

                    message: `No records found for due date ${today.toISOString().split('T')[0]}`

                });

            } else {

                this.setState({

                    tasks,

                    filteredTasks: tasksWithDetails,

                    buckets: bucketNames,

                    taskStats,

                    loading: false,

                    noRecordsFound: false,

                    message: ''

                });

            }

 

        } catch (error) {

            console.error('Error fetching planner data:', error);

            this.setState({

                loading: false,

                error: 'Error loading tasks. Please try again later.'

            });

        }

    }

 

    private async fetchTaskDetails(taskId: string): Promise<{ description?: string; checklist?: any }> {

        try {

            const client = await this.props.context.msGraphClientFactory.getClient('3');

            const taskDetailsResponse = await client.api(`/planner/tasks/${taskId}/details`).select('description,checklist').get();

            return {

                description: taskDetailsResponse.description,

                checklist: taskDetailsResponse.checklist,

            };

        } catch (error) {

            console.error(`Error fetching task details for task ${taskId}:`, error);

            return {};

        }

    }

 

    private calculateTaskStats(tasks: IPlannerTask[]): {

        notStarted: number;

        inProgress: number;

        completed: number;

        overdue: number;

    } {

        return tasks.reduce((stats, task) => {

            const status = this.calculateTaskStatus(task);

            if (status === 'notStarted') stats.notStarted++;

            else if (status === 'inProgress') stats.inProgress++;

            else if (status === 'completed') stats.completed++;

            else if (status === 'overdue') stats.overdue++;

            return stats;

        }, { notStarted: 0, inProgress: 0, completed: 0, overdue: 0 });

    }

 

    private onDateChange = async (date: Date | null | undefined): Promise<void> => {

        const { tasks } = this.state;

        if (date) {

            const filteredTasks = tasks.filter(task =>

                task.dueDateTime && new Date(task.dueDateTime).toDateString() === date.toDateString()

            );

            const tasksWithDetails = await Promise.all(

                filteredTasks.map(async (task) => {

                    const taskDetails = await this.fetchTaskDetails(task.id);

                    return {

                        ...task,

                        description: taskDetails.description,

                        checklist: taskDetails.checklist,

                    };

                })

            );

            if (tasksWithDetails.length === 0) {

                this.setState({

                    selectedDate: date,

                    filteredTasks: [],

                    noRecordsFound: true,

                    message: `No records found for due date ${date.toISOString().split('T')[0]}`

                });

            } else {

                this.setState({

                    selectedDate: date,

                    filteredTasks: tasksWithDetails,

                    noRecordsFound: false,

                    message: ''

                });

            }

        }

        else {

            const tasksWithDetails = await Promise.all(

                tasks.map(async (task) => {

                    const taskDetails = await this.fetchTaskDetails(task.id);

                    return {

                        ...task,

                        description: taskDetails.description,

                        checklist: taskDetails.checklist,

                    };

                })

            );

            const taskStats = this.calculateTaskStats(tasksWithDetails);

            this.setState({

                selectedDate: undefined,

                filteredTasks: tasksWithDetails,

                noRecordsFound: false,

                message: '',

                taskStats

            });

        }

    };

 

    private onColumnClick = (ev: React.MouseEvent<HTMLElement>, column: IColumn): void => {

        const { filteredTasks, isSortedDescending } = this.state;

        const newItems = [...filteredTasks].sort((a, b) => {

            const aValue = this.getColumnValue(a, column.key);

            const bValue = this.getColumnValue(b, column.key);

            return (isSortedDescending ? -1 : 1) * (aValue > bValue ? 1 : -1);

        });

 

        this.setState({

            filteredTasks: newItems,

            isSortedDescending: !isSortedDescending

        });

    }

 

    private getColumnValue(item: IPlannerTask, columnKey: string): any {

        switch (columnKey) {

            case 'bucketName':

                return this.state.buckets[item.bucketId] || '';

            case 'status':

                return this.calculateTaskStatus(item);

            default:

                return item[columnKey as keyof IPlannerTask];

        }

    }

 

    private onTaskClick = (task: IPlannerTask): void => {

        this.setState({ selectedTask: task, showTaskModal: true });

    }

 

    private closeDialog = (): void => {

        this.setState({ showTaskModal: false, selectedTask: null });

    }

    public isdataExist = () => {

        const { noRecordsFound } = this.state;

        if (noRecordsFound) {

            return true;

        }

        else {

            return false;

        }

    }

    render() {

       

        const { filteredTasks, selectedTask, showTaskModal, message, loading, error, taskStats } = this.state;

        if (loading) {

            return <Spinner label="Loading tasks..." />;

        }

        if (error) {

            return <Text variant="large" className={styles.error}>{error}</Text>;

        }

        const content = this.isdataExist() ? (

            <div className={styles.taskDashboard}>

                <Text variant="xLarge" className={styles.taskHeading}>

                    My Tasks</Text>

 

                <Stack horizontal tokens={{ childrenGap: 8 }} className={styles.dateFilter}>

                    <DatePicker

                        placeholder="Filter by due date"

                        // value={this.state.selectedDate}

                        onSelectDate={this.onDateChange}

                        value={this.state.selectedDate}

                    />

                    <IconButton

                        iconProps={{ iconName: 'Clear' }}

                        title="Reset"

                        onClick={() => this.onDateChange(undefined)}

                    />

                </Stack>

 

                <Stack horizontal tokens={{ childrenGap: 16 }} className={styles.cardContainer}>

                    <StackItem grow>

                        <DocumentCard className={`${styles.card} ${styles.NotStarted}`}>

                            <div className={styles.cardContent}>

                                <AlertCircle size={24} />

                                <Text variant="large">Not Started</Text>

                                <Text variant="xLarge">{taskStats.notStarted}</Text>

                            </div>

                        </DocumentCard>

                    </StackItem>

                    <StackItem grow>

                        <DocumentCard className={`${styles.card} ${styles.inProgress}`}>

                            <div className={styles.cardContent}>

                                <Clock className="cardIcon" size={24} />

                                <Text variant="large">In Progress</Text>

                                <Text variant="xLarge">{taskStats.inProgress}</Text>

                            </div>

                        </DocumentCard>

                    </StackItem>

                    <StackItem grow>

                        <DocumentCard className={`${styles.card} ${styles.completed}`}>

                            <div className={styles.cardContent}>

                                <CheckCircle size={24} />

                                <Text variant="large">Completed</Text>

                                <Text variant="xLarge">{taskStats.completed}</Text>

                            </div>

                        </DocumentCard>

                    </StackItem>

                </Stack>

                <Text variant="large" className={styles.error}>{message}</Text>

 

                <Dialog

                    hidden={!showTaskModal}

                    onDismiss={this.closeDialog}

                    dialogContentProps={{

                        title: selectedTask?.title || ''

                    }}

                >

                    {selectedTask && (

                        <Stack tokens={{ childrenGap: 10 }}>

                            <Text>Bucket: {this.state.buckets[selectedTask.bucketId]}</Text>

                            <hr style={{ border: '1px solid #ccc', margin: '10px 0' }} />

                            <Text>Due Date: {selectedTask.dueDateTime ?

                                new Date(selectedTask.dueDateTime).toLocaleDateString() :

                                'No due date'}

                            </Text>

                            <hr style={{ border: '1px solid #ccc', margin: '10px 0' }} />

                            <Text>Status: {this.getStatusDisplay(selectedTask)}</Text>

                            <hr style={{ border: '1px solid #ccc', margin: '10px 0' }} />

                            <Text>Priority: {this.getPriorityDisplay(selectedTask.priority)}</Text>

                            <hr style={{ border: '1px solid #ccc', margin: '10px 0' }} />

                            <Text>Progress: {selectedTask.percentComplete}%</Text>

                            <hr style={{ border: '1px solid #ccc', margin: '10px 0' }} />

                            {selectedTask.description && (

                                <Text>Description: {selectedTask.description}</Text>

 

                            )}

                            <hr style={{ border: '1px solid #ccc', margin: '10px 0' }} />

                            {selectedTask.checklist && (

                                <div>

                                    <Text>Checklist:</Text>

                                    {Object.values(selectedTask.checklist).map((checklistItem, index) => (

                                        <div key={index} className={styles.checklistItem}>

                                            <input

                                                type="checkbox"

                                                checked={checklistItem.isChecked}

                                                readOnly

                                            />

                                            <Text variant="medium">{checklistItem.title}</Text>

                                        </div>

                                    ))}

                                    <hr style={{ border: '1px solid #ccc', margin: '10px 0' }} />

                                </div>

                            )}

                        </Stack>

                    )}

                    <DialogFooter>

                        <PrimaryButton onClick={this.closeDialog} text="Close" />

                    </DialogFooter>

                </Dialog>

            </div>

        ) : <div className={styles.taskDashboard}>

            <Text variant="xLarge" className={styles.taskHeading}>

                My Tasks</Text>

 

            <Stack horizontal tokens={{ childrenGap: 8 }} className={styles.dateFilter}>

                <DatePicker

                    placeholder="Filter by due date"

                    // value={this.state.selectedDate}

                    onSelectDate={this.onDateChange}

                    value={this.state.selectedDate}

                />

                <IconButton

                    iconProps={{ iconName: 'Clear' }}

                    title="Reset"

                    onClick={() => this.onDateChange(undefined)}

                />

            </Stack>

 

            <Stack horizontal tokens={{ childrenGap: 16 }} className={styles.cardContainer}>

                <StackItem grow>

                    <DocumentCard className={`${styles.card} ${styles.NotStarted}`}>

                        <div className={styles.cardContent}>

                            <AlertCircle size={24} />

                            <Text variant="large">Not Started</Text>

                            <Text variant="xLarge">{taskStats.notStarted}</Text>

                        </div>

                    </DocumentCard>

                </StackItem>

                <StackItem grow>

                    <DocumentCard className={`${styles.card} ${styles.inProgress}`}>

                        <div className={styles.cardContent}>

                            <Clock className="cardIcon" size={24} />

                            <Text variant="large">In Progress</Text>

                            <Text variant="xLarge">{taskStats.inProgress}</Text>

                        </div>

                    </DocumentCard>

                </StackItem>

                <StackItem grow>

                    <DocumentCard className={`${styles.card} ${styles.completed}`}>

                        <div className={styles.cardContent}>

                            <CheckCircle size={24} />

                            <Text variant="large">Completed</Text>

                            <Text variant="xLarge">{taskStats.completed}</Text>

                        </div>

                    </DocumentCard>

                </StackItem>

            </Stack>

            <DetailsList

                items={filteredTasks}

                columns={this.columns}

                selectionMode={SelectionMode.none}

                onColumnHeaderClick={this.onColumnClick}

            />

 

            <Dialog

                hidden={!showTaskModal}

                onDismiss={this.closeDialog}

                dialogContentProps={{

                    title: selectedTask?.title || ''

                }}

            >

                {selectedTask && (

                    <Stack tokens={{ childrenGap: 10 }}>

                        <Text>Bucket: {this.state.buckets[selectedTask.bucketId]}</Text>

                        <hr style={{ border: '1px solid #ccc', margin: '10px 0' }} />

                        <Text>Due Date: {selectedTask.dueDateTime ?

                            new Date(selectedTask.dueDateTime).toLocaleDateString() :

                            'No due date'}

                        </Text>

                        <hr style={{ border: '1px solid #ccc', margin: '10px 0' }} />

                        <Text>Status: {this.getStatusDisplay(selectedTask)}</Text>

                        <hr style={{ border: '1px solid #ccc', margin: '10px 0' }} />

                        <Text>Priority: {this.getPriorityDisplay(selectedTask.priority)}</Text>

                        <hr style={{ border: '1px solid #ccc', margin: '10px 0' }} />

                        <Text>Progress: {selectedTask.percentComplete}%</Text>

                        <hr style={{ border: '1px solid #ccc', margin: '10px 0' }} />

                        {selectedTask.description && (

                            <Text>Description: {selectedTask.description}</Text>

 

                        )}

                        <hr style={{ border: '1px solid #ccc', margin: '10px 0' }} />

                        {selectedTask.checklist && (

                            <div>

                                <Text>Checklist:</Text>

                                {Object.values(selectedTask.checklist).map((checklistItem, index) => (

                                    <div key={index} className={styles.checklistItem}>

                                        <input

                                            type="checkbox"

                                            checked={checklistItem.isChecked}

                                            readOnly

                                        />

                                        <Text variant="medium">{checklistItem.title}</Text>

                                    </div>

                                ))}

                                <hr style={{ border: '1px solid #ccc', margin: '10px 0' }} />

                            </div>

                        )}

                    </Stack>

                )}

                <DialogFooter>

                    <PrimaryButton onClick={this.closeDialog} text="Close" />

                </DialogFooter>

            </Dialog>

        </div>

        return (

            content

        )

    }

}

 

//serve.json

{

  "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/spfx-serve.schema.json",

  "port": 4321,

  "https": true,

  "initialPage": "https://sitecollectionurl/_layouts/workbench.aspx"

}

Below are the commands to deploy the webpart

gulp clean

gulp build

gulp bundle --ship

gulp package-solution –ship

Outcome



Comments