From fd816eab2ab9c6f25bd00e33214e7382868c0847 Mon Sep 17 00:00:00 2001 From: AWolf81 Date: Mon, 19 Nov 2018 09:17:43 +0100 Subject: [PATCH 01/23] WIP: Business logic added. Styling needs to be improved. --- package.json | 3 + src/components/App/App.js | 2 + .../CreateNewProjectWizard/BuildPane.js | 16 +- .../CreateNewProjectWizard.js | 19 +- .../Gatsby/SelectStarterDialog.js | 139 +++++++++++++ .../CreateNewProjectWizard/MainPane.js | 182 ++++++++++++++---- .../CreateNewProjectWizard/SummaryPane.js | 17 ++ src/config/app.js | 2 +- src/config/project-types.js | 15 +- src/global-styles.js | 9 + src/reducers/index.js | 2 + src/sagas/task.saga.js | 37 +--- src/services/config-variables.service.js | 36 ++++ src/services/create-project.service.js | 36 +++- yarn.lock | 22 ++- 15 files changed, 443 insertions(+), 94 deletions(-) create mode 100644 src/components/CreateNewProjectWizard/Gatsby/SelectStarterDialog.js create mode 100644 src/services/config-variables.service.js diff --git a/package.json b/package.json index d953b215..6e1627d2 100644 --- a/package.json +++ b/package.json @@ -55,8 +55,11 @@ "electron-updater": "3.1.2", "fix-path": "2.1.0", "gatsby-cli": "1.1.58", + "js-yaml": "3.12.0", + "node-fetch": "2.3.0", "ps-tree": "1.1.0", "react-custom-scrollbars": "4.2.1", + "react-redux-toastr": "7.4.1", "rimraf": "2.6.2", "yarn": "1.9.2" }, diff --git a/src/components/App/App.js b/src/components/App/App.js index b9030c81..3bd4e648 100644 --- a/src/components/App/App.js +++ b/src/components/App/App.js @@ -2,6 +2,7 @@ import React, { PureComponent, Fragment } from 'react'; import { connect } from 'react-redux'; import styled, { keyframes } from 'styled-components'; +import ReduxToastr from 'react-redux-toastr'; import { COLORS } from '../../constants'; import { getSelectedProjectId } from '../../reducers/projects.reducer'; @@ -43,6 +44,7 @@ class App extends PureComponent { + ) } diff --git a/src/components/CreateNewProjectWizard/BuildPane.js b/src/components/CreateNewProjectWizard/BuildPane.js index 11c4a0c8..3db6eb24 100644 --- a/src/components/CreateNewProjectWizard/BuildPane.js +++ b/src/components/CreateNewProjectWizard/BuildPane.js @@ -79,7 +79,12 @@ class BuildPane extends PureComponent { } buildProject = () => { - const { projectName, projectType, projectIcon } = this.props; + const { + projectName, + projectType, + projectIcon, + projectStarter: projectStarterInput, + } = this.props; if (!projectName || !projectType || !projectIcon) { console.error('Missing one of:', { @@ -92,8 +97,15 @@ class BuildPane extends PureComponent { ); } + // Add url to starter if not passed + // Todo: We need error handling to show a notification that it failed to use the starter + // --> Just needed if we allow the user to enter an url to a starter. + const projectStarter = !projectStarterInput.includes('http') + ? 'https://github.com/gatsbyjs/' + projectStarterInput + : projectStarterInput; + createProject( - { projectName, projectType, projectIcon }, + { projectName, projectType, projectIcon, projectStarter }, this.props.projectHomePath, this.handleStatusUpdate, this.handleError, diff --git a/src/components/CreateNewProjectWizard/CreateNewProjectWizard.js b/src/components/CreateNewProjectWizard/CreateNewProjectWizard.js index cf27b090..5e8f3e7a 100644 --- a/src/components/CreateNewProjectWizard/CreateNewProjectWizard.js +++ b/src/components/CreateNewProjectWizard/CreateNewProjectWizard.js @@ -26,7 +26,12 @@ import type { Field, Status, Step } from './types'; import type { ProjectType, ProjectInternal, AppSettings } from '../../types'; import type { Dispatch } from '../../actions/types'; -const FORM_STEPS: Array = ['projectName', 'projectType', 'projectIcon']; +const FORM_STEPS: Array = [ + 'projectName', + 'projectType', + 'projectStarter', + 'projectIcon', +]; const { dialog } = remote; type Props = { @@ -44,6 +49,7 @@ type State = { projectName: string, projectType: ?ProjectType, projectIcon: ?string, + projectStarter: ?string, activeField: ?Field, settings: ?AppSettings, status: Status, @@ -55,6 +61,7 @@ const initialState = { projectName: '', projectType: null, projectIcon: null, + projectStarter: '', activeField: 'projectName', status: 'filling-in-form', currentStep: 'projectName', @@ -157,7 +164,11 @@ class CreateNewProjectWizard extends PureComponent { }; finishBuilding = (project: ProjectInternal) => { - const { isOnboardingCompleted, projectHomePath } = this.props; + const { + isOnboardingCompleted, + projectHomePath, + projectStarter, + } = this.props; const { projectType } = this.state; // Should be impossible @@ -172,6 +183,7 @@ class CreateNewProjectWizard extends PureComponent { project, projectHomePath, projectType, + projectStarter, // todo: check project reducer, if it's available on state isOnboardingCompleted ); @@ -194,13 +206,14 @@ class CreateNewProjectWizard extends PureComponent { projectName, projectType, projectIcon, + projectStarter, activeField, status, currentStep, isProjectNameTaken, } = this.state; - const project = { projectName, projectType, projectIcon }; + const project = { projectName, projectType, projectIcon, projectStarter }; const readyToBeBuilt = status !== 'filling-in-form'; diff --git a/src/components/CreateNewProjectWizard/Gatsby/SelectStarterDialog.js b/src/components/CreateNewProjectWizard/Gatsby/SelectStarterDialog.js new file mode 100644 index 00000000..d266099f --- /dev/null +++ b/src/components/CreateNewProjectWizard/Gatsby/SelectStarterDialog.js @@ -0,0 +1,139 @@ +// @flow +import React, { PureComponent } from 'react'; +import fetch from 'node-fetch'; // Note: This is using net.request from Node. Browser fetch throws CORS error. +import styled from 'styled-components'; +import yaml from 'js-yaml'; +import Scrollbars from 'react-custom-scrollbars'; +// import { SearchBox } from 'react-instantsearch/dom'; +// import { +// InstantSearch, +// InfiniteHits, +// Configure, +// } from 'react-instantsearch/dom'; + +import Paragraph from '../../Paragraph'; +import Heading from '../../Heading'; + +// import { ALGOLIA_KEYS } from '../../../constants'; + +type Props = { + onSelect: string => string, + selectedStarter: string, +}; + +type State = { + loading: boolean, + starters: Array, +}; + +/* +Gatsby starter selection dialog. +A starter object from starter.yml contains the following data: +[ + { + "url": "https://wonism.github.io/", + "repo": "https://github.com/wonism/gatsby-advanced-blog", + "description": "n/a", + "tags": [ + "Portfolio", + "Redux" + ], + "features": [ + "feature items" + ] + }, ... +] +*/ +class SelectStarterDialog extends PureComponent { + state = { + loading: true, + starters: [], + }; + + static getDerivedStateFromProps(nextProps, prevState) { + console.log('new props', nextProps, prevState); + return prevState; + } + + componentDidMount() { + fetch( + 'https://raw.githubusercontent.com/gatsbyjs/gatsby/master/docs/starters.yml' + ) + .then(response => response.text()) + .then(yamlText => { + const starters = yaml.safeLoad(yamlText); + + this.setState({ + starters, + }); + }); + } + + render() { + const { onSelect, selectedStarter } = this.props; + const { starters } = this.state; + console.log('render dialog', selectedStarter); + return ( + + + Please select a starter template for your new project. + + + + {selectedStarter} + {starters.slice(0, 10).map((starter, index) => ( + onSelect(starter.repo)} + > + {starter.repo} + {starter.description !== 'n/a' && ( + {starter.description} + )} + + ))} + + + {/* We could add Algolia here --> Setup at Algolia required */} + {/* + + console.log(value)} /> + ( +
onSelect(hit.name)}> + {hit.name} +
+ )} + /> +
*/} +
+
+ ); + } +} + +const ScrollContainer = styled(Scrollbars)` + min-height: 60vh; +`; + +const StarterList = styled.div``; +const StarterItem = styled.div` + border: 2px solid ${props => (props.selected ? 'red' : 'transparent')}; +`; + +const Wrapper = styled.div` + padding: 10px; +`; + +export default SelectStarterDialog; diff --git a/src/components/CreateNewProjectWizard/MainPane.js b/src/components/CreateNewProjectWizard/MainPane.js index 528b88e2..91cf2c45 100644 --- a/src/components/CreateNewProjectWizard/MainPane.js +++ b/src/components/CreateNewProjectWizard/MainPane.js @@ -2,15 +2,19 @@ import React, { PureComponent, Fragment } from 'react'; import { Motion, spring } from 'react-motion'; import styled from 'styled-components'; +import { toastr } from 'react-redux-toastr'; import FormField from '../FormField'; import FadeIn from '../FadeIn'; +import TextInput from '../TextInput'; // todo: move to SelectStarter Component +import FillButton from '../Button/FillButton'; // dito import ProjectName from './ProjectName'; import ProjectPath from './ProjectPath'; import SubmitButton from './SubmitButton'; import ProjectIconSelection from '../ProjectIconSelection'; import ProjectTypeSelection from '../ProjectTypeSelection'; +import SelectStarterDialog from './Gatsby/SelectStarterDialog'; import type { Field, Status } from './types'; import type { ProjectType } from '../../types'; @@ -19,6 +23,7 @@ type Props = { projectName: string, projectType: ?ProjectType, projectIcon: ?string, + projectStarter: ?string, activeField: ?Field, status: Status, currentStepIndex: number, @@ -29,9 +34,18 @@ type Props = { handleSubmit: () => Promise | void, }; -class MainPane extends PureComponent { +type State = { + gatsbyStarter: string, // temporary value during selection in selection toast +}; + +class MainPane extends PureComponent { + state = { + gatsbyStarter: '', + }; + handleFocusProjectName = () => this.props.focusField('projectName'); handleBlurProjectName = () => this.props.focusField(null); + handleFocusStarter = () => this.props.focusField('projectStarter'); updateProjectName = (projectName: string) => this.props.updateFieldValue('projectName', projectName); @@ -39,12 +53,132 @@ class MainPane extends PureComponent { this.props.updateFieldValue('projectType', projectType); updateProjectIcon = (projectIcon: string) => this.props.updateFieldValue('projectIcon', projectIcon); + updateGatsbyStarter = (selectedStarter: string) => + this.props.updateFieldValue('projectStarter', selectedStarter); + + static getDerivedStateFromProps(nextProps, prevState) { + console.log('MainPane new props', nextProps, prevState); + return prevState; + } + // Change method needed so we can dismiss the selection on close click of toastr + changeGatsbyStarter = (selectedStarter: string) => { + console.log('change starter', selectedStarter, this); + this.setState( + { + gatsbyStarter: selectedStarter, + }, + () => { + console.log('updated', this.state); + } + ); + }; + + projectSpecificSteps(projectType: ProjectType) { + const { activeField, projectStarter } = this.props; + switch (projectType) { + case 'gatsby': + return ( + + + {/* + this.updateProjectType(selectedProjectType) + } + /> */} + this.updateGatsbyStarter(evt.target.value)} + value={projectStarter} + onFocus={this.handleFocusStarter} + placeholder="Enter a starter" + /> + + toastr.confirm('Select starter', { + component: () => ( + + ), + okText: 'Use selected', + onOk: () => + this.updateGatsbyStarter(this.state.gatsbyStarter), + }) + } + > + Select Starter + + + + ); + default: + return null; + } + } + + renderConditionalSteps(currentStepIndex) { + const { activeField, projectType, projectIcon } = this.props; + const buildSteps = [ + // currentStepIndex > 0 -- > 1 + + + + this.updateProjectType(selectedProjectType) + } + /> + + , + this.projectSpecificSteps(projectType), // currentStepIndex > 1 + + + + + , + ]; + + const renderedSteps = buildSteps + .filter(step => !!step) + .slice(0, currentStepIndex); + + console.log('render steps', renderedSteps); + return { + lastIndex: projectType === 'gatsby' ? 3 : 2, //buildSteps.length, // Todo: Use buildSteps array to find last index + steps: renderedSteps, + }; + } + validateField(currentStepIndex) { + // todo: Refactor - Move buildsteps to component scope & use an array method to check current validation + // --> For now we're doing a different check for Gatsby flow + const { projectIcon, projectStarter, projectType } = this.props; + return projectType === 'gatsby' + ? (currentStepIndex > 0 && !projectType) || + (currentStepIndex > 1 && projectStarter === '') || + (currentStepIndex > 2 && !projectIcon) + : (currentStepIndex > 0 && !projectType) || + (currentStepIndex > 1 && !projectIcon); + } render() { const { projectName, - projectType, - projectIcon, activeField, currentStepIndex, hasBeenSubmitted, @@ -52,8 +186,10 @@ class MainPane extends PureComponent { handleSubmit, } = this.props; + const { lastIndex, steps } = this.renderConditionalSteps(currentStepIndex); return ( + {/*
{JSON.stringify(this.props, null, 2)}
*/} {({ offset }) => ( @@ -68,38 +204,7 @@ class MainPane extends PureComponent { /> - {currentStepIndex > 0 && ( - - - - this.updateProjectType(selectedProjectType) - } - /> - - - )} - - {currentStepIndex > 1 && ( - - - - - - )} + {steps} )} @@ -108,10 +213,9 @@ class MainPane extends PureComponent { isDisabled={ isProjectNameTaken || !projectName || - (currentStepIndex > 0 && !projectType) || - (currentStepIndex > 1 && !projectIcon) + this.validateField(currentStepIndex) } - readyToBeSubmitted={currentStepIndex >= 2} + readyToBeSubmitted={currentStepIndex >= lastIndex} hasBeenSubmitted={hasBeenSubmitted} onSubmit={handleSubmit} /> @@ -122,7 +226,7 @@ class MainPane extends PureComponent { } const Wrapper = styled.div` - height: 500px; + height: 80vh; will-change: transform; `; diff --git a/src/components/CreateNewProjectWizard/SummaryPane.js b/src/components/CreateNewProjectWizard/SummaryPane.js index f945be11..fb530682 100644 --- a/src/components/CreateNewProjectWizard/SummaryPane.js +++ b/src/components/CreateNewProjectWizard/SummaryPane.js @@ -185,6 +185,23 @@ class SummaryPane extends PureComponent {
); } + case 'projectStarter': { + // todo: why is a key needed on FadeIn? Was s3t. + // todo: should we rename projectStarter to be mores specific as this is Gatbsy only. + return ( + + + Gatsby Starter + + + Please enter a starter for your project (e.g. + gatsby-starter-blog). Later we'll have a starter search modal + here. + + + + ); + } default: throw new Error('Unrecognized `focusField`: ' + focusField); diff --git a/src/config/app.js b/src/config/app.js index 4dbd9df0..0a1f739b 100644 --- a/src/config/app.js +++ b/src/config/app.js @@ -3,5 +3,5 @@ module.exports = { PACKAGE_MANAGER: 'yarn', // Enable logging, if enabled all terminal responses are visible in the console (useful for debugging) - LOGGING: false, + LOGGING: true, }; diff --git a/src/config/project-types.js b/src/config/project-types.js index 2e59fe7a..84289bdc 100644 --- a/src/config/project-types.js +++ b/src/config/project-types.js @@ -17,7 +17,7 @@ const config: { }, }, create: { - args: (projectPath: string) => Array, + args: Array, }, }, } = { @@ -31,10 +31,10 @@ const config: { }, create: { // not sure if we need that nesting but I think there could be more to configure - args: projectPath => [ + args: [ // used for project creation previous getBuildInstructions 'create-react-app', - projectPath, + '$projectPath', ], }, }, @@ -46,11 +46,12 @@ const config: { }, create: { // not sure if we need that nesting but I think there could be more to configure - args: projectPath => [ + args: [ // used for project creation previous getBuildInstructions 'gatsby', 'new', - projectPath, // todo replace later with config variables like $projectPath - so we can remove the function. Also check if it's getting complicated. + '$projectPath', + '$projectStarter', ], }, }, @@ -60,9 +61,9 @@ const config: { args: ['run', 'dev', '-p', '$port'], }, create: { - args: projectPath => [ + args: [ 'github:awolf81/create-next-app', // later will be 'create-next-app' --> added a comment to the following issue https://github.com/segmentio/create-next-app/issues/30 - projectPath, + '$projectPath', ], }, }, diff --git a/src/global-styles.js b/src/global-styles.js index f2fe68f3..e32c2860 100644 --- a/src/global-styles.js +++ b/src/global-styles.js @@ -1,6 +1,7 @@ // @flow import { injectGlobal } from 'styled-components'; import 'react-tippy/dist/tippy.css'; +import 'react-redux-toastr/lib/css/react-redux-toastr.min.css'; import { COLORS } from './constants'; import './fonts.css'; import './base.css'; @@ -19,4 +20,12 @@ injectGlobal` body { background: ${COLORS.gray[50]}; } + + /* Modify top position of React-Redux-Toastr so it's closer to the upper edge - was top: 20% */ + div .rrt-confirm-holder .rrt-confirm { + top: 5%; + width: 70vw; + margin-left: 15vw; + left: 0; + } `; diff --git a/src/reducers/index.js b/src/reducers/index.js index a60bfec3..cdcab5db 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -1,5 +1,6 @@ // @flow import { combineReducers } from 'redux'; +import { reducer as toastrReducer } from 'react-redux-toastr'; // todo: Would be better to separate library reducers and add them before creating store. Not sure how to do that and if it's really needed. import appSettings from './app-settings.reducer'; import appLoaded from './app-loaded.reducer'; @@ -23,4 +24,5 @@ export default combineReducers({ onboardingStatus, paths, queue, + toastr: toastrReducer, }); diff --git a/src/sagas/task.saga.js b/src/sagas/task.saga.js index 588eabcb..0815c44a 100644 --- a/src/sagas/task.saga.js +++ b/src/sagas/task.saga.js @@ -30,6 +30,7 @@ import { PACKAGE_MANAGER_CMD, } from '../services/platform.service'; import { processLogger } from '../services/process-logger.service'; +import { substituteConfigVariables } from '../services/config-variables.service'; import type { Saga } from 'redux-saga'; import type { ChildProcess } from 'child_process'; @@ -404,42 +405,6 @@ const createStdioChannel = ( }); }; -// We're using "template" variables inside the project type configuration file (config/project-types.js) -// so with the following function we can replace the string $port with the real port number e.g. 3000 -// (see type VariableMap for used mapping strings) -export const substituteConfigVariables = ( - configObject: any, - variableMap: VariableMap -) => { - // e.g. $port inside args will be replaced with variable reference from variabeMap obj. {$port: port} - return Object.keys(configObject).reduce( - (config, key) => { - if (config[key] instanceof Array) { - // replace $port inside args array - config[key] = config[key].map(arg => variableMap[arg] || arg); - } else { - // check config[key] e.g. is {env: { PORT: '$port'} } - if (config[key] instanceof Object) { - // config[key] = {PORT: '$port'}, key = 'env' - config[key] = Object.keys(config[key]).reduce( - (newObj, nestedKey) => { - // use replacement value if available - newObj[nestedKey] = - variableMap[newObj[nestedKey]] || newObj[nestedKey]; - return newObj; - }, - { ...config[key] } - ); - } - } - // todo: add top level substitution - not used yet but maybe needed later e.g. { env: $port } won't be replaced. - // Bad example but just to have it as reminder. - return config; - }, - { ...configObject } - ); -}; - export const getDevServerCommand = ( task: Task, projectType: ProjectType, diff --git a/src/services/config-variables.service.js b/src/services/config-variables.service.js new file mode 100644 index 00000000..947d7c87 --- /dev/null +++ b/src/services/config-variables.service.js @@ -0,0 +1,36 @@ +// @flow +// We're using "template" variables inside the project type configuration file (config/project-types.js) +// so with the following function we can replace the string $port with the real port number e.g. 3000 +// (see type VariableMap for used mapping strings) +export const substituteConfigVariables = ( + configObject: any, + variableMap: VariableMap +) => { + // e.g. $port inside args will be replaced with variable reference from variabeMap obj. {$port: port} + return Object.keys(configObject).reduce( + (config, key) => { + if (config[key] instanceof Array) { + // replace $port inside args array + config[key] = config[key].map(arg => variableMap[arg] || arg); + } else { + // check config[key] e.g. is {env: { PORT: '$port'} } + if (config[key] instanceof Object) { + // config[key] = {PORT: '$port'}, key = 'env' + config[key] = Object.keys(config[key]).reduce( + (newObj, nestedKey) => { + // use replacement value if available + newObj[nestedKey] = + variableMap[newObj[nestedKey]] || newObj[nestedKey]; + return newObj; + }, + { ...config[key] } + ); + } + } + // todo: add top level substitution - not used yet but maybe needed later e.g. { env: $port } won't be replaced. + // Bad example but just to have it as reminder. + return config; + }, + { ...configObject } + ); +}; diff --git a/src/services/create-project.service.js b/src/services/create-project.service.js index 7be6d898..be2e760f 100644 --- a/src/services/create-project.service.js +++ b/src/services/create-project.service.js @@ -7,6 +7,7 @@ import * as path from 'path'; import * as uuid from 'uuid/v1'; import projectConfigs from '../config/project-types'; import { processLogger } from './process-logger.service'; +import { substituteConfigVariables } from './config-variables.service'; import { COLORS } from '../constants'; @@ -28,6 +29,11 @@ type ProjectInfo = { projectName: string, projectType: ProjectType, projectIcon: string, + projectStarter?: string, +}; + +type BuildOptions = { + projectStarter: ?string, // used for gatsby }; export const checkIfProjectExists = (dir: string, projectName: string) => @@ -50,7 +56,7 @@ export const checkIfProjectExists = (dir: string, projectName: string) => * fire multiple times, to handle updates mid-creation. Maybe an observable? */ export default ( - { projectName, projectType, projectIcon }: ProjectInfo, + { projectName, projectType, projectIcon, projectStarter }: ProjectInfo, projectHomePath: string, onStatusUpdate: (update: string) => void, onError: (err: string) => void, @@ -75,7 +81,16 @@ export default ( // To support cross platform with slashes and escapes const projectPath = path.join(projectHomePath, projectDirectoryName); - const [instruction, ...args] = getBuildInstructions(projectType, projectPath); + // Add starter for Gatsby. Check is optional as it can't be entered in the build steps for other project types. + const buildOptions = projectType === 'gatsby' && { + projectStarter, + }; + + const [instruction, ...args] = getBuildInstructions( + projectType, + projectPath, + buildOptions + ); const process = childProcess.spawn(instruction, args, { env: getBaseProjectEnvironment(projectPath), @@ -185,7 +200,8 @@ export const getColorForProject = (projectName: string) => { export const getBuildInstructions = ( projectType: ProjectType, - projectPath: string + projectPath: string, + options: BuildOptions ) => { // For Windows Support // Windows tries to run command as a script rather than on a cmd @@ -195,5 +211,17 @@ export const getBuildInstructions = ( throw new Error('Unrecognized project type: ' + projectType); } - return [command, ...projectConfigs[projectType].create.args(projectPath)]; + const createCommand = substituteConfigVariables( + projectConfigs[projectType].create, + { + $projectPath: projectPath, + $projectStarter: options.projectStarter, + } + ); + console.log('creact command', createCommand, { + $projectPath: projectPath, + $projectStarter: options.projectStarter, + }); + + return [command, ...createCommand.args]; }; diff --git a/yarn.lock b/yarn.lock index f030ff53..87d2f8a6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3597,7 +3597,7 @@ class-utils@^0.3.5: isobject "^3.0.0" static-extend "^0.1.1" -classnames@^2.2.5: +classnames@^2.2.3, classnames@^2.2.5: version "2.2.6" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce" integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q== @@ -5520,6 +5520,11 @@ eventemitter3@1.x.x: resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-1.2.0.tgz#1c86991d816ad1e504750e73874224ecf3bec508" integrity sha1-HIaZHYFq0eUEdQ5zh0Ik7PO+xQg= +eventemitter3@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-2.0.3.tgz#b5e1079b59fb5e1ba2771c0a993be060a58c99ba" + integrity sha1-teEHm1n7XhuidxwKmTvgYKWMmbo= + eventemitter3@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.0.tgz#090b4d6cdbd645ed10bf750d4b5407942d7ba163" @@ -8108,7 +8113,7 @@ js-tokens@^3.0.0, js-tokens@^3.0.2: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -js-yaml@^3.12.0: +js-yaml@3.12.0, js-yaml@^3.12.0: version "3.12.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.0.tgz#eaed656ec8344f10f527c6bfa1b6e2244de167d1" integrity sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A== @@ -9323,6 +9328,11 @@ node-emoji@^1.0.4: dependencies: lodash.toarray "^4.4.0" +node-fetch@2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.3.0.tgz#1a1d940bbfb916a1d3e0219f037e89e71f8c5fa5" + integrity sha512-MOd8pV3fxENbryESLgVIeaGKrdl+uaYhCSSVkjeOb/31/njTpcis5aWfdqgNlHIrKOLRbMnfPINPOML2CIFeXA== + node-fetch@^1.0.1: version "1.7.3" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef" @@ -11078,6 +11088,14 @@ react-pure-render@^1.0.2: resolved "https://registry.yarnpkg.com/react-pure-render/-/react-pure-render-1.0.2.tgz#9d8a928c7f2c37513c2d064e57b3e3c356e9fabb" integrity sha1-nYqSjH8sN1E8LQZOV7Pjw1bp+rs= +react-redux-toastr@7.4.1: + version "7.4.1" + resolved "https://registry.yarnpkg.com/react-redux-toastr/-/react-redux-toastr-7.4.1.tgz#6351dc075c5b6f45df145d711bf40d6ed4065634" + integrity sha512-U7Z7A217yGgkjZE87MzHbLIlYV0OvZ3HiBRfUWoZF1Y4kvY6JAKn8EkAfPy40ZzoLiH2GpCq9Ac1U7v8on7PvQ== + dependencies: + classnames "^2.2.3" + eventemitter3 "^2.0.3" + react-redux@5.0.7: version "5.0.7" resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.0.7.tgz#0dc1076d9afb4670f993ffaef44b8f8c1155a4c8" From a82bf3ae253da4221a0a2b52e70e1f85d95d720f Mon Sep 17 00:00:00 2001 From: AWolf81 Date: Mon, 19 Nov 2018 09:40:44 +0100 Subject: [PATCH 02/23] WIP: Fix flow typing --- .../CreateNewProjectWizard/MainPane.js | 8 ++++---- .../CreateNewProjectWizard/types.js | 6 +++++- src/sagas/task.saga.js | 6 +----- src/services/config-variables.service.js | 8 ++++++++ src/services/create-project.service.js | 19 +++++++++---------- 5 files changed, 27 insertions(+), 20 deletions(-) diff --git a/src/components/CreateNewProjectWizard/MainPane.js b/src/components/CreateNewProjectWizard/MainPane.js index 91cf2c45..4c9deadd 100644 --- a/src/components/CreateNewProjectWizard/MainPane.js +++ b/src/components/CreateNewProjectWizard/MainPane.js @@ -121,9 +121,9 @@ class MainPane extends PureComponent { } } - renderConditionalSteps(currentStepIndex) { + renderConditionalSteps(currentStepIndex: number) { const { activeField, projectType, projectIcon } = this.props; - const buildSteps = [ + const buildSteps: Array = [ // currentStepIndex > 0 -- > 1 { , ]; - const renderedSteps = buildSteps + const renderedSteps: Array = buildSteps .filter(step => !!step) .slice(0, currentStepIndex); @@ -165,7 +165,7 @@ class MainPane extends PureComponent { steps: renderedSteps, }; } - validateField(currentStepIndex) { + validateField(currentStepIndex: number) { // todo: Refactor - Move buildsteps to component scope & use an array method to check current validation // --> For now we're doing a different check for Gatsby flow const { projectIcon, projectStarter, projectType } = this.props; diff --git a/src/components/CreateNewProjectWizard/types.js b/src/components/CreateNewProjectWizard/types.js index 1a10d7c1..eb51a90e 100644 --- a/src/components/CreateNewProjectWizard/types.js +++ b/src/components/CreateNewProjectWizard/types.js @@ -1,6 +1,10 @@ // @flow -export type Field = 'projectName' | 'projectType' | 'projectIcon'; +export type Field = + | 'projectName' + | 'projectType' + | 'projectIcon' + | 'projectStarter'; export type BuildStep = | 'installingCliTool' | 'creatingProjectDirectory' diff --git a/src/sagas/task.saga.js b/src/sagas/task.saga.js index 0815c44a..c154e05c 100644 --- a/src/sagas/task.saga.js +++ b/src/sagas/task.saga.js @@ -36,14 +36,10 @@ import type { Saga } from 'redux-saga'; import type { ChildProcess } from 'child_process'; import type { Task, ProjectType } from '../types'; import type { ReturnType } from '../actions/types'; +import type { VariableMap } from '../services/config-variables.service'; const { dialog } = remote; -// Mapping type for config template variables '$port' -export type VariableMap = { - $port: string, -}; - const chalk = new chalkRaw.constructor({ level: 3 }); export function* handleLaunchDevServer({ diff --git a/src/services/config-variables.service.js b/src/services/config-variables.service.js index 947d7c87..66786cb6 100644 --- a/src/services/config-variables.service.js +++ b/src/services/config-variables.service.js @@ -1,4 +1,12 @@ // @flow + +// Mapping type for config template variables '$port' +export type VariableMap = { + $port?: string, + $projectPath?: string, + $projectStarter?: ?string, +}; + // We're using "template" variables inside the project type configuration file (config/project-types.js) // so with the following function we can replace the string $port with the real port number e.g. 3000 // (see type VariableMap for used mapping strings) diff --git a/src/services/create-project.service.js b/src/services/create-project.service.js index be2e760f..43851144 100644 --- a/src/services/create-project.service.js +++ b/src/services/create-project.service.js @@ -33,7 +33,7 @@ type ProjectInfo = { }; type BuildOptions = { - projectStarter: ?string, // used for gatsby + projectStarter?: string, // used for gatsby }; export const checkIfProjectExists = (dir: string, projectName: string) => @@ -82,9 +82,12 @@ export default ( const projectPath = path.join(projectHomePath, projectDirectoryName); // Add starter for Gatsby. Check is optional as it can't be entered in the build steps for other project types. - const buildOptions = projectType === 'gatsby' && { - projectStarter, - }; + const buildOptions = + projectType === 'gatsby' + ? { + projectStarter, + } + : null; const [instruction, ...args] = getBuildInstructions( projectType, @@ -201,7 +204,7 @@ export const getColorForProject = (projectName: string) => { export const getBuildInstructions = ( projectType: ProjectType, projectPath: string, - options: BuildOptions + options: ?BuildOptions ) => { // For Windows Support // Windows tries to run command as a script rather than on a cmd @@ -215,13 +218,9 @@ export const getBuildInstructions = ( projectConfigs[projectType].create, { $projectPath: projectPath, - $projectStarter: options.projectStarter, + $projectStarter: (options && options.projectStarter) || '', } ); - console.log('creact command', createCommand, { - $projectPath: projectPath, - $projectStarter: options.projectStarter, - }); return [command, ...createCommand.args]; }; From 559fa13e8d9377c712b668529ec9fc1d6fd96170 Mon Sep 17 00:00:00 2001 From: AWolf81 Date: Mon, 19 Nov 2018 23:10:40 +0100 Subject: [PATCH 03/23] added preview in codesandbox. --- .../Gatsby/SelectStarterDialog.js | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/components/CreateNewProjectWizard/Gatsby/SelectStarterDialog.js b/src/components/CreateNewProjectWizard/Gatsby/SelectStarterDialog.js index d266099f..32463734 100644 --- a/src/components/CreateNewProjectWizard/Gatsby/SelectStarterDialog.js +++ b/src/components/CreateNewProjectWizard/Gatsby/SelectStarterDialog.js @@ -4,6 +4,8 @@ import fetch from 'node-fetch'; // Note: This is using net.request from Node. Br import styled from 'styled-components'; import yaml from 'js-yaml'; import Scrollbars from 'react-custom-scrollbars'; +import ExternalLink from '../../ExternalLink'; + // import { SearchBox } from 'react-instantsearch/dom'; // import { // InstantSearch, @@ -17,7 +19,7 @@ import Heading from '../../Heading'; // import { ALGOLIA_KEYS } from '../../../constants'; type Props = { - onSelect: string => string, + onSelect: string => ?string, selectedStarter: string, }; @@ -50,7 +52,7 @@ class SelectStarterDialog extends PureComponent { starters: [], }; - static getDerivedStateFromProps(nextProps, prevState) { + static getDerivedStateFromProps(nextProps: Props, prevState: State) { console.log('new props', nextProps, prevState); return prevState; } @@ -69,6 +71,16 @@ class SelectStarterDialog extends PureComponent { }); } + prepareUrlForCodesandbox(repoUrl) { + // Remove http protocol + const sandboxUrl = `https://codesandbox.io/s/${repoUrl.replace( + /(^\w+:|^)\/\//, + '' + )}`; + // Remove .com from github.com --> to have /s/github/repo + return sandboxUrl.replace(/\.com/, ''); + } + render() { const { onSelect, selectedStarter } = this.props; const { starters } = this.state; @@ -91,6 +103,11 @@ class SelectStarterDialog extends PureComponent { {starter.description !== 'n/a' && ( {starter.description} )} + + Preview in Codesandbox + ))} From cb92fc06b393941aff88d13533e169e1acf1d7e9 Mon Sep 17 00:00:00 2001 From: AWolf81 Date: Mon, 19 Nov 2018 23:11:09 +0100 Subject: [PATCH 04/23] fixed flow types --- src/components/CreateNewProjectWizard/BuildPane.js | 5 +++-- .../CreateNewProjectWizard/CreateNewProjectWizard.js | 10 +++------- src/components/CreateNewProjectWizard/MainPane.js | 12 ++++++------ src/services/create-project.service.js | 4 ++-- 4 files changed, 14 insertions(+), 17 deletions(-) diff --git a/src/components/CreateNewProjectWizard/BuildPane.js b/src/components/CreateNewProjectWizard/BuildPane.js index 3db6eb24..07d51c62 100644 --- a/src/components/CreateNewProjectWizard/BuildPane.js +++ b/src/components/CreateNewProjectWizard/BuildPane.js @@ -36,6 +36,7 @@ type Props = { projectName: string, projectType: ?ProjectType, projectIcon: ?string, + projectStarter: string, status: Status, projectHomePath: string, handleCompleteBuild: (project: ProjectInternal) => void, @@ -98,8 +99,8 @@ class BuildPane extends PureComponent { } // Add url to starter if not passed - // Todo: We need error handling to show a notification that it failed to use the starter - // --> Just needed if we allow the user to enter an url to a starter. + // Todo: We need error handling to show a notification that it failed to use the starter (e.g. starter doesn't exists or wrong url/name) + // --> Probably just needed if we allow the user to enter an url to a starter. const projectStarter = !projectStarterInput.includes('http') ? 'https://github.com/gatsbyjs/' + projectStarterInput : projectStarterInput; diff --git a/src/components/CreateNewProjectWizard/CreateNewProjectWizard.js b/src/components/CreateNewProjectWizard/CreateNewProjectWizard.js index 5e8f3e7a..36fd91f2 100644 --- a/src/components/CreateNewProjectWizard/CreateNewProjectWizard.js +++ b/src/components/CreateNewProjectWizard/CreateNewProjectWizard.js @@ -38,6 +38,7 @@ type Props = { settings: AppSettings, projects: { [projectId: string]: ProjectInternal }, projectHomePath: string, + projectStarter: string, isVisible: boolean, isOnboardingCompleted: boolean, addProject: Dispatch, @@ -49,7 +50,7 @@ type State = { projectName: string, projectType: ?ProjectType, projectIcon: ?string, - projectStarter: ?string, + projectStarter: string, activeField: ?Field, settings: ?AppSettings, status: Status, @@ -164,11 +165,7 @@ class CreateNewProjectWizard extends PureComponent { }; finishBuilding = (project: ProjectInternal) => { - const { - isOnboardingCompleted, - projectHomePath, - projectStarter, - } = this.props; + const { isOnboardingCompleted, projectHomePath } = this.props; const { projectType } = this.state; // Should be impossible @@ -183,7 +180,6 @@ class CreateNewProjectWizard extends PureComponent { project, projectHomePath, projectType, - projectStarter, // todo: check project reducer, if it's available on state isOnboardingCompleted ); diff --git a/src/components/CreateNewProjectWizard/MainPane.js b/src/components/CreateNewProjectWizard/MainPane.js index 4c9deadd..b9c1245c 100644 --- a/src/components/CreateNewProjectWizard/MainPane.js +++ b/src/components/CreateNewProjectWizard/MainPane.js @@ -56,7 +56,7 @@ class MainPane extends PureComponent { updateGatsbyStarter = (selectedStarter: string) => this.props.updateFieldValue('projectStarter', selectedStarter); - static getDerivedStateFromProps(nextProps, prevState) { + static getDerivedStateFromProps(nextProps: Props, prevState: State) { console.log('MainPane new props', nextProps, prevState); return prevState; } @@ -74,8 +74,8 @@ class MainPane extends PureComponent { ); }; - projectSpecificSteps(projectType: ProjectType) { - const { activeField, projectStarter } = this.props; + projectSpecificSteps() { + const { activeField, projectType, projectStarter } = this.props; switch (projectType) { case 'gatsby': return ( @@ -123,7 +123,7 @@ class MainPane extends PureComponent { renderConditionalSteps(currentStepIndex: number) { const { activeField, projectType, projectIcon } = this.props; - const buildSteps: Array = [ + const buildSteps: Array = [ // currentStepIndex > 0 -- > 1 { /> , - this.projectSpecificSteps(projectType), // currentStepIndex > 1 + this.projectSpecificSteps(), // currentStepIndex > 1 { , ]; - const renderedSteps: Array = buildSteps + const renderedSteps: Array = buildSteps .filter(step => !!step) .slice(0, currentStepIndex); diff --git a/src/services/create-project.service.js b/src/services/create-project.service.js index 43851144..961ffabc 100644 --- a/src/services/create-project.service.js +++ b/src/services/create-project.service.js @@ -29,11 +29,11 @@ type ProjectInfo = { projectName: string, projectType: ProjectType, projectIcon: string, - projectStarter?: string, + projectStarter: ?string, }; type BuildOptions = { - projectStarter?: string, // used for gatsby + projectStarter: ?string, // used for gatsby }; export const checkIfProjectExists = (dir: string, projectName: string) => From 14e37fc534aa3b6299033c5d04859c903c62f981 Mon Sep 17 00:00:00 2001 From: AWolf81 Date: Mon, 19 Nov 2018 23:19:18 +0100 Subject: [PATCH 05/23] fixed test --- .../Gatsby/SelectStarterDialog.js | 2 +- .../CreateNewProjectWizard/MainPane.js | 2 +- src/services/create-project.service.test.js | 16 ++++++++++++++-- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/components/CreateNewProjectWizard/Gatsby/SelectStarterDialog.js b/src/components/CreateNewProjectWizard/Gatsby/SelectStarterDialog.js index 32463734..1eea4c8f 100644 --- a/src/components/CreateNewProjectWizard/Gatsby/SelectStarterDialog.js +++ b/src/components/CreateNewProjectWizard/Gatsby/SelectStarterDialog.js @@ -71,7 +71,7 @@ class SelectStarterDialog extends PureComponent { }); } - prepareUrlForCodesandbox(repoUrl) { + prepareUrlForCodesandbox(repoUrl: string) { // Remove http protocol const sandboxUrl = `https://codesandbox.io/s/${repoUrl.replace( /(^\w+:|^)\/\//, diff --git a/src/components/CreateNewProjectWizard/MainPane.js b/src/components/CreateNewProjectWizard/MainPane.js index b9c1245c..b96bcbae 100644 --- a/src/components/CreateNewProjectWizard/MainPane.js +++ b/src/components/CreateNewProjectWizard/MainPane.js @@ -155,7 +155,7 @@ class MainPane extends PureComponent { , ]; - const renderedSteps: Array = buildSteps + const renderedSteps: Array = buildSteps .filter(step => !!step) .slice(0, currentStepIndex); diff --git a/src/services/create-project.service.test.js b/src/services/create-project.service.test.js index 59f15862..b7e389e2 100644 --- a/src/services/create-project.service.test.js +++ b/src/services/create-project.service.test.js @@ -5,6 +5,8 @@ import { getBuildInstructions, } from './create-project.service'; +import { substituteConfigVariables } from './config-variables.service'; + jest.mock('os', () => ({ homedir: jest.fn(), platform: () => process.platform, @@ -35,8 +37,18 @@ describe('getBuildInstructions', () => { }); it('should return the build instructions for a Gatsby project', () => { - const expectedOutput = ['npx', 'gatsby', 'new', path]; - expect(getBuildInstructions('gatsby', path)).toEqual(expectedOutput); + const expectedOutput = [ + 'npx', + 'gatsby', + 'new', + path, + 'gatsby-starter-blog', + ]; + expect( + getBuildInstructions('gatsby', path, { + projectStarter: 'gatsby-starter-blog', + }) + ).toEqual(expectedOutput); }); it('should throw an exception when passed an unknown project type', () => { From 9c1a18212a0047d92a2d6bec19d4370b4d9bf2db Mon Sep 17 00:00:00 2001 From: AWolf81 Date: Tue, 20 Nov 2018 23:41:44 +0100 Subject: [PATCH 06/23] removed toastr & used modal --- package.json | 1 - src/actions/index.js | 10 ++ .../AddDependencySearchResult.js | 7 +- src/components/App/App.js | 2 - .../CreateNewProjectWizard.js | 79 +++++----- .../Gatsby/ProjectStarterSelection.js | 83 +++++++++++ .../Gatsby/SelectStarterDialog.js | 136 ++++++++++++------ .../CreateNewProjectWizard/MainPane.js | 48 +------ .../CreateNewProjectWizard/SummaryPane.js | 10 +- src/components/Divider/Divider.js | 13 ++ src/components/Divider/index.js | 2 + .../TextInputWithButton.js | 87 +++++++++++ src/components/TextInputWithButton/index.js | 2 + src/global-styles.js | 9 -- src/reducers/index.js | 2 - src/reducers/modal.reducer.js | 12 +- yarn.lock | 15 +- 17 files changed, 361 insertions(+), 157 deletions(-) create mode 100644 src/components/CreateNewProjectWizard/Gatsby/ProjectStarterSelection.js create mode 100644 src/components/Divider/Divider.js create mode 100644 src/components/Divider/index.js create mode 100644 src/components/TextInputWithButton/TextInputWithButton.js create mode 100644 src/components/TextInputWithButton/index.js diff --git a/package.json b/package.json index 6e1627d2..1434602a 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,6 @@ "node-fetch": "2.3.0", "ps-tree": "1.1.0", "react-custom-scrollbars": "4.2.1", - "react-redux-toastr": "7.4.1", "rimraf": "2.6.2", "yarn": "1.9.2" }, diff --git a/src/actions/index.js b/src/actions/index.js index 57674f51..672f1eaf 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -23,6 +23,8 @@ export const CREATE_NEW_PROJECT_CANCEL = 'CREATE_NEW_PROJECT_CANCEL'; export const CREATE_NEW_PROJECT_FINISH = 'CREATE_NEW_PROJECT_FINISH'; export const ADD_PROJECT = 'ADD_PROJECT'; export const SHOW_MODAL = 'SHOW_MODAL'; +export const SHOW_STARTER_SELECTION = 'SHOW_STARTER_SELECTION'; +export const HIDE_STARTER_SELECTION = 'HIDE_STARTER_SELECTION'; export const CHANGE_PROJECT_HOME_PATH = 'CHANGE_PROJECT_HOME_PATH'; export const HIDE_MODAL = 'HIDE_MODAL'; export const DISMISS_SIDEBAR_INTRO = 'DISMISS_SIDEBAR_INTRO'; @@ -417,4 +419,12 @@ export const showResetStatePrompt = () => ({ type: SHOW_RESET_STATE_PROMPT, }); +export const showStarterSelectionModal = () => ({ + type: SHOW_STARTER_SELECTION, +}); + +export const hideStarterSelectionModal = () => ({ + type: HIDE_STARTER_SELECTION, +}); + export const resetAllState = () => ({ type: RESET_ALL_STATE }); diff --git a/src/components/AddDependencySearchResult/AddDependencySearchResult.js b/src/components/AddDependencySearchResult/AddDependencySearchResult.js index 25bb515d..a1489700 100644 --- a/src/components/AddDependencySearchResult/AddDependencySearchResult.js +++ b/src/components/AddDependencySearchResult/AddDependencySearchResult.js @@ -15,6 +15,7 @@ import { } from '../../reducers/projects.reducer'; import { COLORS } from '../../constants'; +import Divider from '../Divider'; import Spacer from '../Spacer'; import Spinner from '../Spinner'; import ExternalLink from '../ExternalLink'; @@ -211,12 +212,6 @@ const Description = styled.div` margin-right: 120px; `; -const Divider = styled.div` - width: 100%; - height: 1px; - background: ${COLORS.gray[100]}; -`; - const StatsItemHighlight = styled.span` font-weight: 600; -webkit-font-smoothing: antialiased; diff --git a/src/components/App/App.js b/src/components/App/App.js index 3bd4e648..b9030c81 100644 --- a/src/components/App/App.js +++ b/src/components/App/App.js @@ -2,7 +2,6 @@ import React, { PureComponent, Fragment } from 'react'; import { connect } from 'react-redux'; import styled, { keyframes } from 'styled-components'; -import ReduxToastr from 'react-redux-toastr'; import { COLORS } from '../../constants'; import { getSelectedProjectId } from '../../reducers/projects.reducer'; @@ -44,7 +43,6 @@ class App extends PureComponent { - ) } diff --git a/src/components/CreateNewProjectWizard/CreateNewProjectWizard.js b/src/components/CreateNewProjectWizard/CreateNewProjectWizard.js index 36fd91f2..e278041c 100644 --- a/src/components/CreateNewProjectWizard/CreateNewProjectWizard.js +++ b/src/components/CreateNewProjectWizard/CreateNewProjectWizard.js @@ -1,5 +1,5 @@ // @flow -import React, { PureComponent } from 'react'; +import React, { Fragment, PureComponent } from 'react'; import { connect } from 'react-redux'; import Transition from 'react-transition-group/Transition'; import { remote } from 'electron'; @@ -20,6 +20,7 @@ import Debounced from '../Debounced'; import MainPane from './MainPane'; import SummaryPane from './SummaryPane'; import BuildPane from './BuildPane'; +import SelectStarterDialog from './Gatsby/SelectStarterDialog'; import type { Field, Status, Step } from './types'; @@ -216,42 +217,48 @@ class CreateNewProjectWizard extends PureComponent { return ( {transitionState => ( - - + + + + } + rightPane={ + - - } - rightPane={ - - } - backface={ - - } - /> + } + backface={ + + } + /> + + )} ); @@ -261,7 +268,7 @@ class CreateNewProjectWizard extends PureComponent { const mapStateToProps = state => ({ projects: getById(state), projectHomePath: getDefaultProjectPath(state), - isVisible: state.modal === 'new-project-wizard', + isVisible: state.modal && state.modal.includes('new-project-wizard'), isOnboardingCompleted: getOnboardingCompleted(state), settings: getAppSettings(state), }); diff --git a/src/components/CreateNewProjectWizard/Gatsby/ProjectStarterSelection.js b/src/components/CreateNewProjectWizard/Gatsby/ProjectStarterSelection.js new file mode 100644 index 00000000..2f60e33b --- /dev/null +++ b/src/components/CreateNewProjectWizard/Gatsby/ProjectStarterSelection.js @@ -0,0 +1,83 @@ +// @flow +import React, { PureComponent } from 'react'; +import { connect } from 'react-redux'; +// import styled from 'styled-components'; + +// import { COLORS } from '../../constants'; + +import TextInputWithButton from '../../TextInputWithButton'; + +import * as actions from '../../../actions'; +import type { Dispatch } from '../../../actions/types'; + +type State = { + gatsbyStarter: string, +}; + +type Props = { + projectStarter: string, + isFocused: boolean, + onSelect: string => void, + onFocus: () => void, + showStarterSelection: Dispatch, +}; + +class ProjectStarter extends PureComponent { + state = { + gatsbyStarter: '', + }; + + static getDerivedStateFromProps(nextProps: Props, prevState: State) { + return { + gatsbyStarter: nextProps.projectStarter, + }; + } + + // Change method needed so we can dismiss the selection on close click of toastr + changeGatsbyStarter = (selectedStarter: string) => { + console.log('change starter', selectedStarter, this); + this.setState( + { + gatsbyStarter: selectedStarter, + }, + () => { + console.log('updated', this.state); + } + ); + }; + + handleSelect = () => { + this.props.onSelect(this.state.gatsbyStarter); + + // clear temporary state value + // this.setState({ + // gatsbyStarter: '', + // }); + }; + + render() { + const { + projectStarter, + isFocused, + onSelect, + showStarterSelection, + } = this.props; + + return ( + + ); + } +} +const mapDispatchToProps = { + showStarterSelection: actions.showStarterSelectionModal, +}; + +export default connect( + null, + mapDispatchToProps +)(ProjectStarter); diff --git a/src/components/CreateNewProjectWizard/Gatsby/SelectStarterDialog.js b/src/components/CreateNewProjectWizard/Gatsby/SelectStarterDialog.js index 1eea4c8f..51b1996b 100644 --- a/src/components/CreateNewProjectWizard/Gatsby/SelectStarterDialog.js +++ b/src/components/CreateNewProjectWizard/Gatsby/SelectStarterDialog.js @@ -1,9 +1,13 @@ // @flow -import React, { PureComponent } from 'react'; +import React, { Fragment, PureComponent } from 'react'; +import { connect } from 'react-redux'; import fetch from 'node-fetch'; // Note: This is using net.request from Node. Browser fetch throws CORS error. import styled from 'styled-components'; import yaml from 'js-yaml'; import Scrollbars from 'react-custom-scrollbars'; + +import Divider from '../../Divider'; +import Spacer from '../../Spacer'; import ExternalLink from '../../ExternalLink'; // import { SearchBox } from 'react-instantsearch/dom'; @@ -13,19 +17,29 @@ import ExternalLink from '../../ExternalLink'; // Configure, // } from 'react-instantsearch/dom'; +import FillButton from '../../Button/FillButton'; import Paragraph from '../../Paragraph'; import Heading from '../../Heading'; +import Modal from '../../Modal'; +import ModalHeader from '../../ModalHeader'; + +import * as actions from '../../../actions'; // import { ALGOLIA_KEYS } from '../../../constants'; +import { COLORS } from '../../../constants'; +import type { Dispatch } from '../../../actions/types'; type Props = { - onSelect: string => ?string, + updateFieldValue: (string, string) => void, selectedStarter: string, + isVisible: boolean, + hideModal: Dispatch, }; type State = { loading: boolean, starters: Array, + selectedStarterInModal: string, }; /* @@ -50,11 +64,13 @@ class SelectStarterDialog extends PureComponent { state = { loading: true, starters: [], + selectedStarterInModal: '', }; static getDerivedStateFromProps(nextProps: Props, prevState: State) { - console.log('new props', nextProps, prevState); - return prevState; + return { + selectedStarterInModal: nextProps.selectedStarter, + }; } componentDidMount() { @@ -66,6 +82,7 @@ class SelectStarterDialog extends PureComponent { const starters = yaml.safeLoad(yamlText); this.setState({ + loading: false, starters, }); }); @@ -81,39 +98,59 @@ class SelectStarterDialog extends PureComponent { return sandboxUrl.replace(/\.com/, ''); } + handleDialogOK = () => { + this.props.updateFieldValue( + 'projectStarter', + this.state.selectedStarterInModal + ); + this.props.hideModal(); + }; + + setStarter = starter => { + this.setState({ + selectedStarterInModal: starter, + }); + }; + render() { - const { onSelect, selectedStarter } = this.props; - const { starters } = this.state; - console.log('render dialog', selectedStarter); + const { isVisible, hideModal } = this.props; + const { starters, selectedStarterInModal } = this.state; + console.log('render dialog', selectedStarterInModal); return ( - - - Please select a starter template for your new project. - - - - {selectedStarter} - {starters.slice(0, 10).map((starter, index) => ( - onSelect(starter.repo)} - > - {starter.repo} - {starter.description !== 'n/a' && ( - {starter.description} - )} - - Preview in Codesandbox - - - ))} - - - {/* We could add Algolia here --> Setup at Algolia required */} - {/* + + + + + + Please select a starter template for your new project. + + + + {starters.slice(0, 10).map((starter, index) => ( + + this.setStarter(starter.repo)} + > + {starter.repo} + {starter.description !== 'n/a' && ( + {starter.description} + )} + + Preview in Codesandbox + + + + + + ))} + + + {/* We could add Algolia here --> Setup at Algolia required */} + {/* { )} /> */} - - + + Use selected + Cancel + + ); } } @@ -146,11 +186,25 @@ const ScrollContainer = styled(Scrollbars)` const StarterList = styled.div``; const StarterItem = styled.div` - border: 2px solid ${props => (props.selected ? 'red' : 'transparent')}; + cursor: pointer; + padding: 8px 10px; + border-radius: 6px; + border: 2px solid + ${props => (props.selected ? COLORS.purple[500] : 'transparent')}; `; -const Wrapper = styled.div` - padding: 10px; +const MainContent = styled.div` + padding: 25px; `; +const mapStateToProps = state => { + return { + isVisible: state.modal === 'new-project-wizard/select-starter', + }; +}; -export default SelectStarterDialog; +export default connect( + mapStateToProps, + { + hideModal: actions.hideStarterSelectionModal, + } +)(SelectStarterDialog); diff --git a/src/components/CreateNewProjectWizard/MainPane.js b/src/components/CreateNewProjectWizard/MainPane.js index b96bcbae..55819cf1 100644 --- a/src/components/CreateNewProjectWizard/MainPane.js +++ b/src/components/CreateNewProjectWizard/MainPane.js @@ -2,7 +2,6 @@ import React, { PureComponent, Fragment } from 'react'; import { Motion, spring } from 'react-motion'; import styled from 'styled-components'; -import { toastr } from 'react-redux-toastr'; import FormField from '../FormField'; import FadeIn from '../FadeIn'; @@ -14,7 +13,7 @@ import ProjectPath from './ProjectPath'; import SubmitButton from './SubmitButton'; import ProjectIconSelection from '../ProjectIconSelection'; import ProjectTypeSelection from '../ProjectTypeSelection'; -import SelectStarterDialog from './Gatsby/SelectStarterDialog'; +import ProjectStarterSelection from './Gatsby/ProjectStarterSelection'; import type { Field, Status } from './types'; import type { ProjectType } from '../../types'; @@ -61,19 +60,6 @@ class MainPane extends PureComponent { return prevState; } - // Change method needed so we can dismiss the selection on close click of toastr - changeGatsbyStarter = (selectedStarter: string) => { - console.log('change starter', selectedStarter, this); - this.setState( - { - gatsbyStarter: selectedStarter, - }, - () => { - console.log('updated', this.state); - } - ); - }; - projectSpecificSteps() { const { activeField, projectType, projectStarter } = this.props; switch (projectType) { @@ -84,35 +70,11 @@ class MainPane extends PureComponent { label="Project Starter" isFocused={activeField === 'projectStarter'} > - {/* - this.updateProjectType(selectedProjectType) - } - /> */} - this.updateGatsbyStarter(evt.target.value)} - value={projectStarter} - onFocus={this.handleFocusStarter} - placeholder="Enter a starter" + - - toastr.confirm('Select starter', { - component: () => ( - - ), - okText: 'Use selected', - onOk: () => - this.updateGatsbyStarter(this.state.gatsbyStarter), - }) - } - > - Select Starter - ); diff --git a/src/components/CreateNewProjectWizard/SummaryPane.js b/src/components/CreateNewProjectWizard/SummaryPane.js index fb530682..9f3d7e86 100644 --- a/src/components/CreateNewProjectWizard/SummaryPane.js +++ b/src/components/CreateNewProjectWizard/SummaryPane.js @@ -195,8 +195,14 @@ class SummaryPane extends PureComponent { Please enter a starter for your project (e.g. - gatsby-starter-blog). Later we'll have a starter search modal - here. + gatsby-starter-blog or repo. url) or pick one from the starters + list. + + + This step is optional. Just leave the field empty to use the + default Gatsby starter. But picking a starter will help to + bootstrap your project e.g. you can easily create your own blog + by picking one of the blog starter templates. diff --git a/src/components/Divider/Divider.js b/src/components/Divider/Divider.js new file mode 100644 index 00000000..9897672f --- /dev/null +++ b/src/components/Divider/Divider.js @@ -0,0 +1,13 @@ +// @flow +// Used in AddDependencySearchResult & StarterSelection +import styled from 'styled-components'; + +import { COLORS } from '../../constants'; + +const Divider = styled.div` + width: 100%; + height: 1px; + background: ${COLORS.gray[100]}; +`; + +export default Divider; diff --git a/src/components/Divider/index.js b/src/components/Divider/index.js new file mode 100644 index 00000000..029cb0d8 --- /dev/null +++ b/src/components/Divider/index.js @@ -0,0 +1,2 @@ +// @flow +export { default } from './Divider'; diff --git a/src/components/TextInputWithButton/TextInputWithButton.js b/src/components/TextInputWithButton/TextInputWithButton.js new file mode 100644 index 00000000..c14cf142 --- /dev/null +++ b/src/components/TextInputWithButton/TextInputWithButton.js @@ -0,0 +1,87 @@ +// @flow +import React, { PureComponent } from 'react'; +import styled from 'styled-components'; +import IconBase from 'react-icons-kit'; +import { moreHorizontal } from 'react-icons-kit/feather/moreHorizontal'; + +import { COLORS } from '../../constants'; + +import TextInput from '../TextInput'; + +type Props = { + value: string, + onClick: () => void, + onChange: string => void, + isFocused?: boolean, + onFocus: string => void, + icon: React$Node, +}; + +class TextInputWithButton extends PureComponent { + static defaultProps = { + value: '', + onFocus: () => {}, + icon: moreHorizontal, + }; + + render() { + const { onChange, onClick, icon, ...props } = this.props; + + return ( + + onChange(ev.target.value)}> + + + + + + + + ); + } +} + +const Wrapper = styled.div` + color: ${COLORS.gray[400]}; +`; + +const ButtonPositionAdjuster = styled.div` + transform: translateY(-2px); +`; + +// const DirectoryButton = styled(TextButton)` +// color: ${COLORS.gray[600]}; +// text-decoration: none; + +// &:after { +// content: ''; +// display: block; +// padding-top: 6px; +// border-bottom: 2px solid ${COLORS.gray[600]}; +// } + +// &:hover:after { +// content: ''; +// display: block; +// border-bottom: 2px solid ${COLORS.purple[700]}; +// } +// `; + +const IconWrapper = styled.div` + width: 42px; + height: 42px; + display: flex; + justify-content: center; + align-items: center; + border: 2px solid ${COLORS.gray[400]}; + border-radius: 50%; + color: ${COLORS.gray[400]}; + cursor: pointer; + + &:hover { + color: ${COLORS.purple[500]}; + border-color: ${COLORS.purple[500]}; + } +`; + +export default TextInputWithButton; diff --git a/src/components/TextInputWithButton/index.js b/src/components/TextInputWithButton/index.js new file mode 100644 index 00000000..b83f3bda --- /dev/null +++ b/src/components/TextInputWithButton/index.js @@ -0,0 +1,2 @@ +// @flow +export { default } from './TextInputWithButton'; diff --git a/src/global-styles.js b/src/global-styles.js index e32c2860..f2fe68f3 100644 --- a/src/global-styles.js +++ b/src/global-styles.js @@ -1,7 +1,6 @@ // @flow import { injectGlobal } from 'styled-components'; import 'react-tippy/dist/tippy.css'; -import 'react-redux-toastr/lib/css/react-redux-toastr.min.css'; import { COLORS } from './constants'; import './fonts.css'; import './base.css'; @@ -20,12 +19,4 @@ injectGlobal` body { background: ${COLORS.gray[50]}; } - - /* Modify top position of React-Redux-Toastr so it's closer to the upper edge - was top: 20% */ - div .rrt-confirm-holder .rrt-confirm { - top: 5%; - width: 70vw; - margin-left: 15vw; - left: 0; - } `; diff --git a/src/reducers/index.js b/src/reducers/index.js index cdcab5db..a60bfec3 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -1,6 +1,5 @@ // @flow import { combineReducers } from 'redux'; -import { reducer as toastrReducer } from 'react-redux-toastr'; // todo: Would be better to separate library reducers and add them before creating store. Not sure how to do that and if it's really needed. import appSettings from './app-settings.reducer'; import appLoaded from './app-loaded.reducer'; @@ -24,5 +23,4 @@ export default combineReducers({ onboardingStatus, paths, queue, - toastr: toastrReducer, }); diff --git a/src/reducers/modal.reducer.js b/src/reducers/modal.reducer.js index ddba3a0d..d96dec79 100644 --- a/src/reducers/modal.reducer.js +++ b/src/reducers/modal.reducer.js @@ -12,18 +12,25 @@ import { SAVE_PROJECT_SETTINGS_FINISH, SHOW_PROJECT_SETTINGS, SHOW_APP_SETTINGS, + SHOW_STARTER_SELECTION, + HIDE_STARTER_SELECTION, HIDE_MODAL, RESET_ALL_STATE, } from '../actions'; import type { Action } from '../actions/types'; -type State = 'new-project-wizard' | 'project-settings' | null; +type State = + | 'new-project-wizard' + | 'project-settings' + | 'new-project-wizard/select-starter' + | null; export const initialState = null; export default (state: State = initialState, action: Action = {}) => { switch (action.type) { + case HIDE_STARTER_SELECTION: case CREATE_NEW_PROJECT_START: return 'new-project-wizard'; @@ -33,6 +40,9 @@ export default (state: State = initialState, action: Action = {}) => { case SHOW_APP_SETTINGS: return 'app-settings'; + case SHOW_STARTER_SELECTION: + return 'new-project-wizard/select-starter'; + case CREATE_NEW_PROJECT_CANCEL: case CREATE_NEW_PROJECT_FINISH: case IMPORT_EXISTING_PROJECT_START: diff --git a/yarn.lock b/yarn.lock index 87d2f8a6..65d7535b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3597,7 +3597,7 @@ class-utils@^0.3.5: isobject "^3.0.0" static-extend "^0.1.1" -classnames@^2.2.3, classnames@^2.2.5: +classnames@^2.2.5: version "2.2.6" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce" integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q== @@ -5520,11 +5520,6 @@ eventemitter3@1.x.x: resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-1.2.0.tgz#1c86991d816ad1e504750e73874224ecf3bec508" integrity sha1-HIaZHYFq0eUEdQ5zh0Ik7PO+xQg= -eventemitter3@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-2.0.3.tgz#b5e1079b59fb5e1ba2771c0a993be060a58c99ba" - integrity sha1-teEHm1n7XhuidxwKmTvgYKWMmbo= - eventemitter3@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.0.tgz#090b4d6cdbd645ed10bf750d4b5407942d7ba163" @@ -11088,14 +11083,6 @@ react-pure-render@^1.0.2: resolved "https://registry.yarnpkg.com/react-pure-render/-/react-pure-render-1.0.2.tgz#9d8a928c7f2c37513c2d064e57b3e3c356e9fabb" integrity sha1-nYqSjH8sN1E8LQZOV7Pjw1bp+rs= -react-redux-toastr@7.4.1: - version "7.4.1" - resolved "https://registry.yarnpkg.com/react-redux-toastr/-/react-redux-toastr-7.4.1.tgz#6351dc075c5b6f45df145d711bf40d6ed4065634" - integrity sha512-U7Z7A217yGgkjZE87MzHbLIlYV0OvZ3HiBRfUWoZF1Y4kvY6JAKn8EkAfPy40ZzoLiH2GpCq9Ac1U7v8on7PvQ== - dependencies: - classnames "^2.2.3" - eventemitter3 "^2.0.3" - react-redux@5.0.7: version "5.0.7" resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.0.7.tgz#0dc1076d9afb4670f993ffaef44b8f8c1155a4c8" From 86efa238cd3ab9b93b692cde881a1af1795146d0 Mon Sep 17 00:00:00 2001 From: AWolf81 Date: Wed, 21 Nov 2018 08:35:52 +0100 Subject: [PATCH 07/23] improved styling --- .../Gatsby/ProjectStarterSelection.js | 3 + .../Gatsby/SelectStarterDialog.js | 67 +++++++++++++------ .../CreateNewProjectWizard/MainPane.js | 3 +- .../TextInputWithButton.js | 20 ++++-- 4 files changed, 65 insertions(+), 28 deletions(-) diff --git a/src/components/CreateNewProjectWizard/Gatsby/ProjectStarterSelection.js b/src/components/CreateNewProjectWizard/Gatsby/ProjectStarterSelection.js index 2f60e33b..540f424e 100644 --- a/src/components/CreateNewProjectWizard/Gatsby/ProjectStarterSelection.js +++ b/src/components/CreateNewProjectWizard/Gatsby/ProjectStarterSelection.js @@ -17,6 +17,7 @@ type State = { type Props = { projectStarter: string, isFocused: boolean, + handleFocus: string => void, onSelect: string => void, onFocus: () => void, showStarterSelection: Dispatch, @@ -57,6 +58,7 @@ class ProjectStarter extends PureComponent { render() { const { + handleFocus, projectStarter, isFocused, onSelect, @@ -65,6 +67,7 @@ class ProjectStarter extends PureComponent { return ( { {starters.slice(0, 10).map((starter, index) => ( - - this.setStarter(starter.repo)} + this.setStarter(starter.repo)} + > + {starter.repo} + {starter.description !== 'n/a' && ( + {starter.description} + )} + - {starter.repo} - {starter.description !== 'n/a' && ( - {starter.description} - )} - - Preview in Codesandbox - - - + Preview in Codesandbox + + - + ))} @@ -172,19 +171,43 @@ class SelectStarterDialog extends PureComponent { /> */} - Use selected - Cancel + + {/* Todo: Refactor OK/Cancel buttons into a component. So this is reusable. */} + + {/* Todo: Add tooltip if disabled */} + Use selected + + + Cancel + ); } } +/* + Actions could be refactored in a component + used here and in ProjectConfigurationModal +*/ +const Actions = styled.div` + display: flex; + justify-content: center; + align-items: center; + padding-top: 8px; +`; + const ScrollContainer = styled(Scrollbars)` min-height: 60vh; `; -const StarterList = styled.div``; +const StarterList = styled.div` + padding: 15px; +`; const StarterItem = styled.div` cursor: pointer; padding: 8px 10px; diff --git a/src/components/CreateNewProjectWizard/MainPane.js b/src/components/CreateNewProjectWizard/MainPane.js index 55819cf1..7bedbd2e 100644 --- a/src/components/CreateNewProjectWizard/MainPane.js +++ b/src/components/CreateNewProjectWizard/MainPane.js @@ -5,8 +5,6 @@ import styled from 'styled-components'; import FormField from '../FormField'; import FadeIn from '../FadeIn'; -import TextInput from '../TextInput'; // todo: move to SelectStarter Component -import FillButton from '../Button/FillButton'; // dito import ProjectName from './ProjectName'; import ProjectPath from './ProjectPath'; @@ -72,6 +70,7 @@ class MainPane extends PureComponent { > diff --git a/src/components/TextInputWithButton/TextInputWithButton.js b/src/components/TextInputWithButton/TextInputWithButton.js index c14cf142..a6e189c7 100644 --- a/src/components/TextInputWithButton/TextInputWithButton.js +++ b/src/components/TextInputWithButton/TextInputWithButton.js @@ -7,9 +7,11 @@ import { moreHorizontal } from 'react-icons-kit/feather/moreHorizontal'; import { COLORS } from '../../constants'; import TextInput from '../TextInput'; +import HoverableOutlineButton from '../HoverableOutlineButton'; type Props = { value: string, + handleFocus: string => void, onClick: () => void, onChange: string => void, isFocused?: boolean, @@ -25,16 +27,26 @@ class TextInputWithButton extends PureComponent { }; render() { - const { onChange, onClick, icon, ...props } = this.props; + const { onChange, onClick, icon, handleFocus, ...props } = this.props; return ( onChange(ev.target.value)}> - + window.requestAnimationFrame(handleFocus)} + onClick={onClick} + style={{ width: 32, height: 32 }} + > + + + + + {/* - + */} ); @@ -46,7 +58,7 @@ const Wrapper = styled.div` `; const ButtonPositionAdjuster = styled.div` - transform: translateY(-2px); + transform: translateY(2px); `; // const DirectoryButton = styled(TextButton)` From badd311fa5d8f2086f91262d30d8425f21c0e95d Mon Sep 17 00:00:00 2001 From: AWolf81 Date: Thu, 22 Nov 2018 00:09:45 +0100 Subject: [PATCH 08/23] improved styling --- src/components/CodesandboxLogo/Logo.js | 71 +++++++ src/components/CodesandboxLogo/index.js | 2 + .../Gatsby/SelectStarterDialog.js | 187 ++++++++++++------ .../TextInputWithButton.js | 40 ---- src/sagas/task.saga.js | 1 - 5 files changed, 201 insertions(+), 100 deletions(-) create mode 100644 src/components/CodesandboxLogo/Logo.js create mode 100644 src/components/CodesandboxLogo/index.js diff --git a/src/components/CodesandboxLogo/Logo.js b/src/components/CodesandboxLogo/Logo.js new file mode 100644 index 00000000..5d711dc8 --- /dev/null +++ b/src/components/CodesandboxLogo/Logo.js @@ -0,0 +1,71 @@ +// @flow +import React from 'react'; +import styled from 'styled-components'; + +const Logo = ({ + width = 35, + height = 35, + className, +}: { + width: number, + height: number, + className: ?string, +}) => ( + + + + + + + + + + + + +); + +const StyledLogo = styled(Logo)` + border-radius: 50%; + background: #000; + padding: 5px; +`; + +export default StyledLogo; diff --git a/src/components/CodesandboxLogo/index.js b/src/components/CodesandboxLogo/index.js new file mode 100644 index 00000000..4c3623e3 --- /dev/null +++ b/src/components/CodesandboxLogo/index.js @@ -0,0 +1,2 @@ +// @flow +export { default } from './Logo'; diff --git a/src/components/CreateNewProjectWizard/Gatsby/SelectStarterDialog.js b/src/components/CreateNewProjectWizard/Gatsby/SelectStarterDialog.js index f3cb631b..52b039f3 100644 --- a/src/components/CreateNewProjectWizard/Gatsby/SelectStarterDialog.js +++ b/src/components/CreateNewProjectWizard/Gatsby/SelectStarterDialog.js @@ -10,13 +10,9 @@ import { Tooltip } from 'react-tippy'; import Divider from '../../Divider'; import Spacer from '../../Spacer'; import ExternalLink from '../../ExternalLink'; - -// import { SearchBox } from 'react-instantsearch/dom'; -// import { -// InstantSearch, -// InfiniteHits, -// Configure, -// } from 'react-instantsearch/dom'; +import TextInput from '../../TextInput'; +import Spinner from '../../Spinner'; +import CodesandboxLogo from '../../CodesandboxLogo'; import StrokeButton from '../../Button/StrokeButton'; import Paragraph from '../../Paragraph'; @@ -26,7 +22,6 @@ import ModalHeader from '../../ModalHeader'; import * as actions from '../../../actions'; -// import { ALGOLIA_KEYS } from '../../../constants'; import { COLORS } from '../../../constants'; import type { Dispatch } from '../../../actions/types'; @@ -66,10 +61,19 @@ class SelectStarterDialog extends PureComponent { loading: true, starters: [], selectedStarterInModal: '', + paginationIndex: 10, + filterString: '', }; + PAGINATION_STEP = 10; + static getDerivedStateFromProps(nextProps: Props, prevState: State) { + // Clear search string on modal open display + const filterString = + !prevState.isVisible && nextProps.isVisible ? '' : prevState.filterString; return { + ...prevState, + filterString, selectedStarterInModal: nextProps.selectedStarter, }; } @@ -104,6 +108,10 @@ class SelectStarterDialog extends PureComponent { 'projectStarter', this.state.selectedStarterInModal ); + + this.setState({ + selectedStarterInModal: '', + }); this.props.hideModal(); }; @@ -113,74 +121,121 @@ class SelectStarterDialog extends PureComponent { }); }; + handleShowMore = () => { + let newIndex = this.state.paginationIndex + this.PAGINATION_STEP; + newIndex = Math.min(this.state.starters.length, newIndex); // limit + this.setState({ + paginationIndex: newIndex, + }); + }; + + updateSearchString = evt => { + this.setState({ + filterString: evt.target.value, + }); + }; + render() { const { isVisible, hideModal } = this.props; - const { starters, selectedStarterInModal } = this.state; - console.log('render dialog', selectedStarterInModal); + const { + loading, + starters, + selectedStarterInModal, + paginationIndex, + filterString, + } = this.state; + const filteredStarters = starters.filter( + starter => filterString === '' || starter.repo.includes(filterString) + ); + const disabledUseSelect = selectedStarterInModal === ''; + return ( - Please select a starter template for your new project. + For a better overview you can also have a look at the Gatsby + starters library{' '} + + here. + + + - {starters.slice(0, 10).map((starter, index) => ( - this.setStarter(starter.repo)} - > - {starter.repo} - {starter.description !== 'n/a' && ( - {starter.description} - )} - - Preview in Codesandbox - - - - - ))} + {loading && ( +
+ +
+ )} + {filteredStarters + .slice(0, paginationIndex) + .map((starter, index) => ( + + + this.setStarter(starter.repo)} + > + {starter.repo.split('/').pop()} + + + + + + + + + + + {starter.description !== 'n/a' && starter.description} + + + + ))}
- {/* We could add Algolia here --> Setup at Algolia required */} - {/* - - console.log(value)} /> - ( -
onSelect(hit.name)}> - {hit.name} -
+ {/* Show more button if we're having more starters to display */} + {paginationIndex < filteredStarters.length && ( + + + Show more... + + )} - /> -
*/}
{/* Todo: Refactor OK/Cancel buttons into a component. So this is reusable. */} - {/* Todo: Add tooltip if disabled */} - Use selected - + disabled={disabledUseSelect} + children={ + disabledUseSelect ? ( + + Use Selection + + ) : ( + 'Use Selection' + ) + } + /> Cancel @@ -205,15 +260,29 @@ const ScrollContainer = styled(Scrollbars)` min-height: 60vh; `; +const ShowMoreWrapper = styled.div` + padding: 10px; +`; + const StarterList = styled.div` padding: 15px; `; const StarterItem = styled.div` - cursor: pointer; padding: 8px 10px; +`; + +const StarterItemHeading = styled(Heading)` + cursor: pointer; border-radius: 6px; border: 2px solid - ${props => (props.selected ? COLORS.purple[500] : 'transparent')}; + ${props => (props.selected ? COLORS.purple[500] : COLORS.gray[200])}; + padding: 6px; +`; + +const StarterItemTitle = styled.div` + display: flex; + justify-content: space-between; + align-items: center; `; const MainContent = styled.div` diff --git a/src/components/TextInputWithButton/TextInputWithButton.js b/src/components/TextInputWithButton/TextInputWithButton.js index a6e189c7..2053592f 100644 --- a/src/components/TextInputWithButton/TextInputWithButton.js +++ b/src/components/TextInputWithButton/TextInputWithButton.js @@ -42,11 +42,6 @@ class TextInputWithButton extends PureComponent { - {/* - - - - */}
); @@ -61,39 +56,4 @@ const ButtonPositionAdjuster = styled.div` transform: translateY(2px); `; -// const DirectoryButton = styled(TextButton)` -// color: ${COLORS.gray[600]}; -// text-decoration: none; - -// &:after { -// content: ''; -// display: block; -// padding-top: 6px; -// border-bottom: 2px solid ${COLORS.gray[600]}; -// } - -// &:hover:after { -// content: ''; -// display: block; -// border-bottom: 2px solid ${COLORS.purple[700]}; -// } -// `; - -const IconWrapper = styled.div` - width: 42px; - height: 42px; - display: flex; - justify-content: center; - align-items: center; - border: 2px solid ${COLORS.gray[400]}; - border-radius: 50%; - color: ${COLORS.gray[400]}; - cursor: pointer; - - &:hover { - color: ${COLORS.purple[500]}; - border-color: ${COLORS.purple[500]}; - } -`; - export default TextInputWithButton; diff --git a/src/sagas/task.saga.js b/src/sagas/task.saga.js index c154e05c..83ef6100 100644 --- a/src/sagas/task.saga.js +++ b/src/sagas/task.saga.js @@ -36,7 +36,6 @@ import type { Saga } from 'redux-saga'; import type { ChildProcess } from 'child_process'; import type { Task, ProjectType } from '../types'; import type { ReturnType } from '../actions/types'; -import type { VariableMap } from '../services/config-variables.service'; const { dialog } = remote; From 032cab44240c052ebd15b5f687bc0d438934c061 Mon Sep 17 00:00:00 2001 From: AWolf81 Date: Thu, 22 Nov 2018 00:18:58 +0100 Subject: [PATCH 09/23] added test for config variable substitution --- src/services/config-variables.service.test.js | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 src/services/config-variables.service.test.js diff --git a/src/services/config-variables.service.test.js b/src/services/config-variables.service.test.js new file mode 100644 index 00000000..0309d561 --- /dev/null +++ b/src/services/config-variables.service.test.js @@ -0,0 +1,34 @@ +// @flow +import { substituteConfigVariables } from './config-variables.service'; + +describe('substitute config variables', () => { + it('should replace $values with real values', () => { + const configuration = { + env: { + cwd: '$projectPath', + PORT: '$port', + }, + create: ['npx', '$projectPath', '$projectStarter'], + }; + + expect( + substituteConfigVariables(configuration, { + $port: 3000, + $projectPath: 'some/path/to/project', + $projectStarter: 'https://github.com/gatsbyjs/gatsby-starter-default', + }) + ).toMatchInlineSnapshot(` +Object { + "create": Array [ + "npx", + "some/path/to/project", + "https://github.com/gatsbyjs/gatsby-starter-default", + ], + "env": Object { + "PORT": 3000, + "cwd": "some/path/to/project", + }, +} +`); + }); +}); From a3581341e9d41dbc4614123becb21160f0f597bd Mon Sep 17 00:00:00 2001 From: AWolf81 Date: Thu, 22 Nov 2018 01:30:51 +0100 Subject: [PATCH 10/23] WIP: CurrentStepIndex not working as expected. --- .../CreateNewProjectWizard/BuildPane.js | 9 ++-- .../CreateNewProjectWizard/MainPane.js | 47 ++++++++----------- src/services/config-variables.service.js | 9 +++- src/services/config-variables.service.test.js | 4 +- src/services/create-project.service.js | 4 +- 5 files changed, 36 insertions(+), 37 deletions(-) diff --git a/src/components/CreateNewProjectWizard/BuildPane.js b/src/components/CreateNewProjectWizard/BuildPane.js index 07d51c62..863e0b0a 100644 --- a/src/components/CreateNewProjectWizard/BuildPane.js +++ b/src/components/CreateNewProjectWizard/BuildPane.js @@ -98,12 +98,13 @@ class BuildPane extends PureComponent { ); } - // Add url to starter if not passed + // Add url to starter if not passed & not an empty string // Todo: We need error handling to show a notification that it failed to use the starter (e.g. starter doesn't exists or wrong url/name) // --> Probably just needed if we allow the user to enter an url to a starter. - const projectStarter = !projectStarterInput.includes('http') - ? 'https://github.com/gatsbyjs/' + projectStarterInput - : projectStarterInput; + const projectStarter = + !projectStarterInput.includes('http') && projectStarterInput !== '' + ? 'https://github.com/gatsbyjs/' + projectStarterInput + : projectStarterInput; createProject( { projectName, projectType, projectIcon, projectStarter }, diff --git a/src/components/CreateNewProjectWizard/MainPane.js b/src/components/CreateNewProjectWizard/MainPane.js index 7bedbd2e..11f9525c 100644 --- a/src/components/CreateNewProjectWizard/MainPane.js +++ b/src/components/CreateNewProjectWizard/MainPane.js @@ -32,7 +32,7 @@ type Props = { }; type State = { - gatsbyStarter: string, // temporary value during selection in selection toast + gatsbyStarter: string, // Temporary value during selection in selection toast }; class MainPane extends PureComponent { @@ -53,11 +53,6 @@ class MainPane extends PureComponent { updateGatsbyStarter = (selectedStarter: string) => this.props.updateFieldValue('projectStarter', selectedStarter); - static getDerivedStateFromProps(nextProps: Props, prevState: State) { - console.log('MainPane new props', nextProps, prevState); - return prevState; - } - projectSpecificSteps() { const { activeField, projectType, projectStarter } = this.props; switch (projectType) { @@ -85,7 +80,7 @@ class MainPane extends PureComponent { renderConditionalSteps(currentStepIndex: number) { const { activeField, projectType, projectIcon } = this.props; const buildSteps: Array = [ - // currentStepIndex > 0 -- > 1 + // currentStepIndex = 0 { /> , - this.projectSpecificSteps(), // currentStepIndex > 1 + this.projectSpecificSteps(), // currentStepIndex = 1 { /> , - ]; + ].filter(step => !!step); - const renderedSteps: Array = buildSteps - .filter(step => !!step) - .slice(0, currentStepIndex); + const renderedSteps: Array = buildSteps.slice( + 0, + currentStepIndex + ); - console.log('render steps', renderedSteps); + // Todo: Fix index or change to a better model. At the moment, difficult to handle. return { - lastIndex: projectType === 'gatsby' ? 3 : 2, //buildSteps.length, // Todo: Use buildSteps array to find last index + lastIndex: buildSteps.length, steps: renderedSteps, }; } - validateField(currentStepIndex: number) { - // todo: Refactor - Move buildsteps to component scope & use an array method to check current validation - // --> For now we're doing a different check for Gatsby flow - const { projectIcon, projectStarter, projectType } = this.props; - return projectType === 'gatsby' - ? (currentStepIndex > 0 && !projectType) || - (currentStepIndex > 1 && projectStarter === '') || - (currentStepIndex > 2 && !projectIcon) - : (currentStepIndex > 0 && !projectType) || - (currentStepIndex > 1 && !projectIcon); + validateField(currentStepIndex: number, lastIndex: number) { + // No validation for projectStarter as it is optional + const { projectIcon, projectType } = this.props; + + return ( + (currentStepIndex > 0 && !projectType) || + (currentStepIndex > lastIndex && !projectIcon) + ); } render() { const { @@ -150,7 +144,6 @@ class MainPane extends PureComponent { const { lastIndex, steps } = this.renderConditionalSteps(currentStepIndex); return ( - {/*
{JSON.stringify(this.props, null, 2)}
*/} {({ offset }) => ( @@ -174,9 +167,9 @@ class MainPane extends PureComponent { isDisabled={ isProjectNameTaken || !projectName || - this.validateField(currentStepIndex) + this.validateField(currentStepIndex, lastIndex) } - readyToBeSubmitted={currentStepIndex >= lastIndex} + readyToBeSubmitted={currentStepIndex > lastIndex} hasBeenSubmitted={hasBeenSubmitted} onSubmit={handleSubmit} /> diff --git a/src/services/config-variables.service.js b/src/services/config-variables.service.js index 66786cb6..7c186b22 100644 --- a/src/services/config-variables.service.js +++ b/src/services/config-variables.service.js @@ -19,7 +19,10 @@ export const substituteConfigVariables = ( (config, key) => { if (config[key] instanceof Array) { // replace $port inside args array - config[key] = config[key].map(arg => variableMap[arg] || arg); + // empty string is special here - we'd like to us it as replacement + config[key] = config[key].map( + arg => (variableMap[arg] === '' ? '' : variableMap[arg] || arg) + ); } else { // check config[key] e.g. is {env: { PORT: '$port'} } if (config[key] instanceof Object) { @@ -28,7 +31,9 @@ export const substituteConfigVariables = ( (newObj, nestedKey) => { // use replacement value if available newObj[nestedKey] = - variableMap[newObj[nestedKey]] || newObj[nestedKey]; + variableMap[newObj[nestedKey]] === '' + ? '' + : variableMap[newObj[nestedKey]] || newObj[nestedKey]; return newObj; }, { ...config[key] } diff --git a/src/services/config-variables.service.test.js b/src/services/config-variables.service.test.js index 0309d561..5c0dff83 100644 --- a/src/services/config-variables.service.test.js +++ b/src/services/config-variables.service.test.js @@ -13,7 +13,7 @@ describe('substitute config variables', () => { expect( substituteConfigVariables(configuration, { - $port: 3000, + $port: '3000', $projectPath: 'some/path/to/project', $projectStarter: 'https://github.com/gatsbyjs/gatsby-starter-default', }) @@ -25,7 +25,7 @@ Object { "https://github.com/gatsbyjs/gatsby-starter-default", ], "env": Object { - "PORT": 3000, + "PORT": "3000", "cwd": "some/path/to/project", }, } diff --git a/src/services/create-project.service.js b/src/services/create-project.service.js index 961ffabc..373fd44c 100644 --- a/src/services/create-project.service.js +++ b/src/services/create-project.service.js @@ -218,9 +218,9 @@ export const getBuildInstructions = ( projectConfigs[projectType].create, { $projectPath: projectPath, - $projectStarter: (options && options.projectStarter) || '', + $projectStarter: options && options.projectStarter, } ); - + console.log('create cmd', createCommand); return [command, ...createCommand.args]; }; From 2c558387073cbfd1a42c558d226709a9a3bf4477 Mon Sep 17 00:00:00 2001 From: AWolf81 Date: Thu, 22 Nov 2018 08:16:06 +0100 Subject: [PATCH 11/23] fixed flow types --- .../Gatsby/SelectStarterDialog.js | 6 +++--- src/services/config-variables.service.test.js | 13 +++++++++---- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/components/CreateNewProjectWizard/Gatsby/SelectStarterDialog.js b/src/components/CreateNewProjectWizard/Gatsby/SelectStarterDialog.js index 52b039f3..33c12aa7 100644 --- a/src/components/CreateNewProjectWizard/Gatsby/SelectStarterDialog.js +++ b/src/components/CreateNewProjectWizard/Gatsby/SelectStarterDialog.js @@ -36,6 +36,8 @@ type State = { loading: boolean, starters: Array, selectedStarterInModal: string, + paginationIndex: number, + filterString: '', }; /* @@ -69,11 +71,9 @@ class SelectStarterDialog extends PureComponent { static getDerivedStateFromProps(nextProps: Props, prevState: State) { // Clear search string on modal open display - const filterString = - !prevState.isVisible && nextProps.isVisible ? '' : prevState.filterString; return { ...prevState, - filterString, + filterString: '', selectedStarterInModal: nextProps.selectedStarter, }; } diff --git a/src/services/config-variables.service.test.js b/src/services/config-variables.service.test.js index 5c0dff83..0c1dada2 100644 --- a/src/services/config-variables.service.test.js +++ b/src/services/config-variables.service.test.js @@ -4,13 +4,18 @@ import { substituteConfigVariables } from './config-variables.service'; describe('substitute config variables', () => { it('should replace $values with real values', () => { const configuration = { - env: { - cwd: '$projectPath', - PORT: '$port', - }, + env: { cwd: '$projectPath', PORT: '$port' }, create: ['npx', '$projectPath', '$projectStarter'], }; + // Flow error here & not sure why. + // It complains about missing property toMatchInlineSnapshot with 6 or cases. + // It error message starts like: + // Cannot call `expect(...).toMatchInlineSnapshot` because: + // - Either property`toMatchInlineSnapshot` is missing in `JestExpectType`[1]. + // - Or property `toMatchInlineSnapshot` is missing in `JestPromiseType` [2]. + // - ... + // $FlowFixMe expect( substituteConfigVariables(configuration, { $port: '3000', From 8bb11ca2d70cc092bf5b131adae1a4daa0818bf4 Mon Sep 17 00:00:00 2001 From: AWolf81 Date: Thu, 22 Nov 2018 09:31:28 +0100 Subject: [PATCH 12/23] WIP: Added starter exists check. Dialog displayed too late. --- .../CreateNewProjectWizard/BuildPane.js | 37 +++++++++++++++---- .../CreateNewProjectWizard/MainPane.js | 5 +-- src/services/check-if-url-exists.service.js | 8 ++++ src/services/create-project.service.js | 2 +- 4 files changed, 40 insertions(+), 12 deletions(-) create mode 100644 src/services/check-if-url-exists.service.js diff --git a/src/components/CreateNewProjectWizard/BuildPane.js b/src/components/CreateNewProjectWizard/BuildPane.js index 863e0b0a..30e4cad4 100644 --- a/src/components/CreateNewProjectWizard/BuildPane.js +++ b/src/components/CreateNewProjectWizard/BuildPane.js @@ -3,9 +3,11 @@ import React, { PureComponent } from 'react'; import styled from 'styled-components'; import IconBase from 'react-icons-kit'; import { check } from 'react-icons-kit/feather/check'; +import { remote } from 'electron'; import { COLORS } from '../../constants'; import createProject from '../../services/create-project.service'; +import { urlExists } from '../../services/check-if-url-exists.service'; import Spacer from '../Spacer'; import WhimsicalInstaller from '../WhimsicalInstaller'; @@ -14,6 +16,8 @@ import BuildStepProgress from './BuildStepProgress'; import type { BuildStep, Status } from './types'; import type { ProjectType, ProjectInternal } from '../../types'; +const { dialog } = remote; + const BUILD_STEPS = { installingCliTool: { copy: 'Installing build tool', @@ -69,17 +73,21 @@ class BuildPane extends PureComponent { // `runInstaller` becomes true, so we want it to be in its final position // when this happens. Otherwise, clicking files in the air instantly // "teleports" them a couple inches from the mouse :/ - this.timeoutId = window.setTimeout(() => { - this.buildProject(); - - this.timeoutId = window.setTimeout(() => { - this.setState({ runInstaller: true }); - }, 500); - }, 600); + this.buildProject().then(result => { + if (result) { + this.timeoutId = window.setTimeout(() => { + // Using promise not async as async can be problematic in life-cycle hooks + // Build can be started + this.timeoutId = window.setTimeout(() => { + this.setState({ runInstaller: true }); + }, 500); + }, 600); + } + }); } } - buildProject = () => { + buildProject = async () => { const { projectName, projectType, @@ -106,6 +114,17 @@ class BuildPane extends PureComponent { ? 'https://github.com/gatsbyjs/' + projectStarterInput : projectStarterInput; + const exists = await urlExists(projectStarter); + + console.log('exists', exists); + if (!exists) { + // starter not found + return dialog.showErrorBox( + 'Starter not found', + 'Please check your starter url or use the starter selection to pick a starter.' + ); + } + createProject( { projectName, projectType, projectIcon, projectStarter }, this.props.projectHomePath, @@ -113,6 +132,8 @@ class BuildPane extends PureComponent { this.handleError, this.handleComplete ); + + return true; }; handleStatusUpdate = (output: any) => { diff --git a/src/components/CreateNewProjectWizard/MainPane.js b/src/components/CreateNewProjectWizard/MainPane.js index 11f9525c..41abfd86 100644 --- a/src/components/CreateNewProjectWizard/MainPane.js +++ b/src/components/CreateNewProjectWizard/MainPane.js @@ -116,7 +116,6 @@ class MainPane extends PureComponent { currentStepIndex ); - // Todo: Fix index or change to a better model. At the moment, difficult to handle. return { lastIndex: buildSteps.length, steps: renderedSteps, @@ -128,7 +127,7 @@ class MainPane extends PureComponent { return ( (currentStepIndex > 0 && !projectType) || - (currentStepIndex > lastIndex && !projectIcon) + (currentStepIndex >= lastIndex && !projectIcon) ); } render() { @@ -169,7 +168,7 @@ class MainPane extends PureComponent { !projectName || this.validateField(currentStepIndex, lastIndex) } - readyToBeSubmitted={currentStepIndex > lastIndex} + readyToBeSubmitted={currentStepIndex >= lastIndex} hasBeenSubmitted={hasBeenSubmitted} onSubmit={handleSubmit} /> diff --git a/src/services/check-if-url-exists.service.js b/src/services/check-if-url-exists.service.js new file mode 100644 index 00000000..a2848284 --- /dev/null +++ b/src/services/check-if-url-exists.service.js @@ -0,0 +1,8 @@ +// @flow +import fetch from 'node-fetch'; + +export const urlExists = (url: string) => + new Promise(async resolve => { + const response = await fetch(url); + resolve(response.ok); + }); diff --git a/src/services/create-project.service.js b/src/services/create-project.service.js index 373fd44c..b04f44a2 100644 --- a/src/services/create-project.service.js +++ b/src/services/create-project.service.js @@ -221,6 +221,6 @@ export const getBuildInstructions = ( $projectStarter: options && options.projectStarter, } ); - console.log('create cmd', createCommand); + return [command, ...createCommand.args]; }; From 2400326341f27e50076d8c430f7b3543aacb3002 Mon Sep 17 00:00:00 2001 From: AWolf81 Date: Fri, 23 Nov 2018 02:32:19 +0100 Subject: [PATCH 13/23] fixed starter check --- .../CreateNewProjectWizard/BuildPane.js | 28 ++++-------------- .../CreateNewProjectWizard.js | 29 +++++++++++++++++-- .../Gatsby/ProjectStarterSelection.js | 8 ----- .../CreateNewProjectWizard/helpers.js | 16 ++++++++++ .../CreateNewProjectWizard/helpers.test.js | 19 ++++++++++++ .../TextInputWithButton.js | 6 +++- src/config/app.js | 2 +- 7 files changed, 74 insertions(+), 34 deletions(-) create mode 100644 src/components/CreateNewProjectWizard/helpers.js create mode 100644 src/components/CreateNewProjectWizard/helpers.test.js diff --git a/src/components/CreateNewProjectWizard/BuildPane.js b/src/components/CreateNewProjectWizard/BuildPane.js index 30e4cad4..c933e72b 100644 --- a/src/components/CreateNewProjectWizard/BuildPane.js +++ b/src/components/CreateNewProjectWizard/BuildPane.js @@ -3,11 +3,10 @@ import React, { PureComponent } from 'react'; import styled from 'styled-components'; import IconBase from 'react-icons-kit'; import { check } from 'react-icons-kit/feather/check'; -import { remote } from 'electron'; import { COLORS } from '../../constants'; import createProject from '../../services/create-project.service'; -import { urlExists } from '../../services/check-if-url-exists.service'; +import { replaceProjectStarterStringWithUrl } from './helpers'; import Spacer from '../Spacer'; import WhimsicalInstaller from '../WhimsicalInstaller'; @@ -16,8 +15,6 @@ import BuildStepProgress from './BuildStepProgress'; import type { BuildStep, Status } from './types'; import type { ProjectType, ProjectInternal } from '../../types'; -const { dialog } = remote; - const BUILD_STEPS = { installingCliTool: { copy: 'Installing build tool', @@ -106,24 +103,11 @@ class BuildPane extends PureComponent { ); } - // Add url to starter if not passed & not an empty string - // Todo: We need error handling to show a notification that it failed to use the starter (e.g. starter doesn't exists or wrong url/name) - // --> Probably just needed if we allow the user to enter an url to a starter. - const projectStarter = - !projectStarterInput.includes('http') && projectStarterInput !== '' - ? 'https://github.com/gatsbyjs/' + projectStarterInput - : projectStarterInput; - - const exists = await urlExists(projectStarter); - - console.log('exists', exists); - if (!exists) { - // starter not found - return dialog.showErrorBox( - 'Starter not found', - 'Please check your starter url or use the starter selection to pick a starter.' - ); - } + // Replace starter string with URL. + // No need to check if it exists as this already happend before we're here. + const projectStarter = replaceProjectStarterStringWithUrl( + projectStarterInput + ); createProject( { projectName, projectType, projectIcon, projectStarter }, diff --git a/src/components/CreateNewProjectWizard/CreateNewProjectWizard.js b/src/components/CreateNewProjectWizard/CreateNewProjectWizard.js index e278041c..6b432e97 100644 --- a/src/components/CreateNewProjectWizard/CreateNewProjectWizard.js +++ b/src/components/CreateNewProjectWizard/CreateNewProjectWizard.js @@ -13,6 +13,8 @@ import { getById } from '../../reducers/projects.reducer'; import { getOnboardingCompleted } from '../../reducers/onboarding-status.reducer'; import { getProjectNameSlug } from '../../services/create-project.service'; import { checkIfProjectExists } from '../../services/create-project.service'; +import { urlExists } from '../../services/check-if-url-exists.service'; +import { replaceProjectStarterStringWithUrl } from './helpers'; import TwoPaneModal from '../TwoPaneModal'; import Debounced from '../Debounced'; @@ -141,14 +143,37 @@ class CreateNewProjectWizard extends PureComponent { } }); }; + + checkIfStarterUrlExists = async () => { + const { projectStarter: projectStarterInput } = this.state; + // Add url to starter if not passed & not an empty string + const projectStarter = replaceProjectStarterStringWithUrl( + projectStarterInput + ); + + const exists = await urlExists(projectStarter); + + if (!exists) { + // starter not found + dialog.showErrorBox( + `Starter ${projectStarter} not found`, + 'Please check your starter url or use the starter selection to pick a starter.' + ); + throw new Error('starter-not-found'); + } + }; + handleSubmit = () => { const currentStepIndex = FORM_STEPS.indexOf(this.state.currentStep); const nextStep = FORM_STEPS[currentStepIndex + 1]; if (!nextStep) { - return this.checkProjectLocationUsage() + return Promise.all([ + this.checkProjectLocationUsage(), + this.checkIfStarterUrlExists(), + ]) .then(() => { - // not in use + // not in use & starter exists (if Gatsby project) this.setState({ activeField: null, status: 'building-project', diff --git a/src/components/CreateNewProjectWizard/Gatsby/ProjectStarterSelection.js b/src/components/CreateNewProjectWizard/Gatsby/ProjectStarterSelection.js index 540f424e..a0294932 100644 --- a/src/components/CreateNewProjectWizard/Gatsby/ProjectStarterSelection.js +++ b/src/components/CreateNewProjectWizard/Gatsby/ProjectStarterSelection.js @@ -1,9 +1,6 @@ // @flow import React, { PureComponent } from 'react'; import { connect } from 'react-redux'; -// import styled from 'styled-components'; - -// import { COLORS } from '../../constants'; import TextInputWithButton from '../../TextInputWithButton'; @@ -49,11 +46,6 @@ class ProjectStarter extends PureComponent { handleSelect = () => { this.props.onSelect(this.state.gatsbyStarter); - - // clear temporary state value - // this.setState({ - // gatsbyStarter: '', - // }); }; render() { diff --git a/src/components/CreateNewProjectWizard/helpers.js b/src/components/CreateNewProjectWizard/helpers.js new file mode 100644 index 00000000..132726c9 --- /dev/null +++ b/src/components/CreateNewProjectWizard/helpers.js @@ -0,0 +1,16 @@ +// @flow + +// Not perfect to have this helper but we're having two locations (BuildPane & CreateNewProjectWizard) where it is used +// I'd like to keep the user input unmodified and use only Gatsby github repo as short-hand so entering +// gatsby-starter-blog will be https://github.com/gatsby/gatsby-starter-blog +// Todo: We could also add a short-hand for username/repo to replace with https://github.com/username/repo +// An additional check for string with-out slash would be required to still support the Gatsby replacement. + +export const defaultStarterUrl = 'https://github.com/gatsbyjs/'; + +export const replaceProjectStarterStringWithUrl = ( + projectStarterInput: string +) => + !projectStarterInput.includes('http') && projectStarterInput !== '' + ? defaultStarterUrl + projectStarterInput + : projectStarterInput; diff --git a/src/components/CreateNewProjectWizard/helpers.test.js b/src/components/CreateNewProjectWizard/helpers.test.js new file mode 100644 index 00000000..73b24ec6 --- /dev/null +++ b/src/components/CreateNewProjectWizard/helpers.test.js @@ -0,0 +1,19 @@ +// @flow +import { + replaceProjectStarterStringWithUrl, + defaultStarterUrl, +} from './helpers'; + +describe('Build helpers', () => { + describe('Gatsby helper', () => { + it('should replace Gatsby starter string with url', () => { + expect(replaceProjectStarterStringWithUrl('gatsby-starter-blog')).toEqual( + defaultStarterUrl + 'gatsby-starter-blog' + ); + }); + + it('should ignore empty starter strings', () => { + expect(replaceProjectStarterStringWithUrl('')).toEqual(''); + }); + }); +}); diff --git a/src/components/TextInputWithButton/TextInputWithButton.js b/src/components/TextInputWithButton/TextInputWithButton.js index 2053592f..3260315c 100644 --- a/src/components/TextInputWithButton/TextInputWithButton.js +++ b/src/components/TextInputWithButton/TextInputWithButton.js @@ -31,7 +31,11 @@ class TextInputWithButton extends PureComponent { return ( - onChange(ev.target.value)}> + onChange(ev.target.value)} + > window.requestAnimationFrame(handleFocus)} diff --git a/src/config/app.js b/src/config/app.js index 0a1f739b..4dbd9df0 100644 --- a/src/config/app.js +++ b/src/config/app.js @@ -3,5 +3,5 @@ module.exports = { PACKAGE_MANAGER: 'yarn', // Enable logging, if enabled all terminal responses are visible in the console (useful for debugging) - LOGGING: true, + LOGGING: false, }; From c277acf08358069318fe32cb39272938e3d63b73 Mon Sep 17 00:00:00 2001 From: AWolf81 Date: Fri, 23 Nov 2018 02:38:25 +0100 Subject: [PATCH 14/23] fixed flow type --- src/services/check-if-url-exists.service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/check-if-url-exists.service.js b/src/services/check-if-url-exists.service.js index a2848284..d4fb2ee5 100644 --- a/src/services/check-if-url-exists.service.js +++ b/src/services/check-if-url-exists.service.js @@ -2,7 +2,7 @@ import fetch from 'node-fetch'; export const urlExists = (url: string) => - new Promise(async resolve => { + new Promise(async resolve => { const response = await fetch(url); resolve(response.ok); }); From f1218d86baa8c368eca6e182b08dfead008a04f6 Mon Sep 17 00:00:00 2001 From: AWolf81 Date: Sun, 25 Nov 2018 03:12:22 +0100 Subject: [PATCH 15/23] addressed review comments. --- .../CreateNewProjectWizard.js | 9 +- .../Gatsby/ProjectStarterSelection.js | 38 ++--- .../Gatsby/SelectStarterDialog.js | 5 +- .../CreateNewProjectWizard/MainPane.js | 138 +++++++++--------- .../CreateNewProjectWizard/SummaryPane.js | 2 +- .../TextInputWithButton.js | 4 +- 6 files changed, 101 insertions(+), 95 deletions(-) diff --git a/src/components/CreateNewProjectWizard/CreateNewProjectWizard.js b/src/components/CreateNewProjectWizard/CreateNewProjectWizard.js index 6b432e97..3f85672c 100644 --- a/src/components/CreateNewProjectWizard/CreateNewProjectWizard.js +++ b/src/components/CreateNewProjectWizard/CreateNewProjectWizard.js @@ -32,8 +32,8 @@ import type { Dispatch } from '../../actions/types'; const FORM_STEPS: Array = [ 'projectName', 'projectType', - 'projectStarter', 'projectIcon', + 'projectStarter', ]; const { dialog } = remote; @@ -151,6 +151,10 @@ class CreateNewProjectWizard extends PureComponent { projectStarterInput ); + if (projectStarter === '') { + return; + } + const exists = await urlExists(projectStarter); if (!exists) { @@ -166,8 +170,9 @@ class CreateNewProjectWizard extends PureComponent { handleSubmit = () => { const currentStepIndex = FORM_STEPS.indexOf(this.state.currentStep); const nextStep = FORM_STEPS[currentStepIndex + 1]; + const lastStep = this.state.projectType === 'gatsby' ? 3 : 2; - if (!nextStep) { + if (currentStepIndex >= lastStep) { return Promise.all([ this.checkProjectLocationUsage(), this.checkIfStarterUrlExists(), diff --git a/src/components/CreateNewProjectWizard/Gatsby/ProjectStarterSelection.js b/src/components/CreateNewProjectWizard/Gatsby/ProjectStarterSelection.js index a0294932..bdf0f510 100644 --- a/src/components/CreateNewProjectWizard/Gatsby/ProjectStarterSelection.js +++ b/src/components/CreateNewProjectWizard/Gatsby/ProjectStarterSelection.js @@ -21,32 +21,26 @@ type Props = { }; class ProjectStarter extends PureComponent { - state = { - gatsbyStarter: '', - }; + // state = { + // gatsbyStarter: '', + // }; - static getDerivedStateFromProps(nextProps: Props, prevState: State) { - return { - gatsbyStarter: nextProps.projectStarter, - }; - } + // static getDerivedStateFromProps(nextProps: Props, prevState: State) { + // return { + // gatsbyStarter: nextProps.projectStarter, + // }; + // } // Change method needed so we can dismiss the selection on close click of toastr - changeGatsbyStarter = (selectedStarter: string) => { - console.log('change starter', selectedStarter, this); - this.setState( - { - gatsbyStarter: selectedStarter, - }, - () => { - console.log('updated', this.state); - } - ); - }; + // changeGatsbyStarter = (selectedStarter: string) => { + // this.setState({ + // gatsbyStarter: selectedStarter, + // }); + // }; - handleSelect = () => { - this.props.onSelect(this.state.gatsbyStarter); - }; + // handleSelect = () => { + // this.props.onSelect(this.state.gatsbyStarter); + // }; render() { const { diff --git a/src/components/CreateNewProjectWizard/Gatsby/SelectStarterDialog.js b/src/components/CreateNewProjectWizard/Gatsby/SelectStarterDialog.js index 33c12aa7..5b26cc63 100644 --- a/src/components/CreateNewProjectWizard/Gatsby/SelectStarterDialog.js +++ b/src/components/CreateNewProjectWizard/Gatsby/SelectStarterDialog.js @@ -155,10 +155,9 @@ class SelectStarterDialog extends PureComponent { - For a better overview you can also have a look at the Gatsby - starters library{' '} + For a better overview you can also have a look at the{' '} - here. + Gatsby starters library { updateGatsbyStarter = (selectedStarter: string) => this.props.updateFieldValue('projectStarter', selectedStarter); - projectSpecificSteps() { - const { activeField, projectType, projectStarter } = this.props; - switch (projectType) { - case 'gatsby': - return ( - - - - - - ); - default: - return null; - } - } - - renderConditionalSteps(currentStepIndex: number) { - const { activeField, projectType, projectIcon } = this.props; - const buildSteps: Array = [ - // currentStepIndex = 0 - - - - this.updateProjectType(selectedProjectType) - } - /> - - , - this.projectSpecificSteps(), // currentStepIndex = 1 - + projectSpecificGatsbyStep() { + const { activeField, projectStarter } = this.props; + return ( + - - , - ].filter(step => !!step); - - const renderedSteps: Array = buildSteps.slice( - 0, - currentStepIndex + ); + } + + renderConditionalSteps(currentStepIndex: number) { + const { activeField, projectType, projectIcon } = this.props; + const steps: Array = []; + let lastIndex = 2; + + if (projectType === 'gatsby') { + lastIndex = 3; + } + + if (currentStepIndex > 0) { + // currentStepIndex = 1 + steps.push( + + + + this.updateProjectType(selectedProjectType) + } + /> + + + ); + } + if (currentStepIndex > 1) { + steps.push( + // 2 + + + + + + ); + } + + if (currentStepIndex > 2 && projectType === 'gatsby') { + // 3 + steps.push(this.projectSpecificGatsbyStep()); + } return { - lastIndex: buildSteps.length, - steps: renderedSteps, + lastIndex, + steps, }; } - validateField(currentStepIndex: number, lastIndex: number) { + isSubmitDisabled(currentStepIndex: number, lastIndex: number) { // No validation for projectStarter as it is optional const { projectIcon, projectType } = this.props; - return ( - (currentStepIndex > 0 && !projectType) || - (currentStepIndex >= lastIndex && !projectIcon) - ); + const needsProjectType = !projectType && currentStepIndex > 1; + const needsProjectIcon = !projectIcon && currentStepIndex >= 2; + + return needsProjectType || needsProjectIcon; } render() { const { @@ -166,7 +174,7 @@ class MainPane extends PureComponent { isDisabled={ isProjectNameTaken || !projectName || - this.validateField(currentStepIndex, lastIndex) + this.isSubmitDisabled(currentStepIndex, lastIndex) } readyToBeSubmitted={currentStepIndex >= lastIndex} hasBeenSubmitted={hasBeenSubmitted} diff --git a/src/components/CreateNewProjectWizard/SummaryPane.js b/src/components/CreateNewProjectWizard/SummaryPane.js index 9f3d7e86..ddb8c675 100644 --- a/src/components/CreateNewProjectWizard/SummaryPane.js +++ b/src/components/CreateNewProjectWizard/SummaryPane.js @@ -187,7 +187,7 @@ class SummaryPane extends PureComponent { } case 'projectStarter': { // todo: why is a key needed on FadeIn? Was s3t. - // todo: should we rename projectStarter to be mores specific as this is Gatbsy only. + // todo: should we rename projectStarter to be mores specific as this is Gatsby only. return ( diff --git a/src/components/TextInputWithButton/TextInputWithButton.js b/src/components/TextInputWithButton/TextInputWithButton.js index 3260315c..e3c5a316 100644 --- a/src/components/TextInputWithButton/TextInputWithButton.js +++ b/src/components/TextInputWithButton/TextInputWithButton.js @@ -27,12 +27,12 @@ class TextInputWithButton extends PureComponent { }; render() { - const { onChange, onClick, icon, handleFocus, ...props } = this.props; + const { onChange, onClick, icon, handleFocus, isFocused } = this.props; return ( onChange(ev.target.value)} > From 74533f46810bb3e3f1ee294d3614079b19064935 Mon Sep 17 00:00:00 2001 From: AWolf81 Date: Mon, 26 Nov 2018 00:10:21 +0100 Subject: [PATCH 16/23] removed starter modal & changed starter selection --- package.json | 3 +- src/actions/index.js | 10 - src/components/CodesandboxLogo/Logo.js | 4 +- .../CreateNewProjectWizard.js | 5 - .../Gatsby/ProjectStarterSelection.js | 141 +++++--- .../Gatsby/SelectStarterDialog.js | 301 ------------------ .../Gatsby/SelectStarterList.js | 171 ++++++++++ .../CreateNewProjectWizard/MainPane.js | 22 +- .../CreateNewProjectWizard/ProjectName.js | 1 + .../CreateNewProjectWizard/SummaryPane.js | 10 + src/components/FormField/FormField.js | 25 +- .../TextInputWithButton.js | 12 +- src/reducers/modal.reducer.js | 6 - yarn.lock | 7 +- 14 files changed, 315 insertions(+), 403 deletions(-) delete mode 100644 src/components/CreateNewProjectWizard/Gatsby/SelectStarterDialog.js create mode 100644 src/components/CreateNewProjectWizard/Gatsby/SelectStarterList.js diff --git a/package.json b/package.json index 1434602a..95082b40 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "electron-store": "2.0.0", "electron-updater": "3.1.2", "fix-path": "2.1.0", + "fuse.js": "3.3.0", "gatsby-cli": "1.1.58", "js-yaml": "3.12.0", "node-fetch": "2.3.0", @@ -89,7 +90,7 @@ "dotenv-expand": "4.2.0", "electron": "2.0.1", "electron-builder": "20.28.4", - "electron-log": "^2.2.17", + "electron-log": "2.2.17", "eslint": "5.7.0", "eslint-config-react-app": "3.0.4", "eslint-loader": "1.9.0", diff --git a/src/actions/index.js b/src/actions/index.js index 672f1eaf..57674f51 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -23,8 +23,6 @@ export const CREATE_NEW_PROJECT_CANCEL = 'CREATE_NEW_PROJECT_CANCEL'; export const CREATE_NEW_PROJECT_FINISH = 'CREATE_NEW_PROJECT_FINISH'; export const ADD_PROJECT = 'ADD_PROJECT'; export const SHOW_MODAL = 'SHOW_MODAL'; -export const SHOW_STARTER_SELECTION = 'SHOW_STARTER_SELECTION'; -export const HIDE_STARTER_SELECTION = 'HIDE_STARTER_SELECTION'; export const CHANGE_PROJECT_HOME_PATH = 'CHANGE_PROJECT_HOME_PATH'; export const HIDE_MODAL = 'HIDE_MODAL'; export const DISMISS_SIDEBAR_INTRO = 'DISMISS_SIDEBAR_INTRO'; @@ -419,12 +417,4 @@ export const showResetStatePrompt = () => ({ type: SHOW_RESET_STATE_PROMPT, }); -export const showStarterSelectionModal = () => ({ - type: SHOW_STARTER_SELECTION, -}); - -export const hideStarterSelectionModal = () => ({ - type: HIDE_STARTER_SELECTION, -}); - export const resetAllState = () => ({ type: RESET_ALL_STATE }); diff --git a/src/components/CodesandboxLogo/Logo.js b/src/components/CodesandboxLogo/Logo.js index 5d711dc8..8badb675 100644 --- a/src/components/CodesandboxLogo/Logo.js +++ b/src/components/CodesandboxLogo/Logo.js @@ -3,8 +3,8 @@ import React from 'react'; import styled from 'styled-components'; const Logo = ({ - width = 35, - height = 35, + width = 32, + height = 32, className, }: { width: number, diff --git a/src/components/CreateNewProjectWizard/CreateNewProjectWizard.js b/src/components/CreateNewProjectWizard/CreateNewProjectWizard.js index 3f85672c..b580af75 100644 --- a/src/components/CreateNewProjectWizard/CreateNewProjectWizard.js +++ b/src/components/CreateNewProjectWizard/CreateNewProjectWizard.js @@ -22,7 +22,6 @@ import Debounced from '../Debounced'; import MainPane from './MainPane'; import SummaryPane from './SummaryPane'; import BuildPane from './BuildPane'; -import SelectStarterDialog from './Gatsby/SelectStarterDialog'; import type { Field, Status, Step } from './types'; @@ -284,10 +283,6 @@ class CreateNewProjectWizard extends PureComponent { /> } /> - )} diff --git a/src/components/CreateNewProjectWizard/Gatsby/ProjectStarterSelection.js b/src/components/CreateNewProjectWizard/Gatsby/ProjectStarterSelection.js index bdf0f510..701fa34d 100644 --- a/src/components/CreateNewProjectWizard/Gatsby/ProjectStarterSelection.js +++ b/src/components/CreateNewProjectWizard/Gatsby/ProjectStarterSelection.js @@ -1,15 +1,11 @@ // @flow -import React, { PureComponent } from 'react'; -import { connect } from 'react-redux'; +import React, { Fragment, Component } from 'react'; +import fetch from 'node-fetch'; // Note: This is using net.request from Node. Browser fetch throws CORS error. +import yaml from 'js-yaml'; +import Fuse from 'fuse.js'; import TextInputWithButton from '../../TextInputWithButton'; - -import * as actions from '../../../actions'; -import type { Dispatch } from '../../../actions/types'; - -type State = { - gatsbyStarter: string, -}; +import SelectStarterList from './SelectStarterList'; type Props = { projectStarter: string, @@ -17,56 +13,103 @@ type Props = { handleFocus: string => void, onSelect: string => void, onFocus: () => void, - showStarterSelection: Dispatch, }; -class ProjectStarter extends PureComponent { - // state = { - // gatsbyStarter: '', - // }; +type State = { + starterListVisible: boolean, + starters: Array, + paginationIndex: number, +}; + +const fuseOptions = { + shouldSort: true, + tokenize: true, + findAllMatches: true, + threshold: 0.6, + location: 0, + distance: 100, + maxPatternLength: 80, + minMatchCharLength: 1, + keys: ['repo', 'description'], +}; +class ProjectStarter extends Component { + state = { + starterListVisible: false, + filterString: '', + starters: [], + paginationIndex: 4, + }; + + PAGINATION_STEP = 4; - // static getDerivedStateFromProps(nextProps: Props, prevState: State) { - // return { - // gatsbyStarter: nextProps.projectStarter, - // }; - // } + componentDidMount() { + fetch( + 'https://raw.githubusercontent.com/gatsbyjs/gatsby/master/docs/starters.yml' + ) + .then(response => response.text()) + .then(yamlText => { + const starters = yaml.safeLoad(yamlText); + + this.setState({ + starters, + }); + }); + } - // Change method needed so we can dismiss the selection on close click of toastr - // changeGatsbyStarter = (selectedStarter: string) => { - // this.setState({ - // gatsbyStarter: selectedStarter, - // }); - // }; + handleShowMore = () => { + let newIndex = this.state.paginationIndex + this.PAGINATION_STEP; + newIndex = Math.min(this.state.starters.length, newIndex); // limit + this.setState({ + paginationIndex: newIndex, + }); + }; - // handleSelect = () => { - // this.props.onSelect(this.state.gatsbyStarter); - // }; + toggleStarterSelection = () => { + this.setState(state => ({ + starterListVisible: !state.starterListVisible, + })); + }; + + updateSearchString = filterString => { + this.setState({ + starterListVisible: filterString !== '', + }); + // Note: We're directly using the projectStarter to filter the list. + this.props.onSelect(filterString); + }; render() { - const { - handleFocus, - projectStarter, - isFocused, - onSelect, - showStarterSelection, - } = this.props; + const { handleFocus, projectStarter, isFocused, onSelect } = this.props; + const { starterListVisible, starters, paginationIndex } = this.state; + + const fuse = new Fuse(starters, fuseOptions); + const filteredStarters = + projectStarter === '' ? starters : fuse.search(projectStarter); + const limitedStarters = filteredStarters.slice(0, paginationIndex); return ( - + + + + ); } } -const mapDispatchToProps = { - showStarterSelection: actions.showStarterSelectionModal, -}; -export default connect( - null, - mapDispatchToProps -)(ProjectStarter); +export default ProjectStarter; diff --git a/src/components/CreateNewProjectWizard/Gatsby/SelectStarterDialog.js b/src/components/CreateNewProjectWizard/Gatsby/SelectStarterDialog.js deleted file mode 100644 index 5b26cc63..00000000 --- a/src/components/CreateNewProjectWizard/Gatsby/SelectStarterDialog.js +++ /dev/null @@ -1,301 +0,0 @@ -// @flow -import React, { PureComponent } from 'react'; -import { connect } from 'react-redux'; -import fetch from 'node-fetch'; // Note: This is using net.request from Node. Browser fetch throws CORS error. -import styled from 'styled-components'; -import yaml from 'js-yaml'; -import Scrollbars from 'react-custom-scrollbars'; -import { Tooltip } from 'react-tippy'; - -import Divider from '../../Divider'; -import Spacer from '../../Spacer'; -import ExternalLink from '../../ExternalLink'; -import TextInput from '../../TextInput'; -import Spinner from '../../Spinner'; -import CodesandboxLogo from '../../CodesandboxLogo'; - -import StrokeButton from '../../Button/StrokeButton'; -import Paragraph from '../../Paragraph'; -import Heading from '../../Heading'; -import Modal from '../../Modal'; -import ModalHeader from '../../ModalHeader'; - -import * as actions from '../../../actions'; - -import { COLORS } from '../../../constants'; -import type { Dispatch } from '../../../actions/types'; - -type Props = { - updateFieldValue: (string, string) => void, - selectedStarter: string, - isVisible: boolean, - hideModal: Dispatch, -}; - -type State = { - loading: boolean, - starters: Array, - selectedStarterInModal: string, - paginationIndex: number, - filterString: '', -}; - -/* -Gatsby starter selection dialog. -A starter object from starter.yml contains the following data: -[ - { - "url": "https://wonism.github.io/", - "repo": "https://github.com/wonism/gatsby-advanced-blog", - "description": "n/a", - "tags": [ - "Portfolio", - "Redux" - ], - "features": [ - "feature items" - ] - }, ... -] -*/ -class SelectStarterDialog extends PureComponent { - state = { - loading: true, - starters: [], - selectedStarterInModal: '', - paginationIndex: 10, - filterString: '', - }; - - PAGINATION_STEP = 10; - - static getDerivedStateFromProps(nextProps: Props, prevState: State) { - // Clear search string on modal open display - return { - ...prevState, - filterString: '', - selectedStarterInModal: nextProps.selectedStarter, - }; - } - - componentDidMount() { - fetch( - 'https://raw.githubusercontent.com/gatsbyjs/gatsby/master/docs/starters.yml' - ) - .then(response => response.text()) - .then(yamlText => { - const starters = yaml.safeLoad(yamlText); - - this.setState({ - loading: false, - starters, - }); - }); - } - - prepareUrlForCodesandbox(repoUrl: string) { - // Remove http protocol - const sandboxUrl = `https://codesandbox.io/s/${repoUrl.replace( - /(^\w+:|^)\/\//, - '' - )}`; - // Remove .com from github.com --> to have /s/github/repo - return sandboxUrl.replace(/\.com/, ''); - } - - handleDialogOK = () => { - this.props.updateFieldValue( - 'projectStarter', - this.state.selectedStarterInModal - ); - - this.setState({ - selectedStarterInModal: '', - }); - this.props.hideModal(); - }; - - setStarter = starter => { - this.setState({ - selectedStarterInModal: starter, - }); - }; - - handleShowMore = () => { - let newIndex = this.state.paginationIndex + this.PAGINATION_STEP; - newIndex = Math.min(this.state.starters.length, newIndex); // limit - this.setState({ - paginationIndex: newIndex, - }); - }; - - updateSearchString = evt => { - this.setState({ - filterString: evt.target.value, - }); - }; - - render() { - const { isVisible, hideModal } = this.props; - const { - loading, - starters, - selectedStarterInModal, - paginationIndex, - filterString, - } = this.state; - const filteredStarters = starters.filter( - starter => filterString === '' || starter.repo.includes(filterString) - ); - const disabledUseSelect = selectedStarterInModal === ''; - - return ( - - - - - - For a better overview you can also have a look at the{' '} - - Gatsby starters library - - - - - - - {loading && ( -
- -
- )} - {filteredStarters - .slice(0, paginationIndex) - .map((starter, index) => ( - - - this.setStarter(starter.repo)} - > - {starter.repo.split('/').pop()} - - - - - - - - - - - {starter.description !== 'n/a' && starter.description} - - - - ))} -
- - {/* Show more button if we're having more starters to display */} - {paginationIndex < filteredStarters.length && ( - - - Show more... - - - )} -
- - {/* Todo: Refactor OK/Cancel buttons into a component. So this is reusable. */} - - Use Selection - - ) : ( - 'Use Selection' - ) - } - /> - - Cancel - -
-
- ); - } -} - -/* - Actions could be refactored in a component - used here and in ProjectConfigurationModal -*/ -const Actions = styled.div` - display: flex; - justify-content: center; - align-items: center; - padding-top: 8px; -`; - -const ScrollContainer = styled(Scrollbars)` - min-height: 60vh; -`; - -const ShowMoreWrapper = styled.div` - padding: 10px; -`; - -const StarterList = styled.div` - padding: 15px; -`; -const StarterItem = styled.div` - padding: 8px 10px; -`; - -const StarterItemHeading = styled(Heading)` - cursor: pointer; - border-radius: 6px; - border: 2px solid - ${props => (props.selected ? COLORS.purple[500] : COLORS.gray[200])}; - padding: 6px; -`; - -const StarterItemTitle = styled.div` - display: flex; - justify-content: space-between; - align-items: center; -`; - -const MainContent = styled.div` - padding: 25px; -`; -const mapStateToProps = state => { - return { - isVisible: state.modal === 'new-project-wizard/select-starter', - }; -}; - -export default connect( - mapStateToProps, - { - hideModal: actions.hideStarterSelectionModal, - } -)(SelectStarterDialog); diff --git a/src/components/CreateNewProjectWizard/Gatsby/SelectStarterList.js b/src/components/CreateNewProjectWizard/Gatsby/SelectStarterList.js new file mode 100644 index 00000000..6dc37e45 --- /dev/null +++ b/src/components/CreateNewProjectWizard/Gatsby/SelectStarterList.js @@ -0,0 +1,171 @@ +// @flow +import React, { PureComponent } from 'react'; +import styled from 'styled-components'; +import Scrollbars from 'react-custom-scrollbars'; +import { Tooltip } from 'react-tippy'; + +import Divider from '../../Divider'; +import ExternalLink from '../../ExternalLink'; +import CodesandboxLogo from '../../CodesandboxLogo'; +import StrokeButton from '../../Button/StrokeButton'; + +import { COLORS } from '../../../constants'; + +type Props = { + updateStarter: string => void, + handleShowMore: () => void, + selectedStarter: string, + isVisible: boolean, + starters: [], + paginationIndex: number, + lastIndex: number, +}; + +/* +Gatsby starter selection dialog. +A starter object from starter.yml contains the following data: +[ + { + "url": "https://wonism.github.io/", + "repo": "https://github.com/wonism/gatsby-advanced-blog", + "description": "n/a", + "tags": [ + "Portfolio", + "Redux" + ], + "features": [ + "feature items" + ] + }, ... +] +*/ +class SelectStarterList extends PureComponent { + node: HTMLElement; + + handleUpdateStarter = (starter: string) => { + const { updateStarter } = this.props; + + updateStarter(starter); + + // Scroll to top as the selected starter will be the first entry + this.node.scrollTop(0); + }; + + prepareUrlForCodesandbox(repoUrl: string) { + // Remove http protocol + const sandboxUrl = `https://codesandbox.io/s/${repoUrl.replace( + /(^\w+:|^)\/\//, + '' + )}`; + // Remove .com from github.com --> to have /s/github/repo + return sandboxUrl.replace(/\.com/, ''); + } + + render() { + const { + isVisible, + starters, + selectedStarter, + paginationIndex, + lastIndex, + handleShowMore, + } = this.props; + + return ( + + (this.node = node)}> + + {starters.map((starter, index) => ( + + + this.handleUpdateStarter(starter.repo)} + > + {starter.repo.split('/').pop()} + + + + + + + + + + {starter.description !== 'n/a' && starter.description} + + + + ))} + + {/* Show more button if we're having more starters to display */} + {paginationIndex < lastIndex && ( + + + Show more... + + + )} + + + + ); + } +} + +const Description = styled.p` + font-size: 15px; + padding: 0 5px 2px; + hyphens: auto; + text-align: justify; +`; + +const ScrollContainer = styled(Scrollbars)` + min-height: 120px; + border: 1px solid ${COLORS.gray[400]}; + border-radius: 4px; +`; + +const ShowMoreWrapper = styled.div` + padding: 4px; +`; + +const StarterList = styled.div` + padding: 10px; +`; +const StarterItem = styled.div` + font-size: 15px; + padding: 4px; + padding-right: 10px; +`; + +const StarterItemHeading = styled.div` + cursor: pointer; + border-radius: 6px; + border: 2px solid + ${props => (props.selected ? COLORS.purple[500] : COLORS.gray[200])}; + padding: 2px 5px; + font-size 15px; + font-weight: bold; +`; + +const StarterItemTitle = styled.div` + display: flex; + justify-content: space-between; + align-items: center; +`; + +const MainContent = styled.div` + padding: 5px 0; + opacity: ${({ isVisible }) => (isVisible ? 1.0 : 0)}; + pointer-events: ${({ isVisible }) => (isVisible ? 'all' : 'none')}; + z-index: 1; +`; + +export default SelectStarterList; diff --git a/src/components/CreateNewProjectWizard/MainPane.js b/src/components/CreateNewProjectWizard/MainPane.js index 777a72ae..de87d7de 100644 --- a/src/components/CreateNewProjectWizard/MainPane.js +++ b/src/components/CreateNewProjectWizard/MainPane.js @@ -20,7 +20,7 @@ type Props = { projectName: string, projectType: ?ProjectType, projectIcon: ?string, - projectStarter: ?string, + projectStarter: string, activeField: ?Field, status: Status, currentStepIndex: number, @@ -31,15 +31,7 @@ type Props = { handleSubmit: () => Promise | void, }; -type State = { - gatsbyStarter: string, // Temporary value during selection in selection toast -}; - class MainPane extends PureComponent { - state = { - gatsbyStarter: '', - }; - handleFocusProjectName = () => this.props.focusField('projectName'); handleBlurProjectName = () => this.props.focusField(null); handleFocusStarter = () => this.props.focusField('projectStarter'); @@ -60,6 +52,7 @@ class MainPane extends PureComponent { { { label="Project Icon" focusOnClick={false} isFocused={activeField === 'projectIcon'} + spacing={10} > @@ -187,15 +182,12 @@ class MainPane extends PureComponent { } const Wrapper = styled.div` - height: 80vh; + min-height: 500px; + max-height: 90vh; will-change: transform; `; const SubmitButtonWrapper = styled.div` - position: absolute; - left: 0; - right: 0; - bottom: 30px; text-align: center; `; diff --git a/src/components/CreateNewProjectWizard/ProjectName.js b/src/components/CreateNewProjectWizard/ProjectName.js index 8e70216c..00c67f3a 100644 --- a/src/components/CreateNewProjectWizard/ProjectName.js +++ b/src/components/CreateNewProjectWizard/ProjectName.js @@ -173,6 +173,7 @@ class ProjectName extends PureComponent { label="Project Name" isFocused={isFocused} hasError={isProjectNameTaken} + spacing={15} > (this.node = node)} diff --git a/src/components/CreateNewProjectWizard/SummaryPane.js b/src/components/CreateNewProjectWizard/SummaryPane.js index ddb8c675..a9959c27 100644 --- a/src/components/CreateNewProjectWizard/SummaryPane.js +++ b/src/components/CreateNewProjectWizard/SummaryPane.js @@ -204,6 +204,16 @@ class SummaryPane extends PureComponent { bootstrap your project e.g. you can easily create your own blog by picking one of the blog starter templates. + + For a better overview you can also have a look at the{' '} + + Gatsby starters library + +
); diff --git a/src/components/FormField/FormField.js b/src/components/FormField/FormField.js index 36d2db29..a3cc32e3 100644 --- a/src/components/FormField/FormField.js +++ b/src/components/FormField/FormField.js @@ -12,16 +12,24 @@ type Props = { isFocused?: boolean, hasError?: boolean, children: React$Node, + spacing?: number, }; class FormField extends PureComponent { render() { - const { label, useLabelTag, isFocused, hasError, children } = this.props; + const { + label, + useLabelTag, + isFocused, + hasError, + children, + spacing, + } = this.props; const Wrapper = useLabelTag ? WrapperLabel : WrapperDiv; return ( - + {label} @@ -41,13 +49,18 @@ const getTextColor = (props: Props) => { } }; -const WrapperLabel = styled.label` +const WrapperLabel = styled.label.attrs({ + // default to 30px spacing between FormFields + spacing: props => props.spacing || 30, +})` display: block; - margin-bottom: 30px; + margin-bottom: ${props => props.spacing}px; `; -const WrapperDiv = styled.div` - margin-bottom: 30px; +const WrapperDiv = styled.div.attrs({ + spacing: props => props.spacing || 30, +})` + margin-bottom: ${props => props.spacing}px; `; const LabelText = styled(Label)` diff --git a/src/components/TextInputWithButton/TextInputWithButton.js b/src/components/TextInputWithButton/TextInputWithButton.js index e3c5a316..c5ba27b0 100644 --- a/src/components/TextInputWithButton/TextInputWithButton.js +++ b/src/components/TextInputWithButton/TextInputWithButton.js @@ -27,18 +27,16 @@ class TextInputWithButton extends PureComponent { }; render() { - const { onChange, onClick, icon, handleFocus, isFocused } = this.props; + const { onChange, onClick, icon, ...delegated } = this.props; return ( - onChange(ev.target.value)} - > + onChange(ev.target.value)}> window.requestAnimationFrame(handleFocus)} + onMouseDown={() => + window.requestAnimationFrame(delegated.handleFocus) + } onClick={onClick} style={{ width: 32, height: 32 }} > diff --git a/src/reducers/modal.reducer.js b/src/reducers/modal.reducer.js index d96dec79..c63d7015 100644 --- a/src/reducers/modal.reducer.js +++ b/src/reducers/modal.reducer.js @@ -12,8 +12,6 @@ import { SAVE_PROJECT_SETTINGS_FINISH, SHOW_PROJECT_SETTINGS, SHOW_APP_SETTINGS, - SHOW_STARTER_SELECTION, - HIDE_STARTER_SELECTION, HIDE_MODAL, RESET_ALL_STATE, } from '../actions'; @@ -30,7 +28,6 @@ export const initialState = null; export default (state: State = initialState, action: Action = {}) => { switch (action.type) { - case HIDE_STARTER_SELECTION: case CREATE_NEW_PROJECT_START: return 'new-project-wizard'; @@ -40,9 +37,6 @@ export default (state: State = initialState, action: Action = {}) => { case SHOW_APP_SETTINGS: return 'app-settings'; - case SHOW_STARTER_SELECTION: - return 'new-project-wizard/select-starter'; - case CREATE_NEW_PROJECT_CANCEL: case CREATE_NEW_PROJECT_FINISH: case IMPORT_EXISTING_PROJECT_START: diff --git a/yarn.lock b/yarn.lock index 65d7535b..ddfbe5e4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4971,7 +4971,7 @@ electron-localshortcut@^3.0.0: keyboardevent-from-electron-accelerator "^1.1.0" keyboardevents-areequal "^0.2.1" -electron-log@^2.2.17: +electron-log@2.2.17: version "2.2.17" resolved "https://registry.yarnpkg.com/electron-log/-/electron-log-2.2.17.tgz#e71e2ebb949fc96ded7cdb99eeee7202e48981d2" integrity sha512-v+Af5W5z99ehhaLOfE9eTSXUwjzh2wFlQjz51dvkZ6ZIrET6OB/zAZPvsuwT6tm3t5x+M1r+Ed3U3xtPZYAyuQ== @@ -6290,6 +6290,11 @@ functional-red-black-tree@^1.0.1: resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= +fuse.js@3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-3.3.0.tgz#1e4fe172a60687230fb54a5cb247eb96e2e7e885" + integrity sha512-ESBRkGLWMuVkapqYCcNO1uqMg5qbCKkgb+VS6wsy17Rix0/cMS9kSOZoYkjH8Ko//pgJ/EEGu0GTjk2mjX2LGQ== + fuse.js@^3.0.1, fuse.js@^3.2.0: version "3.2.1" resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-3.2.1.tgz#6320cb94ce56ec9755c89ade775bcdbb0358d425" From b0cddab72abfe28d8bbd14db614e8d3c29326f2b Mon Sep 17 00:00:00 2001 From: AWolf81 Date: Mon, 26 Nov 2018 00:55:09 +0100 Subject: [PATCH 17/23] small improvements --- .../CreateNewProjectWizard/BuildPane.js | 20 ++++++++----------- .../CreateNewProjectWizard.js | 3 +-- .../Gatsby/ProjectStarterSelection.js | 3 +-- .../Gatsby/SelectStarterList.js | 4 ++-- .../CreateNewProjectWizard/MainPane.js | 2 +- .../TextInputWithButton.js | 4 ++-- 6 files changed, 15 insertions(+), 21 deletions(-) diff --git a/src/components/CreateNewProjectWizard/BuildPane.js b/src/components/CreateNewProjectWizard/BuildPane.js index c933e72b..59793dda 100644 --- a/src/components/CreateNewProjectWizard/BuildPane.js +++ b/src/components/CreateNewProjectWizard/BuildPane.js @@ -70,21 +70,17 @@ class BuildPane extends PureComponent { // `runInstaller` becomes true, so we want it to be in its final position // when this happens. Otherwise, clicking files in the air instantly // "teleports" them a couple inches from the mouse :/ - this.buildProject().then(result => { - if (result) { - this.timeoutId = window.setTimeout(() => { - // Using promise not async as async can be problematic in life-cycle hooks - // Build can be started - this.timeoutId = window.setTimeout(() => { - this.setState({ runInstaller: true }); - }, 500); - }, 600); - } - }); + this.timeoutId = window.setTimeout(() => { + this.buildProject(); + + this.timeoutId = window.setTimeout(() => { + this.setState({ runInstaller: true }); + }, 500); + }, 600); } } - buildProject = async () => { + buildProject = () => { const { projectName, projectType, diff --git a/src/components/CreateNewProjectWizard/CreateNewProjectWizard.js b/src/components/CreateNewProjectWizard/CreateNewProjectWizard.js index b580af75..e31ed10e 100644 --- a/src/components/CreateNewProjectWizard/CreateNewProjectWizard.js +++ b/src/components/CreateNewProjectWizard/CreateNewProjectWizard.js @@ -40,7 +40,6 @@ type Props = { settings: AppSettings, projects: { [projectId: string]: ProjectInternal }, projectHomePath: string, - projectStarter: string, isVisible: boolean, isOnboardingCompleted: boolean, addProject: Dispatch, @@ -293,7 +292,7 @@ class CreateNewProjectWizard extends PureComponent { const mapStateToProps = state => ({ projects: getById(state), projectHomePath: getDefaultProjectPath(state), - isVisible: state.modal && state.modal.includes('new-project-wizard'), + isVisible: state.modal === 'new-project-wizard', isOnboardingCompleted: getOnboardingCompleted(state), settings: getAppSettings(state), }); diff --git a/src/components/CreateNewProjectWizard/Gatsby/ProjectStarterSelection.js b/src/components/CreateNewProjectWizard/Gatsby/ProjectStarterSelection.js index 701fa34d..5208491a 100644 --- a/src/components/CreateNewProjectWizard/Gatsby/ProjectStarterSelection.js +++ b/src/components/CreateNewProjectWizard/Gatsby/ProjectStarterSelection.js @@ -12,7 +12,6 @@ type Props = { isFocused: boolean, handleFocus: string => void, onSelect: string => void, - onFocus: () => void, }; type State = { @@ -70,7 +69,7 @@ class ProjectStarter extends Component { })); }; - updateSearchString = filterString => { + updateSearchString = (filterString: string) => { this.setState({ starterListVisible: filterString !== '', }); diff --git a/src/components/CreateNewProjectWizard/Gatsby/SelectStarterList.js b/src/components/CreateNewProjectWizard/Gatsby/SelectStarterList.js index 6dc37e45..76f88215 100644 --- a/src/components/CreateNewProjectWizard/Gatsby/SelectStarterList.js +++ b/src/components/CreateNewProjectWizard/Gatsby/SelectStarterList.js @@ -16,7 +16,7 @@ type Props = { handleShowMore: () => void, selectedStarter: string, isVisible: boolean, - starters: [], + starters: Array, paginationIndex: number, lastIndex: number, }; @@ -40,7 +40,7 @@ A starter object from starter.yml contains the following data: ] */ class SelectStarterList extends PureComponent { - node: HTMLElement; + node: any; handleUpdateStarter = (starter: string) => { const { updateStarter } = this.props; diff --git a/src/components/CreateNewProjectWizard/MainPane.js b/src/components/CreateNewProjectWizard/MainPane.js index de87d7de..22d9d80d 100644 --- a/src/components/CreateNewProjectWizard/MainPane.js +++ b/src/components/CreateNewProjectWizard/MainPane.js @@ -31,7 +31,7 @@ type Props = { handleSubmit: () => Promise | void, }; -class MainPane extends PureComponent { +class MainPane extends PureComponent { handleFocusProjectName = () => this.props.focusField('projectName'); handleBlurProjectName = () => this.props.focusField(null); handleFocusStarter = () => this.props.focusField('projectStarter'); diff --git a/src/components/TextInputWithButton/TextInputWithButton.js b/src/components/TextInputWithButton/TextInputWithButton.js index c5ba27b0..62938deb 100644 --- a/src/components/TextInputWithButton/TextInputWithButton.js +++ b/src/components/TextInputWithButton/TextInputWithButton.js @@ -12,10 +12,10 @@ import HoverableOutlineButton from '../HoverableOutlineButton'; type Props = { value: string, handleFocus: string => void, - onClick: () => void, onChange: string => void, - isFocused?: boolean, + onClick: () => void, onFocus: string => void, + isFocused?: boolean, icon: React$Node, }; From a69d553155b127a9aeb91ec01c0d89e4fb52a8df Mon Sep 17 00:00:00 2001 From: AWolf81 Date: Sun, 16 Dec 2018 23:21:58 +0100 Subject: [PATCH 18/23] added default spacing --- src/components/FormField/FormField.js | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/components/FormField/FormField.js b/src/components/FormField/FormField.js index a3cc32e3..6052f53e 100644 --- a/src/components/FormField/FormField.js +++ b/src/components/FormField/FormField.js @@ -12,10 +12,14 @@ type Props = { isFocused?: boolean, hasError?: boolean, children: React$Node, - spacing?: number, + spacing: number, }; class FormField extends PureComponent { + static defaultProps = { + spacing: 30, + }; + render() { const { label, @@ -49,17 +53,12 @@ const getTextColor = (props: Props) => { } }; -const WrapperLabel = styled.label.attrs({ - // default to 30px spacing between FormFields - spacing: props => props.spacing || 30, -})` +const WrapperLabel = styled.label` display: block; margin-bottom: ${props => props.spacing}px; `; -const WrapperDiv = styled.div.attrs({ - spacing: props => props.spacing || 30, -})` +const WrapperDiv = styled.div` margin-bottom: ${props => props.spacing}px; `; From 4830ae0761f47917d6ec112b7ab733d6610bbcc0 Mon Sep 17 00:00:00 2001 From: AWolf81 Date: Wed, 16 Jan 2019 00:00:09 +0100 Subject: [PATCH 19/23] fixed flow --- src/components/CreateNewProjectWizard/helpers.test.js | 2 +- src/services/config-variables.service.js | 4 ++-- src/services/config-variables.service.test.js | 2 +- src/services/create-project.service.js | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/CreateNewProjectWizard/helpers.test.js b/src/components/CreateNewProjectWizard/helpers.test.js index 73b24ec6..b3256b3f 100644 --- a/src/components/CreateNewProjectWizard/helpers.test.js +++ b/src/components/CreateNewProjectWizard/helpers.test.js @@ -1,4 +1,4 @@ -// @flow +/* eslint-disable flowtype/require-valid-file-annotation */ import { replaceProjectStarterStringWithUrl, defaultStarterUrl, diff --git a/src/services/config-variables.service.js b/src/services/config-variables.service.js index 7c186b22..bf89e93f 100644 --- a/src/services/config-variables.service.js +++ b/src/services/config-variables.service.js @@ -11,12 +11,12 @@ export type VariableMap = { // so with the following function we can replace the string $port with the real port number e.g. 3000 // (see type VariableMap for used mapping strings) export const substituteConfigVariables = ( - configObject: any, + configObject: Object, variableMap: VariableMap ) => { // e.g. $port inside args will be replaced with variable reference from variabeMap obj. {$port: port} return Object.keys(configObject).reduce( - (config, key) => { + (config: any, key) => { if (config[key] instanceof Array) { // replace $port inside args array // empty string is special here - we'd like to us it as replacement diff --git a/src/services/config-variables.service.test.js b/src/services/config-variables.service.test.js index 0c1dada2..db772c97 100644 --- a/src/services/config-variables.service.test.js +++ b/src/services/config-variables.service.test.js @@ -1,4 +1,4 @@ -// @flow +/* eslint-disable flowtype/require-valid-file-annotation */ import { substituteConfigVariables } from './config-variables.service'; describe('substitute config variables', () => { diff --git a/src/services/create-project.service.js b/src/services/create-project.service.js index b04f44a2..a22603dd 100644 --- a/src/services/create-project.service.js +++ b/src/services/create-project.service.js @@ -214,7 +214,7 @@ export const getBuildInstructions = ( throw new Error('Unrecognized project type: ' + projectType); } - const createCommand = substituteConfigVariables( + const createCommand: Object = substituteConfigVariables( projectConfigs[projectType].create, { $projectPath: projectPath, From c741cba1890e08b767a07a26c4c48bc75eb46673 Mon Sep 17 00:00:00 2001 From: AWolf81 Date: Thu, 17 Jan 2019 00:33:07 +0100 Subject: [PATCH 20/23] added tests --- config/testFrameworkSetup.js | 3 + package.json | 1 + .../Gatsby/MockStarterYaml.js | 74 ++++++++++++++++ .../Gatsby/ProjectStarterSelection.test.js | 65 ++++++++++++++ .../Gatsby/SelectStarterList.js | 7 +- .../Gatsby/SelectStarterList.test.js | 84 +++++++++++++++++++ .../CreateNewProjectWizard/MainPane.js | 3 +- yarn.lock | 31 +++++++ 8 files changed, 263 insertions(+), 5 deletions(-) create mode 100644 src/components/CreateNewProjectWizard/Gatsby/MockStarterYaml.js create mode 100644 src/components/CreateNewProjectWizard/Gatsby/ProjectStarterSelection.test.js create mode 100644 src/components/CreateNewProjectWizard/Gatsby/SelectStarterList.test.js diff --git a/config/testFrameworkSetup.js b/config/testFrameworkSetup.js index 4a2eaea7..ed431e14 100644 --- a/config/testFrameworkSetup.js +++ b/config/testFrameworkSetup.js @@ -1,6 +1,7 @@ const { JSDOM } = require('jsdom'); const jsdom = new JSDOM(''); const { window } = jsdom; +const fetch = require('jest-fetch-mock'); global.document = window.document; global.window = window; @@ -19,3 +20,5 @@ global.cancelAnimationFrame = function(id) { require('jsdom-global')(); // Import test framework for styled components for better snapshot messages require('jest-styled-components'); + +jest.setMock('node-fetch', fetch); diff --git a/package.json b/package.json index 2a0630e0..c3d2bcb3 100644 --- a/package.json +++ b/package.json @@ -114,6 +114,7 @@ "immer": "1.3.1", "import-all.macro": "2.0.3", "jest": "23.3", + "jest-fetch-mock": "2.1.0", "jest-styled-components": "6.2.2", "jsdom": "13.0.0", "jsdom-global": "3.0.2", diff --git a/src/components/CreateNewProjectWizard/Gatsby/MockStarterYaml.js b/src/components/CreateNewProjectWizard/Gatsby/MockStarterYaml.js new file mode 100644 index 00000000..5bc2a65d --- /dev/null +++ b/src/components/CreateNewProjectWizard/Gatsby/MockStarterYaml.js @@ -0,0 +1,74 @@ +// @flow +// Exporting 6 starters for testing +export default ` +- url: https://wonism.github.io/ + repo: https://github.com/wonism/gatsby-advanced-blog + description: n/a + tags: + - Portfolio + - Redux + features: + - Blog post listing with previews (image + summary) for each blog post + - Categories and tags for blog posts with pagination + - Search post with keyword + - Put react application / tweet into post + - Copy some codes in post with clicking button + - Portfolio + - Resume + - Redux for managing statement (with redux-saga / reselect) +- url: https://vagr9k.github.io/gatsby-advanced-starter/ + repo: https://github.com/Vagr9K/gatsby-advanced-starter + description: Great for learning about advanced features and their implementations + tags: + - Styling:None + features: + - Does not contain any UI frameworks + - Provides only a skeleton + - Tags + - Categories + - Google Analytics + - Disqus + - Offline support + - Web App Manifest + - SEO +- url: https://gatsby-tailwind-emotion-starter.netlify.com/ + repo: https://github.com/muhajirframe/gatsby-tailwind-emotion-starter + description: A Gatsby Starter with Tailwind CSS + Emotion JS + tags: + - Styling:Tailwind + features: + - Eslint Airbnb without semicolon and without .jsx extension + - Offline support + - Web App Manifest +- url: https://gatsby-starter-redux-firebase.netlify.com/ + repo: https://github.com/muhajirframe/gatsby-starter-redux-firebase + description: A Gatsby + Redux + Firebase Starter. With Authentication + tags: + - Styling:None + - Firebase + - Client-side App + features: + - Eslint Airbnb without semicolon and without .jsx extension + - Firebase + - Web App Manifest +- url: https://dschau.github.io/gatsby-blog-starter-kit/ + repo: https://github.com/dschau/gatsby-blog-starter-kit + description: n/a + tags: + - Blog + features: + - Blog post listing with previews for each blog post + - Navigation between posts with a previous/next post button + - Tags and tag navigation +- url: https://contentful-userland.github.io/gatsby-contentful-starter/ + repo: https://github.com/contentful-userland/gatsby-contentful-starter + description: n/a + tags: + - Blog + - Contentful + - Headless CMS + features: + - Based on the Gatsby Starter Blog + - Includes Contentful Delivery API for production build + - Includes Contentful Preview API for development +`; diff --git a/src/components/CreateNewProjectWizard/Gatsby/ProjectStarterSelection.test.js b/src/components/CreateNewProjectWizard/Gatsby/ProjectStarterSelection.test.js new file mode 100644 index 00000000..b9377a93 --- /dev/null +++ b/src/components/CreateNewProjectWizard/Gatsby/ProjectStarterSelection.test.js @@ -0,0 +1,65 @@ +/* eslint-disable flowtype/require-valid-file-annotation */ +import React from 'react'; +import { shallow } from 'enzyme'; +import ProjectStarterSelection from './ProjectStarterSelection'; +import fetch from 'node-fetch'; + +import mockStartersYaml from './MockStarterYaml'; + +describe('ProjectStarterSelection component', () => { + let wrapper; + let instance; + let mockedOnSelect; + + fetch.mockResponse(mockStartersYaml); + + beforeEach(() => { + mockedOnSelect = jest.fn(); + wrapper = shallow(); + instance = wrapper.instance(); + }); + + it('should render two elements', () => { + expect(wrapper.children()).toHaveLength(2); + }); + + it('should use all starters (paginated)', () => { + // const wrapperAllStarters = shallow(); + expect(instance.state.starters).toHaveLength(6); + wrapper.setProps({ projectStarter: '' }); + expect( + wrapper + .children() + .at(1) + .props().starters + ).toHaveLength(instance.PAGINATION_STEP); + }); + + it('should fetch starters (mocked)', () => { + expect(instance.state.starters).toHaveLength(6); + }); + + it('should increment paginationIndex with limiting', () => { + instance.PAGINATION_STEP = 1; // reduce pagination step for testing + expect(instance.state.paginationIndex).toBe(4); + instance.handleShowMore(); + expect(instance.state.paginationIndex).toBe(5); + instance.handleShowMore(); + instance.handleShowMore(); // check limiting to length + expect(instance.state.paginationIndex).toBe(6); + }); + + it('should toggle list visibility', () => { + expect(instance.state.starterListVisible).toBeFalsy(); + instance.toggleStarterSelection(); + expect(instance.state.starterListVisible).toBeTruthy(); + }); + + it('should update search string & call onSelect', () => { + const starter = 'blog'; + instance.updateSearchString(starter); + expect(instance.state.starterListVisible).toBeTruthy(); + expect(mockedOnSelect.mock.calls).toHaveLength(1); + expect(mockedOnSelect).toBeCalledWith(starter); + }); +}); diff --git a/src/components/CreateNewProjectWizard/Gatsby/SelectStarterList.js b/src/components/CreateNewProjectWizard/Gatsby/SelectStarterList.js index 76f88215..a0212a12 100644 --- a/src/components/CreateNewProjectWizard/Gatsby/SelectStarterList.js +++ b/src/components/CreateNewProjectWizard/Gatsby/SelectStarterList.js @@ -132,20 +132,21 @@ const ScrollContainer = styled(Scrollbars)` border-radius: 4px; `; -const ShowMoreWrapper = styled.div` +export const ShowMoreWrapper = styled.div` padding: 4px; `; const StarterList = styled.div` padding: 10px; `; -const StarterItem = styled.div` + +export const StarterItem = styled.div` font-size: 15px; padding: 4px; padding-right: 10px; `; -const StarterItemHeading = styled.div` +export const StarterItemHeading = styled.div` cursor: pointer; border-radius: 6px; border: 2px solid diff --git a/src/components/CreateNewProjectWizard/Gatsby/SelectStarterList.test.js b/src/components/CreateNewProjectWizard/Gatsby/SelectStarterList.test.js new file mode 100644 index 00000000..f22d9f9e --- /dev/null +++ b/src/components/CreateNewProjectWizard/Gatsby/SelectStarterList.test.js @@ -0,0 +1,84 @@ +/* eslint-disable flowtype/require-valid-file-annotation */ +import React from 'react'; +import { shallow, mount } from 'enzyme'; +import yaml from 'js-yaml'; + +import SelectStarterList, { + StarterItem, + StarterItemHeading, + ShowMoreWrapper, +} from './SelectStarterList'; +import mockStartersYaml from './MockStarterYaml'; + +describe('ProjectStarterSelection component', () => { + let wrapper; + let instance; + const starters = yaml.safeLoad(mockStartersYaml); + const mockHandleShowMore = jest.fn(); + const mockUpdateStarter = jest.fn(); + + beforeEach(() => { + wrapper = shallow( + + ); + instance = wrapper.instance(); + }); + + it('should render a list with 6 items', () => { + expect(wrapper.find(StarterItem)).toHaveLength(6); + }); + + it('should render ShowMoreWrapper', () => { + expect(wrapper.find(ShowMoreWrapper)).toBeDefined(); + }); + + it('should add node reference to ScrollContainer', () => { + // Mount required so innerRef will be set + const mountedWrapper = mount( + + ); + expect(mountedWrapper.instance().node).toBeDefined(); + }); + + it('should call handleShowMore', () => { + const showMoreButton = wrapper + .find(ShowMoreWrapper) + .children() + .first(); + showMoreButton.simulate('click'); + expect(mockHandleShowMore).toBeCalled(); + }); + + it('should call updateStarter', () => { + // mock this.node + instance.node = { + scrollTop: jest.fn(), + }; + wrapper + .find(StarterItemHeading) + .first() + .simulate('click'); + expect(mockUpdateStarter).toBeCalledWith(starters[0].repo); + // check scrollTop call + expect(instance.node.scrollTop).toBeCalledWith(0); + }); + + it('should replace github.com with github in Codesandbox url', () => { + const url = 'https://github.com/wonism/gatsby-advanced-blog'; + const expectedSandboxUrl = + 'https://codesandbox.io/s/github/wonism/gatsby-advanced-blog'; + + expect(instance.prepareUrlForCodesandbox(url)).toBe(expectedSandboxUrl); + }); +}); diff --git a/src/components/CreateNewProjectWizard/MainPane.js b/src/components/CreateNewProjectWizard/MainPane.js index a802ced1..21e99243 100644 --- a/src/components/CreateNewProjectWizard/MainPane.js +++ b/src/components/CreateNewProjectWizard/MainPane.js @@ -190,8 +190,7 @@ const Wrapper = animated(styled.div.attrs({ transform: `translateY(${props.translateY}px)`, }), })` - min-height: 500px; - max-height: 90vh; + height: 75vh; will-change: transform; `); diff --git a/yarn.lock b/yarn.lock index 870d97ad..5756c109 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4175,6 +4175,14 @@ cross-env@5.2.0: cross-spawn "^6.0.5" is-windows "^1.0.0" +cross-fetch@^2.2.2: + version "2.2.3" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-2.2.3.tgz#e8a0b3c54598136e037f8650f8e823ccdfac198e" + integrity sha512-PrWWNH3yL2NYIb/7WF/5vFG3DCQiXDOVf8k3ijatbrtnwNuhMWLC7YF7uqf53tbTFDzHIUD8oITw4Bxt8ST3Nw== + dependencies: + node-fetch "2.1.2" + whatwg-fetch "2.0.4" + cross-spawn@5.1.0, cross-spawn@^5.0.1: version "5.1.0" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" @@ -8066,6 +8074,14 @@ jest-environment-node@^23.4.0: jest-mock "^23.2.0" jest-util "^23.4.0" +jest-fetch-mock@2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/jest-fetch-mock/-/jest-fetch-mock-2.1.0.tgz#49c16451b82f311158ec897e467d704e0cb118f9" + integrity sha512-jrTNlxDsZZCq6tMhdyH7gIbt4iDUHRr6C4Jp+kXItLaaaladOm9/wJjIwU3tCAEohbuW/7/naOSfg2A8H6/35g== + dependencies: + cross-fetch "^2.2.2" + promise-polyfill "^7.1.1" + jest-get-type@^22.1.0: version "22.4.3" resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-22.4.3.tgz#e3a8504d8479342dd4420236b322869f18900ce4" @@ -9568,6 +9584,11 @@ node-emoji@^1.0.4: dependencies: lodash.toarray "^4.4.0" +node-fetch@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.1.2.tgz#ab884e8e7e57e38a944753cec706f788d1768bb5" + integrity sha1-q4hOjn5X44qUR1POxwb3iNF2i7U= + node-fetch@2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.3.0.tgz#1a1d940bbfb916a1d3e0219f037e89e71f8c5fa5" @@ -10890,6 +10911,11 @@ promise-inflight@^1.0.1: resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM= +promise-polyfill@^7.1.1: + version "7.1.2" + resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-7.1.2.tgz#ab05301d8c28536301622d69227632269a70ca3b" + integrity sha512-FuEc12/eKqqoRYIGBrUptCBRhobL19PS2U31vMNTfyck1FxPyMfgsXyW4Mav85y/ZN1hop3hOwRlUDok23oYfQ== + promise.prototype.finally@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/promise.prototype.finally/-/promise.prototype.finally-3.1.0.tgz#66f161b1643636e50e7cf201dc1b84a857f3864e" @@ -14165,6 +14191,11 @@ whatwg-encoding@^1.0.5: dependencies: iconv-lite "0.4.24" +whatwg-fetch@2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz#dde6a5df315f9d39991aa17621853d720b85566f" + integrity sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng== + whatwg-fetch@>=0.10.0: version "2.0.3" resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz#9c84ec2dcf68187ff00bc64e1274b442176e1c84" From c8e56fdc06904f141da6c5d66edb25e55f58ccf5 Mon Sep 17 00:00:00 2001 From: AWolf81 Date: Thu, 17 Jan 2019 21:25:29 +0100 Subject: [PATCH 21/23] removed commented line --- .../Gatsby/ProjectStarterSelection.test.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/CreateNewProjectWizard/Gatsby/ProjectStarterSelection.test.js b/src/components/CreateNewProjectWizard/Gatsby/ProjectStarterSelection.test.js index b9377a93..e4998c51 100644 --- a/src/components/CreateNewProjectWizard/Gatsby/ProjectStarterSelection.test.js +++ b/src/components/CreateNewProjectWizard/Gatsby/ProjectStarterSelection.test.js @@ -24,7 +24,6 @@ describe('ProjectStarterSelection component', () => { }); it('should use all starters (paginated)', () => { - // const wrapperAllStarters = shallow(); expect(instance.state.starters).toHaveLength(6); wrapper.setProps({ projectStarter: '' }); expect( From 6ac1a1724ba765086905a65bfd5fe2eebbf66b95 Mon Sep 17 00:00:00 2001 From: AWolf81 Date: Sun, 20 Jan 2019 16:13:38 +0100 Subject: [PATCH 22/23] addressed review - new typeahead logic --- package.json | 2 +- src/components/App/App.js | 13 ++- .../Gatsby/ProjectStarterSelection.js | 89 ++++++++++++++----- .../Gatsby/ProjectStarterSelection.test.js | 71 +++++++++++++-- .../Gatsby/SelectStarterList.js | 11 ++- .../Gatsby/SelectStarterList.test.js | 2 +- 6 files changed, 151 insertions(+), 37 deletions(-) diff --git a/package.json b/package.json index c3d2bcb3..7616e8dc 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,6 @@ "fuse.js": "3.3.0", "gatsby-cli": "1.1.58", "js-yaml": "3.12.0", - "node-fetch": "2.3.0", "ps-tree": "1.1.0", "react-custom-scrollbars": "4.2.1", "rimraf": "2.6.2", @@ -122,6 +121,7 @@ "loader-utils": "1.1.0", "mixpanel-browser": "2.22.4", "moment": "2.22.2", + "node-fetch": "2.3.0", "postcss-flexbugs-fixes": "3.2.0", "postcss-loader": "2.0.10", "prettier": "1.13.7", diff --git a/src/components/App/App.js b/src/components/App/App.js index c4c208cd..f4fc2d3f 100644 --- a/src/components/App/App.js +++ b/src/components/App/App.js @@ -26,7 +26,7 @@ type Props = { class App extends PureComponent { render() { - const { selectedProjectId } = this.props; + const { selectedProjectId, modalVisible } = this.props; return ( {wasSuccessfullyInitialized => @@ -37,7 +37,7 @@ class App extends PureComponent { - + {selectedProjectId ? : } @@ -69,12 +69,19 @@ const Wrapper = styled.div` const MainContent = styled.div` position: relative; - min-height: 100vh; flex: 1; + ${props => + props.disableScroll + ? ` + height: 100vh; + overflow: hidden; + ` + : 'min-height: 100vh;'}; `; const mapStateToProps = state => ({ selectedProjectId: getSelectedProjectId(state), + modalVisible: !!state.modal, }); export default connect(mapStateToProps)(App); diff --git a/src/components/CreateNewProjectWizard/Gatsby/ProjectStarterSelection.js b/src/components/CreateNewProjectWizard/Gatsby/ProjectStarterSelection.js index 5208491a..4edb1c3b 100644 --- a/src/components/CreateNewProjectWizard/Gatsby/ProjectStarterSelection.js +++ b/src/components/CreateNewProjectWizard/Gatsby/ProjectStarterSelection.js @@ -18,6 +18,9 @@ type State = { starterListVisible: boolean, starters: Array, paginationIndex: number, + filterString: string, + loading: boolean, + error: ?object, }; const fuseOptions = { @@ -37,12 +40,15 @@ class ProjectStarter extends Component { filterString: '', starters: [], paginationIndex: 4, + loading: true, + error: null, }; PAGINATION_STEP = 4; + filteredStarters = []; componentDidMount() { - fetch( + return fetch( 'https://raw.githubusercontent.com/gatsbyjs/gatsby/master/docs/starters.yml' ) .then(response => response.text()) @@ -51,6 +57,14 @@ class ProjectStarter extends Component { this.setState({ starters, + loading: false, + error: null, + }); + }) + .catch(error => { + this.setState({ + loading: false, + error, }); }); } @@ -63,29 +77,59 @@ class ProjectStarter extends Component { }); }; + handleOnSelect = (selectedStarterUrl, removeSelection) => { + const { onSelect } = this.props; + onSelect(removeSelection ? '' : selectedStarterUrl); + }; + toggleStarterSelection = () => { + const { projectStarter } = this.props; this.setState(state => ({ - starterListVisible: !state.starterListVisible, + starterListVisible: !state.starterListVisible || !!projectStarter, })); }; updateSearchString = (filterString: string) => { + const { projectStarter } = this.props; this.setState({ - starterListVisible: filterString !== '', + starterListVisible: filterString !== '' || !!projectStarter, + filterString, }); - // Note: We're directly using the projectStarter to filter the list. - this.props.onSelect(filterString); }; render() { - const { handleFocus, projectStarter, isFocused, onSelect } = this.props; - const { starterListVisible, starters, paginationIndex } = this.state; + const { handleFocus, projectStarter, isFocused } = this.props; + const { + starterListVisible, + starters, + paginationIndex, + filterString, + loading, + error, + } = this.state; + let fuse; + let selectedStarter = []; + let limitedStarters = []; - const fuse = new Fuse(starters, fuseOptions); - const filteredStarters = - projectStarter === '' ? starters : fuse.search(projectStarter); - const limitedStarters = filteredStarters.slice(0, paginationIndex); + if (!loading && starters) { + // Loaded & starters available + fuse = new Fuse(starters, fuseOptions); + this.filteredStarters = + filterString === '' ? starters : fuse.search(filterString); //projectStarter); + selectedStarter = starters.find( + starter => starter.repo === projectStarter + ); + if (selectedStarter) { + // Remove the selected from list if available + this.filteredStarters = this.filteredStarters.filter( + starter => starter !== selectedStarter + ); + // Always re-add selectedStarter to top of list otherwise the selected starter could be on a different pagination page + this.filteredStarters.unshift(selectedStarter); // add selected as first element as we always want the selected to be in the list + } + limitedStarters = this.filteredStarters.slice(0, paginationIndex); + } return ( { onFocus={handleFocus} handleFocus={handleFocus} isFocused={isFocused} - value={projectStarter} + value={filterString} onClick={this.toggleStarterSelection} /> - + {!loading && + !error && ( + + )} ); } diff --git a/src/components/CreateNewProjectWizard/Gatsby/ProjectStarterSelection.test.js b/src/components/CreateNewProjectWizard/Gatsby/ProjectStarterSelection.test.js index e4998c51..a57be806 100644 --- a/src/components/CreateNewProjectWizard/Gatsby/ProjectStarterSelection.test.js +++ b/src/components/CreateNewProjectWizard/Gatsby/ProjectStarterSelection.test.js @@ -11,9 +11,35 @@ describe('ProjectStarterSelection component', () => { let instance; let mockedOnSelect; - fetch.mockResponse(mockStartersYaml); + describe('Fetch starters', () => { + beforeEach(() => { + fetch.resetMocks(); + }); + + it('should succeed', async done => { + fetch.mockResponse(mockStartersYaml); + wrapper = shallow(); + instance = wrapper.instance(); + + await instance.componentDidMount(); + expect(instance.state.starters).toHaveLength(6); + done(); + }); + + it('should fail', async done => { + const error = new Error('ENOTFOUND'); + fetch.mockReject(error); + wrapper = shallow(); + instance = wrapper.instance(); + + await instance.componentDidMount(); + expect(instance.state.error).toBe(error); + done(); + }); + }); beforeEach(() => { + fetch.mockResponse(mockStartersYaml); mockedOnSelect = jest.fn(); wrapper = shallow(); instance = wrapper.instance(); @@ -34,10 +60,6 @@ describe('ProjectStarterSelection component', () => { ).toHaveLength(instance.PAGINATION_STEP); }); - it('should fetch starters (mocked)', () => { - expect(instance.state.starters).toHaveLength(6); - }); - it('should increment paginationIndex with limiting', () => { instance.PAGINATION_STEP = 1; // reduce pagination step for testing expect(instance.state.paginationIndex).toBe(4); @@ -52,13 +74,46 @@ describe('ProjectStarterSelection component', () => { expect(instance.state.starterListVisible).toBeFalsy(); instance.toggleStarterSelection(); expect(instance.state.starterListVisible).toBeTruthy(); + + // Don't hide list if projectStarter selected + wrapper.setProps({ projectStarter: 'blog' }); + instance.toggleStarterSelection(); + expect(instance.state.starterListVisible).toBeTruthy(); }); - it('should update search string & call onSelect', () => { + it('should deselect starter if selectedStarter clicked', () => { + wrapper.setProps({ projectStarter: 'blog' }); + instance.handleOnSelect('blog', true); + expect(mockedOnSelect).toBeCalledWith(''); + }); + + it('should update projectStarter', () => { + instance.handleOnSelect('blog', false); + expect(mockedOnSelect).toBeCalledWith('blog'); + }); + + it('should update search string & display starter list', () => { const starter = 'blog'; instance.updateSearchString(starter); expect(instance.state.starterListVisible).toBeTruthy(); - expect(mockedOnSelect.mock.calls).toHaveLength(1); - expect(mockedOnSelect).toBeCalledWith(starter); + expect(instance.state.filterString).toEqual(starter); + + wrapper.setProps({ projectStarter: 'blog' }); + instance.updateSearchString(''); + expect(instance.state.starterListVisible).toBeTruthy(); + + wrapper.setProps({ projectStarter: '' }); + instance.updateSearchString(''); + expect(instance.state.starterListVisible).toBeFalsy(); + }); + + it('should move the selected starter to first entry of starter list', () => { + const blogStarterUrl = 'https://github.com/dschau/gatsby-blog-starter-kit'; + wrapper.setProps({ projectStarter: blogStarterUrl }); + expect( + instance.filteredStarters.findIndex( + starter => starter.repo === blogStarterUrl + ) + ).toBe(0); }); }); diff --git a/src/components/CreateNewProjectWizard/Gatsby/SelectStarterList.js b/src/components/CreateNewProjectWizard/Gatsby/SelectStarterList.js index a0212a12..fce59821 100644 --- a/src/components/CreateNewProjectWizard/Gatsby/SelectStarterList.js +++ b/src/components/CreateNewProjectWizard/Gatsby/SelectStarterList.js @@ -42,10 +42,10 @@ A starter object from starter.yml contains the following data: class SelectStarterList extends PureComponent { node: any; - handleUpdateStarter = (starter: string) => { + handleUpdateStarter = (starter: string, removeSelection: boolean) => { const { updateStarter } = this.props; - updateStarter(starter); + updateStarter(starter, removeSelection); // Scroll to top as the selected starter will be the first entry this.node.scrollTop(0); @@ -80,7 +80,12 @@ class SelectStarterList extends PureComponent { this.handleUpdateStarter(starter.repo)} + onClick={() => + this.handleUpdateStarter( + starter.repo, + selectedStarter === starter.repo + ) + } > {starter.repo.split('/').pop()} diff --git a/src/components/CreateNewProjectWizard/Gatsby/SelectStarterList.test.js b/src/components/CreateNewProjectWizard/Gatsby/SelectStarterList.test.js index f22d9f9e..e98513ee 100644 --- a/src/components/CreateNewProjectWizard/Gatsby/SelectStarterList.test.js +++ b/src/components/CreateNewProjectWizard/Gatsby/SelectStarterList.test.js @@ -69,7 +69,7 @@ describe('ProjectStarterSelection component', () => { .find(StarterItemHeading) .first() .simulate('click'); - expect(mockUpdateStarter).toBeCalledWith(starters[0].repo); + expect(mockUpdateStarter).toBeCalledWith(starters[0].repo, false); // check scrollTop call expect(instance.node.scrollTop).toBeCalledWith(0); }); From 088357e1bf0e39a8fbb0b950bc010eca6f3d67e3 Mon Sep 17 00:00:00 2001 From: AWolf81 Date: Sun, 20 Jan 2019 16:29:25 +0100 Subject: [PATCH 23/23] fixed flow --- src/components/App/App.js | 1 + .../Gatsby/ProjectStarterSelection.js | 6 +++--- .../CreateNewProjectWizard/Gatsby/SelectStarterList.js | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/components/App/App.js b/src/components/App/App.js index f4fc2d3f..cce84ae0 100644 --- a/src/components/App/App.js +++ b/src/components/App/App.js @@ -22,6 +22,7 @@ import type { Project } from '../../types'; type Props = { selectedProjectId: ?Project, + modalVisible: boolean, }; class App extends PureComponent { diff --git a/src/components/CreateNewProjectWizard/Gatsby/ProjectStarterSelection.js b/src/components/CreateNewProjectWizard/Gatsby/ProjectStarterSelection.js index 4edb1c3b..9bb7457e 100644 --- a/src/components/CreateNewProjectWizard/Gatsby/ProjectStarterSelection.js +++ b/src/components/CreateNewProjectWizard/Gatsby/ProjectStarterSelection.js @@ -20,7 +20,7 @@ type State = { paginationIndex: number, filterString: string, loading: boolean, - error: ?object, + error: ?Error, }; const fuseOptions = { @@ -45,7 +45,7 @@ class ProjectStarter extends Component { }; PAGINATION_STEP = 4; - filteredStarters = []; + filteredStarters: Array = []; componentDidMount() { return fetch( @@ -77,7 +77,7 @@ class ProjectStarter extends Component { }); }; - handleOnSelect = (selectedStarterUrl, removeSelection) => { + handleOnSelect = (selectedStarterUrl: string, removeSelection: boolean) => { const { onSelect } = this.props; onSelect(removeSelection ? '' : selectedStarterUrl); }; diff --git a/src/components/CreateNewProjectWizard/Gatsby/SelectStarterList.js b/src/components/CreateNewProjectWizard/Gatsby/SelectStarterList.js index fce59821..5505f75b 100644 --- a/src/components/CreateNewProjectWizard/Gatsby/SelectStarterList.js +++ b/src/components/CreateNewProjectWizard/Gatsby/SelectStarterList.js @@ -12,7 +12,7 @@ import StrokeButton from '../../Button/StrokeButton'; import { COLORS } from '../../../constants'; type Props = { - updateStarter: string => void, + updateStarter: (string, boolean) => void, handleShowMore: () => void, selectedStarter: string, isVisible: boolean,