diff options
Diffstat (limited to 'app')
203 files changed, 1567 insertions, 4223 deletions
diff --git a/app/assets/javascripts/behaviors/toggler_behavior.js b/app/assets/javascripts/behaviors/toggler_behavior.js index 417ac31fc86..81c89441424 100644 --- a/app/assets/javascripts/behaviors/toggler_behavior.js +++ b/app/assets/javascripts/behaviors/toggler_behavior.js @@ -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(); diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js index b8749a13f68..efc0da2e7a2 100644 --- a/app/assets/javascripts/boards/index.js +++ b/app/assets/javascripts/boards/index.js @@ -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'; diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue index ce19069f103..466a5b5d635 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_table.vue +++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue @@ -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" diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js index ee49a7be0b2..e6390f0855b 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js @@ -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(); diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js index c6970d7837f..71b7e80335b 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js @@ -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; diff --git a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js index a19bb882410..600024c21c3 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js +++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js @@ -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)) diff --git a/app/assets/javascripts/groups/components/app.vue b/app/assets/javascripts/groups/components/app.vue index b8f0566f48c..0578f43d5af 100644 --- a/app/assets/javascripts/groups/components/app.vue +++ b/app/assets/javascripts/groups/components/app.vue @@ -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" diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list.vue b/app/assets/javascripts/ide/components/commit_sidebar/list.vue deleted file mode 100644 index a8459b011df..00000000000 --- a/app/assets/javascripts/ide/components/commit_sidebar/list.vue +++ /dev/null @@ -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> diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue deleted file mode 100644 index 6a0262f271b..00000000000 --- a/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue +++ /dev/null @@ -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> diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue deleted file mode 100644 index 742f746e02f..00000000000 --- a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue +++ /dev/null @@ -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> diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue deleted file mode 100644 index 89981ab2c65..00000000000 --- a/app/assets/javascripts/ide/components/ide.vue +++ /dev/null @@ -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> diff --git a/app/assets/javascripts/ide/components/ide_context_bar.vue b/app/assets/javascripts/ide/components/ide_context_bar.vue deleted file mode 100644 index 9d933b8891d..00000000000 --- a/app/assets/javascripts/ide/components/ide_context_bar.vue +++ /dev/null @@ -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> diff --git a/app/assets/javascripts/ide/components/ide_project_branches_tree.vue b/app/assets/javascripts/ide/components/ide_project_branches_tree.vue deleted file mode 100644 index 2fbff2bd789..00000000000 --- a/app/assets/javascripts/ide/components/ide_project_branches_tree.vue +++ /dev/null @@ -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> diff --git a/app/assets/javascripts/ide/components/ide_project_tree.vue b/app/assets/javascripts/ide/components/ide_project_tree.vue deleted file mode 100644 index 32bf7175c88..00000000000 --- a/app/assets/javascripts/ide/components/ide_project_tree.vue +++ /dev/null @@ -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> diff --git a/app/assets/javascripts/ide/components/ide_repo_tree.vue b/app/assets/javascripts/ide/components/ide_repo_tree.vue deleted file mode 100644 index 4a324264992..00000000000 --- a/app/assets/javascripts/ide/components/ide_repo_tree.vue +++ /dev/null @@ -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> diff --git a/app/assets/javascripts/ide/components/ide_side_bar.vue b/app/assets/javascripts/ide/components/ide_side_bar.vue deleted file mode 100644 index 18b5059a17f..00000000000 --- a/app/assets/javascripts/ide/components/ide_side_bar.vue +++ /dev/null @@ -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> diff --git a/app/assets/javascripts/ide/components/ide_status_bar.vue b/app/assets/javascripts/ide/components/ide_status_bar.vue deleted file mode 100644 index 97ae64b206d..00000000000 --- a/app/assets/javascripts/ide/components/ide_status_bar.vue +++ /dev/null @@ -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> diff --git a/app/assets/javascripts/ide/components/new_branch_form.vue b/app/assets/javascripts/ide/components/new_branch_form.vue deleted file mode 100644 index 1e8d5bb6453..00000000000 --- a/app/assets/javascripts/ide/components/new_branch_form.vue +++ /dev/null @@ -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> diff --git a/app/assets/javascripts/ide/components/new_dropdown/index.vue b/app/assets/javascripts/ide/components/new_dropdown/index.vue deleted file mode 100644 index ef653357f5f..00000000000 --- a/app/assets/javascripts/ide/components/new_dropdown/index.vue +++ /dev/null @@ -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> diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue deleted file mode 100644 index 36cd825c6dd..00000000000 --- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue +++ /dev/null @@ -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> diff --git a/app/assets/javascripts/ide/components/new_dropdown/upload.vue b/app/assets/javascripts/ide/components/new_dropdown/upload.vue deleted file mode 100644 index 6244737fa43..00000000000 --- a/app/assets/javascripts/ide/components/new_dropdown/upload.vue +++ /dev/null @@ -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> diff --git a/app/assets/javascripts/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue deleted file mode 100644 index 37f2cf30a29..00000000000 --- a/app/assets/javascripts/ide/components/repo_commit_section.vue +++ /dev/null @@ -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> diff --git a/app/assets/javascripts/ide/components/repo_edit_button.vue b/app/assets/javascripts/ide/components/repo_edit_button.vue deleted file mode 100644 index fe4320731d9..00000000000 --- a/app/assets/javascripts/ide/components/repo_edit_button.vue +++ /dev/null @@ -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> diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue deleted file mode 100644 index f31cc12339b..00000000000 --- a/app/assets/javascripts/ide/components/repo_editor.vue +++ /dev/null @@ -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> diff --git a/app/assets/javascripts/ide/components/repo_file.vue b/app/assets/javascripts/ide/components/repo_file.vue deleted file mode 100644 index cbbab765e1c..00000000000 --- a/app/assets/javascripts/ide/components/repo_file.vue +++ /dev/null @@ -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> diff --git a/app/assets/javascripts/ide/components/repo_file_buttons.vue b/app/assets/javascripts/ide/components/repo_file_buttons.vue deleted file mode 100644 index aabc0d8eada..00000000000 --- a/app/assets/javascripts/ide/components/repo_file_buttons.vue +++ /dev/null @@ -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> diff --git a/app/assets/javascripts/ide/components/repo_loading_file.vue b/app/assets/javascripts/ide/components/repo_loading_file.vue deleted file mode 100644 index 79af8c0b0c7..00000000000 --- a/app/assets/javascripts/ide/components/repo_loading_file.vue +++ /dev/null @@ -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> diff --git a/app/assets/javascripts/ide/components/repo_prev_directory.vue b/app/assets/javascripts/ide/components/repo_prev_directory.vue deleted file mode 100644 index 7cd359ea4ed..00000000000 --- a/app/assets/javascripts/ide/components/repo_prev_directory.vue +++ /dev/null @@ -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> diff --git a/app/assets/javascripts/ide/components/repo_preview.vue b/app/assets/javascripts/ide/components/repo_preview.vue deleted file mode 100644 index a216269e292..00000000000 --- a/app/assets/javascripts/ide/components/repo_preview.vue +++ /dev/null @@ -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> diff --git a/app/assets/javascripts/ide/components/repo_tab.vue b/app/assets/javascripts/ide/components/repo_tab.vue deleted file mode 100644 index 5656081c598..00000000000 --- a/app/assets/javascripts/ide/components/repo_tab.vue +++ /dev/null @@ -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> diff --git a/app/assets/javascripts/ide/components/repo_tabs.vue b/app/assets/javascripts/ide/components/repo_tabs.vue deleted file mode 100644 index ca363bba0ef..00000000000 --- a/app/assets/javascripts/ide/components/repo_tabs.vue +++ /dev/null @@ -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> diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js deleted file mode 100644 index a7fb9e0588a..00000000000 --- a/app/assets/javascripts/ide/ide_router.js +++ /dev/null @@ -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; diff --git a/app/assets/javascripts/ide/index.js b/app/assets/javascripts/ide/index.js deleted file mode 100644 index e8a19f47cee..00000000000 --- a/app/assets/javascripts/ide/index.js +++ /dev/null @@ -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); diff --git a/app/assets/javascripts/ide/lib/common/disposable.js b/app/assets/javascripts/ide/lib/common/disposable.js deleted file mode 100644 index 84b29bdb600..00000000000 --- a/app/assets/javascripts/ide/lib/common/disposable.js +++ /dev/null @@ -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(); - } -} diff --git a/app/assets/javascripts/ide/lib/common/model.js b/app/assets/javascripts/ide/lib/common/model.js deleted file mode 100644 index 14d9fe4771e..00000000000 --- a/app/assets/javascripts/ide/lib/common/model.js +++ /dev/null @@ -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(); - } -} diff --git a/app/assets/javascripts/ide/lib/common/model_manager.js b/app/assets/javascripts/ide/lib/common/model_manager.js deleted file mode 100644 index fd462252795..00000000000 --- a/app/assets/javascripts/ide/lib/common/model_manager.js +++ /dev/null @@ -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(); - } -} diff --git a/app/assets/javascripts/ide/lib/decorations/controller.js b/app/assets/javascripts/ide/lib/decorations/controller.js deleted file mode 100644 index 0954b7973c4..00000000000 --- a/app/assets/javascripts/ide/lib/decorations/controller.js +++ /dev/null @@ -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(); - } -} diff --git a/app/assets/javascripts/ide/lib/diff/controller.js b/app/assets/javascripts/ide/lib/diff/controller.js deleted file mode 100644 index dc0b1c95e59..00000000000 --- a/app/assets/javascripts/ide/lib/diff/controller.js +++ /dev/null @@ -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(); - } -} diff --git a/app/assets/javascripts/ide/lib/diff/diff.js b/app/assets/javascripts/ide/lib/diff/diff.js deleted file mode 100644 index 0e37f5c4704..00000000000 --- a/app/assets/javascripts/ide/lib/diff/diff.js +++ /dev/null @@ -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; - }, []); -}; diff --git a/app/assets/javascripts/ide/lib/diff/diff_worker.js b/app/assets/javascripts/ide/lib/diff/diff_worker.js deleted file mode 100644 index e74c4046330..00000000000 --- a/app/assets/javascripts/ide/lib/diff/diff_worker.js +++ /dev/null @@ -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), - }); -}); diff --git a/app/assets/javascripts/ide/lib/editor.js b/app/assets/javascripts/ide/lib/editor.js deleted file mode 100644 index 51255f15658..00000000000 --- a/app/assets/javascripts/ide/lib/editor.js +++ /dev/null @@ -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)), - ); - } -} diff --git a/app/assets/javascripts/ide/lib/editor_options.js b/app/assets/javascripts/ide/lib/editor_options.js deleted file mode 100644 index 701affc466e..00000000000 --- a/app/assets/javascripts/ide/lib/editor_options.js +++ /dev/null @@ -1,2 +0,0 @@ -export default [{ -}]; diff --git a/app/assets/javascripts/ide/monaco_loader.js b/app/assets/javascripts/ide/monaco_loader.js deleted file mode 100644 index 142a220097b..00000000000 --- a/app/assets/javascripts/ide/monaco_loader.js +++ /dev/null @@ -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; diff --git a/app/assets/javascripts/ide/services/index.js b/app/assets/javascripts/ide/services/index.js deleted file mode 100644 index 1fb24e93f2e..00000000000 --- a/app/assets/javascripts/ide/services/index.js +++ /dev/null @@ -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', - }, - }); - }, -}; diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js deleted file mode 100644 index 2c690b1f635..00000000000 --- a/app/assets/javascripts/ide/stores/actions.js +++ /dev/null @@ -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'; diff --git a/app/assets/javascripts/ide/stores/actions/branch.js b/app/assets/javascripts/ide/stores/actions/branch.js deleted file mode 100644 index bc6fd2d4163..00000000000 --- a/app/assets/javascripts/ide/stores/actions/branch.js +++ /dev/null @@ -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); -}); diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js deleted file mode 100644 index 670af2fb89e..00000000000 --- a/app/assets/javascripts/ide/stores/actions/file.js +++ /dev/null @@ -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); -}; diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js deleted file mode 100644 index faeceb430a2..00000000000 --- a/app/assets/javascripts/ide/stores/actions/project.js +++ /dev/null @@ -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}`]); - } -}); diff --git a/app/assets/javascripts/ide/stores/actions/tree.js b/app/assets/javascripts/ide/stores/actions/tree.js deleted file mode 100644 index 302ba45edee..00000000000 --- a/app/assets/javascripts/ide/stores/actions/tree.js +++ /dev/null @@ -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 }); -}; diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js deleted file mode 100644 index 6b51ccff817..00000000000 --- a/app/assets/javascripts/ide/stores/getters.js +++ /dev/null @@ -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); diff --git a/app/assets/javascripts/ide/stores/index.js b/app/assets/javascripts/ide/stores/index.js deleted file mode 100644 index 6ac9bfd8189..00000000000 --- a/app/assets/javascripts/ide/stores/index.js +++ /dev/null @@ -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, -}); diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js deleted file mode 100644 index 69b218a5e7d..00000000000 --- a/app/assets/javascripts/ide/stores/mutation_types.js +++ /dev/null @@ -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'; - diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js deleted file mode 100644 index 03d81be10a1..00000000000 --- a/app/assets/javascripts/ide/stores/mutations.js +++ /dev/null @@ -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, -}; diff --git a/app/assets/javascripts/ide/stores/mutations/branch.js b/app/assets/javascripts/ide/stores/mutations/branch.js deleted file mode 100644 index 04b9582c5bb..00000000000 --- a/app/assets/javascripts/ide/stores/mutations/branch.js +++ /dev/null @@ -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, - }); - }, -}; diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js deleted file mode 100644 index 72db1c180c9..00000000000 --- a/app/assets/javascripts/ide/stores/mutations/file.js +++ /dev/null @@ -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); - }, -}; diff --git a/app/assets/javascripts/ide/stores/mutations/project.js b/app/assets/javascripts/ide/stores/mutations/project.js deleted file mode 100644 index 2816562a919..00000000000 --- a/app/assets/javascripts/ide/stores/mutations/project.js +++ /dev/null @@ -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, - }), - }); - }, -}; diff --git a/app/assets/javascripts/ide/stores/mutations/tree.js b/app/assets/javascripts/ide/stores/mutations/tree.js deleted file mode 100644 index 4fe438ab465..00000000000 --- a/app/assets/javascripts/ide/stores/mutations/tree.js +++ /dev/null @@ -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); - }, -}; diff --git a/app/assets/javascripts/ide/stores/state.js b/app/assets/javascripts/ide/stores/state.js deleted file mode 100644 index 61d12096946..00000000000 --- a/app/assets/javascripts/ide/stores/state.js +++ /dev/null @@ -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, -}); diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js deleted file mode 100644 index d556404faa5..00000000000 --- a/app/assets/javascripts/ide/stores/utils.js +++ /dev/null @@ -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, - }); -}; diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index e741789fbb6..ed90db317df 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -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); /** diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index 2841ecb558b..c259d5405bd 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -216,6 +216,9 @@ export default class MilestoneSelect { $value.html(milestoneLinkNoneTemplate); return $sidebarCollapsedValue.find('span').text('No'); } + }) + .catch(() => { + $loading.fadeOut(); }); } } diff --git a/app/assets/javascripts/mr_notes/index.js b/app/assets/javascripts/mr_notes/index.js index f4cba998fa7..972fdb2b791 100644 --- a/app/assets/javascripts/mr_notes/index.js +++ b/app/assets/javascripts/mr_notes/index.js @@ -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'); }, }); -}); +} diff --git a/app/assets/javascripts/pages/projects/environments/terminal/index.js b/app/assets/javascripts/pages/projects/environments/terminal/index.js new file mode 100644 index 00000000000..7129e24cee1 --- /dev/null +++ b/app/assets/javascripts/pages/projects/environments/terminal/index.js @@ -0,0 +1,3 @@ +import initTerminal from '~/terminal/'; + +document.addEventListener('DOMContentLoaded', initTerminal); diff --git a/app/assets/javascripts/pages/projects/merge_requests/show/index.js b/app/assets/javascripts/pages/projects/merge_requests/show/index.js index 3e72f7a6f37..e5b2827b50c 100644 --- a/app/assets/javascripts/pages/projects/merge_requests/show/index.js +++ b/app/assets/javascripts/pages/projects/merge_requests/show/index.js @@ -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(); + } }); diff --git a/app/assets/javascripts/pages/projects/pipelines/index/index.js b/app/assets/javascripts/pages/projects/pipelines/index/index.js index 25dfa99ad9c..a84e2790680 100644 --- a/app/assets/javascripts/pages/projects/pipelines/index/index.js +++ b/app/assets/javascripts/pages/projects/pipelines/index/index.js @@ -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, }, }); }, diff --git a/app/assets/javascripts/pages/search/init_filtered_search.js b/app/assets/javascripts/pages/search/init_filtered_search.js index 57f08701a4f..7fdf4ee0bf3 100644 --- a/app/assets/javascripts/pages/search/init_filtered_search.js +++ b/app/assets/javascripts/pages/search/init_filtered_search.js @@ -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, }); diff --git a/app/assets/javascripts/pipelines/components/blank_state.vue b/app/assets/javascripts/pipelines/components/blank_state.vue new file mode 100644 index 00000000000..8d3d6223d7b --- /dev/null +++ b/app/assets/javascripts/pipelines/components/blank_state.vue @@ -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> diff --git a/app/assets/javascripts/pipelines/components/empty_state.vue b/app/assets/javascripts/pipelines/components/empty_state.vue index dfaa2574091..10ac8c08bed 100644 --- a/app/assets/javascripts/pipelines/components/empty_state.vue +++ b/app/assets/javascripts/pipelines/components/empty_state.vue @@ -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> diff --git a/app/assets/javascripts/pipelines/components/error_state.vue b/app/assets/javascripts/pipelines/components/error_state.vue deleted file mode 100644 index 012853b201d..00000000000 --- a/app/assets/javascripts/pipelines/components/error_state.vue +++ /dev/null @@ -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> diff --git a/app/assets/javascripts/pipelines/components/nav_controls.vue b/app/assets/javascripts/pipelines/components/nav_controls.vue index f31a91c3403..383ab51fe56 100644 --- a/app/assets/javascripts/pipelines/components/nav_controls.vue +++ b/app/assets/javascripts/pipelines/components/nav_controls.vue @@ -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, + }, + + resetCachePath: { + type: String, + required: false, + default: null, + }, + + ciLintPath: { + type: String, + required: false, + default: null, + }, }, - - hasCiEnabled: { - type: Boolean, - required: true, - }, - - helpPagePath: { - type: String, - required: true, - }, - - 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> diff --git a/app/assets/javascripts/pipelines/components/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines.vue index 90930d5ff44..6e5ee68eeb1 100644 --- a/app/assets/javascripts/pipelines/components/pipelines.vue +++ b/app/assets/javascripts/pipelines/components/pipelines.vue @@ -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: {}, }; }, - computed: { - canCreatePipelineParsed() { - return convertPermissionToBoolean(this.canCreatePipeline); - }, + 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: { /** - * 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); + * `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. + */ + stateToRender() { + const { stateMap } = this.$options; + + 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; }, /** - * When a specific scope does not have pipelines we render a message. - * - * @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. */ - shouldRenderNoPipelinesMessage() { - return !this.isLoading && - !this.hasError && - !this.state.pipelines.length && - this.scope !== 'all' && - this.scope !== null; + shouldRenderTabs() { + const { stateMap } = this.$options; + return this.hasMadeRequest && + [ + stateMap.loading, + stateMap.tableList, + stateMap.error, + stateMap.emptyTab, + ].includes(this.stateToRender); }, - shouldRenderTable() { - return !this.hasError && - !this.isLoading && this.state.pipelines.length; + shouldRenderButtons() { + return (this.newPipelinePath || + this.resetCachePath || + this.ciLintPath) && this.shouldRenderTabs; }, - /** - * Pagination should only be rendered when there is more than one page. - * - * @return {Boolean} - */ + 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 diff --git a/app/assets/javascripts/pipelines/mixins/pipelines.js b/app/assets/javascripts/pipelines/mixins/pipelines.js index 50bdf80c3e3..9fcc07abee5 100644 --- a/app/assets/javascripts/pipelines/mixins/pipelines.js +++ b/app/assets/javascripts/pipelines/mixins/pipelines.js @@ -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.'))); }, }, }; diff --git a/app/assets/javascripts/terminal/terminal_bundle.js b/app/assets/javascripts/terminal/index.js index 134522ef961..1a75e072c4e 100644 --- a/app/assets/javascripts/terminal/terminal_bundle.js +++ b/app/assets/javascripts/terminal/index.js @@ -6,4 +6,4 @@ import './terminal'; window.Terminal = Terminal; -$(() => new gl.Terminal({ selector: '#terminal' })); +export default () => new gl.Terminal({ selector: '#terminal' }); diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue new file mode 100644 index 00000000000..3b17135f0e5 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue @@ -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> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue new file mode 100644 index 00000000000..47497c1de98 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue @@ -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> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue new file mode 100644 index 00000000000..4200d1e8473 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue @@ -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> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer.vue new file mode 100644 index 00000000000..e951a863811 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer.vue @@ -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> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header.vue new file mode 100644 index 00000000000..7664acdf19c --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header.vue @@ -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> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input.vue new file mode 100644 index 00000000000..1832c3c1757 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input.vue @@ -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> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input.vue new file mode 100644 index 00000000000..ae633460c95 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input.vue @@ -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> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title.vue new file mode 100644 index 00000000000..7da82e90e29 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title.vue @@ -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> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue new file mode 100644 index 00000000000..ba4c8fba5ec --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue @@ -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> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue new file mode 100644 index 00000000000..5cf728fe050 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue @@ -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> diff --git a/app/assets/javascripts/boards/models/label.js b/app/assets/javascripts/vue_shared/models/label.js index 98c1ec014c4..70b9efe0c68 100644 --- a/app/assets/javascripts/boards/models/label.js +++ b/app/assets/javascripts/vue_shared/models/label.js @@ -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; diff --git a/app/controllers/admin/impersonation_tokens_controller.rb b/app/controllers/admin/impersonation_tokens_controller.rb index 7a2c7234a1e..a7b562b1d8e 100644 --- a/app/controllers/admin/impersonation_tokens_controller.rb +++ b/app/controllers/admin/impersonation_tokens_controller.rb @@ -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 diff --git a/app/controllers/ide_controller.rb b/app/controllers/ide_controller.rb deleted file mode 100644 index 1ff25a45398..00000000000 --- a/app/controllers/ide_controller.rb +++ /dev/null @@ -1,6 +0,0 @@ -class IdeController < ApplicationController - layout 'nav_only' - - def index - end -end diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb index 52430ea771f..025d8270b7c 100644 --- a/app/controllers/invites_controller.rb +++ b/app/controllers/invites_controller.rb @@ -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 diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 74c25505e36..405726c017c 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -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 diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb index cabafe26357..965cece600e 100644 --- a/app/controllers/projects/branches_controller.rb +++ b/app/controllers/projects/branches_controller.rb @@ -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 diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index 3cb4eb23981..2b0c2ca97c0 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -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 diff --git a/app/controllers/projects/network_controller.rb b/app/controllers/projects/network_controller.rb index 3b10a93e97f..35fec229db7 100644 --- a/app/controllers/projects/network_controller.rb +++ b/app/controllers/projects/network_controller.rb @@ -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") - - 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 + @url = project_network_path(@project, @ref, @options.merge(format: :json)) + @commit_url = project_commit_path(@project, 'ae45ca32').gsub("ae45ca32", "%s") - 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 diff --git a/app/controllers/projects/tree_controller.rb b/app/controllers/projects/tree_controller.rb index f752a46f828..ee9b5458282 100644 --- a/app/controllers/projects/tree_controller.rb +++ b/app/controllers/projects/tree_controller.rb @@ -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 diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 913689a1e74..ee197c75764 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -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? diff --git a/app/finders/branches_finder.rb b/app/finders/branches_finder.rb index 852eac3647d..8bb1366867c 100644 --- a/app/finders/branches_finder.rb +++ b/app/finders/branches_finder.rb @@ -1,5 +1,5 @@ class BranchesFinder - def initialize(repository, params) + def initialize(repository, params = {}) @repository = repository @params = params end diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 9dd6634b38f..b2d4f9938ff 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -19,6 +19,10 @@ # non_archived: boolean # iids: integer[] # my_reaction_emoji: string +# created_after: datetime +# created_before: datetime +# updated_after: datetime +# updated_before: datetime # class IssuableFinder prepend FinderWithCrossProjectAccess @@ -79,6 +83,7 @@ class IssuableFinder def filter_items(items) items = by_scope(items) items = by_created_at(items) + items = by_updated_at(items) items = by_state(items) items = by_group(items) items = by_search(items) @@ -283,6 +288,13 @@ class IssuableFinder end end + def by_updated_at(items) + items = items.updated_after(params[:updated_after]) if params[:updated_after].present? + items = items.updated_before(params[:updated_before]) if params[:updated_before].present? + + items + end + def by_state(items) case params[:state].to_s when 'closed' diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb index d65c620e75a..2a27ff0e386 100644 --- a/app/finders/issues_finder.rb +++ b/app/finders/issues_finder.rb @@ -17,6 +17,10 @@ # my_reaction_emoji: string # public_only: boolean # due_date: date or '0', '', 'overdue', 'week', or 'month' +# created_after: datetime +# created_before: datetime +# updated_after: datetime +# updated_before: datetime # class IssuesFinder < IssuableFinder CONFIDENTIAL_ACCESS_LEVEL = Gitlab::Access::REPORTER diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb index 068ae7f8c89..64dc1e6af0f 100644 --- a/app/finders/merge_requests_finder.rb +++ b/app/finders/merge_requests_finder.rb @@ -19,6 +19,10 @@ # my_reaction_emoji: string # source_branch: string # target_branch: string +# created_after: datetime +# created_before: datetime +# updated_after: datetime +# updated_before: datetime # class MergeRequestsFinder < IssuableFinder def klass diff --git a/app/finders/notes_finder.rb b/app/finders/notes_finder.rb index 33ee1e975b9..35f4ff2f62f 100644 --- a/app/finders/notes_finder.rb +++ b/app/finders/notes_finder.rb @@ -48,11 +48,23 @@ class NotesFinder def init_collection if target notes_on_target + elsif target_type + notes_of_target_type else notes_of_any_type end end + def notes_of_target_type + notes = notes_for_type(target_type) + + search(notes) + end + + def target_type + @params[:target_type] + end + def notes_of_any_type types = %w(commit issue merge_request snippet) note_relations = types.map { |t| notes_for_type(t) } diff --git a/app/finders/snippets_finder.rb b/app/finders/snippets_finder.rb index a73c573736e..d498a2d6d11 100644 --- a/app/finders/snippets_finder.rb +++ b/app/finders/snippets_finder.rb @@ -58,11 +58,37 @@ class SnippetsFinder < UnionFinder .public_or_visible_to_user(current_user) end + # Returns a collection of projects that is either public or visible to the + # logged in user. + # + # A caller must pass in a block to modify individual parts of + # the query, e.g. to apply .with_feature_available_for_user on top of it. + # This is useful for performance as we can stick those additional filters + # at the bottom of e.g. the UNION. + def projects_for_user + return yield(Project.public_to_user) unless current_user + + # If the current_user is allowed to see all projects, + # we can shortcut and just return. + return yield(Project.all) if current_user.full_private_access? + + authorized_projects = yield(Project.where('EXISTS (?)', current_user.authorizations_for_projects)) + + levels = Gitlab::VisibilityLevel.levels_for_user(current_user) + visible_projects = yield(Project.where(visibility_level: levels)) + + # We use a UNION here instead of OR clauses since this results in better + # performance. + union = Gitlab::SQL::Union.new([authorized_projects.select('projects.id'), visible_projects.select('projects.id')]) + + Project.from("(#{union.to_sql}) AS #{Project.table_name}") + end + def feature_available_projects # Don't return any project related snippets if the user cannot read cross project return table[:id].eq(nil) unless Ability.allowed?(current_user, :read_cross_project) - projects = Project.public_or_visible_to_user(current_user, use_where_in: false) do |part| + projects = projects_for_user do |part| part.with_feature_available_for_user(:snippets, current_user) end.select(:id) diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb index edb17843002..150f4c7688b 100644 --- a/app/finders/todos_finder.rb +++ b/app/finders/todos_finder.rb @@ -110,10 +110,6 @@ class TodosFinder ids end - def projects(items) - ProjectsFinder.new(current_user: current_user, project_ids_relation: project_ids(items)).execute - end - def type? type.present? && %w(Issue MergeRequest).include?(type) end @@ -152,13 +148,12 @@ class TodosFinder def by_project(items) if project? - items = items.where(project: project) + items.where(project: project) else - item_projects = projects(items) - items = items.merge(item_projects).joins(:project) - end + projects = Project.public_or_visible_to_user(current_user) - items + items.joins(:project).merge(projects) + end end def by_state(items) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 475341cf9b1..af9c8bf1bd3 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -320,10 +320,6 @@ module ApplicationHelper cookies["sidebar_collapsed"] == "true" end - def show_new_ide? - cookies["new_repo"] == "true" && body_data_page != 'projects:show' - end - def locale_path asset_path("locale/#{Gitlab::I18n.locale}/app.js") end diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 0e806d16bc5..5ff09b23a78 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -33,20 +33,6 @@ module BlobHelper ref) end - def ide_edit_button(project = @project, ref = @ref, path = @path, options = {}) - return unless show_new_ide? - return unless blob = readable_blob(options, path, project, ref) - - common_classes = "btn js-edit-ide #{options[:extra_class]}" - - edit_button_tag(blob, - common_classes, - _('Web IDE'), - ide_edit_path(project, ref, path, options), - project, - ref) - end - def modify_file_button(project = @project, ref = @ref, path = @path, label:, action:, btn_class:, modal_type:) return unless current_user diff --git a/app/helpers/branches_helper.rb b/app/helpers/branches_helper.rb index 00b9a0e00eb..07b1fc3d7cf 100644 --- a/app/helpers/branches_helper.rb +++ b/app/helpers/branches_helper.rb @@ -1,15 +1,4 @@ module BranchesHelper - def filter_branches_path(options = {}) - exist_opts = { - search: params[:search], - sort: params[:sort] - } - - options = exist_opts.merge(options) - - project_branches_path(@project, @id, options) - end - def project_branches options_for_select(@project.repository.branch_names, @project.default_branch) end diff --git a/app/helpers/import_helper.rb b/app/helpers/import_helper.rb index a18ebfb6030..b484a868f92 100644 --- a/app/helpers/import_helper.rb +++ b/app/helpers/import_helper.rb @@ -1,19 +1,45 @@ module ImportHelper + def has_ci_cd_only_params? + false + end + def import_project_target(owner, name) namespace = current_user.can_create_group? ? owner : current_user.namespace_path "#{namespace}/#{name}" end - def provider_project_link(provider, path_with_namespace) - url = __send__("#{provider}_project_url", path_with_namespace) # rubocop:disable GitlabSecurity/PublicSend + def provider_project_link(provider, full_path) + url = __send__("#{provider}_project_url", full_path) # rubocop:disable GitlabSecurity/PublicSend + + link_to full_path, url, target: '_blank', rel: 'noopener noreferrer' + end + + def import_will_timeout_message(_ci_cd_only) + timeout = time_interval_in_words(Gitlab.config.gitlab_shell.git_timeout) + _('The import will time out after %{timeout}. For repositories that take longer, use a clone/push combination.') % { timeout: timeout } + end + + def import_svn_message(_ci_cd_only) + svn_link = link_to _('this document'), help_page_path('user/project/import/svn') + _('To import an SVN repository, check out %{svn_link}.').html_safe % { svn_link: svn_link } + end + + def import_in_progress_title + if @project.forked? + _('Forking in progress') + else + _('Import in progress') + end + end - link_to path_with_namespace, url, target: '_blank', rel: 'noopener noreferrer' + def import_wait_and_refresh_message + _('Please wait while we import the repository for you. Refresh at will.') end private - def github_project_url(path_with_namespace) - "#{github_root_url}/#{path_with_namespace}" + def github_project_url(full_path) + "#{github_root_url}/#{full_path}" end def github_root_url @@ -23,7 +49,7 @@ module ImportHelper @github_url = provider.fetch('url', 'https://github.com') if provider end - def gitea_project_url(path_with_namespace) - "#{@gitea_host_url.sub(%r{/+\z}, '')}/#{path_with_namespace}" + def gitea_project_url(full_path) + "#{@gitea_host_url.sub(%r{/+\z}, '')}/#{full_path}" end end diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 44ecc2212f2..f6ddb6d4cfe 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -99,7 +99,7 @@ module IssuablesHelper project = Project.find_by(id: project_id) if project - project.name_with_namespace + project.full_name else default_label end diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb index c1c19062c91..b2c641a5dbd 100644 --- a/app/helpers/labels_helper.rb +++ b/app/helpers/labels_helper.rb @@ -1,4 +1,5 @@ module LabelsHelper + extend self include ActionView::Helpers::TagHelper def show_label_issuables_link?(label, issuables_type, current_user: nil, project: nil) diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb index e86e43b5ebf..a70e73a6da9 100644 --- a/app/helpers/notes_helper.rb +++ b/app/helpers/notes_helper.rb @@ -11,7 +11,7 @@ module NotesHelper end def note_supports_quick_actions?(note) - Notes::QuickActionsService.supported?(note, current_user) + Notes::QuickActionsService.supported?(note) end def noteable_json(noteable) diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index cc1c69a1999..da9fe734f1c 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -97,13 +97,13 @@ module ProjectsHelper end def remove_project_message(project) - _("You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?") % - { project_name_with_namespace: project.name_with_namespace } + _("You are going to remove %{project_full_name}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?") % + { project_full_name: project.full_name } end def transfer_project_message(project) - _("You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?") % - { project_name_with_namespace: project.name_with_namespace } + _("You are going to transfer %{project_full_name} to another owner. Are you ABSOLUTELY sure?") % + { project_full_name: project.full_name } end def remove_fork_project_message(project) diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index e6a6496871a..761c1252fc8 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -110,7 +110,7 @@ module SearchHelper category: "Projects", id: p.id, value: "#{search_result_sanitize(p.name)}", - label: "#{search_result_sanitize(p.name_with_namespace)}", + label: "#{search_result_sanitize(p.full_name)}", url: project_path(p) } end diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb index ddb48371c79..f7620e0b6b8 100644 --- a/app/helpers/todos_helper.rb +++ b/app/helpers/todos_helper.rb @@ -114,7 +114,7 @@ module TodosHelper projects = current_user.authorized_projects.sorted_by_activity.non_archived.with_route projects = projects.map do |project| - { id: project.id, text: project.name_with_namespace } + { id: project.id, text: project.full_name } end projects.unshift({ id: '', text: 'Any Project' }).to_json diff --git a/app/models/badge.rb b/app/models/badge.rb new file mode 100644 index 00000000000..f7e10c2ebfc --- /dev/null +++ b/app/models/badge.rb @@ -0,0 +1,51 @@ +class Badge < ActiveRecord::Base + # This structure sets the placeholders that the urls + # can have. This hash also sets which action to ask when + # the placeholder is found. + PLACEHOLDERS = { + 'project_path' => :full_path, + 'project_id' => :id, + 'default_branch' => :default_branch, + 'commit_sha' => ->(project) { project.commit&.sha } + }.freeze + + # This regex is built dynamically using the keys from the PLACEHOLDER struct. + # So, we can easily add new placeholder just by modifying the PLACEHOLDER hash. + # This regex will build the new PLACEHOLDER_REGEX with the new information + PLACEHOLDERS_REGEX = /(#{PLACEHOLDERS.keys.join('|')})/.freeze + + default_scope { order_created_at_asc } + + scope :order_created_at_asc, -> { reorder(created_at: :asc) } + + validates :link_url, :image_url, url_placeholder: { protocols: %w(http https), placeholder_regex: PLACEHOLDERS_REGEX } + validates :type, presence: true + + def rendered_link_url(project = nil) + build_rendered_url(link_url, project) + end + + def rendered_image_url(project = nil) + build_rendered_url(image_url, project) + end + + private + + def build_rendered_url(url, project = nil) + return url unless valid? && project + + Gitlab::StringPlaceholderReplacer.replace_string_placeholders(url, PLACEHOLDERS_REGEX) do |arg| + replace_placeholder_action(PLACEHOLDERS[arg], project) + end + end + + # The action param represents the :symbol or Proc to call in order + # to retrieve the return value from the project. + # This method checks if it is a Proc and use the call method, and if it is + # a symbol just send the action + def replace_placeholder_action(action, project) + return unless project + + action.is_a?(Proc) ? action.call(project) : project.public_send(action) # rubocop:disable GitlabSecurity/PublicSend + end +end diff --git a/app/models/badges/group_badge.rb b/app/models/badges/group_badge.rb new file mode 100644 index 00000000000..f4b2bdecdcc --- /dev/null +++ b/app/models/badges/group_badge.rb @@ -0,0 +1,5 @@ +class GroupBadge < Badge + belongs_to :group + + validates :group, presence: true +end diff --git a/app/models/badges/project_badge.rb b/app/models/badges/project_badge.rb new file mode 100644 index 00000000000..3945b376052 --- /dev/null +++ b/app/models/badges/project_badge.rb @@ -0,0 +1,15 @@ +class ProjectBadge < Badge + belongs_to :project + + validates :project, presence: true + + def rendered_link_url(project = nil) + project ||= self.project + super + end + + def rendered_image_url(project = nil) + project ||= self.project + super + end +end diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb index 7adf1663c35..16efe90fa27 100644 --- a/app/models/clusters/applications/runner.rb +++ b/app/models/clusters/applications/runner.rb @@ -56,12 +56,13 @@ module Clusters def specification { "gitlabUrl" => gitlab_url, - "runnerToken" => ensure_runner.token + "runnerToken" => ensure_runner.token, + "runners" => { "privileged" => privileged } } end def content_values - specification.merge(YAML.load_file(chart_values_file)) + YAML.load_file(chart_values_file).deep_merge!(specification) end end end diff --git a/app/models/commit.rb b/app/models/commit.rb index b9106309142..cceae5efb72 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -9,6 +9,7 @@ class Commit include Mentionable include Referable include StaticModel + include ::Gitlab::Utils::StrongMemoize attr_mentionable :safe_message, pipeline: :single_line @@ -225,11 +226,13 @@ class Commit end def parents - @parents ||= parent_ids.map { |id| project.commit(id) } + @parents ||= parent_ids.map { |oid| Commit.lazy(project, oid) } end def parent - @parent ||= project.commit(self.parent_id) if self.parent_id + strong_memoize(:parent) do + project.commit_by(oid: self.parent_id) if self.parent_id + end end def notes diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 7049f340c9d..4560bc23193 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -19,6 +19,7 @@ module Issuable include AfterCommitQueue include Sortable include CreatedAtFilterable + include UpdatedAtFilterable # This object is used to gather issuable meta data for displaying # upvotes, downvotes, notes and closing merge requests count for issues and merge requests diff --git a/app/models/concerns/updated_at_filterable.rb b/app/models/concerns/updated_at_filterable.rb new file mode 100644 index 00000000000..edb423b7828 --- /dev/null +++ b/app/models/concerns/updated_at_filterable.rb @@ -0,0 +1,12 @@ +module UpdatedAtFilterable + extend ActiveSupport::Concern + + included do + scope :updated_before, ->(date) { where(scoped_table[:updated_at].lteq(date)) } + scope :updated_after, ->(date) { where(scoped_table[:updated_at].gteq(date)) } + + def self.scoped_table + arel_table.alias(table_name) + end + end +end diff --git a/app/models/event.rb b/app/models/event.rb index 75538ba196c..be0fc7efa9a 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -158,7 +158,7 @@ class Event < ActiveRecord::Base def project_name if project - project.name_with_namespace + project.full_name else "(deleted project)" end diff --git a/app/models/group.rb b/app/models/group.rb index 32a0bd0c70b..8d183006c65 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -32,6 +32,7 @@ class Group < Namespace has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :boards + has_many :badges, class_name: 'GroupBadge' accepts_nested_attributes_for :variables, allow_destroy: true diff --git a/app/models/lfs_object.rb b/app/models/lfs_object.rb index fc586fa216e..b444812a4cf 100644 --- a/app/models/lfs_object.rb +++ b/app/models/lfs_object.rb @@ -15,4 +15,8 @@ class LfsObject < ActiveRecord::Base .where(lfs_objects_projects: { id: nil }) .destroy_all end + + def self.calculate_oid(path) + Digest::SHA256.file(path).hexdigest + end end diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index c1c27ccf3e5..06aa67c600f 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -197,10 +197,6 @@ class MergeRequestDiff < ActiveRecord::Base CompareService.new(project, head_commit_sha).execute(project, sha, straight: true) end - def commits_count - super || merge_request_diff_commits.size - end - private def create_merge_request_diff_files(diffs) diff --git a/app/models/network/commit.rb b/app/models/network/commit.rb index 9357e55b419..22d48c9e661 100644 --- a/app/models/network/commit.rb +++ b/app/models/network/commit.rb @@ -24,12 +24,7 @@ module Network end def parents(map) - @commit.parents.map do |p| - if map.include?(p.id) - map[p.id] - end - end - .compact + map.values_at(*@commit.parent_ids).compact end end end diff --git a/app/models/project.rb b/app/models/project.rb index 5206a7d4d25..5cd1da43645 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -221,6 +221,8 @@ class Project < ActiveRecord::Base has_one :auto_devops, class_name: 'ProjectAutoDevops' has_many :custom_attributes, class_name: 'ProjectCustomAttribute' + has_many :project_badges, class_name: 'ProjectBadge' + accepts_nested_attributes_for :variables, allow_destroy: true accepts_nested_attributes_for :project_feature, update_only: true accepts_nested_attributes_for :import_data @@ -274,7 +276,8 @@ class Project < ActiveRecord::Base scope :without_storage_feature, ->(feature) { where('storage_version < :version OR storage_version IS NULL', version: HASHED_STORAGE_FEATURES[feature]) } scope :with_unmigrated_storage, -> { where('storage_version < :version OR storage_version IS NULL', version: LATEST_STORAGE_VERSION) } - scope :sorted_by_activity, -> { reorder(last_activity_at: :desc) } + # last_activity_at is throttled every minute, but last_repository_updated_at is updated with every push + scope :sorted_by_activity, -> { reorder("GREATEST(COALESCE(last_activity_at, '1970-01-01'), COALESCE(last_repository_updated_at, '1970-01-01')) DESC") } scope :sorted_by_stars, -> { reorder('projects.star_count DESC') } scope :in_namespace, ->(namespace_ids) { where(namespace_id: namespace_ids) } @@ -317,42 +320,13 @@ class Project < ActiveRecord::Base # Returns a collection of projects that is either public or visible to the # logged in user. - # - # A caller may pass in a block to modify individual parts of - # the query, e.g. to apply .with_feature_available_for_user on top of it. - # This is useful for performance as we can stick those additional filters - # at the bottom of e.g. the UNION. - # - # Optionally, turning `use_where_in` off leads to returning a - # relation using #from instead of #where. This can perform much better - # but leads to trouble when used in conjunction with AR's #merge method. - def self.public_or_visible_to_user(user = nil, use_where_in: true, &block) - # If we don't get a block passed, use identity to avoid if/else repetitions - block = ->(part) { part } unless block_given? - - return block.call(public_to_user) unless user - - # If the user is allowed to see all projects, - # we can shortcut and just return. - return block.call(all) if user.full_private_access? - - authorized = user - .project_authorizations - .select(1) - .where('project_authorizations.project_id = projects.id') - authorized_projects = block.call(where('EXISTS (?)', authorized)) - - levels = Gitlab::VisibilityLevel.levels_for_user(user) - visible_projects = block.call(where(visibility_level: levels)) - - # We use a UNION here instead of OR clauses since this results in better - # performance. - union = Gitlab::SQL::Union.new([authorized_projects.select('projects.id'), visible_projects.select('projects.id')]) - - if use_where_in - where("projects.id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection + def self.public_or_visible_to_user(user = nil) + if user + where('EXISTS (?) OR projects.visibility_level IN (?)', + user.authorizations_for_projects, + Gitlab::VisibilityLevel.levels_for_user(user)) else - from("(#{union.to_sql}) AS #{table_name}") + public_to_user end end @@ -371,14 +345,11 @@ class Project < ActiveRecord::Base elsif user column = ProjectFeature.quoted_access_level_column(feature) - authorized = user.project_authorizations.select(1) - .where('project_authorizations.project_id = projects.id') - with_project_feature .where("#{column} IN (?) OR (#{column} = ? AND EXISTS (?))", visible, ProjectFeature::PRIVATE, - authorized) + user.authorizations_for_projects) else with_feature_access_level(feature, visible) end @@ -808,7 +779,7 @@ class Project < ActiveRecord::Base end def last_activity_date - last_repository_updated_at || last_activity_at || updated_at + [last_activity_at, last_repository_updated_at, updated_at].compact.max end def project_id @@ -1557,16 +1528,34 @@ class Project < ActiveRecord::Base end end + def import_export_shared + @import_export_shared ||= Gitlab::ImportExport::Shared.new(self) + end + def export_path return nil unless namespace.present? || hashed_storage?(:repository) - File.join(Gitlab::ImportExport.storage_path, disk_path) + import_export_shared.archive_path end def export_project_path Dir.glob("#{export_path}/*export.tar.gz").max_by { |f| File.ctime(f) } end + def export_status + if export_in_progress? + :started + elsif export_project_path + :finished + else + :none + end + end + + def export_in_progress? + import_export_shared.active_export_count > 0 + end + def remove_exports return nil unless export_path.present? @@ -1799,6 +1788,17 @@ class Project < ActiveRecord::Base .set(import_jid, StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION) end + def badges + return project_badges unless group + + group_badges_rel = GroupBadge.where(group: group.self_and_ancestors) + + union = Gitlab::SQL::Union.new([project_badges.select(:id), + group_badges_rel.select(:id)]) + + Badge.where("id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection + end + private def storage diff --git a/app/models/project_services/asana_service.rb b/app/models/project_services/asana_service.rb index 109258d1eb7..4f289e6e215 100644 --- a/app/models/project_services/asana_service.rb +++ b/app/models/project_services/asana_service.rb @@ -68,7 +68,7 @@ http://app.asana.com/-/account_api' end user = data[:user_name] - project_name = project.name_with_namespace + project_name = project.full_name data[:commits].each do |commit| push_msg = "#{user} pushed to branch #{branch} of #{project_name} ( #{commit[:url]} ):" diff --git a/app/models/project_services/campfire_service.rb b/app/models/project_services/campfire_service.rb index c3f5b310619..8d7a4fceb08 100644 --- a/app/models/project_services/campfire_service.rb +++ b/app/models/project_services/campfire_service.rb @@ -86,7 +86,7 @@ class CampfireService < Service after = push[:after] message = "" - message << "[#{project.name_with_namespace}] " + message << "[#{project.full_name}] " message << "#{push[:user_name]} " if Gitlab::Git.blank_ref?(before) diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb index e2ffdf72201..dab0ea1a681 100644 --- a/app/models/project_services/chat_notification_service.rb +++ b/app/models/project_services/chat_notification_service.rb @@ -129,7 +129,7 @@ class ChatNotificationService < Service end def project_name - project.name_with_namespace.gsub(/\s/, '') + project.full_name.gsub(/\s/, '') end def project_url diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index bfe7ac29c18..f31c3f02af2 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -120,7 +120,7 @@ class HipchatService < Service else message << "pushed to #{ref_type} <a href=\""\ "#{project.web_url}/commits/#{CGI.escape(ref)}\">#{ref}</a> " - message << "of <a href=\"#{project.web_url}\">#{project.name_with_namespace.gsub!(/\s/, '')}</a> " + message << "of <a href=\"#{project.web_url}\">#{project.full_name.gsub!(/\s/, '')}</a> " message << "(<a href=\"#{project.web_url}/compare/#{before}...#{after}\">Compare changes</a>)" push[:commits].take(MAX_COMMITS).each do |commit| @@ -274,7 +274,7 @@ class HipchatService < Service end def project_name - project.name_with_namespace.gsub(/\s/, '') + project.full_name.gsub(/\s/, '') end def project_url diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index 436a870b0c4..e5035c81df0 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -1,5 +1,7 @@ class JiraService < IssueTrackerService include Gitlab::Routing + include ApplicationHelper + include ActionView::Helpers::AssetUrlHelper validates :url, url: true, presence: true, if: :activated? validates :api_url, url: true, allow_blank: true @@ -268,7 +270,9 @@ class JiraService < IssueTrackerService url: url, title: title, status: status, - icon: { title: 'GitLab', url16x16: 'https://gitlab.com/favicon.ico' } + icon: { + title: 'GitLab', url16x16: asset_url('favicon.ico', host: gitlab_config.url) + } } } end diff --git a/app/models/project_services/mattermost_slash_commands_service.rb b/app/models/project_services/mattermost_slash_commands_service.rb index 4d2037286a2..227d430083d 100644 --- a/app/models/project_services/mattermost_slash_commands_service.rb +++ b/app/models/project_services/mattermost_slash_commands_service.rb @@ -37,7 +37,7 @@ class MattermostSlashCommandsService < SlashCommandsService private def command(params) - pretty_project_name = project.name_with_namespace + pretty_project_name = project.full_name params.merge( auto_complete: true, diff --git a/app/models/project_services/pushover_service.rb b/app/models/project_services/pushover_service.rb index aa7bd4c3c84..e3a1ca2d45f 100644 --- a/app/models/project_services/pushover_service.rb +++ b/app/models/project_services/pushover_service.rb @@ -88,10 +88,10 @@ class PushoverService < Service user: user_key, device: device, priority: priority, - title: "#{project.name_with_namespace}", + title: "#{project.full_name}", message: message, url: data[:project][:web_url], - url_title: "See project #{project.name_with_namespace}" + url_title: "See project #{project.full_name}" } # Sound parameter MUST NOT be sent to API if not selected diff --git a/app/models/repository.rb b/app/models/repository.rb index 242d9d5f125..1a14afb951a 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -253,7 +253,7 @@ class Repository # branches or tags, but we want to keep some of these commits around, for # example if they have comments or CI builds. def keep_around(sha) - return unless sha && commit_by(oid: sha) + return unless sha.present? && commit_by(oid: sha) return if kept_around?(sha) diff --git a/app/models/todo.rb b/app/models/todo.rb index bb5965e20eb..8afacd188e0 100644 --- a/app/models/todo.rb +++ b/app/models/todo.rb @@ -32,8 +32,6 @@ class Todo < ActiveRecord::Base validates :target_id, presence: true, unless: :for_commit? validates :commit_id, presence: true, if: :for_commit? - default_scope { reorder(id: :desc) } - scope :pending, -> { with_state(:pending) } scope :done, -> { with_state(:done) } @@ -53,10 +51,14 @@ class Todo < ActiveRecord::Base # milestones, but still show something if the user has a URL with that # selected. def sort(method) - case method.to_s - when 'priority', 'label_priority' then order_by_labels_priority - else order_by(method) - end + sorted = + case method.to_s + when 'priority', 'label_priority' then order_by_labels_priority + else order_by(method) + end + + # Break ties with the ID column for pagination + sorted.order(id: :desc) end # Order by priority depending on which issue/merge request the Todo belongs to diff --git a/app/models/user.rb b/app/models/user.rb index 9547506d33d..9c60adf0c90 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -601,6 +601,15 @@ class User < ActiveRecord::Base authorized_projects(min_access_level).exists?({ id: project.id }) end + # Typically used in conjunction with projects table to get projects + # a user has been given access to. + # + # Example use: + # `Project.where('EXISTS(?)', user.authorizations_for_projects)` + def authorizations_for_projects + project_authorizations.select(1).where('project_authorizations.project_id = projects.id') + end + # Returns the projects this user has reporter (or greater) access to, limited # to at most the given projects. # diff --git a/app/services/badges/base_service.rb b/app/services/badges/base_service.rb new file mode 100644 index 00000000000..4f87426bd38 --- /dev/null +++ b/app/services/badges/base_service.rb @@ -0,0 +1,11 @@ +module Badges + class BaseService + protected + + attr_accessor :params + + def initialize(params = {}) + @params = params.dup + end + end +end diff --git a/app/services/badges/build_service.rb b/app/services/badges/build_service.rb new file mode 100644 index 00000000000..6267e571838 --- /dev/null +++ b/app/services/badges/build_service.rb @@ -0,0 +1,12 @@ +module Badges + class BuildService < Badges::BaseService + # returns the created badge + def execute(source) + if source.is_a?(Group) + GroupBadge.new(params.merge(group: source)) + else + ProjectBadge.new(params.merge(project: source)) + end + end + end +end diff --git a/app/services/badges/create_service.rb b/app/services/badges/create_service.rb new file mode 100644 index 00000000000..aafb87f7dcd --- /dev/null +++ b/app/services/badges/create_service.rb @@ -0,0 +1,10 @@ +module Badges + class CreateService < Badges::BaseService + # returns the created badge + def execute(source) + badge = Badges::BuildService.new(params).execute(source) + + badge.tap { |b| b.save } + end + end +end diff --git a/app/services/badges/update_service.rb b/app/services/badges/update_service.rb new file mode 100644 index 00000000000..7ca84b5df31 --- /dev/null +++ b/app/services/badges/update_service.rb @@ -0,0 +1,12 @@ +module Badges + class UpdateService < Badges::BaseService + # returns the updated badge + def execute(badge) + if params.present? + badge.update_attributes(params) + end + + badge + end + end +end diff --git a/app/services/ci/create_trace_artifact_service.rb b/app/services/ci/create_trace_artifact_service.rb deleted file mode 100644 index ffde824972c..00000000000 --- a/app/services/ci/create_trace_artifact_service.rb +++ /dev/null @@ -1,36 +0,0 @@ -module Ci - class CreateTraceArtifactService < BaseService - def execute(job) - return if job.job_artifacts_trace - - job.trace.read do |stream| - break unless stream.file? - - clone_file!(stream.path, JobArtifactUploader.workhorse_upload_path) do |clone_path| - create_job_trace!(job, clone_path) - FileUtils.rm(stream.path) - end - end - end - - private - - def create_job_trace!(job, path) - File.open(path) do |stream| - job.create_job_artifacts_trace!( - project: job.project, - file_type: :trace, - file: stream) - end - end - - def clone_file!(src_path, temp_dir) - FileUtils.mkdir_p(temp_dir) - Dir.mktmpdir('tmp-trace', temp_dir) do |dir_path| - temp_path = File.join(dir_path, "job.log") - FileUtils.copy(src_path, temp_path) - yield(temp_path) - end - end - end -end diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index e87fd49d193..02fb48108fb 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -109,6 +109,10 @@ class IssuableBaseService < BaseService @available_labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute end + def handle_quick_actions_on_create(issuable) + merge_quick_actions_into_params!(issuable) + end + def merge_quick_actions_into_params!(issuable) original_description = params.fetch(:description, issuable.description) @@ -131,7 +135,7 @@ class IssuableBaseService < BaseService end def create(issuable) - merge_quick_actions_into_params!(issuable) + handle_quick_actions_on_create(issuable) filter_params(issuable) params.delete(:state_event) diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb index 20a2b50d3de..23262b62615 100644 --- a/app/services/merge_requests/base_service.rb +++ b/app/services/merge_requests/base_service.rb @@ -24,6 +24,17 @@ module MergeRequests private + def handle_wip_event(merge_request) + if wip_event = params.delete(:wip_event) + # We update the title that is provided in the params or we use the mr title + title = params[:title] || merge_request.title + params[:title] = case wip_event + when 'wip' then MergeRequest.wip_title(title) + when 'unwip' then MergeRequest.wipless_title(title) + end + end + end + def merge_request_metrics_service(merge_request) MergeRequestMetricsService.new(merge_request.metrics) end diff --git a/app/services/merge_requests/create_service.rb b/app/services/merge_requests/create_service.rb index a18b1c90765..c57a2445341 100644 --- a/app/services/merge_requests/create_service.rb +++ b/app/services/merge_requests/create_service.rb @@ -34,6 +34,12 @@ module MergeRequests super end + # Override from IssuableBaseService + def handle_quick_actions_on_create(merge_request) + super + handle_wip_event(merge_request) + end + private def update_merge_requests_head_pipeline(merge_request) diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb index c153872c874..8a40ad88182 100644 --- a/app/services/merge_requests/update_service.rb +++ b/app/services/merge_requests/update_service.rb @@ -98,17 +98,6 @@ module MergeRequests private - def handle_wip_event(merge_request) - if wip_event = params.delete(:wip_event) - # We update the title that is provided in the params or we use the mr title - title = params[:title] || merge_request.title - params[:title] = case wip_event - when 'wip' then MergeRequest.wip_title(title) - when 'unwip' then MergeRequest.wipless_title(title) - end - end - end - def create_branch_change_note(issuable, branch_type, old_branch, new_branch) SystemNoteService.change_branch( issuable, issuable.project, current_user, branch_type, diff --git a/app/services/notes/quick_actions_service.rb b/app/services/notes/quick_actions_service.rb index a8d0cc15527..0a33d5f3f3d 100644 --- a/app/services/notes/quick_actions_service.rb +++ b/app/services/notes/quick_actions_service.rb @@ -9,14 +9,12 @@ module Notes UPDATE_SERVICES[note.noteable_type] end - def self.supported?(note, current_user) - noteable_update_service(note) && - current_user && - current_user.can?(:"update_#{note.to_ability_name}", note.noteable) + def self.supported?(note) + !!noteable_update_service(note) end def supported?(note) - self.class.supported?(note, current_user) + self.class.supported?(note) end def extract_commands(note, options = {}) diff --git a/app/services/projects/import_export/export_service.rb b/app/services/projects/import_export/export_service.rb index fe4e8ea10bf..af41ce82f65 100644 --- a/app/services/projects/import_export/export_service.rb +++ b/app/services/projects/import_export/export_service.rb @@ -2,7 +2,7 @@ module Projects module ImportExport class ExportService < BaseService def execute(_options = {}) - @shared = Gitlab::ImportExport::Shared.new(relative_path: File.join(project.disk_path, 'work')) + @shared = project.import_export_shared save_all end diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb index c760bd3b626..00fdd047208 100644 --- a/app/services/projects/update_pages_service.rb +++ b/app/services/projects/update_pages_service.rb @@ -1,5 +1,8 @@ module Projects class UpdatePagesService < BaseService + InvaildStateError = Class.new(StandardError) + FailedToExtractError = Class.new(StandardError) + BLOCK_SIZE = 32.kilobytes MAX_SIZE = 1.terabyte SITE_PATH = 'public/'.freeze @@ -11,13 +14,15 @@ module Projects end def execute + register_attempt + # Create status notifying the deployment of pages @status = create_status @status.enqueue! @status.run! - raise 'missing pages artifacts' unless build.artifacts? - raise 'pages are outdated' unless latest? + raise InvaildStateError, 'missing pages artifacts' unless build.artifacts? + raise InvaildStateError, 'pages are outdated' unless latest? # Create temporary directory in which we will extract the artifacts FileUtils.mkdir_p(tmp_path) @@ -26,24 +31,22 @@ module Projects # Check if we did extract public directory archive_public_path = File.join(archive_path, 'public') - raise 'pages miss the public folder' unless Dir.exist?(archive_public_path) - raise 'pages are outdated' unless latest? + raise FailedToExtractError, 'pages miss the public folder' unless Dir.exist?(archive_public_path) + raise InvaildStateError, 'pages are outdated' unless latest? deploy_page!(archive_public_path) success end - rescue => e + rescue InvaildStateError, FailedToExtractError => e register_failure error(e.message) - ensure - register_attempt - build.erase_artifacts! unless build.has_expiring_artifacts? end private def success @status.success + delete_artifact! super end @@ -52,6 +55,7 @@ module Projects @status.allow_failure = !latest? @status.description = message @status.drop(:script_failure) + delete_artifact! super end @@ -72,7 +76,7 @@ module Projects elsif artifacts.ends_with?('.zip') extract_zip_archive!(temp_path) else - raise 'unsupported artifacts format' + raise FailedToExtractError, 'unsupported artifacts format' end end @@ -81,17 +85,17 @@ module Projects %W(dd bs=#{BLOCK_SIZE} count=#{blocks}), %W(tar -x -C #{temp_path} #{SITE_PATH}), err: '/dev/null') - raise 'pages failed to extract' unless results.compact.all?(&:success?) + raise FailedToExtractError, 'pages failed to extract' unless results.compact.all?(&:success?) end def extract_zip_archive!(temp_path) - raise 'missing artifacts metadata' unless build.artifacts_metadata? + raise FailedToExtractError, 'missing artifacts metadata' unless build.artifacts_metadata? # Calculate page size after extract public_entry = build.artifacts_metadata_entry(SITE_PATH, recursive: true) if public_entry.total_size > max_size - raise "artifacts for pages are too large: #{public_entry.total_size}" + raise FailedToExtractError, "artifacts for pages are too large: #{public_entry.total_size}" end # Requires UnZip at least 6.00 Info-ZIP. @@ -100,7 +104,7 @@ module Projects # We add * to end of SITE_PATH, because we want to extract SITE_PATH and all subdirectories site_path = File.join(SITE_PATH, '*') unless system(*%W(unzip -qq -n #{artifacts} #{site_path} -d #{temp_path})) - raise 'pages failed to extract' + raise FailedToExtractError, 'pages failed to extract' end end @@ -163,6 +167,11 @@ module Projects build.artifacts_file.path end + def delete_artifact! + build.reload # Reload stable object to prevent erase artifacts with old state + build.erase_artifacts! unless build.has_expiring_artifacts? + end + def latest_sha project.commit(build.ref).try(:sha).to_s end diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb index 1e9bd84e749..cba49faac31 100644 --- a/app/services/quick_actions/interpret_service.rb +++ b/app/services/quick_actions/interpret_service.rb @@ -347,9 +347,9 @@ module QuickActions "#{verb} this #{noun} as Work In Progress." end condition do - issuable.persisted? && - issuable.respond_to?(:work_in_progress?) && - current_user.can?(:"update_#{issuable.to_ability_name}", issuable) + issuable.respond_to?(:work_in_progress?) && + # Allow it to mark as WIP on MR creation page _or_ through MR notes. + (issuable.new_record? || current_user.can?(:"update_#{issuable.to_ability_name}", issuable)) end command :wip do @updates[:wip_event] = issuable.work_in_progress? ? 'unwip' : 'wip' diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb index af8c02a10b7..ba7946fd23c 100644 --- a/app/services/system_hooks_service.rb +++ b/app/services/system_hooks_service.rb @@ -20,8 +20,8 @@ class SystemHooksService def build_event_data(model, event) data = { event_name: build_event_name(model, event), - created_at: model.created_at.xmlschema, - updated_at: model.updated_at.xmlschema + created_at: model.created_at&.xmlschema, + updated_at: model.updated_at&.xmlschema } case model diff --git a/app/validators/url_placeholder_validator.rb b/app/validators/url_placeholder_validator.rb new file mode 100644 index 00000000000..dd681218b6b --- /dev/null +++ b/app/validators/url_placeholder_validator.rb @@ -0,0 +1,32 @@ +# UrlValidator +# +# Custom validator for URLs. +# +# By default, only URLs for the HTTP(S) protocols will be considered valid. +# Provide a `:protocols` option to configure accepted protocols. +# +# Also, this validator can help you validate urls with placeholders inside. +# Usually, if you have a url like 'http://www.example.com/%{project_path}' the +# URI parser will reject that URL format. Provide a `:placeholder_regex` option +# to configure accepted placeholders. +# +# Example: +# +# class User < ActiveRecord::Base +# validates :personal_url, url: true +# +# validates :ftp_url, url: { protocols: %w(ftp) } +# +# validates :git_url, url: { protocols: %w(http https ssh git) } +# +# validates :placeholder_url, url: { placeholder_regex: /(project_path|project_id|default_branch)/ } +# end +# +class UrlPlaceholderValidator < UrlValidator + def validate_each(record, attribute, value) + placeholder_regex = self.options[:placeholder_regex] + value = value.gsub(/%{#{placeholder_regex}}/, 'foo') if placeholder_regex && value + + super(record, attribute, value) + end +end diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index e3711421b61..05c41082882 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -164,7 +164,7 @@ %h4 Latest projects - @projects.each do |project| %p - = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project], class: 'str-truncated-60' + = link_to project.full_name, [:admin, project.namespace.becomes(Namespace), project], class: 'str-truncated-60' %span.light.pull-right #{time_ago_with_tooltip(project.created_at)} .col-md-4 diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml index 2545cecc721..324f3c0a22f 100644 --- a/app/views/admin/groups/show.html.haml +++ b/app/views/admin/groups/show.html.haml @@ -68,7 +68,7 @@ - @projects.each do |project| %li %strong - = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project] + = link_to project.full_name, [:admin, project.namespace.becomes(Namespace), project] %span.badge = storage_counter(project.statistics.storage_size) %span.pull-right.light @@ -86,7 +86,7 @@ - @group.shared_projects.sort_by(&:name).each do |project| %li %strong - = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project] + = link_to project.full_name, [:admin, project.namespace.becomes(Namespace), project] %span.badge = storage_counter(project.statistics.storage_size) %span.pull-right.light diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml index 42f92079d85..c02ddafe108 100644 --- a/app/views/admin/projects/show.html.haml +++ b/app/views/admin/projects/show.html.haml @@ -1,8 +1,8 @@ - add_to_breadcrumbs "Projects", admin_projects_path -- breadcrumb_title @project.name_with_namespace -- page_title @project.name_with_namespace, "Projects" +- breadcrumb_title @project.full_name +- page_title @project.full_name, "Projects" %h3.page-title - Project: #{@project.name_with_namespace} + Project: #{@project.full_name} = link_to edit_project_path(@project), class: "btn btn-nr pull-right" do %i.fa.fa-pencil-square-o Edit diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml index 6d8fad0eb8d..185e9d7b35d 100644 --- a/app/views/admin/runners/show.html.haml +++ b/app/views/admin/runners/show.html.haml @@ -39,7 +39,7 @@ %tr.alert-info %td %strong - = project.name_with_namespace + = project.full_name %td .pull-right = link_to 'Disable', [:admin, project.namespace.becomes(Namespace), project, runner_project], method: :delete, class: 'btn btn-danger btn-xs' @@ -61,7 +61,7 @@ - @projects.each do |project| %tr %td - = project.name_with_namespace + = project.full_name %td .pull-right = form_for [:admin, project.namespace.becomes(Namespace), project, project.runner_projects.new] do |f| @@ -95,7 +95,7 @@ %td.status - if project - = project.name_with_namespace + = project.full_name %td.build-link - if project diff --git a/app/views/admin/users/projects.html.haml b/app/views/admin/users/projects.html.haml index 4a440f3f6d4..96835ee9af5 100644 --- a/app/views/admin/users/projects.html.haml +++ b/app/views/admin/users/projects.html.haml @@ -29,12 +29,12 @@ .panel.panel-default .panel-heading Joined projects (#{@joined_projects.count}) %ul.well-list - - @joined_projects.sort_by(&:name_with_namespace).each do |project| + - @joined_projects.sort_by(&:full_name).each do |project| - member = project.team.find_member(@user.id) %li.project_member .list-item-name = link_to admin_project_path(project), class: dom_class(project) do - = project.name_with_namespace + = project.full_name - if member .pull-right diff --git a/app/views/groups/projects.html.haml b/app/views/groups/projects.html.haml index 8d2bc810a7d..ef181b425bc 100644 --- a/app/views/groups/projects.html.haml +++ b/app/views/groups/projects.html.haml @@ -14,7 +14,7 @@ .list-item-name %span{ class: visibility_level_color(project.visibility_level) } = visibility_level_icon(project.visibility_level) - %strong= link_to project.name_with_namespace, project + %strong= link_to project.full_name, project .pull-right - if project.archived %span.label.label-warning archived diff --git a/app/views/ide/index.html.haml b/app/views/ide/index.html.haml deleted file mode 100644 index 3dbdfc97654..00000000000 --- a/app/views/ide/index.html.haml +++ /dev/null @@ -1,11 +0,0 @@ -- @body_class = 'ide' -- page_title 'IDE' - -- content_for :page_specific_javascripts do - = webpack_bundle_tag 'common_vue' - = webpack_bundle_tag 'ide', force_same_domain: true - -#ide.ide-loading{ data: {"empty-state-svg-path" => image_path('illustrations/multi_file_editor_empty.svg')} } - .text-center - = icon('spinner spin 2x') - %h2.clgray= _('Loading the GitLab IDE...') diff --git a/app/views/invites/show.html.haml b/app/views/invites/show.html.haml index ad6213b4efd..c2bb1216c5f 100644 --- a/app/views/invites/show.html.haml +++ b/app/views/invites/show.html.haml @@ -12,7 +12,7 @@ - project = @member.source project %strong - = link_to project.name_with_namespace, project_url(project) + = link_to project.full_name, project_url(project) - when Group - group = @member.source group diff --git a/app/views/layouts/nav/projects_dropdown/_show.html.haml b/app/views/layouts/nav/projects_dropdown/_show.html.haml index 59becb043d3..5809d6f7fea 100644 --- a/app/views/layouts/nav/projects_dropdown/_show.html.haml +++ b/app/views/layouts/nav/projects_dropdown/_show.html.haml @@ -1,4 +1,4 @@ -- project_meta = { id: @project.id, name: @project.name, namespace: @project.name_with_namespace, web_url: project_path(@project), avatar_url: @project.avatar_url } if @project&.persisted? +- project_meta = { id: @project.id, name: @project.name, namespace: @project.full_name, web_url: project_path(@project), avatar_url: @project.avatar_url } if @project&.persisted? .projects-dropdown-container .project-dropdown-sidebar.qa-projects-dropdown-sidebar %ul diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml index 6b847fb4b7c..6b51483810e 100644 --- a/app/views/layouts/project.html.haml +++ b/app/views/layouts/project.html.haml @@ -1,4 +1,4 @@ -- page_title @project.name_with_namespace +- page_title @project.full_name - page_description @project.description unless page_description - header_title project_title(@project) unless header_title - nav "project" diff --git a/app/views/notify/project_was_exported_email.html.haml b/app/views/notify/project_was_exported_email.html.haml index f0ba7827cef..71c62f6be4e 100644 --- a/app/views/notify/project_was_exported_email.html.haml +++ b/app/views/notify/project_was_exported_email.html.haml @@ -3,6 +3,6 @@ %p The project export can be downloaded from: = link_to download_export_project_url(@project), rel: 'nofollow', download: '' do - = @project.name_with_namespace + " export" + = @project.full_name + " export" %p The download link will expire in 24 hours. diff --git a/app/views/notify/project_was_moved_email.html.haml b/app/views/notify/project_was_moved_email.html.haml index c476a39b661..1b6b1a81665 100644 --- a/app/views/notify/project_was_moved_email.html.haml +++ b/app/views/notify/project_was_moved_email.html.haml @@ -3,7 +3,7 @@ %p The project is now located under = link_to project_url(@project) do - = @project.name_with_namespace + = @project.full_name %p To update the remote url in your local repository run (for ssh): %p{ style: "background: #f5f5f5; padding:10px; border:1px solid #ddd" } diff --git a/app/views/profiles/chat_names/_chat_name.html.haml b/app/views/profiles/chat_names/_chat_name.html.haml index fe1cf802971..c7094800fb2 100644 --- a/app/views/profiles/chat_names/_chat_name.html.haml +++ b/app/views/profiles/chat_names/_chat_name.html.haml @@ -4,7 +4,7 @@ %td %strong - if can?(current_user, :read_project, project) - = link_to project.name_with_namespace, project_path(project) + = link_to project.full_name, project_path(project) - else .light N/A %td diff --git a/app/views/profiles/keys/index.html.haml b/app/views/profiles/keys/index.html.haml index 457583cfd35..1e206def7ee 100644 --- a/app/views/profiles/keys/index.html.haml +++ b/app/views/profiles/keys/index.html.haml @@ -12,7 +12,9 @@ Add an SSH key %p.profile-settings-content Before you can add an SSH key you need to - = link_to "generate it.", help_page_path("ssh/README") + = link_to "generate one", help_page_path("ssh/README", anchor: 'generating-a-new-ssh-key-pair') + or use an + = link_to "existing key.", help_page_path("ssh/README", anchor: 'locating-an-existing-ssh-key-pair') = render 'form' %hr %h5 diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index b565f14747a..a2ecfddb163 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -23,6 +23,12 @@ - deleted_message = s_('ForkedFromProjectPath|Forked from %{project_name} (deleted)') = deleted_message % { project_name: fork_source_name(@project) } + .project-badges + - @project.badges.each do |badge| + - badge_link_url = badge.rendered_link_url(@project) + %a{ href: badge_link_url, target: '_blank', rel: 'noopener noreferrer' } + %img{ src: badge.rendered_image_url(@project), alt: badge_link_url } + .project-repo-buttons .count-buttons = render 'projects/buttons/star' diff --git a/app/views/projects/_new_project_fields.html.haml b/app/views/projects/_new_project_fields.html.haml index d367bd6be7b..f4b5ef1555e 100644 --- a/app/views/projects/_new_project_fields.html.haml +++ b/app/views/projects/_new_project_fields.html.haml @@ -1,6 +1,8 @@ - visibility_level = params.dig(:project, :visibility_level) || default_project_visibility +- ci_cd_only = local_assigns.fetch(:ci_cd_only, false) .row{ id: project_name_id } + = f.hidden_field :ci_cd_only, value: ci_cd_only .form-group.project-path.col-sm-6 = f.label :namespace_id, class: 'label-light' do %span diff --git a/app/views/projects/blob/_header.html.haml b/app/views/projects/blob/_header.html.haml index 1b150ec3e5c..f93bb02acb9 100644 --- a/app/views/projects/blob/_header.html.haml +++ b/app/views/projects/blob/_header.html.haml @@ -12,7 +12,6 @@ .btn-group{ role: "group" }< = edit_blob_button - = ide_edit_button - if current_user = replace_blob_link = delete_blob_link diff --git a/app/views/projects/branches/_panel.html.haml b/app/views/projects/branches/_panel.html.haml new file mode 100644 index 00000000000..12e5a8e8d69 --- /dev/null +++ b/app/views/projects/branches/_panel.html.haml @@ -0,0 +1,19 @@ +- branches = local_assigns.fetch(:branches) +- state = local_assigns.fetch(:state) +- panel_title = local_assigns.fetch(:panel_title) +- show_more_text = local_assigns.fetch(:show_more_text) +- project = local_assigns.fetch(:project) +- overview_max_branches = local_assigns.fetch(:overview_max_branches) + +- return unless branches.any? + +.panel.panel-default.prepend-top-10 + .panel-heading + %h4.panel-title + = panel_title + %ul.content-list.all-branches + - branches.first(overview_max_branches).each do |branch| + = render "projects/branches/branch", branch: branch, merged: project.repository.merged_to_root_ref?(branch) + - if branches.size > overview_max_branches + .panel-footer.text-center + = link_to show_more_text, project_branches_filtered_path(project, state: state), id: "state-#{state}", data: { state: state } diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml index fb770764364..5dcc72d8263 100644 --- a/app/views/projects/branches/index.html.haml +++ b/app/views/projects/branches/index.html.haml @@ -3,26 +3,35 @@ %div{ class: container_class } .top-area.adjust - - if can?(current_user, :admin_project, @project) - .nav-text - - project_settings_link = link_to s_('Branches|project settings'), project_protected_branches_path(@project) - = s_('Branches|Protected branches can be managed in %{project_settings_link}').html_safe % { project_settings_link: project_settings_link } + %ul.nav-links.issues-state-filters + %li{ class: active_when(@mode == 'overview') }> + = link_to s_('Branches|Overview'), project_branches_path(@project), title: s_('Branches|Show overview of the branches') + + %li{ class: active_when(@mode == 'active') }> + = link_to s_('Branches|Active'), project_branches_filtered_path(@project, state: 'active'), title: s_('Branches|Show active branches') + + %li{ class: active_when(@mode == 'stale') }> + = link_to s_('Branches|Stale'), project_branches_filtered_path(@project, state: 'stale'), title: s_('Branches|Show stale branches') + + %li{ class: active_when(!%w[overview active stale].include?(@mode)) }> + = link_to s_('Branches|All'), project_branches_filtered_path(@project, state: 'all'), title: s_('Branches|Show all branches') .nav-controls - = form_tag(filter_branches_path, method: :get) do + = form_tag(project_branches_filtered_path(@project, state: 'all'), method: :get) do = search_field_tag :search, params[:search], { placeholder: s_('Branches|Filter by branch name'), id: 'branch-search', class: 'form-control search-text-input input-short', spellcheck: false } - .dropdown.inline> - %button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' } - %span.light - = branches_sort_options_hash[@sort] - = icon('chevron-down') - %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable - %li.dropdown-header - = s_('Branches|Sort by') - - branches_sort_options_hash.each do |value, title| - %li - = link_to title, filter_branches_path(sort: value), class: ("is-active" if @sort == value) + - unless @mode == 'overview' + .dropdown.inline> + %button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' } + %span.light + = branches_sort_options_hash[@sort] + = icon('chevron-down') + %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable + %li.dropdown-header + = s_('Branches|Sort by') + - branches_sort_options_hash.each do |value, title| + %li + = link_to title, project_branches_filtered_path(@project, state: 'all', search: params[:search], sort: value), class: ("is-active" if @sort == value) - if can? current_user, :push_code, @project = link_to project_merged_branches_path(@project), @@ -35,7 +44,17 @@ = link_to new_project_branch_path(@project), class: 'btn btn-create' do = s_('Branches|New branch') - - if @branches.any? + - if can?(current_user, :admin_project, @project) + - project_settings_link = link_to s_('Branches|project settings'), project_protected_branches_path(@project) + .row-content-block + %h5 + = s_('Branches|Protected branches can be managed in %{project_settings_link}.').html_safe % { project_settings_link: project_settings_link } + + - if @mode == 'overview' && (@active_branches.any? || @stale_branches.any?) + = render "projects/branches/panel", branches: @active_branches, state: 'active', panel_title: s_('Branches|Active branches'), show_more_text: s_('Branches|Show more active branches'), project: @project, overview_max_branches: @overview_max_branches + = render "projects/branches/panel", branches: @stale_branches, state: 'stale', panel_title: s_('Branches|Stale branches'), show_more_text: s_('Branches|Show more stale branches'), project: @project, overview_max_branches: @overview_max_branches + + - elsif @branches.any? %ul.content-list.all-branches - @branches.each do |branch| = render "projects/branches/branch", branch: branch, merged: @merged_branch_names.include?(branch.name) diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index 0cd2d45c74b..9126476e79e 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -63,7 +63,7 @@ - if admin %td - if job.project - = link_to job.project.name_with_namespace, admin_project_path(job.project) + = link_to job.project.full_name, admin_project_path(job.project) %td - if job.try(:runner) = runner_link(job.runner) diff --git a/app/views/projects/environments/terminal.html.haml b/app/views/projects/environments/terminal.html.haml index 7be4ef39117..6ec4ff56552 100644 --- a/app/views/projects/environments/terminal.html.haml +++ b/app/views/projects/environments/terminal.html.haml @@ -3,7 +3,6 @@ - content_for :page_specific_javascripts do = stylesheet_link_tag "xterm/xterm" - = webpack_bundle_tag("terminal") %div{ class: container_class } .top-area diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml index 2599ce5c4b8..620fd1906ba 100644 --- a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml +++ b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml @@ -53,7 +53,7 @@ - if admin %td - if generic_commit_status.project - = link_to generic_commit_status.project.name_with_namespace, admin_project_path(generic_commit_status.project) + = link_to generic_commit_status.project.full_name, admin_project_path(generic_commit_status.project) %td - if generic_commit_status.try(:runner) = runner_link(generic_commit_status.runner) diff --git a/app/views/projects/imports/show.html.haml b/app/views/projects/imports/show.html.haml index 8c490773a56..3b0c828ccd1 100644 --- a/app/views/projects/imports/show.html.haml +++ b/app/views/projects/imports/show.html.haml @@ -1,12 +1,11 @@ -- page_title @project.forked? ? "Forking in progress" : "Import in progress" +- page_title import_in_progress_title + .save-project-loader .center %h2 %i.fa.fa-spinner.fa-spin - - if @project.forked? - Forking in progress. - - else - Import in progress. - - if @project.external_import? + = import_in_progress_title + - if !has_ci_cd_only_params? && @project.external_import? %p.monospace git clone --bare #{@project.safe_import_url} - %p Please wait while we import the repository for you. Refresh at will. + %p + = import_wait_and_refresh_message diff --git a/app/views/projects/issues/_merge_requests.html.haml b/app/views/projects/issues/_merge_requests.html.haml index 5f97d31f610..5c36d2202a6 100644 --- a/app/views/projects/issues/_merge_requests.html.haml +++ b/app/views/projects/issues/_merge_requests.html.haml @@ -18,7 +18,7 @@ - unless @issue.project.id == merge_request.target_project.id in - project = merge_request.target_project - = link_to project.name_with_namespace, project_path(project) + = link_to project.full_name, project_path(project) - if merge_request.merged? %span.merge-request-status.prepend-left-10.merged diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml index f2e35ef6e0c..9866cc716ee 100644 --- a/app/views/projects/merge_requests/show.html.haml +++ b/app/views/projects/merge_requests/show.html.haml @@ -5,11 +5,6 @@ - page_title "#{@merge_request.title} (#{@merge_request.to_reference})", "Merge Requests" - page_description @merge_request.description - page_card_attributes @merge_request.card_attributes -- content_for :page_specific_javascripts do - = webpack_bundle_tag('common_vue') - - - if has_vue_discussions_cookie? - = webpack_bundle_tag('mr_notes') .merge-request{ data: { mr_action: j(params[:tab].presence || 'show'), url: merge_request_path(@merge_request, format: :json), project_path: project_path(@merge_request.project) } } = render "projects/merge_requests/mr_title" diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 679ba23a4db..1d31b58a2cc 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -12,11 +12,14 @@ .row.prepend-top-default .col-lg-3.profile-settings-sidebar %h4.prepend-top-0 - New project + = _('New project') %p - A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), #{link_to 'among other things', help_page_path("user/project/index.md", anchor: "projects-features"), target: '_blank'}. + - among_other_things_link = link_to _('among other things'), help_page_path("user/project/index.md", anchor: "projects-features"), target: '_blank' + = _('A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), %{among_other_things_link}.').html_safe % { among_other_things_link: among_other_things_link } %p - All features are enabled when you create a project, but you can disable the ones you don’t need in the project settings. + = _('All features are enabled for blank projects, from templates, or when importing, but you can disable them afterward in the project settings.') + -# EE-specific start + -# EE-specific end .md = brand_new_project_guidelines %p @@ -28,36 +31,38 @@ .col-lg-9.js-toggle-container %ul.nav-links.gitlab-tabs{ role: 'tablist' } - %li{ class: ('active' if active_tab == 'blank'), role: 'presentation' } + %li{ class: active_when(active_tab == 'blank'), role: 'presentation' } %a{ href: '#blank-project-pane', id: 'blank-project-tab', data: { toggle: 'tab' }, role: 'tab' } %span.hidden-xs Blank project %span.visible-xs Blank - %li{ class: ('active' if active_tab == 'template'), role: 'presentation' } + %li{ class: active_when(active_tab == 'template'), role: 'presentation' } %a{ href: '#create-from-template-pane', id: 'create-from-template-tab', data: { toggle: 'tab' }, role: 'tab' } %span.hidden-xs Create from template %span.visible-xs Template - %li{ class: ('active' if active_tab == 'import'), role: 'presentation' } + %li{ class: active_when(active_tab == 'import'), role: 'presentation' } %a{ href: '#import-project-pane', id: 'import-project-tab', data: { toggle: 'tab' }, role: 'tab' } %span.hidden-xs Import project %span.visible-xs Import + -# EE-specific start + -# EE-specific end .tab-content.gitlab-tab-content - .tab-pane{ id: 'blank-project-pane', class: ('active' if active_tab == 'blank'), role: 'tabpanel' } + .tab-pane{ id: 'blank-project-pane', class: active_when(active_tab == 'blank'), role: 'tabpanel' } = form_for @project, html: { class: 'new_project' } do |f| = render 'new_project_fields', f: f, project_name_id: "blank-project-name" - .tab-pane.no-padding{ id: 'create-from-template-pane', class: ('active' if active_tab == 'template'), role: 'tabpanel' } + .tab-pane.no-padding{ id: 'create-from-template-pane', class: active_when(active_tab == 'template'), role: 'tabpanel' } = form_for @project, html: { class: 'new_project' } do |f| .project-template .form-group %div = render 'project_templates', f: f - .tab-pane.import-project-pane{ id: 'import-project-pane', class: ('active' if active_tab == 'import'), role: 'tabpanel' } + .tab-pane.import-project-pane.js-toggle-container{ id: 'import-project-pane', class: active_when(active_tab == 'import'), role: 'tabpanel' } = form_for @project, html: { class: 'new_project' } do |f| - if import_sources_enabled? .project-import.row - .col-sm-12 + .col-lg-12 .form-group.import-btn-container.clearfix = f.label :visibility_level, class: 'label-light' do #the label here seems wrong Import project from @@ -97,7 +102,7 @@ Gitea %div - if git_import_enabled? - %button.btn.js-toggle-button.import_git{ type: "button" } + %button.btn.js-toggle-button.js-import-git-toggle-button{ type: "button", data: { toggle_open_class: 'active' } } = icon('git', text: 'Repo by URL') .col-lg-12 .js-toggle-content.toggle-import-form{ class: ('hide' if active_tab != 'import') } @@ -105,6 +110,10 @@ = render "shared/import_form", f: f = render 'new_project_fields', f: f, project_name_id: "import-url-name" + + -# EE-specific start + -# EE-specific end + .save-project-loader.hide .center %h2 diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index cf95cdbfec2..3e6b3346787 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -7,8 +7,9 @@ "help-auto-devops-path" => help_page_path('topics/autodevops/index.md'), "empty-state-svg-path" => image_path('illustrations/pipelines_empty.svg'), "error-state-svg-path" => image_path('illustrations/pipelines_failed.svg'), - "new-pipeline-path" => new_project_pipeline_path(@project), + "no-pipelines-svg-path" => image_path('illustrations/pipelines_pending.svg'), "can-create-pipeline" => can?(current_user, :create_pipeline, @project).to_s, - "has-ci" => @repository.gitlab_ci_yml, - "ci-lint-path" => ci_lint_path, - "reset-cache-path" => reset_cache_project_settings_ci_cd_path(@project) } } + "new-pipeline-path" => can?(current_user, :create_pipeline, @project) && new_project_pipeline_path(@project), + "ci-lint-path" => can?(current_user, :create_pipeline, @project) && ci_lint_path, + "reset-cache-path" => can?(current_user, :admin_pipeline, @project) && reset_cache_project_settings_ci_cd_path(@project) , + "has-gitlab-ci" => (@project.has_ci? && @project.builds_enabled?).to_s } } diff --git a/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml index 5dbcbf7eba6..2ab0227126a 100644 --- a/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml +++ b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml @@ -1,4 +1,4 @@ -- run_actions_text = "Perform common operations on GitLab project: #{@project.name_with_namespace}" +- run_actions_text = "Perform common operations on GitLab project: #{@project.full_name}" %p To setup this service: %ul.list-unstyled.indent-list @@ -20,7 +20,7 @@ .form-group = label_tag :display_name, 'Display name', class: 'col-sm-2 col-xs-12 control-label' .col-sm-10.col-xs-12.input-group - = text_field_tag :display_name, "GitLab / #{@project.name_with_namespace}", class: 'form-control input-sm', readonly: 'readonly' + = text_field_tag :display_name, "GitLab / #{@project.full_name}", class: 'form-control input-sm', readonly: 'readonly' .input-group-btn = clipboard_button(target: '#display_name') diff --git a/app/views/projects/services/slack_slash_commands/_help.html.haml b/app/views/projects/services/slack_slash_commands/_help.html.haml index c31c95608c6..d592a5e4663 100644 --- a/app/views/projects/services/slack_slash_commands/_help.html.haml +++ b/app/views/projects/services/slack_slash_commands/_help.html.haml @@ -1,4 +1,4 @@ -- pretty_name = defined?(@project) ? @project.name_with_namespace : 'namespace / path' +- pretty_name = defined?(@project) ? @project.full_name : 'namespace / path' - run_actions_text = "Perform common operations on GitLab project: #{pretty_name}" .well diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml index da364b58e36..10415d011d6 100644 --- a/app/views/projects/tags/index.html.haml +++ b/app/views/projects/tags/index.html.haml @@ -1,7 +1,6 @@ - @no_container = true - @sort ||= sort_value_recently_updated - page_title s_('TagsPage|Tags') -- add_to_breadcrumbs("Repository", project_tree_path(@project)) .flex-list{ class: container_class } .top-area.adjust diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml index 39511435508..06bce52e709 100644 --- a/app/views/projects/tree/_tree_header.html.haml +++ b/app/views/projects/tree/_tree_header.html.haml @@ -72,11 +72,6 @@ #{ _('New tag') } .tree-controls - - if show_new_ide? - = succeed " " do - = link_to ide_edit_path(@project, @id), class: 'btn btn-default' do - = _('Web IDE') - = link_to s_('Commits|History'), project_commits_path(@project, @id), class: 'btn' = render 'projects/find_file_link' diff --git a/app/views/search/_category.html.haml b/app/views/search/_category.html.haml index 915e648a5d3..7d43fd61081 100644 --- a/app/views/search/_category.html.haml +++ b/app/views/search/_category.html.haml @@ -14,25 +14,25 @@ = link_to search_filter_path(scope: 'issues') do Issues %span.badge - = @search_results.issues_count + = limited_count(@search_results.limited_issues_count) - if project_search_tabs?(:merge_requests) %li{ class: active_when(@scope == 'merge_requests') } = link_to search_filter_path(scope: 'merge_requests') do Merge requests %span.badge - = @search_results.merge_requests_count + = limited_count(@search_results.limited_merge_requests_count) - if project_search_tabs?(:milestones) %li{ class: active_when(@scope == 'milestones') } = link_to search_filter_path(scope: 'milestones') do Milestones %span.badge - = @search_results.milestones_count + = limited_count(@search_results.limited_milestones_count) - if project_search_tabs?(:notes) %li{ class: active_when(@scope == 'notes') } = link_to search_filter_path(scope: 'notes') do Comments %span.badge - = @search_results.notes_count + = limited_count(@search_results.limited_notes_count) - if project_search_tabs?(:wiki) %li{ class: active_when(@scope == 'wiki_blobs') } = link_to search_filter_path(scope: 'wiki_blobs') do diff --git a/app/views/search/_filter.html.haml b/app/views/search/_filter.html.haml index e43796e9654..e4902d368e7 100644 --- a/app/views/search/_filter.html.haml +++ b/app/views/search/_filter.html.haml @@ -22,7 +22,7 @@ %span.dropdown-toggle-text Project: - if @project.present? - = @project.name_with_namespace + = @project.full_name - else Any = icon("chevron-down") diff --git a/app/views/search/_results.html.haml b/app/views/search/_results.html.haml index 60ef44482f0..ab56f48ba4d 100644 --- a/app/views/search/_results.html.haml +++ b/app/views/search/_results.html.haml @@ -6,7 +6,7 @@ = search_entries_info(@search_objects, @scope, @search_term) - unless @show_snippets - if @project - in project #{link_to @project.name_with_namespace, [@project.namespace.becomes(Namespace), @project]} + in project #{link_to @project.full_name, [@project.namespace.becomes(Namespace), @project]} - elsif @group in group #{link_to @group.name, @group} diff --git a/app/views/search/results/_issue.html.haml b/app/views/search/results/_issue.html.haml index b4bc8982c05..b7a27ef6be2 100644 --- a/app/views/search/results/_issue.html.haml +++ b/app/views/search/results/_issue.html.haml @@ -10,4 +10,4 @@ .description.term = search_md_sanitize(issue, :description) %span.light - #{issue.project.name_with_namespace} + #{issue.project.full_name} diff --git a/app/views/search/results/_merge_request.html.haml b/app/views/search/results/_merge_request.html.haml index 1a5499e4d58..8b0fd74f680 100644 --- a/app/views/search/results/_merge_request.html.haml +++ b/app/views/search/results/_merge_request.html.haml @@ -11,4 +11,4 @@ .description.term = search_md_sanitize(merge_request, :description) %span.light - #{merge_request.project.name_with_namespace} + #{merge_request.project.full_name} diff --git a/app/views/search/results/_note.html.haml b/app/views/search/results/_note.html.haml index a7e178dfa71..e4ab7b0541f 100644 --- a/app/views/search/results/_note.html.haml +++ b/app/views/search/results/_note.html.haml @@ -7,7 +7,7 @@ %i.fa.fa-comment = link_to_member(project, note.author, avatar: false) commented on - = link_to project.name_with_namespace, project + = link_to project.full_name, project · - if note.for_commit? diff --git a/app/views/search/results/_snippet_title.html.haml b/app/views/search/results/_snippet_title.html.haml index 65710c09a89..d46c4d11e51 100644 --- a/app/views/search/results/_snippet_title.html.haml +++ b/app/views/search/results/_snippet_title.html.haml @@ -11,7 +11,7 @@ %small.pull-right.cgray - if snippet_title.project_id? - = link_to snippet_title.project.name_with_namespace, project_path(snippet_title.project) + = link_to snippet_title.project.full_name, project_path(snippet_title.project) .snippet-info = snippet_title.to_reference diff --git a/app/views/sent_notifications/unsubscribe.html.haml b/app/views/sent_notifications/unsubscribe.html.haml index de52fd00157..7d3e243495f 100644 --- a/app/views/sent_notifications/unsubscribe.html.haml +++ b/app/views/sent_notifications/unsubscribe.html.haml @@ -1,7 +1,7 @@ - noteable = @sent_notification.noteable - noteable_type = @sent_notification.noteable_type.titleize.downcase - noteable_text = %(#{noteable.title} (#{noteable.to_reference})) -- page_title "Unsubscribe", noteable_text, noteable_type.pluralize, @sent_notification.project.name_with_namespace +- page_title "Unsubscribe", noteable_text, noteable_type.pluralize, @sent_notification.project.full_name %h3.page-title Unsubscribe from #{noteable_type} diff --git a/app/views/shared/_import_form.html.haml b/app/views/shared/_import_form.html.haml index 736afa085e8..5eaaa1448d5 100644 --- a/app/views/shared/_import_form.html.haml +++ b/app/views/shared/_import_form.html.haml @@ -1,17 +1,22 @@ +- ci_cd_only = local_assigns.fetch(:ci_cd_only, false) + .form-group.import-url-data = f.label :import_url, class: 'label-light' do - %span Git repository URL + %span + = _('Git repository URL') - = f.text_field :import_url, autocomplete: 'off', class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git' + = f.text_field :import_url, autocomplete: 'off', class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git', required: true .well.prepend-top-20 %ul %li - The repository must be accessible over <code>http://</code>, <code>https://</code> or <code>git://</code>. + = _('The repository must be accessible over <code>http://</code>, <code>https://</code> or <code>git://</code>.').html_safe %li - If your HTTP repository is not publicly accessible, add authentication information to the URL: <code>https://username:password@gitlab.company.com/group/project.git</code>. + = _('If your HTTP repository is not publicly accessible, add authentication information to the URL: <code>https://username:password@gitlab.company.com/group/project.git</code>.').html_safe %li - The import will time out after #{time_interval_in_words(Gitlab.config.gitlab_shell.git_timeout)}. - For repositories that take longer, use a clone/push combination. + = import_will_timeout_message(ci_cd_only) %li - To migrate an SVN repository, check out #{link_to "this document", help_page_path('user/project/import/svn')}. + = import_svn_message(ci_cd_only) + +-# EE-specific start +-# EE-specific end diff --git a/app/views/shared/_ref_switcher.html.haml b/app/views/shared/_ref_switcher.html.haml index 479bd2cdb38..4c8c92d722a 100644 --- a/app/views/shared/_ref_switcher.html.haml +++ b/app/views/shared/_ref_switcher.html.haml @@ -1,6 +1,5 @@ - show_create = local_assigns.fetch(:show_create, false) -- show_new_branch_form = show_new_ide? && show_create && can?(current_user, :push_code, @project) - dropdown_toggle_text = @ref || @project.default_branch = form_tag switch_project_refs_path(@project), method: :get, class: "project-refs-form" do = hidden_field_tag :destination, destination @@ -16,14 +15,3 @@ = dropdown_filter _("Search branches and tags") = dropdown_content = dropdown_loading - - if show_new_branch_form - = dropdown_footer do - %ul.dropdown-footer-list - %li - %a.dropdown-toggle-page{ href: "#" } - Create new branch - - if show_new_branch_form - .dropdown-page-two - = dropdown_title("Create new branch", options: { back: true }) - = dropdown_content do - .js-new-branch-dropdown diff --git a/app/views/shared/milestones/_issuable.html.haml b/app/views/shared/milestones/_issuable.html.haml index 129f6ab604e..eba64daaadc 100644 --- a/app/views/shared/milestones/_issuable.html.haml +++ b/app/views/shared/milestones/_issuable.html.haml @@ -12,7 +12,7 @@ - if show_project_name %strong #{project.name} · - elsif show_full_project_name - %strong #{project.name_with_namespace} · + %strong #{project.full_name} · - if issuable.is_a?(Issue) = confidential_icon(issuable) = link_to issuable.title, issuable_url_args, title: issuable.title diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml index e3b2b53833e..da01fc02d07 100644 --- a/app/views/shared/milestones/_milestone.html.haml +++ b/app/views/shared/milestones/_milestone.html.haml @@ -27,7 +27,7 @@ - milestone.milestones.each do |milestone| = link_to milestone_path(milestone) do %span.label.label-gray - = dashboard ? milestone.project.name_with_namespace : milestone.project.name + = dashboard ? milestone.project.full_name : milestone.project.name - if @group .col-sm-6.milestone-actions - if can?(current_user, :admin_milestones, @group) diff --git a/app/views/shared/milestones/_top.html.haml b/app/views/shared/milestones/_top.html.haml index fd0760d83a5..6006ab8b43f 100644 --- a/app/views/shared/milestones/_top.html.haml +++ b/app/views/shared/milestones/_top.html.haml @@ -56,7 +56,7 @@ - milestone.milestones.each do |ms| %tr %td - - project_name = group ? ms.project.name : ms.project.name_with_namespace + - project_name = group ? ms.project.name : ms.project.full_name = link_to project_name, project_milestone_path(ms.project, ms) %td = ms.issues_visible_to_user(current_user).opened.count diff --git a/app/views/shared/snippets/_snippet.html.haml b/app/views/shared/snippets/_snippet.html.haml index 491a8a41090..3acec88c2e3 100644 --- a/app/views/shared/snippets/_snippet.html.haml +++ b/app/views/shared/snippets/_snippet.html.haml @@ -31,7 +31,7 @@ %span.hidden-xs in = link_to project_path(snippet.project) do - = snippet.project.name_with_namespace + = snippet.project.full_name .pull-right.snippet-updated-at %span updated #{time_ago_with_tooltip(snippet.updated_at, placement: 'bottom')} diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index 328db19be29..9962eaccade 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -43,9 +43,9 @@ - pipeline_cache:expire_pipeline_cache - pipeline_creation:create_pipeline - pipeline_creation:run_pipeline_schedule +- pipeline_background:archive_trace - pipeline_default:build_coverage - pipeline_default:build_trace_sections -- pipeline_default:create_trace_artifact - pipeline_default:pipeline_metrics - pipeline_default:pipeline_notification - pipeline_default:update_head_pipeline_for_merge_request diff --git a/app/workers/archive_trace_worker.rb b/app/workers/archive_trace_worker.rb new file mode 100644 index 00000000000..dea7425ad88 --- /dev/null +++ b/app/workers/archive_trace_worker.rb @@ -0,0 +1,10 @@ +class ArchiveTraceWorker + include ApplicationWorker + include PipelineBackgroundQueue + + def perform(job_id) + Ci::Build.find_by(id: job_id).try do |job| + job.trace.archive! + end + end +end diff --git a/app/workers/build_finished_worker.rb b/app/workers/build_finished_worker.rb index b5ed8d607b3..46f1ac09915 100644 --- a/app/workers/build_finished_worker.rb +++ b/app/workers/build_finished_worker.rb @@ -12,7 +12,7 @@ class BuildFinishedWorker # We execute that async as this are two indepentent operations that can be executed after TraceSections and Coverage BuildHooksWorker.perform_async(build.id) - CreateTraceArtifactWorker.perform_async(build.id) + ArchiveTraceWorker.perform_async(build.id) end end end diff --git a/app/workers/concerns/gitlab/github_import/object_importer.rb b/app/workers/concerns/gitlab/github_import/object_importer.rb index 9a9fbaad653..100d86e38c8 100644 --- a/app/workers/concerns/gitlab/github_import/object_importer.rb +++ b/app/workers/concerns/gitlab/github_import/object_importer.rb @@ -22,7 +22,7 @@ module Gitlab importer_class.new(object, project, client).execute - counter.increment(project: project.path_with_namespace) + counter.increment(project: project.full_path) end def counter diff --git a/app/workers/concerns/pipeline_background_queue.rb b/app/workers/concerns/pipeline_background_queue.rb new file mode 100644 index 00000000000..8bf43de6b26 --- /dev/null +++ b/app/workers/concerns/pipeline_background_queue.rb @@ -0,0 +1,10 @@ +## +# Concern for setting Sidekiq settings for the low priority CI pipeline workers. +# +module PipelineBackgroundQueue + extend ActiveSupport::Concern + + included do + queue_namespace :pipeline_background + end +end diff --git a/app/workers/create_trace_artifact_worker.rb b/app/workers/create_trace_artifact_worker.rb deleted file mode 100644 index 11cda58021e..00000000000 --- a/app/workers/create_trace_artifact_worker.rb +++ /dev/null @@ -1,10 +0,0 @@ -class CreateTraceArtifactWorker - include ApplicationWorker - include PipelineQueue - - def perform(job_id) - Ci::Build.preload(:project, :user).find_by(id: job_id).try do |job| - Ci::CreateTraceArtifactService.new(job.project, job.user).execute(job) - end - end -end diff --git a/app/workers/git_garbage_collect_worker.rb b/app/workers/git_garbage_collect_worker.rb index 7ba224d74c8..55fb817ca6e 100644 --- a/app/workers/git_garbage_collect_worker.rb +++ b/app/workers/git_garbage_collect_worker.rb @@ -44,6 +44,10 @@ class GitGarbageCollectWorker # Refresh the branch cache in case garbage collection caused a ref lookup to fail flush_ref_caches(project) if task == :gc + + # In case pack files are deleted, release libgit2 cache and open file + # descriptors ASAP instead of waiting for Ruby garbage collection + project.cleanup ensure cancel_lease(lease_key, lease_uuid) if lease_key.present? && lease_uuid.present? end diff --git a/app/workers/gitlab/github_import/stage/finish_import_worker.rb b/app/workers/gitlab/github_import/stage/finish_import_worker.rb index 073d6608082..a779e631516 100644 --- a/app/workers/gitlab/github_import/stage/finish_import_worker.rb +++ b/app/workers/gitlab/github_import/stage/finish_import_worker.rb @@ -16,7 +16,7 @@ module Gitlab def report_import_time(project) duration = Time.zone.now - project.created_at - path = project.path_with_namespace + path = project.full_path histogram.observe({ project: path }, duration) counter.increment diff --git a/app/workers/pages_worker.rb b/app/workers/pages_worker.rb index d3b95009364..66a0ff83bef 100644 --- a/app/workers/pages_worker.rb +++ b/app/workers/pages_worker.rb @@ -1,7 +1,7 @@ class PagesWorker include ApplicationWorker - sidekiq_options retry: false + sidekiq_options retry: 3 def perform(action, *arg) send(action, *arg) # rubocop:disable GitlabSecurity/PublicSend diff --git a/app/workers/process_commit_worker.rb b/app/workers/process_commit_worker.rb index 5b25d980bdb..201e7f332b4 100644 --- a/app/workers/process_commit_worker.rb +++ b/app/workers/process_commit_worker.rb @@ -30,10 +30,9 @@ class ProcessCommitWorker end def process_commit_message(project, commit, user, author, default = false) - # this is a GitLab generated commit message, ignore it. - return if commit.merged_merge_request?(user) - - closed_issues = default ? commit.closes_issues(user) : [] + # Ignore closing references from GitLab-generated commit messages. + find_closing_issues = default && !commit.merged_merge_request?(user) + closed_issues = find_closing_issues ? commit.closes_issues(user) : [] close_issues(project, user, author, commit, closed_issues) if closed_issues.any? commit.create_cross_references!(author, closed_issues) |