SBoudrias / Inquirer.js

A collection of common interactive command line user interfaces.
MIT License
20.21k stars 1.3k forks source link

[Feature]: inquirer/search - external pagination support (or be able to pass search value back to new instance) #1489

Open tanepiper opened 3 months ago

tanepiper commented 3 months ago

Description:

I'm using search to implement a cli tool that allows you to pass a name, which is then queried against a product database. The API has a max request of 50 items, but will have more than 50 results so a page value can be passed.

It would be good to either have a pagination option that can allow the search to recall the method with new parameters but keep the search query.

The other option I can see is if I provide my own value here to handle as an answer I can call the search function again, but to do this I would want to be able to pass in the original value to display.

SBoudrias commented 3 months ago

Hi, this topic was brought up here #1487 too. I'm currently leaning on leaving this up to custom prompts - but I'll consider any concrete proposal.

What would you like the API to look like? Some pseudo code of what the API could look like would be useful.

paul-uz commented 3 months ago

I'm going to have a go at this. Config for the prompt could include setting your pagination query params etc, I think. My use case is paginating calls to AWS services, not REST APIs, but I reckon they'd follow a similar approach?

tanepiper commented 3 months ago

Here's how I ended up re-writing my search app to work - as you can see when I go to previous or next pages I have to change the message (I also clear the screen and increase the size but that's a preference)

public async search(searchTerm?: string, page = 0) {
    let message = 'Search for an Product Name';
    if (searchTerm) {
      message = `Search for an Product Name (Current: ${searchTerm})`;
    }

    const answer = await search<string>({
      pageSize: page === 0 ? 5 : 10,
      message,
      theme: {
        helpMode: 'always',
        prefix: '🔍',
      },
      source: async (input: string, { signal }: { signal: AbortSignal }) => {
       await setTimeout(1000);

        if (signal?.aborted) return [];

        if (!searchTerm && !input) {
          return [];
        }

        // Return a promise that resolves after the debounce period
        try {
          const products = await this.client.itemsSearch(
            input ?? searchTerm,
            page,
            50,
            signal,
          );
          const results =
            page > 0
              ? [
                  {
                    name: 'Previous Results',
                    value: `-${input ?? searchTerm}`,
                    description: 'Go to previous results page',
                  },
                ]
              : [];

          products?.forEach(({ content }, index) =>
            results.push({
              name: item.name,
              value: item.id,
              description: `Page: ${page + 1} | Record ${index + 1} / ${products.length} | ${content.id} `,
            }),
          );
          if (products?.length === 50) {
            results.push({
              name: 'Next Results',
              value: `+${input ?? searchTerm}`,
              description: 'Select to load the next page',
            });
          }
          return results;
        } catch (error) {
            console.error('\n Error during search:', error.message);
            return [];
          }
        }
      },
    }).then((answer) => {
      if (!isCorrectID(answer.split('-')?.[1])) {
        page = answer[0] === '-' ? page - 1 : page + 1;
        process.stdout.write('\x1Bc');
        return this.search(answer.substring(1), page);
      }
      return answer;
    });
    return answer;
  }

Looking at the design I guess it would be best to provide it along with the answer. Maybe have it as options on the setup (to enable prev/next) and then in the response:

.then((answer, { prev, next }) => {
  if (prev) {
     return ....
  }
})

Yes the user has to handle it, but you provide a hook into it.

The only other way I see it to make it event-based