SharePoint – Create a JSON Field Customizer Extension

INTRODUCTION

SharePoint Field Customizers are client-side components that run inside the context of a SharePoint page. 

Currently, a React project is available out-of-the-box with the second option of “No JavaScript framework” for those ambitious enough to add their own preferred UI libraries.

SETUP YOUR DEVELOPMENT ENVIRONMENT

This article is not going to go into the details on how to set up your development environment. A good article can be found here.

My only recommendation is to use NVM instead of the default installation of NodeJS.

NVM allows you to install multiple versions of NodeJS and handles the heavy-lifting in swapping between them.

CREATE A FIELD CUSTOMIZER EXTENSION

As part of the article, I will demonstrate how to create a Field Customizer Extension that will display human-readable file size in Bytes, Kb, Mb, Gb, Tb, Pb, Eb, Zb, Yb from a number value store in a Sharepoint number column. In my project, I use a React component called file-bytes-formatter, but once you get the hang of it, you will be able to extend these to most React components.

Follow these steps to create the Field Customizer Extension template project. These steps have been adapted from the official Build your first Field Customizer extension article.

1) Create a new project directory in a convenient location

md Bytes-Field-Extension

2) Go to the project directory

cd Bytes-Field-Extension

3) Run the Yeoman SharePoint Generator 

yo @microsoft/sharepoint

4) When prompted, enter the following values (select the default option for all prompts omitted below): 

  • What is your solution name?: bytes-field-extension
  • Will the components in the solution require permissions to access web APIs that are unique and not shared with other components in the tenant?: (y/n) n
  • Which type of client-side component to create?: Extension
  • Which type of client-side extension to create?: Field Customizer
  • What is your Field Customizer name?: Bytes Field Extension
  • What is your Field Customizer description?: This Field Customizer Extension will return human-readable file size in Bytes, Kb, Mb, Gb, Tb, Pb, Eb, Zb, Yb
  • Which framework would you like to use?: React

At this point, Yeoman installs the required dependencies and scaffolds the solution files along with the HelloWorld extension. This might take a few minutes. When prompted, enter the following values (select the default option for all prompts omitted below):

5) Install file-bytes-formatter React component

npm install file-bytes-formatter 

6) Type the following into the console to start Visual Studio Code or open project in your favourite editor

code .

7) Within src\extensions\bytesFieldExtension\BytesFieldExtensionFieldCustomizer.ts add the following line under the last import command.

import Fsf from "file-bytes-formatter"

This will make the file-bytes-formatter component available in your project.

8) Within src\extensions\bytesFieldExtension\BytesFieldExtensionFieldCustomizer.ts change this line

const text: string = `${this.properties.sampleText}: ${event.fieldValue}`;

to

const text: string = `${Fsf(+(event.fieldValue).split(',').join(''))}`;

This will parse the text from the SharePoint column through the file-bytes-formatter component. The spit function is used to remove any commas that SharePoint might have added to style the number.

9) Make a note of the ClientSideComponentId in sharepoint\assets\elements.xml. You will need this later.

10) Save project and close project

PACKAGING FIELD CUSTOMIZER EXTENSION

1) In the console window, enter the following command to package your client-side solution that contains the extension so that we get the basic structure ready for packaging: 

gulp bundle --ship

2) Execute the following command so that the solution package is created: 

gulp package-solution --ship

The command creates the package: ./sharepoint/solution/bytes-field-extension.sppkg

DEPLOYING FIELD CUSTOMIZER EXTENSION

After deploying the bytes-field-extension.sppkg into your environment (see Deploy your client-side web part to a SharePoint page if you are unfamiliar with this process) you need to configure the field that you want this formatter to take effect on.

1) Using PnP PowerShell connect to your site

Connect-PnPOnline -Url "https://YOURTENANT.sharepoint.com/sites/YOURSITE" -UseWebLogin

2) Get a reference to the field object you want to configure to use this Field Customizer Extension

$field = Get-PnPField -List "YOURLIST" -Identity "YOURFIELDNAME"

3) Set the GUID from CREATE A FIELD CUSTOMIZER EXTENSION step 9 

$field.ClientSideComponentId = "YOURGUID"

4) Mark the field as updates

field.Update()

5) Execute the pending update

Invoke-PnPQuery

TEST FIELD CUSTOMIZER EXTENSION

If all sets are followed correctly any number column that has this Field Customizer Extension will take input like this.

