React Content Query : Upgraded to drop 1.2.0 and added site and web automatic pre-selection (#313)

* Upgrade to drop 1.2.0 and added site url and web url preselection

Upgrade to drop 1.2.0 and added site url and web url preselection

* Added a working .sppkg for easy testing

* Added a bug fix to the Pull Request #313

Since pull request hasn't been merged yet, added a one-liner bug fix for
fields that had spaces in their internal names.

* Updated the .sppkg solution with the last bug fix.

* Updated the .sppkg solution with the bug fix.

* Removing the .sppkg solution to update it

* Updated the .sppkg solution with the bug fix
This commit is contained in:
Simon-Pierre Plante 2017-09-27 08:23:38 -04:00 committed by Vesa Juvonen
parent 5bb3d181eb
commit f1f01ec43b
17 changed files with 1401 additions and 1636 deletions

View File

@ -29,6 +29,7 @@ Version|Date|Comments
1.0.3|August 12, 2017|Added external scripts functionnality
1.0.4|August 31, 2017|Fixed a bug where tenant sites/subsites were missing from the **Web Url** dropdown
1.0.5|September 1st, 2017|Added a **Site Url** parameter next to the **Web Url** parameter in order to narrow down the results
1.0.6|September 19, 2017| Upgraded to SharePoint drop 1.2.0 and added the site url and web url preselection when adding the WebPart for the first time on a page. Also fixed a bug with fields that had spaces in their internal names (automatically replaced with `_x0020_` by SharePoint).
## Disclaimer
**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
@ -37,7 +38,8 @@ Version|Date|Comments
### Cross site collection
The WebPart uses the search in order to get all sites under the current domain, which makes it possible to query not only subsites but other site collections and their subsites as well.
The WebPart uses the search in order to get all sites under the current domain, which makes it possible to query not only subsites but other site collections and their subsites as well. By default, the current site collection and the current web on which the user is adding the WebPart will be pre-selected automatically.
<img src="Misc/allsites_v2.gif" />
<br>

View File

@ -1,13 +1,18 @@
{
"entries": [
{
"entry": "./lib/webparts/contentQuery/ContentQueryWebPart.js",
"manifest": "./src/webparts/contentQuery/ContentQueryWebPart.manifest.json",
"outputPath": "./dist/content-query.bundle.js"
"$schema": "https://dev.office.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"content-query-bundle": {
"components": [
{
"entrypoint": "./lib/webparts/contentQuery/ContentQueryWebPart.js",
"manifest": "./src/webparts/contentQuery/ContentQueryWebPart.manifest.json"
}
]
}
],
"externals": {},
},
"localizedResources": {
"contentQueryStrings": "webparts/contentQuery/loc/{locale}.js"
}
}
"contentQueryStrings": "lib/webparts/contentQuery/loc/{locale}.js"
},
"externals": {}
}

View File

