SAP / yeoman-ui

Provide rich user experience for Yeoman generators using VSCode extension or the browser.
Apache License 2.0
101 stars 46 forks source link

Help wanted #492

Open Cre8tiveDigital opened 3 years ago

Cre8tiveDigital commented 3 years ago

Hi, I am building a yeoman generator in a way that suits the UI running in VS (Great tool by the way), I have a master app which has one prompt in it which askes what client (sub-generator) you whish to choose. Once the user chooses a client (sub-generator) that client (sub-generator) is launched.

I have followed the example in the food q generator and when I only have one option in the master app I can use the async initializing() function but when I have more than one client (sub-generator) I have to move to async writing() function, which I am ok with as it seems to work fine and renders in Application Wizard ok apart from the next button on each prompt now appears to be a finish button.

I can navigate through the whole process for UI purposes I would like this to be the Next button. Any help on this would be appreciated.

Also, I am looking for help with being able to stop the Application/generator if it detects there is already a project with the same name.

Thanks.

Cre8tiveDigital commented 3 years ago

Has anyone got any advice for me on this issue its been 20 days.

Thanks

tomer-epstein commented 3 years ago

Hi @Cre8tiveDigital

Sorry for the late reply and thanks for the compliments. Do you have an option to share with us your source code so we can reproduce/debug this issue?

Regards, Tomer

tomer-epstein commented 3 years ago

@Cre8tiveDigital in each of your sub-generators please add this code:

module.exports = class extends Generator {

constructor(args, opts) {

super(args, opts);
this.prompts = opts.prompts; // get a list of all parent prompts
this.parentPromptsQuantity = this.prompts.size(); // save the initial quantity of parent promts

// create all dynamic prompts (name and description only)
this.dynamicAddressPrompt = {name: "Dynamic Prompt Name", description: "Dynamic Prompt N Description"}; 

const prompts = [
  {name: "First SubGen Prompt Name", description: "First SubGen Prompt Description"},
  this.dynamicAddressPrompt,
  {name: "Second SubGen Prompt Name", description: "Second SubGen Prompt Description"}];

// add sub-generator prompts to the parent generator prompts
this.prompts.splice(this.parentPromptsQuantity, 0, prompts);
}
bjldevelopers commented 3 years ago

Thank's Tomer, in my main application, I have the following,

const Generator = require("yeoman-generator");
const chalkPipe = require("chalk-pipe");
const Inquirer = require("inquirer");
const path = require("path");
const _ = require("lodash");
const types = require("@sap-devx/yeoman-ui-types");
const Datauri = require("datauri/sync");
const DEFAULT_IMAGE = require("./images/defaultImage");

module.exports = class extends Generator {
  _getAnswer(name, res) {
        return this._getOption(name) || _.get(res, "[${name}]");
  }
  _getOption(name) {
        return _.get(this.options, "[${name}]");
    }
  _getImage(imagePath) {
        let image;
        try {
            image = Datauri(imagePath).content;
        } catch (error) {
            image = DEFAULT_IMAGE;
            this.log("Error = ${error}");
        }

        return image;
  }
  constructor(args, opts) {
    super(args, opts);

    this.data = opts.data;

        this.appWizard = types.AppWizard.create(opts);

    this.setPromptsCallback = fn => { // set callback method for yeoman-ui framework
      if (this.prompts) {
        this.prompts.setCallback(fn);
      }
    };

    var prompts = [ // set list of all virtual prompts
      {name: "Which Client", description: "Please choose a client you wish to create an email for."}
    ];
    this.prompts = new types.Prompts(prompts); // save the propmpts in a variable
  }

  async prompting() {
    let prompts = [
      {
            name: "client",
            type: "list",
            message: "Choose client",
            guiOptions: {
                type: "tiles"},
        choices: [
          { value: "client1", name: "client1", description: "Select this to start building client 1 Emails", image: this._getImage(path.join(this.sourceRoot(), "../images/client1.png")) },
          { value: "client2", name: "client2", description: "Select this to start building client2 Emails", image: this._getImage(path.join(this.sourceRoot(), "../images/client2.png")) },
          { value: "client3", name: "client2", description: "Select this to start building client3 Emails", image: this._getImage(path.join(this.sourceRoot(), "../images/client3.jpg")) },
        ],
      }
    ];
    this.answers = await this.prompt(prompts);
        this.answers.client = this._getAnswer("client", this.answers);
  }

  async writing() {
    var _this = this;
    if (this.answers.client === 'client1'){
      this.composeWith(require.resolve("../client1"), { prompts: this.prompts, appWizard: this.appWizard });
    }    
    else if (this.answers.client === 'client2'){
    this.composeWith(require.resolve("../client2"), { prompts: this.prompts, appWizard: this.appWizard });    
    }
    else if (this.answers.client === 'client3'){
      this.composeWith(require.resolve("../client2"), { prompts: this.prompts, appWizard: this.appWizard });    
      }
  }; 
}

And then this is the top of the client1 sub-generator

var Generator = require('yeoman-generator');
var _ = require('lodash');
var path = require('path');
var str;
const Datauri = require('datauri/sync');
const DEFAULT_IMAGE = require("./images/defaultImage");
const types = require('@sap-devx/yeoman-ui-types');
const fs = require('fs');

module.exports = class extends Generator {
    constructor(args, opts) {
        super(args, opts);
        this.prompts = opts.prompts; // get list of all parent prompts
        this.appWizard = opts.appWizard;
        this.parentPromptsQuantity = this.prompts.size(); // save initial quantity of parent promts
        // create all dynamic prompts (name and description only)

        const prompts = [
            { name: "Multiple Versions", description: "Plese select if you require mutiple versions of this build, if so please then enter how many." },
            { name: "Job Number", description: "Please enter a job number" },
            { name: "Job Title", description: "Please enter the name of your project" }
        ];

        // add sub-generator prompts to the parent generator prompts
        this.prompts.splice(this.parentPromptsQuantity, 0, prompts);
    }
    _getAnswer(name, res) {
        return this._getOption(name) || _.get(res, '[${name}]');
    }
    _getOption(name) {
        return _.get(this.options, '[${name}]');
    }
    _requireLetters(value) {
        if (/\s/.test(value)) {
            // It has only spaces, or is empty
            return 'Description must not contain spaces';
        }
        return true
    }

    async prompting() {
        // Prompting the user if they require multiple version then how many
        const prompts = [
            {
                type: "confirm",
                name: "multipleversions",
                message: "Do you require more than one version",
                default: false
            },
            {
                when: async response => {
                    return new Promise(resolve => {
                        setTimeout(() => {
                            resolve(this._getAnswer("multipleversions", response));
                        }, 2000);
                    });
                },
                type: "input",
                name: "noofbuilds",
                message: "How many versions do you require?"

            }
        ];
        this.answers = await this.prompt(prompts);

        //Promting the user for a job number
        const jobnumberPrompt = [
            {
                type: "input",
                name: "jobnumber",
                message: "Please enter a job number",
                validate: function (answer) {
                    if (answer.length < 7) {
                        return 'Job number must be at least 7 numbers long';
                    }

                    return true;
                },
            },
        ];
        const answersJobnumber = await this.prompt(jobnumberPrompt);
        this.answers = Object.assign({}, this.answers, answersJobnumber);

        //Promting the user for a description
        const descriptionPrompt = [
            {
                type: "input",
                name: "description",
                message: "Please enter a name for your build",
                validate: this._requireLetters
            },
        ];
        const answersDescription = await this.prompt(descriptionPrompt);
        this.answers = Object.assign({}, this.answers, answersDescription);

    }

    _getImage(imagePath) {
        let image;
        try {
            image = Datauri(imagePath).content;
        } catch (error) {
            image = DEFAULT_IMAGE;
            this.log('Error = ${error}');
        }

        return image;
    }

    writing() {

To note after inserting client 3 the application crashes out as complete / not failed but it should be entering the client 3 sub-generator.

I could share the entire application with you but would need to do that on a private.

Thanks in advance.

tomer-epstein commented 3 years ago

Hi @Cre8tiveDigital

Thank you for bringing this scenario to us. We have investigate it and found that it's currently not supported. It's a valid and important flow and we intend to discuss it and give it priority.

As workaround we suggest to eliminate the use of the Prompts in the constructor.

Regards, Tomer

`

constructor(args, opts) {

super(args, opts);

this.data = opts.data;

    this.appWizard = types.AppWizard.create(opts);

}

`

Cre8tiveDigital commented 3 years ago

Thank's @tomer-epstein for the quick response, I implemented the suggested workaround which has fixed the issue on the main generator but once the sub-generator runs I get the finish button on each step instead of the next button. I have tried implementing the same workaround into the sub-generator but has no effect. I am still in development so can hope for an update if this has been made a priority.

Regards

tomer-epstein commented 3 years ago

Hi @Cre8tiveDigital ,

Did you eliminate the use of the Prompts in the constructor both for the root and sub generators? I suggest not call the composeWith in the 'writing' method, but in your case do it in the end of the prompt method.

Regards, Tomer

Cre8tiveDigital commented 3 years ago

Thanks, @tomer-epstein,

This is a great workaround and seems to be working, I will implement it across all my subgens now and will update if I run into any more issues.

Thanks for the help