and change it to a nice human-readable format like below

CONCLUSION

I hope you found this tutorial useful. You are encouraged to ask questions, report any bugs or make any other comments about it below. 

Tool to identify commercial MP3s

Introduction

Media files have become an important part of business content. Seeing that in some environments we cannot explicitly block MP3s, how do we identify what files are business recordings, and which are (possibly illegal) music?

IsMP3Song.exe is my attempt to address this issue. 

The idea is very simple, pass the filename to the tool and it returns a score indicating the confidence of it deeming the file as a commercial MP3.

The tool achieves this by doing a web-service call to iTunes and then scoring the iTunes results with the filename of the MP3.

Demo Execution

As you can see, the first couple of executions return a score because these are identified as commercial songs and the last one is scored as not commercial.

These scores are also returned by the application as an exit code, allowing return values to be used for other logic, such as email notifications or file classifications.

USING WITH POWERSHELL

Going a step further, the PowerShell snippet below can be used as a starting point to loop through folders and score all the MP3 files found.

$inputDir = "E:\SomeDataFolder";
$filterExt = "*.mp3";

$files = Get-ChildItem -Path $inputDir -Recurse -Filter $filterExt;
foreach ($file in $files)
{
$process = start-process .\IsMP3Song.exe -windowstyle Hidden -ArgumentList """$($file.Name)""" -PassThru -Wait
"$($file.Name) $($process.ExitCode)"
}

The C# code

using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;

namespace IsMP3Song
{
public class Song
{
public string ArtistName;
public string TrackName;
public int Match;
}
class Program
{
static void Main(string[] args)
{
List<Song> songs = new List<Song>();

try
{
if (args.Length == 1)
{
string searchTerm = FormatSearchTerm(args[0]);
string jsonReturn = GetRequest($"https://itunes.apple.com/search?term={searchTerm}");
dynamic jsonCollection = JObject.Parse(jsonReturn);
foreach (var result in jsonCollection.results)
{
dynamic jsonObject = JObject.Parse(result.ToString());
double match = ((CalculateSimilarity(args[0], $"{jsonObject.artistName} - {jsonObject.trackName}")) * 100);

Song song = new Song();
song.ArtistName = jsonObject.artistName;
song.TrackName = jsonObject.trackName;
song.Match = (int)match;
songs.Add(song);
}

if (songs.Count > 0)
{
Song bestMatch = songs.OrderByDescending(s => s.Match).First();
Console.WriteLine($"{bestMatch.ArtistName} - {bestMatch.TrackName} [{bestMatch.Match}]");
Environment.Exit(bestMatch.Match);
}
else
{
Console.WriteLine($"Seems that is not a commercial MP3...");
Environment.Exit(0);
}

}
}
catch
{

}
}
// Returns JSON string
static string GetRequest(string url)
{
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
try
{
WebResponse response = request.GetResponse();
using (Stream responseStream = response.GetResponseStream())
{
StreamReader reader = new StreamReader(responseStream, System.Text.Encoding.UTF8);
return reader.ReadToEnd();
}
}
catch (WebException ex)
{
WebResponse errorResponse = ex.Response;
using (Stream responseStream = errorResponse.GetResponseStream())
{
StreamReader reader = new StreamReader(responseStream, System.Text.Encoding.GetEncoding("utf-8"));
String errorText = reader.ReadToEnd();
// log errorText
}
throw;
}
}
public static double CalculateSimilarity(string source, string target)
{
if ((source == null) || (target == null)) return 0.0;
if ((source.Length == 0) || (target.Length == 0)) return 0.0;
if (source == target) return 1.0;

int stepsToSame = ComputeLevenshteinDistance(source, target);
return (1.0 - ((double)stepsToSame / (double)Math.Max(source.Length, target.Length)));
}
public static int ComputeLevenshteinDistance(string source, string target)
{
if ((source == null) || (target == null)) return 0;
if ((source.Length == 0) || (target.Length == 0)) return 0;
if (source == target) return source.Length;

int sourceWordCount = source.Length;
int targetWordCount = target.Length;

// Step 1
if (sourceWordCount == 0)
return targetWordCount;

if (targetWordCount == 0)
return sourceWordCount;

int[,] distance = new int[sourceWordCount + 1, targetWordCount + 1];

// Step 2
for (int i = 0; i <= sourceWordCount; distance[i, 0] = i++) ;
for (int j = 0; j <= targetWordCount; distance[0, j] = j++) ;

for (int i = 1; i <= sourceWordCount; i++)
{
for (int j = 1; j <= targetWordCount; j++)
{
// Step 3
int cost = (target[j - 1] == source[i - 1]) ? 0 : 1;

// Step 4
distance[i, j] = Math.Min(Math.Min(distance[i - 1, j] + 1, distance[i, j - 1] + 1), distance[i - 1, j - 1] + cost);
}
}

return distance[sourceWordCount, targetWordCount];
}
public static string FormatSearchTerm(string fileName)
{
//Get only filename from full path
fileName = fileName.Split('\\')[fileName.Split('\\').Length - 1];
string searchTerm = fileName;

//Remove MP3 extension if present
searchTerm = Regex.Replace(searchTerm, ".mp3", "", RegexOptions.IgnoreCase).Trim();

//Remove all non-alpha characters
Regex regEx = new Regex("[^a-zA-Z ]");
searchTerm = regEx.Replace(searchTerm, "");

//Remove duplicate spaces
RegexOptions options = RegexOptions.None;
Regex regex = new Regex("[ ]{2,}", options);
searchTerm = regex.Replace(searchTerm, " ");

return searchTerm.Trim().Replace(" ", "+");
}
}
}

