diff --git a/packages/@vue/cli-ui/src/App.vue b/packages/@vue/cli-ui/src/App.vue index 44018328f2..b5df14fb76 100644 --- a/packages/@vue/cli-ui/src/App.vue +++ b/packages/@vue/cli-ui/src/App.vue @@ -1,10 +1,40 @@ + + + + diff --git a/packages/@vue/cli-ui/src/components/FolderExplorer.vue b/packages/@vue/cli-ui/src/components/FolderExplorer.vue index 23ffa42470..796dd9dd44 100644 --- a/packages/@vue/cli-ui/src/components/FolderExplorer.vue +++ b/packages/@vue/cli-ui/src/components/FolderExplorer.vue @@ -214,13 +214,13 @@ export default { @import "~@/style/imports" .toolbar - padding 12px + padding $padding-item background $color-light-background h-box() align-items center >>> > * - space-between-x(12px) + space-between-x($padding-item) .current-path flex 100% 1 1 diff --git a/packages/@vue/cli-ui/src/components/FolderExplorerItem.vue b/packages/@vue/cli-ui/src/components/FolderExplorerItem.vue index bc677fd3d6..0eb68b03f6 100644 --- a/packages/@vue/cli-ui/src/components/FolderExplorerItem.vue +++ b/packages/@vue/cli-ui/src/components/FolderExplorerItem.vue @@ -33,7 +33,7 @@ export default { .folder-name flex 100% 1 1 - margin-left 12px + margin-left $padding-item ellipsis() .vue-project-icon @@ -44,7 +44,7 @@ export default { top 5px .foder-explorer-item - padding 12px + padding $padding-item h-box() align-items center user-select none diff --git a/packages/@vue/cli-ui/src/components/ListItemInfo.vue b/packages/@vue/cli-ui/src/components/ListItemInfo.vue new file mode 100644 index 0000000000..29bd56cc2a --- /dev/null +++ b/packages/@vue/cli-ui/src/components/ListItemInfo.vue @@ -0,0 +1,62 @@ + + + + + diff --git a/packages/@vue/cli-ui/src/components/ProjectFeatureItem.vue b/packages/@vue/cli-ui/src/components/ProjectFeatureItem.vue index 62ccf77b00..8a77ef0daa 100644 --- a/packages/@vue/cli-ui/src/components/ProjectFeatureItem.vue +++ b/packages/@vue/cli-ui/src/components/ProjectFeatureItem.vue @@ -1,34 +1,32 @@ + + diff --git a/packages/@vue/cli-ui/src/components/PromptCheckbox.vue b/packages/@vue/cli-ui/src/components/PromptCheckbox.vue new file mode 100644 index 0000000000..d545e5ad15 --- /dev/null +++ b/packages/@vue/cli-ui/src/components/PromptCheckbox.vue @@ -0,0 +1,65 @@ + + + + + diff --git a/packages/@vue/cli-ui/src/components/PromptConfirm.vue b/packages/@vue/cli-ui/src/components/PromptConfirm.vue new file mode 100644 index 0000000000..4dd74007c4 --- /dev/null +++ b/packages/@vue/cli-ui/src/components/PromptConfirm.vue @@ -0,0 +1,33 @@ + + + + + diff --git a/packages/@vue/cli-ui/src/components/PromptList.vue b/packages/@vue/cli-ui/src/components/PromptList.vue new file mode 100644 index 0000000000..31158b5c08 --- /dev/null +++ b/packages/@vue/cli-ui/src/components/PromptList.vue @@ -0,0 +1,33 @@ + + + diff --git a/packages/@vue/cli-ui/src/components/PromptsList.vue b/packages/@vue/cli-ui/src/components/PromptsList.vue new file mode 100644 index 0000000000..1c60dd0c0f --- /dev/null +++ b/packages/@vue/cli-ui/src/components/PromptsList.vue @@ -0,0 +1,37 @@ + + + + + diff --git a/packages/@vue/cli-ui/src/components/StatusBar.vue b/packages/@vue/cli-ui/src/components/StatusBar.vue index f619592f60..c91d38a652 100644 --- a/packages/@vue/cli-ui/src/components/StatusBar.vue +++ b/packages/@vue/cli-ui/src/components/StatusBar.vue @@ -65,7 +65,7 @@ export default { h-box() align-items center background $vue-color-light-neutral - font-size 12px + font-size $padding-item .section h-box() diff --git a/packages/@vue/cli-ui/src/components/StepWizard.vue b/packages/@vue/cli-ui/src/components/StepWizard.vue index c6c853b699..ac5a5e27dd 100644 --- a/packages/@vue/cli-ui/src/components/StepWizard.vue +++ b/packages/@vue/cli-ui/src/components/StepWizard.vue @@ -5,22 +5,24 @@ 'hide-tabs': hideTabs }" > -
-
{{ title }}
+
+
+
{{ title }}
+
+ + + +
- - - -
@@ -57,7 +59,11 @@ export default { @import "~@/style/imports" .step-wizard - v-box() + box-sizing border-box + + .shell + v-box() + height 100% .main-tabs height 100% @@ -90,7 +96,7 @@ export default { justify-content center .title - padding 12px + padding $padding-item font-size 24px text-align center font-weight lighter @@ -99,4 +105,12 @@ export default { >>> .tabs display none + &.frame + margin 0 auto + padding $padding-item + max-width 1200px + .shell + border solid 1px $vue-color-light-neutral + border-radius $br + diff --git a/packages/@vue/cli-ui/src/graphql-api/connectors/projects.js b/packages/@vue/cli-ui/src/graphql-api/connectors/projects.js index bdd5607714..a7f6b07ad9 100644 --- a/packages/@vue/cli-ui/src/graphql-api/connectors/projects.js +++ b/packages/@vue/cli-ui/src/graphql-api/connectors/projects.js @@ -3,6 +3,7 @@ const { getPromptModules } = require('@vue/cli/lib/util/createTools') const { getFeatures } = require('@vue/cli/lib/util/features') const { toShortPluginId } = require('@vue/cli-shared-utils') const cwd = require('./cwd') +const prompts = require('./prompts') let currentProject = null let creator = null @@ -30,7 +31,8 @@ function generatePresetDescription (preset) { function generateProjectCreation (creator) { return { presets, - features + features, + prompts: prompts.list() } } @@ -50,6 +52,7 @@ function initCreator () { id: key, name: key === 'default' ? 'Default preset' : key, features, + link: null, raw: preset } info.description = generatePresetDescription(info) @@ -58,8 +61,9 @@ function initCreator () { ), { id: 'manual', - name: 'No preset', - description: 'No included features', + name: 'Manual', + description: 'Manually select features', + link: null, features: [] } ] @@ -87,6 +91,10 @@ function initCreator () { } ] + // Prompts + prompts.reset() + creator.injectedPrompts.forEach(prompts.add) + return creator } @@ -97,13 +105,24 @@ function getCreation (context) { return generateProjectCreation(creator) } -function setFeatureEnabled ({ id, enabled }, context) { +function updatePromptsFeatures () { + prompts.changeAnswers(answers => { + answers.features = features.filter( + f => f.enabled + ).map( + f => f.id + ) + }) +} + +function setFeatureEnabled ({ id, enabled, updatePrompts = true }, context) { const feature = features.find(f => f.id === id) if (feature) { feature.enabled = enabled } else { console.warn(`Feature '${id}' not found`) } + if (updatePrompts) updatePromptsFeatures() return feature } @@ -118,18 +137,19 @@ function applyPreset (id, context) { } if (preset.raw) { if (preset.raw.router) { - setFeatureEnabled({ id: 'router', enabled: true }, context) + setFeatureEnabled({ id: 'router', enabled: true, updatePrompts: false }, context) } if (preset.raw.vuex) { - setFeatureEnabled({ id: 'vuex', enabled: true }, context) + setFeatureEnabled({ id: 'vuex', enabled: true, updatePrompts: false }, context) } if (preset.raw.cssPreprocessor) { - setFeatureEnabled({ id: 'css-preprocessor', enabled: true }, context) + setFeatureEnabled({ id: 'css-preprocessor', enabled: true, updatePrompts: false }, context) } if (preset.raw.useConfigFiles) { - setFeatureEnabled({ id: 'use-config-files', enabled: true }, context) + setFeatureEnabled({ id: 'use-config-files', enabled: true, updatePrompts: false }, context) } } + updatePromptsFeatures() } else { console.warn(`Preset '${id}' not found`) } @@ -137,10 +157,16 @@ function applyPreset (id, context) { return generateProjectCreation(creator) } +function answerPrompt ({ id, value }, context) { + prompts.setValue({ id, value: JSON.parse(value) }) + return prompts.list() +} + module.exports = { list, getCurrent, getCreation, applyPreset, - setFeatureEnabled + setFeatureEnabled, + answerPrompt } diff --git a/packages/@vue/cli-ui/src/graphql-api/connectors/prompts.js b/packages/@vue/cli-ui/src/graphql-api/connectors/prompts.js new file mode 100644 index 0000000000..149f1adead --- /dev/null +++ b/packages/@vue/cli-ui/src/graphql-api/connectors/prompts.js @@ -0,0 +1,205 @@ +let answers = {} +let prompts = [] + +function getPrompt (id) { + return prompts.find( + p => p.id === id + ) +} + +function generatePromptError (value) { + let message + if (typeof value === 'string') { + message = value + } else { + message = 'Invalid input' + } + return { + message + } +} + +function getDefaultValue (prompt) { + const defaultValue = prompt.raw.default + if (typeof defaultValue === 'function') { + return defaultValue(answers) + } else if (prompt.type === 'checkbox') { + const choices = getChoices(prompt) + if (choices) { + return choices.filter( + c => c.checked + ).map( + c => c.value + ) + } + } else if (prompt.type === 'confirm') { + return false + } + return defaultValue +} + +function getEnabled (value) { + const type = typeof value + if (type === 'function') { + return value(answers) + } else if (type === 'boolean') { + return value + } else { + return true + } +} + +function validateInput (prompt, value) { + const validate = prompt.raw.validate + if (typeof validate === 'function') { + return validate(value) + } + return true +} + +function getValue (prompt, value) { + const filter = prompt.raw.filter + if (typeof filter === 'function') { + return filter(value) + } + return value +} + +function getDisplayedValue (prompt, value) { + const transform = prompt.raw.transform + if (typeof transform === 'function') { + value = transform(value) + } + return JSON.stringify(value) +} + +function generatePromptChoice (prompt, data) { + return { + value: getDisplayedValue(prompt, data.value), + name: data.name, + checked: data.checked, + disabled: data.disabled + } +} + +function getChoices (prompt) { + const data = prompt.raw.choices + if (!data) { + return null + } + + let result + if (typeof data === 'function') { + result = data(answers) + } else { + result = data + } + return result.map( + item => generatePromptChoice(prompt, item) + ) +} + +function setAnswer (id, value) { + const fields = id.split('.') + let obj = answers + const l = fields.length + for (let i = 0; i < l - 1; i++) { + const key = fields[i] + if (!obj[key]) { + obj[key] = {} + } + obj = obj[key] + } + obj[fields[l - 1]] = value +} + +function generatePrompt (data) { + return { + id: data.name, + enabled: true, + type: data.type, + name: data.short || null, + message: data.message, + description: data.description || null, + link: data.link || null, + choices: null, + value: null, + valueChanged: false, + raw: data + } +} + +function updatePrompts () { + for (const prompt of prompts) { + prompt.enabled = getEnabled(prompt.raw.when) + + prompt.choices = getChoices(prompt) + + if (!prompt.valueChanged) { + let value = getDefaultValue(prompt) + prompt.value = getDisplayedValue(prompt, value) + setAnswer(prompt.id, getValue(prompt, value)) + } + } +} + +// Public API + +function setAnswers (newAnswers) { + answers = newAnswers + updatePrompts() +} + +function changeAnswers (cb) { + cb(answers) + updatePrompts() +} + +function reset () { + prompts = [] + setAnswers({}) +} + +function list () { + return prompts +} + +function add (data) { + prompts.push(generatePrompt(data)) +} + +function remove (id) { + const index = prompts.findIndex(p => p.id === id) + index !== -1 && prompts.splice(index, 1) +} + +function setValue ({ id, value }) { + const prompt = getPrompt(id) + if (!prompt) { + console.warn(`Prompt '${prompt}' not found`) + return null + } + + const validation = validateInput(prompt, value) + if (validation !== true) { + prompt.error = generatePromptError(validation) + } else { + prompt.error = undefined + } + prompt.value = getDisplayedValue(prompt, value) + const finalValue = getValue(prompt, value) + prompt.valueChanged = true + setAnswer(prompt.id, finalValue) + updatePrompts() + return prompt +} + +module.exports = { + setAnswers, + changeAnswers, + reset, + list, + add, + remove, + setValue +} diff --git a/packages/@vue/cli-ui/src/graphql-api/resolvers.js b/packages/@vue/cli-ui/src/graphql-api/resolvers.js index ff14304b9e..ea8bc9bdc2 100644 --- a/packages/@vue/cli-ui/src/graphql-api/resolvers.js +++ b/packages/@vue/cli-ui/src/graphql-api/resolvers.js @@ -27,7 +27,8 @@ module.exports = { favorite: args.favorite }, context), presetApply: (root, { id }, context) => projects.applyPreset(id, context), - featureSetEnabled: (root, args, context) => projects.setFeatureEnabled(args, context) + featureSetEnabled: (root, args, context) => projects.setFeatureEnabled(args, context), + promptAnswer: (root, { input }, context) => projects.answerPrompt(input, context) }, Subscription: { diff --git a/packages/@vue/cli-ui/src/graphql-api/type-defs.js b/packages/@vue/cli-ui/src/graphql-api/type-defs.js index d72dfe0889..f9fcba8713 100644 --- a/packages/@vue/cli-ui/src/graphql-api/type-defs.js +++ b/packages/@vue/cli-ui/src/graphql-api/type-defs.js @@ -49,6 +49,20 @@ input ProjectImportInput { path: String! } +type Preset { + id: ID! + name: String + description: String + link: String + features: [String] +} + +type ProjectCreation { + presets: [Preset] + features: [Feature] + prompts: [Prompt] +} + type Version { current: String! latest: String @@ -94,6 +108,9 @@ enum PromptType { type PromptChoice { value: String! + name: String + checked: Boolean + disabled: Boolean } type PromptError { @@ -110,7 +127,8 @@ type Prompt { description: String link: String choices: [PromptChoice] - currentValue: String + value: String + valueChanged: Boolean error: PromptError } @@ -119,18 +137,6 @@ input PromptInput { value: String! } -type Preset { - id: ID! - name: String - description: String - features: [String] -} - -type ProjectCreation { - presets: [Preset] - features: [Feature] -} - type Query { cwd: String! folderCurrent: Folder @@ -152,7 +158,7 @@ type Mutation { presetApply (id: ID!): ProjectCreation featureSetEnabled (id: ID!, enabled: Boolean): Feature pluginAdd (id: ID!): Plugin - promptAnswer (input: PromptInput!): Prompt + promptAnswer (input: PromptInput!): [Prompt] } type Subscription { diff --git a/packages/@vue/cli-ui/src/graphql/presetFragment.gql b/packages/@vue/cli-ui/src/graphql/presetFragment.gql index 18b6ffdf65..719c263037 100644 --- a/packages/@vue/cli-ui/src/graphql/presetFragment.gql +++ b/packages/@vue/cli-ui/src/graphql/presetFragment.gql @@ -2,5 +2,5 @@ fragment preset on Preset { id name description - features + link } diff --git a/packages/@vue/cli-ui/src/graphql/projectCreationFragment.gql b/packages/@vue/cli-ui/src/graphql/projectCreationFragment.gql index f2f42574fd..f38cf2029a 100644 --- a/packages/@vue/cli-ui/src/graphql/projectCreationFragment.gql +++ b/packages/@vue/cli-ui/src/graphql/projectCreationFragment.gql @@ -1,5 +1,6 @@ #import "./presetFragment.gql" #import "./featureFragment.gql" +#import "./promptFragment.gql" fragment projectCreation on ProjectCreation { presets { @@ -8,4 +9,7 @@ fragment projectCreation on ProjectCreation { features { ...feature } + prompts { + ...prompt + } } diff --git a/packages/@vue/cli-ui/src/graphql/promptAnswer.gql b/packages/@vue/cli-ui/src/graphql/promptAnswer.gql new file mode 100644 index 0000000000..bb28bbb27e --- /dev/null +++ b/packages/@vue/cli-ui/src/graphql/promptAnswer.gql @@ -0,0 +1,7 @@ +#import "./promptFragment.gql" + +mutation promptAnswer ($input: PromptInput!) { + promptAnswer(input: $input) { + ...prompt + } +} diff --git a/packages/@vue/cli-ui/src/graphql/promptChoiceFragment.gql b/packages/@vue/cli-ui/src/graphql/promptChoiceFragment.gql new file mode 100644 index 0000000000..85b92804dd --- /dev/null +++ b/packages/@vue/cli-ui/src/graphql/promptChoiceFragment.gql @@ -0,0 +1,6 @@ +fragment promptChoice on PromptChoice { + value + name + checked + disabled +} diff --git a/packages/@vue/cli-ui/src/graphql/promptErrorFragment.gql b/packages/@vue/cli-ui/src/graphql/promptErrorFragment.gql new file mode 100644 index 0000000000..719d472cc7 --- /dev/null +++ b/packages/@vue/cli-ui/src/graphql/promptErrorFragment.gql @@ -0,0 +1,4 @@ +fragment promptError on PromptError { + message + link +} diff --git a/packages/@vue/cli-ui/src/graphql/promptFragment.gql b/packages/@vue/cli-ui/src/graphql/promptFragment.gql new file mode 100644 index 0000000000..c761adf9aa --- /dev/null +++ b/packages/@vue/cli-ui/src/graphql/promptFragment.gql @@ -0,0 +1,20 @@ +#import "./promptChoiceFragment.gql" +#import "./promptErrorFragment.gql" + +fragment prompt on Prompt { + id + enabled + type + name + message + description + link + choices { + ...promptChoice + } + value + valueChanged + error { + ...promptError + } +} diff --git a/packages/@vue/cli-ui/src/style/imports.styl b/packages/@vue/cli-ui/src/style/imports.styl index a97d1bf9af..29baddd710 100644 --- a/packages/@vue/cli-ui/src/style/imports.styl +++ b/packages/@vue/cli-ui/src/style/imports.styl @@ -1,2 +1,4 @@ @import "~@vue/ui/src/style/imports" @import "colors" +@import "vars" +@import "mixins" diff --git a/packages/@vue/cli-ui/src/style/main.styl b/packages/@vue/cli-ui/src/style/main.styl index ba6e8434d2..8e36bd280f 100644 --- a/packages/@vue/cli-ui/src/style/main.styl +++ b/packages/@vue/cli-ui/src/style/main.styl @@ -13,13 +13,13 @@ body, height @width .actions-bar - padding 12px + padding $padding-item background $color-light-background h-box() box-center() > * - space-between-x(12px) + space-between-x($padding-item) &.space-between justify-content space-between @@ -31,6 +31,9 @@ body, justify-content flex-end .cta-text - margin 12px + margin $padding-item color lighten($vue-color-dark, 40%) font-size 18px + +.list-item + list-item() diff --git a/packages/@vue/cli-ui/src/style/mixins.styl b/packages/@vue/cli-ui/src/style/mixins.styl new file mode 100644 index 0000000000..de10c8b9b1 --- /dev/null +++ b/packages/@vue/cli-ui/src/style/mixins.styl @@ -0,0 +1,9 @@ +list-item() + user-select none + cursor pointer + + &.selected + background rgba($vue-color-primary, .05) + + &:hover + background rgba($vue-color-primary, .1) diff --git a/packages/@vue/cli-ui/src/style/vars.styl b/packages/@vue/cli-ui/src/style/vars.styl new file mode 100644 index 0000000000..78f9a06b4f --- /dev/null +++ b/packages/@vue/cli-ui/src/style/vars.styl @@ -0,0 +1 @@ +$padding-item = 12px diff --git a/packages/@vue/cli-ui/src/views/Home.vue b/packages/@vue/cli-ui/src/views/Home.vue index 03a759858b..8444cee1ad 100644 --- a/packages/@vue/cli-ui/src/views/Home.vue +++ b/packages/@vue/cli-ui/src/views/Home.vue @@ -5,21 +5,17 @@
- - @@ -30,8 +26,8 @@ export default { .home display grid grid-template-columns 46px 1fr - grid-template-rows auto 28px - grid-template-areas "side-left content" "footer footer" + grid-template-rows auto + grid-template-areas "side-left content" .project-nav grid-area side-left @@ -39,6 +35,4 @@ export default { .content grid-area content -.status-bar - grid-area footer diff --git a/packages/@vue/cli-ui/src/views/ProjectCreate.vue b/packages/@vue/cli-ui/src/views/ProjectCreate.vue index af3cb297c6..22dfa475f2 100644 --- a/packages/@vue/cli-ui/src/views/ProjectCreate.vue +++ b/packages/@vue/cli-ui/src/views/ProjectCreate.vue @@ -3,6 +3,7 @@