feat(ui): git FileDiff

This commit is contained in:
Guillaume Chau
2018-03-27 17:46:53 +02:00
parent dde426a60e
commit 2b0ac9ff01
21 changed files with 620 additions and 11 deletions

View File

@@ -60,9 +60,9 @@ export default {
.content
grid-area content
background darken($color-light-background, 3%)
background darken($color-background-light, 3%)
.wrapper
background $color-light-background
background $color-background-light
position relative
overflow-x hidden
overflow-y auto

View File

@@ -0,0 +1,112 @@
<template>
<div
:class="{
new: fileDiff.new,
deleted: fileDiff.deleted
}"
class="file-diff"
>
<div class="toolbar" @click="$emit('update:collapsed', !collapsed)">
<VueIcon class="file-icon" :icon="icon"/>
<template v-if="fileDiff.from !== fileDiff.to && !fileDiff.new">
<div class="name from-file">
<span v-tooltip="fileDiff.from">{{ fileDiff.from }}</span>
</div>
<VueIcon v-if="!fileDiff.deleted" icon="arrow_forward"/>
</template>
<div v-if="!fileDiff.deleted" class="name to-file">
<span v-tooltip="fileDiff.to">{{ fileDiff.to }}</span>
</div>
<div class="vue-ui-spacer"/>
<VueButton
:icon-left="collapsed ? 'keyboard_arrow_down' : 'keyboard_arrow_up'"
class="icon-button"
/>
</div>
<div v-if="!collapsed" class="content">
<FileDiffChunk
v-for="(chunk, index) in fileDiff.chunks"
:key="index"
:chunk="chunk"
/>
</div>
</div>
</template>
<script>
export default {
props: {
fileDiff: {
type: Object,
required: true
},
collapsed: {
type: Boolean,
default: false
}
},
computed: {
icon () {
if (this.fileDiff.new) {
return 'note_add'
} else if (this.fileDiff.deleted) {
return 'delete'
}
return 'insert_drive_file'
}
}
}
</script>
<style lang="stylus" scoped>
@import "~@/style/imports"
status-color($color)
.name
color $color
.file-icon
>>> svg
fill $color
.file-diff
background $vue-ui-color-light
border solid 1px $vue-ui-color-light-neutral
margin $padding-item
.toolbar
padding $padding-item
background $color-background-light
h-box()
align-items center
>>> > *
space-between-x($padding-item)
.file-icon
>>> svg
fill darken($vue-ui-color-light-neutral, 20%)
.name
flex auto 1 0
font-family $font-mono
font-size 12px
ellipsis()
&.from-file
text-decoration line-through
&.to-file
flex 100% 1 1
width 0
.content
overflow-x auto
&.new
status-color($vue-ui-color-success)
&.deleted
status-color($vue-ui-color-danger)
</style>

View File

@@ -0,0 +1,86 @@
<template>
<div
:class="[
`type-${change.type}`
]"
class="file-diff-change"
>
<div class="lines">
<div class="ln ln1">
{{ ln1 }}
</div>
<div class="ln ln2">
{{ ln2 }}
</div>
</div>
<div class="content">{{ change.content }}</div>
</div>
</template>
<script>
export default {
props: {
change: {
type: Object,
required: true
}
},
computed: {
ln1 () {
if (this.change.normal) {
return this.change.ln1
} else if (this.change.type === 'del') {
return this.change.ln
}
},
ln2 () {
if (this.change.normal) {
return this.change.ln2
} else if (this.change.type === 'add') {
return this.change.ln
}
}
}
}
</script>
<style lang="stylus" scoped>
@import "~@/style/imports"
.file-diff-change
font-family $font-mono
font-size 12px
h-box()
.ln,
.content
padding 4px $padding-item
.lines
width 120px
h-box()
background $color-background-light
color rgba($vue-ui-color-dark, .4)
.ln
text-align right
flex 100% 1 1
width 0
overflow hidden
.content
flex auto 1 1
white-space pre
&.type-add
background lighten($vue-ui-color-success, 80%)
.lines
background lighten($vue-ui-color-success, 60%)
&.type-del
background lighten($vue-ui-color-danger, 80%)
.lines
background lighten($vue-ui-color-danger, 60%)
</style>

View File

