Merge branch 'master' into 'issue_38337'
# Conflicts: # app/models/group.rb # db/schema.rb
This commit is contained in:
commit
e77c4e9efe
543 changed files with 29634 additions and 12041 deletions
|
@ -619,9 +619,10 @@ codequality:
|
|||
cache: {}
|
||||
dependencies: []
|
||||
script:
|
||||
- apk update && apk add jq
|
||||
- ./scripts/codequality analyze -f json > raw_codeclimate.json || true
|
||||
# The following line keeps only the fields used in the MR widget, reducing the JSON artifact size
|
||||
- cat raw_codeclimate.json | docker run -i stedolan/jq -c 'map({check_name,description,fingerprint,location})' > codeclimate.json
|
||||
- jq -c 'map({check_name,description,fingerprint,location})' raw_codeclimate.json > codeclimate.json
|
||||
artifacts:
|
||||
paths: [codeclimate.json]
|
||||
expire_in: 1 week
|
||||
|
|
|
@ -196,6 +196,17 @@ release. There are two levels of priority labels:
|
|||
milestone. If these issues are not done in the current release, they will
|
||||
strongly be considered for the next release.
|
||||
|
||||
### Severity labels (~S1, ~S2, etc.)
|
||||
|
||||
Severity labels help us clearly communicate the impact of a ~bug on users.
|
||||
|
||||
| Label | Meaning | Example |
|
||||
|-------|------------------------------------------|---------|
|
||||
| ~S1 | Feature broken, no workaround | Unable to create an issue |
|
||||
| ~S2 | Feature broken, workaround unacceptable | Can push commits, but only via the command line |
|
||||
| ~S3 | Feature broken, workaround acceptable | Can create merge requests only from the Merge Requests page, not through the Issue |
|
||||
| ~S4 | Cosmetic issue | Label colors are incorrect / not being displayed |
|
||||
|
||||
### Label for community contributors (~"Accepting Merge Requests")
|
||||
|
||||
Issues that are beneficial to our users, 'nice to haves', that we currently do
|
||||
|
|
|
@ -1 +1 @@
|
|||
0.87.0
|
||||
0.88.0
|
||||
|
|
|
@ -1 +1 @@
|
|||
3.6.0
|
||||
3.8.0
|
||||
|
|
6
Gemfile
6
Gemfile
|
@ -411,7 +411,11 @@ group :ed25519 do
|
|||
end
|
||||
|
||||
# Gitaly GRPC client
|
||||
gem 'gitaly-proto', '~> 0.87.0', require: 'gitaly'
|
||||
gem 'gitaly-proto', '~> 0.88.0', require: 'gitaly'
|
||||
# Explicitly lock grpc as we know 1.9 is bad
|
||||
# 1.10 is still being tested. See gitlab-org/gitaly#1059
|
||||
gem 'grpc', '~> 1.8.3'
|
||||
|
||||
# Locked until https://github.com/google/protobuf/issues/4210 is closed
|
||||
gem 'google-protobuf', '= 3.5.1'
|
||||
|
||||
|
|
|
@ -285,7 +285,7 @@ GEM
|
|||
po_to_json (>= 1.0.0)
|
||||
rails (>= 3.2.0)
|
||||
gherkin-ruby (0.3.2)
|
||||
gitaly-proto (0.87.0)
|
||||
gitaly-proto (0.88.0)
|
||||
google-protobuf (~> 3.1)
|
||||
grpc (~> 1.0)
|
||||
github-linguist (5.3.3)
|
||||
|
@ -601,7 +601,7 @@ GEM
|
|||
atomic (>= 1.0.0)
|
||||
mysql2
|
||||
peek
|
||||
peek-performance_bar (1.3.0)
|
||||
peek-performance_bar (1.3.1)
|
||||
peek (>= 0.1.0)
|
||||
peek-pg (1.3.0)
|
||||
concurrent-ruby
|
||||
|
@ -1057,7 +1057,7 @@ DEPENDENCIES
|
|||
gettext (~> 3.2.2)
|
||||
gettext_i18n_rails (~> 1.8.0)
|
||||
gettext_i18n_rails_js (~> 1.2.0)
|
||||
gitaly-proto (~> 0.87.0)
|
||||
gitaly-proto (~> 0.88.0)
|
||||
github-linguist (~> 5.3.3)
|
||||
gitlab-flowdock-git-hook (~> 1.0.1)
|
||||
gitlab-markup (~> 1.6.2)
|
||||
|
@ -1073,6 +1073,7 @@ DEPENDENCIES
|
|||
grape-entity (~> 0.6.0)
|
||||
grape-route-helpers (~> 2.1.0)
|
||||
grape_logging (~> 1.7)
|
||||
grpc (~> 1.8.3)
|
||||
haml_lint (~> 0.26.0)
|
||||
hamlit (~> 2.6.1)
|
||||
hashie-forbidden_attributes
|
||||
|
|
|
@ -12,7 +12,7 @@ $(() => {
|
|||
const $container = $(container);
|
||||
|
||||
$container
|
||||
.find('.js-toggle-button .fa')
|
||||
.find('.js-toggle-button .fa-chevron-up, .js-toggle-button .fa-chevron-down')
|
||||
.toggleClass('fa-chevron-up', toggleState)
|
||||
.toggleClass('fa-chevron-down', toggleState !== undefined ? !toggleState : undefined);
|
||||
|
||||
|
@ -22,7 +22,7 @@ $(() => {
|
|||
}
|
||||
|
||||
$('body').on('click', '.js-toggle-button', function toggleButton(e) {
|
||||
e.target.classList.toggle('open');
|
||||
e.currentTarget.classList.toggle(e.currentTarget.dataset.toggleOpenClass || 'open');
|
||||
toggleContainer($(this).closest('.js-toggle-container'));
|
||||
|
||||
const targetTag = e.currentTarget.tagName.toLowerCase();
|
||||
|
|
|
@ -5,12 +5,12 @@ import Vue from 'vue';
|
|||
|
||||
import Flash from '~/flash';
|
||||
import { __ } from '~/locale';
|
||||
import '~/vue_shared/models/label';
|
||||
|
||||
import FilteredSearchBoards from './filtered_search_boards';
|
||||
import eventHub from './eventhub';
|
||||
import sidebarEventHub from '~/sidebar/event_hub'; // eslint-disable-line import/first
|
||||
import './models/issue';
|
||||
import './models/label';
|
||||
import './models/list';
|
||||
import './models/milestone';
|
||||
import './models/project';
|
||||
|
|
|
@ -20,10 +20,6 @@
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
emptyStateSvgPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
errorStateSvgPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
|
@ -45,23 +41,14 @@
|
|||
},
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Empty state is only rendered if after the first request we receive no pipelines.
|
||||
*
|
||||
* @return {Boolean}
|
||||
*/
|
||||
shouldRenderEmptyState() {
|
||||
return !this.state.pipelines.length &&
|
||||
!this.isLoading &&
|
||||
this.hasMadeRequest &&
|
||||
!this.hasError;
|
||||
},
|
||||
|
||||
shouldRenderTable() {
|
||||
return !this.isLoading &&
|
||||
this.state.pipelines.length > 0 &&
|
||||
!this.hasError;
|
||||
},
|
||||
shouldRenderErrorState() {
|
||||
return this.hasError && !this.isLoading;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.service = new PipelinesService(this.endpoint);
|
||||
|
@ -92,25 +79,22 @@
|
|||
<div class="content-list pipelines">
|
||||
|
||||
<loading-icon
|
||||
label="Loading pipelines"
|
||||
:label="s__('Pipelines|Loading Pipelines')"
|
||||
size="3"
|
||||
v-if="isLoading"
|
||||
class="prepend-top-20"
|
||||
/>
|
||||
|
||||
<empty-state
|
||||
v-if="shouldRenderEmptyState"
|
||||
:help-page-path="helpPagePath"
|
||||
:empty-state-svg-path="emptyStateSvgPath"
|
||||
/>
|
||||
|
||||
<error-state
|
||||
v-if="shouldRenderErrorState"
|
||||
:error-state-svg-path="errorStateSvgPath"
|
||||
<svg-blank-state
|
||||
v-else-if="shouldRenderErrorState"
|
||||
:svg-path="errorStateSvgPath"
|
||||
:message="s__(`Pipelines|There was an error fetching the pipelines.
|
||||
Try again in a few moments or contact your support team.`)"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="table-holder"
|
||||
v-if="shouldRenderTable"
|
||||
v-else-if="shouldRenderTable"
|
||||
>
|
||||
<pipelines-table-component
|
||||
:pipelines="state.pipelines"
|
||||
|
|
|
@ -16,6 +16,7 @@ export default class FilteredSearchDropdownManager {
|
|||
page,
|
||||
isGroup,
|
||||
isGroupAncestor,
|
||||
isGroupDecendent,
|
||||
filteredSearchTokenKeys,
|
||||
}) {
|
||||
this.container = FilteredSearchContainer.container;
|
||||
|
@ -26,6 +27,7 @@ export default class FilteredSearchDropdownManager {
|
|||
this.page = page;
|
||||
this.groupsOnly = isGroup;
|
||||
this.groupAncestor = isGroupAncestor;
|
||||
this.isGroupDecendent = isGroupDecendent;
|
||||
|
||||
this.setupMapping();
|
||||
|
||||
|
|
|
@ -22,11 +22,13 @@ export default class FilteredSearchManager {
|
|||
page,
|
||||
isGroup = false,
|
||||
isGroupAncestor = false,
|
||||
isGroupDecendent = false,
|
||||
filteredSearchTokenKeys = FilteredSearchTokenKeys,
|
||||
stateFiltersSelector = '.issues-state-filters',
|
||||
}) {
|
||||
this.isGroup = isGroup;
|
||||
this.isGroupAncestor = isGroupAncestor;
|
||||
this.isGroupDecendent = isGroupDecendent;
|
||||
this.states = ['opened', 'closed', 'merged', 'all'];
|
||||
|
||||
this.page = page;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import _ from 'underscore';
|
||||
import AjaxCache from '../lib/utils/ajax_cache';
|
||||
import AjaxCache from '~/lib/utils/ajax_cache';
|
||||
import { objectToQueryString } from '~/lib/utils/common_utils';
|
||||
import Flash from '../flash';
|
||||
import FilteredSearchContainer from './container';
|
||||
import UsersCache from '../lib/utils/users_cache';
|
||||
|
@ -16,6 +17,21 @@ export default class FilteredSearchVisualTokens {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a computed API endpoint
|
||||
* and query string composed of values from endpointQueryParams
|
||||
* @param {String} endpoint
|
||||
* @param {String} endpointQueryParams
|
||||
*/
|
||||
static getEndpointWithQueryParams(endpoint, endpointQueryParams) {
|
||||
if (!endpointQueryParams) {
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
const queryString = objectToQueryString(JSON.parse(endpointQueryParams));
|
||||
return `${endpoint}?${queryString}`;
|
||||
}
|
||||
|
||||
static unselectTokens() {
|
||||
const otherTokens = FilteredSearchContainer.container.querySelectorAll('.js-visual-token .selectable.selected');
|
||||
[].forEach.call(otherTokens, t => t.classList.remove('selected'));
|
||||
|
@ -86,7 +102,10 @@ export default class FilteredSearchVisualTokens {
|
|||
static updateLabelTokenColor(tokenValueContainer, tokenValue) {
|
||||
const filteredSearchInput = FilteredSearchContainer.container.querySelector('.filtered-search');
|
||||
const baseEndpoint = filteredSearchInput.dataset.baseEndpoint;
|
||||
const labelsEndpoint = `${baseEndpoint}/labels.json`;
|
||||
const labelsEndpoint = FilteredSearchVisualTokens.getEndpointWithQueryParams(
|
||||
`${baseEndpoint}/labels.json`,
|
||||
filteredSearchInput.dataset.endpointQueryParams,
|
||||
);
|
||||
|
||||
return AjaxCache.retrieve(labelsEndpoint)
|
||||
.then(FilteredSearchVisualTokens.preprocessLabel.bind(null, labelsEndpoint))
|
||||
|
|
|
@ -152,14 +152,14 @@ export default {
|
|||
showLeaveGroupModal(group, parentGroup) {
|
||||
this.targetGroup = group;
|
||||
this.targetParentGroup = parentGroup;
|
||||
this.updateModal = true;
|
||||
this.showModal = true;
|
||||
this.groupLeaveConfirmationMessage = s__(`GroupsTree|Are you sure you want to leave the "${group.fullName}" group?`);
|
||||
},
|
||||
hideLeaveGroupModal() {
|
||||
this.updateModal = false;
|
||||
this.showModal = false;
|
||||
},
|
||||
leaveGroup() {
|
||||
this.updateModal = false;
|
||||
this.showModal = false;
|
||||
this.targetGroup.isBeingRemoved = true;
|
||||
this.service.leaveGroup(this.targetGroup.leavePath)
|
||||
.then(res => res.json())
|
||||
|
@ -208,9 +208,9 @@ export default {
|
|||
:page-info="pageInfo"
|
||||
/>
|
||||
<modal
|
||||
v-show="showModal"
|
||||
:primary-button-label="__('Leave')"
|
||||
v-if="showModal"
|
||||
kind="warning"
|
||||
:primary-button-label="__('Leave')"
|
||||
:title="__('Are you sure?')"
|
||||
:text="groupLeaveConfirmationMessage"
|
||||
@cancel="hideLeaveGroupModal"
|
||||
|
|
|
@ -1,65 +0,0 @@
|
|||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import icon from '../../../vue_shared/components/icon.vue';
|
||||
import listItem from './list_item.vue';
|
||||
import listCollapsed from './list_collapsed.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
icon,
|
||||
listItem,
|
||||
listCollapsed,
|
||||
},
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
fileList: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState([
|
||||
'currentProjectId',
|
||||
'currentBranchId',
|
||||
'rightPanelCollapsed',
|
||||
]),
|
||||
},
|
||||
methods: {
|
||||
toggleCollapsed() {
|
||||
this.$emit('toggleCollapsed');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="multi-file-commit-list">
|
||||
<list-collapsed
|
||||
v-if="rightPanelCollapsed"
|
||||
/>
|
||||
<template v-else>
|
||||
<ul
|
||||
v-if="fileList.length"
|
||||
class="list-unstyled append-bottom-0"
|
||||
>
|
||||
<li
|
||||
v-for="file in fileList"
|
||||
:key="file.key"
|
||||
>
|
||||
<list-item
|
||||
:file="file"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
<div
|
||||
v-else
|
||||
class="help-block prepend-top-0"
|
||||
>
|
||||
No changes
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
|
@ -1,35 +0,0 @@
|
|||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import icon from '../../../vue_shared/components/icon.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
icon,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters([
|
||||
'addedFiles',
|
||||
'modifiedFiles',
|
||||
]),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="multi-file-commit-list-collapsed text-center"
|
||||
>
|
||||
<icon
|
||||
name="file-addition"
|
||||
:size="18"
|
||||
css-classes="multi-file-addition append-bottom-10"
|
||||
/>
|
||||
{{ addedFiles.length }}
|
||||
<icon
|
||||
name="file-modified"
|
||||
:size="18"
|
||||
css-classes="multi-file-modified prepend-top-10 append-bottom-10"
|
||||
/>
|
||||
{{ modifiedFiles.length }}
|
||||
</div>
|
||||
</template>
|
|
@ -1,36 +0,0 @@
|
|||
<script>
|
||||
import icon from '../../../vue_shared/components/icon.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
icon,
|
||||
},
|
||||
props: {
|
||||
file: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
iconName() {
|
||||
return this.file.tempFile ? 'file-addition' : 'file-modified';
|
||||
},
|
||||
iconClass() {
|
||||
return `multi-file-${this.file.tempFile ? 'addition' : 'modified'} append-right-8`;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="multi-file-commit-list-item">
|
||||
<icon
|
||||
:name="iconName"
|
||||
:size="16"
|
||||
:css-classes="iconClass"
|
||||
/>
|
||||
<span class="multi-file-commit-list-path">
|
||||
{{ file.path }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
|
@ -1,99 +0,0 @@
|
|||
<script>
|
||||
import { mapState, mapGetters } from 'vuex';
|
||||
import ideSidebar from './ide_side_bar.vue';
|
||||
import ideContextbar from './ide_context_bar.vue';
|
||||
import repoTabs from './repo_tabs.vue';
|
||||
import repoFileButtons from './repo_file_buttons.vue';
|
||||
import ideStatusBar from './ide_status_bar.vue';
|
||||
import repoPreview from './repo_preview.vue';
|
||||
import repoEditor from './repo_editor.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ideSidebar,
|
||||
ideContextbar,
|
||||
repoTabs,
|
||||
repoFileButtons,
|
||||
ideStatusBar,
|
||||
repoEditor,
|
||||
repoPreview,
|
||||
},
|
||||
props: {
|
||||
emptyStateSvgPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState([
|
||||
'currentBlobView',
|
||||
'selectedFile',
|
||||
]),
|
||||
...mapGetters([
|
||||
'changedFiles',
|
||||
'activeFile',
|
||||
]),
|
||||
},
|
||||
mounted() {
|
||||
const returnValue = 'Are you sure you want to lose unsaved changes?';
|
||||
window.onbeforeunload = (e) => {
|
||||
if (!this.changedFiles.length) return undefined;
|
||||
|
||||
Object.assign(e, {
|
||||
returnValue,
|
||||
});
|
||||
return returnValue;
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="ide-view"
|
||||
>
|
||||
<ide-sidebar />
|
||||
<div
|
||||
class="multi-file-edit-pane"
|
||||
>
|
||||
<template
|
||||
v-if="activeFile"
|
||||
>
|
||||
<repo-tabs/>
|
||||
<component
|
||||
class="multi-file-edit-pane-content"
|
||||
:is="currentBlobView"
|
||||
/>
|
||||
<repo-file-buttons />
|
||||
<ide-status-bar
|
||||
:file="selectedFile"
|
||||
/>
|
||||
</template>
|
||||
<template
|
||||
v-else
|
||||
>
|
||||
<div class="ide-empty-state">
|
||||
<div class="row js-empty-state">
|
||||
<div class="col-xs-12">
|
||||
<div class="svg-content svg-250">
|
||||
<img :src="emptyStateSvgPath" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-12">
|
||||
<div class="text-content text-center">
|
||||
<h4>
|
||||
Welcome to the GitLab IDE
|
||||
</h4>
|
||||
<p>
|
||||
You can select a file in the left sidebar to begin
|
||||
editing and use the right sidebar to commit your changes.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<ide-contextbar/>
|
||||
</div>
|
||||
</template>
|
|
@ -1,108 +0,0 @@
|
|||
<script>
|
||||
import { mapGetters, mapState, mapActions } from 'vuex';
|
||||
import icon from '~/vue_shared/components/icon.vue';
|
||||
import panelResizer from '~/vue_shared/components/panel_resizer.vue';
|
||||
import repoCommitSection from './repo_commit_section.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
repoCommitSection,
|
||||
icon,
|
||||
panelResizer,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
width: 290,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState([
|
||||
'rightPanelCollapsed',
|
||||
]),
|
||||
...mapGetters([
|
||||
'changedFiles',
|
||||
]),
|
||||
currentIcon() {
|
||||
return this.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right';
|
||||
},
|
||||
maxSize() {
|
||||
return window.innerWidth / 2;
|
||||
},
|
||||
panelStyle() {
|
||||
if (!this.rightPanelCollapsed) {
|
||||
return { width: `${this.width}px` };
|
||||
}
|
||||
return {};
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions([
|
||||
'setPanelCollapsedStatus',
|
||||
'setResizingStatus',
|
||||
]),
|
||||
toggleCollapsed() {
|
||||
this.setPanelCollapsedStatus({
|
||||
side: 'right',
|
||||
collapsed: !this.rightPanelCollapsed,
|
||||
});
|
||||
},
|
||||
resizingStarted() {
|
||||
this.setResizingStatus(true);
|
||||
},
|
||||
resizingEnded() {
|
||||
this.setResizingStatus(false);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="multi-file-commit-panel"
|
||||
:class="{
|
||||
'is-collapsed': rightPanelCollapsed,
|
||||
}"
|
||||
:style="panelStyle"
|
||||
>
|
||||
<div class="multi-file-commit-panel-section">
|
||||
<header
|
||||
class="multi-file-commit-panel-header"
|
||||
:class="{
|
||||
'is-collapsed': rightPanelCollapsed,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="multi-file-commit-panel-header-title"
|
||||
v-if="!rightPanelCollapsed"
|
||||
>
|
||||
<icon
|
||||
name="list-bulleted"
|
||||
:size="18"
|
||||
/>
|
||||
Staged
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-transparent multi-file-commit-panel-collapse-btn"
|
||||
@click="toggleCollapsed"
|
||||
>
|
||||
<icon
|
||||
:name="currentIcon"
|
||||
:size="18"
|
||||
/>
|
||||
</button>
|
||||
</header>
|
||||
<repo-commit-section />
|
||||
</div>
|
||||
<panel-resizer
|
||||
:size.sync="width"
|
||||
:enabled="!rightPanelCollapsed"
|
||||
:start-size="290"
|
||||
:min-size="200"
|
||||
:max-size="maxSize"
|
||||
@resize-start="resizingStarted"
|
||||
@resize-end="resizingEnded"
|
||||
side="left"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
|
@ -1,47 +0,0 @@
|
|||
<script>
|
||||
import icon from '~/vue_shared/components/icon.vue';
|
||||
import repoTree from './ide_repo_tree.vue';
|
||||
import newDropdown from './new_dropdown/index.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
repoTree,
|
||||
icon,
|
||||
newDropdown,
|
||||
},
|
||||
props: {
|
||||
projectId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
branch: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="branch-container">
|
||||
<div class="branch-header">
|
||||
<div class="branch-header-title">
|
||||
<icon
|
||||
name="branch"
|
||||
:size="12"
|
||||
/>
|
||||
{{ branch.name }}
|
||||
</div>
|
||||
<div class="branch-header-btns">
|
||||
<new-dropdown
|
||||
:project-id="projectId"
|
||||
:branch="branch.name"
|
||||
path=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<repo-tree :tree-id="branch.treeId" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -1,49 +0,0 @@
|
|||
<script>
|
||||
import projectAvatarImage from '~/vue_shared/components/project_avatar/image.vue';
|
||||
import branchesTree from './ide_project_branches_tree.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
branchesTree,
|
||||
projectAvatarImage,
|
||||
},
|
||||
props: {
|
||||
project: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="projects-sidebar">
|
||||
<div class="context-header">
|
||||
<a
|
||||
:title="project.name"
|
||||
:href="project.web_url"
|
||||
>
|
||||
<div class="avatar-container s40 project-avatar">
|
||||
<project-avatar-image
|
||||
class="avatar-container project-avatar"
|
||||
:link-href="project.path"
|
||||
:img-src="project.avatar_url"
|
||||
:img-alt="project.name"
|
||||
:img-size="40"
|
||||
/>
|
||||
</div>
|
||||
<div class="sidebar-context-title">
|
||||
{{ project.name }}
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="multi-file-commit-panel-inner-scroll">
|
||||
<branches-tree
|
||||
v-for="branch in project.branches"
|
||||
:key="branch.name"
|
||||
:project-id="project.path_with_namespace"
|
||||
:branch="branch"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -1,74 +0,0 @@
|
|||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
|
||||
import repoPreviousDirectory from './repo_prev_directory.vue';
|
||||
import repoFile from './repo_file.vue';
|
||||
import { treeList } from '../stores/utils';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
repoPreviousDirectory,
|
||||
repoFile,
|
||||
skeletonLoadingContainer,
|
||||
},
|
||||
props: {
|
||||
treeId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState([
|
||||
'trees',
|
||||
'isRoot',
|
||||
]),
|
||||
...mapState({
|
||||
projectName(state) {
|
||||
return state.project.name;
|
||||
},
|
||||
}),
|
||||
fetchedList() {
|
||||
return treeList(this.$store.state, this.treeId);
|
||||
},
|
||||
hasPreviousDirectory() {
|
||||
return !this.isRoot && this.fetchedList.length;
|
||||
},
|
||||
showLoading() {
|
||||
if (this.trees[this.treeId]) {
|
||||
return this.trees[this.treeId].loading;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="ide-file-list">
|
||||
<table class="table">
|
||||
<tbody
|
||||
v-if="treeId"
|
||||
>
|
||||
<repo-previous-directory
|
||||
v-if="hasPreviousDirectory"
|
||||
/>
|
||||
<template v-if="showLoading">
|
||||
<div
|
||||
class="multi-file-loading-container"
|
||||
v-for="n in 3"
|
||||
:key="n"
|
||||
>
|
||||
<skeleton-loading-container />
|
||||
</div>
|
||||
</template>
|
||||
<repo-file
|
||||
v-for="file in fetchedList"
|
||||
:key="file.key"
|
||||
:file="file"
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -1,114 +0,0 @@
|
|||
<script>
|
||||
import { mapState, mapActions } from 'vuex';
|
||||
import icon from '~/vue_shared/components/icon.vue';
|
||||
import panelResizer from '~/vue_shared/components/panel_resizer.vue';
|
||||
import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
|
||||
import projectTree from './ide_project_tree.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
projectTree,
|
||||
icon,
|
||||
panelResizer,
|
||||
skeletonLoadingContainer,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
width: 290,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState([
|
||||
'loading',
|
||||
'projects',
|
||||
'leftPanelCollapsed',
|
||||
]),
|
||||
currentIcon() {
|
||||
return this.leftPanelCollapsed ? 'angle-double-right' : 'angle-double-left';
|
||||
},
|
||||
maxSize() {
|
||||
return window.innerWidth / 2;
|
||||
},
|
||||
panelStyle() {
|
||||
if (!this.leftPanelCollapsed) {
|
||||
return { width: `${this.width}px` };
|
||||
}
|
||||
return {};
|
||||
},
|
||||
showLoading() {
|
||||
return this.loading;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions([
|
||||
'setPanelCollapsedStatus',
|
||||
'setResizingStatus',
|
||||
]),
|
||||
toggleCollapsed() {
|
||||
this.setPanelCollapsedStatus({
|
||||
side: 'left',
|
||||
collapsed: !this.leftPanelCollapsed,
|
||||
});
|
||||
},
|
||||
resizingStarted() {
|
||||
this.setResizingStatus(true);
|
||||
},
|
||||
resizingEnded() {
|
||||
this.setResizingStatus(false);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="multi-file-commit-panel"
|
||||
:class="{
|
||||
'is-collapsed': leftPanelCollapsed,
|
||||
}"
|
||||
:style="panelStyle"
|
||||
>
|
||||
<div class="multi-file-commit-panel-inner">
|
||||
<template v-if="showLoading">
|
||||
<div
|
||||
class="multi-file-loading-container"
|
||||
v-for="n in 3"
|
||||
:key="n"
|
||||
>
|
||||
<skeleton-loading-container />
|
||||
</div>
|
||||
</template>
|
||||
<project-tree
|
||||
v-for="project in projects"
|
||||
:key="project.id"
|
||||
:project="project"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-transparent left-collapse-btn"
|
||||
@click="toggleCollapsed"
|
||||
>
|
||||
<icon
|
||||
:name="currentIcon"
|
||||
:size="18"
|
||||
/>
|
||||
<span
|
||||
v-if="!leftPanelCollapsed"
|
||||
class="collapse-text"
|
||||
>
|
||||
Collapse sidebar
|
||||
</span>
|
||||
</button>
|
||||
<panel-resizer
|
||||
:size.sync="width"
|
||||
:enabled="!leftPanelCollapsed"
|
||||
:start-size="290"
|
||||
:min-size="200"
|
||||
:max-size="maxSize"
|
||||
@resize-start="resizingStarted"
|
||||
@resize-end="resizingEnded"
|
||||
side="right"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
|
@ -1,66 +0,0 @@
|
|||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import icon from '~/vue_shared/components/icon.vue';
|
||||
import tooltip from '~/vue_shared/directives/tooltip';
|
||||
import timeAgoMixin from '~/vue_shared/mixins/timeago';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
icon,
|
||||
},
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
mixins: [
|
||||
timeAgoMixin,
|
||||
],
|
||||
props: {
|
||||
file: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState([
|
||||
'selectedFile',
|
||||
]),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ide-status-bar">
|
||||
<div>
|
||||
<icon
|
||||
name="branch"
|
||||
:size="12"
|
||||
/>
|
||||
{{ selectedFile.branchId }}
|
||||
</div>
|
||||
<div>
|
||||
<div v-if="selectedFile.lastCommit && selectedFile.lastCommit.id">
|
||||
Last commit:
|
||||
<a
|
||||
v-tooltip
|
||||
:title="selectedFile.lastCommit.message"
|
||||
:href="selectedFile.lastCommit.url"
|
||||
>
|
||||
{{ timeFormated(selectedFile.lastCommit.updatedAt) }} by
|
||||
{{ selectedFile.lastCommit.author }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
{{ selectedFile.name }}
|
||||
</div>
|
||||
<div class="text-right">
|
||||
{{ selectedFile.eol }}
|
||||
</div>
|
||||
<div class="text-right">
|
||||
{{ file.editorRow }}:{{ file.editorColumn }}
|
||||
</div>
|
||||
<div class="text-right">
|
||||
{{ selectedFile.fileLanguage }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -1,108 +0,0 @@
|
|||
<script>
|
||||
import { mapState, mapActions } from 'vuex';
|
||||
import flash, { hideFlash } from '~/flash';
|
||||
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
loadingIcon,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
branchName: '',
|
||||
loading: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState([
|
||||
'currentBranch',
|
||||
]),
|
||||
btnDisabled() {
|
||||
return this.loading || this.branchName === '';
|
||||
},
|
||||
},
|
||||
created() {
|
||||
// Dropdown is outside of Vue instance & is controlled by Bootstrap
|
||||
this.$dropdown = $('.git-revision-dropdown');
|
||||
|
||||
// text element is outside Vue app
|
||||
this.dropdownText = document.querySelector('.project-refs-form .dropdown-toggle-text');
|
||||
},
|
||||
methods: {
|
||||
...mapActions([
|
||||
'createNewBranch',
|
||||
]),
|
||||
toggleDropdown() {
|
||||
this.$dropdown.dropdown('toggle');
|
||||
},
|
||||
submitNewBranch() {
|
||||
// need to query as the element is appended outside of Vue
|
||||
const flashEl = this.$refs.flashContainer.querySelector('.flash-alert');
|
||||
|
||||
this.loading = true;
|
||||
|
||||
if (flashEl) {
|
||||
hideFlash(flashEl, false);
|
||||
}
|
||||
|
||||
this.createNewBranch(this.branchName)
|
||||
.then(() => {
|
||||
this.loading = false;
|
||||
this.branchName = '';
|
||||
|
||||
if (this.dropdownText) {
|
||||
this.dropdownText.textContent = this.currentBranchId;
|
||||
}
|
||||
|
||||
this.toggleDropdown();
|
||||
})
|
||||
.catch(res => res.json().then((data) => {
|
||||
this.loading = false;
|
||||
flash(data.message, 'alert', this.$el);
|
||||
}));
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
class="flash-container"
|
||||
ref="flashContainer"
|
||||
>
|
||||
</div>
|
||||
<p>
|
||||
Create from:
|
||||
<code>{{ currentBranch }}</code>
|
||||
</p>
|
||||
<input
|
||||
class="form-control js-new-branch-name"
|
||||
type="text"
|
||||
placeholder="Name new branch"
|
||||
v-model="branchName"
|
||||
@keyup.enter.stop.prevent="submitNewBranch"
|
||||
/>
|
||||
<div class="prepend-top-default clearfix">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary pull-left"
|
||||
:disabled="btnDisabled"
|
||||
@click.stop.prevent="submitNewBranch"
|
||||
>
|
||||
<loading-icon
|
||||
v-if="loading"
|
||||
:inline="true"
|
||||
/>
|
||||
<span>Create</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-default pull-right"
|
||||
@click.stop.prevent="toggleDropdown"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -1,101 +0,0 @@
|
|||
<script>
|
||||
import newModal from './modal.vue';
|
||||
import upload from './upload.vue';
|
||||
import icon from '../../../vue_shared/components/icon.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
icon,
|
||||
newModal,
|
||||
upload,
|
||||
},
|
||||
props: {
|
||||
branch: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
path: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
parent: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
openModal: false,
|
||||
modalType: '',
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
createNewItem(type) {
|
||||
this.modalType = type;
|
||||
this.openModal = true;
|
||||
},
|
||||
hideModal() {
|
||||
this.openModal = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="repo-new-btn pull-right">
|
||||
<div class="dropdown">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-default dropdown-toggle add-to-tree"
|
||||
data-toggle="dropdown"
|
||||
aria-label="Create new file or directory"
|
||||
>
|
||||
<icon
|
||||
name="plus"
|
||||
:size="12"
|
||||
css-classes="pull-left"
|
||||
/>
|
||||
<icon
|
||||
name="arrow-down"
|
||||
:size="12"
|
||||
css-classes="pull-left"
|
||||
/>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right">
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
role="button"
|
||||
@click.prevent="createNewItem('blob')"
|
||||
>
|
||||
{{ __('New file') }}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<upload
|
||||
:branch-id="branch"
|
||||
:path="path"
|
||||
:parent="parent"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
role="button"
|
||||
@click.prevent="createNewItem('tree')"
|
||||
>
|
||||
{{ __('New directory') }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<new-modal
|
||||
v-if="openModal"
|
||||
:type="modalType"
|
||||
:branch-id="branch"
|
||||
:path="path"
|
||||
:parent="parent"
|
||||
@hide="hideModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
|
@ -1,112 +0,0 @@
|
|||
<script>
|
||||
import { mapActions, mapState } from 'vuex';
|
||||
import { __ } from '../../../locale';
|
||||
import modal from '../../../vue_shared/components/modal.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
modal,
|
||||
},
|
||||
props: {
|
||||
branchId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
parent: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
path: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
entryName: this.path !== '' ? `${this.path}/` : '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState([
|
||||
'currentProjectId',
|
||||
]),
|
||||
modalTitle() {
|
||||
if (this.type === 'tree') {
|
||||
return __('Create new directory');
|
||||
}
|
||||
|
||||
return __('Create new file');
|
||||
},
|
||||
buttonLabel() {
|
||||
if (this.type === 'tree') {
|
||||
return __('Create directory');
|
||||
}
|
||||
|
||||
return __('Create file');
|
||||
},
|
||||
formLabelName() {
|
||||
if (this.type === 'tree') {
|
||||
return __('Directory name');
|
||||
}
|
||||
|
||||
return __('File name');
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$refs.fieldName.focus();
|
||||
},
|
||||
methods: {
|
||||
...mapActions([
|
||||
'createTempEntry',
|
||||
]),
|
||||
createEntryInStore() {
|
||||
this.createTempEntry({
|
||||
projectId: this.currentProjectId,
|
||||
branchId: this.branchId,
|
||||
parent: this.parent,
|
||||
name: this.entryName.replace(new RegExp(`^${this.path}/`), ''),
|
||||
type: this.type,
|
||||
});
|
||||
|
||||
this.hideModal();
|
||||
},
|
||||
hideModal() {
|
||||
this.$emit('hide');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<modal
|
||||
:title="modalTitle"
|
||||
:primary-button-label="buttonLabel"
|
||||
kind="success"
|
||||
@cancel="hideModal"
|
||||
@submit="createEntryInStore"
|
||||
>
|
||||
<form
|
||||
class="form-horizontal"
|
||||
slot="body"
|
||||
@submit.prevent="createEntryInStore"
|
||||
>
|
||||
<fieldset class="form-group append-bottom-0">
|
||||
<label class="label-light col-sm-3">
|
||||
{{ formLabelName }}
|
||||
</label>
|
||||
<div class="col-sm-9">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
v-model="entryName"
|
||||
ref="fieldName"
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</modal>
|
||||
</template>
|
|
@ -1,87 +0,0 @@
|
|||
<script>
|
||||
import { mapActions, mapState } from 'vuex';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
branchId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
parent: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState([
|
||||
'trees',
|
||||
'currentProjectId',
|
||||
]),
|
||||
},
|
||||
mounted() {
|
||||
this.$refs.fileUpload.addEventListener('change', this.openFile);
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.$refs.fileUpload.removeEventListener('change', this.openFile);
|
||||
},
|
||||
methods: {
|
||||
...mapActions([
|
||||
'createTempEntry',
|
||||
]),
|
||||
createFile(target, file, isText) {
|
||||
const { name } = file;
|
||||
let { result } = target;
|
||||
|
||||
if (!isText) {
|
||||
result = result.split('base64,')[1];
|
||||
}
|
||||
|
||||
this.createTempEntry({
|
||||
name,
|
||||
projectId: this.currentProjectId,
|
||||
branchId: this.branchId,
|
||||
parent: this.parent,
|
||||
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>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<a
|
||||
href="#"
|
||||
role="button"
|
||||
@click.prevent="startFileUpload"
|
||||
>
|
||||
{{ __('Upload file') }}
|
||||
</a>
|
||||
<input
|
||||
id="file-upload"
|
||||
type="file"
|
||||
class="hidden"
|
||||
ref="fileUpload"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
|
@ -1,171 +0,0 @@
|
|||
<script>
|
||||
import { mapGetters, mapState, mapActions } from 'vuex';
|
||||
import tooltip from '~/vue_shared/directives/tooltip';
|
||||
import icon from '~/vue_shared/components/icon.vue';
|
||||
import modal from '~/vue_shared/components/modal.vue';
|
||||
import commitFilesList from './commit_sidebar/list.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
modal,
|
||||
icon,
|
||||
commitFilesList,
|
||||
},
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showNewBranchModal: false,
|
||||
submitCommitsLoading: false,
|
||||
startNewMR: false,
|
||||
commitMessage: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState([
|
||||
'currentProjectId',
|
||||
'currentBranchId',
|
||||
'rightPanelCollapsed',
|
||||
]),
|
||||
...mapGetters([
|
||||
'changedFiles',
|
||||
]),
|
||||
commitButtonDisabled() {
|
||||
return this.commitMessage === '' || this.submitCommitsLoading || !this.changedFiles.length;
|
||||
},
|
||||
commitMessageCount() {
|
||||
return this.commitMessage.length;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions([
|
||||
'checkCommitStatus',
|
||||
'commitChanges',
|
||||
'getTreeData',
|
||||
'setPanelCollapsedStatus',
|
||||
]),
|
||||
makeCommit(newBranch = false) {
|
||||
const createNewBranch = newBranch || this.startNewMR;
|
||||
|
||||
const payload = {
|
||||
branch: createNewBranch ?
|
||||
`${this.currentBranchId}-${new Date().getTime().toString()}` :
|
||||
this.currentBranchId,
|
||||
commit_message: this.commitMessage,
|
||||
actions: this.changedFiles.map(f => ({
|
||||
action: f.tempFile ? 'create' : 'update',
|
||||
file_path: f.path,
|
||||
content: f.content,
|
||||
encoding: f.base64 ? 'base64' : 'text',
|
||||
})),
|
||||
start_branch: createNewBranch ? this.currentBranchId : undefined,
|
||||
};
|
||||
|
||||
this.showNewBranchModal = false;
|
||||
this.submitCommitsLoading = true;
|
||||
|
||||
this.commitChanges({ payload, newMr: this.startNewMR })
|
||||
.then(() => {
|
||||
this.submitCommitsLoading = false;
|
||||
this.commitMessage = '';
|
||||
this.startNewMR = false;
|
||||
})
|
||||
.catch(() => {
|
||||
this.submitCommitsLoading = false;
|
||||
});
|
||||
},
|
||||
tryCommit() {
|
||||
this.submitCommitsLoading = true;
|
||||
|
||||
this.checkCommitStatus()
|
||||
.then((branchChanged) => {
|
||||
if (branchChanged) {
|
||||
this.showNewBranchModal = true;
|
||||
} else {
|
||||
this.makeCommit();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
this.submitCommitsLoading = false;
|
||||
});
|
||||
},
|
||||
toggleCollapsed() {
|
||||
this.setPanelCollapsedStatus({
|
||||
side: 'right',
|
||||
collapsed: !this.rightPanelCollapsed,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="multi-file-commit-panel-section">
|
||||
<modal
|
||||
v-if="showNewBranchModal"
|
||||
:primary-button-label="__('Create new branch')"
|
||||
kind="primary"
|
||||
:title="__('Branch has changed')"
|
||||
:text="__(`This branch has changed since
|
||||
you started editing. Would you like to create a new branch?`)"
|
||||
@cancel="showNewBranchModal = false"
|
||||
@submit="makeCommit(true)"
|
||||
/>
|
||||
<commit-files-list
|
||||
title="Staged"
|
||||
:file-list="changedFiles"
|
||||
:collapsed="rightPanelCollapsed"
|
||||
@toggleCollapsed="toggleCollapsed"
|
||||
/>
|
||||
<form
|
||||
class="form-horizontal multi-file-commit-form"
|
||||
@submit.prevent="tryCommit"
|
||||
v-if="!rightPanelCollapsed"
|
||||
>
|
||||
<div class="multi-file-commit-fieldset">
|
||||
<textarea
|
||||
class="form-control multi-file-commit-message"
|
||||
name="commit-message"
|
||||
v-model="commitMessage"
|
||||
placeholder="Commit message"
|
||||
>
|
||||
</textarea>
|
||||
</div>
|
||||
<div class="multi-file-commit-fieldset">
|
||||
<label
|
||||
v-tooltip
|
||||
title="Create a new merge request with these changes"
|
||||
data-container="body"
|
||||
data-placement="top"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="startNewMR"
|
||||
/>
|
||||
Merge Request
|
||||
</label>
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="commitButtonDisabled"
|
||||
class="btn btn-default btn-sm append-right-10 prepend-left-10"
|
||||
:class="{ disabled: submitCommitsLoading }"
|
||||
>
|
||||
<i
|
||||
v-if="submitCommitsLoading"
|
||||
class="js-commit-loading-icon fa fa-spinner fa-spin"
|
||||
aria-hidden="true"
|
||||
aria-label="loading"
|
||||
>
|
||||
</i>
|
||||
Commit
|
||||
</button>
|
||||
<div
|
||||
class="multi-file-commit-message-count"
|
||||
>
|
||||
{{ commitMessageCount }}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
|
@ -1,57 +0,0 @@
|
|||
<script>
|
||||
import { mapGetters, mapActions, mapState } from 'vuex';
|
||||
import modal from '~/vue_shared/components/modal.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
modal,
|
||||
},
|
||||
computed: {
|
||||
...mapState([
|
||||
'editMode',
|
||||
'discardPopupOpen',
|
||||
]),
|
||||
...mapGetters([
|
||||
'canEditFile',
|
||||
]),
|
||||
buttonLabel() {
|
||||
return this.editMode ? this.__('Cancel edit') : this.__('Edit');
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions([
|
||||
'toggleEditMode',
|
||||
'closeDiscardPopup',
|
||||
]),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="editable-mode">
|
||||
<button
|
||||
v-if="canEditFile"
|
||||
class="btn btn-default"
|
||||
type="button"
|
||||
@click.prevent="toggleEditMode()">
|
||||
<i
|
||||
v-if="!editMode"
|
||||
class="fa fa-pencil"
|
||||
aria-hidden="true">
|
||||
</i>
|
||||
<span>
|
||||
{{ buttonLabel }}
|
||||
</span>
|
||||
</button>
|
||||
<modal
|
||||
v-if="discardPopupOpen"
|
||||
class="text-left"
|
||||
:primary-button-label="__('Discard changes')"
|
||||
kind="warning"
|
||||
:title="__('Are you sure?')"
|
||||
:text="__('Are you sure you want to discard your changes?')"
|
||||
@cancel="closeDiscardPopup"
|
||||
@submit="toggleEditMode(true)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
|
@ -1,136 +0,0 @@
|
|||
<script>
|
||||
/* global monaco */
|
||||
import { mapState, mapGetters, mapActions } from 'vuex';
|
||||
import flash from '~/flash';
|
||||
import monacoLoader from '../monaco_loader';
|
||||
import Editor from '../lib/editor';
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
...mapGetters([
|
||||
'activeFile',
|
||||
'activeFileExtension',
|
||||
]),
|
||||
...mapState([
|
||||
'leftPanelCollapsed',
|
||||
'rightPanelCollapsed',
|
||||
'panelResizing',
|
||||
]),
|
||||
shouldHideEditor() {
|
||||
return this.activeFile.binary && !this.activeFile.raw;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
activeFile(oldVal, newVal) {
|
||||
if (newVal && !newVal.active) {
|
||||
this.initMonaco();
|
||||
}
|
||||
},
|
||||
leftPanelCollapsed() {
|
||||
this.editor.updateDimensions();
|
||||
},
|
||||
rightPanelCollapsed() {
|
||||
this.editor.updateDimensions();
|
||||
},
|
||||
panelResizing(isResizing) {
|
||||
if (isResizing === false) {
|
||||
this.editor.updateDimensions();
|
||||
}
|
||||
},
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.editor.dispose();
|
||||
},
|
||||
mounted() {
|
||||
if (this.editor && monaco) {
|
||||
this.initMonaco();
|
||||
} else {
|
||||
monacoLoader(['vs/editor/editor.main'], () => {
|
||||
this.editor = Editor.create(monaco);
|
||||
|
||||
this.initMonaco();
|
||||
});
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions([
|
||||
'getRawFileData',
|
||||
'changeFileContent',
|
||||
'setFileLanguage',
|
||||
'setEditorPosition',
|
||||
'setFileEOL',
|
||||
]),
|
||||
initMonaco() {
|
||||
if (this.shouldHideEditor) return;
|
||||
|
||||
this.editor.clearEditor();
|
||||
|
||||
this.getRawFileData(this.activeFile)
|
||||
.then(() => {
|
||||
this.editor.createInstance(this.$refs.editor);
|
||||
})
|
||||
.then(() => this.setupEditor())
|
||||
.catch((err) => {
|
||||
flash('Error setting up monaco. Please try again.', 'alert', document, null, false, true);
|
||||
throw err;
|
||||
});
|
||||
},
|
||||
setupEditor() {
|
||||
if (!this.activeFile) return;
|
||||
|
||||
const model = this.editor.createModel(this.activeFile);
|
||||
|
||||
this.editor.attachModel(model);
|
||||
|
||||
model.onChange((m) => {
|
||||
this.changeFileContent({
|
||||
file: this.activeFile,
|
||||
content: m.getValue(),
|
||||
});
|
||||
});
|
||||
|
||||
// Handle Cursor Position
|
||||
this.editor.onPositionChange((instance, e) => {
|
||||
this.setEditorPosition({
|
||||
editorRow: e.position.lineNumber,
|
||||
editorColumn: e.position.column,
|
||||
});
|
||||
});
|
||||
|
||||
this.editor.setPosition({
|
||||
lineNumber: this.activeFile.editorRow,
|
||||
column: this.activeFile.editorColumn,
|
||||
});
|
||||
|
||||
// Handle File Language
|
||||
this.setFileLanguage({
|
||||
fileLanguage: model.language,
|
||||
});
|
||||
|
||||
// Get File eol
|
||||
this.setFileEOL({
|
||||
eol: model.eol,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
id="ide"
|
||||
class="blob-viewer-container blob-editor-container"
|
||||
>
|
||||
<div
|
||||
v-if="shouldHideEditor"
|
||||
v-html="activeFile.html"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
v-show="!shouldHideEditor"
|
||||
ref="editor"
|
||||
class="multi-file-editor-holder"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -1,165 +0,0 @@
|
|||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import timeAgoMixin from '~/vue_shared/mixins/timeago';
|
||||
import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
|
||||
import fileIcon from '~/vue_shared/components/file_icon.vue';
|
||||
import newDropdown from './new_dropdown/index.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
skeletonLoadingContainer,
|
||||
newDropdown,
|
||||
fileIcon,
|
||||
},
|
||||
mixins: [
|
||||
timeAgoMixin,
|
||||
],
|
||||
props: {
|
||||
file: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
showExtraColumns: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState([
|
||||
'leftPanelCollapsed',
|
||||
]),
|
||||
isSubmodule() {
|
||||
return this.file.type === 'submodule';
|
||||
},
|
||||
isTree() {
|
||||
return this.file.type === 'tree';
|
||||
},
|
||||
levelIndentation() {
|
||||
if (this.file.level > 0) {
|
||||
return {
|
||||
marginLeft: `${this.file.level * 16}px`,
|
||||
};
|
||||
}
|
||||
return {};
|
||||
},
|
||||
shortId() {
|
||||
return this.file.id.substr(0, 8);
|
||||
},
|
||||
submoduleColSpan() {
|
||||
return !this.leftPanelCollapsed && this.isSubmodule ? 3 : 1;
|
||||
},
|
||||
fileClass() {
|
||||
if (this.file.type === 'blob') {
|
||||
if (this.file.active) {
|
||||
return 'file-open file-active';
|
||||
}
|
||||
return this.file.opened ? 'file-open' : '';
|
||||
}
|
||||
return '';
|
||||
},
|
||||
changedClass() {
|
||||
return {
|
||||
'fa-circle unsaved-icon': this.file.changed || this.file.tempFile,
|
||||
};
|
||||
},
|
||||
},
|
||||
updated() {
|
||||
if (this.file.type === 'blob' && this.file.active) {
|
||||
this.$el.scrollIntoView();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
clickFile(row) {
|
||||
// Manual Action if a tree is selected/opened
|
||||
if (this.file.type === 'tree' && this.$router.currentRoute.path === `/project${row.url}`) {
|
||||
this.$store.dispatch('toggleTreeOpen', {
|
||||
endpoint: this.file.url,
|
||||
tree: this.file,
|
||||
});
|
||||
}
|
||||
this.$router.push(`/project${row.url}`);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<tr
|
||||
class="file"
|
||||
:class="fileClass"
|
||||
@click="clickFile(file)">
|
||||
<td
|
||||
class="multi-file-table-name"
|
||||
:colspan="submoduleColSpan"
|
||||
>
|
||||
<a
|
||||
class="repo-file-name"
|
||||
>
|
||||
<file-icon
|
||||
:file-name="file.name"
|
||||
:loading="file.loading"
|
||||
:folder="file.type === 'tree'"
|
||||
:opened="file.opened"
|
||||
:style="levelIndentation"
|
||||
:size="16"
|
||||
/>
|
||||
{{ file.name }}
|
||||
</a>
|
||||
<new-dropdown
|
||||
v-if="isTree"
|
||||
:project-id="file.projectId"
|
||||
:branch="file.branchId"
|
||||
:path="file.path"
|
||||
:parent="file"
|
||||
/>
|
||||
<i
|
||||
class="fa"
|
||||
v-if="file.changed || file.tempFile"
|
||||
:class="changedClass"
|
||||
aria-hidden="true"
|
||||
>
|
||||
</i>
|
||||
<template v-if="isSubmodule && file.id">
|
||||
@
|
||||
<span class="commit-sha">
|
||||
<a
|
||||
@click.stop
|
||||
:href="file.tree_url"
|
||||
>
|
||||
{{ shortId }}
|
||||
</a>
|
||||
</span>
|
||||
</template>
|
||||
</td>
|
||||
|
||||
<template v-if="showExtraColumns && !isSubmodule">
|
||||
<td class="multi-file-table-col-commit-message hidden-sm hidden-xs">
|
||||
<a
|
||||
v-if="file.lastCommit.message"
|
||||
@click.stop
|
||||
:href="file.lastCommit.url"
|
||||
>
|
||||
{{ file.lastCommit.message }}
|
||||
</a>
|
||||
<skeleton-loading-container
|
||||
v-else
|
||||
:small="true"
|
||||
/>
|
||||
</td>
|
||||
|
||||
<td class="commit-update hidden-xs text-right">
|
||||
<span
|
||||
v-if="file.lastCommit.updatedAt"
|
||||
:title="tooltipTitle(file.lastCommit.updatedAt)"
|
||||
>
|
||||
{{ timeFormated(file.lastCommit.updatedAt) }}
|
||||
</span>
|
||||
<skeleton-loading-container
|
||||
v-else
|
||||
class="animation-container-right"
|
||||
:small="true"
|
||||
/>
|
||||
</td>
|
||||
</template>
|
||||
</tr>
|
||||
</template>
|
|
@ -1,60 +0,0 @@
|
|||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
...mapGetters([
|
||||
'activeFile',
|
||||
]),
|
||||
showButtons() {
|
||||
return this.activeFile.rawPath ||
|
||||
this.activeFile.blamePath ||
|
||||
this.activeFile.commitsPath ||
|
||||
this.activeFile.permalink;
|
||||
},
|
||||
rawDownloadButtonLabel() {
|
||||
return this.activeFile.binary ? 'Download' : 'Raw';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="showButtons"
|
||||
class="multi-file-editor-btn-group"
|
||||
>
|
||||
<a
|
||||
:href="activeFile.rawPath"
|
||||
target="_blank"
|
||||
class="btn btn-default btn-sm raw"
|
||||
rel="noopener noreferrer">
|
||||
{{ rawDownloadButtonLabel }}
|
||||
</a>
|
||||
|
||||
<div
|
||||
class="btn-group"
|
||||
role="group"
|
||||
aria-label="File actions"
|
||||
>
|
||||
<a
|
||||
:href="activeFile.blamePath"
|
||||
class="btn btn-default btn-sm blame"
|
||||
>
|
||||
Blame
|
||||
</a>
|
||||
<a
|
||||
:href="activeFile.commitsPath"
|
||||
class="btn btn-default btn-sm history"
|
||||
>
|
||||
History
|
||||
</a>
|
||||
<a
|
||||
:href="activeFile.permalink"
|
||||
class="btn btn-default btn-sm permalink"
|
||||
>
|
||||
Permalink
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -1,42 +0,0 @@
|
|||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
skeletonLoadingContainer,
|
||||
},
|
||||
computed: {
|
||||
...mapState([
|
||||
'leftPanelCollapsed',
|
||||
]),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<tr
|
||||
class="loading-file"
|
||||
aria-label="Loading files"
|
||||
>
|
||||
<td class="multi-file-table-col-name">
|
||||
<skeleton-loading-container
|
||||
:small="true"
|
||||
/>
|
||||
</td>
|
||||
<template v-if="!leftPanelCollapsed">
|
||||
<td class="hidden-sm hidden-xs">
|
||||
<skeleton-loading-container
|
||||
:small="true"
|
||||
/>
|
||||
</td>
|
||||
|
||||
<td class="hidden-xs">
|
||||
<skeleton-loading-container
|
||||
class="animation-container-right"
|
||||
:small="true"
|
||||
/>
|
||||
</td>
|
||||
</template>
|
||||
</tr>
|
||||
</template>
|
|
@ -1,32 +0,0 @@
|
|||
<script>
|
||||
import { mapState, mapActions } from 'vuex';
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
...mapState([
|
||||
'parentTreeUrl',
|
||||
'leftPanelCollapsed',
|
||||
]),
|
||||
colSpanCondition() {
|
||||
return this.leftPanelCollapsed ? undefined : 3;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions([
|
||||
'getTreeData',
|
||||
]),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<tr class="file prev-directory">
|
||||
<td
|
||||
:colspan="colSpanCondition"
|
||||
class="table-cell"
|
||||
@click.prevent="getTreeData({ endpoint: parentTreeUrl })"
|
||||
>
|
||||
<a :href="parentTreeUrl">...</a>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
|
@ -1,71 +0,0 @@
|
|||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import LineHighlighter from '~/line_highlighter';
|
||||
import syntaxHighlight from '~/syntax_highlight';
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
...mapGetters([
|
||||
'activeFile',
|
||||
]),
|
||||
renderErrorTooLarge() {
|
||||
return this.activeFile.renderError === 'too_large';
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.highlightFile();
|
||||
this.lineHighlighter = new LineHighlighter({
|
||||
fileHolderSelector: '.blob-viewer-container',
|
||||
scrollFileHolder: true,
|
||||
});
|
||||
},
|
||||
updated() {
|
||||
this.$nextTick(() => {
|
||||
this.highlightFile();
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
highlightFile() {
|
||||
syntaxHighlight($(this.$el).find('.file-content'));
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
v-if="!activeFile.renderError"
|
||||
v-html="activeFile.html"
|
||||
class="multi-file-preview-holder"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="activeFile.tempFile"
|
||||
class="vertical-center render-error">
|
||||
<p class="text-center">
|
||||
The source could not be displayed for this temporary file.
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="renderErrorTooLarge"
|
||||
class="vertical-center render-error">
|
||||
<p class="text-center">
|
||||
The source could not be displayed because it is too large.
|
||||
You can <a
|
||||
:href="activeFile.rawPath"
|
||||
download>download</a> it instead.
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="vertical-center render-error">
|
||||
<p class="text-center">
|
||||
The source could not be displayed because a rendering error occurred.
|
||||
You can <a
|
||||
:href="activeFile.rawPath"
|
||||
download>download</a> it instead.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -1,74 +0,0 @@
|
|||
<script>
|
||||
import { mapActions } from 'vuex';
|
||||
import fileIcon from '~/vue_shared/components/file_icon.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
fileIcon,
|
||||
},
|
||||
props: {
|
||||
tab: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
closeLabel() {
|
||||
if (this.tab.changed || this.tab.tempFile) {
|
||||
return `${this.tab.name} changed`;
|
||||
}
|
||||
return `Close ${this.tab.name}`;
|
||||
},
|
||||
changedClass() {
|
||||
const tabChangedObj = {
|
||||
'fa-times close-icon': !this.tab.changed && !this.tab.tempFile,
|
||||
'fa-circle unsaved-icon': this.tab.changed || this.tab.tempFile,
|
||||
};
|
||||
return tabChangedObj;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
...mapActions([
|
||||
'closeFile',
|
||||
]),
|
||||
clickFile(tab) {
|
||||
this.$router.push(`/project${tab.url}`);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<li @click="clickFile(tab)">
|
||||
<button
|
||||
type="button"
|
||||
class="multi-file-tab-close"
|
||||
@click.stop.prevent="closeFile({ file: tab })"
|
||||
:aria-label="closeLabel"
|
||||
:class="{
|
||||
'modified': tab.changed,
|
||||
}"
|
||||
:disabled="tab.changed"
|
||||
>
|
||||
<i
|
||||
class="fa"
|
||||
:class="changedClass"
|
||||
aria-hidden="true"
|
||||
>
|
||||
</i>
|
||||
</button>
|
||||
|
||||
<div
|
||||
class="multi-file-tab"
|
||||
:class="{active : tab.active }"
|
||||
:title="tab.url"
|
||||
>
|
||||
<file-icon
|
||||
:file-name="tab.name"
|
||||
:size="16"
|
||||
/>
|
||||
{{ tab.name }}
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
|
@ -1,27 +0,0 @@
|
|||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import RepoTab from './repo_tab.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
'repo-tab': RepoTab,
|
||||
},
|
||||
computed: {
|
||||
...mapState([
|
||||
'openFiles',
|
||||
]),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ul
|
||||
class="multi-file-tabs list-unstyled append-bottom-0"
|
||||
>
|
||||
<repo-tab
|
||||
v-for="tab in openFiles"
|
||||
:key="tab.key"
|
||||
:tab="tab"
|
||||
/>
|
||||
</ul>
|
||||
</template>
|
|
@ -1,101 +0,0 @@
|
|||
import Vue from 'vue';
|
||||
import VueRouter from 'vue-router';
|
||||
import store from './stores';
|
||||
import flash from '../flash';
|
||||
import {
|
||||
getTreeEntry,
|
||||
} from './stores/utils';
|
||||
|
||||
Vue.use(VueRouter);
|
||||
|
||||
/**
|
||||
* Routes below /-/ide/:
|
||||
|
||||
/project/h5bp/html5-boilerplate/blob/master
|
||||
/project/h5bp/html5-boilerplate/blob/master/app/js/test.js
|
||||
|
||||
/project/h5bp/html5-boilerplate/mr/123
|
||||
/project/h5bp/html5-boilerplate/mr/123/app/js/test.js
|
||||
|
||||
/workspace/123
|
||||
/workspace/project/h5bp/html5-boilerplate/blob/my-special-branch
|
||||
/workspace/project/h5bp/html5-boilerplate/mr/123
|
||||
|
||||
/ = /workspace
|
||||
|
||||
/settings
|
||||
*/
|
||||
|
||||
// Unfortunately Vue Router doesn't work without at least a fake component
|
||||
// If you do only data handling
|
||||
const EmptyRouterComponent = {
|
||||
render(createElement) {
|
||||
return createElement('div');
|
||||
},
|
||||
};
|
||||
|
||||
const router = new VueRouter({
|
||||
mode: 'history',
|
||||
base: `${gon.relative_url_root}/-/ide/`,
|
||||
routes: [
|
||||
{
|
||||
path: '/project/:namespace/:project',
|
||||
component: EmptyRouterComponent,
|
||||
children: [
|
||||
{
|
||||
path: ':targetmode/:branch/*',
|
||||
component: EmptyRouterComponent,
|
||||
},
|
||||
{
|
||||
path: 'mr/:mrid',
|
||||
component: EmptyRouterComponent,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
if (to.params.namespace && to.params.project) {
|
||||
store.dispatch('getProjectData', {
|
||||
namespace: to.params.namespace,
|
||||
projectId: to.params.project,
|
||||
})
|
||||
.then(() => {
|
||||
const fullProjectId = `${to.params.namespace}/${to.params.project}`;
|
||||
|
||||
if (to.params.branch) {
|
||||
store.dispatch('getBranchData', {
|
||||
projectId: fullProjectId,
|
||||
branchId: to.params.branch,
|
||||
});
|
||||
|
||||
store.dispatch('getTreeData', {
|
||||
projectId: fullProjectId,
|
||||
branch: to.params.branch,
|
||||
endpoint: `/tree/${to.params.branch}`,
|
||||
})
|
||||
.then(() => {
|
||||
if (to.params[0]) {
|
||||
const treeEntry = getTreeEntry(store, `${to.params.namespace}/${to.params.project}/${to.params.branch}`, to.params[0]);
|
||||
if (treeEntry) {
|
||||
store.dispatch('handleTreeEntryAction', treeEntry);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
flash('Error while loading the branch files. Please try again.', 'alert', document, null, false, true);
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
flash('Error while loading the project data. Please try again.', 'alert', document, null, false, true);
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
export default router;
|
|
@ -1,31 +0,0 @@
|
|||
import Vue from 'vue';
|
||||
import ide from './components/ide.vue';
|
||||
import store from './stores';
|
||||
import router from './ide_router';
|
||||
import Translate from '../vue_shared/translate';
|
||||
|
||||
function initIde(el) {
|
||||
if (!el) return null;
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
store,
|
||||
router,
|
||||
components: {
|
||||
ide,
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement('ide', {
|
||||
props: {
|
||||
emptyStateSvgPath: el.dataset.emptyStateSvgPath,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const ideElement = document.getElementById('ide');
|
||||
|
||||
Vue.use(Translate);
|
||||
|
||||
initIde(ideElement);
|
|
@ -1,14 +0,0 @@
|
|||
export default class Disposable {
|
||||
constructor() {
|
||||
this.disposers = new Set();
|
||||
}
|
||||
|
||||
add(...disposers) {
|
||||
disposers.forEach(disposer => this.disposers.add(disposer));
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.disposers.forEach(disposer => disposer.dispose());
|
||||
this.disposers.clear();
|
||||
}
|
||||
}
|
|
@ -1,64 +0,0 @@
|
|||
/* global monaco */
|
||||
import Disposable from './disposable';
|
||||
|
||||
export default class Model {
|
||||
constructor(monaco, file) {
|
||||
this.monaco = monaco;
|
||||
this.disposable = new Disposable();
|
||||
this.file = file;
|
||||
this.content = file.content !== '' ? file.content : file.raw;
|
||||
|
||||
this.disposable.add(
|
||||
this.originalModel = this.monaco.editor.createModel(
|
||||
this.file.raw,
|
||||
undefined,
|
||||
new this.monaco.Uri(null, null, `original/${this.file.path}`),
|
||||
),
|
||||
this.model = this.monaco.editor.createModel(
|
||||
this.content,
|
||||
undefined,
|
||||
new this.monaco.Uri(null, null, this.file.path),
|
||||
),
|
||||
);
|
||||
|
||||
this.events = new Map();
|
||||
}
|
||||
|
||||
get url() {
|
||||
return this.model.uri.toString();
|
||||
}
|
||||
|
||||
get language() {
|
||||
return this.model.getModeId();
|
||||
}
|
||||
|
||||
get eol() {
|
||||
return this.model.getEOL() === '\n' ? 'LF' : 'CRLF';
|
||||
}
|
||||
|
||||
get path() {
|
||||
return this.file.path;
|
||||
}
|
||||
|
||||
getModel() {
|
||||
return this.model;
|
||||
}
|
||||
|
||||
getOriginalModel() {
|
||||
return this.originalModel;
|
||||
}
|
||||
|
||||
onChange(cb) {
|
||||
this.events.set(
|
||||
this.path,
|
||||
this.disposable.add(
|
||||
this.model.onDidChangeContent(e => cb(this.model, e)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.disposable.dispose();
|
||||
this.events.clear();
|
||||
}
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
import Disposable from './disposable';
|
||||
import Model from './model';
|
||||
|
||||
export default class ModelManager {
|
||||
constructor(monaco) {
|
||||
this.monaco = monaco;
|
||||
this.disposable = new Disposable();
|
||||
this.models = new Map();
|
||||
}
|
||||
|
||||
hasCachedModel(path) {
|
||||
return this.models.has(path);
|
||||
}
|
||||
|
||||
addModel(file) {
|
||||
if (this.hasCachedModel(file.path)) {
|
||||
return this.models.get(file.path);
|
||||
}
|
||||
|
||||
const model = new Model(this.monaco, file);
|
||||
this.models.set(model.path, model);
|
||||
this.disposable.add(model);
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
// dispose of all the models
|
||||
this.disposable.dispose();
|
||||
this.models.clear();
|
||||
}
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
export default class DecorationsController {
|
||||
constructor(editor) {
|
||||
this.editor = editor;
|
||||
this.decorations = new Map();
|
||||
this.editorDecorations = new Map();
|
||||
}
|
||||
|
||||
getAllDecorationsForModel(model) {
|
||||
if (!this.decorations.has(model.url)) return [];
|
||||
|
||||
const modelDecorations = this.decorations.get(model.url);
|
||||
const decorations = [];
|
||||
|
||||
modelDecorations.forEach(val => decorations.push(...val));
|
||||
|
||||
return decorations;
|
||||
}
|
||||
|
||||
addDecorations(model, decorationsKey, decorations) {
|
||||
const decorationMap = this.decorations.get(model.url) || new Map();
|
||||
|
||||
decorationMap.set(decorationsKey, decorations);
|
||||
|
||||
this.decorations.set(model.url, decorationMap);
|
||||
|
||||
this.decorate(model);
|
||||
}
|
||||
|
||||
decorate(model) {
|
||||
const decorations = this.getAllDecorationsForModel(model);
|
||||
const oldDecorations = this.editorDecorations.get(model.url) || [];
|
||||
|
||||
this.editorDecorations.set(
|
||||
model.url,
|
||||
this.editor.instance.deltaDecorations(oldDecorations, decorations),
|
||||
);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.decorations.clear();
|
||||
this.editorDecorations.clear();
|
||||
}
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
/* global monaco */
|
||||
import { throttle } from 'underscore';
|
||||
import DirtyDiffWorker from './diff_worker';
|
||||
import Disposable from '../common/disposable';
|
||||
|
||||
export const getDiffChangeType = (change) => {
|
||||
if (change.modified) {
|
||||
return 'modified';
|
||||
} else if (change.added) {
|
||||
return 'added';
|
||||
} else if (change.removed) {
|
||||
return 'removed';
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
export const getDecorator = change => ({
|
||||
range: new monaco.Range(
|
||||
change.lineNumber,
|
||||
1,
|
||||
change.endLineNumber,
|
||||
1,
|
||||
),
|
||||
options: {
|
||||
isWholeLine: true,
|
||||
linesDecorationsClassName: `dirty-diff dirty-diff-${getDiffChangeType(change)}`,
|
||||
},
|
||||
});
|
||||
|
||||
export default class DirtyDiffController {
|
||||
constructor(modelManager, decorationsController) {
|
||||
this.disposable = new Disposable();
|
||||
this.editorSimpleWorker = null;
|
||||
this.modelManager = modelManager;
|
||||
this.decorationsController = decorationsController;
|
||||
this.dirtyDiffWorker = new DirtyDiffWorker();
|
||||
this.throttledComputeDiff = throttle(this.computeDiff, 250);
|
||||
this.decorate = this.decorate.bind(this);
|
||||
|
||||
this.dirtyDiffWorker.addEventListener('message', this.decorate);
|
||||
}
|
||||
|
||||
attachModel(model) {
|
||||
model.onChange(() => this.throttledComputeDiff(model));
|
||||
}
|
||||
|
||||
computeDiff(model) {
|
||||
this.dirtyDiffWorker.postMessage({
|
||||
path: model.path,
|
||||
originalContent: model.getOriginalModel().getValue(),
|
||||
newContent: model.getModel().getValue(),
|
||||
});
|
||||
}
|
||||
|
||||
reDecorate(model) {
|
||||
this.decorationsController.decorate(model);
|
||||
}
|
||||
|
||||
decorate({ data }) {
|
||||
const decorations = data.changes.map(change => getDecorator(change));
|
||||
this.decorationsController.addDecorations(data.path, 'dirtyDiff', decorations);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.disposable.dispose();
|
||||
|
||||
this.dirtyDiffWorker.removeEventListener('message', this.decorate);
|
||||
this.dirtyDiffWorker.terminate();
|
||||
}
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
import { diffLines } from 'diff';
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const computeDiff = (originalContent, newContent) => {
|
||||
const changes = diffLines(originalContent, newContent);
|
||||
|
||||
let lineNumber = 1;
|
||||
return changes.reduce((acc, change) => {
|
||||
const findOnLine = acc.find(c => c.lineNumber === lineNumber);
|
||||
|
||||
if (findOnLine) {
|
||||
Object.assign(findOnLine, change, {
|
||||
modified: true,
|
||||
endLineNumber: (lineNumber + change.count) - 1,
|
||||
});
|
||||
} else if ('added' in change || 'removed' in change) {
|
||||
acc.push(Object.assign({}, change, {
|
||||
lineNumber,
|
||||
modified: undefined,
|
||||
endLineNumber: (lineNumber + change.count) - 1,
|
||||
}));
|
||||
}
|
||||
|
||||
if (!change.removed) {
|
||||
lineNumber += change.count;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
};
|
|
@ -1,10 +0,0 @@
|
|||
import { computeDiff } from './diff';
|
||||
|
||||
self.addEventListener('message', (e) => {
|
||||
const data = e.data;
|
||||
|
||||
self.postMessage({
|
||||
path: data.path,
|
||||
changes: computeDiff(data.originalContent, data.newContent),
|
||||
});
|
||||
});
|
|
@ -1,110 +0,0 @@
|
|||
import _ from 'underscore';
|
||||
import DecorationsController from './decorations/controller';
|
||||
import DirtyDiffController from './diff/controller';
|
||||
import Disposable from './common/disposable';
|
||||
import ModelManager from './common/model_manager';
|
||||
import editorOptions from './editor_options';
|
||||
|
||||
export default class Editor {
|
||||
static create(monaco) {
|
||||
this.editorInstance = new Editor(monaco);
|
||||
|
||||
return this.editorInstance;
|
||||
}
|
||||
|
||||
constructor(monaco) {
|
||||
this.monaco = monaco;
|
||||
this.currentModel = null;
|
||||
this.instance = null;
|
||||
this.dirtyDiffController = null;
|
||||
this.disposable = new Disposable();
|
||||
|
||||
this.disposable.add(
|
||||
this.modelManager = new ModelManager(this.monaco),
|
||||
this.decorationsController = new DecorationsController(this),
|
||||
);
|
||||
|
||||
this.debouncedUpdate = _.debounce(() => {
|
||||
this.updateDimensions();
|
||||
}, 200);
|
||||
window.addEventListener('resize', this.debouncedUpdate, false);
|
||||
}
|
||||
|
||||
createInstance(domElement) {
|
||||
if (!this.instance) {
|
||||
this.disposable.add(
|
||||
this.instance = this.monaco.editor.create(domElement, {
|
||||
model: null,
|
||||
readOnly: false,
|
||||
contextmenu: true,
|
||||
scrollBeyondLastLine: false,
|
||||
minimap: {
|
||||
enabled: false,
|
||||
},
|
||||
}),
|
||||
this.dirtyDiffController = new DirtyDiffController(
|
||||
this.modelManager, this.decorationsController,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
createModel(file) {
|
||||
return this.modelManager.addModel(file);
|
||||
}
|
||||
|
||||
attachModel(model) {
|
||||
this.instance.setModel(model.getModel());
|
||||
if (this.dirtyDiffController) this.dirtyDiffController.attachModel(model);
|
||||
|
||||
this.currentModel = model;
|
||||
|
||||
this.instance.updateOptions(editorOptions.reduce((acc, obj) => {
|
||||
Object.keys(obj).forEach((key) => {
|
||||
Object.assign(acc, {
|
||||
[key]: obj[key](model),
|
||||
});
|
||||
});
|
||||
return acc;
|
||||
}, {}));
|
||||
|
||||
if (this.dirtyDiffController) this.dirtyDiffController.reDecorate(model);
|
||||
}
|
||||
|
||||
clearEditor() {
|
||||
if (this.instance) {
|
||||
this.instance.setModel(null);
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.disposable.dispose();
|
||||
window.removeEventListener('resize', this.debouncedUpdate);
|
||||
|
||||
// dispose main monaco instance
|
||||
if (this.instance) {
|
||||
this.instance = null;
|
||||
}
|
||||
}
|
||||
|
||||
updateDimensions() {
|
||||
this.instance.layout();
|
||||
}
|
||||
|
||||
setPosition({ lineNumber, column }) {
|
||||
this.instance.revealPositionInCenter({
|
||||
lineNumber,
|
||||
column,
|
||||
});
|
||||
this.instance.setPosition({
|
||||
lineNumber,
|
||||
column,
|
||||
});
|
||||
}
|
||||
|
||||
onPositionChange(cb) {
|
||||
this.disposable.add(
|
||||
this.instance.onDidChangeCursorPosition(e => cb(this.instance, e)),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
export default [{
|
||||
}];
|
|
@ -1,16 +0,0 @@
|
|||
import monacoContext from 'monaco-editor/dev/vs/loader';
|
||||
|
||||
monacoContext.require.config({
|
||||
paths: {
|
||||
vs: `${__webpack_public_path__}monaco-editor/vs`, // eslint-disable-line camelcase
|
||||
},
|
||||
});
|
||||
|
||||
// ignore CDN config and use local assets path for service worker which cannot be cross-domain
|
||||
const relativeRootPath = (gon && gon.relative_url_root) || '';
|
||||
const monacoPath = `${relativeRootPath}/assets/webpack/monaco-editor/vs`;
|
||||
window.MonacoEnvironment = { getWorkerUrl: () => `${monacoPath}/base/worker/workerMain.js` };
|
||||
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
window.__monaco_context__ = monacoContext;
|
||||
export default monacoContext.require;
|
|
@ -1,47 +0,0 @@
|
|||
import Vue from 'vue';
|
||||
import VueResource from 'vue-resource';
|
||||
import Api from '../../api';
|
||||
|
||||
Vue.use(VueResource);
|
||||
|
||||
export default {
|
||||
getTreeData(endpoint) {
|
||||
return Vue.http.get(endpoint, { params: { format: 'json' } });
|
||||
},
|
||||
getFileData(endpoint) {
|
||||
return Vue.http.get(endpoint, { params: { format: 'json' } });
|
||||
},
|
||||
getRawFileData(file) {
|
||||
if (file.tempFile) {
|
||||
return Promise.resolve(file.content);
|
||||
}
|
||||
|
||||
if (file.raw) {
|
||||
return Promise.resolve(file.raw);
|
||||
}
|
||||
|
||||
return Vue.http.get(file.rawPath, { params: { format: 'json' } })
|
||||
.then(res => res.text());
|
||||
},
|
||||
getProjectData(namespace, project) {
|
||||
return Api.project(`${namespace}/${project}`);
|
||||
},
|
||||
getBranchData(projectId, currentBranchId) {
|
||||
return Api.branchSingle(projectId, currentBranchId);
|
||||
},
|
||||
createBranch(projectId, payload) {
|
||||
const url = Api.buildUrl(Api.createBranchPath).replace(':id', projectId);
|
||||
|
||||
return Vue.http.post(url, payload);
|
||||
},
|
||||
commit(projectId, payload) {
|
||||
return Api.commitMultiple(projectId, payload);
|
||||
},
|
||||
getTreeLastCommit(endpoint) {
|
||||
return Vue.http.get(endpoint, {
|
||||
params: {
|
||||
format: 'json',
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
|
@ -1,196 +0,0 @@
|
|||
import Vue from 'vue';
|
||||
import { visitUrl } from '~/lib/utils/url_utility';
|
||||
import flash from '~/flash';
|
||||
import service from '../services';
|
||||
import * as types from './mutation_types';
|
||||
import { stripHtml } from '../../lib/utils/text_utility';
|
||||
|
||||
export const redirectToUrl = (_, url) => visitUrl(url);
|
||||
|
||||
export const setInitialData = ({ commit }, data) =>
|
||||
commit(types.SET_INITIAL_DATA, data);
|
||||
|
||||
export const closeDiscardPopup = ({ commit }) =>
|
||||
commit(types.TOGGLE_DISCARD_POPUP, false);
|
||||
|
||||
export const discardAllChanges = ({ commit, getters, dispatch }) => {
|
||||
const changedFiles = getters.changedFiles;
|
||||
|
||||
changedFiles.forEach((file) => {
|
||||
commit(types.DISCARD_FILE_CHANGES, file);
|
||||
|
||||
if (file.tempFile) {
|
||||
dispatch('closeFile', { file, force: true });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const closeAllFiles = ({ state, dispatch }) => {
|
||||
state.openFiles.forEach(file => dispatch('closeFile', { file }));
|
||||
};
|
||||
|
||||
export const toggleEditMode = (
|
||||
{ state, commit, getters, dispatch },
|
||||
force = false,
|
||||
) => {
|
||||
const changedFiles = getters.changedFiles;
|
||||
|
||||
if (changedFiles.length && !force) {
|
||||
commit(types.TOGGLE_DISCARD_POPUP, true);
|
||||
} else {
|
||||
commit(types.TOGGLE_EDIT_MODE);
|
||||
commit(types.TOGGLE_DISCARD_POPUP, false);
|
||||
dispatch('toggleBlobView');
|
||||
|
||||
if (!state.editMode) {
|
||||
dispatch('discardAllChanges');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const toggleBlobView = ({ commit, state }) => {
|
||||
if (state.editMode) {
|
||||
commit(types.SET_EDIT_MODE);
|
||||
} else {
|
||||
commit(types.SET_PREVIEW_MODE);
|
||||
}
|
||||
};
|
||||
|
||||
export const setPanelCollapsedStatus = ({ commit }, { side, collapsed }) => {
|
||||
if (side === 'left') {
|
||||
commit(types.SET_LEFT_PANEL_COLLAPSED, collapsed);
|
||||
} else {
|
||||
commit(types.SET_RIGHT_PANEL_COLLAPSED, collapsed);
|
||||
}
|
||||
};
|
||||
|
||||
export const setResizingStatus = ({ commit }, resizing) => {
|
||||
commit(types.SET_RESIZING_STATUS, resizing);
|
||||
};
|
||||
|
||||
export const checkCommitStatus = ({ state }) =>
|
||||
service
|
||||
.getBranchData(state.currentProjectId, state.currentBranchId)
|
||||
.then(({ data }) => {
|
||||
const { id } = data.commit;
|
||||
const selectedBranch =
|
||||
state.projects[state.currentProjectId].branches[state.currentBranchId];
|
||||
|
||||
if (selectedBranch.workingReference !== id) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
})
|
||||
.catch(() => flash('Error checking branch data. Please try again.', 'alert', document, null, false, true));
|
||||
|
||||
export const commitChanges = (
|
||||
{ commit, state, dispatch, getters },
|
||||
{ payload, newMr },
|
||||
) =>
|
||||
service
|
||||
.commit(state.currentProjectId, payload)
|
||||
.then(({ data }) => {
|
||||
const { branch } = payload;
|
||||
if (!data.short_id) {
|
||||
flash(data.message, 'alert', document, null, false, true);
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedProject = state.projects[state.currentProjectId];
|
||||
const lastCommit = {
|
||||
commit_path: `${selectedProject.web_url}/commit/${data.id}`,
|
||||
commit: {
|
||||
message: data.message,
|
||||
authored_date: data.committed_date,
|
||||
},
|
||||
};
|
||||
|
||||
let commitMsg = `Your changes have been committed. Commit ${data.short_id}`;
|
||||
if (data.stats) {
|
||||
commitMsg += ` with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`;
|
||||
}
|
||||
|
||||
flash(
|
||||
commitMsg,
|
||||
'notice',
|
||||
document,
|
||||
null,
|
||||
false,
|
||||
true);
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
|
||||
if (newMr) {
|
||||
dispatch('discardAllChanges');
|
||||
dispatch(
|
||||
'redirectToUrl',
|
||||
`${selectedProject.web_url}/merge_requests/new?merge_request%5Bsource_branch%5D=${branch}`,
|
||||
);
|
||||
} else {
|
||||
commit(types.SET_BRANCH_WORKING_REFERENCE, {
|
||||
projectId: state.currentProjectId,
|
||||
branchId: state.currentBranchId,
|
||||
reference: data.id,
|
||||
});
|
||||
|
||||
getters.changedFiles.forEach((entry) => {
|
||||
commit(types.SET_LAST_COMMIT_DATA, {
|
||||
entry,
|
||||
lastCommit,
|
||||
});
|
||||
});
|
||||
|
||||
dispatch('discardAllChanges');
|
||||
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
let errMsg = 'Error committing changes. Please try again.';
|
||||
if (err.response.data && err.response.data.message) {
|
||||
errMsg += ` (${stripHtml(err.response.data.message)})`;
|
||||
}
|
||||
flash(errMsg, 'alert', document, null, false, true);
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
});
|
||||
|
||||
export const createTempEntry = (
|
||||
{ state, dispatch },
|
||||
{ projectId, branchId, parent, name, type, content = '', base64 = false },
|
||||
) => {
|
||||
const selectedParent = parent || state.trees[`${projectId}/${branchId}`];
|
||||
if (type === 'tree') {
|
||||
dispatch('createTempTree', {
|
||||
projectId,
|
||||
branchId,
|
||||
parent: selectedParent,
|
||||
name,
|
||||
});
|
||||
} else if (type === 'blob') {
|
||||
dispatch('createTempFile', {
|
||||
projectId,
|
||||
branchId,
|
||||
parent: selectedParent,
|
||||
name,
|
||||
base64,
|
||||
content,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const scrollToTab = () => {
|
||||
Vue.nextTick(() => {
|
||||
const tabs = document.getElementById('tabs');
|
||||
|
||||
if (tabs) {
|
||||
const tabEl = tabs.querySelector('.active .repo-tab');
|
||||
|
||||
tabEl.focus();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export * from './actions/tree';
|
||||
export * from './actions/file';
|
||||
export * from './actions/project';
|
||||
export * from './actions/branch';
|
|
@ -1,43 +0,0 @@
|
|||
import service from '../../services';
|
||||
import flash from '../../../flash';
|
||||
import * as types from '../mutation_types';
|
||||
|
||||
export const getBranchData = (
|
||||
{ commit, state, dispatch },
|
||||
{ projectId, branchId, force = false } = {},
|
||||
) => new Promise((resolve, reject) => {
|
||||
if ((typeof state.projects[`${projectId}`] === 'undefined' ||
|
||||
!state.projects[`${projectId}`].branches[branchId])
|
||||
|| force) {
|
||||
service.getBranchData(`${projectId}`, branchId)
|
||||
.then(({ data }) => {
|
||||
const { id } = data.commit;
|
||||
commit(types.SET_BRANCH, { projectPath: `${projectId}`, branchName: branchId, branch: data });
|
||||
commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id });
|
||||
resolve(data);
|
||||
})
|
||||
.catch(() => {
|
||||
flash('Error loading branch data. Please try again.', 'alert', document, null, false, true);
|
||||
reject(new Error(`Branch not loaded - ${projectId}/${branchId}`));
|
||||
});
|
||||
} else {
|
||||
resolve(state.projects[`${projectId}`].branches[branchId]);
|
||||
}
|
||||
});
|
||||
|
||||
export const createNewBranch = ({ state, commit }, branch) => service.createBranch(
|
||||
state.currentProjectId,
|
||||
{
|
||||
branch,
|
||||
ref: state.currentBranchId,
|
||||
},
|
||||
)
|
||||
.then(res => res.json())
|
||||
.then((data) => {
|
||||
const branchName = data.name;
|
||||
const url = location.href.replace(state.currentBranchId, branchName);
|
||||
|
||||
if (this.$router) this.$router.push(url);
|
||||
|
||||
commit(types.SET_CURRENT_BRANCH, branchName);
|
||||
});
|
|
@ -1,137 +0,0 @@
|
|||
import { normalizeHeaders } from '../../../lib/utils/common_utils';
|
||||
import flash from '../../../flash';
|
||||
import service from '../../services';
|
||||
import * as types from '../mutation_types';
|
||||
import router from '../../ide_router';
|
||||
import {
|
||||
findEntry,
|
||||
setPageTitle,
|
||||
createTemp,
|
||||
findIndexOfFile,
|
||||
} from '../utils';
|
||||
|
||||
export const closeFile = ({ commit, state, dispatch }, { file, force = false }) => {
|
||||
if ((file.changed || file.tempFile) && !force) return;
|
||||
|
||||
const indexOfClosedFile = findIndexOfFile(state.openFiles, file);
|
||||
const fileWasActive = file.active;
|
||||
|
||||
commit(types.TOGGLE_FILE_OPEN, file);
|
||||
commit(types.SET_FILE_ACTIVE, { file, active: false });
|
||||
|
||||
if (state.openFiles.length > 0 && fileWasActive) {
|
||||
const nextIndexToOpen = indexOfClosedFile === 0 ? 0 : indexOfClosedFile - 1;
|
||||
const nextFileToOpen = state.openFiles[nextIndexToOpen];
|
||||
|
||||
dispatch('setFileActive', nextFileToOpen);
|
||||
} else if (!state.openFiles.length) {
|
||||
router.push(`/project/${file.projectId}/tree/${file.branchId}/`);
|
||||
}
|
||||
|
||||
dispatch('getLastCommitData');
|
||||
};
|
||||
|
||||
export const setFileActive = ({ commit, state, getters, dispatch }, file) => {
|
||||
const currentActiveFile = getters.activeFile;
|
||||
|
||||
if (file.active) return;
|
||||
|
||||
if (currentActiveFile) {
|
||||
commit(types.SET_FILE_ACTIVE, { file: currentActiveFile, active: false });
|
||||
}
|
||||
|
||||
commit(types.SET_FILE_ACTIVE, { file, active: true });
|
||||
dispatch('scrollToTab');
|
||||
|
||||
// reset hash for line highlighting
|
||||
location.hash = '';
|
||||
|
||||
commit(types.SET_CURRENT_PROJECT, file.projectId);
|
||||
commit(types.SET_CURRENT_BRANCH, file.branchId);
|
||||
};
|
||||
|
||||
export const getFileData = ({ state, commit, dispatch }, file) => {
|
||||
commit(types.TOGGLE_LOADING, file);
|
||||
|
||||
service.getFileData(file.url)
|
||||
.then((res) => {
|
||||
const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']);
|
||||
|
||||
setPageTitle(pageTitle);
|
||||
|
||||
return res.json();
|
||||
})
|
||||
.then((data) => {
|
||||
commit(types.SET_FILE_DATA, { data, file });
|
||||
commit(types.TOGGLE_FILE_OPEN, file);
|
||||
dispatch('setFileActive', file);
|
||||
commit(types.TOGGLE_LOADING, file);
|
||||
})
|
||||
.catch(() => {
|
||||
commit(types.TOGGLE_LOADING, file);
|
||||
flash('Error loading file data. Please try again.', 'alert', document, null, false, true);
|
||||
});
|
||||
};
|
||||
|
||||
export const getRawFileData = ({ commit, dispatch }, file) => service.getRawFileData(file)
|
||||
.then((raw) => {
|
||||
commit(types.SET_FILE_RAW_DATA, { file, raw });
|
||||
})
|
||||
.catch(() => flash('Error loading file content. Please try again.', 'alert', document, null, false, true));
|
||||
|
||||
export const changeFileContent = ({ commit }, { file, content }) => {
|
||||
commit(types.UPDATE_FILE_CONTENT, { file, content });
|
||||
};
|
||||
|
||||
export const setFileLanguage = ({ state, commit }, { fileLanguage }) => {
|
||||
if (state.selectedFile) {
|
||||
commit(types.SET_FILE_LANGUAGE, { file: state.selectedFile, fileLanguage });
|
||||
}
|
||||
};
|
||||
|
||||
export const setFileEOL = ({ state, commit }, { eol }) => {
|
||||
if (state.selectedFile) {
|
||||
commit(types.SET_FILE_EOL, { file: state.selectedFile, eol });
|
||||
}
|
||||
};
|
||||
|
||||
export const setEditorPosition = ({ state, commit }, { editorRow, editorColumn }) => {
|
||||
if (state.selectedFile) {
|
||||
commit(types.SET_FILE_POSITION, { file: state.selectedFile, editorRow, editorColumn });
|
||||
}
|
||||
};
|
||||
|
||||
export const createTempFile = ({ state, commit, dispatch }, { projectId, branchId, parent, name, content = '', base64 = '' }) => {
|
||||
const path = parent.path !== undefined ? parent.path : '';
|
||||
// We need to do the replacement otherwise the web_url + file.url duplicate
|
||||
const newUrl = `/${projectId}/blob/${branchId}/${path}${path ? '/' : ''}${name}`;
|
||||
const file = createTemp({
|
||||
projectId,
|
||||
branchId,
|
||||
name: name.replace(`${path}/`, ''),
|
||||
path,
|
||||
type: 'blob',
|
||||
level: parent.level !== undefined ? parent.level + 1 : 0,
|
||||
changed: true,
|
||||
content,
|
||||
base64,
|
||||
url: newUrl,
|
||||
});
|
||||
|
||||
if (findEntry(parent.tree, 'blob', file.name)) return flash(`The name "${file.name}" is already taken in this directory.`, 'alert', document, null, false, true);
|
||||
|
||||
commit(types.CREATE_TMP_FILE, {
|
||||
parent,
|
||||
file,
|
||||
});
|
||||
commit(types.TOGGLE_FILE_OPEN, file);
|
||||
dispatch('setFileActive', file);
|
||||
|
||||
if (!state.editMode && !file.base64) {
|
||||
dispatch('toggleEditMode', true);
|
||||
}
|
||||
|
||||
router.push(`/project${file.url}`);
|
||||
|
||||
return Promise.resolve(file);
|
||||
};
|
|
@ -1,27 +0,0 @@
|
|||
import service from '../../services';
|
||||
import flash from '../../../flash';
|
||||
import * as types from '../mutation_types';
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const getProjectData = (
|
||||
{ commit, state, dispatch },
|
||||
{ namespace, projectId, force = false } = {},
|
||||
) => new Promise((resolve, reject) => {
|
||||
if (!state.projects[`${namespace}/${projectId}`] || force) {
|
||||
commit(types.TOGGLE_LOADING, state);
|
||||
service.getProjectData(namespace, projectId)
|
||||
.then(res => res.data)
|
||||
.then((data) => {
|
||||
commit(types.TOGGLE_LOADING, state);
|
||||
commit(types.SET_PROJECT, { projectPath: `${namespace}/${projectId}`, project: data });
|
||||
if (!state.currentProjectId) commit(types.SET_CURRENT_PROJECT, `${namespace}/${projectId}`);
|
||||
resolve(data);
|
||||
})
|
||||
.catch(() => {
|
||||
flash('Error loading project data. Please try again.', 'alert', document, null, false, true);
|
||||
reject(new Error(`Project not loaded ${namespace}/${projectId}`));
|
||||
});
|
||||
} else {
|
||||
resolve(state.projects[`${namespace}/${projectId}`]);
|
||||
}
|
||||
});
|
|
@ -1,188 +0,0 @@
|
|||
import { visitUrl } from '../../../lib/utils/url_utility';
|
||||
import { normalizeHeaders } from '../../../lib/utils/common_utils';
|
||||
import flash from '../../../flash';
|
||||
import service from '../../services';
|
||||
import * as types from '../mutation_types';
|
||||
import router from '../../ide_router';
|
||||
import {
|
||||
setPageTitle,
|
||||
findEntry,
|
||||
createTemp,
|
||||
createOrMergeEntry,
|
||||
} from '../utils';
|
||||
|
||||
export const getTreeData = (
|
||||
{ commit, state, dispatch },
|
||||
{ endpoint, tree = null, projectId, branch, force = false } = {},
|
||||
) => new Promise((resolve, reject) => {
|
||||
// We already have the base tree so we resolve immediately
|
||||
if (!tree && state.trees[`${projectId}/${branch}`] && !force) {
|
||||
resolve();
|
||||
} else {
|
||||
if (tree) commit(types.TOGGLE_LOADING, tree);
|
||||
const selectedProject = state.projects[projectId];
|
||||
// We are merging the web_url that we got on the project info with the endpoint
|
||||
// we got on the tree entry, as both contain the projectId, we replace it in the tree endpoint
|
||||
const completeEndpoint = selectedProject.web_url + (endpoint).replace(projectId, '');
|
||||
if (completeEndpoint && (!tree || !tree.tempFile)) {
|
||||
service.getTreeData(completeEndpoint)
|
||||
.then((res) => {
|
||||
const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']);
|
||||
|
||||
setPageTitle(pageTitle);
|
||||
|
||||
return res.json();
|
||||
})
|
||||
.then((data) => {
|
||||
if (!state.isInitialRoot) {
|
||||
commit(types.SET_ROOT, data.path === '/');
|
||||
}
|
||||
|
||||
dispatch('updateDirectoryData', { data, tree, projectId, branch });
|
||||
const selectedTree = tree || state.trees[`${projectId}/${branch}`];
|
||||
|
||||
commit(types.SET_PARENT_TREE_URL, data.parent_tree_url);
|
||||
commit(types.SET_LAST_COMMIT_URL, { tree: selectedTree, url: data.last_commit_path });
|
||||
if (tree) commit(types.TOGGLE_LOADING, selectedTree);
|
||||
|
||||
const prevLastCommitPath = selectedTree.lastCommitPath;
|
||||
if (prevLastCommitPath !== null) {
|
||||
dispatch('getLastCommitData', selectedTree);
|
||||
}
|
||||
resolve(data);
|
||||
})
|
||||
.catch((e) => {
|
||||
flash('Error loading tree data. Please try again.', 'alert', document, null, false, true);
|
||||
if (tree) commit(types.TOGGLE_LOADING, tree);
|
||||
reject(e);
|
||||
});
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const toggleTreeOpen = ({ commit, dispatch }, { endpoint, tree }) => {
|
||||
if (tree.opened) {
|
||||
// send empty data to clear the tree
|
||||
const data = { trees: [], blobs: [], submodules: [] };
|
||||
|
||||
dispatch('updateDirectoryData', { data, tree, projectId: tree.projectId, branchId: tree.branchId });
|
||||
} else {
|
||||
dispatch('getTreeData', { endpoint, tree, projectId: tree.projectId, branch: tree.branchId });
|
||||
}
|
||||
|
||||
commit(types.TOGGLE_TREE_OPEN, tree);
|
||||
};
|
||||
|
||||
export const handleTreeEntryAction = ({ commit, dispatch }, row) => {
|
||||
if (row.type === 'tree') {
|
||||
dispatch('toggleTreeOpen', {
|
||||
endpoint: row.url,
|
||||
tree: row,
|
||||
});
|
||||
} else if (row.type === 'submodule') {
|
||||
commit(types.TOGGLE_LOADING, row);
|
||||
visitUrl(row.url);
|
||||
} else if (row.type === 'blob' && row.opened) {
|
||||
dispatch('setFileActive', row);
|
||||
} else {
|
||||
dispatch('getFileData', row);
|
||||
}
|
||||
};
|
||||
|
||||
export const createTempTree = (
|
||||
{ state, commit, dispatch },
|
||||
{ projectId, branchId, parent, name },
|
||||
) => {
|
||||
let selectedTree = parent;
|
||||
const dirNames = name.replace(new RegExp(`^${state.path}/`), '').split('/');
|
||||
|
||||
dirNames.forEach((dirName) => {
|
||||
const foundEntry = findEntry(selectedTree.tree, 'tree', dirName);
|
||||
|
||||
if (!foundEntry) {
|
||||
const path = selectedTree.path !== undefined ? selectedTree.path : '';
|
||||
const tmpEntry = createTemp({
|
||||
projectId,
|
||||
branchId,
|
||||
name: dirName,
|
||||
path,
|
||||
type: 'tree',
|
||||
level: selectedTree.level !== undefined ? selectedTree.level + 1 : 0,
|
||||
tree: [],
|
||||
url: `/${projectId}/blob/${branchId}/${path}${path ? '/' : ''}${dirName}`,
|
||||
});
|
||||
|
||||
commit(types.CREATE_TMP_TREE, {
|
||||
parent: selectedTree,
|
||||
tmpEntry,
|
||||
});
|
||||
commit(types.TOGGLE_TREE_OPEN, tmpEntry);
|
||||
|
||||
router.push(`/project${tmpEntry.url}`);
|
||||
|
||||
selectedTree = tmpEntry;
|
||||
} else {
|
||||
selectedTree = foundEntry;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = state) => {
|
||||
if (!tree || tree.lastCommitPath === null || !tree.lastCommitPath) return;
|
||||
|
||||
service.getTreeLastCommit(tree.lastCommitPath)
|
||||
.then((res) => {
|
||||
const lastCommitPath = normalizeHeaders(res.headers)['MORE-LOGS-URL'] || null;
|
||||
|
||||
commit(types.SET_LAST_COMMIT_URL, { tree, url: lastCommitPath });
|
||||
|
||||
return res.json();
|
||||
})
|
||||
.then((data) => {
|
||||
data.forEach((lastCommit) => {
|
||||
const entry = findEntry(tree.tree, lastCommit.type, lastCommit.file_name);
|
||||
|
||||
if (entry) {
|
||||
commit(types.SET_LAST_COMMIT_DATA, { entry, lastCommit });
|
||||
}
|
||||
});
|
||||
|
||||
dispatch('getLastCommitData', tree);
|
||||
})
|
||||
.catch(() => flash('Error fetching log data.', 'alert', document, null, false, true));
|
||||
};
|
||||
|
||||
export const updateDirectoryData = (
|
||||
{ commit, state },
|
||||
{ data, tree, projectId, branch },
|
||||
) => {
|
||||
if (!tree) {
|
||||
const existingTree = state.trees[`${projectId}/${branch}`];
|
||||
if (!existingTree) {
|
||||
commit(types.CREATE_TREE, { treePath: `${projectId}/${branch}` });
|
||||
}
|
||||
}
|
||||
|
||||
const selectedTree = tree || state.trees[`${projectId}/${branch}`];
|
||||
const level = selectedTree.level !== undefined ? selectedTree.level + 1 : 0;
|
||||
const parentTreeUrl = data.parent_tree_url ? `${data.parent_tree_url}${data.path}` : state.endpoints.rootUrl;
|
||||
const createEntry = (entry, type) => createOrMergeEntry({
|
||||
tree: selectedTree,
|
||||
projectId: `${projectId}`,
|
||||
branchId: branch,
|
||||
entry,
|
||||
level,
|
||||
type,
|
||||
parentTreeUrl,
|
||||
});
|
||||
|
||||
const formattedData = [
|
||||
...data.trees.map(t => createEntry(t, 'tree')),
|
||||
...data.submodules.map(m => createEntry(m, 'submodule')),
|
||||
...data.blobs.map(b => createEntry(b, 'blob')),
|
||||
];
|
||||
|
||||
commit(types.SET_DIRECTORY_DATA, { tree: selectedTree, data: formattedData });
|
||||
};
|
|
@ -1,19 +0,0 @@
|
|||
export const changedFiles = state => state.openFiles.filter(file => file.changed);
|
||||
|
||||
export const activeFile = state => state.openFiles.find(file => file.active) || null;
|
||||
|
||||
export const activeFileExtension = (state) => {
|
||||
const file = activeFile(state);
|
||||
return file ? `.${file.path.split('.').pop()}` : '';
|
||||
};
|
||||
|
||||
export const canEditFile = (state) => {
|
||||
const currentActiveFile = activeFile(state);
|
||||
|
||||
return state.canCommit &&
|
||||
(currentActiveFile && !currentActiveFile.renderError && !currentActiveFile.binary);
|
||||
};
|
||||
|
||||
export const addedFiles = state => changedFiles(state).filter(f => f.tempFile);
|
||||
|
||||
export const modifiedFiles = state => changedFiles(state).filter(f => !f.tempFile);
|
|
@ -1,15 +0,0 @@
|
|||
import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
import state from './state';
|
||||
import * as actions from './actions';
|
||||
import * as getters from './getters';
|
||||
import mutations from './mutations';
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
export default new Vuex.Store({
|
||||
state: state(),
|
||||
actions,
|
||||
mutations,
|
||||
getters,
|
||||
});
|
|
@ -1,46 +0,0 @@
|
|||
export const SET_INITIAL_DATA = 'SET_INITIAL_DATA';
|
||||
export const TOGGLE_LOADING = 'TOGGLE_LOADING';
|
||||
export const SET_PARENT_TREE_URL = 'SET_PARENT_TREE_URL';
|
||||
export const SET_ROOT = 'SET_ROOT';
|
||||
export const SET_LAST_COMMIT_DATA = 'SET_LAST_COMMIT_DATA';
|
||||
export const SET_LEFT_PANEL_COLLAPSED = 'SET_LEFT_PANEL_COLLAPSED';
|
||||
export const SET_RIGHT_PANEL_COLLAPSED = 'SET_RIGHT_PANEL_COLLAPSED';
|
||||
export const SET_RESIZING_STATUS = 'SET_RESIZING_STATUS';
|
||||
|
||||
// Project Mutation Types
|
||||
export const SET_PROJECT = 'SET_PROJECT';
|
||||
export const SET_CURRENT_PROJECT = 'SET_CURRENT_PROJECT';
|
||||
export const TOGGLE_PROJECT_OPEN = 'TOGGLE_PROJECT_OPEN';
|
||||
|
||||
// Branch Mutation Types
|
||||
export const SET_BRANCH = 'SET_BRANCH';
|
||||
export const SET_BRANCH_WORKING_REFERENCE = 'SET_BRANCH_WORKING_REFERENCE';
|
||||
export const TOGGLE_BRANCH_OPEN = 'TOGGLE_BRANCH_OPEN';
|
||||
|
||||
// Tree mutation types
|
||||
export const SET_DIRECTORY_DATA = 'SET_DIRECTORY_DATA';
|
||||
export const TOGGLE_TREE_OPEN = 'TOGGLE_TREE_OPEN';
|
||||
export const CREATE_TMP_TREE = 'CREATE_TMP_TREE';
|
||||
export const SET_LAST_COMMIT_URL = 'SET_LAST_COMMIT_URL';
|
||||
export const CREATE_TREE = 'CREATE_TREE';
|
||||
|
||||
// File mutation types
|
||||
export const SET_FILE_DATA = 'SET_FILE_DATA';
|
||||
export const TOGGLE_FILE_OPEN = 'TOGGLE_FILE_OPEN';
|
||||
export const SET_FILE_ACTIVE = 'SET_FILE_ACTIVE';
|
||||
export const SET_FILE_RAW_DATA = 'SET_FILE_RAW_DATA';
|
||||
export const UPDATE_FILE_CONTENT = 'UPDATE_FILE_CONTENT';
|
||||
export const SET_FILE_LANGUAGE = 'SET_FILE_LANGUAGE';
|
||||
export const SET_FILE_POSITION = 'SET_FILE_POSITION';
|
||||
export const SET_FILE_EOL = 'SET_FILE_EOL';
|
||||
export const DISCARD_FILE_CHANGES = 'DISCARD_FILE_CHANGES';
|
||||
export const CREATE_TMP_FILE = 'CREATE_TMP_FILE';
|
||||
|
||||
// Viewer mutation types
|
||||
export const SET_PREVIEW_MODE = 'SET_PREVIEW_MODE';
|
||||
export const SET_EDIT_MODE = 'SET_EDIT_MODE';
|
||||
export const TOGGLE_EDIT_MODE = 'TOGGLE_EDIT_MODE';
|
||||
export const TOGGLE_DISCARD_POPUP = 'TOGGLE_DISCARD_POPUP';
|
||||
|
||||
export const SET_CURRENT_BRANCH = 'SET_CURRENT_BRANCH';
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
import * as types from './mutation_types';
|
||||
import projectMutations from './mutations/project';
|
||||
import fileMutations from './mutations/file';
|
||||
import treeMutations from './mutations/tree';
|
||||
import branchMutations from './mutations/branch';
|
||||
|
||||
export default {
|
||||
[types.SET_INITIAL_DATA](state, data) {
|
||||
Object.assign(state, data);
|
||||
},
|
||||
[types.SET_PREVIEW_MODE](state) {
|
||||
Object.assign(state, {
|
||||
currentBlobView: 'repo-preview',
|
||||
});
|
||||
},
|
||||
[types.SET_EDIT_MODE](state) {
|
||||
Object.assign(state, {
|
||||
currentBlobView: 'repo-editor',
|
||||
});
|
||||
},
|
||||
[types.TOGGLE_LOADING](state, entry) {
|
||||
Object.assign(entry, {
|
||||
loading: !entry.loading,
|
||||
});
|
||||
},
|
||||
[types.TOGGLE_EDIT_MODE](state) {
|
||||
Object.assign(state, {
|
||||
editMode: !state.editMode,
|
||||
});
|
||||
},
|
||||
[types.TOGGLE_DISCARD_POPUP](state, discardPopupOpen) {
|
||||
Object.assign(state, {
|
||||
discardPopupOpen,
|
||||
});
|
||||
},
|
||||
[types.SET_ROOT](state, isRoot) {
|
||||
Object.assign(state, {
|
||||
isRoot,
|
||||
isInitialRoot: isRoot,
|
||||
});
|
||||
},
|
||||
[types.SET_LEFT_PANEL_COLLAPSED](state, collapsed) {
|
||||
Object.assign(state, {
|
||||
leftPanelCollapsed: collapsed,
|
||||
});
|
||||
},
|
||||
[types.SET_RIGHT_PANEL_COLLAPSED](state, collapsed) {
|
||||
Object.assign(state, {
|
||||
rightPanelCollapsed: collapsed,
|
||||
});
|
||||
},
|
||||
[types.SET_RESIZING_STATUS](state, resizing) {
|
||||
Object.assign(state, {
|
||||
panelResizing: resizing,
|
||||
});
|
||||
},
|
||||
[types.SET_LAST_COMMIT_DATA](state, { entry, lastCommit }) {
|
||||
Object.assign(entry.lastCommit, {
|
||||
id: lastCommit.commit.id,
|
||||
url: lastCommit.commit_path,
|
||||
message: lastCommit.commit.message,
|
||||
author: lastCommit.commit.author_name,
|
||||
updatedAt: lastCommit.commit.authored_date,
|
||||
});
|
||||
},
|
||||
...projectMutations,
|
||||
...fileMutations,
|
||||
...treeMutations,
|
||||
...branchMutations,
|
||||
};
|
|
@ -1,28 +0,0 @@
|
|||
import * as types from '../mutation_types';
|
||||
|
||||
export default {
|
||||
[types.SET_CURRENT_BRANCH](state, currentBranchId) {
|
||||
Object.assign(state, {
|
||||
currentBranchId,
|
||||
});
|
||||
},
|
||||
[types.SET_BRANCH](state, { projectPath, branchName, branch }) {
|
||||
// Add client side properties
|
||||
Object.assign(branch, {
|
||||
treeId: `${projectPath}/${branchName}`,
|
||||
active: true,
|
||||
workingReference: '',
|
||||
});
|
||||
|
||||
Object.assign(state.projects[projectPath], {
|
||||
branches: {
|
||||
[branchName]: branch,
|
||||
},
|
||||
});
|
||||
},
|
||||
[types.SET_BRANCH_WORKING_REFERENCE](state, { projectId, branchId, reference }) {
|
||||
Object.assign(state.projects[projectId].branches[branchId], {
|
||||
workingReference: reference,
|
||||
});
|
||||
},
|
||||
};
|
|
@ -1,74 +0,0 @@
|
|||
import * as types from '../mutation_types';
|
||||
import { findIndexOfFile } from '../utils';
|
||||
|
||||
export default {
|
||||
[types.SET_FILE_ACTIVE](state, { file, active }) {
|
||||
Object.assign(file, {
|
||||
active,
|
||||
});
|
||||
|
||||
Object.assign(state, {
|
||||
selectedFile: file,
|
||||
});
|
||||
},
|
||||
[types.TOGGLE_FILE_OPEN](state, file) {
|
||||
Object.assign(file, {
|
||||
opened: !file.opened,
|
||||
});
|
||||
|
||||
if (file.opened) {
|
||||
state.openFiles.push(file);
|
||||
} else {
|
||||
state.openFiles.splice(findIndexOfFile(state.openFiles, file), 1);
|
||||
}
|
||||
},
|
||||
[types.SET_FILE_DATA](state, { data, file }) {
|
||||
Object.assign(file, {
|
||||
blamePath: data.blame_path,
|
||||
commitsPath: data.commits_path,
|
||||
permalink: data.permalink,
|
||||
rawPath: data.raw_path,
|
||||
binary: data.binary,
|
||||
html: data.html,
|
||||
renderError: data.render_error,
|
||||
});
|
||||
},
|
||||
[types.SET_FILE_RAW_DATA](state, { file, raw }) {
|
||||
Object.assign(file, {
|
||||
raw,
|
||||
});
|
||||
},
|
||||
[types.UPDATE_FILE_CONTENT](state, { file, content }) {
|
||||
const changed = content !== file.raw;
|
||||
|
||||
Object.assign(file, {
|
||||
content,
|
||||
changed,
|
||||
});
|
||||
},
|
||||
[types.SET_FILE_LANGUAGE](state, { file, fileLanguage }) {
|
||||
Object.assign(file, {
|
||||
fileLanguage,
|
||||
});
|
||||
},
|
||||
[types.SET_FILE_EOL](state, { file, eol }) {
|
||||
Object.assign(file, {
|
||||
eol,
|
||||
});
|
||||
},
|
||||
[types.SET_FILE_POSITION](state, { file, editorRow, editorColumn }) {
|
||||
Object.assign(file, {
|
||||
editorRow,
|
||||
editorColumn,
|
||||
});
|
||||
},
|
||||
[types.DISCARD_FILE_CHANGES](state, file) {
|
||||
Object.assign(file, {
|
||||
content: file.raw,
|
||||
changed: false,
|
||||
});
|
||||
},
|
||||
[types.CREATE_TMP_FILE](state, { file, parent }) {
|
||||
parent.tree.push(file);
|
||||
},
|
||||
};
|
|
@ -1,23 +0,0 @@
|
|||
import * as types from '../mutation_types';
|
||||
|
||||
export default {
|
||||
[types.SET_CURRENT_PROJECT](state, currentProjectId) {
|
||||
Object.assign(state, {
|
||||
currentProjectId,
|
||||
});
|
||||
},
|
||||
[types.SET_PROJECT](state, { projectPath, project }) {
|
||||
// Add client side properties
|
||||
Object.assign(project, {
|
||||
tree: [],
|
||||
branches: {},
|
||||
active: true,
|
||||
});
|
||||
|
||||
Object.assign(state, {
|
||||
projects: Object.assign({}, state.projects, {
|
||||
[projectPath]: project,
|
||||
}),
|
||||
});
|
||||
},
|
||||
};
|
|
@ -1,36 +0,0 @@
|
|||
import * as types from '../mutation_types';
|
||||
|
||||
export default {
|
||||
[types.TOGGLE_TREE_OPEN](state, tree) {
|
||||
Object.assign(tree, {
|
||||
opened: !tree.opened,
|
||||
});
|
||||
},
|
||||
[types.CREATE_TREE](state, { treePath }) {
|
||||
Object.assign(state, {
|
||||
trees: Object.assign({}, state.trees, {
|
||||
[treePath]: {
|
||||
tree: [],
|
||||
},
|
||||
}),
|
||||
});
|
||||
},
|
||||
[types.SET_DIRECTORY_DATA](state, { data, tree }) {
|
||||
Object.assign(tree, {
|
||||
tree: data,
|
||||
});
|
||||
},
|
||||
[types.SET_PARENT_TREE_URL](state, url) {
|
||||
Object.assign(state, {
|
||||
parentTreeUrl: url,
|
||||
});
|
||||
},
|
||||
[types.SET_LAST_COMMIT_URL](state, { tree = state, url }) {
|
||||
Object.assign(tree, {
|
||||
lastCommitPath: url,
|
||||
});
|
||||
},
|
||||
[types.CREATE_TMP_TREE](state, { parent, tmpEntry }) {
|
||||
parent.tree.push(tmpEntry);
|
||||
},
|
||||
};
|
|
@ -1,23 +0,0 @@
|
|||
export default () => ({
|
||||
canCommit: false,
|
||||
currentProjectId: '',
|
||||
currentBranchId: '',
|
||||
currentBlobView: 'repo-editor',
|
||||
discardPopupOpen: false,
|
||||
editMode: true,
|
||||
endpoints: {},
|
||||
isRoot: false,
|
||||
isInitialRoot: false,
|
||||
lastCommitPath: '',
|
||||
loading: false,
|
||||
onTopOfBranch: false,
|
||||
openFiles: [],
|
||||
selectedFile: null,
|
||||
path: '',
|
||||
parentTreeUrl: '',
|
||||
trees: {},
|
||||
projects: {},
|
||||
leftPanelCollapsed: false,
|
||||
rightPanelCollapsed: true,
|
||||
panelResizing: false,
|
||||
});
|
|
@ -1,177 +0,0 @@
|
|||
import _ from 'underscore';
|
||||
|
||||
export const dataStructure = () => ({
|
||||
id: '',
|
||||
key: '',
|
||||
type: '',
|
||||
projectId: '',
|
||||
branchId: '',
|
||||
name: '',
|
||||
url: '',
|
||||
path: '',
|
||||
level: 0,
|
||||
tempFile: false,
|
||||
icon: '',
|
||||
tree: [],
|
||||
loading: false,
|
||||
opened: false,
|
||||
active: false,
|
||||
changed: false,
|
||||
lastCommitPath: '',
|
||||
lastCommit: {
|
||||
id: '',
|
||||
url: '',
|
||||
message: '',
|
||||
updatedAt: '',
|
||||
author: '',
|
||||
},
|
||||
tree_url: '',
|
||||
blamePath: '',
|
||||
commitsPath: '',
|
||||
permalink: '',
|
||||
rawPath: '',
|
||||
binary: false,
|
||||
html: '',
|
||||
raw: '',
|
||||
content: '',
|
||||
parentTreeUrl: '',
|
||||
renderError: false,
|
||||
base64: false,
|
||||
editorRow: 1,
|
||||
editorColumn: 1,
|
||||
fileLanguage: '',
|
||||
eol: '',
|
||||
});
|
||||
|
||||
export const decorateData = (entity) => {
|
||||
const {
|
||||
id,
|
||||
projectId,
|
||||
branchId,
|
||||
type,
|
||||
url,
|
||||
name,
|
||||
icon,
|
||||
tree_url,
|
||||
path,
|
||||
renderError,
|
||||
content = '',
|
||||
tempFile = false,
|
||||
active = false,
|
||||
opened = false,
|
||||
changed = false,
|
||||
parentTreeUrl = '',
|
||||
level = 0,
|
||||
base64 = false,
|
||||
} = entity;
|
||||
|
||||
return {
|
||||
...dataStructure(),
|
||||
id,
|
||||
projectId,
|
||||
branchId,
|
||||
key: `${name}-${type}-${id}`,
|
||||
type,
|
||||
name,
|
||||
url,
|
||||
tree_url,
|
||||
path,
|
||||
level,
|
||||
tempFile,
|
||||
icon: `fa-${icon}`,
|
||||
opened,
|
||||
active,
|
||||
parentTreeUrl,
|
||||
changed,
|
||||
renderError,
|
||||
content,
|
||||
base64,
|
||||
};
|
||||
};
|
||||
|
||||
/*
|
||||
Takes the multi-dimensional tree and returns a flattened array.
|
||||
This allows for the table to recursively render the table rows but keeps the data
|
||||
structure nested to make it easier to add new files/directories.
|
||||
*/
|
||||
export const treeList = (state, treeId) => {
|
||||
const baseTree = state.trees[treeId];
|
||||
if (baseTree) {
|
||||
const mapTree = arr => (!arr.tree || !arr.tree.length ?
|
||||
[] : _.map(arr.tree, a => [a, mapTree(a)]));
|
||||
|
||||
return _.chain(baseTree.tree)
|
||||
.map(arr => [arr, mapTree(arr)])
|
||||
.flatten()
|
||||
.value();
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
export const getTree = state => (namespace, projectId, branch) => state.trees[`${namespace}/${projectId}/${branch}`];
|
||||
|
||||
export const getTreeEntry = (store, treeId, path) => {
|
||||
const fileList = treeList(store.state, treeId);
|
||||
return fileList ? fileList.find(file => file.path === path) : null;
|
||||
};
|
||||
|
||||
export const findEntry = (tree, type, name) => tree.find(
|
||||
f => f.type === type && f.name === name,
|
||||
);
|
||||
|
||||
export const findIndexOfFile = (state, file) => state.findIndex(f => f.path === file.path);
|
||||
|
||||
export const setPageTitle = (title) => {
|
||||
document.title = title;
|
||||
};
|
||||
|
||||
export const createTemp = ({
|
||||
projectId, branchId, name, path, type, level, changed, content, base64, url,
|
||||
}) => {
|
||||
const treePath = path ? `${path}/${name}` : name;
|
||||
|
||||
return decorateData({
|
||||
id: new Date().getTime().toString(),
|
||||
projectId,
|
||||
branchId,
|
||||
name,
|
||||
type,
|
||||
tempFile: true,
|
||||
path: treePath,
|
||||
icon: type === 'tree' ? 'folder' : 'file-text-o',
|
||||
changed,
|
||||
content,
|
||||
parentTreeUrl: '',
|
||||
level,
|
||||
base64,
|
||||
renderError: base64,
|
||||
url,
|
||||
});
|
||||
};
|
||||
|
||||
export const createOrMergeEntry = ({ tree,
|
||||
projectId,
|
||||
branchId,
|
||||
entry,
|
||||
type,
|
||||
parentTreeUrl,
|
||||
level }) => {
|
||||
const found = findEntry(tree.tree || tree, type, entry.name);
|
||||
|
||||
if (found) {
|
||||
return Object.assign({}, found, {
|
||||
id: entry.id,
|
||||
url: entry.url,
|
||||
tempFile: false,
|
||||
});
|
||||
}
|
||||
|
||||
return decorateData({
|
||||
...entry,
|
||||
projectId,
|
||||
branchId,
|
||||
type,
|
||||
parentTreeUrl,
|
||||
level,
|
||||
});
|
||||
};
|
|
@ -302,6 +302,14 @@ export const parseQueryStringIntoObject = (query = '') => {
|
|||
}, {});
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts object with key-value pairs
|
||||
* into query-param string
|
||||
*
|
||||
* @param {Object} params
|
||||
*/
|
||||
export const objectToQueryString = (params = {}) => Object.keys(params).map(param => `${param}=${params[param]}`).join('&');
|
||||
|
||||
export const buildUrlWithCurrentLocation = param => (param ? `${window.location.pathname}${param}` : window.location.pathname);
|
||||
|
||||
/**
|
||||
|
|
|
@ -216,6 +216,9 @@ export default class MilestoneSelect {
|
|||
$value.html(milestoneLinkNoneTemplate);
|
||||
return $sidebarCollapsedValue.find('span').text('No');
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
$loading.fadeOut();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import notesApp from '../notes/components/notes_app.vue';
|
|||
import discussionCounter from '../notes/components/discussion_counter.vue';
|
||||
import store from '../notes/stores';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
export default function initMrNotes() {
|
||||
new Vue({ // eslint-disable-line
|
||||
el: '#js-vue-mr-discussions',
|
||||
components: {
|
||||
|
@ -38,4 +38,4 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
return createElement('discussion-counter');
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
import initTerminal from '~/terminal/';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', initTerminal);
|
|
@ -1,7 +1,13 @@
|
|||
import { hasVueMRDiscussionsCookie } from '~/lib/utils/common_utils';
|
||||
import initMrNotes from '~/mr_notes';
|
||||
import initSidebarBundle from '~/sidebar/sidebar_bundle';
|
||||
import initShow from '../init_merge_request_show';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initShow();
|
||||
initSidebarBundle();
|
||||
|
||||
if (hasVueMRDiscussionsCookie()) {
|
||||
initMrNotes();
|
||||
}
|
||||
});
|
||||
|
|
|
@ -2,6 +2,7 @@ import Vue from 'vue';
|
|||
import PipelinesStore from '../../../../pipelines/stores/pipelines_store';
|
||||
import pipelinesComponent from '../../../../pipelines/components/pipelines.vue';
|
||||
import Translate from '../../../../vue_shared/translate';
|
||||
import { convertPermissionToBoolean } from '../../../../lib/utils/common_utils';
|
||||
|
||||
Vue.use(Translate);
|
||||
|
||||
|
@ -11,16 +12,28 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
|
|||
pipelinesComponent,
|
||||
},
|
||||
data() {
|
||||
const store = new PipelinesStore();
|
||||
|
||||
return {
|
||||
store,
|
||||
store: new PipelinesStore(),
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.dataset = document.querySelector(this.$options.el).dataset;
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement('pipelines-component', {
|
||||
props: {
|
||||
store: this.store,
|
||||
endpoint: this.dataset.endpoint,
|
||||
helpPagePath: this.dataset.helpPagePath,
|
||||
emptyStateSvgPath: this.dataset.emptyStateSvgPath,
|
||||
errorStateSvgPath: this.dataset.errorStateSvgPath,
|
||||
noPipelinesSvgPath: this.dataset.noPipelinesSvgPath,
|
||||
autoDevopsPath: this.dataset.helpAutoDevopsPath,
|
||||
newPipelinePath: this.dataset.newPipelinePath,
|
||||
canCreatePipeline: convertPermissionToBoolean(this.dataset.canCreatePipeline),
|
||||
hasGitlabCi: convertPermissionToBoolean(this.dataset.hasGitlabCi),
|
||||
ciLintPath: this.dataset.ciLintPath,
|
||||
resetCachePath: this.dataset.resetCachePath,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
|
|
@ -5,6 +5,7 @@ export default ({
|
|||
filteredSearchTokenKeys,
|
||||
isGroup,
|
||||
isGroupAncestor,
|
||||
isGroupDecendent,
|
||||
stateFiltersSelector,
|
||||
}) => {
|
||||
const filteredSearchEnabled = FilteredSearchManager && document.querySelector('.filtered-search');
|
||||
|
@ -13,6 +14,7 @@ export default ({
|
|||
page,
|
||||
isGroup,
|
||||
isGroupAncestor,
|
||||
isGroupDecendent,
|
||||
filteredSearchTokenKeys,
|
||||
stateFiltersSelector,
|
||||
});
|
||||
|
|
32
app/assets/javascripts/pipelines/components/blank_state.vue
Normal file
32
app/assets/javascripts/pipelines/components/blank_state.vue
Normal file
|
@ -0,0 +1,32 @@
|
|||
<script>
|
||||
export default {
|
||||
name: 'PipelinesSvgState',
|
||||
props: {
|
||||
svgPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
message: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="row empty-state">
|
||||
<div class="col-xs-12">
|
||||
<div class="svg-content">
|
||||
<img :src="svgPath" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-xs-12 text-center">
|
||||
<div class="text-content">
|
||||
<h4>{{ message }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -1,5 +1,6 @@
|
|||
<script>
|
||||
export default {
|
||||
name: 'PipelinesEmptyState',
|
||||
props: {
|
||||
helpPagePath: {
|
||||
type: String,
|
||||
|
@ -9,6 +10,10 @@
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
canSetCi: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -22,22 +27,36 @@
|
|||
|
||||
<div class="col-xs-12">
|
||||
<div class="text-content">
|
||||
<h4 class="text-center">
|
||||
{{ s__("Pipelines|Build with confidence") }}
|
||||
</h4>
|
||||
<p>
|
||||
{{ s__(`Pipelines|Continous Integration can help
|
||||
catch bugs by running your tests automatically,
|
||||
while Continuous Deployment can help you deliver code to your product environment.`) }}
|
||||
|
||||
<template v-if="canSetCi">
|
||||
<h4 class="text-center">
|
||||
{{ s__('Pipelines|Build with confidence') }}
|
||||
</h4>
|
||||
|
||||
<p>
|
||||
{{ s__(`Pipelines|Continous Integration can help
|
||||
catch bugs by running your tests automatically,
|
||||
while Continuous Deployment can help you deliver
|
||||
code to your product environment.`) }}
|
||||
</p>
|
||||
|
||||
<div class="text-center">
|
||||
<a
|
||||
:href="helpPagePath"
|
||||
class="btn btn-primary js-get-started-pipelines"
|
||||
>
|
||||
{{ s__('Pipelines|Get started with Pipelines') }}
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<p
|
||||
v-else
|
||||
class="text-center"
|
||||
>
|
||||
{{ s__('Pipelines|This project is not currently set up to run pipelines.') }}
|
||||
</p>
|
||||
<div class="text-center">
|
||||
<a
|
||||
:href="helpPagePath"
|
||||
class="btn btn-info"
|
||||
>
|
||||
{{ s__("Pipelines|Get started with Pipelines") }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
<script>
|
||||
export default {
|
||||
props: {
|
||||
errorStateSvgPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="row empty-state js-pipelines-error-state">
|
||||
<div class="col-xs-12">
|
||||
<div class="svg-content">
|
||||
<img :src="errorStateSvgPath"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-xs-12 text-center">
|
||||
<div class="text-content">
|
||||
<h4>The API failed to fetch the pipelines.</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -1,67 +1,52 @@
|
|||
<script>
|
||||
export default {
|
||||
name: 'PipelineNavControls',
|
||||
props: {
|
||||
newPipelinePath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
export default {
|
||||
name: 'PipelineNavControls',
|
||||
props: {
|
||||
newPipelinePath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
|
||||
hasCiEnabled: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
resetCachePath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
|
||||
helpPagePath: {
|
||||
type: String,
|
||||
required: true,
|
||||
ciLintPath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
|
||||
resetCachePath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
ciLintPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
canCreatePipeline: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="nav-controls">
|
||||
<a
|
||||
v-if="canCreatePipeline"
|
||||
v-if="newPipelinePath"
|
||||
:href="newPipelinePath"
|
||||
class="btn btn-create">
|
||||
Run Pipeline
|
||||
</a>
|
||||
|
||||
<a
|
||||
v-if="!hasCiEnabled"
|
||||
:href="helpPagePath"
|
||||
class="btn btn-info">
|
||||
Get started with Pipelines
|
||||
class="btn btn-create js-run-pipeline"
|
||||
>
|
||||
{{ s__('Pipelines|Run Pipeline') }}
|
||||
</a>
|
||||
|
||||
<a
|
||||
v-if="resetCachePath"
|
||||
data-method="post"
|
||||
rel="nofollow"
|
||||
:href="resetCachePath"
|
||||
class="btn btn-default">
|
||||
Clear runner caches
|
||||
class="btn btn-default js-clear-cache"
|
||||
>
|
||||
{{ s__('Pipelines|Clear Runner Caches') }}
|
||||
</a>
|
||||
|
||||
<a
|
||||
v-if="ciLintPath"
|
||||
:href="ciLintPath"
|
||||
class="btn btn-default">
|
||||
CI Lint
|
||||
class="btn btn-default js-ci-lint"
|
||||
>
|
||||
{{ s__('Pipelines|CI Lint') }}
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
<script>
|
||||
import _ from 'underscore';
|
||||
import { __, sprintf, s__ } from '../../locale';
|
||||
import PipelinesService from '../services/pipelines_service';
|
||||
import pipelinesMixin from '../mixins/pipelines';
|
||||
import tablePagination from '../../vue_shared/components/table_pagination.vue';
|
||||
import navigationTabs from '../../vue_shared/components/navigation_tabs.vue';
|
||||
import navigationControls from './nav_controls.vue';
|
||||
import TablePagination from '../../vue_shared/components/table_pagination.vue';
|
||||
import NavigationTabs from '../../vue_shared/components/navigation_tabs.vue';
|
||||
import NavigationControls from './nav_controls.vue';
|
||||
import {
|
||||
convertPermissionToBoolean,
|
||||
getParameterByName,
|
||||
parseQueryStringIntoObject,
|
||||
} from '../../lib/utils/common_utils';
|
||||
|
@ -14,9 +14,9 @@
|
|||
|
||||
export default {
|
||||
components: {
|
||||
tablePagination,
|
||||
navigationTabs,
|
||||
navigationControls,
|
||||
TablePagination,
|
||||
NavigationTabs,
|
||||
NavigationControls,
|
||||
},
|
||||
mixins: [
|
||||
pipelinesMixin,
|
||||
|
@ -36,111 +36,186 @@
|
|||
required: false,
|
||||
default: 'root',
|
||||
},
|
||||
endpoint: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
helpPagePath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
emptyStateSvgPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
errorStateSvgPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
noPipelinesSvgPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
autoDevopsPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
hasGitlabCi: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
canCreatePipeline: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
ciLintPath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
resetCachePath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
newPipelinePath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
const pipelinesData = document.querySelector('#pipelines-list-vue').dataset;
|
||||
|
||||
return {
|
||||
endpoint: pipelinesData.endpoint,
|
||||
helpPagePath: pipelinesData.helpPagePath,
|
||||
emptyStateSvgPath: pipelinesData.emptyStateSvgPath,
|
||||
errorStateSvgPath: pipelinesData.errorStateSvgPath,
|
||||
autoDevopsPath: pipelinesData.helpAutoDevopsPath,
|
||||
newPipelinePath: pipelinesData.newPipelinePath,
|
||||
canCreatePipeline: pipelinesData.canCreatePipeline,
|
||||
hasCi: pipelinesData.hasCi,
|
||||
ciLintPath: pipelinesData.ciLintPath,
|
||||
resetCachePath: pipelinesData.resetCachePath,
|
||||
// Start with loading state to avoid a glitch when the empty state will be rendered
|
||||
isLoading: true,
|
||||
state: this.store.state,
|
||||
scope: getParameterByName('scope') || 'all',
|
||||
page: getParameterByName('page') || '1',
|
||||
requestData: {},
|
||||
};
|
||||
},
|
||||
stateMap: {
|
||||
// with tabs
|
||||
loading: 'loading',
|
||||
tableList: 'tableList',
|
||||
error: 'error',
|
||||
emptyTab: 'emptyTab',
|
||||
|
||||
// without tabs
|
||||
emptyState: 'emptyState',
|
||||
},
|
||||
scopes: {
|
||||
all: 'all',
|
||||
pending: 'pending',
|
||||
running: 'running',
|
||||
finished: 'finished',
|
||||
branches: 'branches',
|
||||
tags: 'tags',
|
||||
},
|
||||
computed: {
|
||||
canCreatePipelineParsed() {
|
||||
return convertPermissionToBoolean(this.canCreatePipeline);
|
||||
},
|
||||
|
||||
/**
|
||||
* The empty state should only be rendered when the request is made to fetch all pipelines
|
||||
* and none is returned.
|
||||
*
|
||||
* @return {Boolean}
|
||||
*/
|
||||
shouldRenderEmptyState() {
|
||||
return !this.isLoading &&
|
||||
!this.hasError &&
|
||||
this.hasMadeRequest &&
|
||||
!this.state.pipelines.length &&
|
||||
(this.scope === 'all' || this.scope === null);
|
||||
},
|
||||
/**
|
||||
* When a specific scope does not have pipelines we render a message.
|
||||
*
|
||||
* @return {Boolean}
|
||||
* `hasGitlabCi` handles both internal and external CI.
|
||||
* The order on which the checks are made in this method is
|
||||
* important to guarantee we handle all the corner cases.
|
||||
*/
|
||||
shouldRenderNoPipelinesMessage() {
|
||||
return !this.isLoading &&
|
||||
!this.hasError &&
|
||||
!this.state.pipelines.length &&
|
||||
this.scope !== 'all' &&
|
||||
this.scope !== null;
|
||||
},
|
||||
stateToRender() {
|
||||
const { stateMap } = this.$options;
|
||||
|
||||
shouldRenderTable() {
|
||||
return !this.hasError &&
|
||||
!this.isLoading && this.state.pipelines.length;
|
||||
if (this.isLoading) {
|
||||
return stateMap.loading;
|
||||
}
|
||||
|
||||
if (this.hasError) {
|
||||
return stateMap.error;
|
||||
}
|
||||
|
||||
if (this.state.pipelines.length) {
|
||||
return stateMap.tableList;
|
||||
}
|
||||
|
||||
if ((this.scope !== 'all' && this.scope !== null) || this.hasGitlabCi) {
|
||||
return stateMap.emptyTab;
|
||||
}
|
||||
|
||||
return stateMap.emptyState;
|
||||
},
|
||||
/**
|
||||
* Pagination should only be rendered when there is more than one page.
|
||||
*
|
||||
* @return {Boolean}
|
||||
*/
|
||||
* Tabs are rendered in all states except empty state.
|
||||
* They are not rendered before the first request to avoid a flicker on first load.
|
||||
*/
|
||||
shouldRenderTabs() {
|
||||
const { stateMap } = this.$options;
|
||||
return this.hasMadeRequest &&
|
||||
[
|
||||
stateMap.loading,
|
||||
stateMap.tableList,
|
||||
stateMap.error,
|
||||
stateMap.emptyTab,
|
||||
].includes(this.stateToRender);
|
||||
},
|
||||
|
||||
shouldRenderButtons() {
|
||||
return (this.newPipelinePath ||
|
||||
this.resetCachePath ||
|
||||
this.ciLintPath) && this.shouldRenderTabs;
|
||||
},
|
||||
|
||||
shouldRenderPagination() {
|
||||
return !this.isLoading &&
|
||||
this.state.pipelines.length &&
|
||||
this.state.pageInfo.total > this.state.pageInfo.perPage;
|
||||
},
|
||||
hasCiEnabled() {
|
||||
return this.hasCi !== undefined;
|
||||
|
||||
emptyTabMessage() {
|
||||
const { scopes } = this.$options;
|
||||
const possibleScopes = [scopes.pending, scopes.running, scopes.finished];
|
||||
|
||||
if (possibleScopes.includes(this.scope)) {
|
||||
return sprintf(s__('Pipelines|There are currently no %{scope} pipelines.'), {
|
||||
scope: this.scope,
|
||||
});
|
||||
}
|
||||
|
||||
return s__('Pipelines|There are currently no pipelines.');
|
||||
},
|
||||
|
||||
tabs() {
|
||||
const { count } = this.state;
|
||||
const { scopes } = this.$options;
|
||||
|
||||
return [
|
||||
{
|
||||
name: 'All',
|
||||
scope: 'all',
|
||||
name: __('All'),
|
||||
scope: scopes.all,
|
||||
count: count.all,
|
||||
isActive: this.scope === 'all',
|
||||
},
|
||||
{
|
||||
name: 'Pending',
|
||||
scope: 'pending',
|
||||
name: __('Pending'),
|
||||
scope: scopes.pending,
|
||||
count: count.pending,
|
||||
isActive: this.scope === 'pending',
|
||||
},
|
||||
{
|
||||
name: 'Running',
|
||||
scope: 'running',
|
||||
name: __('Running'),
|
||||
scope: scopes.running,
|
||||
count: count.running,
|
||||
isActive: this.scope === 'running',
|
||||
},
|
||||
{
|
||||
name: 'Finished',
|
||||
scope: 'finished',
|
||||
name: __('Finished'),
|
||||
scope: scopes.finished,
|
||||
count: count.finished,
|
||||
isActive: this.scope === 'finished',
|
||||
},
|
||||
{
|
||||
name: 'Branches',
|
||||
scope: 'branches',
|
||||
name: __('Branches'),
|
||||
scope: scopes.branches,
|
||||
isActive: this.scope === 'branches',
|
||||
},
|
||||
{
|
||||
name: 'Tags',
|
||||
scope: 'tags',
|
||||
name: __('Tags'),
|
||||
scope: scopes.tags,
|
||||
isActive: this.scope === 'tags',
|
||||
},
|
||||
];
|
||||
|
@ -187,7 +262,7 @@
|
|||
this.errorCallback();
|
||||
|
||||
// restart polling
|
||||
this.poll.restart();
|
||||
this.poll.restart({ data: this.requestData });
|
||||
});
|
||||
},
|
||||
},
|
||||
|
@ -197,69 +272,70 @@
|
|||
<div class="pipelines-container">
|
||||
<div
|
||||
class="top-area scrolling-tabs-container inner-page-scroll-tabs"
|
||||
v-if="!shouldRenderEmptyState"
|
||||
v-if="shouldRenderTabs || shouldRenderButtons"
|
||||
>
|
||||
<div class="fade-left">
|
||||
<i
|
||||
class="fa fa-angle-left"
|
||||
aria-hidden="true">
|
||||
aria-hidden="true"
|
||||
>
|
||||
</i>
|
||||
</div>
|
||||
<div class="fade-right">
|
||||
<i
|
||||
class="fa fa-angle-right"
|
||||
aria-hidden="true">
|
||||
aria-hidden="true"
|
||||
>
|
||||
</i>
|
||||
</div>
|
||||
|
||||
<navigation-tabs
|
||||
v-if="shouldRenderTabs"
|
||||
:tabs="tabs"
|
||||
@onChangeTab="onChangeTab"
|
||||
scope="pipelines"
|
||||
/>
|
||||
|
||||
<navigation-controls
|
||||
v-if="shouldRenderButtons"
|
||||
:new-pipeline-path="newPipelinePath"
|
||||
:has-ci-enabled="hasCiEnabled"
|
||||
:help-page-path="helpPagePath"
|
||||
:reset-cache-path="resetCachePath"
|
||||
:ci-lint-path="ciLintPath"
|
||||
:can-create-pipeline="canCreatePipelineParsed "
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="content-list pipelines">
|
||||
|
||||
<loading-icon
|
||||
label="Loading Pipelines"
|
||||
v-if="stateToRender === $options.stateMap.loading"
|
||||
:label="s__('Pipelines|Loading Pipelines')"
|
||||
size="3"
|
||||
v-if="isLoading"
|
||||
class="prepend-top-20"
|
||||
/>
|
||||
|
||||
<empty-state
|
||||
v-if="shouldRenderEmptyState"
|
||||
v-else-if="stateToRender === $options.stateMap.emptyState"
|
||||
:help-page-path="helpPagePath"
|
||||
:empty-state-svg-path="emptyStateSvgPath"
|
||||
:can-set-ci="canCreatePipeline"
|
||||
/>
|
||||
|
||||
<error-state
|
||||
v-if="shouldRenderErrorState"
|
||||
:error-state-svg-path="errorStateSvgPath"
|
||||
<svg-blank-state
|
||||
v-else-if="stateToRender === $options.stateMap.error"
|
||||
:svg-path="errorStateSvgPath"
|
||||
:message="s__(`Pipelines|There was an error fetching the pipelines.
|
||||
Try again in a few moments or contact your support team.`)"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="blank-state-row"
|
||||
v-if="shouldRenderNoPipelinesMessage"
|
||||
>
|
||||
<div class="blank-state-center">
|
||||
<h2 class="blank-state-title js-blank-state-title">No pipelines to show.</h2>
|
||||
</div>
|
||||
</div>
|
||||
<svg-blank-state
|
||||
v-else-if="stateToRender === $options.stateMap.emptyTab"
|
||||
:svg-path="noPipelinesSvgPath"
|
||||
:message="emptyTabMessage"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="table-holder"
|
||||
v-if="shouldRenderTable"
|
||||
v-else-if="stateToRender === $options.stateMap.tableList"
|
||||
>
|
||||
|
||||
<pipelines-table-component
|
||||
|
|
|
@ -1,23 +1,19 @@
|
|||
import Visibility from 'visibilityjs';
|
||||
import { __ } from '../../locale';
|
||||
import Flash from '../../flash';
|
||||
import Poll from '../../lib/utils/poll';
|
||||
import emptyState from '../components/empty_state.vue';
|
||||
import errorState from '../components/error_state.vue';
|
||||
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
|
||||
import pipelinesTableComponent from '../components/pipelines_table.vue';
|
||||
import EmptyState from '../components/empty_state.vue';
|
||||
import SvgBlankState from '../components/blank_state.vue';
|
||||
import LoadingIcon from '../../vue_shared/components/loading_icon.vue';
|
||||
import PipelinesTableComponent from '../components/pipelines_table.vue';
|
||||
import eventHub from '../event_hub';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
pipelinesTableComponent,
|
||||
errorState,
|
||||
emptyState,
|
||||
loadingIcon,
|
||||
},
|
||||
computed: {
|
||||
shouldRenderErrorState() {
|
||||
return this.hasError && !this.isLoading;
|
||||
},
|
||||
PipelinesTableComponent,
|
||||
SvgBlankState,
|
||||
EmptyState,
|
||||
LoadingIcon,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -85,6 +81,7 @@ export default {
|
|||
this.hasError = true;
|
||||
this.isLoading = false;
|
||||
this.updateGraphDropdown = false;
|
||||
this.hasMadeRequest = true;
|
||||
},
|
||||
setIsMakingRequest(isMakingRequest) {
|
||||
this.isMakingRequest = isMakingRequest;
|
||||
|
@ -96,7 +93,7 @@ export default {
|
|||
postAction(endpoint) {
|
||||
this.service.postAction(endpoint)
|
||||
.then(() => eventHub.$emit('refreshPipelines'))
|
||||
.catch(() => new Flash('An error occurred while making the request.'));
|
||||
.catch(() => Flash(__('An error occurred while making the request.')));
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -6,4 +6,4 @@ import './terminal';
|
|||
|
||||
window.Terminal = Terminal;
|
||||
|
||||
$(() => new gl.Terminal({ selector: '#terminal' }));
|
||||
export default () => new gl.Terminal({ selector: '#terminal' });
|
|
@ -0,0 +1,149 @@
|
|||
<script>
|
||||
import LabelsSelect from '~/labels_select';
|
||||
import LoadingIcon from '../../loading_icon.vue';
|
||||
|
||||
import DropdownTitle from './dropdown_title.vue';
|
||||
import DropdownValue from './dropdown_value.vue';
|
||||
import DropdownValueCollapsed from './dropdown_value_collapsed.vue';
|
||||
import DropdownButton from './dropdown_button.vue';
|
||||
import DropdownHiddenInput from './dropdown_hidden_input.vue';
|
||||
import DropdownHeader from './dropdown_header.vue';
|
||||
import DropdownSearchInput from './dropdown_search_input.vue';
|
||||
import DropdownFooter from './dropdown_footer.vue';
|
||||
import DropdownCreateLabel from './dropdown_create_label.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
LoadingIcon,
|
||||
DropdownTitle,
|
||||
DropdownValue,
|
||||
DropdownValueCollapsed,
|
||||
DropdownButton,
|
||||
DropdownHiddenInput,
|
||||
DropdownHeader,
|
||||
DropdownSearchInput,
|
||||
DropdownFooter,
|
||||
DropdownCreateLabel,
|
||||
},
|
||||
props: {
|
||||
showCreate: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
abilityName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
context: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
namespace: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
updatePath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
labelsPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
labelsWebUrl: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
labelFilterBasePath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
canEdit: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
hiddenInputName() {
|
||||
return this.showCreate ? `${this.abilityName}[label_names][]` : 'label_id[]';
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.labelsDropdown = new LabelsSelect(this.$refs.dropdownButton, {
|
||||
handleClick: this.handleClick,
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
handleClick(label) {
|
||||
this.$emit('onLabelClick', label);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="block labels">
|
||||
<dropdown-value-collapsed
|
||||
v-if="showCreate"
|
||||
:labels="context.labels"
|
||||
/>
|
||||
<dropdown-title
|
||||
:can-edit="canEdit"
|
||||
/>
|
||||
<dropdown-value
|
||||
:labels="context.labels"
|
||||
:label-filter-base-path="labelFilterBasePath"
|
||||
>
|
||||
<slot></slot>
|
||||
</dropdown-value>
|
||||
<div
|
||||
v-if="canEdit"
|
||||
class="selectbox"
|
||||
style="display: none;"
|
||||
>
|
||||
<dropdown-hidden-input
|
||||
v-for="label in context.labels"
|
||||
:key="label.id"
|
||||
:name="hiddenInputName"
|
||||
:label="label"
|
||||
/>
|
||||
<div class="dropdown">
|
||||
<dropdown-button
|
||||
:ability-name="abilityName"
|
||||
:field-name="hiddenInputName"
|
||||
:update-path="updatePath"
|
||||
:labels-path="labelsPath"
|
||||
:namespace="namespace"
|
||||
:labels="context.labels"
|
||||
:show-extra-options="!showCreate"
|
||||
/>
|
||||
<div
|
||||
class="dropdown-menu dropdown-select dropdown-menu-paging
|
||||
dropdown-menu-labels dropdown-menu-selectable"
|
||||
>
|
||||
<div class="dropdown-page-one">
|
||||
<dropdown-header v-if="showCreate" />
|
||||
<dropdown-search-input/>
|
||||
<div class="dropdown-content"></div>
|
||||
<div class="dropdown-loading">
|
||||
<loading-icon />
|
||||
</div>
|
||||
<dropdown-footer
|
||||
v-if="showCreate"
|
||||
:labels-web-url="labelsWebUrl"
|
||||
/>
|
||||
</div>
|
||||
<dropdown-create-label
|
||||
v-if="showCreate"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,78 @@
|
|||
<script>
|
||||
import { __, s__, sprintf } from '~/locale';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
abilityName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
fieldName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
updatePath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
labelsPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
namespace: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
labels: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
showExtraOptions: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
dropdownToggleText() {
|
||||
if (this.labels.length === 0) {
|
||||
return __('Label');
|
||||
}
|
||||
|
||||
if (this.labels.length > 1) {
|
||||
return sprintf(s__('LabelSelect|%{firstLabelName} +%{remainingLabelCount} more'), {
|
||||
firstLabelName: this.labels[0].title,
|
||||
remainingLabelCount: this.labels.length - 1,
|
||||
});
|
||||
}
|
||||
|
||||
return this.labels[0].title;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
type="button"
|
||||
ref="dropdownButton"
|
||||
class="dropdown-menu-toggle wide js-label-select js-multiselect js-context-config-modal"
|
||||
data-toggle="dropdown"
|
||||
:class="{ 'js-extra-options': showExtraOptions }"
|
||||
:data-ability-name="abilityName"
|
||||
:data-field-name="fieldName"
|
||||
:data-issue-update="updatePath"
|
||||
:data-labels="labelsPath"
|
||||
:data-namespace-path="namespace"
|
||||
:data-show-any="showExtraOptions"
|
||||
>
|
||||
<span class="dropdown-toggle-text">
|
||||
{{ dropdownToggleText }}
|
||||
</span>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
class="fa fa-chevron-down"
|
||||
data-hidden="true"
|
||||
>
|
||||
</i>
|
||||
</button>
|
||||
</template>
|
|
@ -0,0 +1,84 @@
|
|||
<script>
|
||||
export default {
|
||||
created() {
|
||||
this.suggestedColors = gon.suggested_label_colors;
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dropdown-page-two dropdown-new-label">
|
||||
<div class="dropdown-title">
|
||||
<button
|
||||
type="button"
|
||||
class="dropdown-title-button dropdown-menu-back"
|
||||
:aria-label="__('Go back')"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
class="fa fa-arrow-left"
|
||||
data-hidden="true"
|
||||
>
|
||||
</i>
|
||||
</button>
|
||||
{{ __('Create new label') }}
|
||||
<button
|
||||
type="button"
|
||||
class="dropdown-title-button dropdown-menu-close"
|
||||
:aria-label="__('Close')"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
class="fa fa-times dropdown-menu-close-icon"
|
||||
data-hidden="true"
|
||||
>
|
||||
</i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="dropdown-content">
|
||||
<div class="dropdown-labels-error js-label-error"></div>
|
||||
<input
|
||||
id="new_label_name"
|
||||
type="text"
|
||||
class="default-dropdown-input"
|
||||
:placeholder="__('Name new label')"
|
||||
/>
|
||||
<div class="suggest-colors suggest-colors-dropdown">
|
||||
<a
|
||||
v-for="(color, index) in suggestedColors"
|
||||
href="#"
|
||||
:key="index"
|
||||
:data-color="color"
|
||||
:style="{
|
||||
backgroundColor: color,
|
||||
}"
|
||||
>
|
||||
|
||||
</a>
|
||||
</div>
|
||||
<div class="dropdown-label-color-input">
|
||||
<div class="dropdown-label-color-preview js-dropdown-label-color-preview"></div>
|
||||
<input
|
||||
id="new_label_color"
|
||||
type="text"
|
||||
class="default-dropdown-input"
|
||||
:placeholder="__('Assign custom color like #FF0000')"
|
||||
/>
|
||||
</div>
|
||||
<div class="clearfix">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary pull-left js-new-label-btn disabled"
|
||||
>
|
||||
{{ __('Create') }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-default pull-right js-cancel-label-btn"
|
||||
>
|
||||
{{ __('Cancel') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,34 @@
|
|||
<script>
|
||||
export default {
|
||||
props: {
|
||||
labelsWebUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dropdown-footer">
|
||||
<ul class="dropdown-footer-list">
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
class="dropdown-toggle-page"
|
||||
>
|
||||
{{ __('Create new label') }}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
data-is-link="true"
|
||||
class="dropdown-external-link"
|
||||
:href="labelsWebUrl"
|
||||
>
|
||||
{{ __('Manage labels') }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,21 @@
|
|||
<script>
|
||||
export default {};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dropdown-title">
|
||||
<span>{{ __('Assign labels') }}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="dropdown-title-button dropdown-menu-close"
|
||||
:aria-label="__('Close')"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
class="fa fa-times dropdown-menu-close-icon"
|
||||
data-hidden="true"
|
||||
>
|
||||
</i>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,22 @@
|
|||
<script>
|
||||
export default {
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
label: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input
|
||||
type="hidden"
|
||||
:name="name"
|
||||
:value="label.id"
|
||||
/>
|
||||
</template>
|
|
@ -0,0 +1,27 @@
|
|||
<script>
|
||||
export default {};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dropdown-input">
|
||||
<input
|
||||
autocomplete="off"
|
||||
class="dropdown-input-field"
|
||||
type="search"
|
||||
:placeholder="__('Search')"
|
||||
/>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
class="fa fa-search dropdown-input-search"
|
||||
data-hidden="true"
|
||||
>
|
||||
</i>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
class="fa fa-times dropdown-input-clear js-dropdown-input-clear"
|
||||
data-hidden="true"
|
||||
role="button"
|
||||
>
|
||||
</i>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,30 @@
|
|||
<script>
|
||||
export default {
|
||||
props: {
|
||||
canEdit: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="title hide-collapsed append-bottom-10">
|
||||
{{ __('Labels') }}
|
||||
<template v-if="canEdit">
|
||||
<i
|
||||
aria-hidden="true"
|
||||
class="fa fa-spinner fa-spin block-loading"
|
||||
data-hidden="true"
|
||||
>
|
||||
</i>
|
||||
<button
|
||||
type="button"
|
||||
class="edit-link btn btn-blank pull-right js-sidebar-dropdown-toggle"
|
||||
>
|
||||
{{ __('Edit') }}
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,63 @@
|
|||
<script>
|
||||
import tooltip from '~/vue_shared/directives/tooltip';
|
||||
|
||||
export default {
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
props: {
|
||||
labels: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
labelFilterBasePath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
isEmpty() {
|
||||
return this.labels.length === 0;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
labelFilterUrl(label) {
|
||||
return `${this.labelFilterBasePath}?label_name[]=${encodeURIComponent(label.title)}`;
|
||||
},
|
||||
labelStyle(label) {
|
||||
return {
|
||||
color: label.textColor,
|
||||
backgroundColor: label.color,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="hide-collapsed value issuable-show-labels">
|
||||
<span
|
||||
v-if="isEmpty"
|
||||
class="text-secondary"
|
||||
>
|
||||
<slot>{{ __('None') }}</slot>
|
||||
</span>
|
||||
<a
|
||||
v-else
|
||||
v-for="label in labels"
|
||||
:key="label.id"
|
||||
:href="labelFilterUrl(label)"
|
||||
>
|
||||
<span
|
||||
v-tooltip
|
||||
class="label color-label"
|
||||
data-placement="bottom"
|
||||
data-container="body"
|
||||
:style="labelStyle(label)"
|
||||
:title="label.description"
|
||||
>
|
||||
{{ label.title }}
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,48 @@
|
|||
<script>
|
||||
import { s__, sprintf } from '~/locale';
|
||||
import tooltip from '~/vue_shared/directives/tooltip';
|
||||
|
||||
export default {
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
props: {
|
||||
labels: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
labelsList() {
|
||||
const labelsString = this.labels.slice(0, 5).map(label => label.title).join(', ');
|
||||
|
||||
if (this.labels.length > 5) {
|
||||
return sprintf(s__('LabelSelect|%{labelsString}, and %{remainingLabelCount} more'), {
|
||||
labelsString,
|
||||
remainingLabelCount: this.labels.length - 5,
|
||||
});
|
||||
}
|
||||
|
||||
return labelsString;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-tooltip
|
||||
class="sidebar-collapsed-icon"
|
||||
data-placement="left"
|
||||
data-container="body"
|
||||
:title="labelsList"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
data-hidden="true"
|
||||
class="fa fa-tags"
|
||||
>
|
||||
</i>
|
||||
<span>{{ labels.length }}</span>
|
||||
</div>
|
||||
</template>
|
|
@ -1,7 +1,5 @@
|
|||
/* eslint-disable no-unused-vars, space-before-function-paren */
|
||||
|
||||
class ListLabel {
|
||||
constructor (obj) {
|
||||
constructor(obj) {
|
||||
this.id = obj.id;
|
||||
this.title = obj.title;
|
||||
this.type = obj.type;
|
|
@ -9,7 +9,6 @@ class Admin::ImpersonationTokensController < Admin::ApplicationController
|
|||
@impersonation_token = finder.build(impersonation_token_params)
|
||||
|
||||
if @impersonation_token.save
|
||||
flash[:impersonation_token] = @impersonation_token.token
|
||||
redirect_to admin_user_impersonation_tokens_path, notice: "A new impersonation token has been created."
|
||||
else
|
||||
set_index_vars
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
class IdeController < ApplicationController
|
||||
layout 'nav_only'
|
||||
|
||||
def index
|
||||
end
|
||||
end
|
|
@ -62,7 +62,7 @@ class InvitesController < ApplicationController
|
|||
case source
|
||||
when Project
|
||||
project = member.source
|
||||
label = "project #{project.name_with_namespace}"
|
||||
label = "project #{project.full_name}"
|
||||
path = project_path(project)
|
||||
when Group
|
||||
group = member.source
|
||||
|
|
|
@ -38,7 +38,7 @@ class Projects::BlobController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
format.json do
|
||||
page_title @blob.path, @ref, @project.name_with_namespace
|
||||
page_title @blob.path, @ref, @project.full_name
|
||||
|
||||
show_json
|
||||
end
|
||||
|
|
|
@ -7,13 +7,19 @@ class Projects::BranchesController < Projects::ApplicationController
|
|||
before_action :authorize_download_code!
|
||||
before_action :authorize_push_code!, only: [:new, :create, :destroy, :destroy_all_merged]
|
||||
|
||||
def index
|
||||
@sort = params[:sort].presence || sort_value_recently_updated
|
||||
@branches = BranchesFinder.new(@repository, params.merge(sort: @sort)).execute
|
||||
@branches = Kaminari.paginate_array(@branches).page(params[:page])
|
||||
# Support legacy URLs
|
||||
before_action :redirect_for_legacy_index_sort_or_search, only: [:index]
|
||||
|
||||
def index
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
@sort = params[:sort].presence || sort_value_recently_updated
|
||||
@mode = params[:state].presence || 'overview'
|
||||
@overview_max_branches = 5
|
||||
|
||||
# Fetch branches for the specified mode
|
||||
fetch_branches_by_mode
|
||||
|
||||
@refs_pipelines = @project.pipelines.latest_successful_for_refs(@branches.map(&:name))
|
||||
@merged_branch_names =
|
||||
repository.merged_branch_names(@branches.map(&:name))
|
||||
|
@ -28,7 +34,9 @@ class Projects::BranchesController < Projects::ApplicationController
|
|||
end
|
||||
end
|
||||
format.json do
|
||||
render json: @branches.map(&:name)
|
||||
branches = BranchesFinder.new(@repository, params).execute
|
||||
branches = Kaminari.paginate_array(branches).page(params[:page])
|
||||
render json: branches.map(&:name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -123,4 +131,27 @@ class Projects::BranchesController < Projects::ApplicationController
|
|||
context: 'autodeploy'
|
||||
)
|
||||
end
|
||||
|
||||
def redirect_for_legacy_index_sort_or_search
|
||||
# Normalize a legacy URL with redirect
|
||||
if request.format != :json && !params[:state].presence && [:sort, :search, :page].any? { |key| params[key].presence }
|
||||
redirect_to project_branches_filtered_path(@project, state: 'all'), notice: 'Update your bookmarked URLs as filtered/sorted branches URL has been changed.'
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_branches_by_mode
|
||||
if @mode == 'overview'
|
||||
# overview mode
|
||||
@active_branches, @stale_branches = BranchesFinder.new(@repository, sort: sort_value_recently_updated).execute.partition(&:active?)
|
||||
# Here we get one more branch to indicate if there are more data we're not showing
|
||||
@active_branches = @active_branches.first(@overview_max_branches + 1)
|
||||
@stale_branches = @stale_branches.first(@overview_max_branches + 1)
|
||||
@branches = @active_branches + @stale_branches
|
||||
else
|
||||
# active/stale/all view mode
|
||||
@branches = BranchesFinder.new(@repository, params.merge(sort: @sort)).execute
|
||||
@branches = @branches.select { |b| b.state.to_s == @mode } if %w[active stale].include?(@mode)
|
||||
@branches = Kaminari.paginate_array(@branches).page(params[:page])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -17,10 +17,8 @@ class Projects::CompareController < Projects::ApplicationController
|
|||
|
||||
def show
|
||||
apply_diff_view_cookie!
|
||||
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37430
|
||||
Gitlab::GitalyClient.allow_n_plus_1_calls do
|
||||
render
|
||||
end
|
||||
|
||||
render
|
||||
end
|
||||
|
||||
def diff_for_path
|
||||
|
|
|
@ -9,25 +9,22 @@ class Projects::NetworkController < Projects::ApplicationController
|
|||
before_action :assign_commit
|
||||
|
||||
def show
|
||||
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37602
|
||||
Gitlab::GitalyClient.allow_n_plus_1_calls do
|
||||
@url = project_network_path(@project, @ref, @options.merge(format: :json))
|
||||
@commit_url = project_commit_path(@project, 'ae45ca32').gsub("ae45ca32", "%s")
|
||||
@url = project_network_path(@project, @ref, @options.merge(format: :json))
|
||||
@commit_url = project_commit_path(@project, 'ae45ca32').gsub("ae45ca32", "%s")
|
||||
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
if @options[:extended_sha1] && !@commit
|
||||
flash.now[:alert] = "Git revision '#{@options[:extended_sha1]}' does not exist."
|
||||
end
|
||||
end
|
||||
|
||||
format.json do
|
||||
@graph = Network::Graph.new(project, @ref, @commit, @options[:filter_ref])
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
if @options[:extended_sha1] && !@commit
|
||||
flash.now[:alert] = "Git revision '#{@options[:extended_sha1]}' does not exist."
|
||||
end
|
||||
end
|
||||
|
||||
render
|
||||
format.json do
|
||||
@graph = Network::Graph.new(project, @ref, @commit, @options[:filter_ref])
|
||||
end
|
||||
end
|
||||
|
||||
render
|
||||
end
|
||||
|
||||
def assign_commit
|
||||
|
|
|
@ -36,7 +36,7 @@ class Projects::TreeController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
format.json do
|
||||
page_title @path.presence || _("Files"), @ref, @project.name_with_namespace
|
||||
page_title @path.presence || _("Files"), @ref, @project.full_name
|
||||
|
||||
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/38261
|
||||
Gitlab::GitalyClient.allow_n_plus_1_calls do
|
||||
|
|
|
@ -41,11 +41,11 @@ class ProjectsController < Projects::ApplicationController
|
|||
cookies[:issue_board_welcome_hidden] = { path: project_path(@project), value: nil, expires: Time.at(0) }
|
||||
|
||||
redirect_to(
|
||||
project_path(@project),
|
||||
project_path(@project, custom_import_params),
|
||||
notice: _("Project '%{project_name}' was successfully created.") % { project_name: @project.name }
|
||||
)
|
||||
else
|
||||
render 'new', locals: { active_tab: ('import' if project_params[:import_url].present?) }
|
||||
render 'new', locals: { active_tab: active_new_project_tab }
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -103,7 +103,7 @@ class ProjectsController < Projects::ApplicationController
|
|||
|
||||
def show
|
||||
if @project.import_in_progress?
|
||||
redirect_to project_import_path(@project)
|
||||
redirect_to project_import_path(@project, custom_import_params)
|
||||
return
|
||||
end
|
||||
|
||||
|
@ -130,7 +130,7 @@ class ProjectsController < Projects::ApplicationController
|
|||
return access_denied! unless can?(current_user, :remove_project, @project)
|
||||
|
||||
::Projects::DestroyService.new(@project, current_user, {}).async_execute
|
||||
flash[:notice] = _("Project '%{project_name}' is in the process of being deleted.") % { project_name: @project.name_with_namespace }
|
||||
flash[:notice] = _("Project '%{project_name}' is in the process of being deleted.") % { project_name: @project.full_name }
|
||||
|
||||
redirect_to dashboard_projects_path, status: 302
|
||||
rescue Projects::DestroyService::DestroyError => ex
|
||||
|
@ -359,6 +359,14 @@ class ProjectsController < Projects::ApplicationController
|
|||
]
|
||||
end
|
||||
|
||||
def custom_import_params
|
||||
{}
|
||||
end
|
||||
|
||||
def active_new_project_tab
|
||||
project_params[:import_url].present? ? 'import' : 'blank'
|
||||
end
|
||||
|
||||
def repo_exists?
|
||||
project.repository_exists? && !project.empty_repo?
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
class BranchesFinder
|
||||
def initialize(repository, params)
|
||||
def initialize(repository, params = {})
|
||||
@repository = repository
|
||||
@params = params
|
||||
end
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue