UNPKG

file-prompt

Version:

An interactive prompt for selecting files from a directory.

293 lines (255 loc) 6.42 kB
import glob from 'glob-all'; import Dispatcher from '../streams/dispatcher'; import DispatchTransform from '../streams/dispatch_transform'; import GenericTransform from '../streams/generic_transform'; import MenuTransform from '../streams/menu_transform'; import Page from '../page'; import path from 'path'; import Prompt from '../prompt'; import QueriesTransform from '../streams/queries_transform'; import VerticalMenu from '../vertical_menu'; /** * Menu Options format * * @example * [ * { * id: 1, * label: 'directories', * name: 'directories', * value: 'directories' * }, * // ... * ] */ /** * Glob Page * The files menu page of our CLI app * * @class GlobPage * @extends {Page} * @property {string} intro - Introduction text * @property {string} question - Prompt question */ export default class GlobPage extends Page { /** * Constructor * Initializes this page's subclass * * @constructor * @param {object} props - Properties to initialize the class with */ constructor (props) { super(props); this.menu = new VerticalMenu({ canUnselect: true, acceptsMany: true, stdin: this.props.stdin, stdout: this.props.stdout, app: this.props.app, }); this.prompt = new Prompt({ stdin: this.props.stdin, stdout: this.props.stdout, }); this.pipeline = this.createPipeline(); } /** LIFECYCLE METHODS */ /** * Get Initial State * Initializes this component's state * * @method * @public * @returns {object} Initial state properties */ getInitialState () { return { files: [], filter: null, }; } /** * Component Should Update * Used to determine if component should re-render when state updates * occur. For this component it should not. * * @method * @public * @returns {boolean} False to prevent component from re-rendering. */ componentShouldUpdate () { return false; } /** HELPER METHODS */ /** * Create Options From * Takes our selected files and builds a menu options array * * @method * @public * @param {array} files - Array of filenames to make into options * @returns {array} Array of menu options */ createOptionsFrom (files) { let selectedFiles = this.select('files'), basedir = this.getBasedir(); return files.map((filename, i) => { return { id: i + 1, label: path.relative(basedir, filename), name: filename, value: filename, isSelected: selectedFiles.indexOf(filename) > -1, }; }) || []; } /** * Get Files * Returns an array of files to select * * @param {string} pattern - Glob string to look for * @returns {array} Array of menu options */ getFiles (pattern) { let basedir = this.getBasedir(); return glob.sync(path.join(basedir, pattern), { cwd: process.cwd() }); } /** * Process Glob * Simple transformer that reads a glob string into an array of files * * @method * @public * @param {Stream} stream - Generic transform stream * @param {object} transformAction - Stdin input value */ processGlob (stream, transformAction) { let input, files = [], { type, creator } = transformAction; if (type !== 'string' || creator !== 'prompt') { stream.push(transformAction); return; } input = transformAction.data; /** * If the only input given is an empty response lets go back to * the index. */ if (!input) { stream.destroy(); // this.navigate('index'); transformAction.type = 'navigate'; transformAction.data = { value: 'blank' }; stream.push(transformAction); return; } files = this.getFiles(input); if (!files.length) { stream.pushError(`No files matched the glob string "${input}"`); return; } this.setState({ filter: input, files, }); } /** * Question * Returns the prompt question based on if files have been selected * or not * * @method * @public * @returns {string} Prompt string to ask the user */ question () { let basedir = this.getBasedir(); if (this.state.files.length) { return 'Add files'; } basedir = path.relative(path.resolve(this.select('config.base'), '..'), basedir); return `Enter glob from ${basedir}`; } /** * Route * Routes the actions from the pipeline to navigation or error events. * * @param {stream} stream - Writable stream at the end of the pipeline * @param {object} action - Final action passed to router */ route (stream, action) { switch (action.type) { case 'navigate': switch (action.data) { case 'blank': stream.end(); this.navigate('index'); break; case 'all': this.navigate('index'); break; } break; case 'done': this.reprompt(); break; case 'error': this.displayError(action.data); break; } } /** * ShowPrompt * Beckons the prompt * * @method * @public * @returns {stream} The resulting writable dispatcher stream. */ showPrompt () { return this.prompt.beckon(this.question()) .pipe(this.pipeline) .pipe(new Dispatcher(this.route)); } /** * Workflow * Returns the steps in the pipeline to send to labeled stream splicer. * * @method * @public * @returns {object} named steps in the pipeline */ workflow () { /** * If files have been found from the glob, then query against those * matching files. */ if (this.state.files.length) { return { query: new QueriesTransform(), menu: new MenuTransform({ menu: this.menu, }), dispatch: new DispatchTransform({ store: this.props.store, }), }; } return { glob: new GenericTransform(this.processGlob.bind(this)), dispatch: new DispatchTransform({ store: this.props.store, }), }; } /** RENDER METHODS */ renderMenu () { if (!this.state.files.length) return ''; this.menu.setOptions(this.createOptionsFrom(this.state.files)); return this.menu.render(); } renderPrompt () { return this.showPrompt; } }