mirror of
https://github.com/vuejs/vue-cli.git
synced 2026-04-22 12:28:44 -05:00
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:
@@ -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!
|
||||
}
|
||||
`]
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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>
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import LOADING from '../graphql/loading.gql'
|
||||
import LOADING from '@/graphql/loading/loading.gql'
|
||||
|
||||
export default {
|
||||
apollo: {
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<ApolloQuery
|
||||
:query="require('../graphql/connected.gql')"
|
||||
:query="require('@/graphql/connected/connected.gql')"
|
||||
fetch-policy="cache-only"
|
||||
class="connection-status"
|
||||
>
|
||||
+3
-3
@@ -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>
|
||||
+1
-1
@@ -52,7 +52,7 @@
|
||||
|
||||
<script>
|
||||
import { DisableScroll } from '@vue/ui'
|
||||
import Progress from '../mixins/Progress'
|
||||
import Progress from '@/mixins/Progress'
|
||||
|
||||
export default {
|
||||
mixins: [
|
||||
+14
-6
@@ -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>
|
||||
+29
-19
@@ -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>
|
||||
+24
-1
@@ -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>
|
||||
+2
-2
@@ -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: {
|
||||
+2
-2
@@ -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: [
|
||||
+6
-3
@@ -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
|
||||
+15
-12
@@ -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: [
|
||||
+16
-6
@@ -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>
|
||||
+9
-7
@@ -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
|
||||
+4
@@ -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">
|
||||
+7
-3
@@ -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>
|
||||
+2
-2
@@ -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
|
||||
+6
-9
@@ -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>
|
||||
+2
@@ -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')"
|
||||
+18
-9
@@ -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 })
|
||||
}
|
||||
+5
-2
@@ -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)
|
||||
+1
-1
@@ -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 () {
|
||||
+1
-1
@@ -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: [
|
||||
+3
-3
@@ -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',
|
||||
+16
-11
@@ -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 {
|
||||
+5
-5
@@ -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
Reference in New Issue
Block a user