Merge branch 'ide-row-dropdown-design-update' into 'master'
Improvements to new entry dropdowns in Web IDE Closes #44845 See merge request gitlab-org/gitlab-ce!20526
This commit is contained in:
commit
d2c5ac61cd
22 changed files with 363 additions and 253 deletions
|
@ -1,6 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import Mousetrap from 'mousetrap';
|
import Mousetrap from 'mousetrap';
|
||||||
import { mapActions, mapState, mapGetters } from 'vuex';
|
import { mapActions, mapState, mapGetters } from 'vuex';
|
||||||
|
import NewModal from './new_dropdown/modal.vue';
|
||||||
import IdeSidebar from './ide_side_bar.vue';
|
import IdeSidebar from './ide_side_bar.vue';
|
||||||
import RepoTabs from './repo_tabs.vue';
|
import RepoTabs from './repo_tabs.vue';
|
||||||
import IdeStatusBar from './ide_status_bar.vue';
|
import IdeStatusBar from './ide_status_bar.vue';
|
||||||
|
@ -13,6 +14,7 @@ const originalStopCallback = Mousetrap.stopCallback;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
NewModal,
|
||||||
IdeSidebar,
|
IdeSidebar,
|
||||||
RepoTabs,
|
RepoTabs,
|
||||||
IdeStatusBar,
|
IdeStatusBar,
|
||||||
|
@ -137,5 +139,6 @@ export default {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<ide-status-bar :file="activeFile"/>
|
<ide-status-bar :file="activeFile"/>
|
||||||
|
<new-modal />
|
||||||
</article>
|
</article>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,12 +1,16 @@
|
||||||
<script>
|
<script>
|
||||||
import { mapState, mapGetters, mapActions } from 'vuex';
|
import { mapState, mapGetters, mapActions } from 'vuex';
|
||||||
import NewDropdown from './new_dropdown/index.vue';
|
import Icon from '~/vue_shared/components/icon.vue';
|
||||||
import IdeTreeList from './ide_tree_list.vue';
|
import IdeTreeList from './ide_tree_list.vue';
|
||||||
|
import Upload from './new_dropdown/upload.vue';
|
||||||
|
import NewEntryButton from './new_dropdown/button.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
NewDropdown,
|
Icon,
|
||||||
|
Upload,
|
||||||
IdeTreeList,
|
IdeTreeList,
|
||||||
|
NewEntryButton,
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(['currentBranchId']),
|
...mapState(['currentBranchId']),
|
||||||
|
@ -20,23 +24,42 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(['updateViewer']),
|
...mapActions(['updateViewer', 'openNewEntryModal', 'createTempEntry']),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ide-tree-list
|
<ide-tree-list
|
||||||
|
header-class="d-flex w-100"
|
||||||
viewer-type="editor"
|
viewer-type="editor"
|
||||||
>
|
>
|
||||||
<template
|
<template
|
||||||
slot="header"
|
slot="header"
|
||||||
>
|
>
|
||||||
{{ __('Edit') }}
|
{{ __('Edit') }}
|
||||||
<new-dropdown
|
<div class="ml-auto d-flex">
|
||||||
:project-id="currentProject.name_with_namespace"
|
<new-entry-button
|
||||||
:branch="currentBranchId"
|
:label="__('New file')"
|
||||||
/>
|
:show-label="false"
|
||||||
|
class="d-flex border-0 p-0 mr-3"
|
||||||
|
icon="doc-new"
|
||||||
|
@click="openNewEntryModal({ type: 'blob' })"
|
||||||
|
/>
|
||||||
|
<upload
|
||||||
|
:show-label="false"
|
||||||
|
class="d-flex mr-3"
|
||||||
|
button-css-classes="border-0 p-0"
|
||||||
|
@create="createTempEntry"
|
||||||
|
/>
|
||||||
|
<new-entry-button
|
||||||
|
:label="__('New directory')"
|
||||||
|
:show-label="false"
|
||||||
|
class="d-flex border-0 p-0"
|
||||||
|
icon="folder-new"
|
||||||
|
@click="openNewEntryModal({ type: 'tree' })"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</ide-tree-list>
|
</ide-tree-list>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
<script>
|
||||||
|
import Icon from '~/vue_shared/components/icon.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Icon,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
iconClasses: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
showLabel: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
clicked() {
|
||||||
|
this.$emit('click');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<button
|
||||||
|
:aria-label="label"
|
||||||
|
type="button"
|
||||||
|
@click.stop.prevent="clicked"
|
||||||
|
>
|
||||||
|
<icon
|
||||||
|
:name="icon"
|
||||||
|
:css-classes="iconClasses"
|
||||||
|
/>
|
||||||
|
<template v-if="showLabel">
|
||||||
|
{{ label }}
|
||||||
|
</template>
|
||||||
|
</button>
|
||||||
|
</template>
|
|
@ -3,12 +3,14 @@ import { mapActions } from 'vuex';
|
||||||
import icon from '~/vue_shared/components/icon.vue';
|
import icon from '~/vue_shared/components/icon.vue';
|
||||||
import newModal from './modal.vue';
|
import newModal from './modal.vue';
|
||||||
import upload from './upload.vue';
|
import upload from './upload.vue';
|
||||||
|
import ItemButton from './button.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
icon,
|
icon,
|
||||||
newModal,
|
newModal,
|
||||||
upload,
|
upload,
|
||||||
|
ItemButton,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
branch: {
|
branch: {
|
||||||
|
@ -20,11 +22,13 @@ export default {
|
||||||
required: false,
|
required: false,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
mouseOver: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
openModal: false,
|
|
||||||
modalType: '',
|
|
||||||
dropdownOpen: false,
|
dropdownOpen: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -34,17 +38,18 @@ export default {
|
||||||
this.$refs.dropdownMenu.scrollIntoView();
|
this.$refs.dropdownMenu.scrollIntoView();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
mouseOver() {
|
||||||
|
if (!this.mouseOver) {
|
||||||
|
this.dropdownOpen = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(['createTempEntry']),
|
...mapActions(['createTempEntry', 'openNewEntryModal']),
|
||||||
createNewItem(type) {
|
createNewItem(type) {
|
||||||
this.modalType = type;
|
this.openNewEntryModal({ type, path: this.path });
|
||||||
this.openModal = true;
|
|
||||||
this.dropdownOpen = false;
|
this.dropdownOpen = false;
|
||||||
},
|
},
|
||||||
hideModal() {
|
|
||||||
this.openModal = false;
|
|
||||||
},
|
|
||||||
openDropdown() {
|
openDropdown() {
|
||||||
this.dropdownOpen = !this.dropdownOpen;
|
this.dropdownOpen = !this.dropdownOpen;
|
||||||
},
|
},
|
||||||
|
@ -58,23 +63,19 @@ export default {
|
||||||
:class="{
|
:class="{
|
||||||
show: dropdownOpen,
|
show: dropdownOpen,
|
||||||
}"
|
}"
|
||||||
class="dropdown"
|
class="dropdown d-flex"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
|
:aria-label="__('Create new file or directory')"
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-sm btn-default dropdown-toggle add-to-tree"
|
class="rounded border-0 d-flex ide-entry-dropdown-toggle"
|
||||||
aria-label="Create new file or directory"
|
|
||||||
@click.stop="openDropdown()"
|
@click.stop="openDropdown()"
|
||||||
>
|
>
|
||||||
<icon
|
<icon
|
||||||
:size="12"
|
name="hamburger"
|
||||||
name="plus"
|
|
||||||
css-classes="float-left"
|
|
||||||
/>
|
/>
|
||||||
<icon
|
<icon
|
||||||
:size="12"
|
|
||||||
name="arrow-down"
|
name="arrow-down"
|
||||||
css-classes="float-left"
|
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<ul
|
<ul
|
||||||
|
@ -82,39 +83,30 @@ export default {
|
||||||
class="dropdown-menu dropdown-menu-right"
|
class="dropdown-menu dropdown-menu-right"
|
||||||
>
|
>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<item-button
|
||||||
href="#"
|
:label="__('New file')"
|
||||||
role="button"
|
class="d-flex"
|
||||||
@click.stop.prevent="createNewItem('blob')"
|
icon="doc-new"
|
||||||
>
|
icon-classes="mr-2"
|
||||||
{{ __('New file') }}
|
@click="createNewItem('blob')"
|
||||||
</a>
|
/>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<upload
|
<upload
|
||||||
:branch-id="branch"
|
|
||||||
:path="path"
|
:path="path"
|
||||||
@create="createTempEntry"
|
@create="createTempEntry"
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<item-button
|
||||||
href="#"
|
:label="__('New directory')"
|
||||||
role="button"
|
class="d-flex"
|
||||||
@click.stop.prevent="createNewItem('tree')"
|
icon="folder-new"
|
||||||
>
|
icon-classes="mr-2"
|
||||||
{{ __('New directory') }}
|
@click="createNewItem('tree')"
|
||||||
</a>
|
/>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<new-modal
|
|
||||||
v-if="openModal"
|
|
||||||
:type="modalType"
|
|
||||||
:branch-id="branch"
|
|
||||||
:path="path"
|
|
||||||
@hide="hideModal"
|
|
||||||
@create="createTempEntry"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,78 +1,70 @@
|
||||||
<script>
|
<script>
|
||||||
import { __ } from '~/locale';
|
import { __ } from '~/locale';
|
||||||
import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
|
import { mapActions, mapState } from 'vuex';
|
||||||
|
import GlModal from '~/vue_shared/components/gl_modal.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
DeprecatedModal,
|
GlModal,
|
||||||
},
|
|
||||||
props: {
|
|
||||||
branchId: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
type: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
path: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
entryName: this.path !== '' ? `${this.path}/` : '',
|
name: '',
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
...mapState(['newEntryModal']),
|
||||||
|
entryName: {
|
||||||
|
get() {
|
||||||
|
return this.name || (this.newEntryModal.path !== '' ? `${this.newEntryModal.path}/` : '');
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.name = val;
|
||||||
|
},
|
||||||
|
},
|
||||||
modalTitle() {
|
modalTitle() {
|
||||||
if (this.type === 'tree') {
|
if (this.newEntryModal.type === 'tree') {
|
||||||
return __('Create new directory');
|
return __('Create new directory');
|
||||||
}
|
}
|
||||||
|
|
||||||
return __('Create new file');
|
return __('Create new file');
|
||||||
},
|
},
|
||||||
buttonLabel() {
|
buttonLabel() {
|
||||||
if (this.type === 'tree') {
|
if (this.newEntryModal.type === 'tree') {
|
||||||
return __('Create directory');
|
return __('Create directory');
|
||||||
}
|
}
|
||||||
|
|
||||||
return __('Create file');
|
return __('Create file');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
|
||||||
this.$refs.fieldName.focus();
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
|
...mapActions(['createTempEntry']),
|
||||||
createEntryInStore() {
|
createEntryInStore() {
|
||||||
this.$emit('create', {
|
this.createTempEntry({
|
||||||
branchId: this.branchId,
|
name: this.name,
|
||||||
name: this.entryName,
|
type: this.newEntryModal.type,
|
||||||
type: this.type,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.hideModal();
|
|
||||||
},
|
},
|
||||||
hideModal() {
|
focusInput() {
|
||||||
this.$emit('hide');
|
setTimeout(() => {
|
||||||
|
this.$refs.fieldName.focus();
|
||||||
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<deprecated-modal
|
<gl-modal
|
||||||
:title="modalTitle"
|
id="ide-new-entry"
|
||||||
:primary-button-label="buttonLabel"
|
:header-title-text="modalTitle"
|
||||||
kind="success"
|
:footer-primary-button-text="buttonLabel"
|
||||||
@cancel="hideModal"
|
footer-primary-button-variant="success"
|
||||||
@submit="createEntryInStore"
|
@submit="createEntryInStore"
|
||||||
|
@open="focusInput"
|
||||||
>
|
>
|
||||||
<form
|
<div
|
||||||
slot="body"
|
|
||||||
class="form-group row"
|
class="form-group row"
|
||||||
@submit.prevent="createEntryInStore"
|
|
||||||
>
|
>
|
||||||
<label class="label-light col-form-label col-sm-3">
|
<label class="label-light col-form-label col-sm-3">
|
||||||
{{ __('Name') }}
|
{{ __('Name') }}
|
||||||
|
@ -85,6 +77,6 @@ export default {
|
||||||
class="form-control"
|
class="form-control"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</div>
|
||||||
</deprecated-modal>
|
</gl-modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,71 +1,85 @@
|
||||||
<script>
|
<script>
|
||||||
export default {
|
import Icon from '~/vue_shared/components/icon.vue';
|
||||||
props: {
|
import ItemButton from './button.vue';
|
||||||
branchId: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
path: {
|
|
||||||
type: String,
|
|
||||||
required: false,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.$refs.fileUpload.addEventListener('change', this.openFile);
|
|
||||||
},
|
|
||||||
beforeDestroy() {
|
|
||||||
this.$refs.fileUpload.removeEventListener('change', this.openFile);
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
createFile(target, file, isText) {
|
|
||||||
const { name } = file;
|
|
||||||
let { result } = target;
|
|
||||||
|
|
||||||
if (!isText) {
|
export default {
|
||||||
// eslint-disable-next-line prefer-destructuring
|
components: {
|
||||||
result = result.split('base64,')[1];
|
Icon,
|
||||||
}
|
ItemButton,
|
||||||
|
},
|
||||||
this.$emit('create', {
|
props: {
|
||||||
name: `${(this.path ? `${this.path}/` : '')}${name}`,
|
path: {
|
||||||
branchId: this.branchId,
|
type: String,
|
||||||
type: 'blob',
|
required: false,
|
||||||
content: result,
|
default: '',
|
||||||
base64: !isText,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
readFile(file) {
|
|
||||||
const reader = new FileReader();
|
|
||||||
const isText = file.type.match(/text.*/) !== null;
|
|
||||||
|
|
||||||
reader.addEventListener('load', e => this.createFile(e.target, file, isText), { once: true });
|
|
||||||
|
|
||||||
if (isText) {
|
|
||||||
reader.readAsText(file);
|
|
||||||
} else {
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
openFile() {
|
|
||||||
Array.from(this.$refs.fileUpload.files).forEach(file => this.readFile(file));
|
|
||||||
},
|
|
||||||
startFileUpload() {
|
|
||||||
this.$refs.fileUpload.click();
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
showLabel: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
buttonCssClasses: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$refs.fileUpload.addEventListener('change', this.openFile);
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.$refs.fileUpload.removeEventListener('change', this.openFile);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
createFile(target, file, isText) {
|
||||||
|
const { name } = file;
|
||||||
|
let { result } = target;
|
||||||
|
|
||||||
|
if (!isText) {
|
||||||
|
// eslint-disable-next-line prefer-destructuring
|
||||||
|
result = result.split('base64,')[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$emit('create', {
|
||||||
|
name: `${this.path ? `${this.path}/` : ''}${name}`,
|
||||||
|
type: 'blob',
|
||||||
|
content: result,
|
||||||
|
base64: !isText,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
readFile(file) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
const isText = file.type.match(/text.*/) !== null;
|
||||||
|
|
||||||
|
reader.addEventListener('load', e => this.createFile(e.target, file, isText), { once: true });
|
||||||
|
|
||||||
|
if (isText) {
|
||||||
|
reader.readAsText(file);
|
||||||
|
} else {
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
openFile() {
|
||||||
|
Array.from(this.$refs.fileUpload.files).forEach(file => this.readFile(file));
|
||||||
|
},
|
||||||
|
startFileUpload() {
|
||||||
|
this.$refs.fileUpload.click();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<a
|
<item-button
|
||||||
href="#"
|
:class="buttonCssClasses"
|
||||||
role="button"
|
:show-label="showLabel"
|
||||||
@click.stop.prevent="startFileUpload"
|
:icon-classes="showLabel ? 'mr-2' : ''"
|
||||||
>
|
:label="__('Upload file')"
|
||||||
{{ __('Upload file') }}
|
class="d-flex"
|
||||||
</a>
|
icon="upload"
|
||||||
|
@click="startFileUpload"
|
||||||
|
/>
|
||||||
<input
|
<input
|
||||||
id="file-upload"
|
id="file-upload"
|
||||||
ref="fileUpload"
|
ref="fileUpload"
|
||||||
|
|
|
@ -40,6 +40,11 @@ export default {
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
mouseOver: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters([
|
...mapGetters([
|
||||||
'getChangesInFolder',
|
'getChangesInFolder',
|
||||||
|
@ -142,6 +147,9 @@ export default {
|
||||||
hasUrlAtCurrentRoute() {
|
hasUrlAtCurrentRoute() {
|
||||||
return this.$router.currentRoute.path === `/project${this.file.url}`;
|
return this.$router.currentRoute.path === `/project${this.file.url}`;
|
||||||
},
|
},
|
||||||
|
toggleHover(over) {
|
||||||
|
this.mouseOver = over;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -153,6 +161,8 @@ export default {
|
||||||
class="file"
|
class="file"
|
||||||
role="button"
|
role="button"
|
||||||
@click="clickFile"
|
@click="clickFile"
|
||||||
|
@mouseover="toggleHover(true)"
|
||||||
|
@mouseout="toggleHover(false)"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="file-name"
|
class="file-name"
|
||||||
|
@ -206,6 +216,7 @@ export default {
|
||||||
:project-id="file.projectId"
|
:project-id="file.projectId"
|
||||||
:branch="file.branchId"
|
:branch="file.branchId"
|
||||||
:path="file.path"
|
:path="file.path"
|
||||||
|
:mouse-over="mouseOver"
|
||||||
class="float-right prepend-left-8"
|
class="float-right prepend-left-8"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -52,7 +52,7 @@ export const setResizingStatus = ({ commit }, resizing) => {
|
||||||
|
|
||||||
export const createTempEntry = (
|
export const createTempEntry = (
|
||||||
{ state, commit, dispatch },
|
{ state, commit, dispatch },
|
||||||
{ branchId, name, type, content = '', base64 = false },
|
{ name, type, content = '', base64 = false },
|
||||||
) =>
|
) =>
|
||||||
new Promise(resolve => {
|
new Promise(resolve => {
|
||||||
const worker = new FilesDecoratorWorker();
|
const worker = new FilesDecoratorWorker();
|
||||||
|
@ -81,7 +81,7 @@ export const createTempEntry = (
|
||||||
commit(types.CREATE_TMP_ENTRY, {
|
commit(types.CREATE_TMP_ENTRY, {
|
||||||
data,
|
data,
|
||||||
projectId: state.currentProjectId,
|
projectId: state.currentProjectId,
|
||||||
branchId,
|
branchId: state.currentBranchId,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (type === 'blob') {
|
if (type === 'blob') {
|
||||||
|
@ -100,7 +100,7 @@ export const createTempEntry = (
|
||||||
worker.postMessage({
|
worker.postMessage({
|
||||||
data: [fullName],
|
data: [fullName],
|
||||||
projectId: state.currentProjectId,
|
projectId: state.currentProjectId,
|
||||||
branchId,
|
branchId: state.currentBranchId,
|
||||||
type,
|
type,
|
||||||
tempFile: true,
|
tempFile: true,
|
||||||
base64,
|
base64,
|
||||||
|
@ -178,6 +178,13 @@ export const setLinks = ({ commit }, links) => commit(types.SET_LINKS, links);
|
||||||
export const setErrorMessage = ({ commit }, errorMessage) =>
|
export const setErrorMessage = ({ commit }, errorMessage) =>
|
||||||
commit(types.SET_ERROR_MESSAGE, errorMessage);
|
commit(types.SET_ERROR_MESSAGE, errorMessage);
|
||||||
|
|
||||||
|
export const openNewEntryModal = ({ commit }, { type, path = '' }) => {
|
||||||
|
commit(types.OPEN_NEW_ENTRY_MODAL, { type, path });
|
||||||
|
|
||||||
|
// open the modal manually so we don't mess around with dropdown/rows
|
||||||
|
$('#ide-new-entry').modal('show');
|
||||||
|
};
|
||||||
|
|
||||||
export * from './actions/tree';
|
export * from './actions/tree';
|
||||||
export * from './actions/file';
|
export * from './actions/file';
|
||||||
export * from './actions/project';
|
export * from './actions/project';
|
||||||
|
|
|
@ -74,3 +74,5 @@ export const CLEAR_PROJECTS = 'CLEAR_PROJECTS';
|
||||||
export const RESET_OPEN_FILES = 'RESET_OPEN_FILES';
|
export const RESET_OPEN_FILES = 'RESET_OPEN_FILES';
|
||||||
|
|
||||||
export const SET_ERROR_MESSAGE = 'SET_ERROR_MESSAGE';
|
export const SET_ERROR_MESSAGE = 'SET_ERROR_MESSAGE';
|
||||||
|
|
||||||
|
export const OPEN_NEW_ENTRY_MODAL = 'OPEN_NEW_ENTRY_MODAL';
|
||||||
|
|
|
@ -166,6 +166,11 @@ export default {
|
||||||
[types.SET_ERROR_MESSAGE](state, errorMessage) {
|
[types.SET_ERROR_MESSAGE](state, errorMessage) {
|
||||||
Object.assign(state, { errorMessage });
|
Object.assign(state, { errorMessage });
|
||||||
},
|
},
|
||||||
|
[types.OPEN_NEW_ENTRY_MODAL](state, { type, path }) {
|
||||||
|
Object.assign(state, {
|
||||||
|
newEntryModal: { type, path },
|
||||||
|
});
|
||||||
|
},
|
||||||
...projectMutations,
|
...projectMutations,
|
||||||
...mergeRequestMutation,
|
...mergeRequestMutation,
|
||||||
...fileMutations,
|
...fileMutations,
|
||||||
|
|
|
@ -26,4 +26,8 @@ export default () => ({
|
||||||
rightPane: null,
|
rightPane: null,
|
||||||
links: {},
|
links: {},
|
||||||
errorMessage: null,
|
errorMessage: null,
|
||||||
|
newEntryModal: {
|
||||||
|
type: '',
|
||||||
|
path: '',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -45,6 +45,11 @@ export default {
|
||||||
emitSubmit(event) {
|
emitSubmit(event) {
|
||||||
this.$emit('submit', event);
|
this.$emit('submit', event);
|
||||||
},
|
},
|
||||||
|
opened({ propertyName }) {
|
||||||
|
if (propertyName === 'opacity') {
|
||||||
|
this.$emit('open');
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -55,6 +60,7 @@ export default {
|
||||||
class="modal fade"
|
class="modal fade"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
role="dialog"
|
role="dialog"
|
||||||
|
@transitionend="opened"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
:class="modalSizeClass"
|
:class="modalSizeClass"
|
||||||
|
|
|
@ -44,6 +44,7 @@
|
||||||
padding-bottom: $grid-size;
|
padding-bottom: $grid-size;
|
||||||
|
|
||||||
.file {
|
.file {
|
||||||
|
height: 32px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&.file-active {
|
&.file-active {
|
||||||
|
@ -716,32 +717,6 @@
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ide-new-btn {
|
|
||||||
.btn {
|
|
||||||
padding-top: 3px;
|
|
||||||
padding-bottom: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-toggle svg {
|
|
||||||
top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-menu {
|
|
||||||
left: auto;
|
|
||||||
right: 0;
|
|
||||||
|
|
||||||
label {
|
|
||||||
font-weight: $gl-font-weight-normal;
|
|
||||||
padding: 5px 8px;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.ide {
|
.ide {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
|
@ -1340,3 +1315,24 @@
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ide-entry-dropdown-toggle {
|
||||||
|
padding: $gl-padding-4;
|
||||||
|
background-color: $theme-gray-100;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $theme-gray-200;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active,
|
||||||
|
&:focus {
|
||||||
|
color: $white-normal;
|
||||||
|
background-color: $blue-500;
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ide-new-btn .dropdown.show .ide-entry-dropdown-toggle {
|
||||||
|
color: $white-normal;
|
||||||
|
background-color: $blue-500;
|
||||||
|
}
|
||||||
|
|
5
changelogs/unreleased/ide-row-dropdown-design-update.yml
Normal file
5
changelogs/unreleased/ide-row-dropdown-design-update.yml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Updated design of new entry dropdown in Web IDE
|
||||||
|
merge_request: 20526
|
||||||
|
author:
|
||||||
|
type: changed
|
|
@ -1720,6 +1720,9 @@ msgstr ""
|
||||||
msgid "Create new file"
|
msgid "Create new file"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Create new file or directory"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Create new label"
|
msgid "Create new label"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
@ -22,9 +22,7 @@ describe 'Multi-file editor new directory', :js do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'creates directory in current directory' do
|
it 'creates directory in current directory' do
|
||||||
find('.add-to-tree').click
|
all('.ide-tree-header button').last.click
|
||||||
|
|
||||||
click_link('New directory')
|
|
||||||
|
|
||||||
page.within('.modal') do
|
page.within('.modal') do
|
||||||
find('.form-control').set('folder name')
|
find('.form-control').set('folder name')
|
||||||
|
@ -32,9 +30,7 @@ describe 'Multi-file editor new directory', :js do
|
||||||
click_button('Create directory')
|
click_button('Create directory')
|
||||||
end
|
end
|
||||||
|
|
||||||
find('.add-to-tree').click
|
first('.ide-tree-header button').click
|
||||||
|
|
||||||
click_link('New file')
|
|
||||||
|
|
||||||
page.within('.modal-dialog') do
|
page.within('.modal-dialog') do
|
||||||
find('.form-control').set('file name')
|
find('.form-control').set('file name')
|
||||||
|
|
|
@ -22,9 +22,7 @@ describe 'Multi-file editor new file', :js do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'creates file in current directory' do
|
it 'creates file in current directory' do
|
||||||
find('.add-to-tree').click
|
first('.ide-tree-header button').click
|
||||||
|
|
||||||
click_link('New file')
|
|
||||||
|
|
||||||
page.within('.modal') do
|
page.within('.modal') do
|
||||||
find('.form-control').set('file name')
|
find('.form-control').set('file name')
|
||||||
|
|
|
@ -24,14 +24,10 @@ describe 'Multi-file editor upload file', :js do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'uploads text file' do
|
it 'uploads text file' do
|
||||||
find('.add-to-tree').click
|
|
||||||
|
|
||||||
# make the field visible so capybara can use it
|
# make the field visible so capybara can use it
|
||||||
execute_script('document.querySelector("#file-upload").classList.remove("hidden")')
|
execute_script('document.querySelector("#file-upload").classList.remove("hidden")')
|
||||||
attach_file('file-upload', txt_file)
|
attach_file('file-upload', txt_file)
|
||||||
|
|
||||||
find('.add-to-tree').click
|
|
||||||
|
|
||||||
expect(page).to have_selector('.multi-file-tab', text: 'doc_sample.txt')
|
expect(page).to have_selector('.multi-file-tab', text: 'doc_sample.txt')
|
||||||
expect(find('.blob-editor-container .lines-content')['innerText']).to have_content(File.open(txt_file, &:readline))
|
expect(find('.blob-editor-container .lines-content')['innerText']).to have_content(File.open(txt_file, &:readline))
|
||||||
end
|
end
|
||||||
|
|
49
spec/javascripts/ide/components/new_dropdown/button_spec.js
Normal file
49
spec/javascripts/ide/components/new_dropdown/button_spec.js
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
import Vue from 'vue';
|
||||||
|
import mountComponent from 'spec/helpers/vue_mount_component_helper';
|
||||||
|
import Button from '~/ide/components/new_dropdown/button.vue';
|
||||||
|
|
||||||
|
describe('IDE new entry dropdown button component', () => {
|
||||||
|
let Component;
|
||||||
|
let vm;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
Component = Vue.extend(Button);
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vm = mountComponent(Component, {
|
||||||
|
label: 'Testing',
|
||||||
|
icon: 'doc-new',
|
||||||
|
});
|
||||||
|
|
||||||
|
spyOn(vm, '$emit');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vm.$destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders button with label', () => {
|
||||||
|
expect(vm.$el.textContent).toContain('Testing');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders icon', () => {
|
||||||
|
expect(vm.$el.querySelector('.ic-doc-new')).not.toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('emits click event', () => {
|
||||||
|
vm.$el.click();
|
||||||
|
|
||||||
|
expect(vm.$emit).toHaveBeenCalledWith('click');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('hides label if showLabel is false', done => {
|
||||||
|
vm.showLabel = false;
|
||||||
|
|
||||||
|
vm.$nextTick(() => {
|
||||||
|
expect(vm.$el.textContent).not.toContain('Testing');
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -13,6 +13,7 @@ describe('new dropdown component', () => {
|
||||||
vm = createComponentWithStore(component, store, {
|
vm = createComponentWithStore(component, store, {
|
||||||
branch: 'master',
|
branch: 'master',
|
||||||
path: '',
|
path: '',
|
||||||
|
mouseOver: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
vm.$store.state.currentProjectId = 'abcproject';
|
vm.$store.state.currentProjectId = 'abcproject';
|
||||||
|
@ -21,6 +22,8 @@ describe('new dropdown component', () => {
|
||||||
tree: [],
|
tree: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
spyOn(vm, 'openNewEntryModal');
|
||||||
|
|
||||||
vm.$mount();
|
vm.$mount();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -31,50 +34,23 @@ describe('new dropdown component', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders new file, upload and new directory links', () => {
|
it('renders new file, upload and new directory links', () => {
|
||||||
expect(vm.$el.querySelectorAll('a')[0].textContent.trim()).toBe('New file');
|
const buttons = vm.$el.querySelectorAll('.dropdown-menu button');
|
||||||
expect(vm.$el.querySelectorAll('a')[1].textContent.trim()).toBe('Upload file');
|
expect(buttons[0].textContent.trim()).toBe('New file');
|
||||||
expect(vm.$el.querySelectorAll('a')[2].textContent.trim()).toBe('New directory');
|
expect(buttons[1].textContent.trim()).toBe('Upload file');
|
||||||
|
expect(buttons[2].textContent.trim()).toBe('New directory');
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('createNewItem', () => {
|
describe('createNewItem', () => {
|
||||||
it('sets modalType to blob when new file is clicked', () => {
|
it('sets modalType to blob when new file is clicked', () => {
|
||||||
vm.$el.querySelectorAll('a')[0].click();
|
vm.$el.querySelectorAll('.dropdown-menu button')[0].click();
|
||||||
|
|
||||||
expect(vm.modalType).toBe('blob');
|
expect(vm.openNewEntryModal).toHaveBeenCalledWith({ type: 'blob', path: '' });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sets modalType to tree when new directory is clicked', () => {
|
it('sets modalType to tree when new directory is clicked', () => {
|
||||||
vm.$el.querySelectorAll('a')[2].click();
|
vm.$el.querySelectorAll('.dropdown-menu button')[2].click();
|
||||||
|
|
||||||
expect(vm.modalType).toBe('tree');
|
expect(vm.openNewEntryModal).toHaveBeenCalledWith({ type: 'tree', path: '' });
|
||||||
});
|
|
||||||
|
|
||||||
it('opens modal when link is clicked', done => {
|
|
||||||
vm.$el.querySelectorAll('a')[0].click();
|
|
||||||
|
|
||||||
Vue.nextTick(() => {
|
|
||||||
expect(vm.$el.querySelector('.modal')).not.toBeNull();
|
|
||||||
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('hideModal', () => {
|
|
||||||
beforeAll(done => {
|
|
||||||
vm.openModal = true;
|
|
||||||
Vue.nextTick(done);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('closes modal after toggling', done => {
|
|
||||||
vm.hideModal();
|
|
||||||
|
|
||||||
Vue.nextTick()
|
|
||||||
.then(() => {
|
|
||||||
expect(vm.$el.querySelector('.modal')).toBeNull();
|
|
||||||
})
|
|
||||||
.then(done)
|
|
||||||
.catch(done.fail);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
|
import { createStore } from '~/ide/stores';
|
||||||
import modal from '~/ide/components/new_dropdown/modal.vue';
|
import modal from '~/ide/components/new_dropdown/modal.vue';
|
||||||
import createComponent from 'spec/helpers/vue_mount_component_helper';
|
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
|
||||||
|
|
||||||
describe('new file modal component', () => {
|
describe('new file modal component', () => {
|
||||||
const Component = Vue.extend(modal);
|
const Component = Vue.extend(modal);
|
||||||
|
@ -13,13 +14,15 @@ describe('new file modal component', () => {
|
||||||
['tree', 'blob'].forEach(type => {
|
['tree', 'blob'].forEach(type => {
|
||||||
describe(type, () => {
|
describe(type, () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vm = createComponent(Component, {
|
const store = createStore();
|
||||||
|
store.state.newEntryModal = {
|
||||||
type,
|
type,
|
||||||
branchId: 'master',
|
|
||||||
path: '',
|
path: '',
|
||||||
});
|
};
|
||||||
|
|
||||||
vm.entryName = 'testing';
|
vm = createComponentWithStore(Component, store).$mount();
|
||||||
|
|
||||||
|
vm.name = 'testing';
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`sets modal title as ${type}`, () => {
|
it(`sets modal title as ${type}`, () => {
|
||||||
|
@ -40,12 +43,11 @@ describe('new file modal component', () => {
|
||||||
|
|
||||||
describe('createEntryInStore', () => {
|
describe('createEntryInStore', () => {
|
||||||
it('$emits create', () => {
|
it('$emits create', () => {
|
||||||
spyOn(vm, '$emit');
|
spyOn(vm, 'createTempEntry');
|
||||||
|
|
||||||
vm.createEntryInStore();
|
vm.createEntryInStore();
|
||||||
|
|
||||||
expect(vm.$emit).toHaveBeenCalledWith('create', {
|
expect(vm.createTempEntry).toHaveBeenCalledWith({
|
||||||
branchId: 'master',
|
|
||||||
name: 'testing',
|
name: 'testing',
|
||||||
type,
|
type,
|
||||||
});
|
});
|
||||||
|
@ -53,22 +55,4 @@ describe('new file modal component', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('focuses field on mount', () => {
|
|
||||||
document.body.innerHTML += '<div class="js-test"></div>';
|
|
||||||
|
|
||||||
vm = createComponent(
|
|
||||||
Component,
|
|
||||||
{
|
|
||||||
type: 'tree',
|
|
||||||
branchId: 'master',
|
|
||||||
path: '',
|
|
||||||
},
|
|
||||||
'.js-test',
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(document.activeElement).toBe(vm.$refs.fieldName);
|
|
||||||
|
|
||||||
vm.$el.remove();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -9,7 +9,6 @@ describe('new dropdown upload', () => {
|
||||||
const Component = Vue.extend(upload);
|
const Component = Vue.extend(upload);
|
||||||
|
|
||||||
vm = createComponent(Component, {
|
vm = createComponent(Component, {
|
||||||
branchId: 'master',
|
|
||||||
path: '',
|
path: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -65,7 +64,6 @@ describe('new dropdown upload', () => {
|
||||||
|
|
||||||
expect(vm.$emit).toHaveBeenCalledWith('create', {
|
expect(vm.$emit).toHaveBeenCalledWith('create', {
|
||||||
name: file.name,
|
name: file.name,
|
||||||
branchId: 'master',
|
|
||||||
type: 'blob',
|
type: 'blob',
|
||||||
content: target.result,
|
content: target.result,
|
||||||
base64: false,
|
base64: false,
|
||||||
|
@ -77,7 +75,6 @@ describe('new dropdown upload', () => {
|
||||||
|
|
||||||
expect(vm.$emit).toHaveBeenCalledWith('create', {
|
expect(vm.$emit).toHaveBeenCalledWith('create', {
|
||||||
name: file.name,
|
name: file.name,
|
||||||
branchId: 'master',
|
|
||||||
type: 'blob',
|
type: 'blob',
|
||||||
content: binaryTarget.result.split('base64,')[1],
|
content: binaryTarget.result.split('base64,')[1],
|
||||||
base64: true,
|
base64: true,
|
||||||
|
|
Loading…
Reference in a new issue