Showing posts with label SharePoint Framework (SPFx). Show all posts
Showing posts with label SharePoint Framework (SPFx). Show all posts

Thursday, December 12, 2019

How to resolve Nintex workflow email action 'The specified string is not in the form required for a subject.' error


We have a running Nintex workflow in production failed for few email actions with the error message “The specified string is not in the form required for a subject.”


 After debugging this, it seems like there is an limitation for O365 outlook that subject line cannot be folded across multiple lines. It does not seem to have the character limitation for the subject. However, no matter how long of a subject you submit, exchange will ultimately truncate the unencoded version of that subject down to 255 characters and append "...".

So in order to resolve the error listed above, we have implemented in two different palaces for our SharePoint framework solution as UI with Nintex workflow.

1. Inside SharePoint framework solution, trim and leading and trailing spaces and returns. Then also replace inside returns with space.


    let trimedTitlestring = title;
    if(title !=null){
//This javascript trim leading and trailing spaces and returns
      trimedTitle = title.trim(); 

      if (trimedTitle != null){
//This javascript replaces all 3 types of line breaks with a space 
        trimedTitle = trimedTitle.replace(/(\r\n|\n|\r)/gm," "); 
    }

//The trimedTitle will be the good subject that could be used in exchange email


2. In Nintex workflow, add a “Trim String” activity on the subject before send email.


This will ultimately resolve the email action error 'The specified string is not in the form required for a subject.'.

Friday, November 8, 2019

Easy to detect device from SharePoint SPFX React application

We have a SharePoint framework application need to behavior differently when invoked from mobile device. Here is the quick way to implement this.

1. Install react-device-detect package
npm install react-device-detect --save
2. Import the package
import {
  BrowserView,
  MobileView,
  isBrowser,
  isMobile
} from "react-device-detect";
3. Use it inside your application

  if(!isSubmitDisabled && !recordIsReadOnly && isMobile){

      submitMenuProps = {
        items: [
          {
            key: 'saveOnly',
            name:'Save only',
            secondaryText: 'Do not submit to approval workflow',
            text: 'Save as draft',
            title: 'Save without submitting to a workflow',
            iconProps: { iconName: 'Save'},
            onClick: this.props.onSaveOnly
          },                       
          {
            key: 'Submit',
            name:'Submit',
            secondaryText: 'Submit to approval workflow',
            text: 'Submit from Mobile',
            title: 'Submitting to a workflow',
            iconProps: { iconName: 'SaveToMobile'},
            onClick: this.props.onSubmit
          }                     
        ]
      };
    }

Thursday, June 13, 2019

Procedure to use new custom properties to SPFx ListView Command project

If you need to add a new custom property like "sampleTextThree" with value "This is new text." to be used in SPFX extension ListView Command project. You should following the following steps.
1. Add sampleTextThree as string in the interface
sampleTextOne: string;
sampleTextTwo: string;
sampleTextThree: string;

2. Add property to serve.json like below.

"default": {
"pageUrl": "https://mysompont.sharepoint.com/sites/HarryTestX/Shared%20Documents/Forms/AllItems.aspx",
"customActions": {
"4de6483f-cccf-465d-b0ee-4aa6ebb0eb81": {
"location": "ClientSideExtension.ListViewCommandSet.CommandBar",
"properties": {
"sampleTextOne": "One item is selected in the list",
"sampleTextTwo": "This command is always visible.",
"sampleTextThree": "This is new text."
...

3. Change the default alert to use new property.

switch (event.itemId) {
case 'COMMAND_3':
Dialog.alert(${this.properties.sampleTextThree});
...

4. Gulp serve run locally is fine. However, after this deployed to tenant, the property cannot be found and is "null" as indicated in here.

You should add another step#5.
5. Add the new custom property to "elements.xml" like below.
 ClientSideComponentProperties="{"sampleTextOne":"One item is selected in the list.", "sampleTextTwo":"This command is always visible."**, "sampleTextThree":"This command is new text."**}">

From my understanding, the serve.json is used by run locally "gulp serve" and the elements.xml is used by SharePoint online tenant. At this point, the new property can be picked up locally or deployed to SPO tenant.
I'm hoping the SPFx can enhance the build process to incorporate the configuration from serve.json and build to element.xml.

One trick to resolve SPFx extension ListView Commands loading manifests script error

As a SharePoint framework (SPFx) developer, three is continues challenge on daily development. There is a common error after run the SPFx extension ListView Commands project locally. The issue is after you have run "gulp serve" once against one site collection and then stopped the local gulp serve, there is always a error message when you browse the site. Here is the error message and the screenshot.

"Error loading debug script. Ensure the server is running and the “debugManifestsFile” parameter URL is correct."

Error: Script error for "https://localhost:4321/temp/manifests.js" 
https://requirejs.org/docs/errors.html#scripterror


Even you have the solution deployed and the SPFx command added to the site, after this message, the commands will NOT appear on the list or library.

After frustration debugging this issue, here is the procedure to resolve this issue.

1. Logout from SharePoint online.
2. Clear browse cache.
3. Login to SharePoint again

You might need to do this few time until cache is cleaned. You could also open another private browser to avoid this issue.



Wednesday, June 12, 2019

How to add office fabric icon to SharePoint framework extension ListView Commands

When you use SharePoint framework extension ListView Commands, you will notice that the icon is configured to point to the local file like the configuration below.

  "items": {
    "COMMAND_1": {
      "title": { "default": "Eln Init" },
      "iconImageUrl": "icons/cancel.png",
      "type": "command"
    }
  }

Unless you download and place the icon to some where, there is no OoB easy way to point to office fabric icon. There is a user voice you should vote so this can be added in the future. At meantime, here is the way to use as base64-encoded image described here as the workaround.

Here is the detailed procedure to convert Office fabric UI icons to base64-encoded image so you could use in the SPFx extension ListView Commands.

1. Search the Office fabric UI icons you need here and copy the icon name like "Archive".

2. Use Josh's tool online to convert to base64-encoded image. You could change the color and some other parameters. After clicking the "Render Front Icon", you could copy the "Data URL" as the base64-encoded image.



3. You could "Color picker" online to identify the color you need as the screenshot below.


4. You could paste the copied to serve.json.

  "items": {
    "COMMAND_1": {
      "title": { "default": "Eln Archive" },
      "iconImageUrl": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA......",
      "type": "command"
    }

  }

Now the icon will be displayed after solution deployed.


Of cause you could also download the icon and placed to CDN or local SharePoint site. Hope the future enhancement will allow us to direct point to office fabric icon.

Thursday, April 25, 2019

Cannot display selected office fabric ui dropdown value from SharePoint framework

We are using SharePoint framework with office fabric UI component. The current project is to use SPFx to display an list item that has a Dropdown field. The issue is we are getting the value all options as an array from the SharePoint but cannot display the option value already set in the list.

After few days debugging the issue, we have isolated to the SharePoint framework REACT how to pass the pros from parent to child component. Here is the simple code to reproduce this issue.

1. Hard coded the array like this below in parent component named LoopUp.

private defaultOptionList: IDropdownOption[] = [
{
key: '0',
text: 'None',
isSelected: false
},
{
key: '1',
text: 'Encore/Review Publication',
isSelected: true
},
{
key: '2',
text: 'Non-Scientific',
isSelected: false
},
{
key: '3',
text: 'Assay Method',
isSelected: false
},
{
key: '4',
text: 'Research Data',
isSelected: false
},
{
key: '5',
text: 'PK/Tox Data',
isSelected: false
},
{
key: '6',
text: 'Clinical Data',
isSelected: false
},
{
key: '7',
text: 'Clinical Study Design',
isSelected: false
},
{
key: '8',
text: 'Clintrials.gov Posting',
isSelected: false
}

];

2. If we initialize the state in the constructor below and pass to child, child can display the selected option value.  The state we set up is named submissionContentListState2.

constructor(props) {

super(props);
this.state = {
submissionContentListState2: [] = this.defaultOptionList, // Set in constructor
submissionContentListState4: [] // Set in componentDidMount
};

}

3. If we set the same value from  componentDidMount  like below, child cannot display the selected value. The state we set up is named submissionContentListState4.

public componentDidMount() {
this.setState({
submissionContentListState4: this.defaultOptionList
});
}           

The code to pass to child is listed below.
public render(): React.ReactElement<ILoopUpProps> {

SPFacade.logOptions(this.state.submissionContentListState2, "Parent Rendor submissionContentListState2");
SPFacade.logOptions(this.state.submissionContentListState4, "Parent Rendor submissionContentListState4");
return (
<Child
defaultOptionListParent2={this.state.submissionContentListState2} // This is in constructor
defaultOptionListParent4={this.state.submissionContentListState4} // This is in componentDidMount
/>

);
}


The code in child is listed below.
import * as React from 'react';
import styles from './LoopUp.module.scss';
import { ILoopUpProps } from './ILoopUpProps';
import { Dropdown, IDropdownOption } from "office-ui-fabric-react";
import { SPFacade } from "./SPFacade";

export class Child extends React.Component<any,
{
}
> {
constructor(props) {
// Required step: always call the parent class' constructor
super(props);
}
public render(): React.ReactElement<ILoopUpProps> {

SPFacade.logOptions(this.props.submissionContentListState2, "Child Rendor submissionContentListState2");
SPFacade.logOptions(this.props.submissionContentListState4, "Child Rendor submissionContentListState4");
return (
<div className={ styles.loopUp }>
<div className={ styles.container }>

<div className={ styles.row }>
<div className={ styles.column }>
<Dropdown
placeHolder="Select a content type"
required={true}
label="Lokup values from parent props set in constructor - working"
options={this.props.defaultOptionListParent2}
/>
</div>
</div>

<div className={ styles.row }>
<div className={ styles.column }>
<Dropdown
placeHolder="Select a content type"
required={true}
label="Lokup values from parent props set in componentDidMount - not working"
options={this.props.defaultOptionListParent4}
/>
</div>
</div>

</div>
</div>
);
}

}


The result is show as below.

You can see the top DOM element did show the selected value named "Encore/Review Publications". However, the bottom DOM element could not display the selected value even all the option values are retrieved.

This is a show stopper for us at this point since we need to get the value from SharePoint list item and it should be done in componentDidMount function and cannot be in constructor.

If you know the solution or workaround, please let me know.

The complete parent code is listed below.

import * as React from 'react';
import { ILoopUpProps } from './ILoopUpProps';
import { IDropdownOption } from "office-ui-fabric-react";
import { Child } from "./Child";
import { SPFacade } from "./SPFacade";

export default class LoopUp extends React.Component<ILoopUpProps,
{
submissionContentListState2: IDropdownOption[];
submissionContentListState4: IDropdownOption[];
}
> {

private defaultOptionList: IDropdownOption[] = [
{
key: '0',
text: 'None',
isSelected: false
},
{
key: '1',
text: 'Encore/Review Publication',
isSelected: true
},
{
key: '2',
text: 'Non-Scientific',
isSelected: false
},
{
key: '3',
text: 'Assay Method',
isSelected: false
},
{
key: '4',
text: 'Research Data',
isSelected: false
},
{
key: '5',
text: 'PK/Tox Data',
isSelected: false
},
{
key: '6',
text: 'Clinical Data',
isSelected: false
},
{
key: '7',
text: 'Clinical Study Design',
isSelected: false
},
{
key: '8',
text: 'Clintrials.gov Posting',
isSelected: false
}

];

constructor(props) {

super(props);
this.state = {
submissionContentListState2: [] = this.defaultOptionList, // Set in constructor
submissionContentListState4: [] // Set in componentDidMount
};

}

// Get the itemId received form the parent and display attachment if itemId is not NaN
public componentDidMount() {
this.setState({
submissionContentListState4: this.defaultOptionList
});
}

public render(): React.ReactElement<ILoopUpProps> {

SPFacade.logOptions(this.state.submissionContentListState2, "Parent Rendor submissionContentListState2");
SPFacade.logOptions(this.state.submissionContentListState4, "Parent Rendor submissionContentListState4");
return (
<Child
defaultOptionListParent2={this.state.submissionContentListState2} // This is in constructor
defaultOptionListParent4={this.state.submissionContentListState4} // This is in componentDidMount
/>

);
}




}








Monday, April 15, 2019

Automate SPFx source code backup with gulp

As a developer working on SharePoint framework (SPFx) with Azure DevOps, it would be convenient to have a automation to back up the source code to local sometime. Here is the example to use gulp task. We would use backing up fiels for SPFx webparts or just components folder. You could modify for your own purpose.

To use it, you could run as the following commands.

gulp bakwebparts
gulp bakcom



gulp.task('bakcom', function() {
// copy all files in source/webparts/pubsWebPart/components to C:\PubsBackup\<year>-<month>-<date>-<hour>-<minute>\components
var backupRoot = "C://PubsBackup";

var d = new Date();
var m = Number(d.getMonth()) + 1;
var n = d.getFullYear() + '-' + m.toString() + '-' + d.getDate() + '-' + d.getHours() + '-' + d.getMinutes();

var pubsRoot = backupRoot + '/' + n;
var webparts = pubsRoot + '/webparts';
var pubsWebPart = webparts + '/pubsWebPart';
var component = pubsWebPart + '/components';
console.log("component" + component);
const folders = [
backupRoot,
pubsRoot,
webparts,
pubsWebPart,
component
];

folders.forEach(dir => {
if(!fs.existsSync(dir)) {
fs.mkdirSync(dir);
console.log('📁 folder created:', dir);
}
});

gulp.src('./src/webparts/pubsWebPart/components/*.*').pipe(gulp.dest(component));

});


gulp.task('bakwebparts', function() {
// copy all files in source/webparts/pubsWebPart/components to C:\PubsBackup\<year>-<month>-<date>-<hour>-<minute>\components
var backupRoot = "C://PubsBackup";

var d = new Date();
var m = Number(d.getMonth()) + 1;
var n = d.getFullYear() + '-' + m.toString() + '-' + d.getDate() + '-' + d.getHours() + '-' + d.getMinutes();

var pubsRoot = backupRoot + '/' + n;
var webparts = pubsRoot + '/webparts';
var pubsWebPart = webparts + '/pubsWebPart';
var component = pubsWebPart + '/components';
var loc = pubsWebPart + '/loc';
var services = pubsWebPart + '/services';
console.log("component" + component);
const folders = [
backupRoot,
pubsRoot,
webparts,
pubsWebPart,
component,
loc,
services
];

folders.forEach(dir => {
if(!fs.existsSync(dir)) {
fs.mkdirSync(dir);
console.log('📁 folder created:', dir);
}
});

gulp.src('./src/webparts/pubsWebPart/*.*').pipe(gulp.dest(pubsWebPart));
gulp.src('./src/webparts/pubsWebPart/components/*.*').pipe(gulp.dest(component));
gulp.src('./src/webparts/pubsWebPart/loc/*.*').pipe(gulp.dest(loc));
gulp.src('./src/webparts/pubsWebPart/services/*.*').pipe(gulp.dest(services));

});




Friday, March 22, 2019

How to debug and resolve sp pnp js Error making HttpClient request in queryable [404]

If you are using SP PNP JS in your SharePoint framework project, you might run into the issue you might getting the following error.

"Error making HttpClient request in queryable [404]"

If the site your are refer to is "https://mycompany.sharepoint.com/yoursite", you could inspect the page and the detailed error should look like one of the two possible situation.

https://mycompany.sharepoint.com/_api/web/lists/getByTitle('Pubs')?$select=ListItemEntityTypeFullName 404

https://mycompany.sharepoint.com/yoursite/SitePages/_api/web/lists/getByTitle('Pubs')?$select=ListItemEntityTypeFullName 404



This is very clear the PnP JS did not get the correct site URL. The first went to the root and the second is from the "SitePages". The root cause is the SP PnP JS does not get the correct context if the request is coming from site page or many be redirected.

There are at least two best practices to resolve this issue.
The first solution is to initialize context in
 onInit method like below.

protected onInit(): Promise<void> {

sp.setup({
spfxContext: this.context
});

return super.onInit();
}



The other way you can ensure you are calling to the correct web is to instantiate the web object directly and pass in the url as shown below. This method is slightly more flexible as you are not setting the base url for all calls, but you must do it each time.

public render(): void {

let web = new Web(this.context.pageContext.web.absoluteUrl);
web.select("Title").getAs<{ Title: string }>().then(w => {
this.domElement.innerHTML = `Web Title: ${w.Title}`;
});
}

If you need to set the web when context is not available, you could always call like this below by passing the URL.

private web: Web = new Web('https://mycompany.sharepoint.com/spfxsite/');

This should resolve the issue that SP OnP JS not be able to find the correct context issue.



Wednesday, February 27, 2019

Different ways to apply SharePoint field customizer extension to existing multiple columns


SharePoint Extensions can be deployed to SharePoint Online, and you can use modern JavaScript tools and libraries to build them. The Field Customizer Extension allows modifying the Views of the field in a list view. It can be used to override the field representation in the list. Field Customizer can be used with site columns or directly to the field inside a list. Microsoftarticle shows the detailed step to create the Field Customizer Extension but with limited instruction to apply it to existing list columns. Here are multiple ways to apply Field Customizer Extension to existing list columns.

The way to do this is to ClientSiteComponentId GUID attribute of Field Customizer for the existing field. There are few ways to implement this.

1. The first way is to use CSOM PowerShell to update ClientSiteComponentId attribute.

$siteURL = "https://mycompany.sharepoint.com/sites/Harry-PM-Testing"

$userId = "admin@mycompany.onmicrosoft.com"
$pwd = Read-Host -Prompt "Please enter your password" -AsSecureString
$creds = New-Object Microsoft.SharePoint.Client.SharePointOnlineCredentials($userId,$pwd)

$listTitle = "Order"
$existingColumnName = "SPFxExeColumn"
$ClientSideComponentId = "acd4d36c-a4ad-123c-a123-93862bd52123"

$ctx = New-Object Microsoft.SharePoint.Client.ClientContext($siteUrl)
$ctx.Credentials = $creds 

try{
    $lists = $ctx.web.Lists
    $list = $lists.GetByTitle($listTitle)
    $listItems = $list.GetItems([Microsoft.SharePoint.Client.CamlQuery]::CreateAllItemsQuery())
    $ctx.load($listItems)
    $ctx.load($list.Fields)
    $ctx.executeQuery()

    $field = $list.Fields.GetByInternalNameOrTitle($existingColumnName)
    $guid = [GUID]$ClientSideComponentId
    $field.ClientSideComponentId=$guid
    $field.update()
    $ctx.Load($field)
    $ctx.executeQuery()

}
catch{
    write-host "$($_.Exception.Message)" - foregroundcolore red
}   


2. The second is to use REST API like mentioned here.

3. The third easiest way is to use the PnP PowerShell like below.

$siteURL = "https://ionisdevops.sharepoint.com/sites/Harry-PM-Testing"

$listTitle = "Order"
$existingColumnName = "SPFxExeColumn"
$ClientSideComponentId = "acd4d36c-a4ad-449c-a312-93862bd52376"
  
Connect-PnPOnline -Url $siteurl  

$guid = [GUID]$ClientSideComponentId
Set-PnPField -List $listTitle -Identity $existingColumnName -Values @{ClientSideComponentId=$guid} -UpdateExistingLists

You can apply the same Field Customizer to as many existing fields as you like as long as the field has all the required schema as the the original list. The fields do not need to be the same type like "Number" since it will not matter.


If you need to apply other SPFx extension like custom action to whole tenant, please refer to other blog.