Tag Archives: PowerShell

What is Microsoft Viva Connections? – Integrating a SharePoint intranet with Teams

Microsoft Viva Connections is a great way to communicate and publish information to users in Microsoft Teams. If you are already familiar with SharePoint, then the simple way of describing Viva Connections is “A SharePoint Intranet inside a Microsoft Teams App”. It means that users can access company news, policies, and other published information without having to leave Teams.

Viva connections - SharePoint in Teams App

During the pandemic, Teams has been an important communication tool for organisations. Users can chat and collaborate as they work in the office or from home. However, to access published organisation information such as news, policies, human resources, etc, users would have to open a browser and go to the organisation intranet. Viva connections brings all of this inside the Teams app to help streamline the organisation’s working practice.

How to setup Viva Connections.

In this video, we show you what Viva Connections looks like and how to set it up. Please note that Viva Connections requires a SharePoint intranet before setting up. If you need help with SharePoint or Teams, get in touch with one of our experts.



Access the Microsoft guide here.

If you would like to find out more about how Cloud Design Box can help you get more out of Microsoft Teams and SharePoint, contact one of our experts today.


Cloud Design Box

Why Should Your School or Trust Use Microsoft School Data Sync? 

When considering Teams for your school, there are several different approaches, you can create your own Class Teams manually, use a PowerShell script or take advantage of Microsoft’s School Data Sync.  

Below, we have rounded up the pros and cons of each method so you can decide which is best for your school, academy or multi-academy trust.  

Create your own classes manually. 

Teachers can create their own Class Teams and manually add or remove students as and when they join or leave the school. 

However, this process is very time consuming and needs to be repeated every new school year, with the teacher also responsible for keeping track of new students and those that may leave the school.  

This is more suited to primary schools or smaller schools where there aren’t that many classes to create and maintain.  

Pros: 

  • Teachers can quickly create and maintain their own Class Teams. 
  • No additional coding or script is needed.  

Cons: 

  • It’s time-consuming. 
  • Whole school reporting is limited. 
  • No standardisation across Teams. 
  • No parental engagement tools. 
  • Manual archiving rollover process each academic year. 

Use a PowerShell script. 

A PowerShell script can be generated to create your Class Team as a one-off or ongoing link via your school’s MIS. This creates the classes and keeps them updated, however, there are limits to using PowerShell, such as some functionality that is provided by Microsoft School Data Sync.  

Pros:  

  • Less time consuming than manually creating your Class Teams.  

Cons: 

  • Limited functionality when it comes to whole school reports.  
  • Some features provided by Microsoft School Data Sync are not available with PowerShell. 
  • Scripts required to archive classes and roll over the academic year. 
  • No parental engagement tools.  

 

Use Microsoft School Data Sync.

Microsoft School Data Sync (SDS) is recommended best practice when creating and maintaining Class Teams. 

Not only are there several APIs and third-party products to help you automate a lot of the process, but there are many added benefits to using SDS.   

Firstly, you’re able to include additional information to your data set, including grades (year groups), courses (subjects), schools and more, which enables you to report on insights across the entire school. These reports help your leadership team improve user adoption and provide support where needed.  

At the end of each academic year, there is a clear rollover and clean-up process, automatically linking up with your MIS and allowing all students and staff  (new and continuing) access to everything they need while removing permissions of school leavers and former members of staff.   

Another useful feature is the parental engagement tools – SDS uploads guardian information to automate weekly email digests of work set through Teams for their children.  

Microsoft is continually working on new features to support the ever-changing landscape of education and blended learning. In fact, it’s going to be rolled up into a bigger Microsoft Data Sync model, so you may see a name change shortly, but the data strategy will still be as powerful, if not more.  

Pros: 

  • Automated creation of Class Teams linked to MIS. 
  • Automated roundup sent to parents/guardians of their child’s work. 
  • In-depth school/trust-wide reports that enable you to provide better leadership, training and support to your students and staff.  
  • Easy-to-use rollover process – no manual set up each academic year.  

Cons: 

  • Third-party products may be required to set up a live link with School MIS because doing it manually with CSVs can be time-consuming. 

Microsoft School Data Sync is best suited to schools, academies and multi-academy trusts that wish to save teacher time, achieve more with their data and support students in a blended learning approach.  

Already have Microsoft School Data Sync? Talk to us about how we can extend this to provide central resource areas, SharePoint intranets, Class Cover tools and much more

