diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b8159b1b..881e5df0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,10 +16,11 @@ Sjekk ut [release notes](./releasenotes/1.8.0.md) for høydepunkter og mer detal - Fikset et problem hvor 'Hooks' ikke ble kjørt på slutten av provisjoneringen av et prosjekt [#1127](https://github.com/Puzzlepart/prosjektportalen365/issues/1127) - Fikset et problem hvor prosjekttidslinje på prosjektnivå ikke returnerte tidslinje-elementer for prosjektet [#1172](https://github.com/Puzzlepart/prosjektportalen365/pull/1172) - Rettet en feil hvor Excel eksporten gir feilmelding når datofelt mangler data. [#1180](https://github.com/Puzzlepart/prosjektportalen365/issues/1180) +- Fikset et program hvor prosjekter ble vist selvom det ikke var tilknyttet prosjekter til et program [#1150](https://github.com/Puzzlepart/prosjektportalen365/pull/1150) --- -## 1.8.2 - TBA +## 1.8.2 - 08.06.23 ### Ny funksjonalitet diff --git a/SharePointFramework/ProgramWebParts/src/components/ProgramAdministration/AddProjectDialog/index.tsx b/SharePointFramework/ProgramWebParts/src/components/ProgramAdministration/AddProjectDialog/index.tsx index c93f543f1..01baf9d20 100644 --- a/SharePointFramework/ProgramWebParts/src/components/ProgramAdministration/AddProjectDialog/index.tsx +++ b/SharePointFramework/ProgramWebParts/src/components/ProgramAdministration/AddProjectDialog/index.tsx @@ -13,7 +13,6 @@ import * as strings from 'ProgramWebPartsStrings' import React, { FC, useContext } from 'react' import { columns } from '../columns' import { ProgramAdministrationContext } from '../context' -import { addChildProjects } from '../data' import { ListHeaderSearch } from '../ListHeaderSearch' import { ADD_CHILD_PROJECTS, TOGGLE_ADD_PROJECT_DIALOG } from '../reducer' import styles from './AddProjectDialog.module.scss' @@ -62,7 +61,7 @@ export const AddProjectDialog: FC = () => { text={strings.Add} disabled={_.isEmpty(context.state.selectedProjectsToAdd)} onClick={async () => { - await addChildProjects(context.props.dataAdapter, context.state.selectedProjectsToAdd) + await context.props.dataAdapter.addChildProjects(context.state.selectedProjectsToAdd) context.dispatch(ADD_CHILD_PROJECTS()) }} /> diff --git a/SharePointFramework/ProgramWebParts/src/components/ProgramAdministration/AddProjectDialog/useAddProjectDialog.ts b/SharePointFramework/ProgramWebParts/src/components/ProgramAdministration/AddProjectDialog/useAddProjectDialog.ts index 91fdcbcb8..40c0e75c3 100644 --- a/SharePointFramework/ProgramWebParts/src/components/ProgramAdministration/AddProjectDialog/useAddProjectDialog.ts +++ b/SharePointFramework/ProgramWebParts/src/components/ProgramAdministration/AddProjectDialog/useAddProjectDialog.ts @@ -1,6 +1,5 @@ import { useContext, useEffect } from 'react' import { ProgramAdministrationContext } from '../context' -import { getHubSiteProjects } from '../data' import { DATA_LOADED, SET_SELECTED_TO_ADD } from '../reducer' import { useRowRenderer } from '../useRowRenderer' import { useSelectionList } from '../useSelectionList' @@ -14,7 +13,7 @@ export const useAddProjectDialog = () => { }) useEffect(() => { - getHubSiteProjects() + context.props.dataAdapter.getHubSiteProjects() .then((availableProjects) => context.dispatch(DATA_LOADED({ data: { availableProjects }, scope: 'AddProjectDialog' })) ) diff --git a/SharePointFramework/ProgramWebParts/src/components/ProgramAdministration/Commands/index.tsx b/SharePointFramework/ProgramWebParts/src/components/ProgramAdministration/Commands/index.tsx index feef6f316..1e92c8b4c 100644 --- a/SharePointFramework/ProgramWebParts/src/components/ProgramAdministration/Commands/index.tsx +++ b/SharePointFramework/ProgramWebParts/src/components/ProgramAdministration/Commands/index.tsx @@ -3,7 +3,6 @@ import { isEmpty } from '@microsoft/sp-lodash-subset' import * as strings from 'ProgramWebPartsStrings' import React, { FC, useContext } from 'react' import { ProgramAdministrationContext } from '../context' -import { removeChildProjects } from '../data' import { CHILD_PROJECTS_REMOVED, TOGGLE_ADD_PROJECT_DIALOG } from '../reducer' export const Commands: FC = () => { @@ -25,7 +24,7 @@ export const Commands: FC = () => { disabled: isEmpty(context.state.selectedProjectsToDelete) || !context.state.userHasManagePermission, onClick: () => { - removeChildProjects(context.props.dataAdapter, context.state.selectedProjectsToDelete).then( + context.props.dataAdapter.removeChildProjects(context.state.selectedProjectsToDelete).then( (childProjects) => { context.dispatch(CHILD_PROJECTS_REMOVED({ childProjects })) } diff --git a/SharePointFramework/ProgramWebParts/src/components/ProgramAdministration/data.ts b/SharePointFramework/ProgramWebParts/src/components/ProgramAdministration/data.ts deleted file mode 100644 index 2a29fc6b3..000000000 --- a/SharePointFramework/ProgramWebParts/src/components/ProgramAdministration/data.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { sp } from '@pnp/sp' -import { SPDataAdapter } from 'data' -import { IProgramAdministrationProject } from './types' -import { dateAdd, PnPClientStorage } from '@pnp/common' -import { SearchQueryInit, SearchResult } from '@pnp/sp/src/search' - -/** - * Fetch items with `sp.search` using the specified `{queryTemplate}` and `{selectProperties}`. - * Uses a `while` loop to fetch all items in batches of `{batchSize}`. - * - * @param queryTemplate Query template - * @param selectProperties Select properties - * @param batchSize Batch size (default: 500) - */ -async function fetchItems( - queryTemplate: string, - selectProperties: string[], - batchSize = 500 -): Promise { - const query: SearchQueryInit = { - QueryTemplate: `${queryTemplate}`, - Querytext: '*', - RowLimit: batchSize, - TrimDuplicates: false, - ClientType: 'ContentSearchRegular', - SelectProperties: [...selectProperties, 'Path', 'SPWebURL', 'SiteTitle', 'UniqueID'] - } - const { PrimarySearchResults, TotalRows } = await sp.search(query) - const results = [...PrimarySearchResults] - while (results.length < TotalRows) { - const response = await sp.search({ ...query, StartRow: results.length }) - results.push(...response.PrimarySearchResults) - } - return results -} - -/** - * Fetches all projects associated with the current hubsite context. This is done by querying the - * search index for all sites with the same DepartmentId as the current hubsite and all project items with - * the same DepartmentId as the current hubsite. The sites are then matched with the items to - * retrieve the SiteId and SPWebURL. The result are cached for 5 minutes. - */ -export async function getHubSiteProjects() { - const { HubSiteId } = await sp.site.select('HubSiteId').usingCaching().get() - return new PnPClientStorage().local.getOrPut( - `hubsiteprojects_${HubSiteId}`, - async () => { - const [sites, items] = await Promise.all([ - fetchItems( - `DepartmentId:{${HubSiteId}} contentclass:STS_Site NOT WebTemplate:TEAMCHANNEL`, - ['Title', 'SiteId'] - ), - fetchItems( - `DepartmentId:{${HubSiteId}} ContentTypeId:0x0100805E9E4FEAAB4F0EABAB2600D30DB70C*`, - ['GtSiteIdOWSTEXT', 'Title'] - ) - ]) - return items - .filter( - (item) => - item['GtSiteIdOWSTEXT'] && - item['GtSiteIdOWSTEXT'] !== '00000000-0000-0000-0000-000000000000' - ) - .map((item) => { - const site = sites.find((site) => site['SiteId'] === item['GtSiteIdOWSTEXT']) - return { - SiteId: item['GtSiteIdOWSTEXT'], - Title: site?.Title ?? item['Title'], - SPWebURL: site && site['SPWebURL'] - } - }) - }, - dateAdd(new Date(), 'minute', 5) - ) -} - -/** - * Fetches current child projects. Fetches all available projects and filters out the ones that are not - * in the child projects project property `GtChildProjects`. - * - * @param dataAdapter Data adapter - */ -export async function fetchChildProjects(dataAdapter: SPDataAdapter): Promise { - const availableProjects = await getHubSiteProjects() - const childProjectsSiteIds = dataAdapter.childProjects.map((p) => p.SiteId) - return availableProjects.filter((p) => childProjectsSiteIds.indexOf(p.SiteId) !== -1) -} - -/** - * Add child projects. - * - * @param dataAdapter Data adapter - * @param newProjects New projects to add - */ -export async function addChildProjects( - dataAdapter: SPDataAdapter, - newProjects: Array> -) { - const [{ GtChildProjects }] = await sp.web.lists - .getByTitle('Prosjektegenskaper') - .items.select('GtChildProjects') - .get() - const projects = JSON.parse(GtChildProjects) - const updatedProjects = [...projects, ...newProjects] - const updateProperties = { GtChildProjects: JSON.stringify(updatedProjects) } - await sp.web.lists.getByTitle('Prosjektegenskaper').items.getById(1).update(updateProperties) - await dataAdapter.updateProjectInHub(updateProperties) -} - -/** - * Remove child projects. - * - * @param dataAdapter Data adapter - * @param projectToRemove Projects to delete - */ -export async function removeChildProjects( - dataAdapter: SPDataAdapter, - projectToRemove: Array> -): Promise>> { - const [currentData] = await sp.web.lists - .getByTitle('Prosjektegenskaper') - .items.select('GtChildProjects') - .get() - const projects: Array> = JSON.parse(currentData.GtChildProjects) - const updatedProjects = projects.filter( - (p) => !projectToRemove.some((el) => el.SiteId === p.SiteId) - ) - const updateProperties = { GtChildProjects: JSON.stringify(updatedProjects) } - await sp.web.lists.getByTitle('Prosjektegenskaper').items.getById(1).update(updateProperties) - await dataAdapter.updateProjectInHub(updateProperties) - return updatedProjects -} diff --git a/SharePointFramework/ProgramWebParts/src/components/ProgramAdministration/useProgramAdministration.ts b/SharePointFramework/ProgramWebParts/src/components/ProgramAdministration/useProgramAdministration.ts index 210f033c0..c8ef2cfb9 100644 --- a/SharePointFramework/ProgramWebParts/src/components/ProgramAdministration/useProgramAdministration.ts +++ b/SharePointFramework/ProgramWebParts/src/components/ProgramAdministration/useProgramAdministration.ts @@ -1,7 +1,6 @@ import { sp } from '@pnp/sp' import { ProjectAdminPermission } from 'pp365-shared/lib/data/SPDataAdapterBase/ProjectAdminPermission' import { useReducer, useEffect } from 'react' -import { fetchChildProjects } from './data' import reducer, { initialState, DATA_LOADED, SET_SELECTED_TO_DELETE } from './reducer' import { IProgramAdministrationProps } from './types' import { useRowRenderer } from './useRowRenderer' @@ -22,14 +21,14 @@ export const useProgramAdministration = (props: IProgramAdministrationProps) => useEffect(() => { props.dataAdapter.project.getPropertiesData().then((properties) => { Promise.all([ - fetchChildProjects(props.dataAdapter), + props.dataAdapter.fetchChildProjects(), props.dataAdapter.checkProjectAdminPermissions( ProjectAdminPermission.ChildProjectsAdmin, properties.fieldValues ) - ]).then(([childProjects, userHasManagePermission]) => + ]).then(([childProjects, userHasManagePermission]) => { dispatch(DATA_LOADED({ data: { childProjects, userHasManagePermission }, scope: 'root' })) - ) + }) }) }, []) diff --git a/SharePointFramework/ProgramWebParts/src/data/index.ts b/SharePointFramework/ProgramWebParts/src/data/index.ts index 3499a0165..2b7828127 100644 --- a/SharePointFramework/ProgramWebParts/src/data/index.ts +++ b/SharePointFramework/ProgramWebParts/src/data/index.ts @@ -1,9 +1,11 @@ import { format } from '@fluentui/react/lib/Utilities' import { flatten } from '@microsoft/sp-lodash-subset' import { WebPartContext } from '@microsoft/sp-webpart-base' -import { dateAdd } from '@pnp/common' +import { PnPClientStorage, dateAdd } from '@pnp/common' import { QueryPropertyValueType, SearchResult, SortDirection, sp } from '@pnp/sp' +import * as strings from 'ProgramWebPartsStrings' import * as cleanDeep from 'clean-deep' +import { IProgramAdministrationProject } from 'components/ProgramAdministration/types' import MSGraph from 'msgraph-helper' import { IGraphGroup, @@ -23,10 +25,9 @@ import { ISPDataAdapterBaseConfiguration, SPDataAdapterBase } from 'pp365-shared import { getUserPhoto } from 'pp365-shared/lib/helpers/getUserPhoto' import { DataSource, PortfolioOverviewView } from 'pp365-shared/lib/models' import { DataSourceService, ProjectDataService } from 'pp365-shared/lib/services' -import * as strings from 'ProgramWebPartsStrings' +import _ from 'underscore' import { GAINS_DEFAULT_SELECT_PROPERTIES } from './config' import { DEFAULT_SEARCH_SETTINGS, IFetchDataForViewItemResult } from './types' -import _ from 'underscore' /** * SPDataAdapter for `ProgramWebParts`. @@ -393,7 +394,7 @@ export class SPDataAdapter extends SPDataAdapterBase child?.SiteId === item?.GtSiteIdLookup?.GtSiteId || item?.GtSiteIdLookup?.GtSiteId === - this?.spfxContext?.pageContext?.site?.id?.toString() + this?.spfxContext?.pageContext?.site?.id?.toString() ) ) { if (item.GtSiteIdLookup?.Title && config && config.showElementPortfolio) { @@ -760,11 +761,178 @@ export class SPDataAdapter extends SPDataAdapterBase): Promise { try { + const siteId = this.spfxContext.pageContext.site.id.toString() const list = this.portal.web.lists.getByTitle(strings.ProjectsListName) const [item] = await list.items - .filter(`GtSiteId eq '${this.spfxContext.pageContext.site.id.toString()}'`) + .filter(`GtSiteId eq '${siteId}'`) .get() await list.items.getById(item.ID).update(properties) - } catch (error) {} + } catch (error) { } + } + + /** + * Get child projects from the Prosjektegenskaper list item. The note field `GtChildProjects` + * contains a JSON string with the child projects, and needs to be parsed. If the retrieve + * fails, an empty array is returned. + * + * @returns {Promise>>} Child projects + */ + public async getChildProjects(): Promise>> { + try { + const projectProperties = await sp.web.lists + .getByTitle('Prosjektegenskaper') + .items + .getById(1) + .usingCaching() + .get() + try { + const childProjects = JSON.parse(projectProperties.GtChildProjects) + return !_.isEmpty(childProjects) ? childProjects : [] + } catch { + return [] + } + } catch (error) { + return [] + } + } + + /** + * Initialize child projects. Runs `getChildProjects` and sets the `childProjects` property + * of the class. + */ + public async initChildProjects(): Promise { + try { + this.childProjects = await this.getChildProjects() + } catch (error) { } + } + + + /** + * Fetches all projects associated with the current hubsite context. This is done by querying the + * search index for all sites with the same DepartmentId as the current hubsite and all project items with + * the same DepartmentId as the current hubsite. The sites are then matched with the items to + * retrieve the SiteId and SPWebURL. The result are cached for 5 minutes. + */ + public async getHubSiteProjects() { + const { HubSiteId } = await sp.site.select('HubSiteId').usingCaching().get() + return new PnPClientStorage().local.getOrPut( + `HubSiteProjects_${HubSiteId}`, + async () => { + const [{ PrimarySearchResults: sts_sites }, { PrimarySearchResults: items }] = + await Promise.all([ + sp.search({ + Querytext: `DepartmentId:{${HubSiteId}} contentclass:STS_Site NOT WebTemplate:TEAMCHANNEL`, + RowLimit: 500, + StartRow: 0, + ClientType: 'ContentSearchRegular', + SelectProperties: ['SPWebURL', 'Title', 'SiteId'], + TrimDuplicates: false + }), + sp.search({ + Querytext: `DepartmentId:{${HubSiteId}} ContentTypeId:0x0100805E9E4FEAAB4F0EABAB2600D30DB70C*`, + RowLimit: 500, + StartRow: 0, + ClientType: 'ContentSearchRegular', + SelectProperties: ['GtSiteIdOWSTEXT', 'Title'], + TrimDuplicates: false + }) + ]) + return items + .filter( + (item) => + item['GtSiteIdOWSTEXT'] && + item['GtSiteIdOWSTEXT'] !== '00000000-0000-0000-0000-000000000000' + ) + .map((item) => { + const site = sts_sites.find((site) => site['SiteId'] === item['GtSiteIdOWSTEXT']) + return { + SiteId: item['GtSiteIdOWSTEXT'], + Title: site?.Title ?? item['Title'], + SPWebURL: site && site['SPWebURL'] + } + }) + }, + dateAdd(new Date(), 'minute', 5) + ) + } + + + + /** + * Get child project site IDs from the Prosjektegenskaper list item. The note field `GtChildProjects` + * contains a JSON string with the child projects, and needs to be parsed. If the retrieve + * fails, an empty array is returned. + */ + public async getChildProjectIds(): Promise { + try { + const projectProperties = await sp.web.lists + .getByTitle('Prosjektegenskaper') + .items + .getById(1) + .usingCaching() + .get() + try { + const childProjects = JSON.parse(projectProperties.GtChildProjects) + return childProjects.map((p: Record) => p.SiteId) + } catch { + return [] + } + } catch (error) { + return [] + } + } + + /** + * Fetches current child projects. Fetches all available projects and filters out the ones that are not + * in the child projects project property `GtChildProjects`. + */ + public async fetchChildProjects(): Promise { + const [availableProjects, childProjects] = await Promise.all([ + this.getHubSiteProjects(), + this.getChildProjects() + ]) + const childProjectsSiteIds = childProjects.map((p: Record) => p.SiteId) + return availableProjects.filter((p) => childProjectsSiteIds.indexOf(p.SiteId) !== -1) + } + + /** + * Add child projects. + * + * @param newProjects New projects to add + */ + public async addChildProjects( + newProjects: Array> + ) { + const [{ GtChildProjects }] = await sp.web.lists + .getByTitle('Prosjektegenskaper') + .items.select('GtChildProjects') + .get() + const projects = JSON.parse(GtChildProjects) + const updatedProjects = [...projects, ...newProjects] + const updateProperties = { GtChildProjects: JSON.stringify(updatedProjects) } + await sp.web.lists.getByTitle('Prosjektegenskaper').items.getById(1).update(updateProperties) + await this.updateProjectInHub(updateProperties) + } + + /** + * Remove child projects. + * + * @param projectToRemove Projects to delete + */ + public async removeChildProjects( + projectToRemove: Array> + ): Promise>> { + const [currentData] = await sp.web.lists + .getByTitle('Prosjektegenskaper') + .items.select('GtChildProjects') + .get() + const projects: Array> = JSON.parse(currentData.GtChildProjects) + const updatedProjects = projects.filter( + (p) => !projectToRemove.some((el) => el.SiteId === p.SiteId) + ) + const updateProperties = { GtChildProjects: JSON.stringify(updatedProjects) } + await sp.web.lists.getByTitle('Prosjektegenskaper').items.getById(1).update(updateProperties) + await this.updateProjectInHub(updateProperties) + return updatedProjects } } diff --git a/SharePointFramework/ProgramWebParts/src/webparts/baseProgramWebPart/index.ts b/SharePointFramework/ProgramWebParts/src/webparts/baseProgramWebPart/index.ts index 75444a415..b646d0c25 100644 --- a/SharePointFramework/ProgramWebParts/src/webparts/baseProgramWebPart/index.ts +++ b/SharePointFramework/ProgramWebParts/src/webparts/baseProgramWebPart/index.ts @@ -8,7 +8,6 @@ import React, { ComponentClass, FC } from 'react' import ReactDom from 'react-dom' import { SPDataAdapter } from '../../data' import { IBaseProgramWebPartProps } from './types' -import _ from 'lodash' export abstract class BaseProgramWebPart< T extends IBaseProgramWebPartProps @@ -35,26 +34,6 @@ export abstract class BaseProgramWebPart< ReactDom.render(element, this.domElement) } - /** - * Get child projects from the Prosjektegenskaper list item. The note field GtChildProjects - * contains a JSON string with the child projects, and needs to be parsed. If the retrieve - * fails, an empty array is returned. - * - * @returns {Promise>>} Child projects - */ - public async getChildProjects(): Promise>> { - try { - const projectProperties = await sp.web.lists - .getByTitle('Prosjektegenskaper') - .items.getById(1) - .get() - const childProjects = JSON.parse(projectProperties.GtChildProjects) - return !_.isEmpty(childProjects) ? childProjects : [] - } catch (error) { - return [] - } - } - private async _setup() { await this.dataAdapter.configure(this.context, { siteId: this.context.pageContext.site.id.toString(), @@ -66,7 +45,7 @@ export abstract class BaseProgramWebPart< public async onInit(): Promise { sp.setup({ spfxContext: this.context }) this.dataAdapter = new SPDataAdapter() - this.dataAdapter.childProjects = await this.getChildProjects() + this.dataAdapter.initChildProjects() this.context.statusRenderer.clearLoadingIndicator(this.domElement) await this._setup() }