@@ -0,0 +1,38 @@
<template>
<div class="file-diff-chunk">
<div class="changes">
<FileDiffChange
v-for="(change, index) of chunk.changes"
:key="index"
:change="change"
/>
</div>
</div>
</template>
<script>
export default {
props: {
chunk: {
type: Object,
required: true
}
}
}
</script>
<style lang="stylus" scoped>
@import "~@/style/imports"
.file-diff-chunk
&:not(:last-child)
&::after
content '•••'
height 44px
background lighten($vue-ui-color-light-neutral, 30%)
h-box()
box-center()
color darken($vue-ui-color-light-neutral, 30%)
letter-spacing 4px
</style>

View File

@@ -0,0 +1,224 @@
<template>
<div class="file-diff-view">
<div class="toolbar">
<VueIcon icon="cached"/>
<div class="title">{{ $t('components.file-diff-view.files-changed') }}</div>
<div class="file-count">{{ fileDiffs.length }}</div>
<div class="vue-ui-spacer"/>
<VueInput
v-model="search"
icon-left="search"
:placeholder="$t('components.file-diff-view.search-file')"
/>
<VueButton
:icon-left="allCollapsed ? 'keyboard_arrow_down' : 'keyboard_arrow_up'"
:label="$t(`components.file-diff-view.actions.${allCollapsed ? 'expand-all' : 'collapse-all'}`)"
@click="setCollapsedToAll(!allCollapsed)"
/>
<VueButton
icon-left="refresh"
class="icon-button"
@click="refresh()"
/>
</div>
<div class="list">
<FileDiff
v-for="fileDiff of filteredList"
:key="fileDiff.id"
:file-diff="fileDiff"
:collapsed="!!collapsed[fileDiff.id]"
@update:collapsed="value => $set(collapsed, fileDiff.id, value)"
/>
<div v-if="!filteredList.length" class="vue-ui-empty">
<VueIcon icon="check_circle" class="empty-icon"/>
<span>{{ $t('components.file-diff-view.empty') }}</span>
</div>
</div>
<div class="actions-bar">
<template v-if="fileDiffs.length">
<VueButton
icon-left="vertical_align_bottom"
:label="$t('components.file-diff-view.actions.commit')"
class="big primary"
@click="openCommitModal()"
/>
<VueButton
:label="$t('components.file-diff-view.actions.skip')"
class="big"
@click="skip()"
/>
</template>
<template v-else>
<VueButton
icon-left="done"
:label="$t('components.file-diff-view.actions.continue')"
class="big primary"
@click="skip()"
/>
<VueButton
:label="$t('components.file-diff-view.actions.refresh')"
class="big"
@click="refresh()"
/>
</template>
</div>
<VueLoadingIndicator
v-if="loading"
class="overlay"
/>
<VueModal
v-if="showCommitModal"
:title="$t('components.file-diff-view.modals.commit.title')"
class="medium"
@close="showCommitModal = false"
>
<div class="default-body">
<VueFormField
:title="$t('components.file-diff-view.modals.commit.input')"
:subtitle="$t('components.file-diff-view.modals.commit.subtitle')"
>
<VueInput
ref="commitMessageInput"
v-model="commitMessage"
icon-left="local_offer"
@keyup.enter="commitMessage && commit()"
/>
</VueFormField>
</div>
<div slot="footer" class="actions space-between">
<VueButton
:label="$t('components.file-diff-view.modals.commit.actions.cancel')"
class="flat"
@click="showCommitModal = false"
/>
<VueButton
:label="$t('components.file-diff-view.modals.commit.actions.commit')"
class="primary"
icon-left="vertical_align_bottom"
:disabled="!commitMessage"
@click="commit()"
/>
</div>
</VueModal>
</div>
</template>
<script>
import FILE_DIFFS from '../graphql/fileDiffs.gql'
import GIT_COMMIT from '../graphql/gitCommit.gql'
export default {
data () {
return {
fileDiffs: [],
collapsed: {},
search: '',
loading: 0,
commitMessage: '',
showCommitModal: false
}
},
apollo: {
fileDiffs: {
query: FILE_DIFFS,
loadingKey: 'loading',
fetchPolicy: 'cahe-and-network'
}
},
computed: {
allCollapsed () {
return !this.fileDiffs.find(
fileDiff => !this.collapsed[fileDiff.id]
)
},
filteredList () {
const search = this.search.trim()
if (search) {
const reg = new RegExp(search.replace(/\s+/g, '.*'), 'i')
return this.fileDiffs.filter(
fileDiff => reg.test(fileDiff.from) || reg.test(fileDiff.to)
)
} else {
return this.fileDiffs
}
}
},
methods: {
setCollapsedToAll (value) {
const map = {}
this.fileDiffs.forEach(fileDiff => {
map[fileDiff.id] = value
})
this.collapsed = map
},
refresh () {
this.$apollo.queries.fileDiffs.refetch()
},
openCommitModal () {
this.showCommitModal = true
requestAnimationFrame(() => {
this.$refs.commitMessageInput.focus()
})
},
async commit () {
this.loading++
try {
await this.$apollo.mutate({
mutation: GIT_COMMIT,
variables: {
message: this.commitMessage
}
})
this.$emit('continue')
} catch (e) {
console.error(e)
}
this.loading--
},
skip () {
this.$emit('continue')
}
}
}
</script>
<style lang="stylus" scoped>
@import "~@/style/imports"
.file-diff-view
v-box()
height 100%
position relative
.toolbar
padding $padding-item
background $color-background-light
h-box()
align-items center
>>> > *
space-between-x($padding-item)
.file-count
padding 3px 6px
background darken(@background, 3%)
border-radius $br
.list
flex 100% 1 1
height 0
overflow-x hidden
overflow-y auto
</style>

