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:
parent
5bb3d181eb
commit
f1f01ec43b
|
@ -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>
|
||||
|
||||
|
|
|
@ -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": {}
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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
|
@ -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",
|
||||
|
|
Binary file not shown.
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 } />
|
||||
);
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -3,5 +3,6 @@ import { IDropdownOption } from 'office-ui-fabric-react';
|
|||
export interface IAsyncDropdownState {
|
||||
processed: boolean;
|
||||
options: IDropdownOption[];
|
||||
selectedKey: string | number;
|
||||
error: string;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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": [{
|
||||
|
|
|
@ -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 = "";
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue