Pnpjs: Issue setting StartRow with search paging

Created on 16 Jul 2018  路  4Comments  路  Source: pnp/pnpjs

Category

  • [ ] Enhancement
  • [X] Bug
  • [ ] Question
  • [ ] Documentation gap/issue

Version

Please specify what version of the library you are using: [ 1.1.2]

Expected / Desired Behavior / Question

Observed Behavior

When making an initial page next using the getPaged method, the library sets the StartRow property to -10 (assuming we do not set and use the default RowCount = 10)

Steps to Reproduce

  1. Make a simple SPFX web part and install the needed library components to perform search.
  2. Create a Next button and Previous button
  3. On render, process the initial request. Sample query object:
private defaultKql: string = "";
private searchPage: number = 0;
private currentResults: SearchResults = null;

public async render(): Promise<void> {

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

    this.defaultKql = `Path:"${this.context.pageContext.site.absoluteUrl}"`;

    let queryParameters: UrlQueryParameterCollection = new UrlQueryParameterCollection(window.location.href);

    let initialSearchValue: string = null;

    let initialResults: ISearchResultItem[] = [];

    if (queryParameters.getValue("k")) {
      initialSearchValue = queryParameters.getValue("k");
      let initialResult: ISearchResult = await this.performSearch(initialSearchValue);
      initialResults = initialResult.results;
    }

    const element: React.ReactElement<SearchWebPartProps> = React.createElement(
      SearchWebPart,
      {
        results: initialResults,
        performSearchHandler: this.performSearch,
        performSearchNext: this.nextSearchPage,
        performSearchPrev: this.prevSearchPage,
        initialSearchValue: initialSearchValue
      });

    ReactDom.render(element, this.domElement);
  }

@autobind
  private async nextSearchPage(): Promise<ISearchResult> {
    this.searchPage++;
    this.currentResults = await this.currentResults.getPage(this.searchPage);

    return await this.processResults(this.currentResults.PrimarySearchResults);
  }

  @autobind
  private async prevSearchPage(): Promise<ISearchResult> {
    --this.searchPage;
    this.currentResults = await this.currentResults.getPage(this.searchPage);

    return await this.processResults(this.currentResults.PrimarySearchResults);
  }

  @autobind
  private async performSearch(query: string): Promise<ISearchResult> {

    this.searchPage = 0;

    let queryParameters: UrlQueryParameterCollection = new UrlQueryParameterCollection(window.location.href);

    let q: SearchQuery = {
      Querytext: `${this.defaultKql} ${query}`,
      StartRow: 0
    };

    this.currentResults = await sp.search(q);

    return await this.processResults(this.currentResults.PrimarySearchResults);
  }

  private async processResults(primaryResults: SearchResult[]): Promise<ISearchResult> {
    let result: ISearchResultItem[] = [];

    let nextPage = this.searchPage + 1;
    let hasNextPageResults = await this.currentResults.getPage(nextPage);
    let hasNextPage: boolean = hasNextPageResults.PrimarySearchResults.length > 0;

    let hasPrevPage: boolean = false;

    if (this.searchPage > 0) {
      let prevPage = this.searchPage - 1;
      let hasPrevPageResults = await this.currentResults.getPage(prevPage);
      hasPrevPage = hasPrevPageResults.PrimarySearchResults.length > 0;
    }

    this.currentResults.PrimarySearchResults.forEach((v: SearchResult) => {
      result.push({
        Title: v.Title,
        Highlights: v.HitHighlightedSummary,
        Url: v.Path
      });
    });

    return {
      results: result,
      hasNextValues: hasNextPage,
      hasPrevValues: hasPrevPage
    };
  }

And here is the react component:

export default class SearchWebPart extends React.Component<ISearchWebPartProps, ISearchWebPartState> {

  private initialSearchValue: string | "";

  constructor(props: ISearchWebPartProps) {
    super(props);

    if (props.initialSearchValue && props.initialSearchValue.length > 0) {
      this.initialSearchValue = props.initialSearchValue;
    }

    this.state = {
      results: props.results,
      hasNextValues: true,
      hasPrevValues: false
    };
  }

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

    let searchResults: JSX.Element[] = this.state.results && this.state.results.length > 0 ? this.state.results.map((v: ISearchResultItem) => {
      let resultItemHighlights: string = v.Highlights.replace(/<c0>/, "").replace(/<\/c0>/, "").replace(/<ddd\/>/, "");
      return (<DocumentCard>
        <FileTypeIcon type={IconType.image} size={ImageSize.medium} path={v.Url} />
        <DocumentCardLocation
          location={v.Title}
          locationHref={v.Url}
        />
        <DocumentCardTitle title={resultItemHighlights}  />
      </DocumentCard>);
    }) : [(<div>No results</div>)];

    let searchBox: JSX.Element = (<SearchBox
      placeholder='Search...'
      ariaLabel='Search...'
      onSearch={this.searchTyped}
      defaultValue='Search...'
    />);

    if (this.initialSearchValue && this.initialSearchValue.length > 0) {
      searchBox = (<SearchBox
        placeholder='Search...'
        ariaLabel='Search...'
        onSearch={this.searchTyped}
        defaultValue='Search...'
        value={this.initialSearchValue}
      />);
    }

    let hiddenStyle = { display: 'none' };

    return (
      <div>
        {searchBox}
        <ActionButton iconProps={{ iconName: 'Search' }} onClick={(e) => { this.searchTyped((e.target as HTMLElement).nodeValue); }}></ActionButton>
        <div className={"cchbc-promise-search-results"}>
          {searchResults}
        </div>
        <ActionButton iconProps={{ iconName: 'Previous' }} onClick={this.performSearchPrev} style={!this.state.hasPrevValues ? hiddenStyle : {}}>Prev</ActionButton>
        <ActionButton iconProps={{ iconName: 'Next' }} onClick={this.performSearchNext} style={!this.state.hasNextValues ? hiddenStyle : {}}>Next</ActionButton>
      </div>
    );
  }

  @autobind
  private performSearchPrev(): void {
    this.initialSearchValue = "";
    this.props.performSearchPrev().then(this.setStateResults).catch((err: any) => {
      console.error(err);
    });
  }

  @autobind
  private performSearchNext(): void {
    this.initialSearchValue = "";
    this.props.performSearchNext().then(this.setStateResults).catch((err: any) => {
      console.error(err);
    });
  }

  @autobind
  private searchTyped(e): void {
    this.initialSearchValue = "";
    this.props.performSearchHandler(e).then(this.setStateResults).catch((err: any) => {
      console.error(err);
    });
  }

  @autobind
  private setStateResults(response: ISearchResult): void {
    this.setState({
      results: response.results,
      hasNextValues: response.hasNextValues,
      hasPrevValues: response.hasPrevValues
    });
  }
}
  1. Try to click next. Error 400 is returned. Observe the network tab of the browser and check the send body of the PostQuery request - its StartRow value is set to -10 (or currentRow - RowCount * currentPage).
code by design question

All 4 comments

Can you create a git repo where we can pull down a minimal reproduction of this issue? That will allow us to easily test and ensure that we are looking at what you are looking at. Thanks!

https://github.com/Sartor87/BugReports/tree/master/sp_PnP_Issue_171

Here is a stripped down code for replicating the issue. Please, note that I've resolved it by removing the startRow parameter and setting the initial start page to 1 instead of 0.

Thanks, will still have a look to see what is going on.

I had a look at this today and I think you found the issue. The pages should start with 1, which you can see in the search docs paging example. We did a long time ago go back and forth on if pages should start with 0 or 1 so perhaps that caused some confusion.

I am going to close this as resolved. Thanks!

Was this page helpful?
0 / 5 - 0 ratings