feat(ui): Redesign, dashboard, local plugins (#2806)

* feat: basic fonctionality, welcome and kill port widgets

* fix: contrast improvements

* feat: plugin/dep/vulnerability widgets design

* fix: widget add/remove animation

* feat: run task widget

* feat: news + wip resizing

* feat: nuxt

* chore: removed widget example

* fix: visual polish for widget transform

* feat(widget): overlap detection

* fix: news default/max size

* feat(dashboard): sidepane transition

* chore: dev api server port

* fix(widget): configure tooltip

* refactor(widget): generic Movable mixin

* refactor(widget): resizable mixin

* feat(widget): resize transition

* feat(widget): resize improvements

* refactor(widget): zoom factor

* refactor(widget): OnGrid mixin

* refactor(widget): resize handler style moved to global

* chore: remove console.log

* refactor: files structure

* feat: improved design and layout

* fix: content background vars

* fix: status bar / view nav z-indexes

* fix: webpack dashboard grid gap

* feat(news feed): handle errors

* fix(card): dimmed box shadow

* fix: view nav & status bar z-index

* fix: remove (wip)

* feat(widget): style tweaks

* feat(widget): details pane (wip)

* feat: news feed widget improvements

* feat(widget): custom header button

* feat(news): item details pane

* feat(widget): custom title

* fix(news): better cache and misc fixes

* feat(widget): resize left and top handles

* feat(widget): transparent widget while moving/resizing

* feat(news): better "big size" style

* fix(news): media sizes in rich content

* feat(plugin): local plugins support

* fix: scrolling issue in Fx

* fix: colors

* fix(nav bar): more item overflowing

* feat(vuln): frontend

* chore: locale update

* fix: image in suggestion dropdown (dev)

* fix(suggestion): missing custom image

* feat(view): user default plugin logo if no provided icon

* feat(view): better loading UX

* feat(view): button background if view is selected

* feat(view): new nav indicator

* feat(widget): use plugin logo as default icon

* feat(widget): better widget modal

* feat(widget): longDescription

* fix(widget): news validate url param

* feat(widget): filter widgets in add pane

* feat(widget): tease upcoming widgets

* chore: fix merge dev

* chore: yarn install

* chore: sync versions

* chore: update apollo

* docs: widget

* fix(progress): graphql error

* fix(deps): localPath

* perf(plugin): faster local plugin refresh

* fix(nav): center active indicator

* feat(task): improved header

* feat(client addon): custom component load timeout message

* feat(suggestion): ping animation to improve discoverability

* chore: update vue-apollo

* feat(api): requestRoute

* fix(suggestion): hide more info link if no link

* fix(style): ul padding

* test(e2e): fix plugin path

* chore: change test scripts

* chore(deps): upgrade

* fix: build error

* fix(widget): removed moving scale transform

* fix(widget): resize handles style

* chore(deps): unpin apollo-utilities

* chore(deps): lock fix

* test(e2e): fix server

* fix: issue with writeQuery

See: https://github.com/apollographql/apollo-client/issues/4031#issuecomment-433668473

* test(e2e): fix tests

* test(e2e): missing widgets build

* fix: missing widgets dep
This commit is contained in:
Guillaume Chau
2018-10-28 04:10:34 +01:00
committed by GitHub
parent 9a64708ed3
commit a09407dd5b
289 changed files with 6096 additions and 1674 deletions
+67
View File
@@ -1288,6 +1288,58 @@ In this example we only display the vue-router suggestion in the plugins view an
Note: `addSuggestion` and `removeSuggestion` can be namespaced with `api.namespace()`.
## Widgets
You can register a widget for the project dashboard in your plugin ui file:
```js
registerWidget({
// Unique ID
id: 'org.vue.widgets.news',
// Basic infos
title: 'org.vue.widgets.news.title',
description: 'org.vue.widgets.news.description',
icon: 'rss_feed',
// Main component used to render the widget
component: 'org.vue.widgets.components.news',
// (Optional) Secondary component for widget 'fullscreen' view
detailsComponent: 'org.vue.widgets.components.news',
// Size
minWidth: 2,
minHeight: 1,
maxWidth: 6,
maxHeight: 6,
defaultWidth: 2,
defaultHeight: 3,
// (Optional) Limit the maximum number of this widget on the dashboard
maxCount: 1,
// (Optional) Add a 'fullscreen' button in widget header
openDetailsButton: true,
// (Optional) Default configuration for the widget
defaultConfig: () => ({
url: 'https://vuenews.fireside.fm/rss'
}),
// (Optional) Require user to configure widget when added
// You shouldn't use `defaultConfig` with this
needsUserConfig: true,
// (Optional) Display prompts to configure the widget
onConfigOpen: async ({ context }) => {
return {
prompts: [
{
name: 'url',
type: 'input',
message: 'org.vue.widgets.news.prompts.url',
validate: input => !!input // Required
}
]
}
}
})
```
Note: `registerWidget` can be namespaced with `api.namespace()`.
## Other methods
### hasPlugin
@@ -1324,6 +1376,21 @@ Get currently open project.
api.getProject()
```
### requestRoute
Switch the user on a specific route in the web client.
```js
api.requestRoute({
name: 'foo',
params: {
id: 'bar'
}
})
api.requestRoute('/foobar')
```
## Public static files
You may need to expose some static files over the cli-ui builtin HTTP server (typically if you want to specify an icon to a custom view).
@@ -1,5 +1,5 @@
<template>
<div class="asset-list list-block">
<div class="asset-list card list-block">
<div class="content">
<div class="title">
{{ $t('org.vue.vue-webpack.dashboard.asset-list.title') }}
@@ -1,6 +1,6 @@
<template>
<div
class="build-progress"
class="build-progress card"
:class="{
[`mode-${mode}`]: true
}"
@@ -1,5 +1,5 @@
<template>
<div class="build-status">
<div class="build-status card">
<div class="content">
<div class="info-block status">
<div class="label">
@@ -1,5 +1,5 @@
<template>
<div class="module-list list-block">
<div class="module-list card list-block">
<div class="content">
<div class="title">
{{ $t('org.vue.vue-webpack.dashboard.module-list.title') }}
@@ -1,5 +1,5 @@
<template>
<div class="speed-stats">
<div class="speed-stats card">
<div class="content">
<div class="title">
{{ $t('org.vue.vue-webpack.dashboard.speed-stats.title') }}
@@ -1,6 +1,6 @@
<template>
<div class="vue-webpack-analyzer">
<div class="pane-toolbar">
<div class="pane-toolbar card">
<VueIcon icon="donut_large"/>
<div class="title">{{ $t('org.vue.vue-webpack.analyzer.title') }}</div>
@@ -84,7 +84,7 @@
</template>
<div v-if="describedModule" class="described-module">
<div class="wrapper">
<div class="wrapper card">
<div class="path" v-html="modulePath"/>
<div
class="stats size"
@@ -345,10 +345,6 @@ export default {
.pane-toolbar,
.described-module .wrapper
padding $padding-item
background $vue-ui-color-light-neutral
border-radius $br
.vue-ui-dark-mode &
background $vue-ui-color-dark
.content
display flex
@@ -1,6 +1,6 @@
<template>
<div class="vue-webpack-dashboard">
<div class="pane-toolbar">
<div class="pane-toolbar card">
<VueIcon icon="dashboard"/>
<div class="title">{{ $t('org.vue.vue-webpack.dashboard.title') }}</div>
@@ -40,7 +40,7 @@
/>
</div>
<div class="content vue-ui-grid default-gap">
<div class="content vue-ui-grid">
<BuildStatus />
<BuildProgress />
<SpeedStats class="span-2"/>
@@ -90,6 +90,7 @@ export default {
.vue-webpack-dashboard
.content
grid-template-columns 9fr 4fr
grid-gap $padding-item
>>>
.title
@@ -124,13 +125,8 @@ export default {
opacity .75
font-size 16px
.pane-toolbar,
.content >>> > div
/deep/ .card
padding $padding-item
background $vue-ui-color-light-neutral
border-radius $br
.vue-ui-dark-mode &
background $vue-ui-color-dark
.pane-toolbar
margin-bottom $padding-item
@@ -0,0 +1,3 @@
> 1%
last 2 versions
not ie <= 8
@@ -0,0 +1,22 @@
module.exports = {
root: true,
env: {
node: true
},
'extends': [
'plugin:vue/essential',
'@vue/standard'
],
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
},
parserOptions: {
parser: 'babel-eslint'
},
globals: {
ClientAddonApi: false,
mapSharedData: false,
Vue: false
}
}
@@ -0,0 +1,21 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw*
@@ -0,0 +1,21 @@
# cli-ui-addon-widgets
## Project setup
```
yarn install
```
### Compiles and hot-reloads for development
```
yarn run serve
```
### Compiles and minifies for production
```
yarn run build
```
### Lints and fixes files
```
yarn run lint
```
@@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/app'
]
}
@@ -0,0 +1,26 @@
{
"name": "@vue/cli-ui-addon-widgets",
"version": "3.0.5",
"files": [
"dist",
"src"
],
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"prepublishOnly": "yarn run lint --no-fix && yarn run build"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^3.0.5",
"@vue/cli-plugin-eslint": "^3.0.5",
"@vue/cli-service": "^3.0.5",
"@vue/eslint-config-standard": "^3.0.5",
"stylus": "^0.54.5",
"stylus-loader": "^3.0.2",
"vue-template-compiler": "^2.5.17"
},
"publishConfig": {
"access": "public"
}
}
@@ -0,0 +1,5 @@
module.exports = {
plugins: {
autoprefixer: {}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>cli-ui-addon-widgets</title>
</head>
<body>
<noscript>
<strong>We're sorry but cli-ui-addon-widgets doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
@@ -0,0 +1,49 @@
<template>
<div class="dependency-updates">
<StatusWidget
v-if="status"
:icon="icons[status.status]"
:icon-class="iconClasses[status.status]"
:title="$t(`org.vue.widgets.dependency-updates.messages.${status.status}`)"
:status="status"
@check="checkForUpdates()"
>
<template slot="more-actions">
<VueButton
:to="{ name: 'project-dependencies' }"
:label="$t('org.vue.widgets.dependency-updates.page')"
icon-left="collections_bookmark"
/>
</template>
</StatusWidget>
</div>
</template>
<script>
import { UPDATES_ICONS, UPDATES_ICON_CLASSES } from '../util/consts'
import StatusWidget from './StatusWidget.vue'
export default {
components: {
StatusWidget
},
sharedData () {
return mapSharedData('org.vue.widgets.dependency-updates.', {
status: 'status'
})
},
created () {
this.icons = UPDATES_ICONS
this.iconClasses = UPDATES_ICON_CLASSES
},
methods: {
checkForUpdates () {
// TODO
}
}
}
</script>
@@ -0,0 +1,137 @@
<template>
<div
class="kill-port"
:class="[
`status-${status}`
]"
>
<div class="wrapper">
<div class="status">
<VueLoadingIndicator
v-if="status === 'killing'"
class="icon"
/>
<ItemLogo
v-else
:image="icon"
class="icon large"
/>
<div class="info">
{{ $t(`org.vue.widgets.kill-port.status.${status}`) }}
</div>
</div>
<div class="actions">
<VueInput
v-model="port"
:placeholder="$t('org.vue.widgets.kill-port.input.placeholder')"
class="input big"
type="number"
@keyup.enter="kill()"
/>
<VueButton
:label="$t('org.vue.widgets.kill-port.kill')"
icon-left="flash_on"
class="primary big"
@click="kill()"
/>
</div>
</div>
</div>
</template>
<script>
const ICONS = {
idle: 'flash_on',
killed: 'check_circle',
error: 'error'
}
export default {
sharedData () {
return mapSharedData('org.vue.widgets.kill-port.', {
status: 'status'
})
},
data () {
return {
port: ''
}
},
computed: {
icon () {
return ICONS[this.status] || ICONS.idle
},
inputValid () {
return /\d+/.test(this.port)
}
},
watch: {
status (value) {
if (value === 'killed') {
this.port = ''
}
if (value !== 'killing' && value !== 'idle') {
this.$_statusTimer = setTimeout(() => {
this.status = 'idle'
}, 3000)
}
}
},
methods: {
kill () {
clearTimeout(this.$_statusTimer)
this.$callPluginAction('org.vue.widgets.actions.kill-port', {
port: this.port
})
}
}
}
</script>
<style lang="stylus" scoped>
@import "~@vue/cli-ui/src/style/imports"
.wrapper
height 100%
position relative
padding $padding-item
box-sizing border-box
v-box()
justify-content space-between
.status
h-box()
align-items center
.icon
width 48px
height @width
>>> .vue-ui-icon
width 32px
height @width
.info
font-size 18px
.status-killed
.status
.icon >>> svg
fill $vue-ui-color-success !important
.status-error
.status
.icon >>> svg
fill $vue-ui-color-danger !important
.actions
h-box()
box-center()
.input
flex 1
margin-right $padding-item
</style>
@@ -0,0 +1,248 @@
<template>
<div
class="news"
:class="{
details: widget.isDetails,
small,
'has-item-selected': selectedItem
}"
>
<div
v-if="loading"
class="loading"
>
<VueLoadingIndicator/>
</div>
<div
v-else-if="error"
class="error vue-ui-empty"
>
<VueIcon :icon="errorData.icon" class="huge"/>
<div>{{ $t(`org.vue.widgets.news.errors.${error}`) }}</div>
</div>
<div
v-else
class="panes"
>
<div class="feed">
<NewsItem
v-for="(item, index) of feed.items"
:key="index"
:item="item"
:class="{
selected: selectedItem === item
}"
@click.native="selectedItem = item"
/>
</div>
<transition>
<div
v-if="selectedItem"
class="item-details"
>
<div v-if="small" class="back">
<VueButton
icon-left="arrow_back"
:label="$t('org.vue.common.back')"
@click="selectedItem = null"
/>
</div>
<NewsItemDetails
v-if="selectedItem"
:item="selectedItem"
/>
</div>
<div v-else-if="!small" class="select-tip vue-ui-empty">
<VueIcon icon="rss_feed" class="huge"/>
<div>{{ $t('org.vue.widgets.news.select-tip') }}</div>
</div>
</transition>
</div>
</div>
</template>
<script>
import NewsItem from './NewsItem.vue'
import NewsItemDetails from './NewsItemDetails.vue'
const ERRORS = {
'fetch': {
icon: 'error'
},
'offline': {
icon: 'cloud_off'
},
'empty': {
icon: 'cake'
}
}
export default {
components: {
NewsItem,
NewsItemDetails
},
inject: [
'widget'
],
data () {
return {
loading: false,
feed: null,
error: null,
selectedItem: null
}
},
computed: {
errorData () {
if (this.error) {
return ERRORS[this.error]
}
},
small () {
return !this.widget.isDetails && this.widget.data.width < 5
}
},
watch: {
'widget.data.config.url': {
handler () {
this.fetchFeed()
},
immediate: true
}
},
created () {
this.widget.addHeaderAction({
id: 'refresh',
icon: 'refresh',
tooltip: 'org.vue.widgets.news.refresh',
disabled: () => this.loading,
onCalled: () => {
this.fetchFeed(true)
}
})
},
methods: {
async fetchFeed (force = false) {
this.selectedItem = null
if (!navigator.onLine) {
this.loading = false
this.error = 'offline'
return
}
this.loading = true
this.error = false
this.widget.customTitle = null
try {
const { results, errors } = await this.$callPluginAction('org.vue.widgets.actions.fetch-news', {
url: this.widget.data.config.url,
force
})
if (errors.length && errors[0]) throw new Error(errors[0])
this.feed = results[0]
if (!this.feed.items.length) {
this.error = 'empty'
}
this.widget.customTitle = this.feed.title
} catch (e) {
this.error = 'fetch'
console.error(e)
}
this.loading = false
}
}
}
</script>
<style lang="stylus" scoped>
@import "~@vue/cli-ui/src/style/imports"
.loading,
.panes,
.feed
height 100%
.loading
h-box()
box-center()
.panes
display flex
align-items stretch
.feed
overflow-x hidden
overflow-y auto
.item-details,
.select-tip
flex 1
height 100%
.item-details
overflow-x hidden
overflow-y auto
.back
padding $padding-item
.select-tip
v-box()
box-center()
.error
height 100%
v-box()
box-center()
padding-bottom 42px
.news
&:not(.small)
padding ($padding-item / 2) 0 $padding-item $padding-item
.feed
width 400px
background lighten($vue-ui-color-light-neutral, 50%)
border-radius $br
.vue-ui-dark-mode &
background $content-bg-list-dark
&.small
.panes
position relative
.feed
transition transform .3s cubic-bezier(0,1,.32,1)
.item-details
position absolute
top 0
left 0
width 100%
transition transform .15s ease-out
background $vue-ui-color-light
.vue-ui-dark-mode &
background $vue-ui-color-darker
&.v-enter-active,
&.v-enter-leave
transition transform .3s cubic-bezier(0,1,.32,1)
&.v-enter,
&.v-leave-to
transform translateX(100%)
&.has-item-selected
.feed
transform translateX(-100%)
</style>
@@ -0,0 +1,57 @@
<template>
<div class="news-item list-item">
<div class="info">
<div class="head">
<div class="title">
<a
:href="item.link"
target="_blank"
@click.stop
>
{{ item.title }}
</a>
</div>
<div class="snippet">{{ snippet }}</div>
<div class="date">{{ item.pubDate | date }}</div>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
item: {
type: Object,
required: true
}
},
computed: {
snippet () {
const text = this.item.contentSnippet
if (text.length > 200) {
return text.substr(0, 197) + '...'
}
return text
}
}
}
</script>
<style lang="stylus" scoped>
@import "~@vue/cli-ui/src/style/imports"
.news-item
padding ($padding-item / 2) $padding-item $padding-item
.title
margin-bottom ($padding-item /2)
.snippet,
.date
font-size 14px
.date
opacity .5
</style>
@@ -0,0 +1,81 @@
<template>
<div class="news-item-details">
<div class="head">
<div class="title">
<a
:href="item.link"
target="_blank"
>
{{ item.title }}
</a>
</div>
<div class="date">{{ item.pubDate | date }}</div>
</div>
<div
class="content"
v-html="item['content:encoded'] || item.content"
/>
<div
v-if="item.enclosure"
class="media"
>
<img
v-if="item.enclosure.type.indexOf('image/') === 0"
:src="item.enclosure.url"
class="image media-content"
/>
<audio
v-if="item.enclosure.type.indexOf('audio/') === 0"
:src="item.enclosure.url"
controls
/>
<video
v-if="item.enclosure.type.indexOf('video/') === 0"
:src="item.enclosure.url"
controls
/>
</div>
</div>
</template>
<script>
export default {
props: {
item: {
type: Object,
required: true
}
}
}
</script>
<style lang="stylus" scoped>
@import "~@vue/cli-ui/src/style/imports"
.news-item-details
padding 0 $padding-item $padding-item
overflow-x hidden
overflow-y auto
.title
font-size 18px
margin-bottom ($padding-item /2)
.date
font-size 14px
opacity .5
.content,
.media
margin-top $padding-item
.media-content,
.content >>> img,
.content >>> video
max-width 100%
max-height 300px
</style>
@@ -0,0 +1,49 @@
<template>
<div class="plugin-updates">
<StatusWidget
v-if="status"
:icon="icons[status.status]"
:icon-class="iconClasses[status.status]"
:title="$t(`org.vue.widgets.plugin-updates.messages.${status.status}`)"
:status="status"
@check="checkForUpdates()"
>
<template slot="more-actions">
<VueButton
:to="{ name: 'project-plugins' }"
:label="$t('org.vue.widgets.plugin-updates.page')"
icon-left="extension"
/>
</template>
</StatusWidget>
</div>
</template>
<script>
import { UPDATES_ICONS, UPDATES_ICON_CLASSES } from '../util/consts'
import StatusWidget from './StatusWidget.vue'
export default {
components: {
StatusWidget
},
sharedData () {
return mapSharedData('org.vue.widgets.plugin-updates.', {
status: 'status'
})
},
created () {
this.icons = UPDATES_ICONS
this.iconClasses = UPDATES_ICON_CLASSES
},
methods: {
checkForUpdates () {
// TODO
}
}
}
</script>
@@ -0,0 +1,108 @@
<template>
<div class="run-task">
<template v-if="task">
<TaskItem
:task="task"
class="info"
/>
<div class="actions">
<VueButton
v-if="task.status !== 'running'"
icon-left="play_arrow"
class="primary"
:label="$t('org.vue.views.project-task-details.actions.play')"
@click="runTask()"
/>
<VueButton
v-else
icon-left="stop"
class="primary"
:label="$t('org.vue.views.project-task-details.actions.stop')"
@click="stopTask()"
/>
<VueButton
icon-left="assignment"
:label="$t('org.vue.widgets.run-task.page')"
:to="{ name: 'project-task-details', params: { id: taskId } }"
/>
</div>
</template>
</div>
</template>
<script>
import TASK from '@vue/cli-ui/src/graphql/task/task.gql'
import TASK_RUN from '@vue/cli-ui/src/graphql/task/taskRun.gql'
import TASK_STOP from '@vue/cli-ui/src/graphql/task/taskStop.gql'
import TASK_CHANGED from '@vue/cli-ui/src/graphql/task/taskChanged.gql'
export default {
inject: [
'widget'
],
apollo: {
task: {
query: TASK,
variables () {
return {
id: this.taskId
}
}
},
$subscribe: {
taskChanged: {
query: TASK_CHANGED
}
}
},
computed: {
taskId () {
return this.widget.data.config.task
}
},
methods: {
runTask () {
this.$apollo.mutate({
mutation: TASK_RUN,
variables: {
id: this.taskId
}
})
},
stopTask () {
this.$apollo.mutate({
mutation: TASK_STOP,
variables: {
id: this.taskId
}
})
}
}
}
</script>
<style lang="stylus" scoped>
@import "~@vue/cli-ui/src/style/imports"
.info
margin 5px 0 6px
padding $padding-item
>>> .name
font-size 18px
.actions
h-box()
box-center()
/deep/ > *
&:not(:last-child)
margin-right ($padding-item / 2)
</style>
@@ -0,0 +1,149 @@
<template>
<div v-if="false" class="status-widget">
<div class="header">
<div class="icon-wrapper">
<ItemLogo
:image="icon"
class="icon"
:class="iconClass"
/>
</div>
<div class="info">
<div class="title" v-html="title"/>
<div class="last-updated">
<template v-if="status.lastUpdate">
<div class="label">
{{ $t('org.vue.widgets.status-widget.last-updated') }}
</div>
<VueTimeago
:datetime="status.lastUpdate"
:auto-update="60"
/>
</template>
<div
v-else
class="label"
>
{{ $t('org.vue.widgets.status-widget.never') }}
</div>
</div>
</div>
</div>
<div class="actions">
<slot name="actions">
<VueButton
v-if="status.status === 'attention'"
:label="$t('org.vue.widgets.status-widget.more-info')"
icon-left="add_circle"
@click="widget.openDetails()"
/>
<slot name="more-actions"/>
</slot>
</div>
</div>
<div v-else class="status-widget soon">
<div class="text">Available Soon</div>
</div>
</template>
<script>
export default {
inject: [
'widget'
],
props: {
icon: {
type: String,
required: true
},
iconClass: {
type: [String, Array, Object],
default: undefined
},
title: {
type: String,
required: true
},
status: {
type: Object,
required: true
}
},
created () {
// this.widget.addHeaderAction({
// id: 'refresh',
// icon: 'refresh',
// tooltip: 'org.vue.widgets.status-widget.check',
// disabled: () => this.status.status === 'loading',
// onCalled: () => {
// this.$emit('check')
// }
// })
// this.widget.addHeaderAction({
// id: 'expand',
// icon: 'zoom_out_map',
// tooltip: 'org.vue.components.widget.open-details',
// hidden: () => this.status.status !== 'attention',
// onCalled: () => {
// this.widget.openDetails()
// }
// })
}
}
</script>
<style lang="stylus" scoped>
@import "~@vue/cli-ui/src/style/imports"
.header
h-box()
align-items center
padding $padding-item
margin 4px 0
.icon-wrapper
.icon
width 48px
height @width
>>> .vue-ui-icon
width 32px
height @width
.title
font-size 18px
.last-updated
color $color-text-light
h-box()
.label
margin-right 4px
.actions
h-box()
box-center()
/deep/ > *
&:not(:last-child)
margin-right ($padding-item / 2)
.soon
display flex
box-center()
height 100%
.text
background $vue-ui-color-primary
color $vue-ui-color-light
padding 4px 14px
border-radius 13px
text-transform lowercase
font-family monospace
opacity .5
</style>
@@ -0,0 +1,47 @@
<template>
<div class="vulnerability">
<StatusWidget
v-if="status"
:icon="icons[status.status]"
:icon-class="iconClasses[status.status]"
:title="$t(`org.vue.widgets.vulnerability.messages.${status.status}`, { n: status.count })"
:status="status"
@check="checkForUpdates()"
/>
</div>
</template>
<script>
import { UPDATES_ICON_CLASSES } from '../util/consts'
import StatusWidget from './StatusWidget.vue'
const UPDATES_ICONS = {
'ok': 'verified_user',
'loading': 'hourglass_full',
'attention': 'error'
}
export default {
components: {
StatusWidget
},
sharedData () {
return mapSharedData('org.vue.widgets.vulnerability.', {
status: 'status'
})
},
created () {
this.icons = UPDATES_ICONS
this.iconClasses = UPDATES_ICON_CLASSES
},
methods: {
checkForUpdates () {
// TODO
}
}
}
</script>
@@ -0,0 +1,53 @@
<template>
<div class="vulnerability-details">
<div
v-if="!details"
class="loading"
>
<VueLoadingIndicator/>
</div>
<template v-else>
<div class="pane-toolbar">
<div class="title">
{{ $t('org.vue.widgets.vulnerability.messages.attention', { n: details.vulnerabilities.length }) }}
</div>
</div>
<div class="items">
<VulnerabilityItem
v-for="(item, index) of details.vulnerabilities"
:key="index"
:item="item"
/>
</div>
</template>
</div>
</template>
<script>
import VulnerabilityItem from './VulnerabilityItem.vue'
export default {
components: {
VulnerabilityItem
},
sharedData () {
return mapSharedData('org.vue.widgets.vulnerability.', {
details: 'details'
})
}
}
</script>
<style lang="stylus" scoped>
@import "~@vue/cli-ui/src/style/imports"
.vulnerability-details
overflow-x hidden
overflow-y auto
.pane-toolbar
padding-bottom $padding-item
</style>
@@ -0,0 +1,126 @@
<template>
<div class="vulnerability-item list-item">
<div class="wrapper">
<ItemLogo
image="error"
:class="severity.class"
/>
<ListItemInfo
:link="item.moreInfo"
>
<template slot="name">
<span class="name">{{ item.name }}</span>
<span class="version">{{ item.version }}</span>
</template>
<template slot="description">
<span
class="severity"
:class="severity.class"
>
{{ $t(`org.vue.widgets.vulnerability.severity.${item.severity}`) }}
</span>
<span class="message">
{{ item.message }}
</span>
</template>
</ListItemInfo>
<div class="parents">
<div v-if="!item.parents" class="vue-ui-empty">
{{ $t('org.vue.widgets.vulnerability.direct-dep') }}
</div>
<template v-else>
<div
v-for="(parent, index) of item.parents"
:key="index"
class="parent"
>
<span class="name">{{ parent.name }}</span>
<span class="version">{{ parent.version }}</span>
<VueIcon
icon="chevron_right"
class="separator-icon medium"
/>
</div>
<div class="parent current">
<span class="name">{{ item.name }}</span>
<span class="version">{{ item.version }}</span>
</div>
</template>
</div>
</div>
</div>
</template>
<script>
const SEVERITIES = {
high: {
class: 'danger'
},
medium: {
class: 'warning'
},
low: {
class: ''
}
}
export default {
props: {
item: {
type: Object,
required: true
}
},
computed: {
severity () {
return SEVERITIES[this.item.severity]
}
}
}
</script>
<style lang="stylus" scoped>
@import "~@vue/cli-ui/src/style/imports"
.vulnerability-item
padding $padding-item
border-top solid 1px rgba($color-text-light, .2)
.wrapper
h-box()
box-center()
.list-item-info
flex 1
.name
font-weight bold
.version
margin-left 4px
font-family monospace
font-size .9em
.severity
color $color-text-light
&.danger
color $vue-ui-color-danger
&.warning
color $vue-ui-color-warning
.parents
h-box()
.separator-icon
>>> svg
fill $color-text-light
.vue-ui-empty
padding 0
</style>
@@ -0,0 +1,96 @@
<template>
<div class="welcome">
<div class="logo-wrapper">
<ItemLogo
image="/public/vue-logo.png"
class="logo vuejs"
/>
</div>
<div class="title">
{{ $t('org.vue.widgets.welcome.content.title') }}
</div>
<div class="tips">
<div
v-for="n in 3"
:key="n"
:data-n="n"
class="tip"
>
<ItemLogo
:image="tipIcons[n - 1]"
class="icon"
/>
<div class="message">
{{ $t(`org.vue.widgets.welcome.content.tip${n}`) }}
</div>
</div>
</div>
<div class="actions">
<VueButton
:label="$t('org.vue.widgets.welcome.content.ok')"
icon-left="done"
class="primary big"
@click="widget.remove()"
/>
</div>
</div>
</template>
<script>
export default {
inject: [
'widget'
],
created () {
this.tipIcons = [
'dashboard',
'arrow_back',
'home'
]
}
}
</script>
<style lang="stylus" scoped>
@import "~@vue/cli-ui/src/style/imports"
.welcome
padding $padding-item
v-box()
.logo-wrapper
v-box()
box-center()
margin $padding-item 0
.logo
width 100px
height @width
.title
font-size 32px
font-weight lighter
text-align center
margin-bottom ($padding-item * 2)
.tips
flex 1
.tip
padding ($padding-item * 2)
h-box()
box-center()
.icon
>>> svg
fill rgba($vue-ui-color-primary, .7) !important
.message
flex 1
margin-left ($padding-item / 2)
.actions
h-box()
box-center()
margin-bottom $padding-item
</style>
@@ -0,0 +1,17 @@
import Welcome from './components/Welcome.vue'
import KillPort from './components/KillPort.vue'
import PluginUpdates from './components/PluginUpdates.vue'
import DependencyUpdates from './components/DependencyUpdates.vue'
import Vulnerability from './components/Vulnerability.vue'
import VulnerabilityDetails from './components/VulnerabilityDetails.vue'
import RunTask from './components/RunTask.vue'
import News from './components/News.vue'
ClientAddonApi.component('org.vue.widgets.components.welcome', Welcome)
ClientAddonApi.component('org.vue.widgets.components.kill-port', KillPort)
ClientAddonApi.component('org.vue.widgets.components.plugin-updates', PluginUpdates)
ClientAddonApi.component('org.vue.widgets.components.dependency-updates', DependencyUpdates)
ClientAddonApi.component('org.vue.widgets.components.vulnerability', Vulnerability)
ClientAddonApi.component('org.vue.widgets.components.vulnerability-details', VulnerabilityDetails)
ClientAddonApi.component('org.vue.widgets.components.run-task', RunTask)
ClientAddonApi.component('org.vue.widgets.components.news', News)
@@ -0,0 +1,11 @@
export const UPDATES_ICONS = {
'ok': 'check_circle',
'loading': 'hourglass_full',
'attention': 'error'
}
export const UPDATES_ICON_CLASSES = {
'ok': 'success',
'loading': '',
'attention': 'warning'
}
@@ -0,0 +1,8 @@
const { clientAddonConfig } = require('@vue/cli-ui')
module.exports = {
...clientAddonConfig({
id: 'org.vue.webpack.client-addon',
port: 8097
})
}
@@ -6,6 +6,7 @@ const views = require('../connectors/views')
const suggestions = require('../connectors/suggestions')
const folders = require('../connectors/folders')
const progress = require('../connectors/progress')
const app = require('../connectors/app')
// Utils
const ipc = require('../util/ipc')
const { notify } = require('../util/notification')
@@ -19,6 +20,7 @@ const { validateView, validateBadge } = require('./view')
const { validateNotify } = require('./notify')
const { validateSuggestion } = require('./suggestion')
const { validateProgress } = require('./progress')
const { validateWidget } = require('./widget')
class PluginApi {
constructor ({ plugins, file, project, lightMode = false }, context) {
@@ -48,6 +50,7 @@ class PluginApi {
this.views = []
this.actions = new Map()
this.ipcHandlers = []
this.widgetDefs = []
}
/**
@@ -575,6 +578,36 @@ class PluginApi {
suggestions.remove(id, this.context)
}
/**
* Register a widget for project dashboard
*
* @param {object} def Widget definition
*/
registerWidget (def) {
if (this.lightMode) return
try {
validateWidget(def)
this.widgetDefs.push({
...def,
pluginId: this.pluginId
})
} catch (e) {
logs.add({
type: 'error',
tag: 'PluginApi',
message: `(${this.pluginId || 'unknown plugin'}) 'registerWidget' widget definition is invalid\n${e.message}`
}, this.context)
console.error(new Error(`Invalid definition: ${e.message}`))
}
}
/**
* Request a route to be displayed in the client
*/
requestRoute (route) {
app.requestRoute(route, this.context)
}
/**
* Create a namespaced version of:
* - getSharedData
@@ -600,7 +633,11 @@ class PluginApi {
options.id = namespace + options.id
return this.addSuggestion(options)
},
removeSuggestion: (id) => this.removeSuggestion(namespace + id)
removeSuggestion: (id) => this.removeSuggestion(namespace + id),
registerWidget: (def) => {
def.id = namespace + def.id
return this.registerWidget(def)
}
}
}
}
@@ -3,7 +3,7 @@ const { createSchema, validateSync } = require('@vue/cli-shared-utils')
const viewSchema = createSchema(joi => ({
id: joi.string().required(),
name: joi.string().required().description('route name (vue-router)'),
icon: joi.string().required(),
icon: joi.string(),
tooltip: joi.string().required(),
projectTypes: joi.array().items(joi.string())
}))
@@ -0,0 +1,38 @@
const { createSchema, validateSync } = require('@vue/cli-shared-utils')
const schema = createSchema(joi => ({
id: joi.string().required(),
// General
title: joi.string().required(),
description: joi.string(),
longDescription: joi.string(),
icon: joi.string(),
screenshot: joi.string(),
link: joi.string(),
// Components
component: joi.string().required(),
detailsComponent: joi.string(),
// Maximum number of instances
maxCount: joi.number(),
// Size
minWidth: joi.number().required(),
minHeight: joi.number().required(),
maxWidth: joi.number().required(),
maxHeight: joi.number().required(),
defaultWidth: joi.number(),
defaultHeight: joi.number(),
// Config
defaultConfig: joi.func(),
needsUserConfig: joi.boolean(),
// UI
openDetailsButton: joi.boolean(),
// Hooks
onAdded: joi.func(),
onRemoved: joi.func(),
onConfigOpen: joi.func(),
onConfigChanged: joi.func()
}))
exports.validateWidget = (options) => {
validateSync(options, schema)
}
@@ -15,5 +15,6 @@ module.exports = {
LOCALE_ADDED: 'locale_added',
SUGGESTION_ADDED: 'suggestion_added',
SUGGESTION_REMOVED: 'suggestion_removed',
SUGGESTION_UPDATED: 'suggestion_updated'
SUGGESTION_UPDATED: 'suggestion_updated',
ROUTE_REQUESTED: 'route_requested'
}
@@ -0,0 +1,9 @@
const channels = require('../channels')
function requestRoute (route, context) {
context.pubsub.publish(channels.ROUTE_REQUESTED, { routeRequested: route })
}
module.exports = {
requestRoute
}
@@ -8,7 +8,7 @@ let addons = []
let baseUrl = process.env.VUE_APP_CLI_UI_URL
if (typeof baseUrl === 'undefined') {
baseUrl = 'http://localhost:4000'
baseUrl = `http://localhost:${process.env.VUE_APP_GRAPHQL_PORT}`
} else {
baseUrl = baseUrl.replace(/ws:\/\/([a-z0-9_-]+:\d+).*/i, 'http://$1')
}
@@ -125,12 +125,19 @@ async function getMetadata (id, context) {
async function getVersion ({ id, installed, versionRange, baseDir }, context) {
let current
// Is local dep
const localPath = getLocalPath(id, context)
// Read module package.json
if (installed) {
const pkg = readPackage({ id, file: baseDir }, context)
current = pkg.version
} else {
current = null
}
// Metadata
let latest, wanted
const metadata = await getMetadata(id, context)
if (metadata) {
@@ -147,10 +154,27 @@ async function getVersion ({ id, installed, versionRange, baseDir }, context) {
current,
latest,
wanted,
range: versionRange
range: versionRange,
localPath
}
}
function getLocalPath (id, context) {
const projects = require('./projects')
const projectPkg = folders.readPackage(projects.getCurrent(context).path, context, true)
const deps = Object.assign(
{},
projectPkg.dependencies || {},
projectPkg.devDependencies || {}
)
const range = deps[id]
if (range && range.match(/^file:/)) {
const localPath = range.substr('file:'.length)
return path.resolve(cwd.get(), localPath)
}
return null
}
async function getDescription ({ id }, context) {
const metadata = await getMetadata(id, context)
if (metadata) {
@@ -166,13 +190,21 @@ function getLink ({ id, file }, context) {
`https://www.npmjs.com/package/${id.replace(`/`, `%2F`)}`
}
function install ({ id, type }, context) {
function install ({ id, type, range }, context) {
return progress.wrap(PROGRESS_ID, context, async setProgress => {
setProgress({
status: 'dependency-install',
args: [id]
})
await installPackage(cwd.get(), getCommand(cwd.get()), null, id, type === 'devDependencies')
let arg
if (range) {
arg = `${id}@${range}`
} else {
arg = id
}
await installPackage(cwd.get(), getCommand(cwd.get()), null, arg, type === 'devDependencies')
logs.add({
message: `Dependency ${id} installed`,
@@ -1,5 +1,5 @@
const path = require('path')
const fs = require('fs')
const fs = require('fs-extra')
const LRU = require('lru-cache')
const chalk = require('chalk')
// Context
@@ -128,6 +128,8 @@ function resetPluginApi ({ file, lightApi }, context) {
return new Promise((resolve, reject) => {
log('Plugin API reloading...', chalk.grey(file))
const widgets = require('./widgets')
let pluginApi = pluginApiInstances.get(file)
let projectId
@@ -141,10 +143,11 @@ function resetPluginApi ({ file, lightApi }, context) {
if (projectId) sharedData.unWatchAll({ projectId }, context)
clientAddons.clear(context)
suggestions.clear(context)
widgets.reset(context)
}
// Cyclic dependency with projects connector
setTimeout(() => {
setTimeout(async () => {
const projects = require('./projects')
const project = projects.findByPath(file, context)
@@ -182,9 +185,17 @@ function resetPluginApi ({ file, lightApi }, context) {
}
}
// Add client addons
pluginApi.clientAddons.forEach(options => clientAddons.add(options, context))
pluginApi.clientAddons.forEach(options => {
clientAddons.add(options, context)
})
// Add views
pluginApi.views.forEach(view => views.add(view, context))
for (const view of pluginApi.views) {
await views.add({ view, project }, context)
}
// Register widgets
for (const definition of pluginApi.widgetDefs) {
await widgets.registerDefinition({ definition, project }, context)
}
if (lightApi) {
resolve(true)
@@ -209,6 +220,9 @@ function resetPluginApi ({ file, lightApi }, context) {
if (currentView) views.open(currentView.id)
}
// Load widgets for current project
widgets.load(context)
resolve(true)
})
})
@@ -329,6 +343,36 @@ function mockInstall (id, context) {
return true
}
function installLocal (context) {
const projects = require('./projects')
const folder = cwd.get()
cwd.set(projects.getCurrent(context).path, context)
return progress.wrap(PROGRESS_ID, context, async setProgress => {
const pkg = loadModule(path.resolve(folder, 'package.json'), cwd.get(), true)
const id = pkg.name
setProgress({
status: 'plugin-install',
args: [id]
})
currentPluginId = id
installationStep = 'install'
const idAndRange = `${id}@file:${folder}`
await installPackage(cwd.get(), getCommand(cwd.get()), null, idAndRange)
await initPrompts(id, context)
installationStep = 'config'
notify({
title: `Plugin installed`,
message: `Plugin ${id} installed, next step is configuration`,
icon: 'done'
})
return getInstallation(context)
})
}
function uninstall (id, context) {
return progress.wrap(PROGRESS_ID, context, async setProgress => {
setProgress({
@@ -418,9 +462,13 @@ function update (id, context) {
})
currentPluginId = id
const plugin = findOne({ id, file: cwd.get() }, context)
const { current, wanted } = await dependencies.getVersion(plugin, context)
const { current, wanted, localPath } = await dependencies.getVersion(plugin, context)
await updatePackage(cwd.get(), getCommand(cwd.get()), null, id)
if (localPath) {
await updateLocalPackage({ cwd: cwd.get(), id, localPath }, context)
} else {
await updatePackage(cwd.get(), getCommand(cwd.get()), null, id)
}
logs.add({
message: `Plugin ${id} updated from ${current} to ${wanted}`,
@@ -441,6 +489,12 @@ function update (id, context) {
})
}
async function updateLocalPackage ({ id, cwd, localPath }, context) {
const from = path.resolve(cwd, localPath)
const to = path.resolve(cwd, 'node_modules', ...id.split('/'))
await fs.copy(from, to)
}
async function updateAll (context) {
return progress.wrap('plugins-update', context, async setProgress => {
const plugins = await list(cwd.get(), context, { resetApi: false })
@@ -554,6 +608,7 @@ module.exports = {
getLogo,
getInstallation,
install,
installLocal,
uninstall,
update,
updateAll,
@@ -27,7 +27,7 @@ function set (data, context) {
}
function remove (id, context) {
context.pubsub.publish(channels.PROGRESS_REMOVED, { progressRemoved: { id } })
context.pubsub.publish(channels.PROGRESS_REMOVED, { progressRemoved: id })
return map.delete(id)
}
@@ -345,7 +345,7 @@ async function create (input, context) {
}
async function importProject (input, context) {
if (!fs.existsSync(path.join(input.path, 'node_modules'))) {
if (!input.force && !fs.existsSync(path.join(input.path, 'node_modules'))) {
throw new Error('NO_MODULES')
}
@@ -162,9 +162,9 @@ function getAnswer (id) {
return get(answers, id)
}
async function reset () {
async function reset (answers = {}) {
prompts = []
await setAnswers({})
await setAnswers(answers)
}
function list () {
@@ -1,5 +1,6 @@
const util = require('util')
const execa = require('execa')
const terminate = require('terminate')
const terminate = util.promisify(require('terminate'))
const chalk = require('chalk')
// Subs
const channels = require('../channels')
@@ -462,19 +463,19 @@ async function run (id, context) {
return task
}
function stop (id, context) {
async function stop (id, context) {
const task = findOne(id, context)
if (task && task.status === 'running' && task.child) {
task._terminating = true
try {
terminate(task.child.pid)
await terminate(task.child.pid)
} catch (e) {
console.error(e)
updateOne({
id: task.id,
status: 'terminated'
}, context)
}
updateOne({
id: task.id,
status: 'terminated'
}, context)
}
return task
}
@@ -2,12 +2,20 @@
const cwd = require('./cwd')
// Subs
const channels = require('../channels')
// Utils
const { log } = require('../util/logger')
let currentView
function createViewsSet () {
// Builtin views
return [
{
id: 'vue-project-dashboard',
name: 'project-dashboard',
icon: 'dashboard',
tooltip: 'org.vue.components.project-nav.tooltips.dashboard'
},
{
id: 'vue-project-plugins',
name: 'project-plugins',
@@ -17,7 +25,7 @@ function createViewsSet () {
{
id: 'vue-project-dependencies',
name: 'project-dependencies',
icon: 'widgets',
icon: 'collections_bookmark',
tooltip: 'org.vue.components.project-nav.tooltips.dependencies',
projectTypes: ['vue', 'unknown']
},
@@ -58,13 +66,23 @@ function findOne (id) {
return views.find(r => r.id === id)
}
function add (view, context) {
async function add ({ view, project }, context) {
remove(view.id, context)
// Default icon
if (!view.icon) {
const plugins = require('./plugins')
const plugin = plugins.findOne({ id: view.pluginId, file: cwd.get() }, context)
const logo = await plugins.getLogo(plugin, context)
view.icon = logo ? `${logo}?project=${project.id}` : 'radio_button_unchecked'
}
const views = getViews()
views.push(view)
context.pubsub.publish(channels.VIEW_ADDED, {
viewAdded: view
})
log('View added', view.id)
}
function remove (id, context) {
@@ -0,0 +1,334 @@
const shortid = require('shortid')
// Connectors
const cwd = require('./cwd')
const prompts = require('./prompts')
// Utils
const { log } = require('../util/logger')
function getDefaultWidgets () {
return [
{
id: shortid(),
definitionId: 'org.vue.widgets.welcome',
x: 0,
y: 0,
width: 3,
height: 4,
configured: true,
config: null
},
{
id: shortid(),
definitionId: 'org.vue.widgets.kill-port',
x: 3,
y: 0,
width: 2,
height: 1,
configured: true,
config: null
}
]
}
let widgetDefs = new Map()
let widgetCount = new Map()
let widgets = []
let currentWidget
let loadPromise, loadResolve
function reset (context) {
widgetDefs = new Map()
widgetCount = new Map()
widgets = []
loadPromise = new Promise((resolve) => {
loadResolve = () => {
loadPromise = null
resolve()
log('Load Promise resolved')
}
})
}
async function registerDefinition ({ definition, project }, context) {
definition.hasConfigPrompts = !!definition.onConfigOpen
// Default icon
if (!definition.icon) {
const plugins = require('./plugins')
const plugin = plugins.findOne({ id: definition.pluginId, file: cwd.get() }, context)
const logo = await plugins.getLogo(plugin, context)
if (logo) {
definition.icon = `${logo}?project=${project.id}`
}
}
widgetDefs.set(definition.id, definition)
}
function listDefinitions (context) {
return widgetDefs.values()
}
function findDefinition ({ definitionId }, context) {
const def = widgetDefs.get(definitionId)
if (!def) {
throw new Error(`Widget definition ${definitionId} not found`)
}
return def
}
async function list (context) {
log('loadPromise', loadPromise)
if (loadPromise) {
await loadPromise
}
log('widgets', widgets)
return widgets
}
function load (context) {
const projects = require('./projects')
const id = projects.getCurrent(context).id
const project = context.db.get('projects').find({ id }).value()
widgets = project.widgets
if (!widgets) {
widgets = getDefaultWidgets()
}
widgets.forEach(widget => {
updateCount(widget.definitionId, 1)
})
log('Widgets loaded', widgets.length)
loadResolve()
}
function save (context) {
const projects = require('./projects')
const id = projects.getCurrent(context).id
context.db.get('projects').find({ id }).assign({
widgets
}).write()
}
function canAddMore (definition, context) {
if (definition.maxCount) {
return getCount(definition.id) < definition.maxCount
}
return true
}
function add ({ definitionId }, context) {
const definition = findDefinition({ definitionId }, context)
const { x, y, width, height } = findValidPosition(definition)
const widget = {
id: shortid(),
definitionId,
x,
y,
width,
height,
config: null,
configured: !definition.needsUserConfig
}
// Default config
if (definition.defaultConfig) {
widget.config = definition.defaultConfig({
definition
})
}
updateCount(definitionId, 1)
widgets.push(widget)
save(context)
if (definition.onAdded) {
definition.onAdded({ widget, definition })
}
return widget
}
function getCount (definitionId) {
if (widgetCount.has(definitionId)) {
return widgetCount.get(definitionId)
} else {
return 0
}
}
function updateCount (definitionId, mod) {
widgetCount.set(definitionId, getCount(definitionId) + mod)
}
function findValidPosition (definition, currentWidget = null) {
// Find next available space
const width = (currentWidget && currentWidget.width) || definition.defaultWidth || definition.minWidth
const height = (currentWidget && currentWidget.height) || definition.defaultHeight || definition.minHeight
// Mark occupied positions on the grid
const grid = new Map()
for (const widget of widgets) {
if (widget !== currentWidget) {
for (let x = widget.x; x < widget.x + widget.width; x++) {
for (let y = widget.y; y < widget.y + widget.height; y++) {
grid.set(`${x}:${y}`, true)
}
}
}
}
// Go through the possible positions
let x = 0
let y = 0
while (true) {
// Virtual "line brak"
if (x !== 0 && x + width >= 7) {
x = 0
y++
}
const { result, testX } = hasEnoughSpace(grid, x, y, width, height)
if (!result) {
x = testX + 1
continue
}
// Found! :)
break
}
return {
x,
y,
width,
height
}
}
function hasEnoughSpace (grid, x, y, width, height) {
// Test if enough horizontal available space
for (let testX = x; testX < x + width; testX++) {
// Test if enough vertical available space
for (let testY = y; testY < y + height; testY++) {
if (grid.get(`${testX}:${testY}`)) {
return { result: false, testX }
}
}
}
return { result: true }
}
function findById ({ id }, context) {
return widgets.find(w => w.id === id)
}
function remove ({ id }, context) {
const index = widgets.findIndex(w => w.id === id)
if (index !== -1) {
const widget = widgets[index]
updateCount(widget.definitionId, -1)
widgets.splice(index, 1)
save(context)
const definition = findDefinition(widget, context)
if (definition.onAdded) {
definition.onAdded({ widget, definition })
}
return widget
}
}
function move (input, context) {
const widget = findById(input, context)
if (widget) {
widget.x = input.x
widget.y = input.y
const definition = findDefinition(widget, context)
widget.width = input.width
widget.height = input.height
if (widget.width < definition.minWidth) widget.width = definition.minWidth
if (widget.width > definition.maxWidth) widget.width = definition.maxWidth
if (widget.height < definition.minHeight) widget.height = definition.minHeight
if (widget.height > definition.maxHeight) widget.height = definition.maxHeight
for (const otherWidget of widgets) {
if (otherWidget !== widget) {
if (areOverlapping(otherWidget, widget)) {
const otherDefinition = findDefinition(otherWidget, context)
Object.assign(otherWidget, findValidPosition(otherDefinition, otherWidget))
}
}
}
save(context)
}
return widgets
}
function areOverlapping (widgetA, widgetB) {
return (
widgetA.x + widgetA.width - 1 >= widgetB.x &&
widgetA.x <= widgetB.x + widgetB.width - 1 &&
widgetA.y + widgetA.height - 1 >= widgetB.y &&
widgetA.y <= widgetB.y + widgetB.height - 1
)
}
async function openConfig ({ id }, context) {
const widget = findById({ id }, context)
const definition = findDefinition(widget, context)
if (definition.onConfigOpen) {
const result = await definition.onConfigOpen({
widget,
definition,
context
})
await prompts.reset(widget.config || {})
result.prompts.forEach(prompts.add)
await prompts.start()
currentWidget = widget
}
return widget
}
function getConfigPrompts ({ id }, context) {
return currentWidget && currentWidget.id === id ? prompts.list() : []
}
function saveConfig ({ id }, context) {
const widget = findById({ id }, context)
widget.config = prompts.getAnswers()
widget.configured = true
save(context)
currentWidget = null
return widget
}
function resetConfig ({ id }, context) {
// const widget = findById({ id }, context)
// TODO
save(context)
}
module.exports = {
reset,
registerDefinition,
listDefinitions,
findDefinition,
list,
load,
save,
canAddMore,
getCount,
add,
remove,
move,
openConfig,
getConfigPrompts,
saveConfig,
resetConfig
}
@@ -60,7 +60,7 @@ const resolvers = [{
// Iterator
(parent, args, { pubsub }) => pubsub.asyncIterator(channels.PROGRESS_REMOVED),
// Filter
(payload, vars) => payload.progressRemoved.id === vars.id
(payload, vars) => payload.progressRemoved === vars.id
)
},
clientAddonAdded: {
@@ -74,6 +74,9 @@ const resolvers = [{
},
localeAdded: {
subscribe: (parent, args, { pubsub }) => pubsub.asyncIterator(channels.LOCALE_ADDED)
},
routeRequested: {
subscribe: (parent, args, { pubsub }) => pubsub.asyncIterator(channels.ROUTE_REQUESTED)
}
}
}]
@@ -34,6 +34,7 @@ enum DependencyType {
input DependencyInstall {
id: ID!
type: DependencyType!
range: String
}
input DependencyUninstall {
@@ -15,6 +15,7 @@ extend type Query {
extend type Mutation {
pluginInstall (id: ID!): PluginInstallation
pluginInstallLocal: PluginInstallation
pluginUninstall (id: ID!): PluginInstallation
pluginInvoke (id: ID!): PluginInstallation
pluginFinishInstall: PluginInstallation
@@ -82,6 +83,7 @@ exports.resolvers = {
Mutation: {
pluginInstall: (root, { id }, context) => plugins.install(id, context),
pluginInstallLocal: (root, args, context) => plugins.installLocal(context),
pluginUninstall: (root, { id }, context) => plugins.uninstall(id, context),
pluginInvoke: (root, { id }, context) => plugins.runInvoke(id, context),
pluginFinishInstall: (root, args, context) => plugins.finishInstall(context),
@@ -55,6 +55,7 @@ input ProjectCreateInput {
input ProjectImportInput {
path: String!
force: Boolean
}
type Preset implements DescribedEntity {
@@ -24,6 +24,7 @@ type Suggestion {
type: SuggestionType!
importance: SuggestionImportance!
label: String!
image: String
message: String
link: String
actionLink: String
@@ -0,0 +1,90 @@
const gql = require('graphql-tag')
// Connectors
const widgets = require('../connectors/widgets')
exports.types = gql`
extend type Query {
widgetDefinitions: [WidgetDefinition]
widgets: [Widget]
}
extend type Mutation {
widgetAdd (input: WidgetAddInput!): Widget!
widgetRemove (id: ID!): Widget
widgetMove (input: WidgetMoveInput!): [Widget]!
widgetConfigOpen (id: ID!): Widget!
widgetConfigSave (id: ID!): Widget!
widgetConfigReset (id: ID!): Widget!
}
type WidgetDefinition {
id: ID!
title: String!
description: String
longDescription: String
link: String
icon: String
screenshot: String
component: String!
detailsComponent: String
canAddMore: Boolean!
hasConfigPrompts: Boolean!
count: Int!
maxCount: Int
minWidth: Int!
minHeight: Int!
maxWidth: Int!
maxHeight: Int!
openDetailsButton: Boolean
}
type Widget {
id: ID!
definition: WidgetDefinition!
x: Int!
y: Int!
width: Int!
height: Int!
prompts: [Prompt]
config: JSON
configured: Boolean!
}
input WidgetAddInput {
definitionId: ID!
}
input WidgetMoveInput {
id: ID!
x: Int
y: Int
width: Int
height: Int
}
`
exports.resolvers = {
WidgetDefinition: {
canAddMore: (definition, args, context) => widgets.canAddMore(definition, context),
count: (definition, args, context) => widgets.getCount(definition.id)
},
Widget: {
definition: (widget, args, context) => widgets.findDefinition(widget, context),
prompts: (widget, args, context) => widgets.getConfigPrompts(widget, context)
},
Query: {
widgetDefinitions: (root, args, context) => widgets.listDefinitions(context),
widgets: (root, args, context) => widgets.list(context)
},
Mutation: {
widgetAdd: (root, { input }, context) => widgets.add(input, context),
widgetRemove: (root, { id }, context) => widgets.remove({ id }, context),
widgetMove: (root, { input }, context) => widgets.move(input, context),
widgetConfigOpen: (root, { id }, context) => widgets.openConfig({ id }, context),
widgetConfigSave: (root, { id }, context) => widgets.saveConfig({ id }, context),
widgetConfigReset: (root, { id }, context) => widgets.resetConfig({ id }, context)
}
}
@@ -21,6 +21,7 @@ type Version {
latest: String
wanted: String
range: String
localPath: String
}
type GitHubStats {
@@ -79,6 +80,7 @@ type Subscription {
clientAddonAdded: ClientAddon
sharedDataUpdated (id: ID!, projectId: ID!): SharedData
localeAdded: Locale
routeRequested: JSON!
}
`]
+7
View File
@@ -18,6 +18,13 @@ exports.clientAddonConfig = function ({ id, port = 8042 }) {
config.plugins.delete('html')
config.plugins.delete('optimize-css')
config.optimization.splitChunks(false)
config.module
.rule('gql')
.test(/\.(gql|graphql)$/)
.use('gql-loader')
.loader('graphql-tag/loader')
.end()
},
devServer: {
headers: {
+141 -3
View File
@@ -1,7 +1,16 @@
{
"org": {
"vue": {
"common": {
"close": "Close",
"back": "Go back",
"more-info": "More info"
},
"components": {
"client-addon-component": {
"timeout": "Component load timeout",
"timeout-info": "The custom component takes too much time to load, there might be an error"
},
"connection-status": {
"disconnected": "Disconnected from UI server",
"connected": "Connected!"
@@ -78,6 +87,7 @@
},
"project-nav": {
"tooltips": {
"dashboard": "Dashboard",
"plugins": "Plugins",
"dependencies": "Dependencies",
"configuration": "Configuration",
@@ -105,8 +115,10 @@
"official": "Official",
"installed": "Installed",
"actions": {
"update": "Update {target}"
}
"update": "Update {target}",
"refresh": "Force Refresh {target}"
},
"local": "Local"
},
"project-dependency-item": {
"version": "version",
@@ -174,6 +186,24 @@
"done": "Done status"
}
}
},
"widget": {
"remove": "Remove widget",
"configure": "Configure widget",
"save": "Save",
"reset-config": "Reset config",
"open-details": "Show more"
},
"widget-add-pane": {
"title": "Add widgets"
},
"widget-add-item": {
"add": "Add widget",
"details": {
"title": "Widget details",
"max-instances": "Max instance count: {count}/{total}",
"unlimited": "unlimited"
}
}
},
"mixins": {
@@ -225,7 +255,8 @@
"title": "Missing modules",
"message": "It seems the project is missing the 'node_modules' folder. Please check you installed the dependencies before importing.",
"close": "Got it"
}
},
"force": "Import anyway"
}
},
"project-create": {
@@ -373,6 +404,15 @@
"cancel": "Cancel without uninstalling",
"uninstall": "Uninstall"
}
},
"buttons": {
"add-local": "Browse local plugin"
}
},
"project-plugin-add-local": {
"title": "Add a local plugin",
"buttons": {
"add": "Add local plugin"
}
},
"project-configurations": {
@@ -421,6 +461,14 @@
"uninstall": "Uninstall {id}"
}
},
"project-dashboard": {
"title": "Project dashboard",
"cutomize": "Customize",
"done": "Done"
},
"settings": {
"title": "Settings"
},
"about": {
"title": "About",
"description": "<a href=\"https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-ui\" target=\"_blank\">@vue/cli-ui</a> is a built-in package of vue-cli which opens a full-blown UI.",
@@ -696,6 +744,96 @@
"watch": "Watch files for changes and rerun tests related to changed files"
}
}
},
"widgets": {
"welcome": {
"title": "Welcome tips",
"description": "Some tips to help you get started",
"content": {
"title": "Welcome to your new project!",
"tip1": "You are looking at the project dashboard where your can put widgets. Use the 'Customize' button to add more! Everything is automatically saved.",
"tip2": "On the left are the different available pages. 'Plugins' let you add new Vue CLI plugins, 'Dependencies' for managing the packages, 'Configuration' to configure the tools and 'Tasks' to run scripts (for example webpack).",
"tip3": "Return to the project manager with the dropdown at the top left of the screen or the home button in the status bar at the bottom.",
"ok": "Understood"
}
},
"kill-port": {
"title": "Kill port",
"description": "Kill processes using a specific network port",
"input": {
"placeholder": "Enter a network port"
},
"kill": "Kill",
"status": {
"idle": "Ready to kill",
"killing": "Killing procress...",
"killed": "Killed successfully!",
"error": "Couldn't kill process"
}
},
"status-widget": {
"last-updated": "Updated",
"never": "Not updated yet",
"check": "Check for updates",
"more-info": "More details"
},
"plugin-updates": {
"title": "Plugin updates",
"description": "Monitor plugin updates",
"messages": {
"ok": "All plugins up-to-date",
"loading": "Checking for updates...",
"attention": "{n} plugin updates available"
},
"page": "Go to Plugins"
},
"dependency-updates": {
"title": "Dependency updates",
"description": "Monitor dependencies updates",
"messages": {
"ok": "All dependencies up-to-date",
"loading": "Checking for updates...",
"attention": "{n} dependency updates available"
},
"page": "Go to Dependencies"
},
"vulnerability": {
"title": "Vulnerability check",
"description": "Check for known vulnerabilities in your project dependencies",
"messages": {
"ok": "No vulnerability found",
"loading": "Checking security reports...",
"attention": "{n} vulnerabilities found"
},
"severity": {
"high": "High severity",
"medium": "Medium severity",
"low": "Low severity"
},
"direct-dep": "Direct dependency"
},
"run-task": {
"title": "Run task",
"description": "Shortcut to run a task",
"prompts": {
"task": "Select a task"
},
"page": "Go to Task"
},
"news": {
"title": "News feed",
"description": "Read news feed",
"refresh": "Force refresh",
"select-tip": "Select an item on the left",
"prompts": {
"url": "RSS feed URL or GitHub repository"
},
"errors": {
"fetch": "Couldn't fetch feed",
"offline": "You are offline",
"empty": "Empty feed"
}
}
}
}
}
+12 -9
View File
@@ -2,19 +2,19 @@
"name": "@vue/cli-ui",
"version": "3.0.5",
"scripts": {
"serve": "cross-env VUE_APP_CLI_UI_URL=ws://localhost:4000/graphql vue-cli-service serve",
"serve": "cross-env VUE_APP_CLI_UI_URL=ws://localhost:4030/graphql VUE_APP_GRAPHQL_PORT=4030 vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"apollo": "cross-env VUE_APP_CLI_UI_DEV=true VUE_APP_GRAPHQL_PORT=4000 vue-cli-service apollo:watch",
"apollo:run": "cross-env VUE_CLI_DEBUG=true VUE_CLI_IPC=vue-cli-dev vue-cli-service apollo:run",
"apollo:run:test": "cross-env VUE_CLI_UI_TEST=true VUE_APP_GRAPHQL_PORT=4040 VUE_APP_CLI_UI_URL=ws://localhost:4040/graphql VUE_CLI_IPC=vue-cli-test vue-cli-service apollo:watch --mode production",
"apollo": "cross-env VUE_APP_CLI_UI_DEV=true VUE_APP_GRAPHQL_PORT=4030 vue-cli-service apollo:watch",
"apollo:run": "cross-env VUE_CLI_PLUGIN_DEV=true VUE_CLI_IPC=vue-cli-dev vue-cli-service apollo:run",
"apollo:run:test": "cross-env VUE_CLI_DEBUG=true VUE_CLI_UI_TEST=true VUE_APP_GRAPHQL_PORT=4040 VUE_APP_CLI_UI_URL=ws://localhost:4040/graphql VUE_CLI_IPC=vue-cli-test vue-cli-service apollo:watch --mode production",
"prepublishOnly": "yarn run lint --no-fix && yarn run build",
"test:e2e": "yarn run test:clear && start-server-and-test apollo:run:test http://localhost:4040/.well-known/apollo/server-health test:e2e:dev",
"test:run": "yarn run test:clear && start-server-and-test apollo:run:test http://localhost:4040/.well-known/apollo/server-health test:e2e:run",
"test:e2e:dev": "cross-env VUE_APP_CLI_UI_URL=ws://localhost:4040/graphql vue-cli-service test:e2e --mode development",
"test:e2e:run": "vue-cli-service test:e2e --mode production --headless --url=http://localhost:4040",
"test:e2e": "yarn run test:clear && start-server-and-test apollo:run:test http://localhost:4040 test:e2e:dev",
"test:run": "yarn run test:clear && start-server-and-test apollo:run:test http://localhost:4040 test:e2e:run",
"test:clear": "rimraf ../../test/cli-ui-test && rimraf ./live-test",
"test": "yarn run build && cd ../cli-ui-addon-webpack && yarn run build && cd ../cli-ui && yarn run test:run"
"test": "yarn run build && cd ../cli-ui-addon-webpack && yarn run build && cd ../cli-ui-addon-widgets && yarn run build && cd ../cli-ui && yarn run test:run"
},
"files": [
"apollo-server",
@@ -34,6 +34,7 @@
"deepmerge": "^2.1.1",
"execa": "^0.10.0",
"express-history-api-fallback": "^2.2.1",
"fkill": "^5.3.0",
"fs-extra": "^6.0.1",
"globby": "^8.0.1",
"graphql-tag": "^2.9.2",
@@ -47,11 +48,12 @@
"node-notifier": "^5.2.1",
"parse-git-config": "^2.0.2",
"portfinder": "^1.0.13",
"rss-parser": "^3.4.3",
"prismjs": "^1.15.0",
"semver": "^5.5.0",
"shortid": "^2.2.11",
"terminate": "^2.1.0",
"vue-cli-plugin-apollo": "^0.16.6",
"vue-cli-plugin-apollo": "^0.17.3",
"watch": "^1.0.2"
},
"devDependencies": {
@@ -73,7 +75,7 @@
"stylus": "^0.54.5",
"stylus-loader": "^3.0.1",
"vue": "^2.5.17",
"vue-apollo": "^3.0.0-beta.17",
"vue-apollo": "^3.0.0-beta.25",
"vue-color": "^2.4.6",
"vue-i18n": "^8.0.0",
"vue-instantsearch": "^1.5.1",
@@ -81,6 +83,7 @@
"vue-observe-visibility": "^0.4.1",
"vue-router": "^3.0.1",
"vue-template-compiler": "^2.5.17",
"vue-timeago": "^5.0.0",
"xterm": "^3.2.0"
},
"browserslist": [
+13
View File
@@ -15,6 +15,8 @@
<script>
import i18n from './i18n'
import ROUTE_REQUESTED from '@/graphql/app/routeRequested.gql'
export default {
metaInfo: {
titleTemplate: chunk => chunk ? `[Beta] ${chunk} - Vue CLI` : '[Beta] Vue CLI'
@@ -24,6 +26,17 @@ export default {
ready () {
return Object.keys(i18n.getLocaleMessage('en')).length
}
},
apollo: {
$subscribe: {
routeRequested: {
query: ROUTE_REQUESTED,
result ({ data }) {
this.$router.push(data.routeRequested)
}
}
}
}
}
</script>
@@ -1,210 +0,0 @@
<template>
<div class="top-bar">
<VueDropdown
v-if="$responsive.wide"
:label="projectCurrent ? projectCurrent.name : $t('org.vue.components.status-bar.project.empty')"
class="current-project"
icon-right="arrow_drop_down"
button-class="round"
>
<!-- Current project options -->
<template v-if="projectCurrent">
<VueSwitch
:value="projectCurrent.favorite"
:icon="projectCurrent.favorite ? 'star' : 'star_border'"
class="extend-left"
@input="toggleCurrentFavorite()"
>
{{ $t('org.vue.components.project-select-list-item.tooltips.favorite') }}
</VueSwitch>
<VueDropdownButton
:label="$t('org.vue.components.project-select-list-item.tooltips.open-in-editor')"
icon-left="open_in_browser"
@click="openInEditor(projectCurrent)"
/>
<VueDropdownButton
v-if="projectCurrent.homepage"
:href="projectCurrent.homepage"
:label="$t('org.vue.components.top-bar.homepage')"
target="_blank"
icon-left="open_in_new"
/>
</template>
<div class="dropdown-separator"/>
<!-- Favorites -->
<div v-if="!favoriteProjects.length" class="vue-ui-empty">{{ $t('org.vue.components.top-bar.no-favorites') }}</div>
<template v-else>
<div class="section-title">
{{ $t('org.vue.components.top-bar.favorite-projects') }}
</div>
<VueDropdownButton
v-for="project of favoriteProjects"
:key="project.id"
:label="project.name"
icon-left="star"
@click="openProject(project)"
/>
</template>
<!-- Recents -->
<template v-if="recentProjects.length">
<div class="dropdown-separator"/>
<div class="section-title">
{{ $t('org.vue.components.top-bar.recent-projects') }}
</div>
<VueDropdownButton
v-for="project of recentProjects"
:key="project.id"
:label="project.name"
icon-left="restore"
@click="openProject(project)"
/>
</template>
<div class="dropdown-separator"/>
<VueDropdownButton
:to="{ name: 'project-select' }"
:label="$t('org.vue.views.project-select.title')"
icon-left="home"
/>
</VueDropdown>
<portal-target name="top-title" class="title">Vue</portal-target>
<AppLoading/>
<div class="vue-ui-spacer"/>
<SuggestionBar/>
<portal-target name="top-actions" class="actions"/>
</div>
</template>
<script>
import { resetApollo } from '../vue-apollo'
import PROJECT_CURRENT from '../graphql/projectCurrent.gql'
import PROJECTS from '../graphql/projects.gql'
import PROJECT_OPEN from '../graphql/projectOpen.gql'
import PROJECT_SET_FAVORITE from '../graphql/projectSetFavorite.gql'
import OPEN_IN_EDITOR from '../graphql/fileOpenInEditor.gql'
import CURRENT_PROJECT_ID_SET from '../graphql/currentProjectIdSet.gql'
export default {
apollo: {
projectCurrent: PROJECT_CURRENT,
projects: PROJECTS
},
computed: {
favoriteProjects () {
if (!this.projects) return []
return this.projects.filter(
p => p.favorite && (!this.projectCurrent || this.projectCurrent.id !== p.id)
)
},
recentProjects () {
if (!this.projects) return []
return this.projects.filter(
p => !p.favorite && (!this.projectCurrent || this.projectCurrent.id !== p.id)
).sort((a, b) => b.openDate - a.openDate).slice(0, 3)
}
},
methods: {
async openProject (project) {
this.$bus('quickOpenProject', project)
await this.$apollo.mutate({
mutation: PROJECT_OPEN,
variables: {
id: project.id
}
})
await resetApollo()
await this.$apollo.mutate({
mutation: CURRENT_PROJECT_ID_SET,
variables: {
projectId: project.id
}
})
},
async toggleCurrentFavorite () {
if (this.projectCurrent) {
await this.$apollo.mutate({
mutation: PROJECT_SET_FAVORITE,
variables: {
id: this.projectCurrent.id,
favorite: this.projectCurrent.favorite ? 0 : 1
}
})
}
},
async openInEditor (project) {
await this.$apollo.mutate({
mutation: OPEN_IN_EDITOR,
variables: {
input: {
file: project.path
}
}
})
}
}
}
</script>
<style lang="stylus" scoped>
.top-bar
background $vue-ui-color-light
padding $padding-item
h-box()
align-items center
position relative
height 32px
z-index 1
box-shadow 0 2px 10px rgba(black, .05)
.vue-ui-dark-mode &
background $vue-ui-color-darker
box-shadow 0 2px 10px rgba(black, .2)
&,
.actions
/deep/ > *
space-between-x($padding-item)
.current-project
min-width (180px - $padding-item * 2)
margin-right ($padding-item * 2)
>>> .trigger
.vue-ui-button
.vue-ui-icon.right
width 20px
height @width
.vue-ui-empty
padding 6px
.title
font-size 22px
font-weight lighter
</style>
@@ -10,7 +10,7 @@
</template>
<script>
import LOADING from '../graphql/loading.gql'
import LOADING from '@/graphql/loading/loading.gql'
export default {
apollo: {
@@ -1,6 +1,6 @@
<template>
<ApolloQuery
:query="require('../graphql/connected.gql')"
:query="require('@/graphql/connected/connected.gql')"
fetch-policy="cache-only"
class="connection-status"
>
@@ -1,8 +1,8 @@
<script>
import { mergeLocale } from '../i18n'
import { mergeLocale } from '@/i18n'
import LOCALES from '../graphql/locales.gql'
import LOCALE_ADDED from '../graphql/localeAdded.gql'
import LOCALES from '@/graphql/locale/locales.gql'
import LOCALE_ADDED from '@/graphql/locale/localeAdded.gql'
export default {
apollo: {
@@ -0,0 +1,58 @@
<template>
<div class="not-found page">
<template v-if="addonRouteTimout">
<VueIcon icon="cake" class="huge"/>
<h1 class="title">Addon route taking too long to load</h1>
<h2 class="subtitle">The route may not exist</h2>
<VueButton :to="{ name: 'home' }">Go home</VueButton>
</template>
<template v-else-if="isAddonRoute">
<VueLoadingIndicator
class="accent big"
/>
</template>
<template v-else>
<VueIcon icon="pets" class="huge"/>
<h1 class="title">View not found</h1>
<VueButton :to="{ name: 'home' }">Go home</VueButton>
</template>
</div>
</template>
<script>
export default {
name: 'NotFound',
data () {
return {
addonRouteTimout: false
}
},
computed: {
isAddonRoute () {
return this.$route.path.includes('/addon/')
}
},
mounted () {
if (this.isAddonRoute) {
setTimeout(() => {
this.addonRouteTimout = true
}, 5000)
}
}
}
</script>
<style lang="stylus" scoped>
.not-found
v-box()
box-center()
height 100%
.vue-ui-icon,
.title,
.subtitle
margin 0 0 $padding-item
</style>
@@ -52,7 +52,7 @@
<script>
import { DisableScroll } from '@vue/ui'
import Progress from '../mixins/Progress'
import Progress from '@/mixins/Progress'
export default {
mixins: [
@@ -5,13 +5,12 @@
wide: $responsive.wide
}"
>
<TopBar />
<div class="panes">
<ProjectNav/>
<ViewNav/>
<div v-if="ready" class="content vue-ui-disable-scroll">
<router-view/>
<TopBar />
<router-view class="router-view"/>
</div>
</div>
@@ -20,7 +19,7 @@
</template>
<script>
import PROJECT_CWD_RESET from '../graphql/projectCwdReset.gql'
import PROJECT_CWD_RESET from '@/graphql/project/projectCwdReset.gql'
export default {
name: 'ProjectHome',
@@ -47,7 +46,7 @@ export default {
&.wide
.project-nav
width 180px
width 220px
.panes
flex auto 1 1
@@ -67,5 +66,14 @@ export default {
width 0
overflow-x hidden
overflow-y auto
display flex
flex-direction column
.top-bar
flex auto 0 0
.router-view
flex 1
height 0
overflow hidden
</style>
@@ -0,0 +1,173 @@
<template>
<VueDropdown
v-if="$responsive.wide"
:label="projectCurrent ? projectCurrent.name : $t('org.vue.components.status-bar.project.empty')"
class="current-project project-quick-dropdown"
icon-right="arrow_drop_down"
button-class="round"
>
<!-- Current project options -->
<template v-if="projectCurrent">
<VueSwitch
:value="projectCurrent.favorite"
:icon="projectCurrent.favorite ? 'star' : 'star_border'"
class="extend-left"
@input="toggleCurrentFavorite()"
>
{{ $t('org.vue.components.project-select-list-item.tooltips.favorite') }}
</VueSwitch>
<VueDropdownButton
:label="$t('org.vue.components.project-select-list-item.tooltips.open-in-editor')"
icon-left="open_in_browser"
@click="openInEditor(projectCurrent)"
/>
<VueDropdownButton
v-if="projectCurrent.homepage"
:href="projectCurrent.homepage"
:label="$t('org.vue.components.top-bar.homepage')"
target="_blank"
icon-left="open_in_new"
/>
</template>
<div class="dropdown-separator"/>
<!-- Favorites -->
<div v-if="!favoriteProjects.length" class="vue-ui-empty">{{ $t('org.vue.components.top-bar.no-favorites') }}</div>
<template v-else>
<div class="section-title">
{{ $t('org.vue.components.top-bar.favorite-projects') }}
</div>
<VueDropdownButton
v-for="project of favoriteProjects"
:key="project.id"
:label="project.name"
icon-left="star"
@click="openProject(project)"
/>
</template>
<!-- Recents -->
<template v-if="recentProjects.length">
<div class="dropdown-separator"/>
<div class="section-title">
{{ $t('org.vue.components.top-bar.recent-projects') }}
</div>
<VueDropdownButton
v-for="project of recentProjects"
:key="project.id"
:label="project.name"
icon-left="restore"
@click="openProject(project)"
/>
</template>
<div class="dropdown-separator"/>
<VueDropdownButton
:to="{ name: 'project-select' }"
:label="$t('org.vue.views.project-select.title')"
icon-left="home"
/>
</VueDropdown>
</template>
<script>
import { resetApollo } from '@/vue-apollo'
import PROJECT_CURRENT from '@/graphql/project/projectCurrent.gql'
import PROJECTS from '@/graphql/project/projects.gql'
import PROJECT_OPEN from '@/graphql/project/projectOpen.gql'
import PROJECT_SET_FAVORITE from '@/graphql/project/projectSetFavorite.gql'
import OPEN_IN_EDITOR from '@/graphql/file/fileOpenInEditor.gql'
import CURRENT_PROJECT_ID_SET from '@/graphql/project/currentProjectIdSet.gql'
export default {
apollo: {
projectCurrent: PROJECT_CURRENT,
projects: PROJECTS
},
computed: {
favoriteProjects () {
if (!this.projects) return []
return this.projects.filter(
p => p.favorite && (!this.projectCurrent || this.projectCurrent.id !== p.id)
)
},
recentProjects () {
if (!this.projects) return []
return this.projects.filter(
p => !p.favorite && (!this.projectCurrent || this.projectCurrent.id !== p.id)
).sort((a, b) => b.openDate - a.openDate).slice(0, 3)
}
},
methods: {
async openProject (project) {
this.$bus('quickOpenProject', project)
await this.$apollo.mutate({
mutation: PROJECT_OPEN,
variables: {
id: project.id
}
})
await resetApollo()
await this.$apollo.mutate({
mutation: CURRENT_PROJECT_ID_SET,
variables: {
projectId: project.id
}
})
},
async toggleCurrentFavorite () {
if (this.projectCurrent) {
await this.$apollo.mutate({
mutation: PROJECT_SET_FAVORITE,
variables: {
id: this.projectCurrent.id,
favorite: this.projectCurrent.favorite ? 0 : 1
}
})
}
},
async openInEditor (project) {
await this.$apollo.mutate({
mutation: OPEN_IN_EDITOR,
variables: {
input: {
file: project.path
}
}
})
}
}
}
</script>
<style lang="stylus" scoped>
.current-project
>>> .trigger
.vue-ui-button
.vue-ui-icon.right
width 20px
height @width
.vue-ui-empty
padding 6px
</style>
@@ -16,13 +16,13 @@
</div>
<ApolloQuery
:query="require('@/graphql/cwd.gql')"
:query="require('@/graphql/cwd/cwd.gql')"
class="section current-path"
v-tooltip="$t('org.vue.components.status-bar.path.tooltip')"
@click.native="onCwdClick()"
>
<ApolloSubscribeToMore
:document="require('@/graphql/cwdChanged.gql')"
:document="require('@/graphql/cwd/cwdChanged.gql')"
:update-query="(previousResult, { subscriptionData }) => ({
cwd: subscriptionData.data.cwd
})"
@@ -82,13 +82,13 @@
</template>
<script>
import PROJECT_CURRENT from '../graphql/projectCurrent.gql'
import CONSOLE_LOG_LAST from '../graphql/consoleLogLast.gql'
import CONSOLE_LOG_ADDED from '../graphql/consoleLogAdded.gql'
import DARK_MODE_SET from '../graphql/darkModeSet.gql'
import PLUGIN_RESET_API from '../graphql/pluginResetApi.gql'
import { resetApollo } from '../vue-apollo'
import { getForcedTheme } from '../util/theme'
import PROJECT_CURRENT from '@/graphql/project/projectCurrent.gql'
import CONSOLE_LOG_LAST from '@/graphql/console-log/consoleLogLast.gql'
import CONSOLE_LOG_ADDED from '@/graphql/console-log/consoleLogAdded.gql'
import DARK_MODE_SET from '@/graphql/dark-mode/darkModeSet.gql'
import PLUGIN_RESET_API from '@/graphql/plugin/pluginResetApi.gql'
import { resetApollo } from '@/vue-apollo'
import { getForcedTheme } from '@/util/theme'
let lastRoute
@@ -186,33 +186,37 @@ export default {
<style lang="stylus" scoped>
.status-bar
position relative
z-index 1
box-shadow 0 -2px 10px rgba(black, .05)
z-index 3
box-shadow 0 -2px 10px rgba(black, .1)
.vue-ui-dark-mode &
box-shadow 0 -2px 10px rgba(black, .2)
.content
h-box()
align-items center
background $vue-ui-color-light
font-size 12px
height 34px
height 28px
background $vue-ui-color-darker
color $vue-ui-color-light
>>> .vue-ui-icon svg
fill @color
.vue-ui-dark-mode &
background $vue-ui-color-darker
background $vue-ui-color-primary
color $vue-ui-color-dark
>>> .vue-ui-icon svg
fill @color
.section
h-box()
align-items center
opacity .8
padding 0 11px
padding 0 8px
height 100%
cursor default
&:hover
opacity 1
background lighten($vue-ui-color-light-neutral, 30%)
background lighten($vue-ui-color-darker, 10%)
.vue-ui-dark-mode &
background $vue-ui-color-dark
background darken($vue-ui-color-primary, 10%)
> .vue-ui-icon + *
margin-left 4px
@@ -235,4 +239,10 @@ export default {
.logger-message
font-size .9em
padding-right 0
.last-message >>> .message
> span
color $vue-ui-color-light
.vue-ui-dark-mode &
color $vue-ui-color-dark
</style>
@@ -0,0 +1,35 @@
<template>
<div class="top-bar">
<portal-target name="top-title" class="title">Vue</portal-target>
<AppLoading/>
<div class="vue-ui-spacer"/>
<SuggestionBar/>
<portal-target name="top-actions" class="actions"/>
</div>
</template>
<style lang="stylus" scoped>
.top-bar
padding $padding-item
h-box()
align-items center
position relative
height 32px
z-index 1
background $content-bg-secondary-light
.vue-ui-dark-mode &
background $content-bg-secondary-dark
&,
.actions
/deep/ > *
space-between-x($padding-item)
.title
font-size 28px
font-weight lighter
</style>
@@ -3,6 +3,18 @@
v-if="component"
:is="component"
/>
<div v-else-if="timeout" class="vue-ui-empty">
<VueIcon
icon="cake"
class="big"
/>
<div class="timeout-title">
{{ $t('org.vue.components.client-addon-component.timeout') }}
</div>
<div class="timeout-info">
{{ $t('org.vue.components.client-addon-component.timeout-info') }}
</div>
</div>
<div v-else class="loading">
<VueLoadingIndicator />
</div>
@@ -19,7 +31,8 @@ export default {
data () {
return {
component: null
component: null,
timeout: false
}
},
@@ -32,6 +45,11 @@ export default {
methods: {
async updateComponent () {
setTimeout(() => {
if (!this.component) {
this.timeout = true
}
}, 5000)
this.component = await ClientAddonApi.awaitComponent(this.name)
}
}
@@ -43,4 +61,9 @@ export default {
v-box()
box-center()
padding 100px
.timeout-info
max-width 200px
font-size 10px
margin auto
</style>
@@ -1,6 +1,6 @@
<script>
import CLIENT_ADDONS from '../graphql/clientAddons.gql'
import CLIENT_ADDON_ADDED from '../graphql/clientAddonAdded.gql'
import CLIENT_ADDONS from '@/graphql/client-addon/clientAddons.gql'
import CLIENT_ADDON_ADDED from '@/graphql/client-addon/clientAddonAdded.gql'
export default {
apollo: {
@@ -8,9 +8,9 @@
</template>
<script>
import Prompts from '../mixins/Prompts'
import Prompts from '@/mixins/Prompts'
import CONFIGURATION from '../graphql/configuration.gql'
import CONFIGURATION from '@/graphql/configuration/configuration.gql'
export default {
mixins: [
@@ -73,9 +73,9 @@
</template>
<script>
import CONFIGURATION from '../graphql/configuration.gql'
import CONFIGURATION_SAVE from '../graphql/configurationSave.gql'
import CONFIGURATION_CANCEL from '../graphql/configurationCancel.gql'
import CONFIGURATION from '@/graphql/configuration/configuration.gql'
import CONFIGURATION_SAVE from '@/graphql/configuration/configurationSave.gql'
import CONFIGURATION_CANCEL from '@/graphql/configuration/configurationCancel.gql'
export default {
metaInfo () {
@@ -179,6 +179,9 @@ export default {
v-box()
align-items stretch
height 100%
background $vue-ui-color-light
.vue-ui-dark-mode &
background $vue-ui-color-darker
.content,
.loading
@@ -4,16 +4,8 @@
:title="$t('org.vue.views.project-configurations.title')"
class="limit-width"
>
<template slot="actions">
<VueInput
v-model="search"
icon-left="search"
class="round"
/>
</template>
<ApolloQuery
:query="require('../graphql/configurations.gql')"
:query="require('@/graphql/configuration/configurations.gql')"
class="fill-height"
>
<template slot-scope="{ result: { data, loading } }">
@@ -27,6 +19,17 @@
:items="generateItems(data.configurations)"
class="configurations"
>
<div
slot="before"
class="list-header"
>
<VueInput
v-model="search"
icon-left="search"
class="search round"
/>
</div>
<ConfigurationItem
slot-scope="{ item, selected }"
:configuration="item.configuration"
@@ -40,10 +43,10 @@
</template>
<script>
import RestoreRoute from '../mixins/RestoreRoute'
import { generateSearchRegex } from '../util/search'
import RestoreRoute from '@/mixins/RestoreRoute'
import { generateSearchRegex } from '@/util/search'
import CONFIGS from '../graphql/configurations.gql'
import CONFIGS from '@/graphql/configuration/configurations.gql'
export default {
mixins: [
@@ -28,25 +28,35 @@ export default {
.content-view
height 100%
.content,
.wrapper
width 100%
height 100%
box-sizing border-box
.content
height 100%
background $color-background-light
background $content-bg-secondary-light
.vue-ui-dark-mode &
background lighten($vue-ui-color-darker, 1%)
background $content-bg-secondary-dark
.wrapper
background $md-white
background $content-bg-primary-light
position relative
overflow-x hidden
overflow-y auto
.vue-ui-dark-mode &
background $vue-ui-color-darker
background $content-bg-primary-dark
&.list
.wrapper
background $content-bg-list-light
.vue-ui-dark-mode &
background $content-bg-list-dark
&.limit-width
.wrapper
max-width 1200px
@media (min-width 1420px)
max-width 1200px
margin auto
$br2 = ($br * 2)
border-radius $br2 $br2 0 0
</style>
@@ -35,6 +35,8 @@
</template>
<script>
import { getImageUrl } from '@/util/image'
export default {
props: {
image: {
@@ -75,11 +77,7 @@ export default {
},
imageUrl () {
// Fix images in development
if (process.env.VUE_APP_CLI_UI_DEV && this.image.charAt(0) === '/') {
return `http://localhost:4000${this.image}`
}
return this.image
return getImageUrl(this.image)
}
},
@@ -101,14 +99,18 @@ export default {
.item-logo
margin-right $padding-item
position relative
width 42px
height @width
.wrapper
h-box()
box-center()
width 42px
width 100%
height @width
background rgba(black, .03)
border-radius 50%
overflow hidden
.vue-ui-dark-mode &
background rgba(white, .07)
.image
width 100%
height @width
@@ -118,7 +120,7 @@ export default {
width 24px
height @width
>>> svg
fill rgba($color-text-light, .3)
fill $color-text-light
.color-bullet
position absolute
@@ -3,9 +3,13 @@
<NavList
:items="items"
>
<slot name="before" slot="before"/>
<template slot-scope="props">
<slot v-bind="props"/>
</template>
<slot name="after" slot="after"/>
</NavList>
<div class="content vue-ui-disable-scroll">
@@ -1,6 +1,8 @@
<template>
<div class="nav-list vue-ui-disable-scroll">
<div class="content">
<slot name="before"/>
<div
v-for="item of items"
:key="item.id"
@@ -11,12 +13,14 @@
:selected="item.route === currentRoute"
/>
</div>
<slot name="after"/>
</div>
</div>
</template>
<script>
import { isSameRoute, isIncludedRoute } from '../util/route'
import { isSameRoute, isIncludedRoute } from '@/util/route'
export default {
props: {
@@ -49,7 +53,7 @@ export default {
.nav-list
overflow-x hidden
overflow-y auto
background $color-background-light
background $content-bg-list-light
.vue-ui-dark-mode &
background lighten($vue-ui-color-darker, 1%)
background $content-bg-list-dark
</style>
@@ -80,9 +80,9 @@ export default {
.header,
>>> .tabs
background $vue-ui-color-light-neutral
background $content-bg-primary-light
.vue-ui-dark-mode &
background $vue-ui-color-dark
background $content-bg-primary-dark
>>> .tabs-content
height 0
@@ -1,12 +1,12 @@
<template>
<div class="terminal-view">
<div class="terminal-view card">
<div v-if="toolbar" class="pane-toolbar">
<VueIcon
icon="dvr"
/>
<div class="title">{{ title }}</div>
<VueButton
class="icon-button"
class="icon-button flat"
icon-left="delete_forever"
v-tooltip="$t('org.vue.components.terminal-view.buttons.clear')"
@click="clear(); $emit('clear')"
@@ -16,7 +16,7 @@
class="separator"
/>
<VueButton
class="icon-button"
class="icon-button flat"
icon-left="subdirectory_arrow_left"
v-tooltip="$t('org.vue.components.terminal-view.buttons.scroll')"
@click="scrollToBottom()"
@@ -41,7 +41,7 @@ Terminal.applyAddon(webLinks)
const defaultTheme = {
foreground: '#2c3e50',
background: '#e4f5ef',
background: '#fff',
cursor: 'rgba(0, 0, 0, .4)',
selection: 'rgba(0, 0, 0, 0.3)',
black: '#000000',
@@ -65,7 +65,7 @@ const defaultTheme = {
const darkTheme = {
...defaultTheme,
foreground: '#fff',
background: '#2c3e50',
background: '#1d2935',
cursor: 'rgba(255, 255, 255, .4)',
selection: 'rgba(255, 255, 255, 0.3)',
magenta: '#e83030',
@@ -142,7 +142,7 @@ export default {
if (typeof oldValue === 'undefined') {
this.initTerminal()
} else if (this.$_terminal) {
this.$_terminal._setTheme(this.theme)
this.$_terminal.setOption('theme', this.theme)
}
}
},
@@ -233,9 +233,6 @@ export default {
.terminal-view
v-box()
align-items stretch
background $vue-ui-color-light-neutral
.vue-ui-dark-mode &
background $vue-ui-color-dark
.view
flex 100% 1 1
@@ -0,0 +1,142 @@
<template>
<div
class="project-dashboard page"
:class="{
customizing: customizeMode,
'widget-details-shown': injected.isWidgetDetailsShown
}"
>
<ContentView
:title="$t('org.vue.views.project-dashboard.title')"
>
<template slot="actions">
<VueButton
v-if="!customizeMode"
icon-left="edit"
:label="$t('org.vue.views.project-dashboard.cutomize')"
class="primary round"
@click="customizeMode = true"
/>
<VueButton
v-else
icon-left="done"
:label="$t('org.vue.views.project-dashboard.done')"
class="primary round"
@click="customizeMode = false"
/>
</template>
<div class="panes fill-height">
<ApolloQuery
ref="widgets"
:query="require('@/graphql/widget/widgets.gql')"
class="widgets"
>
<div
slot-scope="{ result: { data, loading } }"
class="widgets-wrapper"
>
<VueLoadingIndicator
v-if="loading && (!data || !data.widgets)"
class="overlay"
/>
<template v-else-if="data">
<Widget
v-for="widget of data.widgets"
:key="widget.id"
:widget="widget"
:customize-mode="customizeMode"
/>
</template>
</div>
</ApolloQuery>
<transition name="sidepane">
<WidgetAddPane
v-if="customizeMode"
@close="customizeMode = false"
/>
</transition>
</div>
</ContentView>
</div>
</template>
<script>
import OnWindowResize from '@/mixins/OnWindowResize'
const PADDING = 8
export default {
provide () {
return {
dashboard: this.injected
}
},
mixins: [
OnWindowResize()
],
metaInfo () {
return {
title: this.$t('org.vue.views.project-dashboard.title')
}
},
data () {
return {
customizeMode: false,
injected: {
width: 0,
height: 0,
left: 0,
top: 0,
isWidgetDetailsShown: false
}
}
},
methods: {
onWindowResize () {
const el = this.$refs.widgets.$el
if (!el) return
const bounds = el.getBoundingClientRect()
this.injected.width = bounds.width - PADDING * 2
this.injected.height = bounds.height - PADDING * 2
this.injected.left = bounds.left
this.injected.top = bounds.top
}
}
}
</script>
<style lang="stylus" scoped>
.panes
h-box()
.widgets
flex 1
overflow auto
padding ($padding-item / 2)
box-sizing border-box
.widgets-wrapper
position relative
transform-origin top left
transition transform .15s
.widget-add-pane
width 360px
.customizing
.widgets-wrapper
transform scale(.7)
.widget-details-shown
.widgets
overflow hidden
.widgets-wrapper > .widget /deep/ > .shell
opacity 0
</style>
@@ -0,0 +1,615 @@
<template>
<transition duration="150">
<div
class="widget"
:class="{
customizing: customizeMode,
moving: moveState,
resizing: resizeState,
selected: isSelected,
'details-shown': showDetails,
details
}"
>
<div
ref="shell"
class="shell"
:style="shellStyle || (!details && mainStyle)"
>
<div class="wrapper card">
<div class="content-wrapper">
<div class="header">
<div class="title">{{ injected.customTitle || $t(widget.definition.title) }}</div>
<!-- Custom actions -->
<template v-if="widget.configured">
<VueButton
v-for="action of headerActions"
v-if="!action.hidden"
:key="action.id"
:icon-left="action.icon"
:disabled="action.disabled"
class="icon-button flat primary"
v-tooltip="$t(action.tooltip)"
@click="action.onCalled()"
/>
</template>
<!-- Settings button -->
<VueButton
v-if="widget.definition.hasConfigPrompts"
icon-left="settings"
class="icon-button flat primary"
v-tooltip="$t('org.vue.components.widget.configure')"
@click="openConfig()"
/>
<!-- Close button -->
<VueButton
v-if="details"
icon-left="close"
class="icon-button flat primary"
@click="$emit('close')"
/>
<!-- Open details button -->
<VueButton
v-else-if="widget.definition.openDetailsButton"
icon-left="zoom_out_map"
class="icon-button flat primary"
v-tooltip="$t('org.vue.components.widget.open-details')"
@click="openDetails()"
/>
</div>
<div v-if="widget.configured" class="content">
<ClientAddonComponent
:name="component"
class="view"
/>
</div>
<div v-else class="content not-configured">
<VueIcon
icon="settings"
class="icon huge"
/>
<VueButton
:label="$t('org.vue.components.widget.configure')"
@click="openConfig()"
/>
</div>
</div>
<div
v-if="customizeMode"
class="customize-overlay"
@mousedown="onMoveStart"
@click="select()"
>
<div class="definition-chip">
<ItemLogo
:image="widget.definition.icon"
fallback-icon="widgets"
class="icon"
/>
<div class="title">{{ injected.customTitle || $t(widget.definition.title) }}</div>
</div>
<VueButton
class="remove-button primary flat icon-button"
icon-left="close"
v-tooltip="$t('org.vue.components.widget.remove')"
@mousedown.native.stop
@click.stop="remove()"
/>
<template v-if="showResizeHandle">
<div
v-for="handle of resizeHandles"
:key="handle"
class="resize-handle"
:class="[
handle
]"
@mousedown.stop="onResizeStart($event, handle)"
/>
</template>
</div>
</div>
</div>
<div
v-if="moveState"
class="move-ghost"
:style="moveGhostStyle"
>
<div class="backdrop"/>
</div>
<div
v-if="resizeState"
class="resize-ghost"
:style="resizeGhostStyle"
>
<div class="backdrop"/>
</div>
<VueModal
v-if="showConfig"
:title="$t('org.vue.components.widget.configure')"
class="medium"
@close="showConfig = false"
>
<div class="default-body">
<PromptsList
:prompts="visiblePrompts"
@answer="answerPrompt"
/>
</div>
<div slot="footer" class="actions">
<VueButton
class="primary big"
:label="$t('org.vue.components.widget.save')"
@click="saveConfig()"
/>
</div>
</VueModal>
<WidgetDetailsView
v-if="!details && showDetails"
:widget="widget"
:shell-origin="shellOrigin"
@close="closeDetails()"
/>
</div>
</transition>
</template>
<script>
import Vue from 'vue'
import Prompts from '@/mixins/Prompts'
import OnGrid from '@/mixins/OnGrid'
import Movable from '@/mixins/Movable'
import Resizable from '@/mixins/Resizable'
import WIDGET_REMOVE from '@/graphql/widget/widgetRemove.gql'
import WIDGET_MOVE from '@/graphql/widget/widgetMove.gql'
import WIDGETS from '@/graphql/widget/widgets.gql'
import WIDGET_FRAGMENT from '@/graphql/widget/widgetFragment.gql'
import WIDGET_DEFINITION_FRAGMENT from '@/graphql/widget/widgetDefinitionFragment.gql'
import WIDGET_CONFIG_OPEN from '@/graphql/widget/widgetConfigOpen.gql'
import WIDGET_CONFIG_SAVE from '@/graphql/widget/widgetConfigSave.gql'
const GRID_SIZE = 200
const ZOOM = 0.7
const state = new Vue({
data: {
selectedWidgetId: null
}
})
export default {
provide () {
return {
widget: this.injected
}
},
inject: [
'dashboard'
],
mixins: [
Prompts({
field: 'widget',
update (store, prompts) {
store.writeFragment({
fragment: WIDGET_FRAGMENT,
fragmentName: 'widget',
id: this.widget.id,
data: {
prompts
}
})
}
}),
OnGrid({
field: 'widget',
gridSize: GRID_SIZE
}),
Movable({
field: 'widget',
gridSize: GRID_SIZE,
zoom: ZOOM
}),
Resizable({
field: 'widget',
gridSize: GRID_SIZE,
zoom: ZOOM
})
],
props: {
widget: {
type: Object,
required: true
},
customizeMode: {
type: Boolean,
default: false
},
details: {
type: Boolean,
default: false
},
shellStyle: {
type: Object,
default: null
}
},
data () {
return {
showConfig: false,
showDetails: false,
injected: {
// State
data: this.widget,
isDetails: this.details,
// Actions
openConfig: this.openConfig,
openDetails: this.openDetails,
closeDetails: this.closeDetails,
addHeaderAction: this.addHeaderAction,
removeHeaderAction: this.removeHeaderAction,
remove: this.remove,
// Custom
customTitle: null
},
shellOrigin: null,
headerActions: []
}
},
computed: {
isSelected () {
return this.widget.id === state.selectedWidgetId
},
component () {
if (this.details) {
return this.widget.definition.detailsComponent
}
return this.widget.definition.component
}
},
watch: {
widget: {
handler (value) {
this.injected.data = value
}
},
customizeMode (value) {
if (value) {
if (this.showDetails) this.closeDetails()
} else if (this.isSelected) {
state.selectedWidgetId = null
}
},
'dashboard.width': 'updateShellOrigin',
'dashboard.height': 'updateShellOrigin',
'widget.x': 'updateShellOrigin',
'widget.y': 'updateShellOrigin',
'widget.width': 'updateShellOrigin',
'widget.height': 'updateShellOrigin'
},
mounted () {
// Wait for animation
setTimeout(() => {
this.updateShellOrigin()
}, 150)
},
methods: {
async openConfig () {
await this.$apollo.mutate({
mutation: WIDGET_CONFIG_OPEN,
variables: {
id: this.widget.id
}
})
this.showConfig = true
},
async saveConfig () {
this.showConfig = false
await this.$apollo.mutate({
mutation: WIDGET_CONFIG_SAVE,
variables: {
id: this.widget.id
}
})
},
openDetails () {
if (this.widget.definition.detailsComponent) {
this.showDetails = true
this.dashboard.isWidgetDetailsShown = true
}
},
closeDetails () {
this.showDetails = false
this.dashboard.isWidgetDetailsShown = false
},
remove () {
this.$apollo.mutate({
mutation: WIDGET_REMOVE,
variables: {
id: this.widget.id
},
update: (store, { data: { widgetRemove } }) => {
const data = store.readQuery({ query: WIDGETS })
data.widgets = data.widgets.filter(w => w.id !== this.widget.id)
store.writeQuery({ query: WIDGETS, data })
store.writeFragment({
fragment: WIDGET_DEFINITION_FRAGMENT,
id: widgetRemove.definition.id,
data: widgetRemove.definition
})
}
})
},
select () {
state.selectedWidgetId = this.widget.id
},
async onMoved () {
await this.$apollo.mutate({
mutation: WIDGET_MOVE,
variables: {
input: {
id: this.widget.id,
x: this.moveState.x,
y: this.moveState.y,
width: this.widget.width,
height: this.widget.height
}
}
})
},
async onResized () {
await this.$apollo.mutate({
mutation: WIDGET_MOVE,
variables: {
input: {
id: this.widget.id,
x: this.resizeState.x,
y: this.resizeState.y,
width: this.resizeState.width,
height: this.resizeState.height
}
}
})
},
updateShellOrigin () {
const el = this.$refs.shell
if (!el) return
const bounds = el.getBoundingClientRect()
this.shellOrigin = {
x: bounds.left + bounds.width / 2 - this.dashboard.left,
y: bounds.top + bounds.height / 2 - this.dashboard.top
}
},
addHeaderAction (action) {
this.removeHeaderAction(action.id)
// Optional props should still be reactive
if (!action.tooltip) action.tooltip = null
if (!action.disabled) action.disabled = false
if (!action.hidden) action.hidden = false
// Transform the function props into getters
transformToGetter(action, 'tooltip')
transformToGetter(action, 'disabled')
transformToGetter(action, 'hidden')
this.headerActions.push(action)
},
removeHeaderAction (id) {
const index = this.headerActions.findIndex(a => a.id === id)
if (index !== -1) this.headerActions.splice(index, 1)
}
}
}
function transformToGetter (obj, field) {
const value = obj[field]
if (typeof value === 'function') {
delete obj[field]
Object.defineProperty(obj, field, {
get: value,
enumerable: true,
configurable: true
})
}
}
</script>
<style lang="stylus" scoped>
$zoom = .7
.shell,
.move-ghost,
.resize-ghost
position absolute
padding ($padding-item / 2)
box-sizing border-box
.wrapper,
.content-wrapper,
.move-ghost .backdrop,
.resize-ghost .backdrop
width 100%
height 100%
.wrapper,
.content-wrapper
display flex
flex-direction column
position relative
.wrapper
transition box-shadow .15s
.header
$small-padding = ($padding-item / 1.5)
padding $small-padding $small-padding ($small-padding / 2) $padding-item
h-box()
.title
flex 1
opacity .5
color $vue-ui-color-dark-neutral
.vue-ui-dark-mode &
color $vue-ui-color-light-neutral
.icon-button
width 20px
height @width
.content
flex 1
overflow hidden
.view
width 100%
height 100%
box-sizing border-box
.not-configured
v-box()
box-center()
.icon
margin-bottom $padding-item
>>> svg
fill $color-text-light
.customize-overlay
position absolute
top 0
left 0
width 100%
height 100%
z-index 1
border-radius $br
v-box()
box-center()
cursor move
user-select none
box-sizing border-box
border transparent 1px solid
/deep/ > *
transition transform .15s
.definition-chip
background $vue-ui-color-primary
color $vue-ui-color-light
border-radius 21px
user-select none
h-box()
box-center()
.title
padding ($padding-item / 2) $padding-item
padding-left 0
.icon
margin-right ($padding-item / 2)
>>> svg
fill @color
.customize-overlay:hover,
.selected .customize-overlay
background rgba($vue-ui-color-primary, .2)
.remove-button
position absolute
top $padding-item
right $padding-item
.customizing
.wrapper
border-radius ($br / $zoom)
.content-wrapper
opacity .15
.customize-overlay
/deep/ > *
transform scale(1/$zoom)
.move-ghost,
.resize-ghost
z-index 10000
.backdrop
background rgba($vue-ui-color-accent, .2)
border-radius ($br / $zoom)
.vue-ui-dark-mode &
background rgba(lighten($vue-ui-color-accent, 60%), .2)
.moving,
.resizing
.shell
z-index 10001
opacity .7
.moving
.shell
.wrapper
box-shadow 0 5px 30px rgba($md-black, .2)
.resizing
.shell
opacity .5
.widget
.shell
transition opacity .15s, transform .15s
&:not(.moving):not(.resizing)
.shell
transition opacity .15s, left .15s, top .15s, width .15s, height .15s, transform .15s
&.selected
.customize-overlay
border $vue-ui-color-primary solid 1px
&.details-shown
> .shell
transform scale(1.2)
&.v-enter,
&.v-leave-to
.shell
transform scale(.9)
opacity 0
&.details
.shell
transform scale(.4)
</style>
@@ -0,0 +1,162 @@
<template>
<div class="widget-add-item list-item">
<div
class="info"
@click="showDetails = true"
>
<ItemLogo
:image="definition.icon"
fallback-icon="widgets"
/>
<ListItemInfo
:name="$t(definition.title)"
:description="$t(definition.description)"
:link="definition.link"
/>
</div>
<div class="actions">
<VueButton
class="primary icon-button"
v-tooltip="$t('org.vue.components.widget-add-item.add')"
icon-left="add"
@click="add()"
/>
</div>
<VueModal
v-if="showDetails"
:title="$t('org.vue.components.widget-add-item.details.title')"
class="medium"
@close="showDetails = false"
>
<div class="custom-body">
<div class="details">
<ItemLogo
:image="definition.icon"
fallback-icon="widgets"
/>
<ListItemInfo
:name="$t(definition.title)"
:description="$t(definition.description)"
/>
</div>
<div v-if="definition.longDescription" class="details">
<div
class="description"
v-html="$t(definition.longDescription)"
/>
</div>
<div class="instances">
{{ $t('org.vue.components.widget-add-item.details.max-instances', {
count: definition.count,
total: definition.maxCount == null ? $t('org.vue.components.widget-add-item.details.unlimited') : definition.maxCount
}) }}
</div>
</div>
<div slot="footer" class="actions">
<VueButton
v-if="definition.link"
:href="definition.link"
:label="$t('org.vue.common.more-info')"
target="_blank"
class="flat"
icon-right="open_in_new"
/>
<VueButton
class="primary"
:label="$t('org.vue.components.widget-add-item.add')"
icon-left="add"
@click="add()"
/>
</div>
</VueModal>
</div>
</template>
<script>
import WIDGET_ADD from '@/graphql/widget/widgetAdd.gql'
import WIDGETS from '@/graphql/widget/widgets.gql'
import WIDGET_DEFINITION_FRAGMENT from '@/graphql/widget/widgetDefinitionFragment.gql'
export default {
props: {
definition: {
type: Object,
required: true
}
},
data () {
return {
showDetails: false
}
},
methods: {
add () {
this.showDetails = false
this.$apollo.mutate({
mutation: WIDGET_ADD,
variables: {
input: {
definitionId: this.definition.id
}
},
update: (store, { data: { widgetAdd } }) => {
let data = store.readQuery({ query: WIDGETS })
// TODO this is a workaround
// See: https://github.com/apollographql/apollo-client/issues/4031#issuecomment-433668473
data = {
widgets: [...data.widgets, widgetAdd]
}
store.writeQuery({ query: WIDGETS, data })
store.writeFragment({
fragment: WIDGET_DEFINITION_FRAGMENT,
id: widgetAdd.definition.id,
data: widgetAdd.definition
})
}
})
}
}
}
</script>
<style lang="stylus" scoped>
.widget-add-item
.actions
margin-right $padding-item
&,
.actions
h-box()
box-center()
.info
flex 1
overflow hidden
padding $padding-item
h-box()
.list-item-info
flex 1
overflow hidden
>>> .description
flex 1
ellipsis()
// Modal
.custom-body
padding 0 24px $padding-item
.details
display flex
margin-bottom $padding-item
</style>
@@ -0,0 +1,103 @@
<template>
<div class="widget-add-pane">
<div class="pane-toolbar">
<VueIcon
icon="library_add"
/>
<div class="title">
{{ $t('org.vue.components.widget-add-pane.title') }}
</div>
<VueButton
class="icon-button flat"
icon-left="close"
v-tooltip="$t('org.vue.common.close')"
@click="close()"
/>
</div>
<div class="toolbar">
<VueInput
v-model="search"
icon-left="search"
class="round search-input"
/>
</div>
<ApolloQuery
:query="require('@/graphql/widget/widgetDefinitions.gql')"
class="widgets"
>
<template slot-scope="{ result: { data, loading } }">
<VueLoadingIndicator
v-if="loading && (!data || !data.widgets)"
class="overlay"
/>
<template v-else-if="data">
<ListFilter
:list="data.widgetDefinitions"
:filter="filterDefinition"
>
<template slot-scope="{ list }">
<WidgetAddItem
v-for="definition of list"
v-if="definition.canAddMore"
:key="definition.id"
:definition="definition"
/>
</template>
</ListFilter>
</template>
</template>
</ApolloQuery>
</div>
</template>
<script>
export default {
data () {
return {
search: ''
}
},
methods: {
close () {
this.$emit('close')
},
filterDefinition (def) {
if (!this.search) return true
const reg = new RegExp(this.search.replace(/\s+/g, '|'), 'i')
return def.title.match(reg) ||
(def.description && def.description.match(reg)) ||
(def.longDescription && def.longDescription.match(reg))
}
}
}
</script>
<style lang="stylus" scoped>
.widget-add-pane
position relative
z-index 1
v-box()
box-shadow 0 0 10px rgba(black, .1)
background $vue-ui-color-light
.vue-ui-dark-mode &
background $vue-ui-color-darker
.toolbar
h-box()
box-center()
margin $padding-item
.search-input
width 100%
.widgets
flex 1
overflow-x hidden
overflow-y auto
</style>
@@ -0,0 +1,48 @@
<template>
<Widget
:widget="widget"
:shell-style="{
left: `${this.dashboard.left + 8}px`,
top: `${this.dashboard.top + 8}px`,
width: `${this.dashboard.width}px`,
height: `${this.dashboard.height}px`,
transformOrigin: `${this.shellOrigin.x}px ${this.shellOrigin.y}px`
}"
class="widget-details-view"
details
@close="close()"
/>
</template>
<script>
export default {
inject: [
'dashboard'
],
props: {
widget: {
type: Object,
required: true
},
shellOrigin: {
type: Object,
required: true
}
},
methods: {
close () {
this.$emit('close')
}
}
}
</script>
<style lang="stylus" scoped>
.widget-details-view
/deep/ .shell
position fixed
z-index 50
</style>
@@ -56,6 +56,8 @@
<div class="vue-ui-spacer"/>
<slot name="more-actions"/>
<VueButton
icon-left="close"
:label="$t('org.vue.views.project-plugins-add.tabs.search.buttons.cancel')"
@@ -2,7 +2,7 @@
<div class="project-dependencies page">
<ContentView
:title="$t('org.vue.views.project-dependencies.title')"
class="limit-width"
class="limit-width list"
>
<template slot="actions">
<VueInput
@@ -34,7 +34,7 @@
</template>
<ApolloQuery
:query="require('../graphql/dependencies.gql')"
:query="require('@/graphql/dependency/dependencies.gql')"
>
<template slot-scope="{ result: { data, loading } }">
<VueLoadingIndicator
@@ -137,10 +137,10 @@
</template>
<script>
import DEPENDENCIES from '../graphql/dependencies.gql'
import DEPENDENCY_INSTALL from '../graphql/dependencyInstall.gql'
import DEPENDENCY_UNINSTALL from '../graphql/dependencyUninstall.gql'
import DEPENDENCIES_UPDATE from '../graphql/dependenciesUpdate.gql'
import DEPENDENCIES from '@/graphql/dependency/dependencies.gql'
import DEPENDENCY_INSTALL from '@/graphql/dependency/dependencyInstall.gql'
import DEPENDENCY_UNINSTALL from '@/graphql/dependency/dependencyUninstall.gql'
import DEPENDENCIES_UPDATE from '@/graphql/dependency/dependenciesUpdate.gql'
export default {
data () {
@@ -170,8 +170,12 @@ export default {
}
},
update: (store, { data: { dependencyInstall } }) => {
const data = store.readQuery({ query: DEPENDENCIES })
data.dependencies.push(dependencyInstall)
let data = store.readQuery({ query: DEPENDENCIES })
// TODO this is a workaround
// See: https://github.com/apollographql/apollo-client/issues/4031#issuecomment-433668473
data = {
dependencies: [...data.dependencies, dependencyInstall]
}
store.writeQuery({ query: DEPENDENCIES, data })
}
})
@@ -195,9 +199,14 @@ export default {
}
},
update: (store, { data: { dependencyUninstall } }) => {
const data = store.readQuery({ query: DEPENDENCIES })
let data = store.readQuery({ query: DEPENDENCIES })
const index = data.dependencies.findIndex(d => d.id === dependencyUninstall.id)
if (index !== -1) {
// TODO this is a workaround
// See: https://github.com/apollographql/apollo-client/issues/4031#issuecomment-433668473
data = {
dependencies: data.dependencies.slice()
}
data.dependencies.splice(index, 1)
store.writeQuery({ query: DEPENDENCIES, data })
}
@@ -75,8 +75,8 @@
</template>
<script>
import DEPENDENCY_DETAILS from '../graphql/dependencyDetails.gql'
import DEPENDENCY_UPDATE from '../graphql/dependencyUpdate.gql'
import DEPENDENCY_DETAILS from '@/graphql/dependency/dependencyDetails.gql'
import DEPENDENCY_UPDATE from '@/graphql/dependency/dependencyUpdate.gql'
export default {
props: {
@@ -170,6 +170,9 @@ export default {
.wanted,
.latest
min-width 130px
.value
font-family monospace
font-size .9em
.installed
@media (max-width: 1130px)
@@ -51,7 +51,7 @@
</template>
<script>
import FILE_OPEN_IN_EDITOR from '../graphql/fileOpenInEditor.gql'
import FILE_OPEN_IN_EDITOR from '@/graphql/file/fileOpenInEditor.gql'
export default {
provide () {
@@ -24,7 +24,7 @@
</template>
<script>
import FILE_OPEN_IN_EDITOR from '../graphql/fileOpenInEditor.gql'
import FILE_OPEN_IN_EDITOR from '@/graphql/file/fileOpenInEditor.gql'
export default {
inject: [
@@ -120,10 +120,10 @@
</template>
<script>
import PageVisibility from '../mixins/PageVisibility'
import PageVisibility from '@/mixins/PageVisibility'
import FILE_DIFFS from '../graphql/fileDiffs.gql'
import GIT_COMMIT from '../graphql/gitCommit.gql'
import FILE_DIFFS from '@/graphql/git/fileDiffs.gql'
import GIT_COMMIT from '@/graphql/git/gitCommit.gql'
const defaultCollapsed = [
'yarn.lock',
@@ -28,12 +28,12 @@
<ApolloQuery
v-else
:query="require('@/graphql/cwd.gql')"
:query="require('@/graphql/cwd/cwd.gql')"
class="current-path"
@dblclick.native="openPathEdit()"
>
<ApolloSubscribeToMore
:document="require('@/graphql/cwdChanged.gql')"
:document="require('@/graphql/cwd/cwdChanged.gql')"
:update-query="cwdChangedUpdate"
/>
@@ -196,15 +196,15 @@
</template>
<script>
import { isValidMultiName } from '../util/folders'
import { isValidMultiName } from '@/util/folders'
import FOLDER_CURRENT from '../graphql/folderCurrent.gql'
import FOLDERS_FAVORITE from '../graphql/foldersFavorite.gql'
import FOLDER_OPEN from '../graphql/folderOpen.gql'
import FOLDER_OPEN_PARENT from '../graphql/folderOpenParent.gql'
import FOLDER_SET_FAVORITE from '../graphql/folderSetFavorite.gql'
import PROJECT_CWD_RESET from '../graphql/projectCwdReset.gql'
import FOLDER_CREATE from '../graphql/folderCreate.gql'
import FOLDER_CURRENT from '@/graphql/folder/folderCurrent.gql'
import FOLDERS_FAVORITE from '@/graphql/folder/foldersFavorite.gql'
import FOLDER_OPEN from '@/graphql/folder/folderOpen.gql'
import FOLDER_OPEN_PARENT from '@/graphql/folder/folderOpenParent.gql'
import FOLDER_SET_FAVORITE from '@/graphql/folder/folderSetFavorite.gql'
import PROJECT_CWD_RESET from '@/graphql/project/projectCwdReset.gql'
import FOLDER_CREATE from '@/graphql/folder/folderCreate.gql'
const SHOW_HIDDEN = 'vue-ui.show-hidden-folders'
@@ -308,7 +308,12 @@ export default {
update: (store, { data: { folderSetFavorite } }) => {
store.writeQuery({ query: FOLDER_CURRENT, data: { folderCurrent: folderSetFavorite } })
const data = store.readQuery({ query: FOLDERS_FAVORITE })
let data = store.readQuery({ query: FOLDERS_FAVORITE })
// TODO this is a workaround
// See: https://github.com/apollographql/apollo-client/issues/4031#issuecomment-433668473
data = {
foldersFavorite: data.foldersFavorite.slice()
}
if (folderSetFavorite.favorite) {
data.foldersFavorite.push(folderSetFavorite)
} else {
@@ -32,12 +32,12 @@
</div>
<ApolloQuery
ref="logs"
:query="require('../graphql/consoleLogs.gql')"
:query="require('@/graphql/console-log/consoleLogs.gql')"
class="logs"
@result="scrollToBottom()"
>
<ApolloSubscribeToMore
:document="require('../graphql/consoleLogAdded.gql')"
:document="require('@/graphql/console-log/consoleLogAdded.gql')"
:update-query="onConsoleLogAdded"
/>
@@ -64,9 +64,9 @@
</template>
<script>
import CONSOLE_LOGS from '../graphql/consoleLogs.gql'
import CONSOLE_LOG_LAST from '../graphql/consoleLogLast.gql'
import CONSOLE_LOGS_CLEAR from '../graphql/consoleLogsClear.gql'
import CONSOLE_LOGS from '@/graphql/console-log/consoleLogs.gql'
import CONSOLE_LOG_LAST from '@/graphql/console-log/consoleLogLast.gql'
import CONSOLE_LOGS_CLEAR from '@/graphql/console-log/consoleLogsClear.gql'
export default {
methods: {

Some files were not shown because too many files have changed in this diff Show More