2/15/2018

Run SharePoint 2013 and 2016 Search Reports from PowerShell


Updated to include IDs for SharePoint 2016!   Original article here.


Update! Need these reports for every site collection in the farm? See Part 2: http://techtrainingnotes.blogspot.com/2015/04/run-sharepoint-2013-search-reports-from_21.html


In my Search Administration class I stress that admins should dump the search reports on a regular basis as the data is only kept in detail for 14 days and in summary form for 35 months. But who wants to both run these reports at least once every 14 days, even they can remember to do so. So, PowerShell to the rescue… Schedule this script to run each weekend and your work is done.

The following script works for on premise SharePoint 2013. To work with Office 365 you will have to figure out how to include your credentials. The example included here works on premises by using "UseDefaultCredentials = $true".

After lots of hacking, detective work (see below) and just plain trial and error, here's the script:

# This is the URL from YOUR Central Admin Search Service Usage Reports page:
#
# The script will not work unless this is correct!
# $url = "http://yourCentralAdminURL/_layouts/15/reporting.aspx?Category=AnalyticsSearch&appid=ed39c68b%2D7276%2D46f7%2Db94a%2D4ae7125cf567" # This is the path to write the reports to (must exist, but can be anywhere): $path = "c:\SearchReports\" function Get-SPSearchReports ($farmurl, $searchreport, $path, $version) { # TechTrainingNotes.blogspot.com
if ($version -eq "2013")
{ # Report names and IDs $Number_of_Queries = "
21be5dff-c853-4259-ab01-ee8b2f6590c7" $Top_Queries_by_Day = "56928342-6e3b-4382-a14d-3f5f4f8b6979" $Top_Queries_by_Month = "a0a26a8c-bf99-48f4-a679-c283de58a0c4" $Abandoned_Queries_by_Day = "e628cb24-27f3-4331-a683-669b5d9b37f0" $Abandoned_Queries_by_Month = "fbc9e2c1-49c9-44e7-8b6d-80d21c23f612" $No_Result_Queries_by_Day = "5e97860f-0595-4a07-b6c2-222e784dc3a8" $No_Result_Queries_by_Month = "318556b1-cabc-4fad-bbd5-c1bf8ed97ab1" $Query_Rule_Usage_by_Day = "22a16ae2-ded9-499d-934a-d2ddc00d406a" $Query_Rule_Usage_by_Month = "f1d70093-6fa0-4701-909d-c0ed502e3df8" }
else # 2016
{
$Number_of_Queries          = "df46e7fb-8ab0-4ce8-8851-6868a7d986ab"
$Top_Queries_by_Day         = "06dbb459-b6ef-46d1-9bfc-deae4b2bda2d"
$Top_Queries_by_Month       = "8cf96ee8-c905-4301-bdc4-8fdcb557a3d3"
$Abandoned_Queries_by_Day   = "5dd1c2fb-6048-440c-a60f-53b292e26cac"
$Abandoned_Queries_by_Month = "73bd0b5a-08d9-4cd8-ad5b-eb49754a8949"
$No_Result_Queries_by_Day   = "6bfd13f3-048f-474f-a155-d799848be4f1"
$No_Result_Queries_by_Month = "6ae835fa-3c64-40a7-9e90-4f24453f2dfe"
$Query_Rule_Usage_by_Day    = "8b28f21c-4bdb-44b3-adbe-01fdbe96e901"
$Query_Rule_Usage_by_Month  = "95ac3aea-0564-4a7e-a0fc-f8fdfab333f6"
} $filename = $path + (Get-Variable $searchreport).Name + " " + (Get-Date -Format "yyyy-mm-dd") + "
.xlsx" $reportid = (Get-Variable $searchreport).Value $TTNcontent = "&__EVENTTARGET=__Page&__EVENTARGUMENT=ReportId%3D" + $reportid # setup the WebRequest $webRequest = [System.Net.WebRequest]::Create($farmurl) $webRequest.UseDefaultCredentials = $true $webRequest.Accept = "image/jpeg, application/x-ms-application, image/gif, application/xaml+xml, image/pjpeg, application/x-ms-xbap, */*" $webRequest.ContentType = "application/x-www-form-urlencoded" $webRequest.Method = "POST" $encodedContent = [System.Text.Encoding]::UTF8.GetBytes($TTNcontent) $webRequest.ContentLength = $encodedContent.length $requestStream = $webRequest.GetRequestStream() $requestStream.Write($encodedContent, 0, $encodedContent.length) $requestStream.Close() # get the data [System.Net.WebResponse] $resp = $webRequest.GetResponse(); $rs = $resp.GetResponseStream(); #[System.IO.StreamReader] $sr = New-Object System.IO.StreamReader -argumentList $rs; #[byte[]]$results = $sr.ReadToEnd(); [System.IO.BinaryReader] $sr = New-Object System.IO.BinaryReader -argumentList $rs; [byte[]]$results = $sr.ReadBytes(10000000); # write the file Set-Content $filename $results -enc byte } # Note: Change the version to 2013 or 2016
Get-SPSearchReports $url "
Number_of_Queries" $path "2013" Get-SPSearchReports $url "Top_Queries_by_Day" $path "2013" Get-SPSearchReports $url "Top_Queries_by_Month" $path "2013" Get-SPSearchReports $url "Abandoned_Queries_by_Day" $path "2013" Get-SPSearchReports $url "Abandoned_Queries_by_Month" $path "2013" Get-SPSearchReports $url "No_Result_Queries_by_Day" $path "2013" Get-SPSearchReports $url "No_Result_Queries_by_Month" $path "2013" Get-SPSearchReports $url "Query_Rule_Usage_by_Day" $path "2013" Get-SPSearchReports $url "Query_Rule_Usage_by_Month" $path "2013"


The Detective Work…

I could not find anything documented on how the reports are called or details on things like the report GUIDs. So here's how I got there:

  • Go the search reports page in Central Admin and press F12 to open the Internet Explorer F12 Developer Tools then:
    • Click the Network tab and click the play button to start recording.
    • Click one of the report links.
    • Double-click the link generated for the report in the F12 pane to open up the details.
    • Make note of the URL (It's the same as the report page!)
    • Note the Accept, and Content-Type Request Headers.
    • Click the Request Body tab.
    • Stare at 3000 characters in that string until your head really hurts, or until you recognize most of what is there is the normal page postback stuff like VIEWSTATE. So we need to find what's unique in the string. (It's the Report IDs.)
    • Click on each of the nine reports and copy out the report IDs.
    • With a lot of trial and error figure out what the minimum string needed is to generate the reports. (It's ""&__EVENTTARGET=__Page&__EVENTARGUMENT=ReportId" plus the report id.)
    • Find out how to do an HTTP POST using PowerShell. (Steal most of it from here: http://www.codeproject.com/Articles/846061/PowerShell-Http-Get-Post.)
    • Find some other needed .Net code and convert the C# to PowerShell.
    • Fill in some gaps with PowerShell putty …….


.

        1/23/2018

        SharePoint 2016 Durable Links


        I recently had a question in class about “Durable Links”. I did a search of the Microsoft sites to find anything official on SharePoint 2016 “Durable Links”, and basically only found a beta vintage blog article.
        https://blogs.technet.microsoft.com/wbaer/2015/09/22/durable-links-in-sharepoint-server-2016-it-preview/
        While I did find a number of other blog articles from the beta period, mostly of the “what’s new in SharePoint 2016” type, I found no TechNet articles. So… I thought I’d share part of one of my courses that has a section on Durable Links. This course is available from many Microsoft Learning partners, and of course, from MAX!

        From:
        Course 55198A: Microsoft SharePoint Server Content Management for SharePoint 2013 and 2016

        SharePoint 2016 Durable Links

        Prior to SharePoint 2016, renaming or moving a file would break all of the links and shortcuts that pointed to the file. In SharePoint 2013 you might have had a file named “FinancialStatementFY14Q2.xlsx” that had no spaces in the name. This is both an ugly filename and a name that will cause problems with search. (Users searching for “Statement” or “FY14” would never find it based on the title.) The 2013 URL would look something like this:
        http://yourServer/sites/yourSite/Shared%20Documents/FinancialStatementFY14Q2.xlsx
        Renaming this file to include spaces in the name would create the following URL. But, users with links to the old file will no longer be able to find it.
        http://yourServer/sites/yourSite/Shared%20Documents/Financial Statement FY14 Q2.xlsx
        Note the spaces in the URL will be replaced with “%20”.

        Durable Links
        SharePoint 2016 now appends a “d” query string to the URL with a unique ID that will not change even if the file has been renamed. (But not always… see notes below…)
        http://yourServer/sites/yourSite/Shared%20Documents/FinancialStatementFY14Q2.xlsx?d=w780e689061e44dbfb4123fe450f4b957
        After renaming, SharePoint will still find the correct document as it looks for the Durable Link ID first to find the document.
        http://yourServer/sites/yourSite/Shared%20Documents/Financial%20Statement%20FY14%20Q2.xlsx?d=w780e689061e44dbfb4123fe450f4b957
        To find the Durable Link in SharePoint 2016, or the SharePoint Online “Classic UI”, click the “…” next to the filename.


        Notes:
        • Durable Links are not a feature and cannot be enabled or disabled.
        • Durable Links require Office Online Server to be part of the farm.
        • Works with Office documents like Word, Excel and PowerPoint (i.e. things displayed in Office Server), but not other files like .jpeg, .png, etc.
        • At the time of this writing, the Office 365 / SharePoint Online “Modern Library” pages do not offer a way to copy the URL that includes the Durable Link query string.
        • Documents that are moved (drag and drop or cut/paste) will preserve the Durable Link ID. Documents that are copied and pasted will get a new Durable Link ID.
        • The Publishing site Content and Structure feature does not preserve the SharePoint 2016 Durable Link. (A new ID is assigned after a Move.)


          .










        1/22/2018

        SharePoint Search Weirdness – Part 5: Search REST API Ignores Duplicates


        A continuation of the "Search Weirdness" series!


        If you are a developer, or a SharePoint 2013 workflow designer, then you probably have used the SharePoint Search REST API. Did you know that you are probably not getting all of the results expected?

        Here’s a typical REST search for the word “sharepoint”:

        http://yourSiteUrl/_api/search/query?querytext='sharepoint'

        Or if you would like to be a little more selective:

        http://yourSiteUrl/_api/search/query?querytext='sharepoint'&selectproperties='Title,Author'&refinementfilters='fileExtension:equals("docx")'

        or you would like to return more than the default number of items:

        http://yourSiteUrl/_api/search/query?querytext='sharepoint'&selectproperties='Title,Author'&refinementfilters='fileExtension:equals("docx")'&rowlimit=1000


        The problem with the above searches is that Search thinks some of your results are duplicates, so it removed them! To solve this problem just add this to your URL:

            &trimduplicates=true

        Your search URLs then might look like these:

        http://yourSiteUrl/_api/search/query?querytext='sharepoint'&trimduplicates=true

        http://yourSiteUrl/_api/search/query?querytext='sharepoint'&selectproperties='Title,Author'&refinementfilters='fileExtension:equals("docx")'&trimduplicates=true

        http://yourSiteUrl/_api/search/query?querytext='sharepoint'&selectproperties='Title,Author'&refinementfilters='fileExtension:equals("docx")'&rowlimit=1000&trimduplicates=true


        .

        1/15/2018

        Adding HTML to SharePoint Columns – Color, Images and More – Round 2!


        Back in June Microsoft announced they were blocking HTML created by Calculated Columns with the June 2017 Public Update (PU) for SharePoint 2013, 2016 and SharePoint Online.

        See here: http://techtrainingnotes.blogspot.com/2017/12/no-more-html-in-sharepoint-calculated.html

        Before the June update:

        image

        After the June update:

        image


        You can turn this new “feature” off using PowerShell… but only for on-prem.

        https://support.microsoft.com/en-us/help/4032106/handling-html-markup-in-sharepoint-calculated-fields

        $wa = Get-SPWebApplication http://yourWebAppUrl
        $wa.CustomMarkupInCalculatedFieldDisabled = $false
        $wa.Update()

        Repeat for each web application as needed.


        There’s a workaround!

        There's a fairly simple solution that works in all versions, if you don't mind using a workflow.

        1. Edit the Calculated Column with the HTML and change it's "The data type returned from this formula is" back to "Single Line of Text". (Just change the result type... leave the column as a Calculated Column.)
        2. Create a new Multiple Lines of Text column and set it to "Enhanced rich text (Rich text with pictures, tables, and hyperlinks)".
        3. Create a workflow that simply copies the Calculated Column to the new Multiple Lines of Text column. Set the workflow to run on Created and Changed.
        4. Edit your views to hide the Calculated Column and add the Multiple Lines of Text column.

        The workflow is just a single Set Field in Current Item action.

        imageimage

        Set the “field” to the new Multiple Lines of Text column and set “value” to the Calculated column. Publish and test!

        This solution will let you keep the Calculated Column for easy revising of the formula logic. You could also let the workflow do all of the work to create the logic and HTML using a String Builder, and eliminate the need for the Calculated Column.


        Update the Existing Items

        You now need to get the workflow to run on all of the existing items. You can run a PowerShell script to start the workflows, you can run a PowerShell script just to copy the data from the Calculated column the new column, you can manually run the workflows on each item, or if you don't mind the Modified date and Modified By being changed switch to the Quick Edit view and copy all the items in one column and then paste them right back.


        After the workaround:

        image

        What does not work?

        Script tags and Style tags. (and I’m sure there are a few more) Style blocks are emptied and script blocks are completely removed. But, basic HTML for hyperlinks, image tag, etc. still work.

        Before: <style>#test { color:red }</style><script>alert(1)</script> more HTML…

        After: <style></style> more HTML…

        .

        1/11/2018

        SharePoint 2013 Workflow Error: Invalid Text Value


        I got the not so obvious error below from a simple SharePoint 2013 style workflow.

        Invalid text value.
        A text field contains invalid data. Please check the value and try again.

        The problem was actually pretty simple. I was trying to write more the 255 characters to the Workflow Log. You will get the same error writing more than 255 characters to a Single Line of Text column. You can use an Extract Substring from Start of String action to retrieve only the first 255 characters.


        image

        1/01/2018

        PowerShell to Bulk Add Lists and Libraries to SharePoint’s Quick Launch


        Someone created a bunch of lists and libraries in a new site, and then a few days later wondered why they were no longer in Quick Launch. They had seen them there the day before in the “Recent” section.

        I told them the steps to edit the properties of each list to add them to Quick Launch, and then they told me that there are over 30 lists. So… PowerShell to the rescue!  Here’s the on-prem version:

        $site = Get-SPSite http://sp2016/sites/calcdemo

        $web = $site.RootWeb     #or which ever web is needed

        $lists = $web.Lists

        $lists | where {-not $_.Hidden -and $_.Created -gt (Get-Date 12/21/2017)} |
                   foreach { $_.OnQuickLaunch = $true; $_.Update() }

         

        All done!

        I filtered by date so that I would not change any lists that existed before their new work, and filtered by Hidden to exclude the SharePoint auto-generated lists.

        12/23/2017

        No More HTML in SharePoint Calculated Columns!

        Update: Here's a workaround using a workflow: http://techtrainingnotes.blogspot.com/2018/01/adding-html-to-sharepoint-columns-color.html


        Just in case you missed it:
        “Some users have added HTML markup or script elements to calculated fields. This is an undocumented use of the feature, and we will block the execution of custom markup in calculated fields in SharePoint Online from June 13, 2017 onwards. The June 2017 Public Update (PU) and later PUs will make blocking a configurable option for on-premises use in SharePoint Server 2016 and SharePoint Server 2013.”
        https://support.microsoft.com/en-us/help/4032106/handling-html-markup-in-sharepoint-calculated-fields

        So, no more of this:
           image
        Or this:
            =REPT("<img src='http://yourPath/yourImage.GIF' style='border-style:none'/>",[Value])
          image

        12/17/2017

        Topic Mapping for Course 20347 to Microsoft Certification Exams 70-346 and 70-347


        Microsoft’s “20347A: Enabling and Managing Office 365” course is a little different in that it does not have a one-to-one mapping to a single Microsoft exam. For example, while course 20483 maps to exam 70-483, 20347 maps to two exams: 70-346 and 70-347.

        Here’s a guide for my students who are working on the 70-346 and 70-347 exams…


        The course:

        The exams:

        • 70-346 - Managing Office 365 Identities and Requirements
        • 70-347 - Enabling Office 365 Services


        In the content below note that:

        • Some 20347 modules are not on the exam:
          • Module 1 (overview and create a Office 365 tenant)
          • Module 3 (Microsoft Office)
        • Two exam 70-347 topics () are not in 20347!
          • Implement Microsoft Flow and PowerApps
          • Configure and manage Microsoft StaffHub




        For details of each topic see:

        • 70-346 - Managing Office 365 Identities and Requirements
        • 70-347 - Enabling Office 365 Services


          Exam 70-346


          Manage cloud identities (15–20%)

          · 20347 Module 2 – Configure password management

          · 20347 Module 2 – Manage user and security groups

          · 20347 Module 2 – Manage cloud identities with Windows PowerShell


          Implement and manage identities by using Azure AD Connect (15–20%)

          · 20347 Module 4 – Prepare on-premises Active Directory for Azure AD Connect

          · 20347 Module 4 – Set up Azure AD Connect tool

          · 20347 Module 4 – Manage Active Directory users and groups with Azure AD Connect in place


          Implement and manage federated identities for single sign-on (SSO) (15–20%)

          · 20347 Module 13 – Plan requirements for Active Directory Federation Services (AD FS)

          · 20347 Module 13 – Install and manage AD FS servers

          · 20347 Module 13 – Install and manage WAP servers (Web Application Proxy)


          Monitor and troubleshoot Office 365 availability and usage (15–20%)

          · 20347 Module 12 – Analyze reports

          · 20347 Module 12 – Monitor service health

          · 20347 Module 12 – Isolate service interruption



          Exam 70-347

          Manage clients and end-user devices (20–25%)

          · 20347 Module 5 – Manage user-driven client deployments

          · 20347 Module 5 – Manage IT deployments of Office 365 ProPlus

          · 20347 Module 5 – Set up telemetry and reporting

          · 20347 Module 5 – Plan for Office clients


          Provision SharePoint Online site collections (20–25%)

          · 20347 Module 9 – Configure external user sharing

          · 20347 Module 9 – Create SharePoint site collection

          · 20347 Module 9 – Plan a collaboration solution


          Configure Exchange Online and Skype for Business Online for end users (20–25%)

          · 20347 Module 6 – Configure additional email addresses for users

          · 20347 Module 6 – Create and manage external contacts, resources, and groups

          · 20347 Module 11 – Configure personal archive policies

          · 20347 Module 8 – Configure Skype for Business Online end-user communication settings


          Plan for Exchange Online and Skype for Business Online (20–25%)

          · 20347 Module 7 – Manage antimalware and anti-spam policies

          · 20347 Module 7 – Recommend a mailbox migration strategy

          · 20347 Module 11 – Plan for Exchange Online

          · 20347 Module 7 – Manage Skype for Business


          Configure and Secure Office 365 services (20–25%)

          · 20347 Module 10 – Implement Microsoft Teams

          · 20347 Module 10 – Configure and manage OneDrive for Business

          · (not covered in 20347!) – Implement Microsoft Flow and PowerApps

          · (not covered in 20347!) – Configure and manage Microsoft StaffHub

          · 20347 Module 11 – Configure security and governance for Office 365 services


          .

          11/26/2017

          Working with PowerShell’s -ExpandProperty (or… messing with the pipeline)


          One of the great things about PowerShell is how easy and clear much of it is. But then there’s the other things that seem like magic. One of those is when you are dealing with objects that have properties that are collections of other objects.

          Here will look at three solutions for dealing with properties that are collections of other objects:

          • Using -ExpandProperty (and a custom column or two)
          • Using ForEach (and a couple variables)
          • Using ForEach (and a customized object)


          Get-DependentServices

          Sorry, there’s not such a cmdlet. Get-Service does return service objects that have a dependent services property. The only problem is that it returns a summary of the dependent services, and only a partial list. While you could pipe this to Format-List -Wrap, that will not give you any details about the dependent services.

          image


          Using -ExpandProperty

          You can use -ExpandProperty to expand any single property.

          image

          While the above works nicely for a single service, it creates a mess for a collection of services. Which DependentService belongs to which service?

          image

          Your first attempt to solve this will probably fail. In this case, it fails as both the Service object has a Name property and the DependentServices objects have a Name property.

          image

          Your second attempt may run without error, but we still can’t see which Service belongs with which DependentService. Adding a custom column seems to be ignored here:

          image

          This didn’t work as expected because a Service object has a default list of Select properties defined in an XML file somewhere. ( :-) )  Your custom column was passed through the pipeline, but as a NoteProperty (a PowerShell added custom property).

          image

          You will need to pipe the last attempt to another Select to see your NoteProperty.

          image

          So the steps are:

          • Create a custom column with the data to pass on down the pipeline.
               @{n="ParentServiceName";e={$_.Name}}
          • Expand the property with the collection of child objects.
               -ExpandProperty DependentServices
          • Pipe the child object and its new NoteProperty to a final Select.
               select ParentServiceName, Status, Name


          Using ForEach with Variables

          ExpandProperty works great when you need to expand a single property. When you need to futher manipulate the data before sending it on down the pipeline you may want to use ForEach, formally ForEach-Object.

              Get-Service | ForEach { some code } | Select ….

          As a simple ForEach, let’s list Services in color based on Status.

             Get-Service | foreach { 
                if ($_.Status -eq "Running") 
                   { Write-Host $_.Name -ForegroundColor Green } else
                   { Write-Host $_.Name -ForegroundColor Red }
             }

          image

          Write-Host does not add its content to the pipeline, so that’s the end of the line for this one.

          Using ForEach to solve our Service and DependentServices problem, we will create and then reuse a variable that contains the name of the service. (I call this “Store and Forward”.) The ForEach code block can contain one or many statements seperated by semicolons. The last object returned in the code block is forwarded into the pipeline.

             Get-Service | ForEach { $parent = $_.Name; $_.DependentServices } |
                                   Select {$parent}, Status, Name

          This collects one or more pieces of data from the parent object (the Service) as variables, and then passes the DependentServices object on through the pipeline. On the other side of the pipeline you can continute expanding objects and using the variables as needed. Here’s the Service and DependentServices solution using “Store and Forward”.

          image


          Using ForEach with Custom Objects

          You can customize objects by adding “NoteProperties” to the object. You saw one form of this when we used the -ExpandProperty while listing other properties.

             image


          As a fun example, lets create a custom object that looks like a Service object, but has two properties named RunningDependentProperties and NonRunningDependentProperties.

          We will need:

          • A variable for the parent Service
          • Two variables to store the child services (Empty arrays created as $r=@().)
          • The Add-Member cmdlet to add a new NoteProperty to the Service object

          Get-Service |
               foreach {
                 # initialize the arrays
                 $r = @();
                 $nr = @();
                 # process all of the DependentServices
                 ($_.DependentServices |
                    foreach { if ($_.status -eq "Running") { $r += $_ } else {$nr += $_ } }
                 );  `
                 # attach the results to the original service
                 Add-Member -InputObject $_ -name RunningDependentServices -Value $r -MemberType NoteProperty;
                 Add-Member -InputObject $_ -name NonRunningDependentServices -Value $nr -MemberType NoteProperty;
                 $_
               } |
          Select name, RunningDependentServices, NonRunningDependentServices


          So what’s it good for?

          How about a list of only services with running dependent services? Just add a WHERE before the SELECT:

              where { $_.RunningDependentServices.Count -gt 0 }

             image

          Or, how about a list of all of the services where the DependentService “applockerfltr” is not running? Just add a WHERE before the SELECT:

             where { "applockerfltr" -in $_.NonRunningDependentServices.Name }


          (If you are not reading this at http://TechTrainingNotes.blogspot.com… you are reading stolen content!)


          Tip:

          In addition to the expansion of properties that are collections, -ExpandProperty can also be used to return a simple datatype like a string or a number. By default, a Select returns a collection. In the example below note that a basic Select Status returns a property named “Status” that contains the value “Running”. If you pipe that to Get-Member you will see that it is still a ServiceController object. If you expand the property Status you will then get the simple string value of the property.

          image


          .

          11/24/2017

          SharePoint: Running JavaScript Code Only When in Page Edit Mode


          One of my old Content Editor Web Part “tricks” would not work when run in a Publishing page. I was hiding a web part when not in Page Edit mode. You can detect this mode by checking for ‘PageState.ViewModeIsEdit != "1"’. The PageState object is automatically created in normal site pages (“Wiki Pages”). It is also created in Publishing Pages, but only when the page has not been published. I.e. when the page is checked out, or in edit mode.

          My code originally looked like this:

            if ( PageState.ViewModeIsEdit != "1" )
            {  …code to run when not in edit mode… }


          As the PageState object does not exist in a Published page, I had to change it to this:

            if ( typeof PageState == 'undefined' || (PageState.ViewModeIsEdit != "1") )
            {  …code to run when not in edit mode… }


          Of course… none of the above works in SharePoint Online “Modern UI” pages!


          .

          Note to spammers!

          Spammers, don't waste your time... all posts are moderated. If your comment includes unrelated links, is advertising, or just pure spam, it will never be seen.