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 { mapActions, mapGetters, mapState } from 'vuex';
|
||||||
import Icon from '~/vue_shared/components/icon.vue';
|
import Icon from '~/vue_shared/components/icon.vue';
|
||||||
import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.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 NavDropdown from './nav_dropdown.vue';
|
||||||
|
import FileRowExtra from './file_row_extra.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
Icon,
|
Icon,
|
||||||
RepoFile,
|
|
||||||
SkeletonLoadingContainer,
|
SkeletonLoadingContainer,
|
||||||
NavDropdown,
|
NavDropdown,
|
||||||
|
FileRow,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
viewerType: {
|
viewerType: {
|
||||||
|
@ -34,8 +35,9 @@ export default {
|
||||||
this.updateViewer(this.viewerType);
|
this.updateViewer(this.viewerType);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(['updateViewer']),
|
...mapActions(['updateViewer', 'toggleTreeOpen']),
|
||||||
},
|
},
|
||||||
|
FileRowExtra,
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -63,11 +65,13 @@ export default {
|
||||||
<div
|
<div
|
||||||
class="ide-tree-body h-100"
|
class="ide-tree-body h-100"
|
||||||
>
|
>
|
||||||
<repo-file
|
<file-row
|
||||||
v-for="file in currentTree.tree"
|
v-for="file in currentTree.tree"
|
||||||
:key="file.key"
|
:key="file.key"
|
||||||
:file="file"
|
:file="file"
|
||||||
:level="0"
|
:level="0"
|
||||||
|
:extra-component="$options.FileRowExtra"
|
||||||
|
@toggleTreeOpen="toggleTreeOpen"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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;
|
flex: 1;
|
||||||
min-height: 0; // firefox fix
|
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 {
|
a {
|
||||||
color: $gl-text-color;
|
color: $gl-text-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
th {
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-name {
|
|
||||||
display: flex;
|
|
||||||
overflow: visible;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.multi-file-loading-container {
|
.multi-file-loading-container {
|
||||||
|
@ -625,8 +551,7 @@ $ide-commit-header-height: 48px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.multi-file-commit-list-path,
|
.multi-file-commit-list-path {
|
||||||
.ide-file-list .file {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-left: -$grid-size;
|
margin-left: -$grid-size;
|
||||||
|
@ -634,28 +559,14 @@ $ide-commit-header-height: 48px;
|
||||||
padding: $grid-size / 2 $grid-size;
|
padding: $grid-size / 2 $grid-size;
|
||||||
border-radius: $border-radius-default;
|
border-radius: $border-radius-default;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&:focus {
|
|
||||||
background: $theme-gray-100;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
background: $theme-gray-200;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.multi-file-commit-list-path {
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
height: $ide-commit-row-height;
|
height: $ide-commit-row-height;
|
||||||
padding-right: 0;
|
padding-right: 0;
|
||||||
|
|
||||||
&.is-active {
|
|
||||||
background-color: $white-normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover,
|
&:hover,
|
||||||
&:focus {
|
&:focus {
|
||||||
|
background: $theme-gray-100;
|
||||||
|
|
||||||
outline: 0;
|
outline: 0;
|
||||||
|
|
||||||
.multi-file-discard-btn {
|
.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 {
|
svg {
|
||||||
min-width: 16px;
|
min-width: 16px;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
@ -1398,9 +1317,17 @@ $ide-commit-header-height: 48px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ide-new-btn .dropdown.show .ide-entry-dropdown-toggle {
|
.ide-new-btn {
|
||||||
color: $white-normal;
|
display: none;
|
||||||
background-color: $blue-500;
|
|
||||||
|
.btn {
|
||||||
|
padding: 2px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown.show .ide-entry-dropdown-toggle {
|
||||||
|
color: $white-normal;
|
||||||
|
background-color: $blue-500;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ide-preview-header {
|
.ide-preview-header {
|
||||||
|
@ -1465,3 +1392,28 @@ $ide-commit-header-height: 48px;
|
||||||
width: $ide-commit-row-height;
|
width: $ide-commit-row-height;
|
||||||
height: $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