From 54f8fe64ae1280f84fbfc3561278d5ccf589c623 Mon Sep 17 00:00:00 2001 From: Remi Blom-Ohlsen Date: Mon, 11 Sep 2023 19:03:03 +0200 Subject: [PATCH 1/8] Rework commandBar for TimelineList + add farItems to Toolbar component --- .../TimelineList/TimelineList.module.scss | 12 +- .../ToolbarItems/useToolbarItems.tsx | 139 +++++++++++ .../ProjectTimeline/TimelineList/index.tsx | 72 ++++-- .../TimelineList/useColumns.tsx | 77 ++++++ .../TimelineList/useTimelineList.ts | 227 +++--------------- .../ProjectTimeline/data/fetchTimelineData.ts | 6 +- .../src/components/ProjectTimeline/types.ts | 5 +- .../Timeline/useGroupRenderer.tsx | 9 +- .../components/Toolbar/Toolbar.module.scss | 31 ++- .../src/components/Toolbar/index.tsx | 10 +- .../src/components/Toolbar/types.ts | 5 + 11 files changed, 353 insertions(+), 240 deletions(-) create mode 100644 SharePointFramework/ProjectWebParts/src/components/ProjectTimeline/TimelineList/ToolbarItems/useToolbarItems.tsx create mode 100644 SharePointFramework/ProjectWebParts/src/components/ProjectTimeline/TimelineList/useColumns.tsx diff --git a/SharePointFramework/ProjectWebParts/src/components/ProjectTimeline/TimelineList/TimelineList.module.scss b/SharePointFramework/ProjectWebParts/src/components/ProjectTimeline/TimelineList/TimelineList.module.scss index 9c574f5f8..5fea5afe6 100644 --- a/SharePointFramework/ProjectWebParts/src/components/ProjectTimeline/TimelineList/TimelineList.module.scss +++ b/SharePointFramework/ProjectWebParts/src/components/ProjectTimeline/TimelineList/TimelineList.module.scss @@ -1,8 +1,4 @@ -@import '~@fluentui/react/dist/sass/References.scss'; - -.commandBar { - margin: 0px auto; -} +@import "~@fluentui/react/dist/sass/References.scss"; .timelineList { padding: 32px; @@ -12,4 +8,10 @@ position: inherit; } } + + .commandBar { + height: 42px; + margin: 0px auto; + padding-bottom: 16px + } } diff --git a/SharePointFramework/ProjectWebParts/src/components/ProjectTimeline/TimelineList/ToolbarItems/useToolbarItems.tsx b/SharePointFramework/ProjectWebParts/src/components/ProjectTimeline/TimelineList/ToolbarItems/useToolbarItems.tsx new file mode 100644 index 000000000..11a0ef46a --- /dev/null +++ b/SharePointFramework/ProjectWebParts/src/components/ProjectTimeline/TimelineList/ToolbarItems/useToolbarItems.tsx @@ -0,0 +1,139 @@ +import { useMemo } from 'react' +import { + AddFilled, + AddRegular, + bundleIcon, + DeleteFilled, + DeleteRegular, + EditFilled, + EditRegular +} from '@fluentui/react-icons' +import { IProjectTimelineProps, IProjectTimelineState } from 'components/ProjectTimeline/types' +import { ListMenuItem } from 'pp365-shared-library' +import strings from 'ProjectWebPartsStrings' +import SPDataAdapter from 'data/SPDataAdapter' +import { Logger, LogLevel } from '@pnp/logging' + +/** + * Object containing icons used in the toolbar. + */ +const Icons = { + Add: bundleIcon(AddFilled, AddRegular), + Edit: bundleIcon(EditFilled, EditRegular), + Delete: bundleIcon(DeleteFilled, DeleteRegular) +} + +/** + * Returns an array of menu items for the toolbar in the PortfolioOverview component. + * + * @param context - The IPortfolioOverviewContext object containing the necessary data for generating the toolbar items. + * + * @returns An array of IListMenuItem objects representing the toolbar items. + */ +export function useToolbarItems( + props: IProjectTimelineProps, + setState: (newState: Partial) => void, + selectedItems: any[] +) { + /** + * Create new timeline item and send the user to the edit form. + */ + const redirectNewTimelineItem = async () => { + const [project] = await SPDataAdapter.portal.web.lists + .getByTitle(strings.ProjectsListName) + .items.select('Id') + .filter(`GtSiteId eq '${props.siteId}'`)() + + const properties: Record = { + Title: strings.NewItemLabel, + GtSiteIdLookupId: project.Id + } + + Logger.log({ + message: '(TimelineItem) _redirectNewTimelineItem: Created new timeline item', + data: { fieldValues: properties }, + level: LogLevel.Info + }) + + const itemId = await addTimelineItem(properties) + document.location.hash = '' + document.location.href = editFormUrl(itemId) + } + + /** + * Add timeline item + * + * @param properties Properties + */ + const addTimelineItem = async (properties: Record): Promise => { + const list = SPDataAdapter.portal.web.lists.getByTitle(strings.TimelineContentListName) + const itemAddResult = await list.items.add(properties) + return itemAddResult.data + } + + /** + * Delete timelineitem + * + * @param item Item + */ + const deleteTimelineItem = async (items: any) => { + const list = SPDataAdapter.portal.web.lists.getByTitle(strings.TimelineContentListName) + + await items.forEach(async (item: any) => { + await list.items.getById(item.Id).delete() + }) + + setState({ + refetch: new Date().getTime() + }) + } + + /** + * Edit form URL with added Source parameter generated from the item ID + * + * @param item Item + */ + const editFormUrl = (item: any) => { + return [ + `${SPDataAdapter.portal.url}`, + `/Lists/${strings.TimelineContentListName}/EditForm.aspx`, + '?ID=', + item.Id, + '&Source=', + encodeURIComponent(window.location.href) + ].join('') + } + + const menuItems = useMemo( + () => + [ + new ListMenuItem(strings.NewItemLabel, strings.NewItemLabel) + .setIcon(Icons.Add) + .setOnClick(() => { + redirectNewTimelineItem() + }), + new ListMenuItem(strings.EditItemLabel, strings.EditItemLabel) + .setIcon(Icons.Edit) + .setDisabled(selectedItems.length !== 1) + .setOnClick(() => { + window.open(selectedItems[0]?.EditFormUrl, '_blank') + }) + ].filter(Boolean), + [props, selectedItems] + ) + + const farMenuItems = useMemo( + () => + [ + new ListMenuItem(strings.DeleteItemLabel, strings.DeleteItemLabel) + .setIcon(Icons.Delete) + .setDisabled(selectedItems.length === 0) + .setOnClick(() => { + deleteTimelineItem(selectedItems) + }) + ].filter(Boolean), + [props, selectedItems] + ) + + return { menuItems, farMenuItems } +} diff --git a/SharePointFramework/ProjectWebParts/src/components/ProjectTimeline/TimelineList/index.tsx b/SharePointFramework/ProjectWebParts/src/components/ProjectTimeline/TimelineList/index.tsx index a0d6e2053..018367c85 100644 --- a/SharePointFramework/ProjectWebParts/src/components/ProjectTimeline/TimelineList/index.tsx +++ b/SharePointFramework/ProjectWebParts/src/components/ProjectTimeline/TimelineList/index.tsx @@ -1,31 +1,61 @@ -import { CommandBar, DetailsList, DetailsListLayoutMode, SelectionMode } from '@fluentui/react' -import React, { FC, useContext } from 'react' -import { ProjectTimelineContext } from '../context' +import { + DataGrid, + DataGridBody, + DataGridCell, + DataGridHeader, + DataGridHeaderCell, + DataGridRow, + FluentProvider, + webLightTheme +} from '@fluentui/react-components' +import * as React from 'react' +import { FC, useContext } from 'react' import styles from './TimelineList.module.scss' import { useTimelineList } from './useTimelineList' +import { ProjectTimelineContext } from '../context' +import { Toolbar } from 'pp365-shared-library' export const TimelineList: FC = () => { const context = useContext(ProjectTimelineContext) - const { getCommandBarProps, onRenderItemColumn, selection, onColumnHeaderClick } = + const { columns, menuItems, farMenuItems, columnSizingOptions, defaultSortState, onSelection } = useTimelineList() + return ( - <> -
- {context.props.showTimelineListCommands && ( -
- + + {context.props.showTimelineListCommands && ( +
+
+
- )} - -
- +
+ )} + + + + {({ renderHeaderCell }) => ( + {renderHeaderCell()} + )} + + + + {({ item, rowId }) => ( + + {({ renderCell }) => {renderCell(item)}} + + )} + + + ) } diff --git a/SharePointFramework/ProjectWebParts/src/components/ProjectTimeline/TimelineList/useColumns.tsx b/SharePointFramework/ProjectWebParts/src/components/ProjectTimeline/TimelineList/useColumns.tsx new file mode 100644 index 000000000..132792c82 --- /dev/null +++ b/SharePointFramework/ProjectWebParts/src/components/ProjectTimeline/TimelineList/useColumns.tsx @@ -0,0 +1,77 @@ +import { TableCellLayout, TableColumnDefinition } from '@fluentui/react-components' +import React, { useContext } from 'react' +import { ProjectTimelineContext } from '../context' +import { get } from '@microsoft/sp-lodash-subset' +import { tryParseCurrency } from 'pp365-shared-library/lib/util/tryParseCurrency' +import moment from 'moment' +import { stringIsNullOrEmpty } from '@pnp/core' + +export interface IListColumn extends TableColumnDefinition { + minWidth?: number + defaultWidth?: number +} + +export const useColumns = (): IListColumn[] => { + const context = useContext(ProjectTimelineContext) + + return context.state?.data?.listColumns.map((column) => { + if (!column.fieldName) return null + + return { + columnId: column.fieldName, + defaultWidth: column.maxWidth, + minWidth: column.minWidth, + compare: (a, b) => { + switch (column?.data?.type.toLowerCase()) { + case 'counter': + case 'int': + return (a[column.fieldName] ?? '').localeCompare(b[column.fieldName] ?? '', undefined, { + numeric: true + }) + case 'lookup': + return (a[column.fieldName]?.Title ?? '').localeCompare( + b[column.fieldName]?.Title ?? '' + ) + default: + return (a[column.fieldName] ?? '').localeCompare(b[column.fieldName] ?? '') + } + }, + renderHeaderCell: () => { + return column.name + }, + renderCell: (item) => { + const value = get(item, column.fieldName, null) + let cellValue + + if (!stringIsNullOrEmpty(value)) { + switch (column?.data?.type.toLowerCase()) { + case 'int': + cellValue = parseInt(value) + break + case 'date': + cellValue = moment(value).format('DD.MM.YYYY') + break + case 'datetime': + cellValue = moment(value).format('DD.MM.YYYY') + break + case 'currency': + cellValue = tryParseCurrency(value) + break + case 'lookup': + cellValue = value.Title + break + default: + cellValue = value + break + } + } + + return ( + + <>{cellValue} + + ) + } + } + }) +} diff --git a/SharePointFramework/ProjectWebParts/src/components/ProjectTimeline/TimelineList/useTimelineList.ts b/SharePointFramework/ProjectWebParts/src/components/ProjectTimeline/TimelineList/useTimelineList.ts index d81e3d6d7..676a510bf 100644 --- a/SharePointFramework/ProjectWebParts/src/components/ProjectTimeline/TimelineList/useTimelineList.ts +++ b/SharePointFramework/ProjectWebParts/src/components/ProjectTimeline/TimelineList/useTimelineList.ts @@ -1,196 +1,47 @@ -import { getId, IColumn, ICommandBarProps, Selection } from '@fluentui/react' -import { get } from '@microsoft/sp-lodash-subset' -import { stringIsNullOrEmpty } from '@pnp/core' -import { Logger, LogLevel } from '@pnp/logging' -import SPDataAdapter from 'data/SPDataAdapter' -import moment from 'moment' -import { tryParseCurrency } from 'pp365-shared-library/lib/util/tryParseCurrency' -import strings from 'ProjectWebPartsStrings' import { useContext, useState } from 'react' import { ProjectTimelineContext } from '../context' +import { useToolbarItems } from '../TimelineList/ToolbarItems/useToolbarItems' +import { DataGridProps, SortDirection, TableColumnSizingOptions } from '@fluentui/react-components' +import { useColumns } from './useColumns' -export function useTimelineList() { +export function useTimelineList(): any { const context = useContext(ProjectTimelineContext) - const [selectedItem, setSelectedItem] = useState([]) - - const selection: Selection = new Selection({ - onSelectionChanged: () => { - setSelectedItem(selection.getSelection()) - } - }) - - /** - * On render item column - * - * @param item Item - * @param index Index - * @param column Column - */ - const onRenderItemColumn = (item: any, index: number, column: IColumn) => { - if (!column.fieldName) return null - if (column.onRender) return column.onRender(item, index, column) - if (!stringIsNullOrEmpty(column['fieldNameDisplay'])) { - return get(item, column['fieldNameDisplay'], null) - } - const columnValue = get(item, column.fieldName, null) - - switch (column?.data?.type.toLowerCase()) { - case 'int': - return columnValue ? parseInt(columnValue) : null - case 'date': - return columnValue && moment(columnValue).format('DD.MM.YYYY') - case 'datetime': - return columnValue && moment(columnValue).format('DD.MM.YYYY') - case 'currency': - return tryParseCurrency(columnValue) - case 'lookup': - return columnValue && columnValue.Title - default: - return columnValue - } - } - - /** - * Get command bar items - */ - const getCommandBarProps = (): ICommandBarProps => { - const cmd: ICommandBarProps = { items: [], farItems: [] } - cmd.items.push({ - key: getId('NewItem'), - name: strings.NewItemLabel, - iconProps: { iconName: 'Add' }, - buttonStyles: { root: { border: 'none' } }, - onClick: () => { - redirectNewTimelineItem() - } - }) - cmd.items.push({ - key: getId('EditItem'), - name: strings.EditItemLabel, - iconProps: { iconName: 'Edit' }, - buttonStyles: { root: { border: 'none' } }, - disabled: selectedItem.length === 0, - href: selectedItem[0]?.EditFormUrl - }) - cmd.farItems.push({ - key: getId('DeleteItem'), - name: strings.DeleteItemLabel, - iconProps: { iconName: 'Delete' }, - buttonStyles: { root: { border: 'none' } }, - disabled: selectedItem.length === 0, - onClick: () => { - deleteTimelineItem(selectedItem[0]) - } - }) - return cmd - } - - /** - * Create new timeline item and send the user to the edit form. - */ - const redirectNewTimelineItem = async () => { - const [project] = await SPDataAdapter.portal.web.lists - .getByTitle(strings.ProjectsListName) - .items.select('Id') - .filter(`GtSiteId eq ${context.props.siteId}`)() - - const properties: Record = { - Title: 'Nytt element på tidslinjen', - GtSiteIdLookupId: project.Id - } - - Logger.log({ - message: '(TimelineItem) _redirectNewTimelineItem: Created new timeline item', - data: { fieldValues: properties }, - level: LogLevel.Info - }) - - const itemId = await addTimelineItem(properties) - document.location.hash = '' - document.location.href = editFormUrl(itemId) - } - - /** - * Add timeline item - * - * @param properties Properties - */ - const addTimelineItem = async (properties: Record): Promise => { - const list = SPDataAdapter.portal.web.lists.getByTitle(strings.TimelineContentListName) - const itemAddResult = await list.items.add(properties) - return itemAddResult.data - } - - /** - * Delete timelineitem - * - * @param item Item - */ - const deleteTimelineItem = async (item: any) => { - const list = SPDataAdapter.portal.web.lists.getByTitle(strings.TimelineContentListName) - await list.items.getById(item.Id).delete() - context.setState({ - refetch: new Date().getTime() - }) - } - - /** - * Edit form URL with added Source parameter generated from the item ID - * - * @param item Item - */ - const editFormUrl = (item: any) => { - return [ - `${SPDataAdapter.portal.url}`, - `/Lists/${strings.TimelineContentListName}/EditForm.aspx`, - '?ID=', - item.Id, - '&Source=', - encodeURIComponent(window.location.href) - ].join('') - } - - /** - * Copies and sorts items based on colum key - * - * @param items timelineListItems - * @param columnKey Column key - * @param isSortedDescending Is Sorted Descending? - * @returns sorted timeline list items - */ - const copyAndSort = (items: any[], columnKey: string, isSortedDescending?: boolean): any[] => { - const key = columnKey as keyof any - return items - .slice(0) - .sort((a: any, b: any) => ((isSortedDescending ? a[key] < b[key] : a[key] > b[key]) ? 1 : -1)) - } - - /** - * Sorting on column header click - * - * @param _event Event - * @param column Column - */ - const onColumnHeaderClick = (_event: React.MouseEvent, column: IColumn): void => { - const newColumns = context.state.data.listColumns.map((col: IColumn) => { - if (col.key === column.key) { - col.isSortedDescending = !col.isSortedDescending - col.isSorted = true - } else { - col.isSorted = false - col.isSortedDescending = true - } - return col - }) - const newItems = copyAndSort( - context.state.data.listItems, - column.fieldName, - column.isSortedDescending + const columns = useColumns() + const [selectedItems, setSelectedItems] = useState([]) + + const onSelection: DataGridProps['onSelectionChange'] = (e, data) => { + const selectedItemId = Array.from(data.selectedItems) + // find all items that are selected by checking the index of the selected item in the list of all items + const selectedItems = selectedItemId.map((id) => + context.state.data.listItems.find((_, idx) => idx === id) ) - context.setState({ - data: { ...context.state.data, listColumns: newColumns, listItems: newItems } - }) + setSelectedItems(selectedItems) } - return { getCommandBarProps, onRenderItemColumn, selection, onColumnHeaderClick } as const + const columnSizingOptions: TableColumnSizingOptions = columns.reduce( + (options, col) => ({ + ...options, + [col.columnId]: { + defaultWidth: col.defaultWidth, + minWidth: col.minWidth + } + }), + {} + ) + + const defaultSortState = { sortColumn: 'Title', sortDirection: 'ascending' as SortDirection } + const { menuItems, farMenuItems } = useToolbarItems( + context.props, + context.setState, + selectedItems + ) + + return { + columns, + menuItems, + farMenuItems, + columnSizingOptions, + defaultSortState, + onSelection + } as const } diff --git a/SharePointFramework/ProjectWebParts/src/components/ProjectTimeline/data/fetchTimelineData.ts b/SharePointFramework/ProjectWebParts/src/components/ProjectTimeline/data/fetchTimelineData.ts index ed9d35f7a..bc755ce55 100644 --- a/SharePointFramework/ProjectWebParts/src/components/ProjectTimeline/data/fetchTimelineData.ts +++ b/SharePointFramework/ProjectWebParts/src/components/ProjectTimeline/data/fetchTimelineData.ts @@ -1,4 +1,3 @@ -import { IColumn } from '@fluentui/react' import SPDataAdapter from 'data/SPDataAdapter' import _ from 'lodash' import { TimelineConfigurationModel, TimelineContentModel } from 'pp365-shared-library/lib/models' @@ -75,14 +74,13 @@ export async function fetchTimelineData( const columns = timelineColumns .filter((column) => column.InternalName !== 'GtSiteIdLookup') - .map((column) => ({ + .map((column) => ({ key: column.InternalName, name: column.Title, fieldName: column.InternalName, data: { type: column.TypeAsString }, minWidth: 150, - maxWidth: 200, - isResizable: true + maxWidth: 200 })) timelineListItems = timelineListItems.map((item) => ({ diff --git a/SharePointFramework/ProjectWebParts/src/components/ProjectTimeline/types.ts b/SharePointFramework/ProjectWebParts/src/components/ProjectTimeline/types.ts index 353f9664d..1e64869e7 100644 --- a/SharePointFramework/ProjectWebParts/src/components/ProjectTimeline/types.ts +++ b/SharePointFramework/ProjectWebParts/src/components/ProjectTimeline/types.ts @@ -1,4 +1,3 @@ -import { IColumn } from '@fluentui/react' import { IBaseWebPartComponentProps, IBaseWebPartComponentState, @@ -70,8 +69,8 @@ export interface IProjectTimelineState extends IBaseWebPartComponentState[] - listColumns?: IColumn[] + listItems?: any[] + listColumns?: any[] } export enum TimelineGroupType { diff --git a/SharePointFramework/shared-library/src/components/ProjectTimeline/Timeline/useGroupRenderer.tsx b/SharePointFramework/shared-library/src/components/ProjectTimeline/Timeline/useGroupRenderer.tsx index fe3675f9b..5695408d5 100644 --- a/SharePointFramework/shared-library/src/components/ProjectTimeline/Timeline/useGroupRenderer.tsx +++ b/SharePointFramework/shared-library/src/components/ProjectTimeline/Timeline/useGroupRenderer.tsx @@ -12,14 +12,15 @@ export function useGroupRenderer() { return (
- {group.type === TimelineGroupType.Project - ? + {group.type === TimelineGroupType.Project ? ( + {group.title} - : + ) : ( + {group.title} - } + )}
) } diff --git a/SharePointFramework/shared-library/src/components/Toolbar/Toolbar.module.scss b/SharePointFramework/shared-library/src/components/Toolbar/Toolbar.module.scss index 491a0ed6c..87db70b17 100644 --- a/SharePointFramework/shared-library/src/components/Toolbar/Toolbar.module.scss +++ b/SharePointFramework/shared-library/src/components/Toolbar/Toolbar.module.scss @@ -1,18 +1,23 @@ -.toolbar { - gap: 15px; - height: 40px; - padding: 0 !important; +.root { + display: flex; + justify-content: space-between; - button { + .toolbar { + gap: 15px; height: 40px; - width: -moz-fit-content; - width: fit-content; - min-width: 40px; - padding: 5px var(--spacingHorizontalM) !important; - box-shadow: var(--shadow2); - } + padding: 0 !important; + + button { + height: 40px; + width: -moz-fit-content; + width: fit-content; + min-width: 40px; + padding: 5px var(--spacingHorizontalM) !important; + box-shadow: var(--shadow2); + } - button:hover { - background-color: transparent; + button:hover { + background-color: transparent; + } } } diff --git a/SharePointFramework/shared-library/src/components/Toolbar/index.tsx b/SharePointFramework/shared-library/src/components/Toolbar/index.tsx index 9f5be7f02..4292644c7 100644 --- a/SharePointFramework/shared-library/src/components/Toolbar/index.tsx +++ b/SharePointFramework/shared-library/src/components/Toolbar/index.tsx @@ -22,8 +22,13 @@ export const Toolbar: FC = (props) => { const fluentProviderId = useId('fluent-provider') return ( - + {props.items.map(renderToolbarItem)} + {props.farItems && ( + + {props.farItems.map(renderToolbarItem)} + + )} {props.filterPanel && ( = (props) => { } Toolbar.defaultProps = { - items: [] + items: [], + farItems: [] } export * from './types' diff --git a/SharePointFramework/shared-library/src/components/Toolbar/types.ts b/SharePointFramework/shared-library/src/components/Toolbar/types.ts index 992ed6da8..2b3d67f0f 100644 --- a/SharePointFramework/shared-library/src/components/Toolbar/types.ts +++ b/SharePointFramework/shared-library/src/components/Toolbar/types.ts @@ -11,6 +11,11 @@ export interface IToolbarProps { */ items?: ListMenuItem[] + /** + * An array of ListMenuItem objects to be displayed in the toolbar (far right side). + */ + farItems?: ListMenuItem[] + /** * Props for the FilterPanel component. If specfied, the FilterPanel * will be rendered in the toolbar. From 31e10525062795dc4333015789ce8d0e4ec4a269 Mon Sep 17 00:00:00 2001 From: Remi Blom-Ohlsen Date: Tue, 12 Sep 2023 09:44:11 +0200 Subject: [PATCH 2/8] Add missing button to Konfigurasjon page --- Templates/Portfolio/Objects/ClientSidePages/Konfigurasjon.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Templates/Portfolio/Objects/ClientSidePages/Konfigurasjon.xml b/Templates/Portfolio/Objects/ClientSidePages/Konfigurasjon.xml index 9258b08f9..c3e358286 100644 --- a/Templates/Portfolio/Objects/ClientSidePages/Konfigurasjon.xml +++ b/Templates/Portfolio/Objects/ClientSidePages/Konfigurasjon.xml @@ -5,7 +5,7 @@ - + From 6307fc814faee20a85818f64e4ac7a911e5fe7a1 Mon Sep 17 00:00:00 2001 From: Remi Blom-Ohlsen Date: Tue, 12 Sep 2023 09:57:06 +0200 Subject: [PATCH 3/8] Fix errors during serve --- .../ProjectWebParts/.vscode/launch.sample.json | 6 +++--- .../ProjectWebParts/config/serve.sample.json | 8 ++------ SharePointFramework/ProjectWebParts/package.json | 2 +- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/SharePointFramework/ProjectWebParts/.vscode/launch.sample.json b/SharePointFramework/ProjectWebParts/.vscode/launch.sample.json index 8dc287acc..804e7710d 100644 --- a/SharePointFramework/ProjectWebParts/.vscode/launch.sample.json +++ b/SharePointFramework/ProjectWebParts/.vscode/launch.sample.json @@ -2,7 +2,7 @@ "version": "0.2.0", "configurations": [ { - "name": "Hosted workbench", + "name": "Debug", "type": "chrome", "request": "launch", "url": "https://[tenant].sharepoint.com/sites/[site]/_layouts/15/workbench.aspx", @@ -19,7 +19,7 @@ ] }, { - "name": "Hosted workbench (incognito)", + "name": "Debug in incognito mode", "type": "chrome", "request": "launch", "url": "https://[tenant].sharepoint.com/sites/[site]/_layouts/15/workbench.aspx", @@ -37,4 +37,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/SharePointFramework/ProjectWebParts/config/serve.sample.json b/SharePointFramework/ProjectWebParts/config/serve.sample.json index 090cfe9e6..e05918a99 100644 --- a/SharePointFramework/ProjectWebParts/config/serve.sample.json +++ b/SharePointFramework/ProjectWebParts/config/serve.sample.json @@ -1,10 +1,6 @@ { - "$schema": "https://developer.microsoft.com/json-schemas/core-build/serve.schema.json", + "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/spfx-serve.schema.json", "port": 4321, "https": true, - "initialPage": "https://localhost:5432/workbench", - "api": { - "port": 5432, - "entryPath": "node_modules/@microsoft/sp-webpart-workbench/lib/api/" - } + "initialPage": "https://enter-your-SharePoint-site/_layouts/workbench.aspx" } diff --git a/SharePointFramework/ProjectWebParts/package.json b/SharePointFramework/ProjectWebParts/package.json index 5ffce3ea3..c8d5c28c6 100644 --- a/SharePointFramework/ProjectWebParts/package.json +++ b/SharePointFramework/ProjectWebParts/package.json @@ -9,7 +9,7 @@ "license": "MIT", "scripts": { "watch": "concurrently \"npm run serve\" \"livereload './dist/*.js' -e 'js' -w 250\"", - "serve": "concurrently \"gulp serve --locale=nb-no --nobrowser\"", + "serve": "concurrently \"gulp serve-deprecated --locale=nb-no --nobrowser\"", "build": "gulp bundle --ship && gulp package-solution --ship", "postversion": "tsc && npm publish", "lint": "eslint --ext .ts,.tsx ./src --color --fix --config ../.eslintrc.yaml && npm run prettier", From a0a55f1792025f044d60e00ac93d24306530d465 Mon Sep 17 00:00:00 2001 From: Remi Blom-Ohlsen Date: Tue, 12 Sep 2023 13:27:36 +0200 Subject: [PATCH 4/8] Add user fields to TimelineList + fixing to sort different types and rendering --- .../ToolbarItems/useToolbarItems.tsx | 2 +- .../TimelineList/useColumns.tsx | 39 +++++++++++++++---- .../ProjectTimeline/data/fetchTimelineData.ts | 32 ++++++++++++--- .../ProjectWebParts/tsconfig.json | 2 +- 4 files changed, 59 insertions(+), 16 deletions(-) diff --git a/SharePointFramework/ProjectWebParts/src/components/ProjectTimeline/TimelineList/ToolbarItems/useToolbarItems.tsx b/SharePointFramework/ProjectWebParts/src/components/ProjectTimeline/TimelineList/ToolbarItems/useToolbarItems.tsx index 11a0ef46a..da721661a 100644 --- a/SharePointFramework/ProjectWebParts/src/components/ProjectTimeline/TimelineList/ToolbarItems/useToolbarItems.tsx +++ b/SharePointFramework/ProjectWebParts/src/components/ProjectTimeline/TimelineList/ToolbarItems/useToolbarItems.tsx @@ -116,7 +116,7 @@ export function useToolbarItems( .setIcon(Icons.Edit) .setDisabled(selectedItems.length !== 1) .setOnClick(() => { - window.open(selectedItems[0]?.EditFormUrl, '_blank') + window.open(selectedItems[0]?.EditFormUrl, '_self') }) ].filter(Boolean), [props, selectedItems] diff --git a/SharePointFramework/ProjectWebParts/src/components/ProjectTimeline/TimelineList/useColumns.tsx b/SharePointFramework/ProjectWebParts/src/components/ProjectTimeline/TimelineList/useColumns.tsx index 132792c82..34d1b1993 100644 --- a/SharePointFramework/ProjectWebParts/src/components/ProjectTimeline/TimelineList/useColumns.tsx +++ b/SharePointFramework/ProjectWebParts/src/components/ProjectTimeline/TimelineList/useColumns.tsx @@ -1,10 +1,11 @@ -import { TableCellLayout, TableColumnDefinition } from '@fluentui/react-components' +import { Persona, TableCellLayout, TableColumnDefinition } from '@fluentui/react-components' import React, { useContext } from 'react' import { ProjectTimelineContext } from '../context' import { get } from '@microsoft/sp-lodash-subset' import { tryParseCurrency } from 'pp365-shared-library/lib/util/tryParseCurrency' import moment from 'moment' import { stringIsNullOrEmpty } from '@pnp/core' +import { getUserPhoto } from 'pp365-shared-library' export interface IListColumn extends TableColumnDefinition { minWidth?: number @@ -14,6 +15,23 @@ export interface IListColumn extends TableColumnDefinition { export const useColumns = (): IListColumn[] => { const context = useContext(ProjectTimelineContext) + const renderPersona = (item) => { + return ( + + ) + } + return context.state?.data?.listColumns.map((column) => { if (!column.fieldName) return null @@ -23,15 +41,18 @@ export const useColumns = (): IListColumn[] => { minWidth: column.minWidth, compare: (a, b) => { switch (column?.data?.type.toLowerCase()) { + case 'number': case 'counter': - case 'int': - return (a[column.fieldName] ?? '').localeCompare(b[column.fieldName] ?? '', undefined, { - numeric: true - }) + case 'currency': + return a[column.fieldName] - b[column.fieldName] + case 'user': case 'lookup': return (a[column.fieldName]?.Title ?? '').localeCompare( b[column.fieldName]?.Title ?? '' ) + case 'date': + case 'datetime': + return new Date(a[column.fieldName]).getTime() - new Date(b[column.fieldName]).getTime() default: return (a[column.fieldName] ?? '').localeCompare(b[column.fieldName] ?? '') } @@ -45,18 +66,20 @@ export const useColumns = (): IListColumn[] => { if (!stringIsNullOrEmpty(value)) { switch (column?.data?.type.toLowerCase()) { - case 'int': + case 'counter': + case 'number': cellValue = parseInt(value) break case 'date': - cellValue = moment(value).format('DD.MM.YYYY') - break case 'datetime': cellValue = moment(value).format('DD.MM.YYYY') break case 'currency': cellValue = tryParseCurrency(value) break + case 'user': + cellValue = renderPersona(value) + break case 'lookup': cellValue = value.Title break diff --git a/SharePointFramework/ProjectWebParts/src/components/ProjectTimeline/data/fetchTimelineData.ts b/SharePointFramework/ProjectWebParts/src/components/ProjectTimeline/data/fetchTimelineData.ts index bc755ce55..b9ba65a8b 100644 --- a/SharePointFramework/ProjectWebParts/src/components/ProjectTimeline/data/fetchTimelineData.ts +++ b/SharePointFramework/ProjectWebParts/src/components/ProjectTimeline/data/fetchTimelineData.ts @@ -1,9 +1,14 @@ import SPDataAdapter from 'data/SPDataAdapter' import _ from 'lodash' -import { TimelineConfigurationModel, TimelineContentModel } from 'pp365-shared-library/lib/models' +import { + SPField, + TimelineConfigurationModel, + TimelineContentModel +} from 'pp365-shared-library/lib/models' import strings from 'ProjectWebPartsStrings' import { IProjectTimelineProps } from '../types' import '@pnp/sp/items/get-all' +import { getClassProperties } from 'pp365-shared-library' /** * Fetch timeline items and columns. @@ -43,24 +48,39 @@ export async function fetchTimelineData( }) .filter(Boolean) - const defaultViewColumns = ( + let defaultViewColumns = ( await timelineContentList.defaultView.fields.select('Items').top(500)() )['Items'] as string[] const filterString = defaultViewColumns.map((col) => `(InternalName eq '${col}')`).join(' or ') + const defaultColumns = await timelineContentList.fields + .filter(filterString) + .select(...getClassProperties(SPField)) + .top(500)() + + const userFields = defaultColumns + .filter((fld) => fld.TypeAsString.indexOf('User') === 0) + .map((fld) => fld.InternalName) + + //remove user fields from default view columns + defaultViewColumns = defaultViewColumns.filter((col) => userFields.indexOf(col) === -1) + // eslint-disable-next-line prefer-const let [timelineContentItems, timelineColumns] = await Promise.all([ timelineContentList.items .select( - ...defaultViewColumns, 'Id', 'GtTimelineTypeLookup/Title', 'GtSiteIdLookupId', 'GtSiteIdLookup/Title', - 'GtSiteIdLookup/GtSiteId' + 'GtSiteIdLookup/GtSiteId', + ...defaultViewColumns, + ...userFields.map((fieldName) => `${fieldName}/Id`), + ...userFields.map((fieldName) => `${fieldName}/Title`), + ...userFields.map((fieldName) => `${fieldName}/EMail`) ) - .expand('GtSiteIdLookup', 'GtTimelineTypeLookup') + .expand('GtSiteIdLookup', 'GtTimelineTypeLookup', ...userFields) .getAll(), timelineContentList.fields .filter(filterString) @@ -119,6 +139,6 @@ export async function fetchTimelineData( return { timelineContentItems, timelineListItems, columns, timelineConfig } as const } catch (error) { - return null + throw error } } diff --git a/SharePointFramework/ProjectWebParts/tsconfig.json b/SharePointFramework/ProjectWebParts/tsconfig.json index bd34fabe0..7290ddf52 100644 --- a/SharePointFramework/ProjectWebParts/tsconfig.json +++ b/SharePointFramework/ProjectWebParts/tsconfig.json @@ -14,7 +14,7 @@ "outDir": "lib", "inlineSources": false, "strictNullChecks": false, - "noUnusedLocals": true, + "noUnusedLocals": false, "typeRoots": [ "./node_modules/@types", "./node_modules/@microsoft" From 9e99e45953b383f55bc442a047ccfcc7f12ecdd5 Mon Sep 17 00:00:00 2001 From: Remi Blom-Ohlsen Date: Tue, 12 Sep 2023 13:47:36 +0200 Subject: [PATCH 5/8] Adjustment to minWidth of columns --- .../src/components/ProjectTimeline/data/fetchTimelineData.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SharePointFramework/ProjectWebParts/src/components/ProjectTimeline/data/fetchTimelineData.ts b/SharePointFramework/ProjectWebParts/src/components/ProjectTimeline/data/fetchTimelineData.ts index b9ba65a8b..7bad35f57 100644 --- a/SharePointFramework/ProjectWebParts/src/components/ProjectTimeline/data/fetchTimelineData.ts +++ b/SharePointFramework/ProjectWebParts/src/components/ProjectTimeline/data/fetchTimelineData.ts @@ -99,7 +99,7 @@ export async function fetchTimelineData( name: column.Title, fieldName: column.InternalName, data: { type: column.TypeAsString }, - minWidth: 150, + minWidth: 100, maxWidth: 200 })) From 6a6a3e37b0ae3a0ed73f205b12242fdf582571d6 Mon Sep 17 00:00:00 2001 From: Remi Blom-Ohlsen Date: Tue, 12 Sep 2023 13:54:31 +0200 Subject: [PATCH 6/8] Fix issue where all timeline items showed up on Projects --- .../src/components/ProjectTimeline/data/fetchTimelineData.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SharePointFramework/ProjectWebParts/src/components/ProjectTimeline/data/fetchTimelineData.ts b/SharePointFramework/ProjectWebParts/src/components/ProjectTimeline/data/fetchTimelineData.ts index 7bad35f57..6032b5e88 100644 --- a/SharePointFramework/ProjectWebParts/src/components/ProjectTimeline/data/fetchTimelineData.ts +++ b/SharePointFramework/ProjectWebParts/src/components/ProjectTimeline/data/fetchTimelineData.ts @@ -115,7 +115,7 @@ export async function fetchTimelineData( ].join('') })) - timelineContentItems = timelineContentItems + timelineContentItems = timelineListItems .filter((item) => item.GtSiteIdLookup !== null) .map((item) => { const type = item.GtTimelineTypeLookup?.Title From 651815ee596f50b4386c1feb4746c566f714f9e4 Mon Sep 17 00:00:00 2001 From: Remi Blom-Ohlsen Date: Tue, 12 Sep 2023 14:36:44 +0200 Subject: [PATCH 7/8] Timeline group link for project goto timeline for the project + Tooltip --- .../ProjectTimeline/Timeline/Timeline.module.scss | 5 +++-- .../ProjectTimeline/Timeline/useGroupRenderer.tsx | 15 +++++++++++---- .../shared-library/src/loc/mystrings.d.ts | 5 +++-- .../shared-library/src/loc/nb-no.js | 5 +++-- 4 files changed, 20 insertions(+), 10 deletions(-) diff --git a/SharePointFramework/shared-library/src/components/ProjectTimeline/Timeline/Timeline.module.scss b/SharePointFramework/shared-library/src/components/ProjectTimeline/Timeline/Timeline.module.scss index 37426e3ab..eebdaf77b 100644 --- a/SharePointFramework/shared-library/src/components/ProjectTimeline/Timeline/Timeline.module.scss +++ b/SharePointFramework/shared-library/src/components/ProjectTimeline/Timeline/Timeline.module.scss @@ -1,4 +1,4 @@ -@import '~@fluentui/react/dist/sass/References.scss'; +@import "~@fluentui/react/dist/sass/References.scss"; .root { .header { @@ -17,7 +17,8 @@ padding: 0 32px 0 32px; font-size: 12px !important; - .timelineItem, .timelineItemMilestone { + .timelineItem, + .timelineItemMilestone { cursor: pointer !important; border-radius: var(--borderRadiusSmall); min-width: 28px; diff --git a/SharePointFramework/shared-library/src/components/ProjectTimeline/Timeline/useGroupRenderer.tsx b/SharePointFramework/shared-library/src/components/ProjectTimeline/Timeline/useGroupRenderer.tsx index 5695408d5..d05b1bbc5 100644 --- a/SharePointFramework/shared-library/src/components/ProjectTimeline/Timeline/useGroupRenderer.tsx +++ b/SharePointFramework/shared-library/src/components/ProjectTimeline/Timeline/useGroupRenderer.tsx @@ -1,7 +1,8 @@ import { ITimelineGroup, TimelineGroupType } from '../../../interfaces' import { ReactCalendarGroupRendererProps } from 'react-calendar-timeline' import React from 'react' -import { Link } from '@fluentui/react-components' +import { Link, Tooltip } from '@fluentui/react-components' +import strings from 'SharedLibraryStrings' /** * Timeline group renderer hook @@ -13,9 +14,15 @@ export function useGroupRenderer() { return (
{group.type === TimelineGroupType.Project ? ( - - {group.title} - + + + {group.title} + + ) : ( {group.title} diff --git a/SharePointFramework/shared-library/src/loc/mystrings.d.ts b/SharePointFramework/shared-library/src/loc/mystrings.d.ts index d168bc320..5cc6cd486 100644 --- a/SharePointFramework/shared-library/src/loc/mystrings.d.ts +++ b/SharePointFramework/shared-library/src/loc/mystrings.d.ts @@ -1,6 +1,5 @@ /* eslint-disable @typescript-eslint/no-empty-interface */ declare interface ISharedLibraryStrings { - FiltersString: string Aria: { InfoLabelTitle: string MenuOverflowCount: string @@ -18,6 +17,7 @@ declare interface ISharedLibraryStrings { DescriptionFieldLabel: string DiamondLabel: string EndDateLabel: string + FiltersString: string FilterText: string GroupByLabel: string LastPublishedStatusreport: string @@ -36,7 +36,8 @@ declare interface ISharedLibraryStrings { StartDateLabel: string SubPhaseLabel: string TagFieldLabel: string - TriangleLabel: any + TimelineGroupDescription: string + TriangleLabel: string TypeLabel: string } diff --git a/SharePointFramework/shared-library/src/loc/nb-no.js b/SharePointFramework/shared-library/src/loc/nb-no.js index 051b50834..2a8346eaa 100644 --- a/SharePointFramework/shared-library/src/loc/nb-no.js +++ b/SharePointFramework/shared-library/src/loc/nb-no.js @@ -1,6 +1,5 @@ define([], function () { return { - FiltersString: 'Filtre', Aria: { InfoLabelTitle: 'Informasjon om {0}', MenuOverflowCount: '{0} flere elementer', @@ -18,6 +17,7 @@ define([], function () { DescriptionFieldLabel: 'Beskrivelse', DiamondLabel: 'Diamant', EndDateLabel: 'Sluttdato', + FiltersString: 'Filtre', FilterText: 'Filtrer', GroupByLabel: 'Grupper etter', LastPublishedStatusreport: 'Gå til siste statusrapport', @@ -36,7 +36,8 @@ define([], function () { StartDateLabel: 'Startdato', SubPhaseLabel: 'Delfase', TagFieldLabel: 'Etikett', + TimelineGroupDescription: 'Gå til tidslinje for prosjektet', TriangleLabel: 'Trekant', - TypeLabel: 'Type' + TypeLabel: 'Type', } }) From 35ba5c93432b66ee1d6bc1b655d779c22171aa4b Mon Sep 17 00:00:00 2001 From: Remi Blom-Ohlsen Date: Tue, 12 Sep 2023 14:45:57 +0200 Subject: [PATCH 8/8] Truncate group names with ellipsis --- .../components/ProjectTimeline/Timeline/useGroupRenderer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SharePointFramework/shared-library/src/components/ProjectTimeline/Timeline/useGroupRenderer.tsx b/SharePointFramework/shared-library/src/components/ProjectTimeline/Timeline/useGroupRenderer.tsx index d05b1bbc5..d4e853f45 100644 --- a/SharePointFramework/shared-library/src/components/ProjectTimeline/Timeline/useGroupRenderer.tsx +++ b/SharePointFramework/shared-library/src/components/ProjectTimeline/Timeline/useGroupRenderer.tsx @@ -12,7 +12,7 @@ export function useGroupRenderer() { const style: React.CSSProperties = { display: 'block', width: '100%' } return ( -
+
{group.type === TimelineGroupType.Project ? (