@ -2,7 +2,7 @@
"solution": {
"name": "React Content Query",
"id": "00406271-0276-406f-9666-512623eb6709",
"version": "1.0.5.0"
"version": "1.0.6.0"
},
"paths": {
"zippedPackage": "solution/react-content-query-webpart.sppkg"

View File

@ -1,2 +1,3 @@
{
"cdnBasePath": "https://publiccdn.sharepointonline.com/spptechnologies.sharepoint.com/110700492eeea162ee5bad0f35b1f0061ded8bf436ce0199efe2a4d24109e1c0df1ec594/react-content-query-1.0.6"
}

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +1,15 @@
{
"name": "react-content-query",
"version": "1.0.5",
"version": "1.0.6",
"private": true,
"engines": {
"node": ">=0.10.0"
},
"dependencies": {
"@microsoft/sp-core-library": "~1.1.0",
"@microsoft/sp-webpart-base": "~1.1.1",
"@microsoft/sp-core-library": "~1.2.0",
"@microsoft/sp-webpart-base": "~1.2.0",
"@types/handlebars": "4.0.32",
"@types/react": "0.14.46",
"@types/react": "15.0.38",
"@types/react-addons-shallow-compare": "0.14.17",
"@types/react-addons-test-utils": "0.14.15",
"@types/react-addons-update": "0.14.14",
@ -23,9 +23,9 @@
"react-dom": "15.4.2"
},
"devDependencies": {
"@microsoft/sp-build-web": "~1.1.0",
"@microsoft/sp-module-interfaces": "~1.1.0",
"@microsoft/sp-webpart-workbench": "~1.1.0",
"@microsoft/sp-build-web": "~1.2.0",
"@microsoft/sp-module-interfaces": "~1.2.0",
"@microsoft/sp-webpart-workbench": "~1.2.0",
"@types/chai": ">=3.4.34 <3.6.0",
"@types/mocha": ">=2.2.33 <2.6.0",
"awesome-typescript-loader": "^3.2.1",

View File

@ -161,7 +161,7 @@ export class CamlQueryHelper {
}
else if (filter.operator == QueryFilterOperator.ContainsAny || filterUsers == null)
{
let values = filterUsers != null ? filterUsers.map(x => Text.format("<Value Type='Integer'>{0}</Value>", x.value)).join('') : '';
let values = filterUsers != null ? filterUsers.map(x => Text.format("<Value Type='Integer'>{0}</Value>", x.optionalText)).join('') : '';
filterOutput = Text.format("<In><FieldRef Name='{0}' LookupId='TRUE' /><Values>{1}</Values></In>", filter.field.internalName, values);
}
else if (filter.operator == QueryFilterOperator.ContainsAll)
@ -244,8 +244,8 @@ export class CamlQueryHelper {
let digit = parseInt(operatorSplit[operatorSplit.length - 1].replace("[", "").replace("]", "").trim()) * addOrRemove;
let dt = new Date();
dt.setDate(dt.getDate() + digit);
let formattedDate = moment(dt).format("YYYY-MM-DDTHH:mm:ss\\Z");
filterValue = filterValue.replace(result, formattedDate);
let formatDate = moment(dt).format("YYYY-MM-DDTHH:mm:ss\\Z");
filterValue = filterValue.replace(result, formatDate);
}
}

View File

@ -162,6 +162,10 @@ export class ContentQueryService implements IContentQueryService {
this.searchService.getSitesStartingWith(serverUrl)
.then((urls) => {
// Adds the current site collection url to the ones returned by the search (in case the current site isn't indexed yet)
this.ensureUrl(urls, this.context.pageContext.site.absoluteUrl);
// Builds the IDropdownOption[] based on the urls
let options:IDropdownOption[] = [ { key: "", text: strings.SiteUrlFieldPlaceholder } ];
let urlOptions:IDropdownOption[] = urls.sort().map((url) => {
let serverRelativeUrl = !isEmpty(url.replace(serverUrl, '')) ? url.replace(serverUrl, '') : '/';
@ -201,6 +205,12 @@ export class ContentQueryService implements IContentQueryService {
this.searchService.getWebsFromSite(siteUrl)
.then((urls) => {
// If querying the current site, adds the current site collection url to the ones returned by the search (in case the current web isn't indexed yet)
if(siteUrl.toLowerCase().trim() === this.context.pageContext.site.absoluteUrl.toLowerCase().trim()) {
this.ensureUrl(urls, this.context.pageContext.web.absoluteUrl);
}
// Builds the IDropdownOption[] based on the urls
let options:IDropdownOption[] = [ { key: "", text: strings.WebUrlFieldPlaceholder } ];
let urlOptions:IDropdownOption[] = urls.sort().map((url) => {
let siteRelativeUrl = !isEmpty(url.replace(siteUrl, '')) ? url.replace(siteUrl, '') : '/';
@ -372,7 +382,7 @@ export class ContentQueryService implements IContentQueryService {
let users: any[] = JSON.parse(data.value);
let userSuggestions:IPersonaProps[] = users.map((user) => { return {
primaryText: user.DisplayText,
value: user.EntityData.SPUserID || user.EntityData.SPGroupID
optionalText: user.EntityData.SPUserID || user.EntityData.SPGroupID
}; });
resolve(this.removeUserSuggestionsDuplicates(userSuggestions, currentPersonas));
})
@ -546,9 +556,11 @@ export class ContentQueryService implements IContentQueryService {
let normalizedResult: any = {};
for(let viewField of viewFields) {
let spacesFormattedName = viewField.replace(new RegExp("_x0020_", "g"), "_x005f_x0020_x005f_");
normalizedResult[viewField] = {
textValue: result.FieldValuesAsText[viewField],
htmlValue: result.FieldValuesAsHtml[viewField],
textValue: result.FieldValuesAsText[spacesFormattedName],
htmlValue: result.FieldValuesAsHtml[spacesFormattedName],
rawValue: result[viewField] || result[viewField + 'Id']
};
}
@ -626,7 +638,7 @@ export class ContentQueryService implements IContentQueryService {
let trimmedUsers: IPersonaProps[] = [];
for(let user of users) {
let isDuplicate = currentUsers.filter((u) => { return u.value === user.value; }).length > 0;
let isDuplicate = currentUsers.filter((u) => { return u.optionalText === user.optionalText; }).length > 0;
if(!isDuplicate) {
trimmedUsers.push(user);
@ -654,4 +666,19 @@ export class ContentQueryService implements IContentQueryService {
}
return trimmedTerms;
}
/***************************************************************************
* Makes sure the specified url is in the given collection, otherwise adds it
* @param urls : An array of urls
* @param urlToEnsure : The url that needs to be ensured
***************************************************************************/
private ensureUrl(urls: string[], urlToEnsure: string) {
urlToEnsure = urlToEnsure.toLowerCase().trim();
let urlExist = urls.filter((u) => { return u.toLowerCase().trim() === urlToEnsure; }).length > 0;
if(!urlExist) {
urls.push(urlToEnsure);
}
}
}

View File

@ -166,7 +166,7 @@ export class SearchService {
let pathCell = result.Cells.filter((cell) => { return cell.Key == "Path"; })[0];
pathIndex = result.Cells.indexOf(pathCell);
}
urls.push(result.Cells[pathIndex].Value);
urls.push(result.Cells[pathIndex].Value.toLowerCase().trim());
}
return urls;
}

View File

@ -40,7 +40,7 @@ export class AsyncChecklist extends React.Component<IAsyncChecklistProps, IAsync
* @param checked : Whether the checkbox is not checked or not
*************************************************************************************/
private onCheckboxChange(ev?: React.FormEvent<HTMLInputElement>, checked?: boolean) {
let checkboxKey = ev.currentTarget.attributes.getNamedItem('data').value;
let checkboxKey = ev.currentTarget.attributes.getNamedItem('value').value;
let itemIndex = this.checkedItems.indexOf(checkboxKey);
if(checked) {
@ -127,12 +127,12 @@ export class AsyncChecklist extends React.Component<IAsyncChecklistProps, IAsync
const checklistItems = this.state.items.map((item, index) => {
return (
<Checkbox data={ item.id }
<Checkbox id={ item.id }
label={ item.label }
defaultChecked={ this.isCheckboxChecked(item.id) }
disabled={ this.props.disable }
onChange={ this.onCheckboxChange.bind(this) }
inputProps={ { data: item.id } }
inputProps={ { value: item.id } }
className={ styles.checklistItem }
key={ index } />
);

View File

@ -3,9 +3,11 @@ import { Text } from '@microsoft/sp-core-library
import { Dropdown, IDropdownOption, Spinner } from 'office-ui-fabric-react';
import { IAsyncDropdownProps } from './IAsyncDropdownProps';
import { IAsyncDropdownState } from './IAsyncDropdownState';
import { cloneDeep } from '@microsoft/sp-lodash-subset';
export class AsyncDropdown extends React.Component<IAsyncDropdownProps, IAsyncDropdownState> {
/*************************************************************************************
* Component's constructor
* @param props
@ -17,6 +19,7 @@ export class AsyncDropdown extends React.Component<IAsyncDropdownProps, IAsyncDr
this.state = {
processed: false,
options: new Array<IDropdownOption>(),
selectedKey: props.selectedKey,
error: null
};
}
@ -47,14 +50,16 @@ export class AsyncDropdown extends React.Component<IAsyncDropdownProps, IAsyncDr
this.setState({
processed: false,
error: null,
options: new Array<IDropdownOption>()
options: new Array<IDropdownOption>(),
selectedKey: null
});
this.props.loadOptions().then((options: IDropdownOption[]) => {
this.setState({
processed: true,
error: null,
options: options
options: options,
selectedKey: this.props.selectedKey
});
})
.catch((error: any) => {
@ -67,6 +72,32 @@ export class AsyncDropdown extends React.Component<IAsyncDropdownProps, IAsyncDr
}
/*************************************************************************************
* Temporary fix because of an issue introducted in office-ui-fabric-react 4.32.0 :
* https://github.com/OfficeDev/office-ui-fabric-react/issues/2719
* Issue has been resolved but SPFX still refers to 4.32.0, so this is a temporary fix
* while waiting for SPFX to use a more recent version of office-ui-fabric-react
*************************************************************************************/
private onChanged(option: IDropdownOption, index?: number): void {
// reset previously selected options
const options: IDropdownOption[] = this.state.options;
options.forEach((o: IDropdownOption): void => {
if (o.key !== option.key) {
o.selected = false;
}
});
this.setState((prevState: IAsyncDropdownState, props: IAsyncDropdownProps): IAsyncDropdownState => {
prevState.options = options;
prevState.selectedKey = option.key;
return prevState;
});
if (this.props.onChanged) {
this.props.onChanged(option, index);
}
}
/*************************************************************************************
* Renders the the AsyncDropdown component
*************************************************************************************/
@ -79,8 +110,8 @@ export class AsyncDropdown extends React.Component<IAsyncDropdownProps, IAsyncDr
<div>
<Dropdown label={this.props.label}
isDisabled={this.props.disabled}
onChanged={this.props.onChanged}
selectedKey={this.props.selectedKey}
onChanged={this.onChanged.bind(this)}
selectedKey={this.state.selectedKey}
options={this.state.options} />
{loading}

View File

@ -3,5 +3,6 @@ import { IDropdownOption } from 'office-ui-fabric-react';
export interface IAsyncDropdownState {
processed: boolean;
options: IDropdownOption[];
selectedKey: string | number;
error: string;
}

View File

@ -19,20 +19,29 @@ $lightgray: #f5f5f5;
}
}
div[class~="ms-BasePicker-text"] {
background: #fff;
:global .ms-BasePicker-text {
background-color: #fff;
}
:global .ms-Checkbox {
margin-top: 10px;
}
:global .ms-Persona-details {
max-width: 165px;
}
.peoplePicker {
&.disabled {
div[class~="ms-PickerPersona-container"] {
display: none;
:global .ms-BasePicker-text {
background-color: #ccc;
:global .ms-BasePicker-input,
:global .ms-PickerPersona-container {
display: none;
}
}
}
}
span[class~="ms-TagItem-text"] {
max-width: 201px;
}
}
}

View File

@ -4,7 +4,7 @@
"id": "46edf08f-95c7-4ca7-9146-6471f9f471be",
"alias": "ContentQueryWebPart",
"componentType": "WebPart",
"version": "1.0.5",
"version": "1.0.6",
"manifestVersion": 2,
"preconfiguredEntries": [{

View File

@ -56,7 +56,7 @@ export default class ContentQueryWebPart extends BaseClientSideWebPart<IContentQ
* Returns the WebPart's version
***************************************************************************/
protected get dataVersion(): Version {
return Version.parse('1.0.5');
return Version.parse('1.0.6');
}
@ -66,6 +66,8 @@ export default class ContentQueryWebPart extends BaseClientSideWebPart<IContentQ
protected onInit(): Promise<void> {
return new Promise<void>((resolve, reject) => {
this.ContentQueryService = new ContentQueryService(this.context, this.context.spHttpClient);
this.properties.siteUrl = this.properties.siteUrl ? this.properties.siteUrl : this.context.pageContext.site.absoluteUrl.toLowerCase().trim();
this.properties.webUrl = this.properties.webUrl ? this.properties.webUrl : this.context.pageContext.web.absoluteUrl.toLocaleLowerCase().trim();
resolve();
});
}
@ -413,31 +415,6 @@ export default class ContentQueryWebPart extends BaseClientSideWebPart<IContentQ
}
/***************************************************************************
* Resets dependent property panes if needed
***************************************************************************/
private resetDependentPropertyPanes(propertyPath: string): void {
if(propertyPath == ContentQueryConstants.propertySiteUrl) {
this.resetWebUrlPropertyPane();
this.resetListTitlePropertyPane();
this.resetOrderByPropertyPane();
this.resetFiltersPropertyPane();
this.resetViewFieldsPropertyPane();
}
else if(propertyPath == ContentQueryConstants.propertyWebUrl) {
this.resetListTitlePropertyPane();
this.resetOrderByPropertyPane();
this.resetFiltersPropertyPane();
this.resetViewFieldsPropertyPane();
}
else if (propertyPath == ContentQueryConstants.propertyListTitle) {
this.resetOrderByPropertyPane();
this.resetFiltersPropertyPane();
this.resetViewFieldsPropertyPane();
}
}
/***************************************************************************
* Validates the templateUrl property
***************************************************************************/
@ -483,13 +460,38 @@ export default class ContentQueryWebPart extends BaseClientSideWebPart<IContentQ
}
/***************************************************************************
* Resets dependent property panes if needed
***************************************************************************/
private resetDependentPropertyPanes(propertyPath: string): void {
if(propertyPath == ContentQueryConstants.propertySiteUrl) {
this.resetWebUrlPropertyPane();
this.resetListTitlePropertyPane();
this.resetOrderByPropertyPane();
this.resetFiltersPropertyPane();
this.resetViewFieldsPropertyPane();
}
else if(propertyPath == ContentQueryConstants.propertyWebUrl) {
this.resetListTitlePropertyPane();
this.resetOrderByPropertyPane();
this.resetFiltersPropertyPane();
this.resetViewFieldsPropertyPane();
}
else if (propertyPath == ContentQueryConstants.propertyListTitle) {
this.resetOrderByPropertyPane();
this.resetFiltersPropertyPane();
this.resetViewFieldsPropertyPane();
}
}
/***************************************************************************
* Resets the List Title property pane and re-renders it
***************************************************************************/
private resetWebUrlPropertyPane() {
Log.verbose(this.logSource, "Resetting 'webUrl' property...", this.context.serviceScope);
this.properties.webUrl = null;
this.properties.webUrl = "";
this.ContentQueryService.clearCachedWebUrlOptions();
update(this.properties, ContentQueryConstants.propertyWebUrl, (): any => { return this.properties.webUrl; });
this.webUrlDropdown.properties.selectedKey = "";

View File

@ -7,10 +7,8 @@ $container-border: 1px solid #dadada;
border: $container-border;
padding: 20px 20px 15px 20px;
div[class*='ms-Checkbox'] {
&:first-child {
margin-top: 5px;
}
:global .ms-Checkbox {
margin-top: 10px;
}
}
.cqwpError {