Not set up with Microsoft School Data Sync? We can help you get started and future-proof your Class Teams setup. Contact us for a friendly chat with one of our Education Experts.  

Store Teams meeting recordings in SharePoint and OneDrive

Microsoft Teams meeting recordings are no longer in Microsoft Stream for many education licenses. This means it can be difficult to find or delete recordings at the current time.

Old meeting no longer stored in Stream

Microsoft are planning on moving all meeting recordings into OneDrive and SharePoint However, in the meantime recordings are stored in a temporary location (causing issues like deleting or keeping video files). This guide goes through how to enable SharePoint and OneDrive storage for Microsoft Teams meeting recordings early (switchover date for education tenants is 7th July 2021).



Where will new meeting records be stored?

Once enabled, any “Meet Now” recordings created in the Teams calendar will be saved into OneDrive of the person starting the meeting. Any recordings created in the Team channel (such as online lessons) will be stored in the files tab of the Team under a “Recordings” folder. More detailed examples can be found on the Microsoft site.

Recorded meetings folder

Configure OneDrive and SharePoint via PowerShell

Connect to Skype PowerShell using the code below.

Import-Module SkypeOnlineConnector
$sfbSession = New-CsOnlineSession
Import-PSSession $sfbSession

If there are no policies applied to users, apply the setting to the “Global” policy.

Set-CsTeamsMeetingPolicy -Identity Global -RecordingStorageMode "OneDriveForBusiness"

To check if there are any policies applied to individuals, open the Teams admin centre, and locate a typical staff user. Click “View Policies” to locate the name of the Meeting policy applied to the user.
Teams admin centre center

Run the command again but this time for the custom policy. In this example, the policy is called “AllTeachersMeetingPolicy”.

Set-CsTeamsMeetingPolicy -Identity AllTeachersMeetingPolicy -RecordingStorageMode "OneDriveForBusiness"

Sign out and then back in again (it may take a few hours to take effect)

Meeting recordings should now be stored inside OneDrive or SharePoint (Teams file tab).

Meeting recording stored in OneDrive or SharePoint in Teams
files tab storage

Setting Team Policies for Safeguarding in Education

Before you start to use Teams in school, it is important to consider setting policies for safeguarding to promote the welfare of children and protect them from harm.

Each school must consider their own policies because one size does not fit all. For example, some schools might be comfortable with students direct messaging teachers for help while others will want this communication in a more open space. The school’s behaviour policy should also be taken into consideration. It is therefore important for IT to involve the safeguarding officer when planning out which policies to apply to users.

Microsoft have made it easier to assign policies to users (this was previously done through PowerShell and still is for some policies – see our previous PowerShell post).

Teams Admin Center

We recommend you create a custom policy for both staff and students. Staff will need changes to the policies too otherwise they won’t be able to do things like delete student messages in Teams (see our previous PowerShell post).



It is also important to remember that there isn’t a single policy to manage teams, it is broken up into:

  • Meeting Policies
  • Live Event Policies
  • Messaging Policies
  • Permission Policies (PowerShell applied only)
  • Emergency Policies (PowerShell applied only)
  • Voice Routing Policies
  • Call Park Policies
  • Calling Policies
  • Caller ID Policies (PowerShell applied only)

For each of these policy types, you will find a Global (Org-wide default) policy which will apply to everyone. Any changes to that policy will apply to everyone automatically.

Create a new policy

Create a new policy and give it a name using the “Add” button.

Create Policy

New messaging policy

Apply the policy to a group

Click on the “Group policy assignment” tab (if it’s not visible refer to our PowerShell post).

Group policy assignment tab

Click “Add group”.

Add Group Button

Search for a group and then select a policy before clicking “Apply”.

Apply Teams Policy to Group

This is much easier and quicker than running PowerShell scripts, we hope you find that useful!

Update 11/11/2020: We have been informed that if you set a user’s policy through PowerShell, this group method above may not work for you and you may need to use PowerShell to apply the policy.

Useful PowerShell Scripts for Managing Classes in Microsoft Teams

So, you have school data sync setup and all of your class teams have been generated in Microsoft Teams. Teachers are eager to start using it for extending the classroom or remote learning. Teachers then realise that students can do things that they were not aware of and request for some rights to be restricted.