View File

@@ -225,7 +225,7 @@ export default {
.toolbar
padding $padding-item
background $color-light-background
background $color-background-light
h-box()
align-items center

View File

@@ -50,7 +50,7 @@ export default {
.logger-message
h-box()
align-items baseline
font-family 'Roboto Mono', monospace
font-family $font-mono
box-sizing border-box
padding 2px 4px

View File

@@ -51,5 +51,5 @@ export default {
.nav-list
overflow-x none
overflow-y auto
background darken($color-light-background, 2%)
background darken($color-background-light, 2%)
</style>

View File

@@ -121,7 +121,7 @@ export default {
margin 0 auto
$max-width = 1200px
.shell
background $color-light-background
background $color-background-light
.header .content,
>>> .vue-ui-tab
max-width $max-width

View File

@@ -0,0 +1,31 @@
const execa = require('execa')
const parseDiff = require('parse-diff')
// Connectors
const cwd = require('./cwd')
async function getDiffs (context) {
const { stdout } = await execa('git', ['diff', 'HEAD'], {
cwd: cwd.get()
})
return parseDiff(stdout).map(
fileDiff => ({
id: fileDiff.index.join(' '),
...fileDiff
})
)
}
async function commit (message, context) {
await execa('git', ['add', '*'], {
cwd: cwd.get()
})
await execa('git', ['commit', '-m', message.replace(/"/, '\\"')], {
cwd: cwd.get()
})
return true
}
module.exports = {
getDiffs,
commit
}

View File

@@ -12,6 +12,7 @@ const logs = require('./connectors/logs')
const plugins = require('./connectors/plugins')
const tasks = require('./connectors/tasks')
const configurations = require('./connectors/configurations')
const git = require('./connectors/git')
// Prevent code from exiting server process
exit.exitProcess = false
@@ -58,7 +59,8 @@ module.exports = {
tasks: (root, args, context) => tasks.list(context),
task: (root, { id }, context) => tasks.findOne(id, context),
configurations: (root, args, context) => configurations.list(context),
configuration: (root, { id }, context) => configurations.findOne(id, context)
configuration: (root, { id }, context) => configurations.findOne(id, context),
fileDiffs: (root, args, context) => git.getDiffs(context)
},
Mutation: {
@@ -86,7 +88,8 @@ module.exports = {
taskStop: (root, { id }, context) => tasks.stop(id, context),
taskLogsClear: (root, { id }, context) => tasks.clearLogs(id, context),
configurationSave: (root, { id }, context) => configurations.save(id, context),
configurationCancel: (root, { id }, context) => configurations.cancel(id, context)
configurationCancel: (root, { id }, context) => configurations.cancel(id, context),
gitCommit: (root, { message }, context) => git.commit(message, context)
},
Subscription: {

View File

@@ -201,6 +201,38 @@ type Configuration implements DescribedEntity {
prompts: [Prompt]
}
type FileDiff {
id: ID!
from: String
to: String
new: Boolean
deleted: Boolean
chunks: [FileDiffChunk]
}
type FileDiffChunk {
changes: [FileDiffChange]
oldStart: Int
oldLines: Int
newStart: Int
newLines: Int
}
type FileDiffChange {
type: FileDiffChangeType
ln: Int
ln1: Int
ln2: Int
content: String
normal: Boolean
}
enum FileDiffChangeType {
normal
add
del
}
type Query {
progress (id: ID!): Progress
cwd: String!
@@ -217,6 +249,7 @@ type Query {
task (id: ID!): Task
configurations: [Configuration]
configuration (id: ID!): Configuration
fileDiffs: [FileDiff]
}
type Mutation {
@@ -242,6 +275,7 @@ type Mutation {
taskLogsClear (id: ID!): Task
configurationSave (id: ID!): Configuration
configurationCancel (id: ID!): Configuration
gitCommit (message: String!): Boolean
}
type Subscription {

View File

@@ -0,0 +1,23 @@
query fileDiffs {
fileDiffs {
id
from
to
new
deleted
chunks {
oldStart
oldLines
newStart
newLines
changes {
type
ln
ln1
ln2
content
normal
}
}
}
}

View File

@@ -0,0 +1,3 @@
mutation gitCommit ($message: String!) {
gitCommit (message: $message)
}

View File

@@ -1,5 +1,29 @@
{
"components": {
"file-diff-view": {
"files-changed": "Files changed",
"search-file": "Search file",
"empty": "No change found",
"modals": {
"commit": {
"title": "Commit changes",
"input": "Enter a commit message",
"subtitle": "Record changes to the repository",
"actions": {
"commit": "Commit",
"cancel": "Cancel"
}
}
},
"actions": {
"collapse-all": "Collapse all",
"expand-all": "Expand all",
"commit": "Commit changes",
"skip": "Skip",
"continue": "Continue",
"refresh": "Refresh"
}
},
"folder-explorer": {
"toolbar": {
"tooltips": {

View File

@@ -1,5 +1,29 @@
{
"components": {
"file-diff-view": {
"files-changed": "Fichiers modifiés",
"search-file": "Rechercher un fichier",
"empty": "Aucune modification trouvée",
"modals": {
"commit": {
"title": "Valider les modifications",
"input": "Entrer un message pour le commit",
"subtitle": "Enregistre les changements effectués au dépôt",
"actions": {
"commit": "Commit",
"cancel": "Annuler"
}
}
},
"actions": {
"collapse-all": "Tout replier",
"expand-all": "Tout étendre",
"commit": "Valider les modifications",
"skip": "Passer",
"continue": "Continuer",
"refresh": "Rafraîchir"
}
},
"folder-explorer": {
"toolbar": {
"tooltips": {

View File

@@ -12,6 +12,7 @@ import ProjectTaskDetails from './views/ProjectTaskDetails.vue'
import ProjectSelect from './views/ProjectSelect.vue'
import ProjectCreate from './views/ProjectCreate.vue'
import About from './views/About.vue'
import FileDiffView from './components/FileDiffView.vue'
import PROJECT_CURRENT from './graphql/projectCurrent.gql'
@@ -80,6 +81,11 @@ const router = new Router({
name: 'project-create',
component: ProjectCreate
},
{
path: '/file-diff',
name: 'file-diff',
component: FileDiffView
},
{
path: '/about',
name: 'about',

View File

@@ -1,2 +1,2 @@
$color-light-background = lighten($vue-ui-color-light-neutral, 80%)
$color-background-light = lighten($vue-ui-color-light-neutral, 80%)
$color-text-light = lighten($vue-ui-color-dark, 40%)

View File

@@ -17,7 +17,7 @@ h2
.actions-bar
padding $padding-item
background $color-light-background
background $color-background-light
h-box()
box-center()
position relative

View File

@@ -1 +1,2 @@
$padding-item = 12px
$font-mono = 'Roboto Mono', monospace

View File

@@ -215,7 +215,7 @@ export default {
height 100%
.command
font-family 'Roboto Mono', monospace
font-family $font-mono
font-size 12px
background $vue-ui-color-light-neutral
color $vue-ui-color-dark