diff options
Diffstat (limited to 'app/assets/javascripts/ide')
35 files changed, 542 insertions, 189 deletions
diff --git a/app/assets/javascripts/ide/components/activity_bar.vue b/app/assets/javascripts/ide/components/activity_bar.vue index 183816921c1..644808cb83a 100644 --- a/app/assets/javascripts/ide/components/activity_bar.vue +++ b/app/assets/javascripts/ide/components/activity_bar.vue @@ -1,8 +1,6 @@ <script> -import $ from 'jquery'; import { mapActions, mapState } from 'vuex'; -import { GlIcon } from '@gitlab/ui'; -import tooltip from '~/vue_shared/directives/tooltip'; +import { GlIcon, GlTooltipDirective } from '@gitlab/ui'; import { leftSidebarViews } from '../constants'; export default { @@ -10,7 +8,7 @@ export default { GlIcon, }, directives: { - tooltip, + GlTooltip: GlTooltipDirective, }, computed: { ...mapState(['currentActivityView']), @@ -22,9 +20,7 @@ export default { this.updateActivityBarView(view); - // TODO: We must use JQuery here to interact with the Bootstrap tooltip API - // https://gitlab.com/gitlab-org/gitlab/-/issues/217577 - $(e.currentTarget).tooltip('hide'); + this.$root.$emit('bv::hide::tooltip'); }, }, leftSidebarViews, @@ -32,11 +28,11 @@ export default { </script> <template> - <nav class="ide-activity-bar"> + <nav class="ide-activity-bar" data-testid="left-sidebar"> <ul class="list-unstyled"> <li> <button - v-tooltip + v-gl-tooltip.right.viewport :class="{ active: currentActivityView === $options.leftSidebarViews.edit.name, }" @@ -54,7 +50,7 @@ export default { </li> <li> <button - v-tooltip + v-gl-tooltip.right.viewport :class="{ active: currentActivityView === $options.leftSidebarViews.review.name, }" @@ -71,7 +67,7 @@ export default { </li> <li> <button - v-tooltip + v-gl-tooltip.right.viewport :class="{ active: currentActivityView === $options.leftSidebarViews.commit.name, }" diff --git a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue index de4b0a34002..b89329c92ec 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue @@ -1,8 +1,8 @@ <script> -/* eslint-disable vue/no-v-html */ import { escape } from 'lodash'; import { mapState, mapGetters, createNamespacedHelpers } from 'vuex'; -import { sprintf, s__ } from '~/locale'; +import { GlSprintf } from '@gitlab/ui'; +import { s__ } from '~/locale'; import consts from '../../stores/modules/commit/constants'; import RadioGroup from './radio_group.vue'; import NewMergeRequestOption from './new_merge_request_option.vue'; @@ -13,6 +13,7 @@ const { mapState: mapCommitState, mapActions: mapCommitActions } = createNamespa export default { components: { + GlSprintf, RadioGroup, NewMergeRequestOption, }, @@ -20,12 +21,8 @@ export default { ...mapState(['currentBranchId', 'changedFiles', 'stagedFiles']), ...mapCommitState(['commitAction']), ...mapGetters(['currentBranch', 'emptyRepo', 'canPushToBranch']), - commitToCurrentBranchText() { - return sprintf( - s__('IDE|Commit to %{branchName} branch'), - { branchName: `<strong class="monospace">${escape(this.currentBranchId)}</strong>` }, - false, - ); + currentBranchText() { + return escape(this.currentBranchId); }, containsStagedChanges() { return this.changedFiles.length > 0 && this.stagedFiles.length > 0; @@ -77,11 +74,13 @@ export default { :disabled="!canPushToBranch" :title="$options.currentBranchPermissionsTooltip" > - <span - class="ide-option-label" - data-qa-selector="commit_to_current_branch_radio" - v-html="commitToCurrentBranchText" - ></span> + <span class="ide-option-label" data-qa-selector="commit_to_current_branch_radio"> + <gl-sprintf :message="s__('IDE|Commit to %{branchName} branch')"> + <template #branchName> + <strong class="monospace">{{ currentBranchText }}</strong> + </template> + </gl-sprintf> + </span> </radio-group> <template v-if="!emptyRepo"> <radio-group diff --git a/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue b/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue index bbcb866c758..53fac09ab66 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue @@ -1,6 +1,6 @@ <script> import { mapActions } from 'vuex'; -import { GlModal } from '@gitlab/ui'; +import { GlModal, GlButton } from '@gitlab/ui'; import { sprintf, __ } from '~/locale'; import FileIcon from '~/vue_shared/components/file_icon.vue'; import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue'; @@ -8,6 +8,7 @@ import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue'; export default { components: { GlModal, + GlButton, FileIcon, ChangedFileIcon, }, @@ -52,15 +53,16 @@ export default { </strong> <changed-file-icon :file="activeFile" :is-centered="false" /> <div class="ml-auto"> - <button + <gl-button v-if="canDiscard" ref="discardButton" - type="button" - class="btn btn-remove btn-inverted gl-mr-3" + category="secondary" + variant="danger" + class="gl-mr-3" @click="showDiscardModal" > {{ __('Discard changes') }} - </button> + </gl-button> </div> <gl-modal ref="discardModal" diff --git a/app/assets/javascripts/ide/components/commit_sidebar/form.vue b/app/assets/javascripts/ide/components/commit_sidebar/form.vue index 73c56514fce..f36fe87ccfa 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/form.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/form.vue @@ -7,7 +7,6 @@ import CommitMessageField from './message_field.vue'; import Actions from './actions.vue'; import SuccessMessage from './success_message.vue'; import { leftSidebarViews, MAX_WINDOW_HEIGHT_COMPACT } from '../../constants'; -import consts from '../../stores/modules/commit/constants'; import { createUnexpectedCommitError } from '../../lib/errors'; export default { @@ -45,12 +44,11 @@ export default { return this.currentActivityView === leftSidebarViews.commit.name; }, commitErrorPrimaryAction() { - if (!this.lastCommitError?.canCreateBranch) { - return undefined; - } + const { primaryAction } = this.lastCommitError || {}; return { - text: __('Create new branch'), + button: primaryAction ? { text: primaryAction.text } : undefined, + callback: primaryAction?.callback?.bind(this, this.$store) || (() => {}), }; }, }, @@ -78,9 +76,6 @@ export default { commit() { return this.commitChanges(); }, - forceCreateNewBranch() { - return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH).then(() => this.commit()); - }, handleCompactState() { if (this.lastCommitMsg) { this.isCompact = false; @@ -188,9 +183,9 @@ export default { ref="commitErrorModal" modal-id="ide-commit-error-modal" :title="lastCommitError.title" - :action-primary="commitErrorPrimaryAction" + :action-primary="commitErrorPrimaryAction.button" :action-cancel="{ text: __('Cancel') }" - @ok="forceCreateNewBranch" + @ok="commitErrorPrimaryAction.callback" > <div v-safe-html="lastCommitError.messageHTML"></div> </gl-modal> diff --git a/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue b/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue index 2787b10a48b..7d08815b033 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue @@ -1,5 +1,5 @@ <script> -import { GlIcon } from '@gitlab/ui'; +import { GlIcon, GlPopover } from '@gitlab/ui'; import { __, sprintf } from '../../../locale'; import popover from '../../../vue_shared/directives/popover'; import { MAX_TITLE_LENGTH, MAX_BODY_LENGTH } from '../../constants'; @@ -10,6 +10,7 @@ export default { }, components: { GlIcon, + GlPopover, }, props: { text: { @@ -58,7 +59,7 @@ export default { }, }, popoverOptions: { - trigger: 'hover', + triggers: 'hover', placement: 'top', content: sprintf( __(` @@ -83,9 +84,16 @@ export default { <ul class="nav-links"> <li> {{ __('Commit Message') }} - <span v-popover="$options.popoverOptions" class="form-text text-muted gl-ml-3"> - <gl-icon name="question" /> - </span> + <div id="ide-commit-message-popover-container"> + <span id="ide-commit-message-question" class="form-text text-muted gl-ml-3"> + <gl-icon name="question" /> + </span> + <gl-popover + target="ide-commit-message-question" + container="ide-commit-message-popover-container" + v-bind="$options.popoverOptions" + /> + </div> </li> </ul> </div> @@ -108,6 +116,7 @@ export default { :placeholder="placeholder" :value="text" class="note-textarea ide-commit-message-textarea" + data-qa-selector="ide_commit_message_field" dir="auto" name="commit-message" @scroll="handleScroll" diff --git a/app/assets/javascripts/ide/components/editor_mode_dropdown.vue b/app/assets/javascripts/ide/components/editor_mode_dropdown.vue index 732fa0786b0..dec8aa61838 100644 --- a/app/assets/javascripts/ide/components/editor_mode_dropdown.vue +++ b/app/assets/javascripts/ide/components/editor_mode_dropdown.vue @@ -1,8 +1,12 @@ <script> +import { GlButton } from '@gitlab/ui'; import { __, sprintf } from '~/locale'; import { viewerTypes } from '../constants'; export default { + components: { + GlButton, + }, props: { viewer: { type: String, @@ -31,7 +35,7 @@ export default { <template> <div class="dropdown"> - <button type="button" class="btn btn-link" data-toggle="dropdown">{{ __('Edit') }}</button> + <gl-button variant="link" data-toggle="dropdown">{{ __('Edit') }}</gl-button> <div class="dropdown-menu dropdown-menu-selectable dropdown-open-left"> <ul> <li> diff --git a/app/assets/javascripts/ide/components/file_templates/bar.vue b/app/assets/javascripts/ide/components/file_templates/bar.vue index b6a57d1b6e6..88dca2f0556 100644 --- a/app/assets/javascripts/ide/components/file_templates/bar.vue +++ b/app/assets/javascripts/ide/components/file_templates/bar.vue @@ -1,10 +1,12 @@ <script> +import { GlButton } from '@gitlab/ui'; import { mapActions, mapGetters, mapState } from 'vuex'; import Dropdown from './dropdown.vue'; export default { components: { Dropdown, + GlButton, }, computed: { ...mapGetters(['activeFile']), @@ -65,9 +67,9 @@ export default { @click="selectTemplate" /> <transition name="fade"> - <button v-show="updateSuccess" type="button" class="btn btn-default" @click="undo"> + <gl-button v-show="updateSuccess" category="secondary" variant="default" @click="undo"> {{ __('Undo') }} - </button> + </gl-button> </transition> </div> </template> diff --git a/app/assets/javascripts/ide/components/file_templates/dropdown.vue b/app/assets/javascripts/ide/components/file_templates/dropdown.vue index d80662f6ae1..cfd2555b769 100644 --- a/app/assets/javascripts/ide/components/file_templates/dropdown.vue +++ b/app/assets/javascripts/ide/components/file_templates/dropdown.vue @@ -1,12 +1,13 @@ <script> import $ from 'jquery'; import { mapActions, mapState } from 'vuex'; -import { GlLoadingIcon } from '@gitlab/ui'; +import { GlIcon, GlLoadingIcon } from '@gitlab/ui'; import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue'; export default { components: { DropdownButton, + GlIcon, GlLoadingIcon, }, props: { @@ -85,7 +86,7 @@ export default { type="search" class="dropdown-input-field qa-dropdown-filter-input" /> - <i aria-hidden="true" class="fa fa-search dropdown-input-search"></i> + <gl-icon name="search" class="dropdown-input-search" aria-hidden="true" /> </div> <div class="dropdown-content"> <gl-loading-icon v-if="showLoading" size="lg" /> diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue index 1b03d9eee8b..8f23856fd6c 100644 --- a/app/assets/javascripts/ide/components/ide.vue +++ b/app/assets/javascripts/ide/components/ide.vue @@ -2,7 +2,18 @@ import { mapActions, mapGetters, mapState } from 'vuex'; import { GlButton, GlLoadingIcon } from '@gitlab/ui'; import { __ } from '~/locale'; +import { + WEBIDE_MARK_APP_START, + WEBIDE_MARK_FILE_FINISH, + WEBIDE_MARK_FILE_CLICKED, + WEBIDE_MARK_TREE_FINISH, + WEBIDE_MEASURE_TREE_FROM_REQUEST, + WEBIDE_MEASURE_FILE_FROM_REQUEST, + WEBIDE_MEASURE_FILE_AFTER_INTERACTION, +} from '~/performance_constants'; +import { performanceMarkAndMeasure } from '~/performance_utils'; import { modalTypes } from '../constants'; +import eventHub from '../eventhub'; import FindFile from '~/vue_shared/components/file_finder/index.vue'; import NewModal from './new_dropdown/modal.vue'; import IdeSidebar from './ide_side_bar.vue'; @@ -14,6 +25,22 @@ import ErrorMessage from './error_message.vue'; import CommitEditorHeader from './commit_sidebar/editor_header.vue'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import { measurePerformance } from '../utils'; + +eventHub.$on(WEBIDE_MEASURE_TREE_FROM_REQUEST, () => + measurePerformance(WEBIDE_MARK_TREE_FINISH, WEBIDE_MEASURE_TREE_FROM_REQUEST), +); +eventHub.$on(WEBIDE_MEASURE_FILE_FROM_REQUEST, () => + measurePerformance(WEBIDE_MARK_FILE_FINISH, WEBIDE_MEASURE_FILE_FROM_REQUEST), +); +eventHub.$on(WEBIDE_MEASURE_FILE_AFTER_INTERACTION, () => + measurePerformance( + WEBIDE_MARK_FILE_FINISH, + WEBIDE_MEASURE_FILE_AFTER_INTERACTION, + WEBIDE_MARK_FILE_CLICKED, + ), +); + export default { components: { NewModal, @@ -59,6 +86,9 @@ export default { if (this.themeName) document.querySelector('.navbar-gitlab').classList.add(`theme-${this.themeName}`); }, + beforeCreate() { + performanceMarkAndMeasure({ mark: WEBIDE_MARK_APP_START }); + }, methods: { ...mapActions(['toggleFileFinder']), onBeforeUnload(e = {}) { diff --git a/app/assets/javascripts/ide/components/ide_review.vue b/app/assets/javascripts/ide/components/ide_review.vue index e36d0a5a5b1..7d2f0acb08c 100644 --- a/app/assets/javascripts/ide/components/ide_review.vue +++ b/app/assets/javascripts/ide/components/ide_review.vue @@ -23,26 +23,32 @@ export default { }, }, mounted() { - if (this.activeFile && this.activeFile.pending && !this.activeFile.deleted) { - this.$router.push(this.getUrlForPath(this.activeFile.path), () => { - this.updateViewer('editor'); - }); - } else if (this.activeFile && this.activeFile.deleted) { - this.resetOpenFiles(); - } - - this.$nextTick(() => { - this.updateViewer(this.currentMergeRequestId ? viewerTypes.mr : viewerTypes.diff); - }); + this.initialize(); + }, + activated() { + this.initialize(); }, methods: { ...mapActions(['updateViewer', 'resetOpenFiles']), + initialize() { + if (this.activeFile && this.activeFile.pending && !this.activeFile.deleted) { + this.$router.push(this.getUrlForPath(this.activeFile.path), () => { + this.updateViewer(viewerTypes.edit); + }); + } else if (this.activeFile && this.activeFile.deleted) { + this.resetOpenFiles(); + } + + this.$nextTick(() => { + this.updateViewer(this.currentMergeRequestId ? viewerTypes.mr : viewerTypes.diff); + }); + }, }, }; </script> <template> - <ide-tree-list :viewer-type="viewer" header-class="ide-review-header"> + <ide-tree-list header-class="ide-review-header"> <template #header> <div class="ide-review-button-holder"> {{ __('Review') }} diff --git a/app/assets/javascripts/ide/components/ide_side_bar.vue b/app/assets/javascripts/ide/components/ide_side_bar.vue index ed68ca5cae9..53dfc133fc8 100644 --- a/app/assets/javascripts/ide/components/ide_side_bar.vue +++ b/app/assets/javascripts/ide/components/ide_side_bar.vue @@ -7,9 +7,8 @@ import ActivityBar from './activity_bar.vue'; import RepoCommitSection from './repo_commit_section.vue'; import CommitForm from './commit_sidebar/form.vue'; import IdeReview from './ide_review.vue'; -import SuccessMessage from './commit_sidebar/success_message.vue'; import IdeProjectHeader from './ide_project_header.vue'; -import { leftSidebarViews, SIDEBAR_INIT_WIDTH } from '../constants'; +import { SIDEBAR_INIT_WIDTH } from '../constants'; export default { components: { @@ -20,18 +19,11 @@ export default { IdeTree, CommitForm, IdeReview, - SuccessMessage, IdeProjectHeader, }, computed: { ...mapState(['loading', 'currentActivityView', 'changedFiles', 'stagedFiles', 'lastCommitMsg']), ...mapGetters(['currentProject', 'someUncommittedChanges']), - showSuccessMessage() { - return ( - this.currentActivityView === leftSidebarViews.edit.name && - (this.lastCommitMsg && !this.someUncommittedChanges) - ); - }, }, SIDEBAR_INIT_WIDTH, }; @@ -44,7 +36,7 @@ export default { class="multi-file-commit-panel flex-column" > <template v-if="loading"> - <div class="multi-file-commit-panel-inner"> + <div class="multi-file-commit-panel-inner" data-testid="ide-side-bar-inner"> <div v-for="n in 3" :key="n" class="multi-file-loading-container"> <gl-skeleton-loading /> </div> @@ -54,9 +46,11 @@ export default { <ide-project-header :project="currentProject" /> <div class="ide-context-body d-flex flex-fill"> <activity-bar /> - <div class="multi-file-commit-panel-inner"> + <div class="multi-file-commit-panel-inner" data-testid="ide-side-bar-inner"> <div class="multi-file-commit-panel-inner-content"> - <component :is="currentActivityView" /> + <keep-alive> + <component :is="currentActivityView" /> + </keep-alive> </div> <commit-form /> </div> diff --git a/app/assets/javascripts/ide/components/ide_status_bar.vue b/app/assets/javascripts/ide/components/ide_status_bar.vue index 146e818d654..ee292190e06 100644 --- a/app/assets/javascripts/ide/components/ide_status_bar.vue +++ b/app/assets/javascripts/ide/components/ide_status_bar.vue @@ -1,10 +1,9 @@ <script> /* eslint-disable @gitlab/vue-require-i18n-strings */ import { mapActions, mapState, mapGetters } from 'vuex'; -import { GlIcon } from '@gitlab/ui'; +import { GlIcon, GlTooltipDirective } from '@gitlab/ui'; import IdeStatusList from './ide_status_list.vue'; import IdeStatusMr from './ide_status_mr.vue'; -import tooltip from '~/vue_shared/directives/tooltip'; import timeAgoMixin from '~/vue_shared/mixins/timeago'; import CiIcon from '../../vue_shared/components/ci_icon.vue'; import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue'; @@ -19,7 +18,7 @@ export default { IdeStatusMr, }, directives: { - tooltip, + GlTooltip: GlTooltipDirective, }, mixins: [timeAgoMixin], data() { @@ -85,7 +84,7 @@ export default { @click="openRightPane($options.rightSidebarViews.pipelines)" > <ci-icon - v-tooltip + v-gl-tooltip :status="latestPipeline.details.status" :title="latestPipeline.details.status.text" /> @@ -99,7 +98,7 @@ export default { <gl-icon name="commit" /> <a - v-tooltip + v-gl-tooltip :title="lastCommit.message" :href="getCommitPath(lastCommit.short_id)" class="commit-sha" @@ -116,7 +115,7 @@ export default { /> {{ lastCommit.author_name }} <time - v-tooltip + v-gl-tooltip :datetime="lastCommit.committed_date" :title="tooltipTitle(lastCommit.committed_date)" data-placement="top" diff --git a/app/assets/javascripts/ide/components/ide_tree.vue b/app/assets/javascripts/ide/components/ide_tree.vue index 747d5044790..51d783df0ad 100644 --- a/app/assets/javascripts/ide/components/ide_tree.vue +++ b/app/assets/javascripts/ide/components/ide_tree.vue @@ -1,6 +1,6 @@ <script> import { mapState, mapGetters, mapActions } from 'vuex'; -import { modalTypes } from '../constants'; +import { modalTypes, viewerTypes } from '../constants'; import IdeTreeList from './ide_tree_list.vue'; import Upload from './new_dropdown/upload.vue'; import NewEntryButton from './new_dropdown/button.vue'; @@ -18,15 +18,10 @@ export default { ...mapGetters(['currentProject', 'currentTree', 'activeFile', 'getUrlForPath']), }, mounted() { - if (!this.activeFile) return; - - if (this.activeFile.pending && !this.activeFile.deleted) { - this.$router.push(this.getUrlForPath(this.activeFile.path), () => { - this.updateViewer('editor'); - }); - } else if (this.activeFile.deleted) { - this.resetOpenFiles(); - } + this.initialize(); + }, + activated() { + this.initialize(); }, methods: { ...mapActions(['updateViewer', 'createTempEntry', 'resetOpenFiles']), @@ -36,12 +31,27 @@ export default { createNewFolder() { this.$refs.newModal.open(modalTypes.tree); }, + initialize() { + this.$nextTick(() => { + this.updateViewer(viewerTypes.edit); + }); + + if (!this.activeFile) return; + + if (this.activeFile.pending && !this.activeFile.deleted) { + this.$router.push(this.getUrlForPath(this.activeFile.path), () => { + this.updateViewer(viewerTypes.edit); + }); + } else if (this.activeFile.deleted) { + this.resetOpenFiles(); + } + }, }, }; </script> <template> - <ide-tree-list viewer-type="editor"> + <ide-tree-list> <template #header> {{ __('Edit') }} <div class="ide-tree-actions ml-auto d-flex"> diff --git a/app/assets/javascripts/ide/components/ide_tree_list.vue b/app/assets/javascripts/ide/components/ide_tree_list.vue index 776d8459515..dd226f07fb0 100644 --- a/app/assets/javascripts/ide/components/ide_tree_list.vue +++ b/app/assets/javascripts/ide/components/ide_tree_list.vue @@ -2,6 +2,13 @@ import { mapActions, mapGetters, mapState } from 'vuex'; import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui'; import FileTree from '~/vue_shared/components/file_tree.vue'; +import { + WEBIDE_MARK_TREE_START, + WEBIDE_MEASURE_TREE_FROM_REQUEST, + WEBIDE_MARK_FILE_CLICKED, +} from '~/performance_constants'; +import { performanceMarkAndMeasure } from '~/performance_utils'; +import eventHub from '../eventhub'; import IdeFileRow from './ide_file_row.vue'; import NavDropdown from './nav_dropdown.vue'; @@ -12,10 +19,6 @@ export default { FileTree, }, props: { - viewerType: { - type: String, - required: true, - }, headerClass: { type: String, required: false, @@ -29,11 +32,19 @@ export default { return !this.currentTree || this.currentTree.loading; }, }, - mounted() { - this.updateViewer(this.viewerType); + beforeCreate() { + performanceMarkAndMeasure({ mark: WEBIDE_MARK_TREE_START }); + }, + updated() { + if (this.currentTree?.tree?.length) { + eventHub.$emit(WEBIDE_MEASURE_TREE_FROM_REQUEST); + } }, methods: { - ...mapActions(['updateViewer', 'toggleTreeOpen']), + ...mapActions(['toggleTreeOpen']), + clickedFile() { + performanceMarkAndMeasure({ mark: WEBIDE_MARK_FILE_CLICKED }); + }, }, IdeFileRow, }; @@ -51,7 +62,7 @@ export default { <nav-dropdown /> <slot name="header"></slot> </header> - <div class="ide-tree-body h-100"> + <div class="ide-tree-body h-100" data-testid="ide-tree-body"> <template v-if="currentTree.tree.length"> <file-tree v-for="file in currentTree.tree" @@ -60,6 +71,7 @@ export default { :level="0" :file-row-component="$options.IdeFileRow" @toggleTreeOpen="toggleTreeOpen" + @clickFile="clickedFile" /> </template> <div v-else class="file-row">{{ __('No files') }}</div> diff --git a/app/assets/javascripts/ide/components/jobs/detail.vue b/app/assets/javascripts/ide/components/jobs/detail.vue index 11033a5cc88..a5ae8bbfe9a 100644 --- a/app/assets/javascripts/ide/components/jobs/detail.vue +++ b/app/assets/javascripts/ide/components/jobs/detail.vue @@ -2,9 +2,8 @@ /* eslint-disable vue/no-v-html */ import { mapActions, mapState } from 'vuex'; import { throttle } from 'lodash'; -import { GlIcon } from '@gitlab/ui'; +import { GlIcon, GlTooltipDirective } from '@gitlab/ui'; import { __ } from '../../../locale'; -import tooltip from '../../../vue_shared/directives/tooltip'; import ScrollButton from './detail/scroll_button.vue'; import JobDescription from './detail/description.vue'; @@ -15,7 +14,7 @@ const scrollPositions = { export default { directives: { - tooltip, + GlTooltip: GlTooltipDirective, }, components: { GlIcon, @@ -84,7 +83,7 @@ export default { <job-description :job="detailJob" /> <div class="controllers ml-auto"> <a - v-tooltip + v-gl-tooltip :title="__('Show complete raw log')" :href="detailJob.rawPath" data-placement="top" @@ -92,7 +91,7 @@ export default { class="controllers-buttons" target="_blank" > - <i aria-hidden="true" class="fa fa-file-text-o"></i> + <gl-icon name="doc-text" aria-hidden="true" /> </a> <scroll-button :disabled="isScrolledToTop" direction="up" @click="scrollUp" /> <scroll-button :disabled="isScrolledToBottom" direction="down" @click="scrollDown" /> diff --git a/app/assets/javascripts/ide/components/jobs/detail/scroll_button.vue b/app/assets/javascripts/ide/components/jobs/detail/scroll_button.vue index 2c679a3edc7..f4859b9f312 100644 --- a/app/assets/javascripts/ide/components/jobs/detail/scroll_button.vue +++ b/app/assets/javascripts/ide/components/jobs/detail/scroll_button.vue @@ -1,7 +1,6 @@ <script> -import { GlIcon } from '@gitlab/ui'; +import { GlIcon, GlTooltipDirective } from '@gitlab/ui'; import { __ } from '../../../../locale'; -import tooltip from '../../../../vue_shared/directives/tooltip'; const directions = { up: 'up', @@ -10,7 +9,7 @@ const directions = { export default { directives: { - tooltip, + GlTooltip: GlTooltipDirective, }, components: { GlIcon, @@ -46,7 +45,7 @@ export default { <template> <div - v-tooltip + v-gl-tooltip :title="tooltipTitle" class="controllers-buttons" data-container="body" diff --git a/app/assets/javascripts/ide/components/jobs/stage.vue b/app/assets/javascripts/ide/components/jobs/stage.vue index 0b643947139..6c7f084c164 100644 --- a/app/assets/javascripts/ide/components/jobs/stage.vue +++ b/app/assets/javascripts/ide/components/jobs/stage.vue @@ -1,12 +1,11 @@ <script> -import { GlLoadingIcon, GlIcon } from '@gitlab/ui'; -import tooltip from '../../../vue_shared/directives/tooltip'; +import { GlLoadingIcon, GlIcon, GlTooltipDirective } from '@gitlab/ui'; import CiIcon from '../../../vue_shared/components/ci_icon.vue'; import Item from './item.vue'; export default { directives: { - tooltip, + GlTooltip: GlTooltipDirective, }, components: { GlIcon, @@ -67,7 +66,7 @@ export default { <ci-icon :status="stage.status" :size="24" /> <strong ref="stageTitle" - v-tooltip="showTooltip" + v-gl-tooltip="showTooltip" :title="showTooltip ? stage.name : null" data-container="body" class="gl-ml-3 text-truncate" diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue index 528475849de..5ad836f346a 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue @@ -152,6 +152,7 @@ export default { v-model.trim="entryName" type="text" class="form-control" + data-testid="file-name-field" data-qa-selector="file_name_field" :placeholder="placeholder" /> diff --git a/app/assets/javascripts/ide/components/new_dropdown/upload.vue b/app/assets/javascripts/ide/components/new_dropdown/upload.vue index 84ff05c9750..4a9a2a57acd 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/upload.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/upload.vue @@ -35,7 +35,7 @@ export default { name: `${this.path ? `${this.path}/` : ''}${name}`, type: 'blob', content, - rawPath: !isText ? target.result : '', + rawPath: !isText ? URL.createObjectURL(file) : '', }); if (isText) { @@ -44,7 +44,7 @@ export default { reader.addEventListener('load', e => emitCreateEvent(e.target.result), { once: true }); reader.readAsText(file); } else { - emitCreateEvent(encodedContent); + emitCreateEvent(rawContent); } }, readFile(file) { diff --git a/app/assets/javascripts/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue index 5eed57bb6c5..92b99b5c731 100644 --- a/app/assets/javascripts/ide/components/repo_commit_section.vue +++ b/app/assets/javascripts/ide/components/repo_commit_section.vue @@ -26,28 +26,34 @@ export default { }, }, mounted() { - const file = - this.lastOpenedFile && this.lastOpenedFile.type !== 'tree' - ? this.lastOpenedFile - : this.activeFile; - - if (!file) return; - - this.openPendingTab({ - file, - keyPrefix: file.staged ? stageKeys.staged : stageKeys.unstaged, - }) - .then(changeViewer => { - if (changeViewer) { - this.updateViewer('diff'); - } - }) - .catch(e => { - throw e; - }); + this.initialize(); + }, + activated() { + this.initialize(); }, methods: { ...mapActions(['openPendingTab', 'updateViewer', 'updateActivityBarView']), + initialize() { + const file = + this.lastOpenedFile && this.lastOpenedFile.type !== 'tree' + ? this.lastOpenedFile + : this.activeFile; + + if (!file) return; + + this.openPendingTab({ + file, + keyPrefix: file.staged ? stageKeys.staged : stageKeys.unstaged, + }) + .then(changeViewer => { + if (changeViewer) { + this.updateViewer('diff'); + } + }) + .catch(e => { + throw e; + }); + }, }, stageKeys, }; diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue index f342ce1739c..56bbb6349cd 100644 --- a/app/assets/javascripts/ide/components/repo_editor.vue +++ b/app/assets/javascripts/ide/components/repo_editor.vue @@ -5,6 +5,14 @@ import { deprecatedCreateFlash as flash } from '~/flash'; import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue'; import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue'; import { + WEBIDE_MARK_FILE_CLICKED, + WEBIDE_MARK_FILE_START, + WEBIDE_MEASURE_FILE_AFTER_INTERACTION, + WEBIDE_MEASURE_FILE_FROM_REQUEST, +} from '~/performance_constants'; +import { performanceMarkAndMeasure } from '~/performance_utils'; +import eventHub from '../eventhub'; +import { leftSidebarViews, viewerTypes, FILE_VIEW_MODE_EDITOR, @@ -60,7 +68,7 @@ export default { ]), ...mapGetters('fileTemplates', ['showFileTemplatesBar']), shouldHideEditor() { - return this.file && !isTextFile(this.file); + return this.file && !this.file.loading && !isTextFile(this.file); }, showContentViewer() { return ( @@ -164,6 +172,9 @@ export default { } }, }, + beforeCreate() { + performanceMarkAndMeasure({ mark: WEBIDE_MARK_FILE_START }); + }, beforeDestroy() { this.editor.dispose(); }, @@ -224,6 +235,7 @@ export default { return this.getFileData({ path: this.file.path, makeFileActive: false, + toggleLoading: false, }).then(() => this.getRawFileData({ path: this.file.path, @@ -289,6 +301,11 @@ export default { }); this.$emit('editorSetup'); + if (performance.getEntriesByName(WEBIDE_MARK_FILE_CLICKED).length) { + eventHub.$emit(WEBIDE_MEASURE_FILE_AFTER_INTERACTION); + } else { + eventHub.$emit(WEBIDE_MEASURE_FILE_FROM_REQUEST); + } }, refreshEditorDimensions() { if (this.showEditor) { diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js index 59b1969face..bdb11e6b004 100644 --- a/app/assets/javascripts/ide/constants.js +++ b/app/assets/javascripts/ide/constants.js @@ -47,9 +47,9 @@ export const diffViewerErrors = Object.freeze({ }); export const leftSidebarViews = { - edit: { name: 'ide-tree', keepAlive: false }, - review: { name: 'ide-review', keepAlive: false }, - commit: { name: 'repo-commit-section', keepAlive: false }, + edit: { name: 'ide-tree' }, + review: { name: 'ide-review' }, + commit: { name: 'repo-commit-section' }, }; export const rightSidebarViews = { diff --git a/app/assets/javascripts/ide/index.js b/app/assets/javascripts/ide/index.js index 7c767009de5..56d48e87c18 100644 --- a/app/assets/javascripts/ide/index.js +++ b/app/assets/javascripts/ide/index.js @@ -73,11 +73,9 @@ export function initIde(el, options = {}) { * @param {Objects} options - Extra options for the IDE (Used by EE). */ export function startIde(options) { - document.addEventListener('DOMContentLoaded', () => { - const ideElement = document.getElementById('ide'); - if (ideElement) { - resetServiceWorkersPublicPath(); - initIde(ideElement, options); - } - }); + const ideElement = document.getElementById('ide'); + if (ideElement) { + resetServiceWorkersPublicPath(); + initIde(ideElement, options); + } } diff --git a/app/assets/javascripts/ide/lib/editor.js b/app/assets/javascripts/ide/lib/editor.js index 2b12230c7cd..493dedcd89a 100644 --- a/app/assets/javascripts/ide/lib/editor.js +++ b/app/assets/javascripts/ide/lib/editor.js @@ -157,8 +157,10 @@ export default class Editor { } updateDimensions() { - this.instance.layout(); - this.updateDiffView(); + if (this.instance) { + this.instance.layout(); + this.updateDiffView(); + } } setPosition({ lineNumber, column }) { diff --git a/app/assets/javascripts/ide/lib/errors.js b/app/assets/javascripts/ide/lib/errors.js index 6ae18bc8180..e62d9d1e77f 100644 --- a/app/assets/javascripts/ide/lib/errors.js +++ b/app/assets/javascripts/ide/lib/errors.js @@ -1,25 +1,49 @@ import { escape } from 'lodash'; import { __ } from '~/locale'; +import consts from '../stores/modules/commit/constants'; const CODEOWNERS_REGEX = /Push.*protected branches.*CODEOWNERS/; const BRANCH_CHANGED_REGEX = /changed.*since.*start.*edit/; +const BRANCH_ALREADY_EXISTS = /branch.*already.*exists/; -export const createUnexpectedCommitError = () => ({ +const createNewBranchAndCommit = store => + store + .dispatch('commit/updateCommitAction', consts.COMMIT_TO_NEW_BRANCH) + .then(() => store.dispatch('commit/commitChanges')); + +export const createUnexpectedCommitError = message => ({ title: __('Unexpected error'), - messageHTML: __('Could not commit. An unexpected error occurred.'), - canCreateBranch: false, + messageHTML: escape(message) || __('Could not commit. An unexpected error occurred.'), }); export const createCodeownersCommitError = message => ({ title: __('CODEOWNERS rule violation'), messageHTML: escape(message), - canCreateBranch: true, + primaryAction: { + text: __('Create new branch'), + callback: createNewBranchAndCommit, + }, }); export const createBranchChangedCommitError = message => ({ title: __('Branch changed'), messageHTML: `${escape(message)}<br/><br/>${__('Would you like to create a new branch?')}`, - canCreateBranch: true, + primaryAction: { + text: __('Create new branch'), + callback: createNewBranchAndCommit, + }, +}); + +export const branchAlreadyExistsCommitError = message => ({ + title: __('Branch already exists'), + messageHTML: `${escape(message)}<br/><br/>${__( + 'Would you like to try auto-generating a branch name?', + )}`, + primaryAction: { + text: __('Create new branch'), + callback: store => + store.dispatch('commit/addSuffixToBranchName').then(() => createNewBranchAndCommit(store)), + }, }); export const parseCommitError = e => { @@ -33,7 +57,9 @@ export const parseCommitError = e => { return createCodeownersCommitError(message); } else if (BRANCH_CHANGED_REGEX.test(message)) { return createBranchChangedCommitError(message); + } else if (BRANCH_ALREADY_EXISTS.test(message)) { + return branchAlreadyExistsCommitError(message); } - return createUnexpectedCommitError(); + return createUnexpectedCommitError(message); }; diff --git a/app/assets/javascripts/ide/lib/languages/README.md b/app/assets/javascripts/ide/lib/languages/README.md index e4d1a4c7818..c4f3de00783 100644 --- a/app/assets/javascripts/ide/lib/languages/README.md +++ b/app/assets/javascripts/ide/lib/languages/README.md @@ -1,7 +1,7 @@ # Web IDE Languages The Web IDE uses the [Monaco editor](https://microsoft.github.io/monaco-editor/) which uses the [Monarch library](https://microsoft.github.io/monaco-editor/monarch.html) for syntax highlighting. -The Web IDE currently supports all langauges defined in the [monaco-languages](https://github.com/microsoft/monaco-languages/tree/master/src) repository. +The Web IDE currently supports all languages defined in the [monaco-languages](https://github.com/microsoft/monaco-languages/tree/master/src) repository. ## Adding New Languages @@ -14,7 +14,7 @@ Should you be willing to help us and add support to GitLab for any missing langu 2. Create a new file in this folder called `{languageName}.js`, where `{languageName}` is the name of the language you want to add support for. 3. Follow the [Monarch documentation](https://microsoft.github.io/monaco-editor/monarch.html) to add a configuration for the new language. - Example: The [`vue.js`](./vue.js) file in the current directory adds support for Vue.js Syntax Highlighting. -4. Add tests for the new langauge implementation in `spec/frontend/ide/lib/languages/{langaugeName}.js`. +4. Add tests for the new language implementation in `spec/frontend/ide/lib/languages/{langaugeName}.js`. - Example: See [`vue_spec.js`](spec/frontend/ide/lib/languages/vue_spec.js). 5. Create a [Merge Request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html) with your newly added language. diff --git a/app/assets/javascripts/ide/lib/languages/hcl.js b/app/assets/javascripts/ide/lib/languages/hcl.js new file mode 100644 index 00000000000..4539719b1f2 --- /dev/null +++ b/app/assets/javascripts/ide/lib/languages/hcl.js @@ -0,0 +1,192 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See https://github.com/microsoft/monaco-languages/blob/master/LICENSE.md + *--------------------------------------------------------------------------------------------*/ + +/* eslint-disable no-useless-escape */ +/* eslint-disable @gitlab/require-i18n-strings */ + +const conf = { + comments: { + lineComment: '//', + blockComment: ['/*', '*/'], + }, + brackets: [['{', '}'], ['[', ']'], ['(', ')']], + autoClosingPairs: [ + { open: '{', close: '}' }, + { open: '[', close: ']' }, + { open: '(', close: ')' }, + { open: '"', close: '"', notIn: ['string'] }, + ], + surroundingPairs: [ + { open: '{', close: '}' }, + { open: '[', close: ']' }, + { open: '(', close: ')' }, + { open: '"', close: '"' }, + ], +}; + +const language = { + defaultToken: '', + tokenPostfix: '.hcl', + + keywords: [ + 'var', + 'local', + 'path', + 'for_each', + 'any', + 'string', + 'number', + 'bool', + 'true', + 'false', + 'null', + 'if ', + 'else ', + 'endif ', + 'for ', + 'in', + 'endfor', + ], + + operators: [ + '=', + '>=', + '<=', + '==', + '!=', + '+', + '-', + '*', + '/', + '%', + '&&', + '||', + '!', + '<', + '>', + '?', + '...', + ':', + ], + + symbols: /[=><!~?:&|+\-*\/\^%]+/, + escapes: /\\(?:[abfnrtv\\"']|x[0-9A-Fa-f]{1,4}|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})/, + terraformFunctions: /(abs|ceil|floor|log|max|min|pow|signum|chomp|format|formatlist|indent|join|lower|regex|regexall|replace|split|strrev|substr|title|trimspace|upper|chunklist|coalesce|coalescelist|compact|concat|contains|distinct|element|flatten|index|keys|length|list|lookup|map|matchkeys|merge|range|reverse|setintersection|setproduct|setunion|slice|sort|transpose|values|zipmap|base64decode|base64encode|base64gzip|csvdecode|jsondecode|jsonencode|urlencode|yamldecode|yamlencode|abspath|dirname|pathexpand|basename|file|fileexists|fileset|filebase64|templatefile|formatdate|timeadd|timestamp|base64sha256|base64sha512|bcrypt|filebase64sha256|filebase64sha512|filemd5|filemd1|filesha256|filesha512|md5|rsadecrypt|sha1|sha256|sha512|uuid|uuidv5|cidrhost|cidrnetmask|cidrsubnet|tobool|tolist|tomap|tonumber|toset|tostring)/, + terraformMainBlocks: /(module|data|terraform|resource|provider|variable|output|locals)/, + tokenizer: { + root: [ + // highlight main blocks + [ + /^@terraformMainBlocks([ \t]*)([\w-]+|"[\w-]+"|)([ \t]*)([\w-]+|"[\w-]+"|)([ \t]*)(\{)/, + ['type', '', 'string', '', 'string', '', '@brackets'], + ], + // highlight all the remaining blocks + [ + /(\w+[ \t]+)([ \t]*)([\w-]+|"[\w-]+"|)([ \t]*)([\w-]+|"[\w-]+"|)([ \t]*)(\{)/, + ['identifier', '', 'string', '', 'string', '', '@brackets'], + ], + // highlight block + [ + /(\w+[ \t]+)([ \t]*)([\w-]+|"[\w-]+"|)([ \t]*)([\w-]+|"[\w-]+"|)(=)(\{)/, + ['identifier', '', 'string', '', 'operator', '', '@brackets'], + ], + // terraform general highlight - shared with expressions + { include: '@terraform' }, + ], + terraform: [ + // highlight terraform functions + [/@terraformFunctions(\()/, ['type', '@brackets']], + // all other words are variables or keywords + [ + /[a-zA-Z_]\w*-*/, // must work with variables such as foo-bar and also with negative numbers + { + cases: { + '@keywords': { token: 'keyword.$0' }, + '@default': 'variable', + }, + }, + ], + { include: '@whitespace' }, + { include: '@heredoc' }, + // delimiters and operators + [/[{}()\[\]]/, '@brackets'], + [/[<>](?!@symbols)/, '@brackets'], + [ + /@symbols/, + { + cases: { + '@operators': 'operator', + '@default': '', + }, + }, + ], + // numbers + [/\d*\d+[eE]([\-+]?\d+)?/, 'number.float'], + [/\d*\.\d+([eE][\-+]?\d+)?/, 'number.float'], + [/\d[\d']*/, 'number'], + [/\d/, 'number'], + [/[;,.]/, 'delimiter'], // delimiter: after number because of .\d floats + // strings + [/"/, 'string', '@string'], // this will include expressions + [/'/, 'invalid'], + ], + heredoc: [ + [ + /<<[-]*\s*["]?([\w\-]+)["]?/, + { token: 'string.heredoc.delimiter', next: '@heredocBody.$1' }, + ], + ], + heredocBody: [ + [ + /^([\w\-]+)$/, + { + cases: { + '$1==$S2': [ + { + token: 'string.heredoc.delimiter', + next: '@popall', + }, + ], + '@default': 'string.heredoc', + }, + }, + ], + [/./, 'string.heredoc'], + ], + whitespace: [ + [/[ \t\r\n]+/, ''], + [/\/\*/, 'comment', '@comment'], + [/\/\/.*$/, 'comment'], + [/#.*$/, 'comment'], + ], + comment: [[/[^\/*]+/, 'comment'], [/\*\//, 'comment', '@pop'], [/[\/*]/, 'comment']], + string: [ + [/\$\{/, { token: 'delimiter', next: '@stringExpression' }], + [/[^\\"\$]+/, 'string'], + [/@escapes/, 'string.escape'], + [/\\./, 'string.escape.invalid'], + [/"/, 'string', '@popall'], + ], + stringInsideExpression: [ + [/[^\\"]+/, 'string'], + [/@escapes/, 'string.escape'], + [/\\./, 'string.escape.invalid'], + [/"/, 'string', '@pop'], + ], + stringExpression: [ + [/\}/, { token: 'delimiter', next: '@pop' }], + [/"/, 'string', '@stringInsideExpression'], + { include: '@terraform' }, + ], + }, +}; + +export default { + id: 'hcl', + extensions: ['.tf', '.tfvars', '.hcl'], + aliases: ['Terraform', 'tf', 'HCL', 'hcl'], + conf, + language, +}; diff --git a/app/assets/javascripts/ide/lib/languages/index.js b/app/assets/javascripts/ide/lib/languages/index.js index 0c85a1104fc..580ad820bf9 100644 --- a/app/assets/javascripts/ide/lib/languages/index.js +++ b/app/assets/javascripts/ide/lib/languages/index.js @@ -1,5 +1,6 @@ import vue from './vue'; +import hcl from './hcl'; -const languages = [vue]; +const languages = [vue, hcl]; export default languages; diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js index 3515d1fc933..a0df85540f9 100644 --- a/app/assets/javascripts/ide/stores/actions/file.js +++ b/app/assets/javascripts/ide/stores/actions/file.js @@ -59,7 +59,7 @@ export const setFileActive = ({ commit, state, getters, dispatch }, path) => { export const getFileData = ( { state, commit, dispatch, getters }, - { path, makeFileActive = true, openFile = makeFileActive }, + { path, makeFileActive = true, openFile = makeFileActive, toggleLoading = true }, ) => { const file = state.entries[path]; const fileDeletedAndReadded = getters.isFileDeletedAndReadded(path); @@ -99,7 +99,7 @@ export const getFileData = ( }); }) .finally(() => { - commit(types.TOGGLE_LOADING, { entry: file, forceValue: false }); + if (toggleLoading) commit(types.TOGGLE_LOADING, { entry: file, forceValue: false }); }); }; diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js index b8304a9b68d..500ce9f32d5 100644 --- a/app/assets/javascripts/ide/stores/getters.js +++ b/app/assets/javascripts/ide/stores/getters.js @@ -6,6 +6,7 @@ import { PERMISSION_CREATE_MR, PERMISSION_PUSH_CODE, } from '../constants'; +import { addNumericSuffix } from '~/ide/utils'; import Api from '~/api'; export const activeFile = state => state.openFiles.find(file => file.active) || null; @@ -167,10 +168,7 @@ export const getAvailableFileName = (state, getters) => path => { let newPath = path; while (getters.entryExists(newPath)) { - newPath = newPath.replace( - /([ _-]?)(\d*)(\..+?$|$)/, - (_, before, number, after) => `${before || '_'}${Number(number) + 1}${after}`, - ); + newPath = addNumericSuffix(newPath); } return newPath; diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js index 90a6c644d17..e0d2028d2e1 100644 --- a/app/assets/javascripts/ide/stores/modules/commit/actions.js +++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js @@ -8,6 +8,7 @@ import consts from './constants'; import { leftSidebarViews } from '../../../constants'; import eventHub from '../../../eventhub'; import { parseCommitError } from '../../../lib/errors'; +import { addNumericSuffix } from '~/ide/utils'; export const updateCommitMessage = ({ commit }, message) => { commit(types.UPDATE_COMMIT_MESSAGE, message); @@ -17,11 +18,8 @@ export const discardDraft = ({ commit }) => { commit(types.UPDATE_COMMIT_MESSAGE, ''); }; -export const updateCommitAction = ({ commit, getters }, commitAction) => { - commit(types.UPDATE_COMMIT_ACTION, { - commitAction, - }); - commit(types.TOGGLE_SHOULD_CREATE_MR, !getters.shouldHideNewMrOption); +export const updateCommitAction = ({ commit }, commitAction) => { + commit(types.UPDATE_COMMIT_ACTION, { commitAction }); }; export const toggleShouldCreateMR = ({ commit }) => { @@ -32,6 +30,12 @@ export const updateBranchName = ({ commit }, branchName) => { commit(types.UPDATE_NEW_BRANCH_NAME, branchName); }; +export const addSuffixToBranchName = ({ commit, state }) => { + const newBranchName = addNumericSuffix(state.newBranchName, true); + + commit(types.UPDATE_NEW_BRANCH_NAME, newBranchName); +}; + export const setLastCommitMessage = ({ commit, rootGetters }, data) => { const { currentProject } = rootGetters; const commitStats = data.stats @@ -107,7 +111,7 @@ export const updateFilesAfterCommit = ({ commit, dispatch, rootState, rootGetter export const commitChanges = ({ commit, state, getters, dispatch, rootState, rootGetters }) => { // Pull commit options out because they could change // During some of the pre and post commit processing - const { shouldCreateMR, isCreatingNewBranch, branchName } = getters; + const { shouldCreateMR, shouldHideNewMrOption, isCreatingNewBranch, branchName } = getters; const newBranch = state.commitAction !== consts.COMMIT_TO_CURRENT_BRANCH; const stageFilesPromise = rootState.stagedFiles.length ? Promise.resolve() @@ -167,7 +171,7 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo commit(rootTypes.SET_LAST_COMMIT_MSG, '', { root: true }); }, 5000); - if (shouldCreateMR) { + if (shouldCreateMR && !shouldHideNewMrOption) { const { currentProject } = rootGetters; const targetBranch = isCreatingNewBranch ? rootState.currentBranchId diff --git a/app/assets/javascripts/ide/stores/modules/commit/mutations.js b/app/assets/javascripts/ide/stores/modules/commit/mutations.js index 2cf6e8e6f36..c4bfad6405e 100644 --- a/app/assets/javascripts/ide/stores/modules/commit/mutations.js +++ b/app/assets/javascripts/ide/stores/modules/commit/mutations.js @@ -10,9 +10,7 @@ export default { Object.assign(state, { commitAction }); }, [types.UPDATE_NEW_BRANCH_NAME](state, newBranchName) { - Object.assign(state, { - newBranchName, - }); + Object.assign(state, { newBranchName }); }, [types.UPDATE_LOADING](state, submitCommitLoading) { Object.assign(state, { diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js index c90bc2a3320..a981f86fa40 100644 --- a/app/assets/javascripts/ide/stores/mutations/file.js +++ b/app/assets/javascripts/ide/stores/mutations/file.js @@ -19,19 +19,20 @@ export default { } }, [types.TOGGLE_FILE_OPEN](state, path) { - Object.assign(state.entries[path], { - opened: !state.entries[path].opened, - }); + const entry = state.entries[path]; - if (state.entries[path].opened) { + entry.opened = !entry.opened; + if (entry.opened && !entry.tempFile) { + entry.loading = true; + } + + if (entry.opened) { Object.assign(state, { openFiles: state.openFiles.filter(f => f.path !== path).concat(state.entries[path]), }); } else { - const file = state.entries[path]; - Object.assign(state, { - openFiles: state.openFiles.filter(f => f.key !== file.key), + openFiles: state.openFiles.filter(f => f.key !== entry.key), }); } }, diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js index d9cdc7727ad..b7ced3a271a 100644 --- a/app/assets/javascripts/ide/stores/utils.js +++ b/app/assets/javascripts/ide/stores/utils.js @@ -3,7 +3,7 @@ import { relativePathToAbsolute, isAbsolute, isRootRelative, - isBase64DataUrl, + isBlobUrl, } from '~/lib/utils/url_utility'; export const dataStructure = () => ({ @@ -110,14 +110,19 @@ export const createCommitPayload = ({ }) => ({ branch, commit_message: state.commitMessage || getters.preBuiltCommitMessage, - actions: getCommitFiles(rootState.stagedFiles).map(f => ({ - action: commitActionForFile(f), - file_path: f.path, - previous_path: f.prevPath || undefined, - content: f.prevPath && !f.changed ? null : f.content || undefined, - encoding: isBase64DataUrl(f.rawPath) ? 'base64' : 'text', - last_commit_id: newBranch || f.deleted || f.prevPath ? undefined : f.lastCommitSha, - })), + actions: getCommitFiles(rootState.stagedFiles).map(f => { + const isBlob = isBlobUrl(f.rawPath); + const content = isBlob ? btoa(f.content) : f.content; + + return { + action: commitActionForFile(f), + file_path: f.path, + previous_path: f.prevPath || undefined, + content: f.prevPath && !f.changed ? null : content || undefined, + encoding: isBlob ? 'base64' : 'text', + last_commit_id: newBranch || f.deleted || f.prevPath ? undefined : f.lastCommitSha, + }; + }), start_sha: newBranch ? rootGetters.lastCommit.id : undefined, }); diff --git a/app/assets/javascripts/ide/utils.js b/app/assets/javascripts/ide/utils.js index cde53e1ef00..4cf4f5e1d81 100644 --- a/app/assets/javascripts/ide/utils.js +++ b/app/assets/javascripts/ide/utils.js @@ -1,6 +1,7 @@ import { languages } from 'monaco-editor'; import { flatten, isString } from 'lodash'; import { SIDE_LEFT, SIDE_RIGHT } from './constants'; +import { performanceMarkAndMeasure } from '~/performance_utils'; const toLowerCase = x => x.toLowerCase(); @@ -42,16 +43,17 @@ const KNOWN_TYPES = [ }, ]; -export function isTextFile({ name, content, mimeType = '' }) { +export function isTextFile({ name, raw, content, mimeType = '' }) { const knownType = KNOWN_TYPES.find(type => type.isMatch(mimeType, name)); - if (knownType) return knownType.isText; // does the string contain ascii characters only (ranges from space to tilde, tabs and new lines) const asciiRegex = /^[ -~\t\n\r]+$/; + const fileContents = raw || content; + // for unknown types, determine the type by evaluating the file contents - return isString(content) && (content === '' || asciiRegex.test(content)); + return isString(fileContents) && (fileContents === '' || asciiRegex.test(fileContents)); } export const createPathWithExt = p => { @@ -137,3 +139,49 @@ export function readFileAsDataURL(file) { export function getFileEOL(content = '') { return content.includes('\r\n') ? 'CRLF' : 'LF'; } + +/** + * Adds or increments the numeric suffix to a filename/branch name. + * Retains underscore or dash before the numeric suffix if it already exists. + * + * Examples: + * hello -> hello-1 + * hello-2425 -> hello-2425 + * hello.md -> hello-1.md + * hello_2.md -> hello_3.md + * hello_ -> hello_1 + * master-patch-22432 -> master-patch-22433 + * patch_332 -> patch_333 + * + * @param {string} filename File name or branch name + * @param {number} [randomize] Should randomize the numeric suffix instead of auto-incrementing? + */ +export function addNumericSuffix(filename, randomize = false) { + return filename.replace(/([ _-]?)(\d*)(\..+?$|$)/, (_, before, number, after) => { + const n = randomize + ? Math.random() + .toString() + .substring(2, 7) + .slice(-5) + : Number(number) + 1; + return `${before || '-'}${n}${after}`; + }); +} + +export const measurePerformance = ( + mark, + measureName, + measureStart = undefined, + measureEnd = mark, +) => { + performanceMarkAndMeasure({ + mark, + measures: [ + { + name: measureName, + start: measureStart, + end: measureEnd, + }, + ], + }); +}; |