Here is a list of useful PowerShell scripts to help you manage some of the most common issues that schools face.

  • – Allow teachers to delete student messages
  • – Stop students emailing the class group
  • – Disable chat for students
  • – Calling and Live Event Policies

Allow teachers to delete student messages

It’s surprising that this is not enabled as standard. Owners in class teams cannot delete member messages unless a custom message policy is set.

Create a messaging policy in the Teams Admin centre

Create a new messaging policy and select “Owners can delete sent messages”

Create custom message policy in Teams
Owners can delete messages
Apply Custom Message Policy using PowerShell
 

This needs to be run as a global admin. The variables at the top of the script should be changed to the AAD (or synced AD) group that you want to apply the messaging policy to and the message policy name.

#Variables to change - add the AAD group and custom message policy name here
$ADSecurityGroupNameToApplyPolicyTo="All Teachers"
$customMessagePolicyName="CustomTeacherMessagingPolicy1"

# Install AzureAD PowerShell if you don't already have it - commented out below
# install-module azuread

#Import modules if you haven't already
Import-Module SkypeOnlineConnector
Import-Module AzureAD

#Connect to Skype and Azure AD
$userCredential = Get-Credential
$sfbSession = New-CsOnlineSession -Credential $userCredential
Import-PSSession $sfbSession
Connect-AzureAD -Credential $userCredential

$GroupUsers = Get-AzureADGroup -ALL $true -Filter "DisplayName eq '$ADSecurityGroupNameToApplyPolicyTo'" | Get-AzureADGroupMember -ALL $true | select mail
 
foreach ($GroupUser in $GroupUsers)
{
	$userEmail=$GroupUser.Mail
	write-host "Processing $userEmail"
	Grant-CsTeamsMessagingPolicy -PolicyName "$customMessagePolicyName" -Identity "$userEmail"
}

Stop students emailing the class group

Once a student receives a welcome message into a group, they may reply back to it or find it in the address list and start a large group email.

In the script below connect to Microsoft Exchange PowerShell. You should update the variables with an AD security group for students to apply the policy to. To ensure you only apply this to the relevant teams, use the wildcard search to filter them. In this example we are assuming teams have been named in a format of SchoolCode-AcademicYear-ClassName so we can set the wildcard to only apply this setting to Teams starting with SCH-2019.

######Replace the following variables if necessary##########
$studentADSecurityGroup ="All Students"   #AD Group for all students
$wildcardsearch="SCH-2019*"                #Wildcard for Teams display name - Search for Teams beginning with ....  
###########################################################

$MyCredential = Get-Credential
$Session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri https://outlook.office365.com/powershell-liveid/ -Credential $MyCredential -Authentication Basic -AllowRedirection
Import-PSSession $Session -AllowClobber
$groups = Get-UnifiedGroup -ResultSize 20000 -SortBy DisplayName -Identity "$wildcardsearch" | Select DisplayName,WhenCreated,Id
 
foreach ($group in $groups)
{
    $teamName = $group.DisplayName
    Write-Host "restricting group emails on $teamName for $studentADSecurityGroup"
    Set-UnifiedGroup -Identity "$teamName" -RejectMessagesFromSendersOrMembers "$studentADSecurityGroup"
}

Disable chat for students

Teams is a safe environment for students to chat, chats can be audited and monitored more closely than if they where to use WhatsApp or snapchat outside of the school systems. However, there are some situations where it might require turning off for safeguarding reasons.

Create message policy in Teams admin centre
Teams message policy

Click “Add” to create a new message policy and turn off the chat setting.

Turn off chat for students


Apply Custom Message Policy using PowerShell

This needs to be run as a global admin. The variables at the top of the script should be changed to the AAD (or synced AD) group that you want to apply the messaging policy to and the message policy name.

#Variables to change - add the AAD group and custom message policy name here
$ADSecurityGroupNameToApplyPolicyTo="All Students"
$customMessagePolicyName="CustomStudentMessagingPolicy1"

# Install AzureAD PowerShell if you don't already have it - commented out below
# install-module azuread

#Import modules if you haven't already
Import-Module SkypeOnlineConnector
Import-Module AzureAD

#Connect to Skype and Azure AD
$userCredential = Get-Credential
$sfbSession = New-CsOnlineSession -Credential $userCredential
Import-PSSession $sfbSession
Connect-AzureAD -Credential $userCredential

