Web IDE context header redesign
This commit is contained in:
parent
379083bee4
commit
db739548fe
|
|
@ -13,11 +13,8 @@ export default {
|
|||
tooltip,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['currentProject', 'hasChanges']),
|
||||
...mapGetters(['hasChanges']),
|
||||
...mapState(['currentActivityView']),
|
||||
goBackUrl() {
|
||||
return document.referrer || this.currentProject.web_url;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['updateActivityBarView']),
|
||||
|
|
@ -36,22 +33,6 @@ export default {
|
|||
<template>
|
||||
<nav class="ide-activity-bar">
|
||||
<ul class="list-unstyled">
|
||||
<li v-once>
|
||||
<a
|
||||
v-tooltip
|
||||
:href="goBackUrl"
|
||||
:title="s__('IDE|Go back')"
|
||||
:aria-label="s__('IDE|Go back')"
|
||||
data-container="body"
|
||||
data-placement="right"
|
||||
class="ide-sidebar-link"
|
||||
>
|
||||
<icon
|
||||
:size="16"
|
||||
name="go-back"
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
v-tooltip
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
<script>
|
||||
import ProjectAvatarDefault from '~/vue_shared/components/project_avatar/default.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ProjectAvatarDefault,
|
||||
},
|
||||
props: {
|
||||
project: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="context-header ide-context-header">
|
||||
<a
|
||||
:href="project.web_url"
|
||||
:title="s__('IDE|Go to project')"
|
||||
>
|
||||
<project-avatar-default
|
||||
:project="project"
|
||||
:size="48"
|
||||
/>
|
||||
<span class="ide-sidebar-project-title">
|
||||
<span class="sidebar-context-title">
|
||||
{{ project.name }}
|
||||
</span>
|
||||
<span class="sidebar-context-title text-secondary">
|
||||
{{ project.path_with_namespace }}
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -1,12 +1,6 @@
|
|||
<script>
|
||||
import $ from 'jquery';
|
||||
import { mapState, mapGetters } from 'vuex';
|
||||
import ProjectAvatarImage from '~/vue_shared/components/project_avatar/image.vue';
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import tooltip from '~/vue_shared/directives/tooltip';
|
||||
import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
|
||||
import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
|
||||
import Identicon from '../../vue_shared/components/identicon.vue';
|
||||
import IdeTree from './ide_tree.vue';
|
||||
import ResizablePanel from './resizable_panel.vue';
|
||||
import ActivityBar from './activity_bar.vue';
|
||||
|
|
@ -14,43 +8,28 @@ import CommitSection from './repo_commit_section.vue';
|
|||
import CommitForm from './commit_sidebar/form.vue';
|
||||
import IdeReview from './ide_review.vue';
|
||||
import SuccessMessage from './commit_sidebar/success_message.vue';
|
||||
import MergeRequestDropdown from './merge_requests/dropdown.vue';
|
||||
import IdeProjectHeader from './ide_project_header.vue';
|
||||
import { activityBarViews } from '../constants';
|
||||
|
||||
export default {
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
components: {
|
||||
Icon,
|
||||
PanelResizer,
|
||||
SkeletonLoadingContainer,
|
||||
ResizablePanel,
|
||||
ActivityBar,
|
||||
ProjectAvatarImage,
|
||||
Identicon,
|
||||
CommitSection,
|
||||
IdeTree,
|
||||
CommitForm,
|
||||
IdeReview,
|
||||
SuccessMessage,
|
||||
MergeRequestDropdown,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showTooltip: false,
|
||||
showMergeRequestsDropdown: false,
|
||||
};
|
||||
IdeProjectHeader,
|
||||
},
|
||||
computed: {
|
||||
...mapState([
|
||||
'loading',
|
||||
'currentBranchId',
|
||||
'currentActivityView',
|
||||
'changedFiles',
|
||||
'stagedFiles',
|
||||
'lastCommitMsg',
|
||||
'currentMergeRequestId',
|
||||
]),
|
||||
...mapGetters(['currentProject', 'someUncommitedChanges']),
|
||||
showSuccessMessage() {
|
||||
|
|
@ -59,46 +38,6 @@ export default {
|
|||
(this.lastCommitMsg && !this.someUncommitedChanges)
|
||||
);
|
||||
},
|
||||
branchTooltipTitle() {
|
||||
return this.showTooltip ? this.currentBranchId : undefined;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
currentBranchId() {
|
||||
this.$nextTick(() => {
|
||||
if (!this.$refs.branchId) return;
|
||||
|
||||
this.showTooltip = this.$refs.branchId.scrollWidth > this.$refs.branchId.offsetWidth;
|
||||
});
|
||||
},
|
||||
loading() {
|
||||
this.$nextTick(() => {
|
||||
this.addDropdownListeners();
|
||||
});
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.addDropdownListeners();
|
||||
},
|
||||
beforeDestroy() {
|
||||
$(this.$refs.mergeRequestDropdown)
|
||||
.off('show.bs.dropdown')
|
||||
.off('hide.bs.dropdown');
|
||||
},
|
||||
methods: {
|
||||
addDropdownListeners() {
|
||||
if (!this.$refs.mergeRequestDropdown) return;
|
||||
|
||||
$(this.$refs.mergeRequestDropdown)
|
||||
.on('show.bs.dropdown', () => {
|
||||
this.toggleMergeRequestDropdown();
|
||||
}).on('hide.bs.dropdown', () => {
|
||||
this.toggleMergeRequestDropdown();
|
||||
});
|
||||
},
|
||||
toggleMergeRequestDropdown() {
|
||||
this.showMergeRequestsDropdown = !this.showMergeRequestsDropdown;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -108,12 +47,10 @@ export default {
|
|||
:collapsible="false"
|
||||
:initial-width="340"
|
||||
side="left"
|
||||
class="flex-column"
|
||||
>
|
||||
<activity-bar
|
||||
v-if="!loading"
|
||||
/>
|
||||
<div class="multi-file-commit-panel-inner">
|
||||
<template v-if="loading">
|
||||
<template v-if="loading">
|
||||
<div class="multi-file-commit-panel-inner">
|
||||
<div
|
||||
v-for="n in 3"
|
||||
:key="n"
|
||||
|
|
@ -121,81 +58,23 @@ export default {
|
|||
>
|
||||
<skeleton-loading-container />
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div
|
||||
ref="mergeRequestDropdown"
|
||||
class="context-header ide-context-header dropdown"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
data-toggle="dropdown"
|
||||
>
|
||||
<div
|
||||
v-if="currentProject.avatar_url"
|
||||
class="avatar-container s40 project-avatar"
|
||||
>
|
||||
<project-avatar-image
|
||||
:link-href="currentProject.path"
|
||||
:img-src="currentProject.avatar_url"
|
||||
:img-alt="currentProject.name"
|
||||
:img-size="40"
|
||||
class="avatar-container project-avatar"
|
||||
/>
|
||||
</div>
|
||||
<identicon
|
||||
v-else
|
||||
:entity-id="currentProject.id"
|
||||
:entity-name="currentProject.name"
|
||||
size-class="s40"
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<ide-project-header
|
||||
:project="currentProject"
|
||||
/>
|
||||
<div class="ide-context-body d-flex flex-fill">
|
||||
<activity-bar />
|
||||
<div class="multi-file-commit-panel-inner">
|
||||
<div class="multi-file-commit-panel-inner-content">
|
||||
<component
|
||||
:is="currentActivityView"
|
||||
/>
|
||||
<div class="ide-sidebar-project-title">
|
||||
<div class="sidebar-context-title">
|
||||
{{ currentProject.name }}
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
<div
|
||||
v-tooltip
|
||||
v-if="currentBranchId"
|
||||
ref="branchId"
|
||||
:title="branchTooltipTitle"
|
||||
class="sidebar-context-title ide-sidebar-branch-title"
|
||||
>
|
||||
<icon
|
||||
name="branch"
|
||||
css-classes="append-right-5"
|
||||
/>{{ currentBranchId }}
|
||||
</div>
|
||||
<div
|
||||
v-if="currentMergeRequestId"
|
||||
:class="{
|
||||
'prepend-left-8': currentBranchId
|
||||
}"
|
||||
class="sidebar-context-title ide-sidebar-branch-title"
|
||||
>
|
||||
<icon
|
||||
name="git-merge"
|
||||
css-classes="append-right-5"
|
||||
/>!{{ currentMergeRequestId }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<icon
|
||||
class="ml-auto"
|
||||
name="chevron-down"
|
||||
/>
|
||||
</button>
|
||||
<merge-request-dropdown
|
||||
:show="showMergeRequestsDropdown"
|
||||
/>
|
||||
</div>
|
||||
<commit-form />
|
||||
</div>
|
||||
<div class="multi-file-commit-panel-inner-scroll">
|
||||
<component
|
||||
:is="currentActivityView"
|
||||
/>
|
||||
</div>
|
||||
<commit-form />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</resizable-panel>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -35,7 +35,6 @@ export default {
|
|||
|
||||
<template>
|
||||
<ide-tree-list
|
||||
header-class="d-flex w-100"
|
||||
viewer-type="editor"
|
||||
>
|
||||
<template
|
||||
|
|
|
|||
|
|
@ -59,12 +59,16 @@ export default {
|
|||
>
|
||||
<slot name="header"></slot>
|
||||
</header>
|
||||
<repo-file
|
||||
v-for="file in currentTree.tree"
|
||||
:key="file.key"
|
||||
:file="file"
|
||||
:level="0"
|
||||
/>
|
||||
<div
|
||||
class="ide-tree-body"
|
||||
>
|
||||
<repo-file
|
||||
v-for="file in currentTree.tree"
|
||||
:key="file.key"
|
||||
:file="file"
|
||||
:level="0"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
<script>
|
||||
import Identicon from '../identicon.vue';
|
||||
import ProjectAvatarImage from './image.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Identicon,
|
||||
ProjectAvatarImage,
|
||||
},
|
||||
props: {
|
||||
project: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
default: 40,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
sizeClass() {
|
||||
return `s${this.size}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span
|
||||
:class="sizeClass"
|
||||
class="avatar-container project-avatar"
|
||||
>
|
||||
<project-avatar-image
|
||||
v-if="project.avatar_url"
|
||||
:link-href="project.path"
|
||||
:img-src="project.avatar_url"
|
||||
:img-alt="project.name"
|
||||
:img-size="size"
|
||||
/>
|
||||
<identicon
|
||||
v-else
|
||||
:entity-id="project.id"
|
||||
:entity-name="project.name"
|
||||
:size-class="sizeClass"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
|
|
@ -78,6 +78,7 @@
|
|||
&.s26 { font-size: 20px; line-height: 1.33; }
|
||||
&.s32 { font-size: 20px; line-height: 30px; }
|
||||
&.s40 { font-size: 16px; line-height: 38px; }
|
||||
&.s48 { font-size: 20px; line-height: 46px; }
|
||||
&.s60 { font-size: 32px; line-height: 58px; }
|
||||
&.s70 { font-size: 34px; line-height: 70px; }
|
||||
&.s90 { font-size: 36px; line-height: 88px; }
|
||||
|
|
|
|||
|
|
@ -55,6 +55,11 @@
|
|||
.sidebar-context-title {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&.text-secondary {
|
||||
font-weight: normal;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,12 @@
|
|||
@import 'framework/variables';
|
||||
@import 'framework/mixins';
|
||||
|
||||
$ide-activity-bar-width: 60px;
|
||||
$ide-context-header-padding: 10px;
|
||||
$ide-project-avatar-end: $ide-context-header-padding + 48px;
|
||||
$ide-tree-padding: $gl-padding;
|
||||
$ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding;
|
||||
|
||||
.project-refs-form,
|
||||
.project-refs-target-form {
|
||||
display: inline-block;
|
||||
|
|
@ -24,7 +30,6 @@
|
|||
display: flex;
|
||||
height: calc(100vh - #{$header-height});
|
||||
margin-top: 0;
|
||||
border-top: 1px solid $white-dark;
|
||||
padding-bottom: $ide-statusbar-height;
|
||||
color: $gl-text-color;
|
||||
|
||||
|
|
@ -41,10 +46,10 @@
|
|||
}
|
||||
|
||||
.ide-file-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
padding-left: $gl-padding;
|
||||
padding-right: $gl-padding;
|
||||
padding-bottom: $grid-size;
|
||||
overflow: hidden;
|
||||
|
||||
.file {
|
||||
height: 32px;
|
||||
|
|
@ -517,17 +522,12 @@
|
|||
|
||||
> a,
|
||||
> button {
|
||||
height: 60px;
|
||||
text-decoration: none;
|
||||
padding-top: $gl-padding-8;
|
||||
padding-bottom: $gl-padding-8;
|
||||
}
|
||||
}
|
||||
|
||||
.projects-sidebar {
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.multi-file-commit-panel-inner {
|
||||
position: relative;
|
||||
display: flex;
|
||||
|
|
@ -537,11 +537,11 @@
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.multi-file-commit-panel-inner-scroll {
|
||||
.multi-file-commit-panel-inner-content {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
overflow: hidden;
|
||||
background-color: $white-light;
|
||||
border-left: 1px solid $white-dark;
|
||||
border-top: 1px solid $white-dark;
|
||||
|
|
@ -803,12 +803,6 @@
|
|||
height: calc(100vh - #{$header-height + $flash-height});
|
||||
}
|
||||
}
|
||||
|
||||
.projects-sidebar {
|
||||
.multi-file-commit-panel-inner-scroll {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -964,7 +958,7 @@
|
|||
|
||||
.ide-activity-bar {
|
||||
position: relative;
|
||||
flex: 0 0 60px;
|
||||
flex: 0 0 $ide-activity-bar-width;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
|
|
@ -1060,10 +1054,12 @@
|
|||
}
|
||||
|
||||
.ide-tree-header {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
padding: 12px 0;
|
||||
margin-left: $ide-tree-padding;
|
||||
margin-right: $ide-tree-padding;
|
||||
border-bottom: 1px solid $white-dark;
|
||||
|
||||
.ide-new-btn {
|
||||
|
|
@ -1075,6 +1071,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
.ide-tree-body {
|
||||
overflow: auto;
|
||||
padding-left: $ide-tree-padding;
|
||||
padding-right: $ide-tree-padding;
|
||||
}
|
||||
|
||||
.ide-sidebar-branch-title {
|
||||
font-weight: $gl-font-weight-normal;
|
||||
|
||||
|
|
@ -1163,14 +1165,23 @@
|
|||
}
|
||||
|
||||
.ide-context-header {
|
||||
.avatar {
|
||||
flex: 0 0 38px;
|
||||
}
|
||||
|
||||
.ide-merge-requests-dropdown.dropdown-menu {
|
||||
width: 385px;
|
||||
max-height: initial;
|
||||
}
|
||||
|
||||
.avatar-container {
|
||||
flex: initial;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.ide-sidebar-project-title {
|
||||
margin-left: $ide-tree-text-start - $ide-project-avatar-end;
|
||||
}
|
||||
}
|
||||
|
||||
.ide-context-body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ide-sidebar-project-title {
|
||||
|
|
@ -1178,10 +1189,11 @@
|
|||
|
||||
.sidebar-context-title {
|
||||
white-space: nowrap;
|
||||
}
|
||||
display: block;
|
||||
|
||||
.ide-sidebar-branch-title {
|
||||
min-width: 50px;
|
||||
&.text-secondary {
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Redesign Web IDE back button and context header
|
||||
merge_request: 20850
|
||||
author:
|
||||
type: changed
|
||||
|
|
@ -2911,7 +2911,7 @@ msgstr ""
|
|||
msgid "IDE|Edit"
|
||||
msgstr ""
|
||||
|
||||
msgid "IDE|Go back"
|
||||
msgid "IDE|Go to project"
|
||||
msgstr ""
|
||||
|
||||
msgid "IDE|Open in file view"
|
||||
|
|
|
|||
|
|
@ -24,26 +24,6 @@ describe('IDE activity bar', () => {
|
|||
resetStore(vm.$store);
|
||||
});
|
||||
|
||||
describe('goBackUrl', () => {
|
||||
it('renders the Go Back link with the referrer when present', () => {
|
||||
const fakeReferrer = '/example/README.md';
|
||||
spyOnProperty(document, 'referrer').and.returnValue(fakeReferrer);
|
||||
|
||||
vm.$mount();
|
||||
|
||||
expect(vm.goBackUrl).toEqual(fakeReferrer);
|
||||
});
|
||||
|
||||
it('renders the Go Back link with the project url when referrer is not present', () => {
|
||||
const fakeReferrer = '';
|
||||
spyOnProperty(document, 'referrer').and.returnValue(fakeReferrer);
|
||||
|
||||
vm.$mount();
|
||||
|
||||
expect(vm.goBackUrl).toEqual('testing');
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateActivityBarView', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(vm, 'updateActivityBarView');
|
||||
|
|
|
|||
|
|
@ -0,0 +1,58 @@
|
|||
import Vue from 'vue';
|
||||
import ProjectAvatarDefault from '~/vue_shared/components/project_avatar/default.vue';
|
||||
import mountComponent from 'spec/helpers/vue_mount_component_helper';
|
||||
import { projectData } from 'spec/ide/mock_data';
|
||||
import { getFirstCharacterCapitalized } from '~/lib/utils/text_utility';
|
||||
import { TEST_HOST } from 'spec/test_constants';
|
||||
|
||||
describe('ProjectAvatarDefault component', () => {
|
||||
const Component = Vue.extend(ProjectAvatarDefault);
|
||||
let vm;
|
||||
|
||||
beforeEach(() => {
|
||||
vm = mountComponent(Component, {
|
||||
project: projectData,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vm.$destroy();
|
||||
});
|
||||
|
||||
it('renders identicon if project has no avatar_url', done => {
|
||||
const expectedText = getFirstCharacterCapitalized(projectData.name);
|
||||
|
||||
vm.project = {
|
||||
...vm.project,
|
||||
avatar_url: null,
|
||||
};
|
||||
|
||||
vm.$nextTick()
|
||||
.then(() => {
|
||||
const identiconEl = vm.$el.querySelector('.identicon');
|
||||
|
||||
expect(identiconEl).not.toBe(null);
|
||||
expect(identiconEl.textContent.trim()).toEqual(expectedText);
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('renders avatar image if project has avatar_url', done => {
|
||||
const avatarUrl = `${TEST_HOST}/images/home/nasa.svg`;
|
||||
|
||||
vm.project = {
|
||||
...vm.project,
|
||||
avatar_url: avatarUrl,
|
||||
};
|
||||
|
||||
vm.$nextTick()
|
||||
.then(() => {
|
||||
expect(vm.$el).toContainElement('.avatar');
|
||||
expect(vm.$el).not.toContainElement('.identicon');
|
||||
expect(vm.$el.querySelector('img')).toHaveAttr('src', avatarUrl);
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue