diff options
Diffstat (limited to 'app/assets/javascripts')
29 files changed, 508 insertions, 160 deletions
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue index 0661087a1ba..e9a0dbaa59d 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_table.vue +++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue @@ -25,6 +25,11 @@ type: String, required: true, }, + viewType: { + type: String, + required: false, + default: 'child', + }, }, mixins: [ pipelinesMixin, @@ -110,6 +115,7 @@ :pipelines="state.pipelines" :update-graph-dropdown="updateGraphDropdown" :auto-devops-help-path="autoDevopsHelpPath" + :view-type="viewType" /> </div> </div> diff --git a/app/assets/javascripts/new_sidebar.js b/app/assets/javascripts/contextual_sidebar.js index 997550b37fb..46b68ebe158 100644 --- a/app/assets/javascripts/new_sidebar.js +++ b/app/assets/javascripts/contextual_sidebar.js @@ -2,7 +2,7 @@ import Cookies from 'js-cookie'; import _ from 'underscore'; import bp from './breakpoints'; -export default class NewNavSidebar { +export default class ContextualSidebar { constructor() { this.initDomElements(); this.render(); @@ -55,7 +55,7 @@ export default class NewNavSidebar { this.$sidebar.toggleClass('sidebar-icons-only', collapsed); this.$page.toggleClass('page-with-icon-sidebar', breakpoint === 'sm' ? true : collapsed); } - NewNavSidebar.setCollapsedCookie(collapsed); + ContextualSidebar.setCollapsedCookie(collapsed); this.toggleSidebarOverflow(); } diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 5ca1708f1b3..f20162c48e9 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -8,7 +8,8 @@ /* global NewBranchForm */ /* global NotificationsForm */ /* global NotificationsDropdown */ -/* global GroupAvatar */ +import groupAvatar from './group_avatar'; +import GroupLabelSubscription from './group_label_subscription'; /* global LineHighlighter */ import BuildArtifacts from './build_artifacts'; import CILintEditor from './ci_lint_editor'; @@ -239,7 +240,8 @@ import Diff from './diff'; break; case 'projects:compare:show': new Diff(); - initChangesDropdown(); + const paddingTop = 16; + initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight - paddingTop); break; case 'projects:branches:new': case 'projects:branches:create': @@ -423,11 +425,11 @@ import Diff from './diff'; case 'admin:groups:create': BindInOut.initAll(); new Group(); - new GroupAvatar(); + groupAvatar(); break; case 'groups:edit': case 'admin:groups:edit': - new GroupAvatar(); + groupAvatar(); break; case 'projects:tree:show': shortcut_handler = new ShortcutsNavigation(); @@ -474,7 +476,7 @@ import Diff from './diff'; const $el = $(el); if ($el.find('.dropdown-group-label').length) { - new gl.GroupLabelSubscription($el); + new GroupLabelSubscription($el); } else { new gl.ProjectLabelSubscription($el); } diff --git a/app/assets/javascripts/group_avatar.js b/app/assets/javascripts/group_avatar.js index f03b47b1c1d..2168ff3a8ba 100644 --- a/app/assets/javascripts/group_avatar.js +++ b/app/assets/javascripts/group_avatar.js @@ -1,19 +1,12 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, no-var, one-var, one-var-declaration-per-line, no-useless-escape, max-len */ - -window.GroupAvatar = (function() { - function GroupAvatar() { - $('.js-choose-group-avatar-button').on("click", function() { - var form; - form = $(this).closest("form"); - return form.find(".js-group-avatar-input").click(); - }); - $('.js-group-avatar-input').on("change", function() { - var filename, form; - form = $(this).closest("form"); - filename = $(this).val().replace(/^.*[\\\/]/, ''); - return form.find(".js-avatar-filename").text(filename); - }); - } - - return GroupAvatar; -})(); +export default function groupAvatar() { + $('.js-choose-group-avatar-button').on('click', function onClickGroupAvatar() { + const form = $(this).closest('form'); + return form.find('.js-group-avatar-input').click(); + }); + $('.js-group-avatar-input').on('change', function onChangeAvatarInput() { + const form = $(this).closest('form'); + // eslint-disable-next-line no-useless-escape + const filename = $(this).val().replace(/^.*[\\\/]/, ''); + return form.find('.js-avatar-filename').text(filename); + }); +} diff --git a/app/assets/javascripts/group_label_subscription.js b/app/assets/javascripts/group_label_subscription.js index 7dc9ce898e8..befaebb635e 100644 --- a/app/assets/javascripts/group_label_subscription.js +++ b/app/assets/javascripts/group_label_subscription.js @@ -1,6 +1,4 @@ -/* eslint-disable func-names, object-shorthand, comma-dangle, wrap-iife, space-before-function-paren, no-param-reassign, max-len */ - -class GroupLabelSubscription { +export default class GroupLabelSubscription { constructor(container) { const $container = $(container); this.$dropdown = $container.find('.dropdown'); @@ -18,7 +16,7 @@ class GroupLabelSubscription { $.ajax({ type: 'POST', - url: url + url, }).done(() => { this.toggleSubscriptionButtons(); this.$unsubscribeButtons.removeAttr('data-url'); @@ -35,7 +33,7 @@ class GroupLabelSubscription { $.ajax({ type: 'POST', - url: url + url, }).done(() => { this.toggleSubscriptionButtons(); }); @@ -47,6 +45,3 @@ class GroupLabelSubscription { this.$unsubscribeButtons.toggleClass('hidden'); } } - -window.gl = window.gl || {}; -window.gl.GroupLabelSubscription = GroupLabelSubscription; diff --git a/app/assets/javascripts/init_changes_dropdown.js b/app/assets/javascripts/init_changes_dropdown.js index f785ed29e6c..1bab7965c19 100644 --- a/app/assets/javascripts/init_changes_dropdown.js +++ b/app/assets/javascripts/init_changes_dropdown.js @@ -1,7 +1,7 @@ import stickyMonitor from './lib/utils/sticky'; -export default () => { - stickyMonitor(document.querySelector('.js-diff-files-changed')); +export default (stickyTop) => { + stickyMonitor(document.querySelector('.js-diff-files-changed'), stickyTop); $('.js-diff-stats-dropdown').glDropdown({ filterable: true, diff --git a/app/assets/javascripts/issuable_context.js b/app/assets/javascripts/issuable_context.js index 1d305f1eb2f..73791edaebb 100644 --- a/app/assets/javascripts/issuable_context.js +++ b/app/assets/javascripts/issuable_context.js @@ -51,20 +51,19 @@ const PARTICIPANTS_ROW_COUNT = 7; } IssuableContext.prototype.initParticipants = function() { - $(document).on("click", ".js-participants-more", this.toggleHiddenParticipants); - return $(".js-participants-author").each(function(i) { + $(document).on('click', '.js-participants-more', this.toggleHiddenParticipants); + return $('.js-participants-author').each(function(i) { if (i >= PARTICIPANTS_ROW_COUNT) { - return $(this).addClass("js-participants-hidden").hide(); + return $(this).addClass('js-participants-hidden').hide(); } }); }; - IssuableContext.prototype.toggleHiddenParticipants = function(e) { - var currentText, lessText, originalText; - e.preventDefault(); - currentText = $(this).text().trim(); - lessText = $(this).data("less-text"); - originalText = $(this).data("original-text"); + IssuableContext.prototype.toggleHiddenParticipants = function() { + const currentText = $(this).text().trim(); + const lessText = $(this).data('less-text'); + const originalText = $(this).data('original-text'); + if (currentText === originalText) { $(this).text(lessText); @@ -73,7 +72,7 @@ const PARTICIPANTS_ROW_COUNT = 7; $(this).text(originalText); } - $(".js-participants-hidden").toggle(); + $('.js-participants-hidden').toggle(); }; return IssuableContext; diff --git a/app/assets/javascripts/layout_nav.js b/app/assets/javascripts/layout_nav.js index d064a2c0024..a6f82b247e2 100644 --- a/app/assets/javascripts/layout_nav.js +++ b/app/assets/javascripts/layout_nav.js @@ -1,7 +1,7 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, no-unused-vars, one-var, one-var-declaration-per-line, vars-on-top, max-len */ import _ from 'underscore'; import Cookies from 'js-cookie'; -import NewNavSidebar from './new_sidebar'; +import ContextualSidebar from './contextual_sidebar'; import initFlyOutNav from './fly_out_nav'; (function() { @@ -51,8 +51,8 @@ import initFlyOutNav from './fly_out_nav'; }); $(() => { - const newNavSidebar = new NewNavSidebar(); - newNavSidebar.bindEvents(); + const contextualSidebar = new ContextualSidebar(); + contextualSidebar.bindEvents(); initFlyOutNav(); }); diff --git a/app/assets/javascripts/lib/utils/sticky.js b/app/assets/javascripts/lib/utils/sticky.js index 64db42701ce..098afcfa1b4 100644 --- a/app/assets/javascripts/lib/utils/sticky.js +++ b/app/assets/javascripts/lib/utils/sticky.js @@ -28,14 +28,10 @@ export const isSticky = (el, scrollY, stickyTop, insertPlaceholder) => { } }; -export default (el, insertPlaceholder = true) => { +export default (el, stickyTop, insertPlaceholder = true) => { if (!el) return; - const computedStyle = window.getComputedStyle(el); - - if (!/sticky/.test(computedStyle.position)) return; - - const stickyTop = parseInt(computedStyle.top, 10); + if (typeof CSS === 'undefined' || !(CSS.supports('(position: -webkit-sticky) or (position: sticky)'))) return; document.addEventListener('scroll', () => isSticky(el, window.scrollY, stickyTop, insertPlaceholder), { passive: true, diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 6b13fda0d44..52715fba43f 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -55,8 +55,6 @@ import './gl_dropdown'; import './gl_field_error'; import './gl_field_errors'; import './gl_form'; -import './group_avatar'; -import './group_label_subscription'; import './groups_select'; import './header'; import './importer_status'; diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 789ccf48190..54c1b7a268e 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -67,6 +67,10 @@ import Diff from './diff'; class MergeRequestTabs { constructor({ action, setUrl, stubLocation } = {}) { + const mergeRequestTabs = document.querySelector('.js-tabs-affix'); + const navbar = document.querySelector('.navbar-gitlab'); + const paddingTop = 16; + this.diffsLoaded = false; this.pipelinesLoaded = false; this.commitsLoaded = false; @@ -76,6 +80,11 @@ import Diff from './diff'; this.setCurrentAction = this.setCurrentAction.bind(this); this.tabShown = this.tabShown.bind(this); this.showTab = this.showTab.bind(this); + this.stickyTop = navbar ? navbar.offsetHeight - paddingTop : 0; + + if (mergeRequestTabs) { + this.stickyTop += mergeRequestTabs.offsetHeight; + } if (stubLocation) { location = stubLocation; @@ -278,7 +287,7 @@ import Diff from './diff'; const $container = $('#diffs'); $container.html(data.html); - initChangesDropdown(); + initChangesDropdown(this.stickyTop); if (typeof gl.diffNotesCompileComponents !== 'undefined') { gl.diffNotesCompileComponents(); diff --git a/app/assets/javascripts/pipelines/components/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines.vue index 085bd20cefe..3da60e88474 100644 --- a/app/assets/javascripts/pipelines/components/pipelines.vue +++ b/app/assets/javascripts/pipelines/components/pipelines.vue @@ -12,6 +12,15 @@ type: Object, required: true, }, + // Can be rendered in 3 different places, with some visual differences + // Accepts root | child + // `root` -> main view + // `child` -> rendered inside MR or Commit View + viewType: { + type: String, + required: false, + default: 'root', + }, }, components: { tablePagination, @@ -187,7 +196,7 @@ :empty-state-svg-path="emptyStateSvgPath" /> - <error-state + <error-state v-if="shouldRenderErrorState" :error-state-svg-path="errorStateSvgPath" /> @@ -206,6 +215,7 @@ :pipelines="state.pipelines" :update-graph-dropdown="updateGraphDropdown" :auto-devops-help-path="autoDevopsPath" + :view-type="viewType" /> </div> diff --git a/app/assets/javascripts/pipelines/components/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_table.vue index 7aa0c0e8a7f..16a705cbaff 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_table.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_table.vue @@ -21,6 +21,10 @@ type: String, required: true, }, + viewType: { + type: String, + required: true, + }, }, components: { pipelinesTableRowComponent, @@ -59,6 +63,7 @@ :pipeline="model" :update-graph-dropdown="updateGraphDropdown" :auto-devops-help-path="autoDevopsHelpPath" + :view-type="viewType" /> </div> </template> diff --git a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue index 5b9bb6c3750..33fbce993b2 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue @@ -29,6 +29,10 @@ export default { type: String, required: true, }, + viewType: { + type: String, + required: true, + }, }, components: { asyncButtonComponent, @@ -203,9 +207,13 @@ export default { displayPipelineActions() { return this.pipeline.flags.retryable || - this.pipeline.flags.cancelable || - this.pipeline.details.manual_actions.length || - this.pipeline.details.artifacts.length; + this.pipeline.flags.cancelable || + this.pipeline.details.manual_actions.length || + this.pipeline.details.artifacts.length; + }, + + isChildView() { + return this.viewType === 'child'; }, }, }; @@ -218,7 +226,10 @@ export default { Status </div> <div class="table-mobile-content"> - <ci-badge :status="pipelineStatus"/> + <ci-badge + :status="pipelineStatus" + :show-text="!isChildView" + /> </div> </div> @@ -240,7 +251,9 @@ export default { :commit-url="commitUrl" :short-sha="commitShortSha" :title="commitTitle" - :author="commitAuthor"/> + :author="commitAuthor" + :show-branch="!isChildView" + /> </div> </div> diff --git a/app/assets/javascripts/repo/components/new_dropdown/index.vue b/app/assets/javascripts/repo/components/new_dropdown/index.vue new file mode 100644 index 00000000000..3ccb50213ab --- /dev/null +++ b/app/assets/javascripts/repo/components/new_dropdown/index.vue @@ -0,0 +1,86 @@ +<script> + import RepoStore from '../../stores/repo_store'; + import RepoHelper from '../../helpers/repo_helper'; + import eventHub from '../../event_hub'; + import newModal from './modal.vue'; + + export default { + components: { + newModal, + }, + data() { + return { + openModal: false, + modalType: '', + currentPath: RepoStore.path, + }; + }, + methods: { + createNewItem(type) { + this.modalType = type; + this.toggleModalOpen(); + }, + toggleModalOpen() { + this.openModal = !this.openModal; + }, + createNewEntryInStore(name, type) { + RepoHelper.createNewEntry(name, type); + + this.toggleModalOpen(); + }, + }, + created() { + eventHub.$on('createNewEntry', this.createNewEntryInStore); + }, + beforeDestroy() { + eventHub.$off('createNewEntry', this.createNewEntryInStore); + }, + }; +</script> + +<template> + <div> + <ul class="breadcrumb repo-breadcrumb"> + <li class="dropdown"> + <button + type="button" + class="btn btn-default dropdown-toggle add-to-tree" + data-toggle="dropdown" + aria-label="Create new file or directory" + > + <i + class="fa fa-plus" + aria-hidden="true" + > + </i> + </button> + <ul class="dropdown-menu"> + <li> + <a + href="#" + role="button" + @click.prevent="createNewItem('blob')" + > + {{ __('New file') }} + </a> + </li> + <li> + <a + href="#" + role="button" + @click.prevent="createNewItem('tree')" + > + {{ __('New directory') }} + </a> + </li> + </ul> + </li> + </ul> + <new-modal + v-if="openModal" + :type="modalType" + :current-path="currentPath" + @toggle="toggleModalOpen" + /> + </div> +</template> diff --git a/app/assets/javascripts/repo/components/new_dropdown/modal.vue b/app/assets/javascripts/repo/components/new_dropdown/modal.vue new file mode 100644 index 00000000000..5ef629e0dde --- /dev/null +++ b/app/assets/javascripts/repo/components/new_dropdown/modal.vue @@ -0,0 +1,90 @@ +<script> + import { __ } from '../../../locale'; + import popupDialog from '../../../vue_shared/components/popup_dialog.vue'; + import eventHub from '../../event_hub'; + + export default { + props: { + currentPath: { + type: String, + required: true, + }, + type: { + type: String, + required: true, + }, + }, + data() { + return { + entryName: this.currentPath !== '' ? `${this.currentPath}/` : '', + }; + }, + components: { + popupDialog, + }, + methods: { + createEntryInStore() { + eventHub.$emit('createNewEntry', this.entryName, this.type); + }, + toggleModalOpen() { + this.$emit('toggle'); + }, + }, + computed: { + 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(); + }, + }; +</script> + +<template> + <popup-dialog + :title="modalTitle" + :primary-button-label="buttonLabel" + kind="success" + @toggle="toggleModalOpen" + @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> + </popup-dialog> +</template> diff --git a/app/assets/javascripts/repo/components/repo.vue b/app/assets/javascripts/repo/components/repo.vue index 6310bdb3270..788976a9804 100644 --- a/app/assets/javascripts/repo/components/repo.vue +++ b/app/assets/javascripts/repo/components/repo.vue @@ -46,6 +46,10 @@ export default { dialogSubmitted(status) { this.toggleDialogOpen(false); this.dialog.status = status; + + // remove tmp files + Helper.removeAllTmpFiles('openedFiles'); + Helper.removeAllTmpFiles('files'); }, toggleBlobView: Store.toggleBlobView, createNewBranch(branch) { diff --git a/app/assets/javascripts/repo/components/repo_commit_section.vue b/app/assets/javascripts/repo/components/repo_commit_section.vue index 185cd90ac06..0d6259a37a8 100644 --- a/app/assets/javascripts/repo/components/repo_commit_section.vue +++ b/app/assets/javascripts/repo/components/repo_commit_section.vue @@ -49,7 +49,7 @@ export default { // see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions const commitMessage = this.commitMessage; const actions = this.changedFiles.map(f => ({ - action: 'update', + action: f.tempFile ? 'create' : 'update', file_path: f.path, content: f.newContent, })); @@ -62,7 +62,6 @@ export default { if (newBranch) { payload.start_branch = this.currentBranch; } - this.submitCommitsLoading = true; Service.commitFiles(payload) .then(() => { this.resetCommitState(); @@ -78,6 +77,8 @@ export default { }, tryCommit(e, skipBranchCheck = false, newBranch = false) { + this.submitCommitsLoading = true; + if (skipBranchCheck) { this.makeCommit(newBranch); } else { @@ -90,6 +91,7 @@ export default { this.makeCommit(newBranch); }) .catch(() => { + this.submitCommitsLoading = false; Flash('An error occurred while committing your changes'); }); } diff --git a/app/assets/javascripts/repo/components/repo_editor.vue b/app/assets/javascripts/repo/components/repo_editor.vue index 4639bee6d66..df4caba51d8 100644 --- a/app/assets/javascripts/repo/components/repo_editor.vue +++ b/app/assets/javascripts/repo/components/repo_editor.vue @@ -16,7 +16,7 @@ const RepoEditor = { }, mounted() { - Service.getRaw(this.activeFile.raw_path) + Service.getRaw(this.activeFile) .then((rawResponse) => { Store.blobRaw = rawResponse.data; Store.activeFile.plain = rawResponse.data; diff --git a/app/assets/javascripts/repo/components/repo_file_buttons.vue b/app/assets/javascripts/repo/components/repo_file_buttons.vue index 03cd219e718..c98f641c853 100644 --- a/app/assets/javascripts/repo/components/repo_file_buttons.vue +++ b/app/assets/javascripts/repo/components/repo_file_buttons.vue @@ -11,7 +11,12 @@ const RepoFileButtons = { mixins: [RepoMixin], computed: { - + showButtons() { + return this.activeFile.raw_path || + this.activeFile.blame_path || + this.activeFile.commits_path || + this.activeFile.permalink; + }, rawDownloadButtonLabel() { return this.binary ? 'Download' : 'Raw'; }, @@ -30,7 +35,10 @@ export default RepoFileButtons; </script> <template> - <div id="repo-file-buttons"> + <div + v-if="showButtons" + class="repo-file-buttons" + > <a :href="activeFile.raw_path" target="_blank" diff --git a/app/assets/javascripts/repo/components/repo_tab.vue b/app/assets/javascripts/repo/components/repo_tab.vue index 098715915b0..405d7b4cf86 100644 --- a/app/assets/javascripts/repo/components/repo_tab.vue +++ b/app/assets/javascripts/repo/components/repo_tab.vue @@ -18,8 +18,8 @@ const RepoTab = { }, changedClass() { const tabChangedObj = { - 'fa-times close-icon': !this.tab.changed, - 'fa-circle unsaved-icon': this.tab.changed, + 'fa-times close-icon': !this.tab.changed && !this.tab.tempFile, + 'fa-circle unsaved-icon': this.tab.changed || this.tab.tempFile, }; return tabChangedObj; }, @@ -30,7 +30,7 @@ const RepoTab = { Store.setActiveFiles(file); }, closeTab(file) { - if (file.changed) return; + if (file.changed || file.tempFile) return; Store.removeFromOpenedFiles(file); }, diff --git a/app/assets/javascripts/repo/helpers/repo_helper.js b/app/assets/javascripts/repo/helpers/repo_helper.js index f7b7f93e4b8..fb26f3b7380 100644 --- a/app/assets/javascripts/repo/helpers/repo_helper.js +++ b/app/assets/javascripts/repo/helpers/repo_helper.js @@ -1,4 +1,3 @@ -import { convertPermissionToBoolean } from '../../lib/utils/common_utils'; import Service from '../services/repo_service'; import Store from '../stores/repo_store'; import Flash from '../../flash'; @@ -8,6 +7,7 @@ const RepoHelper = { getDefaultActiveFile() { return { + id: '', active: true, binary: false, extension: '', @@ -62,6 +62,7 @@ const RepoHelper = { }); RepoHelper.updateHistoryEntry(tree.url, title); + Store.path = tree.path; }, setDirectoryToClosed(entry) { @@ -96,8 +97,8 @@ const RepoHelper = { .then((response) => { const data = response.data; if (response.headers && response.headers['page-title']) data.pageTitle = decodeURI(response.headers['page-title']); - if (response.headers && response.headers['is-root'] && !Store.isInitialRoot) { - Store.isRoot = convertPermissionToBoolean(response.headers['is-root']); + if (data.path && !Store.isInitialRoot) { + Store.isRoot = data.path === '/'; Store.isInitialRoot = Store.isRoot; } @@ -110,7 +111,7 @@ const RepoHelper = { RepoHelper.setBinaryDataAsBase64(data); Store.setViewToPreview(); } else if (!Store.isPreviewView() && !data.render_error) { - Service.getRaw(data.raw_path) + Service.getRaw(data) .then((rawResponse) => { Store.blobRaw = rawResponse.data; data.plain = rawResponse.data; @@ -138,6 +139,10 @@ const RepoHelper = { addToDirectory(file, data) { const tree = file || Store; + + // TODO: Figure out why `popstate` is being trigger in the specs + if (!tree.files) return; + const files = tree.files.concat(this.dataToListOfFiles(data, file ? file.level + 1 : 0)); tree.files = files; @@ -157,7 +162,18 @@ const RepoHelper = { }, serializeRepoEntity(type, entity, level = 0) { - const { id, url, name, icon, last_commit, tree_url } = entity; + const { + id, + url, + name, + icon, + last_commit, + tree_url, + path, + tempFile, + active, + opened, + } = entity; return { id, @@ -165,11 +181,14 @@ const RepoHelper = { name, url, tree_url, + path, level, + tempFile, icon: `fa-${icon}`, files: [], loading: false, - opened: false, + opened, + active, // eslint-disable-next-line camelcase lastCommit: last_commit ? { url: `${Store.projectUrl}/commit/${last_commit.id}`, @@ -213,7 +232,7 @@ const RepoHelper = { }, findOpenedFileFromActive() { - return Store.openedFiles.find(openedFile => Store.activeFile.url === openedFile.url); + return Store.openedFiles.find(openedFile => Store.activeFile.id === openedFile.id); }, getFileFromPath(path) { @@ -223,6 +242,76 @@ const RepoHelper = { loadingError() { Flash('Unable to load this content at this time.'); }, + openEditMode() { + Store.editMode = true; + Store.currentBlobView = 'repo-editor'; + }, + updateStorePath(path) { + Store.path = path; + }, + findOrCreateEntry(type, tree, name) { + let exists = true; + let foundEntry = tree.files.find(dir => dir.type === type && dir.name === name); + + if (!foundEntry) { + foundEntry = RepoHelper.serializeRepoEntity(type, { + id: name, + name, + path: tree.path ? `${tree.path}/${name}` : name, + icon: type === 'tree' ? 'folder' : 'file-text-o', + tempFile: true, + opened: true, + active: true, + }, tree.level !== undefined ? tree.level + 1 : 0); + + exists = false; + tree.files.push(foundEntry); + } + + return { + entry: foundEntry, + exists, + }; + }, + removeAllTmpFiles(storeFilesKey) { + Store[storeFilesKey] = Store[storeFilesKey].filter(f => !f.tempFile); + }, + createNewEntry(name, type) { + const originalPath = Store.path; + let entryName = name; + + if (entryName.indexOf(`${originalPath}/`) !== 0) { + this.updateStorePath(''); + } else { + entryName = entryName.replace(`${originalPath}/`, ''); + } + + if (entryName === '') return; + + const fileName = type === 'tree' ? '.gitkeep' : entryName; + let tree = Store; + + if (type === 'tree') { + const dirNames = entryName.split('/'); + + dirNames.forEach((dirName) => { + if (dirName === '') return; + + tree = this.findOrCreateEntry('tree', tree, dirName).entry; + }); + } + + if ((type === 'tree' && tree.tempFile) || type === 'blob') { + const file = this.findOrCreateEntry('blob', tree, fileName); + + if (!file.exists) { + this.setFile(file.entry, file.entry); + this.openEditMode(); + } + } + + this.updateStorePath(originalPath); + }, }; export default RepoHelper; diff --git a/app/assets/javascripts/repo/index.js b/app/assets/javascripts/repo/index.js index 85e960df497..72fc5a70648 100644 --- a/app/assets/javascripts/repo/index.js +++ b/app/assets/javascripts/repo/index.js @@ -6,6 +6,7 @@ import Store from './stores/repo_store'; import Repo from './components/repo.vue'; import RepoEditButton from './components/repo_edit_button.vue'; import newBranchForm from './components/new_branch_form.vue'; +import newDropdown from './components/new_dropdown/index.vue'; import Translate from '../vue_shared/translate'; function initDropdowns() { @@ -28,6 +29,7 @@ function setInitialStore(data) { Store.service = Service; Store.service.url = data.url; Store.service.refsUrl = data.refsUrl; + Store.path = data.currentPath; Store.projectId = data.projectId; Store.projectName = data.projectName; Store.projectUrl = data.projectUrl; @@ -63,6 +65,18 @@ function initRepoEditButton(el) { }); } +function initNewDropdown(el) { + return new Vue({ + el, + components: { + newDropdown, + }, + render(createElement) { + return createElement('new-dropdown'); + }, + }); +} + function initNewBranchForm() { const el = document.querySelector('.js-new-branch-dropdown'); @@ -86,6 +100,7 @@ function initNewBranchForm() { function initRepoBundle() { const repo = document.getElementById('repo'); const editButton = document.querySelector('.editable-mode'); + const newDropdownHolder = document.querySelector('.js-new-dropdown'); setInitialStore(repo.dataset); addEventsForNonVueEls(); initDropdowns(); @@ -95,6 +110,7 @@ function initRepoBundle() { initRepo(repo); initRepoEditButton(editButton); initNewBranchForm(); + initNewDropdown(newDropdownHolder); } $(initRepoBundle); diff --git a/app/assets/javascripts/repo/mixins/repo_mixin.js b/app/assets/javascripts/repo/mixins/repo_mixin.js index c8e8238a0d3..efeda426b96 100644 --- a/app/assets/javascripts/repo/mixins/repo_mixin.js +++ b/app/assets/javascripts/repo/mixins/repo_mixin.js @@ -8,7 +8,7 @@ const RepoMixin = { changedFiles() { const changedFileList = this.openedFiles - .filter(file => file.changed); + .filter(file => file.changed || file.tempFile); return changedFileList; }, }, diff --git a/app/assets/javascripts/repo/services/repo_service.js b/app/assets/javascripts/repo/services/repo_service.js index 786b5637493..c9fa5cc8bf8 100644 --- a/app/assets/javascripts/repo/services/repo_service.js +++ b/app/assets/javascripts/repo/services/repo_service.js @@ -16,8 +16,14 @@ const RepoService = { createBranchPath: '/api/:version/projects/:id/repository/branches', richExtensionRegExp: /md/, - getRaw(url) { - return axios.get(url, { + getRaw(file) { + if (file.tempFile) { + return Promise.resolve({ + data: '', + }); + } + + return axios.get(file.raw_path, { // Stop Axios from parsing a JSON file into a JS object transformResponse: [res => res], }); diff --git a/app/assets/javascripts/repo/stores/repo_store.js b/app/assets/javascripts/repo/stores/repo_store.js index 39e1b4e5849..38df1e3e0d2 100644 --- a/app/assets/javascripts/repo/stores/repo_store.js +++ b/app/assets/javascripts/repo/stores/repo_store.js @@ -39,6 +39,7 @@ const RepoStore = { newMrTemplateUrl: '', branchChanged: false, commitMessage: '', + path: '', loading: { tree: false, blob: false, @@ -77,21 +78,23 @@ const RepoStore = { } else if (file.newContent || file.plain) { RepoStore.blobRaw = file.newContent || file.plain; } else { - Service.getRaw(file.raw_path) + Service.getRaw(file) .then((rawResponse) => { RepoStore.blobRaw = rawResponse.data; Helper.findOpenedFileFromActive().plain = rawResponse.data; }).catch(Helper.loadingError); } - if (!file.loading) Helper.updateHistoryEntry(file.url, file.pageTitle || file.name); + if (!file.loading && !file.tempFile) { + Helper.updateHistoryEntry(file.url, file.pageTitle || file.name); + } RepoStore.binary = file.binary; RepoStore.setActiveLine(-1); }, setFileActivity(file, openedFile, i) { const activeFile = openedFile; - activeFile.active = file.url === activeFile.url; + activeFile.active = file.id === activeFile.id; if (activeFile.active) RepoStore.setActiveFile(activeFile, i); @@ -99,7 +102,7 @@ const RepoStore = { }, setActiveFile(activeFile, i) { - RepoStore.activeFile = Object.assign({}, RepoStore.activeFile, activeFile); + RepoStore.activeFile = Object.assign({}, Helper.getDefaultActiveFile(), activeFile); RepoStore.activeFileIndex = i; }, @@ -121,6 +124,11 @@ const RepoStore = { return openedFile.path !== file.path; }); + // remove the file from the sidebar if it is a tempFile + if (file.tempFile) { + RepoStore.files = RepoStore.files.filter(f => !(f.tempFile && f.path === file.path)); + } + // now activate the right tab based on what you closed. if (RepoStore.openedFiles.length === 0) { RepoStore.activeFile = {}; @@ -170,7 +178,7 @@ const RepoStore = { // getters isActiveFile(file) { - return file && file.url === RepoStore.activeFile.url; + return file && file.id === RepoStore.activeFile.id; }, isPreviewView() { diff --git a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue index caa28bff6db..5b6c6e8d0b9 100644 --- a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue +++ b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue @@ -1,52 +1,64 @@ <script> -import ciIcon from './ci_icon.vue'; -/** - * Renders CI Badge link with CI icon and status text based on - * API response shared between all places where it is used. - * - * Receives status object containing: - * status: { - * details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url - * group:"running" // used for CSS class - * icon: "icon_status_running" // used to render the icon - * label:"running" // used for potential tooltip - * text:"running" // text rendered - * } - * - * Used in: - * - Pipelines table - first column - * - Jobs table - first column - * - Pipeline show view - header - * - Job show view - header - * - MR widget - */ + import ciIcon from './ci_icon.vue'; + import tooltip from '../directives/tooltip'; + /** + * Renders CI Badge link with CI icon and status text based on + * API response shared between all places where it is used. + * + * Receives status object containing: + * status: { + * details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url + * group:"running" // used for CSS class + * icon: "icon_status_running" // used to render the icon + * label:"running" // used for potential tooltip + * text:"running" // text rendered + * } + * + * Used in: + * - Pipelines table - first column + * - Jobs table - first column + * - Pipeline show view - header + * - Job show view - header + * - MR widget + */ -export default { - props: { - status: { - type: Object, - required: true, + export default { + props: { + status: { + type: Object, + required: true, + }, + showText: { + type: Boolean, + required: false, + default: true, + }, }, - }, - - components: { - ciIcon, - }, - - computed: { - cssClass() { - const className = this.status.group; + components: { + ciIcon, + }, + directives: { + tooltip, + }, + computed: { + cssClass() { + const className = this.status.group; - return className ? `ci-status ci-${this.status.group}` : 'ci-status'; + return className ? `ci-status ci-${className}` : 'ci-status'; + }, }, - }, -}; + }; </script> <template> <a :href="status.details_path" - :class="cssClass"> + :class="cssClass" + v-tooltip + :title="!showText ? status.text : ''"> <ci-icon :status="status" /> - {{status.text}} + + <template v-if="showText"> + {{status.text}} + </template> </a> </template> diff --git a/app/assets/javascripts/vue_shared/components/commit.vue b/app/assets/javascripts/vue_shared/components/commit.vue index 50d14282cad..52814de8b2d 100644 --- a/app/assets/javascripts/vue_shared/components/commit.vue +++ b/app/assets/javascripts/vue_shared/components/commit.vue @@ -63,14 +63,17 @@ required: false, default: () => ({}), }, + showBranch: { + type: Boolean, + required: false, + default: true, + }, }, computed: { /** * Used to verify if all the properties needed to render the commit * ref section were provided. * - * TODO: Improve this! Use lodash _.has when we have it. - * * @returns {Boolean} */ hasCommitRef() { @@ -80,8 +83,6 @@ * Used to verify if all the properties needed to render the commit * author section were provided. * - * TODO: Improve this! Use lodash _.has when we have it. - * * @returns {Boolean} */ hasAuthor() { @@ -114,31 +115,30 @@ </script> <template> <div class="branch-commit"> - <div - v-if="hasCommitRef" - class="icon-container hidden-xs"> - <i - v-if="tag" - class="fa fa-tag" - aria-hidden="true"> - </i> - <i - v-if="!tag" - class="fa fa-code-fork" - aria-hidden="true"> - </i> - </div> - - <a - v-if="hasCommitRef" - class="ref-name hidden-xs" - :href="commitRef.ref_url" - v-tooltip - data-container="body" - :title="commitRef.name"> - {{commitRef.name}} - </a> + <template v-if="hasCommitRef && showBranch"> + <div + class="icon-container hidden-xs"> + <i + v-if="tag" + class="fa fa-tag" + aria-hidden="true"> + </i> + <i + v-if="!tag" + class="fa fa-code-fork" + aria-hidden="true"> + </i> + </div> + <a + class="ref-name hidden-xs" + :href="commitRef.ref_url" + v-tooltip + data-container="body" + :title="commitRef.name"> + {{commitRef.name}} + </a> + </template> <div v-html="commitIconSvg" class="commit-icon js-commit-icon"> diff --git a/app/assets/javascripts/vue_shared/components/popup_dialog.vue b/app/assets/javascripts/vue_shared/components/popup_dialog.vue index 7d8c5936b7d..9e8c10bdc1a 100644 --- a/app/assets/javascripts/vue_shared/components/popup_dialog.vue +++ b/app/assets/javascripts/vue_shared/components/popup_dialog.vue @@ -9,7 +9,7 @@ export default { }, text: { type: String, - required: true, + required: false, }, kind: { type: String, @@ -82,14 +82,15 @@ export default { type="button" class="btn" :class="btnCancelKindClass" - @click="emitSubmit(false)"> - {{closeButtonLabel}} + @click="close"> + {{ closeButtonLabel }} </button> - <button type="button" + <button + type="button" class="btn" :class="btnKindClass" @click="emitSubmit(true)"> - {{primaryButtonLabel}} + {{ primaryButtonLabel }} </button> </div> </div> |