$GroupUsers = Get-AzureADGroup -ALL $true -Filter "DisplayName eq '$ADSecurityGroupNameToApplyPolicyTo'" | Get-AzureADGroupMember -ALL $true | select mail
 
foreach ($GroupUser in $GroupUsers)
{
	$userEmail=$GroupUser.Mail
	write-host "Processing $userEmail"
	Grant-CsTeamsMessagingPolicy -PolicyName "$customMessagePolicyName" -Identity "$userEmail"
}

Calling Policies

Calling policies can be used to configure what can and can’t be done by users when calling on Teams. An example of this might be for preventing students from calling on Teams.

Calling policies can be found under Voice as shown below:

Calling Policies

These are the settings that can be applied:

Teams Calling Policy for Students

This is how we apply a calling policy:

#Variables to change - add the AAD group and custom message policy name here
$ADSecurityGroupNameToApplyPolicyTo="All Students"
$customMessagePolicyName="CallingPolicyForStudents"

# Install AzureAD PowerShell if you don't already have it - commented out below
# install-module azuread

#Import modules if you haven't already
Import-Module SkypeOnlineConnector
Import-Module AzureAD

#Connect to Skype and Azure AD
$userCredential = Get-Credential
$sfbSession = New-CsOnlineSession -Credential $userCredential
Import-PSSession $sfbSession
Connect-AzureAD -Credential $userCredential

$GroupUsers = Get-AzureADGroup -ALL $true -Filter "DisplayName eq '$ADSecurityGroupNameToApplyPolicyTo'" | Get-AzureADGroupMember -ALL $true | select mail
 
foreach ($GroupUser in $GroupUsers)
{
	$userEmail=$GroupUser.Mail
	write-host "Processing $userEmail"
	Grant-CsTeamsCallingPolicy -Identity "$userEmail" -PolicyName "$customMessagePolicyName"
}

Live Event Policies

Live Event policies might be used restricting who can attend or record them live events.

Live event policies can be found under Meetings as shown below:

Live Event Policies

These are the options when setting up a Live Events policy.

Teams Live Event Policy for Teachers

This is how we apply a Live Event policy:

#Variables to change - add the AAD group and custom message policy name here
$ADSecurityGroupNameToApplyPolicyTo="All Students"
$customMessagePolicyName="LiveEventPolicyForStudents"

# Install AzureAD PowerShell if you don't already have it - commented out below
# install-module azuread

#Import modules if you haven't already
Import-Module SkypeOnlineConnector
Import-Module AzureAD

#Connect to Skype and Azure AD
$userCredential = Get-Credential
$sfbSession = New-CsOnlineSession -Credential $userCredential
Import-PSSession $sfbSession
Connect-AzureAD -Credential $userCredential

$GroupUsers = Get-AzureADGroup -ALL $true -Filter "DisplayName eq '$ADSecurityGroupNameToApplyPolicyTo'" | Get-AzureADGroupMember -ALL $true | select mail
 
foreach ($GroupUser in $GroupUsers)
{
	$userEmail=$GroupUser.Mail
	write-host "Processing $userEmail"
	Grant-CsTeamsMeetingBroadcastPolicy -Identity "$userEmail" -PolicyName "$customMessagePolicyName"
}

Update 11/11/2020: We have been informed that you may need to connect to Teams PowerShell to run these commands rather than Skype on some tenants (Connect-MicrosoftTeams).

Changing the default reply button in Outlook Web Access from “Reply all”

Your users may have noticed that the default reply button in Outlook Web Access is “Reply all”. This can result in emails accidentally being sent to the wrong person (as many users assume this is the reply button without reading it).

Reply all in OWA is default

Luckily you can change this default behaviour.

Change for an individual user

The user can change this setting themselves by going into the Mail settings.

Mail Settings

Under Mail, Automatic processing and Reply settings, the user can change the default response to “Reply”.

Reply settings

Change for all users

There is a PowerShell command which will set this for a mailbox.

Set-MailboxMessageConfiguration cloudacademy -IsReplyAllTheDefaultResponse $false

We can take this further and loop through all the mailboxes to apply this setting.

Get-Mailbox -ResultSize unlimited -Filter {(RecipientTypeDetails -eq 'UserMailbox')} | Set-MailboxMessageConfiguration -IsReplyAllTheDefaultResponse $false

Changed to reply

Hopefully you have happy users again after that change! Video guide below.



Gaining access to OneDrives within your organisation

