Merge branch 'decouple-file-row-from-ide' into 'master'
Decouple file row from IDE See merge request gitlab-org/gitlab-ce!21742
This commit is contained in:
commit
07c6c9db57
8 changed files with 602 additions and 471 deletions
104
app/assets/javascripts/ide/components/file_row_extra.vue
Normal file
104
app/assets/javascripts/ide/components/file_row_extra.vue
Normal file
|
@ -0,0 +1,104 @@
|
|||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { n__, __, sprintf } from '~/locale';
|
||||
import tooltip from '~/vue_shared/directives/tooltip';
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import NewDropdown from './new_dropdown/index.vue';
|
||||
import ChangedFileIcon from './changed_file_icon.vue';
|
||||
import MrFileIcon from './mr_file_icon.vue';
|
||||
|
||||
export default {
|
||||
name: 'FileRowExtra',
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
components: {
|
||||
Icon,
|
||||
NewDropdown,
|
||||
ChangedFileIcon,
|
||||
MrFileIcon,
|
||||
},
|
||||
props: {
|
||||
file: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
mouseOver: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters([
|
||||
'getChangesInFolder',
|
||||
'getUnstagedFilesCountForPath',
|
||||
'getStagedFilesCountForPath',
|
||||
]),
|
||||
folderUnstagedCount() {
|
||||
return this.getUnstagedFilesCountForPath(this.file.path);
|
||||
},
|
||||
folderStagedCount() {
|
||||
return this.getStagedFilesCountForPath(this.file.path);
|
||||
},
|
||||
changesCount() {
|
||||
return this.getChangesInFolder(this.file.path);
|
||||
},
|
||||
folderChangesTooltip() {
|
||||
if (this.changesCount === 0) return undefined;
|
||||
|
||||
if (this.folderUnstagedCount > 0 && this.folderStagedCount === 0) {
|
||||
return n__('%d unstaged change', '%d unstaged changes', this.folderUnstagedCount);
|
||||
} else if (this.folderUnstagedCount === 0 && this.folderStagedCount > 0) {
|
||||
return n__('%d staged change', '%d staged changes', this.folderStagedCount);
|
||||
}
|
||||
|
||||
return sprintf(__('%{unstaged} unstaged and %{staged} staged changes'), {
|
||||
unstaged: this.folderUnstagedCount,
|
||||
staged: this.folderStagedCount,
|
||||
});
|
||||
},
|
||||
showTreeChangesCount() {
|
||||
return this.file.type === 'tree' && this.changesCount > 0 && !this.file.opened;
|
||||
},
|
||||
showChangedFileIcon() {
|
||||
return this.file.changed || this.file.tempFile || this.file.staged;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="float-right ide-file-icon-holder">
|
||||
<mr-file-icon
|
||||
v-if="file.mrChange"
|
||||
/>
|
||||
<span
|
||||
v-if="showTreeChangesCount"
|
||||
class="ide-tree-changes"
|
||||
>
|
||||
{{ changesCount }}
|
||||
<icon
|
||||
v-tooltip
|
||||
:title="folderChangesTooltip"
|
||||
:size="12"
|
||||
data-container="body"
|
||||
data-placement="right"
|
||||
name="file-modified"
|
||||
css-classes="prepend-left-5 ide-file-modified"
|
||||
/>
|
||||
</span>
|
||||
<changed-file-icon
|
||||
v-else-if="showChangedFileIcon"
|
||||
:file="file"
|
||||
:show-tooltip="true"
|
||||
:show-staged-icon="true"
|
||||
:force-modified-icon="true"
|
||||
/>
|
||||
<new-dropdown
|
||||
:type="file.type"
|
||||
:path="file.path"
|
||||
:mouse-over="mouseOver"
|
||||
class="prepend-left-8"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
|
@ -2,15 +2,16 @@
|
|||
import { mapActions, mapGetters, mapState } from 'vuex';
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
|
||||
import RepoFile from './repo_file.vue';
|
||||
import FileRow from '~/vue_shared/components/file_row.vue';
|
||||
import NavDropdown from './nav_dropdown.vue';
|
||||
import FileRowExtra from './file_row_extra.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Icon,
|
||||
RepoFile,
|
||||
SkeletonLoadingContainer,
|
||||
NavDropdown,
|
||||
FileRow,
|
||||
},
|
||||
props: {
|
||||
viewerType: {
|
||||
|
@ -34,8 +35,9 @@ export default {
|
|||
this.updateViewer(this.viewerType);
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['updateViewer']),
|
||||
...mapActions(['updateViewer', 'toggleTreeOpen']),
|
||||
},
|
||||
FileRowExtra,
|
||||
};
|
||||
</script>
|
||||
|
||||
|
@ -63,11 +65,13 @@ export default {
|
|||
<div
|
||||
class="ide-tree-body h-100"
|
||||
>
|
||||
<repo-file
|
||||
<file-row
|
||||
v-for="file in currentTree.tree"
|
||||
:key="file.key"
|
||||
:file="file"
|
||||
:level="0"
|
||||
:extra-component="$options.FileRowExtra"
|
||||
@toggleTreeOpen="toggleTreeOpen"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -1,227 +0,0 @@
|
|||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
import { n__, __, sprintf } from '~/locale';
|
||||
import tooltip from '~/vue_shared/directives/tooltip';
|
||||
import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import FileIcon from '~/vue_shared/components/file_icon.vue';
|
||||
import router from '../ide_router';
|
||||
import NewDropdown from './new_dropdown/index.vue';
|
||||
import FileStatusIcon from './repo_file_status_icon.vue';
|
||||
import ChangedFileIcon from './changed_file_icon.vue';
|
||||
import MrFileIcon from './mr_file_icon.vue';
|
||||
|
||||
export default {
|
||||
name: 'RepoFile',
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
components: {
|
||||
SkeletonLoadingContainer,
|
||||
NewDropdown,
|
||||
FileStatusIcon,
|
||||
FileIcon,
|
||||
ChangedFileIcon,
|
||||
MrFileIcon,
|
||||
Icon,
|
||||
},
|
||||
props: {
|
||||
file: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
level: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
mouseOver: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters([
|
||||
'getChangesInFolder',
|
||||
'getUnstagedFilesCountForPath',
|
||||
'getStagedFilesCountForPath',
|
||||
]),
|
||||
folderUnstagedCount() {
|
||||
return this.getUnstagedFilesCountForPath(this.file.path);
|
||||
},
|
||||
folderStagedCount() {
|
||||
return this.getStagedFilesCountForPath(this.file.path);
|
||||
},
|
||||
changesCount() {
|
||||
return this.getChangesInFolder(this.file.path);
|
||||
},
|
||||
folderChangesTooltip() {
|
||||
if (this.changesCount === 0) return undefined;
|
||||
|
||||
if (this.folderUnstagedCount > 0 && this.folderStagedCount === 0) {
|
||||
return n__('%d unstaged change', '%d unstaged changes', this.folderUnstagedCount);
|
||||
} else if (this.folderUnstagedCount === 0 && this.folderStagedCount > 0) {
|
||||
return n__('%d staged change', '%d staged changes', this.folderStagedCount);
|
||||
}
|
||||
|
||||
return sprintf(__('%{unstaged} unstaged and %{staged} staged changes'), {
|
||||
unstaged: this.folderUnstagedCount,
|
||||
staged: this.folderStagedCount,
|
||||
});
|
||||
},
|
||||
isTree() {
|
||||
return this.file.type === 'tree';
|
||||
},
|
||||
isBlob() {
|
||||
return this.file.type === 'blob';
|
||||
},
|
||||
levelIndentation() {
|
||||
return {
|
||||
marginLeft: `${this.level * 16}px`,
|
||||
};
|
||||
},
|
||||
fileClass() {
|
||||
return {
|
||||
'file-open': this.isBlob && this.file.opened,
|
||||
'file-active': this.isBlob && this.file.active,
|
||||
folder: this.isTree,
|
||||
'is-open': this.file.opened,
|
||||
};
|
||||
},
|
||||
showTreeChangesCount() {
|
||||
return this.isTree && this.changesCount > 0 && !this.file.opened;
|
||||
},
|
||||
showChangedFileIcon() {
|
||||
return this.file.changed || this.file.tempFile || this.file.staged;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
'file.active': function fileActiveWatch(active) {
|
||||
if (this.file.type === 'blob' && active) {
|
||||
this.scrollIntoView();
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (this.hasPathAtCurrentRoute()) {
|
||||
this.scrollIntoView(true);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['toggleTreeOpen']),
|
||||
clickFile() {
|
||||
// Manual Action if a tree is selected/opened
|
||||
if (this.isTree && this.hasUrlAtCurrentRoute()) {
|
||||
this.toggleTreeOpen(this.file.path);
|
||||
}
|
||||
|
||||
router.push(`/project${this.file.url}`);
|
||||
},
|
||||
scrollIntoView(isInit = false) {
|
||||
const block = isInit && this.isTree ? 'center' : 'nearest';
|
||||
|
||||
this.$el.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block,
|
||||
});
|
||||
},
|
||||
hasPathAtCurrentRoute() {
|
||||
if (!this.$router || !this.$router.currentRoute) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// - strip route up to "/-/" and ending "/"
|
||||
const routePath = this.$router.currentRoute.path
|
||||
.replace(/^.*?[/]-[/]/g, '')
|
||||
.replace(/[/]$/g, '');
|
||||
|
||||
// - strip ending "/"
|
||||
const filePath = this.file.path.replace(/[/]$/g, '');
|
||||
|
||||
return filePath === routePath;
|
||||
},
|
||||
hasUrlAtCurrentRoute() {
|
||||
return this.$router.currentRoute.path === `/project${this.file.url}`;
|
||||
},
|
||||
toggleHover(over) {
|
||||
this.mouseOver = over;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
:class="fileClass"
|
||||
class="file"
|
||||
role="button"
|
||||
@click="clickFile"
|
||||
@mouseover="toggleHover(true)"
|
||||
@mouseout="toggleHover(false)"
|
||||
>
|
||||
<div
|
||||
class="file-name"
|
||||
>
|
||||
<span
|
||||
:style="levelIndentation"
|
||||
class="ide-file-name str-truncated"
|
||||
>
|
||||
<file-icon
|
||||
:file-name="file.name"
|
||||
:loading="file.loading"
|
||||
:folder="isTree"
|
||||
:opened="file.opened"
|
||||
:size="16"
|
||||
/>
|
||||
{{ file.name }}
|
||||
<file-status-icon
|
||||
:file="file"
|
||||
/>
|
||||
</span>
|
||||
<span class="float-right ide-file-icon-holder">
|
||||
<mr-file-icon
|
||||
v-if="file.mrChange"
|
||||
/>
|
||||
<span
|
||||
v-if="showTreeChangesCount"
|
||||
class="ide-tree-changes"
|
||||
>
|
||||
{{ changesCount }}
|
||||
<icon
|
||||
v-tooltip
|
||||
:title="folderChangesTooltip"
|
||||
:size="12"
|
||||
data-container="body"
|
||||
data-placement="right"
|
||||
name="file-modified"
|
||||
css-classes="prepend-left-5 ide-file-modified"
|
||||
/>
|
||||
</span>
|
||||
<changed-file-icon
|
||||
v-else-if="showChangedFileIcon"
|
||||
:file="file"
|
||||
:show-tooltip="true"
|
||||
:show-staged-icon="true"
|
||||
:force-modified-icon="true"
|
||||
class="float-right"
|
||||
/>
|
||||
</span>
|
||||
<new-dropdown
|
||||
:type="file.type"
|
||||
:path="file.path"
|
||||
:mouse-over="mouseOver"
|
||||
class="float-right prepend-left-8"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="file.opened">
|
||||
<repo-file
|
||||
v-for="childFile in file.tree"
|
||||
:key="childFile.key"
|
||||
:file="childFile"
|
||||
:level="level + 1"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
210
app/assets/javascripts/vue_shared/components/file_row.vue
Normal file
210
app/assets/javascripts/vue_shared/components/file_row.vue
Normal file
|
@ -0,0 +1,210 @@
|
|||
<script>
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import FileIcon from '~/vue_shared/components/file_icon.vue';
|
||||
|
||||
export default {
|
||||
name: 'FileRow',
|
||||
components: {
|
||||
FileIcon,
|
||||
Icon,
|
||||
},
|
||||
props: {
|
||||
file: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
level: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
extraComponent: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
mouseOver: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isTree() {
|
||||
return this.file.type === 'tree';
|
||||
},
|
||||
isBlob() {
|
||||
return this.file.type === 'blob';
|
||||
},
|
||||
levelIndentation() {
|
||||
return {
|
||||
marginLeft: `${this.level * 16}px`,
|
||||
};
|
||||
},
|
||||
fileClass() {
|
||||
return {
|
||||
'file-open': this.isBlob && this.file.opened,
|
||||
'is-active': this.isBlob && this.file.active,
|
||||
folder: this.isTree,
|
||||
'is-open': this.file.opened,
|
||||
};
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
'file.active': function fileActiveWatch(active) {
|
||||
if (this.file.type === 'blob' && active) {
|
||||
this.scrollIntoView();
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (this.hasPathAtCurrentRoute()) {
|
||||
this.scrollIntoView(true);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleTreeOpen(path) {
|
||||
this.$emit('toggleTreeOpen', path);
|
||||
},
|
||||
clickFile() {
|
||||
// Manual Action if a tree is selected/opened
|
||||
if (this.isTree && this.hasUrlAtCurrentRoute()) {
|
||||
this.toggleTreeOpen(this.file.path);
|
||||
}
|
||||
|
||||
if (this.$router) this.$router.push(`/project${this.file.url}`);
|
||||
},
|
||||
scrollIntoView(isInit = false) {
|
||||
const block = isInit && this.isTree ? 'center' : 'nearest';
|
||||
|
||||
this.$el.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block,
|
||||
});
|
||||
},
|
||||
hasPathAtCurrentRoute() {
|
||||
if (!this.$router || !this.$router.currentRoute) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// - strip route up to "/-/" and ending "/"
|
||||
const routePath = this.$router.currentRoute.path
|
||||
.replace(/^.*?[/]-[/]/g, '')
|
||||
.replace(/[/]$/g, '');
|
||||
|
||||
// - strip ending "/"
|
||||
const filePath = this.file.path.replace(/[/]$/g, '');
|
||||
|
||||
return filePath === routePath;
|
||||
},
|
||||
hasUrlAtCurrentRoute() {
|
||||
if (!this.$router || !this.$router.currentRoute) return true;
|
||||
|
||||
return this.$router.currentRoute.path === `/project${this.file.url}`;
|
||||
},
|
||||
toggleHover(over) {
|
||||
this.mouseOver = over;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
:class="fileClass"
|
||||
class="file-row"
|
||||
role="button"
|
||||
@click="clickFile"
|
||||
@mouseover="toggleHover(true)"
|
||||
@mouseout="toggleHover(false)"
|
||||
>
|
||||
<div
|
||||
class="file-row-name-container"
|
||||
>
|
||||
<span
|
||||
:style="levelIndentation"
|
||||
class="file-row-name str-truncated"
|
||||
>
|
||||
<file-icon
|
||||
:file-name="file.name"
|
||||
:loading="file.loading"
|
||||
:folder="isTree"
|
||||
:opened="file.opened"
|
||||
:size="16"
|
||||
/>
|
||||
{{ file.name }}
|
||||
</span>
|
||||
<component
|
||||
v-if="extraComponent"
|
||||
:is="extraComponent"
|
||||
:file="file"
|
||||
:mouse-over="mouseOver"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="file.opened">
|
||||
<file-row
|
||||
v-for="childFile in file.tree"
|
||||
:key="childFile.key"
|
||||
:file="childFile"
|
||||
:level="level + 1"
|
||||
:extra-component="extraComponent"
|
||||
@toggleTreeOpen="toggleTreeOpen"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.file-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 32px;
|
||||
padding: 4px 8px;
|
||||
margin-left: -8px;
|
||||
margin-right: -8px;
|
||||
border-radius: 3px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.file-row:hover,
|
||||
.file-row:focus {
|
||||
background: #f2f2f2;
|
||||
}
|
||||
|
||||
.file-row:active {
|
||||
background: #dfdfdf;
|
||||
}
|
||||
|
||||
.file-row.is-active {
|
||||
background: #f2f2f2;
|
||||
}
|
||||
|
||||
.file-row-name-container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.file-row-name {
|
||||
display: inline-block;
|
||||
flex: 1;
|
||||
max-width: inherit;
|
||||
height: 18px;
|
||||
line-height: 16px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.file-row-name svg {
|
||||
margin-right: 2px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.file-row-name .loading-container {
|
||||
display: inline-block;
|
||||
margin-right: 4px;
|
||||
}
|
||||
</style>
|
|
@ -53,83 +53,9 @@ $ide-commit-header-height: 48px;
|
|||
flex: 1;
|
||||
min-height: 0; // firefox fix
|
||||
|
||||
.file {
|
||||
height: 32px;
|
||||
cursor: pointer;
|
||||
|
||||
&.file-active {
|
||||
background: $theme-gray-100;
|
||||
}
|
||||
|
||||
.ide-file-name {
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
max-width: inherit;
|
||||
line-height: 16px;
|
||||
display: inline-block;
|
||||
height: 18px;
|
||||
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
margin-right: 4px;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.ide-file-icon-holder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: $theme-gray-700;
|
||||
}
|
||||
|
||||
.ide-file-changed-icon {
|
||||
margin-left: auto;
|
||||
|
||||
> svg {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.ide-new-btn {
|
||||
display: none;
|
||||
|
||||
.btn {
|
||||
padding: 2px 5px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
.ide-new-btn {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.folder-icon {
|
||||
fill: $gl-text-color-secondary;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: $gl-text-color;
|
||||
}
|
||||
|
||||
th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.file-name {
|
||||
display: flex;
|
||||
overflow: visible;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.multi-file-loading-container {
|
||||
|
@ -625,8 +551,7 @@ $ide-commit-header-height: 48px;
|
|||
}
|
||||
}
|
||||
|
||||
.multi-file-commit-list-path,
|
||||
.ide-file-list .file {
|
||||
.multi-file-commit-list-path {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: -$grid-size;
|
||||
|
@ -634,28 +559,14 @@ $ide-commit-header-height: 48px;
|
|||
padding: $grid-size / 2 $grid-size;
|
||||
border-radius: $border-radius-default;
|
||||
text-align: left;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background: $theme-gray-100;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: $theme-gray-200;
|
||||
}
|
||||
}
|
||||
|
||||
.multi-file-commit-list-path {
|
||||
cursor: pointer;
|
||||
height: $ide-commit-row-height;
|
||||
padding-right: 0;
|
||||
|
||||
&.is-active {
|
||||
background-color: $white-normal;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background: $theme-gray-100;
|
||||
|
||||
outline: 0;
|
||||
|
||||
.multi-file-discard-btn {
|
||||
|
@ -665,6 +576,14 @@ $ide-commit-header-height: 48px;
|
|||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: $theme-gray-200;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background-color: $white-normal;
|
||||
}
|
||||
|
||||
svg {
|
||||
min-width: 16px;
|
||||
vertical-align: middle;
|
||||
|
@ -1398,9 +1317,17 @@ $ide-commit-header-height: 48px;
|
|||
}
|
||||
}
|
||||
|
||||
.ide-new-btn .dropdown.show .ide-entry-dropdown-toggle {
|
||||
color: $white-normal;
|
||||
background-color: $blue-500;
|
||||
.ide-new-btn {
|
||||
display: none;
|
||||
|
||||
.btn {
|
||||
padding: 2px 5px;
|
||||
}
|
||||
|
||||
.dropdown.show .ide-entry-dropdown-toggle {
|
||||
color: $white-normal;
|
||||
background-color: $blue-500;
|
||||
}
|
||||
}
|
||||
|
||||
.ide-preview-header {
|
||||
|
@ -1465,3 +1392,28 @@ $ide-commit-header-height: 48px;
|
|||
width: $ide-commit-row-height;
|
||||
height: $ide-commit-row-height;
|
||||
}
|
||||
|
||||
.ide-file-icon-holder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: $theme-gray-700;
|
||||
}
|
||||
|
||||
.ide-file-changed-icon {
|
||||
margin-left: auto;
|
||||
|
||||
> svg {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.file-row:hover,
|
||||
.file-row:focus {
|
||||
.ide-new-btn {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.folder-icon {
|
||||
fill: $gl-text-color-secondary;
|
||||
}
|
||||
}
|
||||
|
|
159
spec/javascripts/ide/components/file_row_extra_spec.js
Normal file
159
spec/javascripts/ide/components/file_row_extra_spec.js
Normal file
|
@ -0,0 +1,159 @@
|
|||
import Vue from 'vue';
|
||||
import { createStore } from '~/ide/stores';
|
||||
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
|
||||
import FileRowExtra from '~/ide/components/file_row_extra.vue';
|
||||
import { file, resetStore } from '../helpers';
|
||||
|
||||
describe('IDE extra file row component', () => {
|
||||
let Component;
|
||||
let vm;
|
||||
let unstagedFilesCount = 0;
|
||||
let stagedFilesCount = 0;
|
||||
let changesCount = 0;
|
||||
|
||||
beforeAll(() => {
|
||||
Component = Vue.extend(FileRowExtra);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vm = createComponentWithStore(Component, createStore(), {
|
||||
file: {
|
||||
...file('test'),
|
||||
},
|
||||
mouseOver: false,
|
||||
});
|
||||
|
||||
spyOnProperty(vm, 'getUnstagedFilesCountForPath').and.returnValue(() => unstagedFilesCount);
|
||||
spyOnProperty(vm, 'getStagedFilesCountForPath').and.returnValue(() => stagedFilesCount);
|
||||
spyOnProperty(vm, 'getChangesInFolder').and.returnValue(() => changesCount);
|
||||
|
||||
vm.$mount();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vm.$destroy();
|
||||
resetStore(vm.$store);
|
||||
|
||||
stagedFilesCount = 0;
|
||||
unstagedFilesCount = 0;
|
||||
changesCount = 0;
|
||||
});
|
||||
|
||||
describe('folderChangesTooltip', () => {
|
||||
it('returns undefined when changes count is 0', () => {
|
||||
expect(vm.folderChangesTooltip).toBe(undefined);
|
||||
});
|
||||
|
||||
it('returns unstaged changes text', () => {
|
||||
changesCount = 1;
|
||||
unstagedFilesCount = 1;
|
||||
|
||||
expect(vm.folderChangesTooltip).toBe('1 unstaged change');
|
||||
});
|
||||
|
||||
it('returns staged changes text', () => {
|
||||
changesCount = 1;
|
||||
stagedFilesCount = 1;
|
||||
|
||||
expect(vm.folderChangesTooltip).toBe('1 staged change');
|
||||
});
|
||||
|
||||
it('returns staged and unstaged changes text', () => {
|
||||
changesCount = 1;
|
||||
stagedFilesCount = 1;
|
||||
unstagedFilesCount = 1;
|
||||
|
||||
expect(vm.folderChangesTooltip).toBe('1 unstaged and 1 staged changes');
|
||||
});
|
||||
});
|
||||
|
||||
describe('show tree changes count', () => {
|
||||
it('does not show for blobs', () => {
|
||||
vm.file.type = 'blob';
|
||||
|
||||
expect(vm.$el.querySelector('.ide-tree-changes')).toBe(null);
|
||||
});
|
||||
|
||||
it('does not show when changes count is 0', () => {
|
||||
vm.file.type = 'tree';
|
||||
|
||||
expect(vm.$el.querySelector('.ide-tree-changes')).toBe(null);
|
||||
});
|
||||
|
||||
it('does not show when tree is open', done => {
|
||||
vm.file.type = 'tree';
|
||||
vm.file.opened = true;
|
||||
changesCount = 1;
|
||||
|
||||
vm.$nextTick(() => {
|
||||
expect(vm.$el.querySelector('.ide-tree-changes')).toBe(null);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows for trees with changes', done => {
|
||||
vm.file.type = 'tree';
|
||||
vm.file.opened = false;
|
||||
changesCount = 1;
|
||||
|
||||
vm.$nextTick(() => {
|
||||
expect(vm.$el.querySelector('.ide-tree-changes')).not.toBe(null);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('changes file icon', () => {
|
||||
it('hides when file is not changed', () => {
|
||||
expect(vm.$el.querySelector('.ide-file-changed-icon')).toBe(null);
|
||||
});
|
||||
|
||||
it('shows when file is changed', done => {
|
||||
vm.file.changed = true;
|
||||
|
||||
vm.$nextTick(() => {
|
||||
expect(vm.$el.querySelector('.ide-file-changed-icon')).not.toBe(null);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows when file is staged', done => {
|
||||
vm.file.staged = true;
|
||||
|
||||
vm.$nextTick(() => {
|
||||
expect(vm.$el.querySelector('.ide-file-changed-icon')).not.toBe(null);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows when file is a tempFile', done => {
|
||||
vm.file.tempFile = true;
|
||||
|
||||
vm.$nextTick(() => {
|
||||
expect(vm.$el.querySelector('.ide-file-changed-icon')).not.toBe(null);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('merge request icon', () => {
|
||||
it('hides when not a merge request change', () => {
|
||||
expect(vm.$el.querySelector('.ic-git-merge')).toBe(null);
|
||||
});
|
||||
|
||||
it('shows when a merge request change', done => {
|
||||
vm.file.mrChange = true;
|
||||
|
||||
vm.$nextTick(() => {
|
||||
expect(vm.$el.querySelector('.ic-git-merge')).not.toBe(null);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,145 +0,0 @@
|
|||
import Vue from 'vue';
|
||||
import store from '~/ide/stores';
|
||||
import repoFile from '~/ide/components/repo_file.vue';
|
||||
import router from '~/ide/ide_router';
|
||||
import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
|
||||
import { file } from '../helpers';
|
||||
|
||||
describe('RepoFile', () => {
|
||||
let vm;
|
||||
|
||||
function createComponent(propsData) {
|
||||
const RepoFile = Vue.extend(repoFile);
|
||||
|
||||
vm = createComponentWithStore(RepoFile, store, propsData);
|
||||
|
||||
vm.$mount();
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vm.$destroy();
|
||||
});
|
||||
|
||||
it('renders link, icon and name', () => {
|
||||
createComponent({
|
||||
file: file('t4'),
|
||||
level: 0,
|
||||
});
|
||||
|
||||
const name = vm.$el.querySelector('.ide-file-name');
|
||||
|
||||
expect(name.href).toMatch('');
|
||||
expect(name.textContent.trim()).toEqual(vm.file.name);
|
||||
});
|
||||
|
||||
it('fires clickFile when the link is clicked', done => {
|
||||
spyOn(router, 'push');
|
||||
createComponent({
|
||||
file: file('t3'),
|
||||
level: 0,
|
||||
});
|
||||
|
||||
vm.$el.querySelector('.file-name').click();
|
||||
|
||||
setTimeout(() => {
|
||||
expect(router.push).toHaveBeenCalledWith(`/project${vm.file.url}`);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('folder', () => {
|
||||
it('renders changes count inside folder', () => {
|
||||
const f = {
|
||||
...file('folder'),
|
||||
path: 'testing',
|
||||
type: 'tree',
|
||||
branchId: 'master',
|
||||
projectId: 'project',
|
||||
};
|
||||
|
||||
store.state.changedFiles.push({
|
||||
...file('fileName'),
|
||||
path: 'testing/fileName',
|
||||
});
|
||||
|
||||
createComponent({
|
||||
file: f,
|
||||
level: 0,
|
||||
});
|
||||
|
||||
const treeChangesEl = vm.$el.querySelector('.ide-tree-changes');
|
||||
|
||||
expect(treeChangesEl).not.toBeNull();
|
||||
expect(treeChangesEl.textContent).toContain('1');
|
||||
});
|
||||
|
||||
it('renders action dropdown', done => {
|
||||
createComponent({
|
||||
file: {
|
||||
...file('t4'),
|
||||
type: 'tree',
|
||||
branchId: 'master',
|
||||
projectId: 'project',
|
||||
},
|
||||
level: 0,
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
expect(vm.$el.querySelector('.ide-new-btn')).not.toBeNull();
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('locked file', () => {
|
||||
let f;
|
||||
|
||||
beforeEach(() => {
|
||||
f = file('locked file');
|
||||
f.file_lock = {
|
||||
user: {
|
||||
name: 'testuser',
|
||||
updated_at: new Date(),
|
||||
},
|
||||
};
|
||||
|
||||
createComponent({
|
||||
file: f,
|
||||
level: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('renders lock icon', () => {
|
||||
expect(vm.$el.querySelector('.file-status-icon')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('renders a tooltip', () => {
|
||||
expect(
|
||||
vm.$el.querySelector('.ide-file-name span:nth-child(2)').dataset.originalTitle,
|
||||
).toContain('Locked by testuser');
|
||||
});
|
||||
});
|
||||
|
||||
it('calls scrollIntoView if made active', done => {
|
||||
createComponent({
|
||||
file: {
|
||||
...file(),
|
||||
type: 'blob',
|
||||
active: false,
|
||||
},
|
||||
level: 0,
|
||||
});
|
||||
|
||||
spyOn(vm, 'scrollIntoView');
|
||||
|
||||
vm.file.active = true;
|
||||
|
||||
vm.$nextTick(() => {
|
||||
expect(vm.scrollIntoView).toHaveBeenCalled();
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
74
spec/javascripts/vue_shared/components/file_row_spec.js
Normal file
74
spec/javascripts/vue_shared/components/file_row_spec.js
Normal file
|
@ -0,0 +1,74 @@
|
|||
import Vue from 'vue';
|
||||
import FileRow from '~/vue_shared/components/file_row.vue';
|
||||
import { file } from 'spec/ide/helpers';
|
||||
import mountComponent from '../../helpers/vue_mount_component_helper';
|
||||
|
||||
describe('RepoFile', () => {
|
||||
let vm;
|
||||
|
||||
function createComponent(propsData) {
|
||||
const FileRowComponent = Vue.extend(FileRow);
|
||||
|
||||
vm = mountComponent(FileRowComponent, propsData);
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vm.$destroy();
|
||||
});
|
||||
|
||||
it('renders name', () => {
|
||||
createComponent({
|
||||
file: file('t4'),
|
||||
level: 0,
|
||||
});
|
||||
|
||||
const name = vm.$el.querySelector('.file-row-name');
|
||||
|
||||
expect(name.textContent.trim()).toEqual(vm.file.name);
|
||||
});
|
||||
|
||||
it('emits toggleTreeOpen on click', () => {
|
||||
createComponent({
|
||||
file: {
|
||||
...file('t3'),
|
||||
type: 'tree',
|
||||
},
|
||||
level: 0,
|
||||
});
|
||||
spyOn(vm, '$emit').and.stub();
|
||||
|
||||
vm.$el.querySelector('.file-row').click();
|
||||
|
||||
expect(vm.$emit).toHaveBeenCalledWith('toggleTreeOpen', vm.file.path);
|
||||
});
|
||||
|
||||
it('calls scrollIntoView if made active', done => {
|
||||
createComponent({
|
||||
file: {
|
||||
...file(),
|
||||
type: 'blob',
|
||||
active: false,
|
||||
},
|
||||
level: 0,
|
||||
});
|
||||
|
||||
spyOn(vm, 'scrollIntoView').and.stub();
|
||||
|
||||
vm.file.active = true;
|
||||
|
||||
vm.$nextTick(() => {
|
||||
expect(vm.scrollIntoView).toHaveBeenCalled();
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('indents row based on level', () => {
|
||||
createComponent({
|
||||
file: file('t4'),
|
||||
level: 2,
|
||||
});
|
||||
|
||||
expect(vm.$el.querySelector('.file-row-name').style.marginLeft).toBe('32px');
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue