UNPKG

file-prompt

Version:

An interactive prompt for selecting files from a directory.

284 lines (249 loc) 6.69 kB
import colors from 'chalk'; import fs from 'fs'; import Dispatcher from '../streams/dispatcher'; import DispatchTransform from '../streams/dispatch_transform'; import GenericTransform from '../streams/generic_transform'; import MenuTransform from '../streams/menu_transform'; import minimatch from 'minimatch-all'; 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' * }, * // ... * ] */ /** * Directory Page * The files menu page of our CLI app * * @class DirectoriesPage * @extends {Page} * @property {string} intro - Introduction text * @property {string} question - Prompt question */ export default class DirectoriesPage extends Page { question = 'Add files or enter directory'; /** * 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(); } /** * Get Initial State * Initializes this component's state * * @method * @public * @returns {object} Initial state properties */ getInitialState () { return { selected: [], targetDir: null, }; } /** * Get Files * Returns an array of files to select * * @param {string} glob - Globstr to test the page against * @param {string} [dir] - Directory to look through * @returns {array} Array of menu options */ getFiles (glob, dir) { let configBasedir = this.select('config.base'), isBaseDir = dir === configBasedir, selectedFiles = this.select('files'), files = [], directories = []; // Force glob to be an array if (!Array.isArray(glob)) glob = [glob]; files = fs.readdirSync(dir); files = files // Map to full filepath .map((file) => path.join(dir, file)) // First filter files against our glob .filter((filepath) => { let stats = fs.statSync(filepath); // If we have a directory store it and move on as we are good if (stats.isDirectory()) { directories.push(filepath); return true; } return minimatch(filepath, glob); }) // Second filter files against .map((filepath, i) => { let label = path.relative(dir, filepath), isDirectory = directories.indexOf(filepath) > -1; // if file was a directory add a slash to the label if (isDirectory) label += '/'; return { id: isBaseDir ? i + 1 : i + 2, name: label, value: filepath, isSelected: selectedFiles.indexOf(filepath) > -1, // Make dir labels bold bold label: isDirectory ? colors.bold(label) : label, }; }); // If we nested in the baedir add an option to go back if (dir !== configBasedir) { files.unshift({ id: 1, name: '..', value: path.resolve(dir, '..'), label: colors.bold('..'), }); } return files; } /** * Process File * * @method * @public * @param {Stream} stream - The ongoing transformation stream * @param {object} transformAction - Transform action */ processFile (stream, transformAction) { let selectedDir = null, data = transformAction.data, params = transformAction.params, filepath = data.value, stats; // If transform Action is not a file, just push it down. if (transformAction.type !== 'file') { stream.push(transformAction); return; } stats = fs.statSync(filepath); /** * If directory change the transform action so that it does not get * selected in the store. */ if (stats.isDirectory() && transformAction.data.type === 'single') { selectedDir = filepath; transformAction.type = 'directory'; } else if (stats.isDirectory()) { return; } /** * If we have a selected dir, there is not one already and it's the * only value input and not from a multiple item select then we're good. */ if ( selectedDir && params.queryCount === 1 && !this.state.targetDir && data.type === 'single' ) { this.state.targetDir = selectedDir; } stream.push(transformAction); } /** * 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; default: stream.end(); this.navigate('directories', { base: this.state.targetDir }); break; } break; case 'done': this.reprompt(); break; case 'error': this.displayError(action.data); break; } } /** * Show Prompt * 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 () { return { query: new QueriesTransform(), menu: new MenuTransform({ menu: this.menu, }), process: new GenericTransform( this.processFile.bind(this) ), dispatch: new DispatchTransform({ store: this.props.store, }), }; } renderMenu () { this.menu.setOptions(this.getFiles(this.getGlob(), this.getBasedir())); return this.menu.render(); } renderPrompt () { return this.showPrompt; } }