Download

Visual Studio project download

https://bitbucket.org/svermaak/ismp3song/downloads/

Binary download

https://blog.ittelligence.com/wp-content/uploads/2018/03/IsMP3Song.zip

 

CircularLogArchiver v2.0

Usage : CircularLogArchiver.exe GO [/LP:LogPath] [/LE:LogExtention] [/AI:ArchiveInterval] [/AOT:ArchiveOlderThan] [/DAOT:DontArchiveOlderThan] [/DOT:DeleteOlderThan]
Example : CircularLogArchiver.exe GO /LP:”C:\Logs” /LE:”*.log” /AI:”M” /AOT:”1″ /DAOT:”3″ /DOT:”3″
Example above will archive all files in “C:\Logs” with *.log extention that are between one and three months old and delete files older than three months

Attachment(s):
[list-attachments]

Before

clbefore

After

clafter

Clear all Active Directory users’ manager attribute

Clear all Active Directory users’ manager attribute. See POC video http://screencast-o-matic.com/watch/cDeUQw1uBB

[sourcecode language=”css”]
Option Explicit

Dim adoCommand
Dim adoConnection
Dim objRootDSE
Dim strDNSDomain
Dim strBase
Dim strFilter
Dim strAttributes
Dim strQuery
Dim adoRecordset
Dim strDN
Dim objUser

Const ADS_PROPERTY_CLEAR = 1

‘ Setup ADO objects.
Set adoCommand = CreateObject("ADODB.Command")
Set adoConnection = CreateObject("ADODB.Connection")
adoConnection.Provider = "ADsDSOObject"
adoConnection.Open "Active Directory Provider"
Set adoCommand.ActiveConnection = adoConnection

‘ Search entire Active Directory domain.
Set objRootDSE = GetObject("LDAP://RootDSE")
strDNSDomain = objRootDSE.Get("defaultNamingContext")
strBase = "<LDAP://" & strDNSDomain & ">"

‘ Filter for users
strFilter = "(&(objectCategory=person)(objectClass=user))"

‘ Comma delimited list of attribute values to retrieve.
strAttributes = "distinguishedName"

‘ Construct the LDAP syntax query.
strQuery = strBase & ";" & strFilter & ";" & strAttributes & ";subtree"
adoCommand.CommandText = strQuery
adoCommand.Properties("Page Size") = 100
adoCommand.Properties("Timeout") = 30
adoCommand.Properties("Cache Results") = False

‘ Run the query.
Set adoRecordset = adoCommand.Execute

‘ Enumerate the resulting recordset.
Do Until adoRecordset.EOF

‘ Retrieve values.
strDN = adoRecordset.Fields("distinguishedName").Value
‘ Bind to the user object.
Set objUser = GetObject("LDAP://" & strDN)

‘ Clear the manager attribute.
objUser.PutEx ADS_PROPERTY_CLEAR, "manager", 0

‘ Save change to AD.
objUser.SetInfo
‘ Move to the next record in the recordset.
adoRecordset.MoveNext
Loop

‘ Clean up.
adoRecordset.Close
adoConnection.Close
[/sourcecode]