Below I have created a quick guide to show you how to gain access to a user’s OneDrive within your organisation. This video is for SharePoint administrators and you will need to be at least a SharePoint admin in Office 365 to carry out these steps.

A OneDrive site is effectively a SharePoint site collection with a document library. When a OneDrive is created by the user in Office 365, it grants the user site collection admin rights. It doesn’t add any other administrators or groups to the permissions. You can do this manually using the steps shown in the video below or you could create a script to apply permissions to all of your OneDrive sites using the PowerShell Client Object Model.



Using PowerShell to add a list or library WebPart to a SharePoint publishing page via CSOM

Thought I would share this as I struggled to find a complete article online how to do this. First of all, the code below is based on José Quinto’s post USING POWERSHELL TO ADD WEBPART TO SHAREPOINT PAGE VIA CSOM IN OFFICE 365. It’s a really good article on adding a content editor web part to a publishing page.

I couldn’t find any posts online on how to use the same technique to add a list view web part to a page. Eventually I figured out how to create the XML for adding a list view web part.

This is an adaptation of Jose’s function to add a web part to a page:

function AddWebPartToPage ($ctx, $sitesURL, $WebPartXml, $pageRelativeUrl, $wpZoneID, $wpZoneOrder) {
	try{
		Write-Host "Starting the Process to add the User WebPart to the Home Page" -ForegroundColor Yellow
		#Adding the reference to the client libraries. Here I'm executing this for a SharePoint Server (and I'm referencing it from the SharePoint ISAPI directory, 
		#but we could execute it from wherever we want, only need to copy the dlls and reference the path from here        
		Add-Type -Path "c:\Program Files\Common Files\microsoft shared\Web Server Extensions\15\ISAPI\Microsoft.SharePoint.Client.dll" 
		Add-Type -Path "c:\Program Files\Common Files\microsoft shared\Web Server Extensions\15\ISAPI\Microsoft.SharePoint.Client.Runtime.dll" 
		Write-Host "Getting the page with the webpart we are going to modify" -ForegroundColor Green
		#Using the params, build the page url
		$pageUrl = $sitesURL + $pageRelativeUrl
		Write-Host "Getting the page with the webpart we are going to modify: " $pageUrl -ForegroundColor Green
		#Getting the page using the GetFileByServerRelativeURL and do the Checkout
		#After that, we need to call the executeQuery to do the actions in the site
		$page = $ctx.Web.GetFileByServerRelativeUrl($pageUrl);
		$page.CheckOut()
		$ctx.ExecuteQuery()
		try{
		#Get the webpart manager from the page, to handle the webparts
		Write-Host "The page is checkout" -ForegroundColor Green
		$webpartManager = $page.GetLimitedWebPartManager([Microsoft.Sharepoint.Client.WebParts.PersonalizationScope]::Shared);
		Write-Host $WebPartXml.OuterXml
		#Load and execute the query to get the data in the webparts
		Write-Host "Getting the webparts from the page" -ForegroundColor Green
		$ctx.Load($webpartManager);
		$ctx.ExecuteQuery();
		#Import the webpart
		$wp = $webpartManager.ImportWebPart($WebPartXml.OuterXml)
		#Add the webpart to the page
		Write-Host "Add the webpart to the Page" -ForegroundColor Green
		$webPartToAdd = $webpartManager.AddWebPart($wp.WebPart, $wpZoneID, $wpZoneOrder)
		$ctx.Load($webPartToAdd);
		$ctx.ExecuteQuery()
		}
		catch{
			Write-Host "Errors found:`n$_" -ForegroundColor Red
		}
		finally{
			#CheckIn and Publish the Page
			Write-Host "Checkin and Publish the Page" -ForegroundColor Green
			$page.CheckIn("Add the User Profile WebPart", [Microsoft.SharePoint.Client.CheckinType]::MajorCheckIn)
			$page.Publish("Add the User Profile WebPart")
			$ctx.ExecuteQuery()
			Write-Host "The webpart has been added" -ForegroundColor Yellow 
		}	
	}
	catch{
		Write-Host "Errors found:`n$_" -ForegroundColor Red
	}
}

And this is the XML to add a SharePoint document library called “Documents” to the page.

$WebPartXml1 =  "
		<webParts>
			<webPart xmlns='http://schemas.microsoft.com/WebPart/v3'>
				<metaData>
				<type name='Microsoft.SharePoint.WebPartPages.XsltListViewWebPart, Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c' />
				<importErrorMessage>Cannot import this Web Part.</importErrorMessage>
			</metaData>
			<data>
				<properties>
					<property name='ListUrl' type='string'>Documents</property>
					<property name='ListName' type='string'>Documents</property>
				</properties>
			</data>
			</webPart>
		</webParts>"

The URL for a list is slightly different to a document library, the example below is the XML for an announcement list.

$WebPartXml1 =  "
		<webParts>
			<webPart xmlns='http://schemas.microsoft.com/WebPart/v3'>
				<metaData>
					<type name='Microsoft.SharePoint.WebPartPages.XsltListViewWebPart, Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c' />
					<importErrorMessage>Cannot import this Web Part.</importErrorMessage>
				</metaData>
				<data>
					<properties>
						<property name='ListUrl' type='string'>Lists/Student Announcements</property>
						<property name='ListName' type='string'>Student Announcements</property>
						<property name='JSLink' type='string'>~sitecollection/Style%20Library/cdb_custom_announcements/cdb_custom_announcements.js</property>
					</properties>
				</data>
			</webPart>
		</webParts>"

I then get the client context and pass the XML and variables to the function to add it to the page

$tenantAdmin = "user@domain.com"
$tenantAdminPassword = "password"
$secureAdminPassword = $(convertto-securestring $tenantAdminPassword -asplaintext -force)
$siteURL = "https://domain.com/sites/subsite";
$ctx = New-Object Microsoft.SharePoint.Client.ClientContext($siteUrl) 
$credentials = New-Object Microsoft.SharePoint.Client.SharePointOnlineCredentials($tenantAdmin, $secureAdminPassword)  
$ctx.Credentials = $credentials
$relUrl = "/sites/subsite"
$pageRelativeUrl1 = "/Pages/Default.aspx"
$wpZoneID1 = "Top Left"
$wpZoneOrder1 = 0
#Run function
AddWebPartToPage $ctx $relUrl $WebPartXml1 $pageRelativeUrl1 $wpZoneID1 $wpZoneOrder1

Nice example of adding web parts to a SharePoint Online publishing page using PowerShell CSOM.

Adding SharePoint Online navigation from XML using PowerShell CSOM

The following PowerShell scripts were created to enable me to deploy a custom navigation across multiple site collections. You can use managed metadata navigation as mentioned in my previous post. Unfortunately this method doesn’t allow the user to reuse managed metadata navigation across multiple site collections (no idea why, I thought that was one of the advantages of managed metadata navigation!).

So a new and clean way of doing this is to use the CSOM for PowerShell. The code below deletes every navigation node using the first function and then adds each item added to an XML file. A strength of using this method is it can be manipulated to add additional logic for adding links to particular site collections depending on the variables in the XML file. Hope you find this useful.

For SharePoint design, workflows, automation, training and support please visit my SharePoint consultancy site www.clouddesignbox.co.uk. We offer education and business SharePoint solutions and services.

Deleting all navigation nodes using CSOM PowerShell

It’s fairly straightforward to enumerate nodes in an array, in this example I’m deleting all the top navigation menu nodes in a SharePoint site. This is how I would normally loop through the top navigation menu:

$topNav = $context.Web.Navigation.TopNavigationBar;
$context.Load($topNav);
foreach ($topNavItem in $topNav)
{
	Write-Host $topNavItem.Title
}

However if I want to loop through the menu and delete all the nodes, the above function errors as the array has changed each time it loops, the method below works but doesn’t catch all the menu items.

for ($ii = 0; $ii -lt $topNodes.Count; $ii++)
{
	Write-Host $topNodes[$ii].Title 
	$topNodes[$ii].deleteObject();
	$context.ExecuteQuery();
}

As we are enumerating the nodes, we are removing nodes from the start and changing the position of the other nodes in the array. As the loop continues to run, it can skip positions of some of the nodes.

A solution which works better is looping through the array backwards. As you loop through the array backwards, it doesn’t change the position of items still in the array.

for ($ii = $topNodes.Count - 1; $ii -ge 0; $ii--)
{
	Write-Host $topNodes[$ii].Title 
	$topNodes[$ii].deleteObject();
	$context.ExecuteQuery();
}

Hope you may find this useful, it can be difficult to find why the loop misses some random items and hopefully looping backwards will avoid any issues like this.