diff options
Diffstat (limited to 'app/assets')
55 files changed, 1011 insertions, 320 deletions
diff --git a/app/assets/javascripts/badges/components/badge.vue b/app/assets/javascripts/badges/components/badge.vue index b4bfaee1d85..155c348286c 100644 --- a/app/assets/javascripts/badges/components/badge.vue +++ b/app/assets/javascripts/badges/components/badge.vue @@ -93,7 +93,7 @@ export default { <icon :size="16" class="prepend-left-8 append-right-8" - name="doc_image" + name="doc-image" aria-hidden="true" /> </div> diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js index 200d1923635..bc263cbbfea 100644 --- a/app/assets/javascripts/boards/index.js +++ b/app/assets/javascripts/boards/index.js @@ -1,5 +1,3 @@ -/* eslint-disable quote-props, comma-dangle */ - import $ from 'jquery'; import _ from 'underscore'; import Vue from 'vue'; @@ -47,7 +45,7 @@ export default () => { gl.IssueBoardsApp = new Vue({ el: $boardApp, components: { - 'board': gl.issueBoards.Board, + board: gl.issueBoards.Board, 'board-sidebar': gl.issueBoards.BoardSidebar, BoardAddIssuesModal, }, @@ -65,11 +63,11 @@ export default () => { defaultAvatar: $boardApp.dataset.defaultAvatar, }, computed: { - detailIssueVisible () { + detailIssueVisible() { return Object.keys(this.detailIssue.issue).length; }, }, - created () { + created() { gl.boardService = new BoardService({ boardsEndpoint: this.boardsEndpoint, listsEndpoint: this.listsEndpoint, @@ -89,15 +87,16 @@ export default () => { eventHub.$off('clearDetailIssue', this.clearDetailIssue); sidebarEventHub.$off('toggleSubscription', this.toggleSubscription); }, - mounted () { + mounted() { this.filterManager = new FilteredSearchBoards(Store.filter, true, Store.cantEdit); this.filterManager.setup(); Store.disabled = this.disabled; - gl.boardService.all() + gl.boardService + .all() .then(res => res.data) - .then((data) => { - data.forEach((board) => { + .then(data => { + data.forEach(board => { const list = Store.addList(board, this.defaultAvatar); if (list.type === 'closed') { @@ -126,7 +125,7 @@ export default () => { newIssue.setFetchingState('subscriptions', true); BoardService.getIssueInfo(sidebarInfoEndpoint) .then(res => res.data) - .then((data) => { + .then(data => { newIssue.setFetchingState('subscriptions', false); newIssue.updateData({ subscribed: data.subscribed, @@ -159,7 +158,7 @@ export default () => { Flash(__('An error occurred when toggling the notification subscription')); }); } - } + }, }, }); @@ -168,77 +167,81 @@ export default () => { data: { filters: Store.state.filters, }, - mounted () { + mounted() { gl.issueBoards.newListDropdownInit(); }, }); - gl.IssueBoardsModalAddBtn = new Vue({ - el: document.getElementById('js-add-issues-btn'), - mixins: [modalMixin], - data() { - return { - modal: ModalStore.store, - store: Store.state, - canAdminList: this.$options.el.hasAttribute('data-can-admin-list'), - }; - }, - computed: { - disabled() { - if (!this.store) { - return true; - } - return !this.store.lists.filter(list => !list.preset).length; + const issueBoardsModal = document.getElementById('js-add-issues-btn'); + + if (issueBoardsModal) { + gl.IssueBoardsModalAddBtn = new Vue({ + el: issueBoardsModal, + mixins: [modalMixin], + data() { + return { + modal: ModalStore.store, + store: Store.state, + canAdminList: this.$options.el.hasAttribute('data-can-admin-list'), + }; }, - tooltipTitle() { - if (this.disabled) { - return 'Please add a list to your board first'; - } + computed: { + disabled() { + if (!this.store) { + return true; + } + return !this.store.lists.filter(list => !list.preset).length; + }, + tooltipTitle() { + if (this.disabled) { + return 'Please add a list to your board first'; + } - return ''; + return ''; + }, }, - }, - watch: { - disabled() { + watch: { + disabled() { + this.updateTooltip(); + }, + }, + mounted() { this.updateTooltip(); }, - }, - mounted() { - this.updateTooltip(); - }, - methods: { - updateTooltip() { - const $tooltip = $(this.$refs.addIssuesButton); - - this.$nextTick(() => { - if (this.disabled) { - $tooltip.tooltip(); - } else { - $tooltip.tooltip('dispose'); + methods: { + updateTooltip() { + const $tooltip = $(this.$refs.addIssuesButton); + + this.$nextTick(() => { + if (this.disabled) { + $tooltip.tooltip(); + } else { + $tooltip.tooltip('dispose'); + } + }); + }, + openModal() { + if (!this.disabled) { + this.toggleModal(true); } - }); - }, - openModal() { - if (!this.disabled) { - this.toggleModal(true); - } + }, }, - }, - template: ` - <div class="board-extra-actions"> - <button - class="btn btn-create prepend-left-10" - type="button" - data-placement="bottom" - ref="addIssuesButton" - :class="{ 'disabled': disabled }" - :title="tooltipTitle" - :aria-disabled="disabled" - v-if="canAdminList" - @click="openModal"> - Add issues - </button> - </div> - `, - }); + template: ` + <div class="board-extra-actions"> + <button + class="btn btn-create prepend-left-10" + type="button" + data-placement="bottom" + ref="addIssuesButton" + :class="{ 'disabled': disabled }" + :title="tooltipTitle" + :aria-disabled="disabled" + v-if="canAdminList" + @click="openModal"> + Add issues + </button> + </div> + `, + }); + } }; diff --git a/app/assets/javascripts/commons/gitlab_ui.js b/app/assets/javascripts/commons/gitlab_ui.js new file mode 100644 index 00000000000..923c036f5a4 --- /dev/null +++ b/app/assets/javascripts/commons/gitlab_ui.js @@ -0,0 +1,4 @@ +import Vue from 'vue'; +import progressBar from '@gitlab-org/gitlab-ui/dist/base/progress_bar'; + +Vue.component('gl-progress-bar', progressBar); diff --git a/app/assets/javascripts/commons/index.js b/app/assets/javascripts/commons/index.js index 0d2fe2925d8..ea945cd3fa5 100644 --- a/app/assets/javascripts/commons/index.js +++ b/app/assets/javascripts/commons/index.js @@ -3,4 +3,5 @@ import './polyfills'; import './jquery'; import './bootstrap'; import './vue'; +import './gitlab_ui'; import '../lib/utils/axios_utils'; diff --git a/app/assets/javascripts/ide/components/changed_file_icon.vue b/app/assets/javascripts/ide/components/changed_file_icon.vue index a4e06bbbe3c..720ae11aaa6 100644 --- a/app/assets/javascripts/ide/components/changed_file_icon.vue +++ b/app/assets/javascripts/ide/components/changed_file_icon.vue @@ -3,6 +3,7 @@ import tooltip from '~/vue_shared/directives/tooltip'; import Icon from '~/vue_shared/components/icon.vue'; import { pluralize } from '~/lib/utils/text_utility'; import { __, sprintf } from '~/locale'; +import { getCommitIconMap } from '../utils'; export default { components: { @@ -34,16 +35,14 @@ export default { }, computed: { changedIcon() { - const suffix = this.file.staged && !this.showStagedIcon ? '-solid' : ''; - return this.file.tempFile && !this.forceModifiedIcon - ? `file-addition${suffix}` - : `file-modified${suffix}`; - }, - stagedIcon() { - return `${this.changedIcon}-solid`; + const suffix = !this.file.changed && this.file.staged && !this.showStagedIcon ? '-solid' : ''; + + if (this.forceModifiedIcon) return `file-modified${suffix}`; + + return `${getCommitIconMap(this.file).icon}${suffix}`; }, changedIconClass() { - return `multi-${this.changedIcon} float-left`; + return `ide-${this.changedIcon} float-left`; }, tooltipTitle() { if (!this.showTooltip) return undefined; @@ -66,6 +65,9 @@ export default { return undefined; }, + showIcon() { + return this.file.changed || this.file.tempFile || this.file.staged || this.file.deleted; + }, }, }; </script> @@ -79,7 +81,7 @@ export default { class="ide-file-changed-icon" > <icon - v-if="file.changed || file.tempFile || file.staged" + v-if="showIcon" :name="changedIcon" :size="12" :css-classes="changedIconClass" diff --git a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue index eb7cb9745ec..a8b5c7a16d0 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue @@ -1,4 +1,5 @@ <script> +import _ from 'underscore'; import { mapActions, mapState, mapGetters } from 'vuex'; import { sprintf, __ } from '~/locale'; import * as consts from '../../stores/modules/commit/constants'; @@ -14,7 +15,7 @@ export default { commitToCurrentBranchText() { return sprintf( __('Commit to %{branchName} branch'), - { branchName: `<strong class="monospace">${this.currentBranchId}</strong>` }, + { branchName: `<strong class="monospace">${_.escape(this.currentBranchId)}</strong>` }, false, ); }, diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue index ee21eeda3cd..391004dcd3c 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue @@ -5,6 +5,7 @@ import Icon from '~/vue_shared/components/icon.vue'; import StageButton from './stage_button.vue'; import UnstageButton from './unstage_button.vue'; import { viewerTypes } from '../../constants'; +import { getCommitIconMap } from '../../utils'; export default { components: { @@ -42,11 +43,12 @@ export default { }, computed: { iconName() { - const prefix = this.stagedList ? '-solid' : ''; - return this.file.tempFile ? `file-addition${prefix}` : `file-modified${prefix}`; + const suffix = this.stagedList ? '-solid' : ''; + + return `${getCommitIconMap(this.file).icon}${suffix}`; }, iconClass() { - return `multi-file-${this.file.tempFile ? 'addition' : 'modified'} append-right-8`; + return `${getCommitIconMap(this.file).class} append-right-8`; }, fullKey() { return `${this.keyPrefix}-${this.file.key}`; @@ -67,6 +69,8 @@ export default { 'stageChange', ]), openFileInEditor() { + if (this.file.type === 'tree') return null; + return this.openPendingTab({ file: this.file, keyPrefix: this.keyPrefix, diff --git a/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue b/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue index 7014b9f605e..e6044401c9f 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue @@ -56,7 +56,7 @@ export default { > <icon :size="12" - name="more" + name="ellipsis_h" /> </button> <div class="dropdown-menu dropdown-menu-right"> diff --git a/app/assets/javascripts/ide/components/ide_review.vue b/app/assets/javascripts/ide/components/ide_review.vue index f9978762c45..d09c99050fe 100644 --- a/app/assets/javascripts/ide/components/ide_review.vue +++ b/app/assets/javascripts/ide/components/ide_review.vue @@ -10,7 +10,7 @@ export default { EditorModeDropdown, }, computed: { - ...mapGetters(['currentMergeRequest']), + ...mapGetters(['currentMergeRequest', 'activeFile']), ...mapState(['viewer', 'currentMergeRequestId']), showLatestChangesText() { return !this.currentMergeRequestId || this.viewer === viewerTypes.diff; @@ -23,12 +23,20 @@ export default { }, }, mounted() { + if (this.activeFile && this.activeFile.pending && !this.activeFile.deleted) { + this.$router.push(`/project${this.activeFile.url}`, () => { + this.updateViewer('editor'); + }); + } else if (this.activeFile && this.activeFile.deleted) { + this.resetOpenFiles(); + } + this.$nextTick(() => { this.updateViewer(this.currentMergeRequestId ? viewerTypes.mr : viewerTypes.diff); }); }, methods: { - ...mapActions(['updateViewer']), + ...mapActions(['updateViewer', 'resetOpenFiles']), }, }; </script> @@ -36,7 +44,6 @@ export default { <template> <ide-tree-list :viewer-type="viewer" - :disable-action-dropdown="true" header-class="ide-review-header" > <template diff --git a/app/assets/javascripts/ide/components/ide_tree.vue b/app/assets/javascripts/ide/components/ide_tree.vue index 0a95c0bb30d..e996dd9059e 100644 --- a/app/assets/javascripts/ide/components/ide_tree.vue +++ b/app/assets/javascripts/ide/components/ide_tree.vue @@ -17,14 +17,18 @@ export default { ...mapGetters(['currentProject', 'currentTree', 'activeFile']), }, mounted() { - if (this.activeFile && this.activeFile.pending) { + if (!this.activeFile) return; + + if (this.activeFile.pending && !this.activeFile.deleted) { this.$router.push(`/project${this.activeFile.url}`, () => { this.updateViewer('editor'); }); + } else if (this.activeFile.deleted) { + this.resetOpenFiles(); } }, methods: { - ...mapActions(['updateViewer', 'openNewEntryModal', 'createTempEntry']), + ...mapActions(['updateViewer', 'openNewEntryModal', 'createTempEntry', 'resetOpenFiles']), }, }; </script> diff --git a/app/assets/javascripts/ide/components/ide_tree_list.vue b/app/assets/javascripts/ide/components/ide_tree_list.vue index 0df99798d21..2e7226b727c 100644 --- a/app/assets/javascripts/ide/components/ide_tree_list.vue +++ b/app/assets/javascripts/ide/components/ide_tree_list.vue @@ -22,11 +22,6 @@ export default { required: false, default: null, }, - disableActionDropdown: { - type: Boolean, - required: false, - default: false, - }, }, computed: { ...mapState(['currentBranchId']), @@ -69,7 +64,6 @@ export default { :key="file.key" :file="file" :level="0" - :disable-action-dropdown="disableActionDropdown" /> </template> </div> diff --git a/app/assets/javascripts/ide/components/new_dropdown/index.vue b/app/assets/javascripts/ide/components/new_dropdown/index.vue index c29e49ba766..440e480d596 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/index.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/index.vue @@ -13,7 +13,7 @@ export default { ItemButton, }, props: { - branch: { + type: { type: String, required: true, }, @@ -45,7 +45,7 @@ export default { }, }, methods: { - ...mapActions(['createTempEntry', 'openNewEntryModal']), + ...mapActions(['createTempEntry', 'openNewEntryModal', 'deleteEntry']), createNewItem(type) { this.openNewEntryModal({ type, path: this.path }); this.dropdownOpen = false; @@ -82,28 +82,40 @@ export default { ref="dropdownMenu" class="dropdown-menu dropdown-menu-right" > + <template v-if="type === 'tree'"> + <li> + <item-button + :label="__('New file')" + class="d-flex" + icon="doc-new" + icon-classes="mr-2" + @click="createNewItem('blob')" + /> + </li> + <li> + <upload + :path="path" + @create="createTempEntry" + /> + </li> + <li> + <item-button + :label="__('New directory')" + class="d-flex" + icon="folder-new" + icon-classes="mr-2" + @click="createNewItem('tree')" + /> + </li> + <li class="divider"></li> + </template> <li> <item-button - :label="__('New file')" + :label="__('Delete')" class="d-flex" - icon="doc-new" + icon="remove" icon-classes="mr-2" - @click="createNewItem('blob')" - /> - </li> - <li> - <upload - :path="path" - @create="createTempEntry" - /> - </li> - <li> - <item-button - :label="__('New directory')" - class="d-flex" - icon="folder-new" - icon-classes="mr-2" - @click="createNewItem('tree')" + @click="deleteEntry(path)" /> </li> </ul> diff --git a/app/assets/javascripts/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue index 50ab242ba2a..6f1a941fbc4 100644 --- a/app/assets/javascripts/ide/components/repo_commit_section.vue +++ b/app/assets/javascripts/ide/components/repo_commit_section.vue @@ -44,7 +44,7 @@ export default { }, }, mounted() { - if (this.lastOpenedFile) { + if (this.lastOpenedFile && this.lastOpenedFile.type !== 'tree') { this.openPendingTab({ file: this.lastOpenedFile, keyPrefix: this.lastOpenedFile.changed ? stageKeys.unstaged : stageKeys.staged, diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue index 08ee12fd98f..f9badb01535 100644 --- a/app/assets/javascripts/ide/components/repo_editor.vue +++ b/app/assets/javascripts/ide/components/repo_editor.vue @@ -87,7 +87,9 @@ export default { this.editor.updateDimensions(); }, viewer() { - this.createEditorInstance(); + if (!this.file.pending) { + this.createEditorInstance(); + } }, panelResizing() { if (!this.panelResizing) { @@ -109,6 +111,7 @@ export default { }, methods: { ...mapActions([ + 'getFileData', 'getRawFileData', 'changeFileContent', 'setFileLanguage', @@ -123,10 +126,16 @@ export default { this.editor.clearEditor(); - this.getRawFileData({ + this.getFileData({ path: this.file.path, - baseSha: this.currentMergeRequest ? this.currentMergeRequest.baseCommitSha : '', + makeFileActive: false, }) + .then(() => + this.getRawFileData({ + path: this.file.path, + baseSha: this.currentMergeRequest ? this.currentMergeRequest.baseCommitSha : '', + }), + ) .then(() => { this.createEditorInstance(); }) @@ -246,6 +255,8 @@ export default { ref="editor" :class="{ 'is-readonly': isCommitModeActive, + 'is-deleted': file.deleted, + 'is-added': file.tempFile }" class="multi-file-editor-holder" > diff --git a/app/assets/javascripts/ide/components/repo_file.vue b/app/assets/javascripts/ide/components/repo_file.vue index 3b4dd5ae9aa..eb4a927fe0d 100644 --- a/app/assets/javascripts/ide/components/repo_file.vue +++ b/app/assets/javascripts/ide/components/repo_file.vue @@ -34,11 +34,6 @@ export default { type: Number, required: true, }, - disableActionDropdown: { - type: Boolean, - required: false, - default: false, - }, }, data() { return { @@ -212,8 +207,7 @@ export default { /> </span> <new-dropdown - v-if="isTree && !disableActionDropdown" - :project-id="file.projectId" + :type="file.type" :branch="file.branchId" :path="file.path" :mouse-over="mouseOver" diff --git a/app/assets/javascripts/ide/components/repo_tab.vue b/app/assets/javascripts/ide/components/repo_tab.vue index 03772ae4a4c..db47b75ec5c 100644 --- a/app/assets/javascripts/ide/components/repo_tab.vue +++ b/app/assets/javascripts/ide/components/repo_tab.vue @@ -37,7 +37,7 @@ export default { return this.fileHasChanged ? !this.tabMouseOver : false; }, fileHasChanged() { - return this.tab.changed || this.tab.tempFile || this.tab.staged; + return this.tab.changed || this.tab.tempFile || this.tab.staged || this.tab.deleted; }, }, @@ -71,7 +71,8 @@ export default { <template> <li :class="{ - active: tab.active + active: tab.active, + disabled: tab.pending }" @click="clickFile(tab)" @mouseover="mouseOverTab" @@ -105,7 +106,6 @@ export default { <changed-file-icon v-else :file="tab" - :force-modified-icon="true" /> </button> </li> diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js index 45d36f6f42c..0b514f31467 100644 --- a/app/assets/javascripts/ide/constants.js +++ b/app/assets/javascripts/ide/constants.js @@ -38,3 +38,18 @@ export const stageKeys = { unstaged: 'unstaged', staged: 'staged', }; + +export const commitItemIconMap = { + addition: { + icon: 'file-addition', + class: 'ide-file-addition', + }, + modified: { + icon: 'file-modified', + class: 'ide-file-modified', + }, + deleted: { + icon: 'file-deletion', + class: 'ide-file-deletion', + }, +}; diff --git a/app/assets/javascripts/ide/lib/common/model.js b/app/assets/javascripts/ide/lib/common/model.js index 78e6f632728..60bddb34977 100644 --- a/app/assets/javascripts/ide/lib/common/model.js +++ b/app/assets/javascripts/ide/lib/common/model.js @@ -7,7 +7,7 @@ export default class Model { this.disposable = new Disposable(); this.file = file; this.head = head; - this.content = file.content !== '' ? file.content : file.raw; + this.content = file.content !== '' || file.deleted ? file.content : file.raw; this.disposable.add( (this.originalModel = monacoEditor.createModel( diff --git a/app/assets/javascripts/ide/services/index.js b/app/assets/javascripts/ide/services/index.js index 49a481f25d5..cb93fba1665 100644 --- a/app/assets/javascripts/ide/services/index.js +++ b/app/assets/javascripts/ide/services/index.js @@ -18,7 +18,7 @@ export default { return axios .get(file.rawPath, { - params: { format: 'json' }, + transformResponse: [f => f], }) .then(({ data }) => data); }, @@ -33,7 +33,7 @@ export default { return axios .get(file.rawPath.replace(`/raw/${file.branchId}/${file.path}`, `/raw/${sha}/${file.path}`), { - params: { format: 'json' }, + transformResponse: [f => f], }) .then(({ data }) => data); }, diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js index b5bd6f5a6bb..2765acada48 100644 --- a/app/assets/javascripts/ide/stores/actions.js +++ b/app/assets/javascripts/ide/stores/actions.js @@ -185,6 +185,14 @@ export const openNewEntryModal = ({ commit }, { type, path = '' }) => { $('#ide-new-entry').modal('show'); }; +export const deleteEntry = ({ commit, dispatch, state }, path) => { + dispatch('burstUnusedSeal'); + dispatch('closeFile', state.entries[path]); + commit(types.DELETE_ENTRY, path); +}; + +export const resetOpenFiles = ({ commit }) => commit(types.RESET_OPEN_FILES); + export * from './actions/tree'; export * from './actions/file'; export * from './actions/project'; diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js index 6c0887e11ee..b343750f789 100644 --- a/app/assets/javascripts/ide/stores/actions/file.js +++ b/app/assets/javascripts/ide/stores/actions/file.js @@ -61,7 +61,11 @@ export const setFileActive = ({ commit, state, getters, dispatch }, path) => { export const getFileData = ({ state, commit, dispatch }, { path, makeFileActive = true }) => { const file = state.entries[path]; + + if (file.raw || file.tempFile) return Promise.resolve(); + commit(types.TOGGLE_LOADING, { entry: file }); + return service .getFileData( `${gon.relative_url_root ? gon.relative_url_root : ''}${file.url.replace('/-/', '/')}`, @@ -71,7 +75,7 @@ export const getFileData = ({ state, commit, dispatch }, { path, makeFileActive setPageTitle(decodeURI(normalizedHeaders['PAGE-TITLE'])); commit(types.SET_FILE_DATA, { data, file }); - commit(types.TOGGLE_FILE_OPEN, path); + if (makeFileActive) commit(types.TOGGLE_FILE_OPEN, path); if (makeFileActive) dispatch('setFileActive', path); commit(types.TOGGLE_LOADING, { entry: file }); }) @@ -97,7 +101,7 @@ export const getRawFileData = ({ state, commit, dispatch }, { path, baseSha }) = service .getRawFileData(file) .then(raw => { - commit(types.SET_FILE_RAW_DATA, { file, raw }); + if (!file.tempFile) commit(types.SET_FILE_RAW_DATA, { file, raw }); if (file.mrChange && file.mrChange.new_file === false) { service .getBaseRawFileData(file, baseSha) diff --git a/app/assets/javascripts/ide/stores/actions/tree.js b/app/assets/javascripts/ide/stores/actions/tree.js index ffaaaabff17..acb6ef5e6d4 100644 --- a/app/assets/javascripts/ide/stores/actions/tree.js +++ b/app/assets/javascripts/ide/stores/actions/tree.js @@ -21,14 +21,12 @@ export const showTreeEntry = ({ commit, dispatch, state }, path) => { export const handleTreeEntryAction = ({ commit, dispatch }, row) => { if (row.type === 'tree') { dispatch('toggleTreeOpen', row.path); - } else if (row.type === 'blob' && (row.opened || row.changed)) { - if (row.changed && !row.opened) { + } else if (row.type === 'blob') { + if (!row.opened) { commit(types.TOGGLE_FILE_OPEN, row.path); } dispatch('setFileActive', row.path); - } else { - dispatch('getFileData', { path: row.path }); } dispatch('showTreeEntry', row.path); diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js index 7828c31f20e..462ca45db9b 100644 --- a/app/assets/javascripts/ide/stores/modules/commit/actions.js +++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js @@ -174,11 +174,13 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo dispatch('updateActivityBarView', activityBarViews.edit, { root: true }); dispatch('updateViewer', 'editor', { root: true }); - router.push( - `/project/${rootState.currentProjectId}/blob/${getters.branchName}/-/${ - rootGetters.activeFile.path - }`, - ); + if (rootGetters.activeFile) { + router.push( + `/project/${rootState.currentProjectId}/blob/${getters.branchName}/-/${ + rootGetters.activeFile.path + }`, + ); + } } }) .then(() => dispatch('updateCommitAction', consts.COMMIT_TO_CURRENT_BRANCH)) diff --git a/app/assets/javascripts/ide/stores/modules/commit/getters.js b/app/assets/javascripts/ide/stores/modules/commit/getters.js index 3db4b2f903e..03777e6c10b 100644 --- a/app/assets/javascripts/ide/stores/modules/commit/getters.js +++ b/app/assets/javascripts/ide/stores/modules/commit/getters.js @@ -1,7 +1,15 @@ -import { sprintf, n__ } from '../../../../locale'; +import { sprintf, n__, __ } from '../../../../locale'; import * as consts from './constants'; const BRANCH_SUFFIX_COUNT = 5; +const createTranslatedTextForFiles = (files, text) => { + if (!files.length) return null; + + return sprintf(n__('%{text} %{files}', '%{text} %{files} files', files.length), { + files: files.reduce((acc, val) => acc.concat(val.path), []).join(', '), + text, + }); +}; export const discardDraftButtonDisabled = state => state.commitMessage === '' || state.submitCommitLoading; @@ -29,14 +37,16 @@ export const branchName = (state, getters, rootState) => { export const preBuiltCommitMessage = (state, _, rootState) => { if (state.commitMessage) return state.commitMessage; - const files = (rootState.stagedFiles.length - ? rootState.stagedFiles - : rootState.changedFiles - ).reduce((acc, val) => acc.concat(val.path), []); + const files = rootState.stagedFiles.length ? rootState.stagedFiles : rootState.changedFiles; + const modifiedFiles = files.filter(f => !f.deleted); + const deletedFiles = files.filter(f => f.deleted); - return sprintf(n__('Update %{files}', 'Update %{files} files', files.length), { - files: files.join(', '), - }); + return [ + createTranslatedTextForFiles(modifiedFiles, __('Update')), + createTranslatedTextForFiles(deletedFiles, __('Deleted')), + ] + .filter(t => t) + .join('\n'); }; // prevent babel-plugin-rewire from generating an invalid default during karma tests diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js index 8d6f9ccaf34..dae60f4d65a 100644 --- a/app/assets/javascripts/ide/stores/mutation_types.js +++ b/app/assets/javascripts/ide/stores/mutation_types.js @@ -76,3 +76,4 @@ export const RESET_OPEN_FILES = 'RESET_OPEN_FILES'; export const SET_ERROR_MESSAGE = 'SET_ERROR_MESSAGE'; export const OPEN_NEW_ENTRY_MODAL = 'OPEN_NEW_ENTRY_MODAL'; +export const DELETE_ENTRY = 'DELETE_ENTRY'; diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js index f8091f5b5e0..799c2f51e8d 100644 --- a/app/assets/javascripts/ide/stores/mutations.js +++ b/app/assets/javascripts/ide/stores/mutations.js @@ -1,3 +1,4 @@ +/* eslint-disable no-param-reassign */ import * as types from './mutation_types'; import projectMutations from './mutations/project'; import mergeRequestMutation from './mutations/merge_request'; @@ -171,6 +172,16 @@ export default { newEntryModal: { type, path }, }); }, + [types.DELETE_ENTRY](state, path) { + const entry = state.entries[path]; + const parent = entry.parentPath + ? state.entries[entry.parentPath] + : state.trees[`${state.currentProjectId}/${state.currentBranchId}`]; + + entry.deleted = true; + state.changedFiles = state.changedFiles.concat(entry); + parent.tree = parent.tree.filter(f => f.path !== entry.path); + }, ...projectMutations, ...mergeRequestMutation, ...fileMutations, diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js index 46547820425..9a87d50d6d5 100644 --- a/app/assets/javascripts/ide/stores/mutations/file.js +++ b/app/assets/javascripts/ide/stores/mutations/file.js @@ -1,5 +1,6 @@ /* eslint-disable no-param-reassign */ import * as types from '../mutation_types'; +import { sortTree } from '../utils'; import { diffModes } from '../../constants'; export default { @@ -51,9 +52,17 @@ export default { }); }, [types.SET_FILE_RAW_DATA](state, { file, raw }) { + const openPendingFile = state.openFiles.find( + f => f.path === file.path && f.pending && !f.tempFile, + ); + Object.assign(state.entries[file.path], { raw, }); + + if (openPendingFile) { + openPendingFile.raw = raw; + } }, [types.SET_FILE_BASE_RAW_DATA](state, { file, baseRaw }) { Object.assign(state.entries[file.path], { @@ -109,11 +118,22 @@ export default { }, [types.DISCARD_FILE_CHANGES](state, path) { const stagedFile = state.stagedFiles.find(f => f.path === path); + const entry = state.entries[path]; + const { deleted } = entry; Object.assign(state.entries[path], { content: stagedFile ? stagedFile.content : state.entries[path].raw, changed: false, + deleted: false, }); + + if (deleted) { + const parent = entry.parentPath + ? state.entries[entry.parentPath] + : state.trees[`${state.currentProjectId}/${state.currentBranchId}`]; + + parent.tree = sortTree(parent.tree.concat(entry)); + } }, [types.ADD_FILE_TO_CHANGED](state, path) { Object.assign(state, { diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js index 9e6b86dd844..bf7ab93ff5e 100644 --- a/app/assets/javascripts/ide/stores/utils.js +++ b/app/assets/javascripts/ide/stores/utils.js @@ -46,6 +46,7 @@ export const dataStructure = () => ({ parentPath: null, lastOpenedAt: 0, mrChange: null, + deleted: false, }); export const decorateData = entity => { @@ -105,15 +106,37 @@ export const setPageTitle = title => { document.title = title; }; +export const commitActionForFile = file => { + if (file.deleted) { + return 'delete'; + } else if (file.tempFile) { + return 'create'; + } + + return 'update'; +}; + +export const getCommitFiles = (stagedFiles, deleteTree = false) => + stagedFiles.reduce((acc, file) => { + if ((file.deleted || deleteTree) && file.type === 'tree') { + return acc.concat(getCommitFiles(file.tree, true)); + } + + return acc.concat({ + ...file, + deleted: deleteTree || file.deleted, + }); + }, []); + export const createCommitPayload = ({ branch, getters, newBranch, state, rootState }) => ({ branch, commit_message: state.commitMessage || getters.preBuiltCommitMessage, - actions: rootState.stagedFiles.map(f => ({ - action: f.tempFile ? 'create' : 'update', + actions: getCommitFiles(rootState.stagedFiles).map(f => ({ + action: commitActionForFile(f), file_path: f.path, content: f.content, encoding: f.base64 ? 'base64' : 'text', - last_commit_id: newBranch ? undefined : f.lastCommitSha, + last_commit_id: newBranch || f.deleted ? undefined : f.lastCommitSha, })), start_branch: newBranch ? rootState.currentBranchId : undefined, }); diff --git a/app/assets/javascripts/ide/utils.js b/app/assets/javascripts/ide/utils.js new file mode 100644 index 00000000000..92b15cf232d --- /dev/null +++ b/app/assets/javascripts/ide/utils.js @@ -0,0 +1,12 @@ +import { commitItemIconMap } from './constants'; + +// eslint-disable-next-line import/prefer-default-export +export const getCommitIconMap = file => { + if (file.deleted) { + return commitItemIconMap.deleted; + } else if (file.tempFile) { + return commitItemIconMap.addition; + } + + return commitItemIconMap.modified; +}; diff --git a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue index c32dc83da8e..14518f86dc7 100644 --- a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue @@ -1,5 +1,6 @@ <script> import $ from 'jquery'; +import _ from 'underscore'; import JobNameComponent from './job_name_component.vue'; import JobComponent from './job_component.vue'; import tooltip from '../../../vue_shared/directives/tooltip'; @@ -46,7 +47,7 @@ export default { computed: { tooltipText() { - return `${this.job.name} - ${this.job.status.label}`; + return _.escape(`${this.job.name} - ${this.job.status.label}`); }, }, diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue index 4ec67f6c01b..1952dd453f4 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue @@ -1,4 +1,5 @@ <script> +import _ from 'underscore'; import LoadingIcon from '~/vue_shared/components/loading_icon.vue'; import StageColumnComponent from './stage_column_component.vue'; @@ -26,7 +27,8 @@ export default { methods: { capitalizeStageName(name) { - return name.charAt(0).toUpperCase() + name.slice(1); + const escapedName = _.escape(name); + return escapedName.charAt(0).toUpperCase() + escapedName.slice(1); }, isFirstColumn(index) { diff --git a/app/assets/javascripts/pipelines/components/graph/job_component.vue b/app/assets/javascripts/pipelines/components/graph/job_component.vue index 8af984ef91a..84a3d58b770 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_component.vue @@ -1,4 +1,5 @@ <script> +import _ from 'underscore'; import ActionComponent from './action_component.vue'; import JobNameComponent from './job_name_component.vue'; import tooltip from '../../../vue_shared/directives/tooltip'; @@ -61,7 +62,7 @@ export default { const textBuilder = []; if (this.job.name) { - textBuilder.push(this.job.name); + textBuilder.push(_.escape(this.job.name)); } if (this.job.name && this.status.tooltip) { @@ -69,7 +70,7 @@ export default { } if (this.status.tooltip) { - textBuilder.push(`${this.job.status.tooltip}`); + textBuilder.push(this.job.status.tooltip); } return textBuilder.join(' '); diff --git a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue index 2c728582b7c..e7b2de52f76 100644 --- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue @@ -1,4 +1,5 @@ <script> +import _ from 'underscore'; import JobComponent from './job_component.vue'; import DropdownJobComponent from './dropdown_job_component.vue'; @@ -37,7 +38,7 @@ export default { }, jobId(job) { - return `ci-badge-${job.name}`; + return `ci-badge-${_.escape(job.name)}`; }, buildConnnectorClass(index) { diff --git a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue index d335c3f55af..dc599e1b9fc 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue @@ -42,11 +42,14 @@ export default { return this.timeEstimate - this.timeSpent; }, timeRemainingPercent() { - return `${Math.floor((this.timeSpent / this.timeEstimate) * 100)}%`; + return Math.floor((this.timeSpent / this.timeEstimate) * 100); }, timeRemainingStatusClass() { return this.timeEstimate >= this.timeSpent ? 'within_estimate' : 'over_estimate'; }, + progressBarVariant() { + return this.timeRemainingPercent > 100 ? 'danger' : 'primary'; + }, }, }; </script> @@ -62,16 +65,10 @@ export default { data-placement="top" role="timeRemainingDisplay" > - <div - :aria-valuenow="timeRemainingPercent" - class="meter-container" - > - <div - :style="{ width: timeRemainingPercent }" - class="meter-fill" - > - </div> - </div> + <gl-progress-bar + :value="timeRemainingPercent" + :variant="progressBarVariant" + /> <div class="compare-display-container"> <div class="compare-display float-left"> <span class="compare-label"> diff --git a/app/assets/javascripts/vue_shared/components/bar_chart.vue b/app/assets/javascripts/vue_shared/components/bar_chart.vue new file mode 100644 index 00000000000..3ced4eb691a --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/bar_chart.vue @@ -0,0 +1,391 @@ +<script> +import * as d3 from 'd3'; +import tooltip from '../directives/tooltip'; +import Icon from './icon.vue'; +import SvgGradient from './svg_gradient.vue'; +import { + GRADIENT_COLORS, + GRADIENT_OPACITY, + INVERSE_GRADIENT_COLORS, + INVERSE_GRADIENT_OPACITY, +} from './bar_chart_constants'; + +/** + * Renders a bar chart that can be dragged(scrolled) when the number + * of elements to renders surpasses that of the available viewport space + * while keeping even padding and a width of 24px (customizable) + * + * It can render data with the following format: + * graphData: [{ + * name: 'element' // x domain data + * value: 1 // y domain data + * }] + * + * Used in: + * - Contribution analytics - all of the rows describing pushes, merge requests and issues + */ + +export default { + directives: { + tooltip, + }, + components: { + Icon, + SvgGradient, + }, + props: { + graphData: { + type: Array, + required: true, + }, + barWidth: { + type: Number, + required: false, + default: 24, + }, + yAxisLabel: { + type: String, + required: true, + }, + }, + data() { + return { + minX: -40, + minY: 0, + vbWidth: 0, + vbHeight: 0, + vpWidth: 0, + vpHeight: 350, + preserveAspectRatioType: 'xMidYMid meet', + containerMargin: { + leftRight: 30, + }, + viewBoxMargin: { + topBottom: 150, + }, + panX: 0, + xScale: {}, + yScale: {}, + zoom: {}, + bars: {}, + xGraphRange: 0, + isLoading: true, + paddingThreshold: 50, + showScrollIndicator: false, + showLeftScrollIndicator: false, + isGrabbed: false, + isPanAvailable: false, + gradientColors: GRADIENT_COLORS, + gradientOpacity: GRADIENT_OPACITY, + inverseGradientColors: INVERSE_GRADIENT_COLORS, + inverseGradientOpacity: INVERSE_GRADIENT_OPACITY, + maxTextWidth: 72, + rectYAxisLabelDims: {}, + xAxisTextElements: {}, + yAxisRectTransformPadding: 20, + yAxisTextTransformPadding: 10, + yAxisTextRotation: 90, + }; + }, + computed: { + svgViewBox() { + return `${this.minX} ${this.minY} ${this.vbWidth} ${this.vbHeight}`; + }, + xAxisLocation() { + return `translate(${this.panX}, ${this.vbHeight})`; + }, + barTranslationTransform() { + return `translate(${this.panX}, 0)`; + }, + scrollIndicatorTransform() { + return `translate(${this.vbWidth - 80}, 0)`; + }, + activateGrabCursor() { + return { + 'svg-graph-container-with-grab': this.isPanAvailable, + 'svg-graph-container-grabbed': this.isPanAvailable && this.isGrabbed, + }; + }, + yAxisLabelRectTransform() { + const rectWidth = + this.rectYAxisLabelDims.height != null ? this.rectYAxisLabelDims.height / 2 : 0; + const yCoord = this.vbHeight / 2 - rectWidth; + + return `translate(${this.minX - this.yAxisRectTransformPadding}, ${yCoord})`; + }, + yAxisLabelTextTransform() { + const rectWidth = + this.rectYAxisLabelDims.height != null ? this.rectYAxisLabelDims.height / 2 : 0; + const yCoord = this.vbHeight / 2 + rectWidth - 5; + + return `translate(${this.minX + this.yAxisTextTransformPadding}, ${yCoord}) rotate(-${this.yAxisTextRotation})`; + }, + }, + mounted() { + if (!this.allValuesEmpty) { + this.draw(); + } + }, + methods: { + draw() { + // update viewport + this.vpWidth = this.$refs.svgContainer.clientWidth - this.containerMargin.leftRight; + // update viewbox + this.vbWidth = this.vpWidth; + this.vbHeight = this.vpHeight - this.viewBoxMargin.topBottom; + let padding = 0; + if (this.graphData.length * this.barWidth > this.vbWidth) { + this.xGraphRange = this.graphData.length * this.barWidth; + padding = this.calculatePadding(this.barWidth); + this.showScrollIndicator = true; + this.isPanAvailable = true; + } else { + this.xGraphRange = this.vbWidth - Math.abs(this.minX); + } + + this.xScale = d3 + .scaleBand() + .range([0, this.xGraphRange]) + .round(true) + .paddingInner(padding); + this.yScale = d3.scaleLinear().rangeRound([this.vbHeight, 0]); + + this.xScale.domain(this.graphData.map(d => d.name)); + this.yScale.domain([0, d3.max(this.graphData.map(d => d.value))]); + + // Zoom/Panning Function + this.zoom = d3 + .zoom() + .translateExtent([[0, 0], [this.xGraphRange, this.vbHeight]]) + .on('zoom', this.panGraph) + .on('end', this.removeGrabStyling); + + const xAxis = d3.axisBottom().scale(this.xScale); + + const yAxis = d3 + .axisLeft() + .scale(this.yScale) + .ticks(4); + + const renderedXAxis = d3 + .select(this.$refs.baseSvg) + .select('.x-axis') + .call(xAxis); + + this.xAxisTextElements = this.$refs.xAxis.querySelectorAll('text'); + + renderedXAxis.select('.domain').remove(); + + renderedXAxis + .selectAll('text') + .style('text-anchor', 'end') + .attr('dx', '-.3em') + .attr('dy', '-.95em') + .attr('class', 'tick-text') + .attr('transform', 'rotate(-90)'); + + renderedXAxis.selectAll('line').remove(); + + const { maxTextWidth } = this; + renderedXAxis.selectAll('text').each(function formatText() { + const axisText = d3.select(this); + let textLength = axisText.node().getComputedTextLength(); + let textContent = axisText.text(); + while (textLength > maxTextWidth && textContent.length > 0) { + textContent = textContent.slice(0, -1); + axisText.text(`${textContent}...`); + textLength = axisText.node().getComputedTextLength(); + } + }); + + const width = this.vbWidth; + + const renderedYAxis = d3 + .select(this.$refs.baseSvg) + .select('.y-axis') + .call(yAxis); + + renderedYAxis.selectAll('.tick').each(function createTickLines(d, i) { + if (i > 0) { + d3 + .select(this) + .select('line') + .attr('x2', width) + .attr('class', 'axis-tick'); + } + }); + + // Add the panning capabilities + if (this.isPanAvailable) { + d3 + .select(this.$refs.baseSvg) + .call(this.zoom) + .on('wheel.zoom', null); // This disables the pan of the graph with the scroll of the mouse wheel + } + + this.isLoading = false; + // Update the yAxisLabel coordinates + const labelDims = this.$refs.yAxisLabel.getBBox(); + this.rectYAxisLabelDims = { + height: labelDims.width + 10, + }; + }, + panGraph() { + const allowedRightScroll = this.xGraphRange - this.vbWidth - this.paddingThreshold; + const graphMaxPan = Math.abs(d3.event.transform.x) < allowedRightScroll; + this.isGrabbed = true; + this.panX = d3.event.transform.x; + + if (d3.event.transform.x === 0) { + this.showLeftScrollIndicator = false; + } else { + this.showLeftScrollIndicator = true; + this.showScrollIndicator = true; + } + + if (!graphMaxPan) { + this.panX = -1 * (this.xGraphRange - this.vbWidth + this.paddingThreshold); + this.showScrollIndicator = false; + } + }, + setTooltipTitle(data) { + return data !== null ? `${data.name}: ${data.value}` : ''; + }, + calculatePadding(desiredBarWidth) { + const widthWithMargin = this.vbWidth - Math.abs(this.minX); + const dividend = widthWithMargin - this.graphData.length * desiredBarWidth; + const divisor = widthWithMargin - desiredBarWidth; + + return dividend / divisor; + }, + removeGrabStyling() { + this.isGrabbed = false; + }, + barHoveredIn(index) { + this.xAxisTextElements[index].classList.add('x-axis-text'); + }, + barHoveredOut(index) { + this.xAxisTextElements[index].classList.remove('x-axis-text'); + }, + }, +}; +</script> +<template> + <div + ref="svgContainer" + :class="activateGrabCursor" + class="svg-graph-container" + > + <svg + ref="baseSvg" + :width="vpWidth" + :height="vpHeight" + :viewBox="svgViewBox" + :preserveAspectRatio="preserveAspectRatioType"> + <g + ref="xAxis" + :transform="xAxisLocation" + class="x-axis" + /> + <g v-if="!isLoading"> + <template + v-for="(data, index) in graphData"> + <rect + v-tooltip + :key="index" + :width="xScale.bandwidth()" + :x="xScale(data.name)" + :y="yScale(data.value)" + :height="vbHeight - yScale(data.value)" + :transform="barTranslationTransform" + :title="setTooltipTitle(data)" + class="bar-rect" + data-placement="top" + @mouseover="barHoveredIn(index)" + @mouseout="barHoveredOut(index)" + /> + </template> + </g> + <rect + :height="vbHeight + 100" + transform="translate(-100, -5)" + width="100" + fill="#fff" + /> + <g class="y-axis-label"> + <line + :x1="0" + :x2="0" + :y1="0" + :y2="vbHeight" + transform="translate(-35, 0)" + stroke="black" + /> + <!--Get text length and change the height of this rect accordingly--> + <rect + :height="rectYAxisLabelDims.height" + :transform="yAxisLabelRectTransform" + :width="30" + fill="#fff" + /> + <text + ref="yAxisLabel" + :transform="yAxisLabelTextTransform" + > + {{ yAxisLabel }} + </text> + </g> + <g + class="y-axis" + /> + <g v-if="showScrollIndicator"> + <rect + :height="vbHeight + 100" + :transform="`translate(${vpWidth - 60}, -5)`" + width="40" + fill="#fff" + /> + <icon + :x="vpWidth - 50" + :y="vbHeight / 2" + :width="14" + :height="14" + name="chevron-right" + class="animate-flicker" + /> + </g> + <!--The line that shows up when the data elements surpass the available width --> + <g + v-if="showScrollIndicator" + :transform="scrollIndicatorTransform"> + <rect + :height="vbHeight" + x="0" + y="0" + width="20" + fill="url(#shadow-gradient)" + /> + </g> + <!--Left scroll indicator--> + <g + v-if="showLeftScrollIndicator" + transform="translate(0, 0)"> + <rect + :height="vbHeight" + x="0" + y="0" + width="20" + fill="url(#left-shadow-gradient)" + /> + </g> + <svg-gradient + :colors="gradientColors" + :opacity="gradientOpacity" + identifier-name="shadow-gradient"/> + <svg-gradient + :colors="inverseGradientColors" + :opacity="inverseGradientOpacity" + identifier-name="left-shadow-gradient"/> + </svg> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/bar_chart_constants.js b/app/assets/javascripts/vue_shared/components/bar_chart_constants.js new file mode 100644 index 00000000000..6957b112da6 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/bar_chart_constants.js @@ -0,0 +1,4 @@ +export const GRADIENT_COLORS = ['#000', '#a7a7a7']; +export const GRADIENT_OPACITY = ['0', '0.4']; +export const INVERSE_GRADIENT_COLORS = ['#a7a7a7', '#000']; +export const INVERSE_GRADIENT_OPACITY = ['0.4', '0']; diff --git a/app/assets/javascripts/vue_shared/components/panel_resizer.vue b/app/assets/javascripts/vue_shared/components/panel_resizer.vue index 8c2dcc2d902..7947ae1e4da 100644 --- a/app/assets/javascripts/vue_shared/components/panel_resizer.vue +++ b/app/assets/javascripts/vue_shared/components/panel_resizer.vue @@ -32,7 +32,7 @@ }, computed: { className() { - return `drag${this.side}`; + return `drag-${this.side}`; }, cursorStyle() { if (this.enabled) { @@ -44,8 +44,15 @@ methods: { resetSize(e) { e.preventDefault(); + this.$emit('resize-start', this.size); + this.size = this.startSize; this.$emit('update:size', this.size); + + // End resizing on next tick so that listeners can react to DOM changes + this.$nextTick(() => { + this.$emit('resize-end', this.size); + }); }, startDrag(e) { if (this.enabled) { @@ -84,7 +91,7 @@ <div :class="className" :style="cursorStyle" - class="dragHandle" + class="drag-handle" @mousedown="startDrag" @dblclick="resetSize" ></div> diff --git a/app/assets/javascripts/vue_shared/components/reports/constants.js b/app/assets/javascripts/vue_shared/components/reports/constants.js new file mode 100644 index 00000000000..dbde648bfdb --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/reports/constants.js @@ -0,0 +1,3 @@ +export const STATUS_FAILED = 'failed'; +export const STATUS_SUCCESS = 'success'; +export const STATUS_NEUTRAL = 'neutral'; diff --git a/app/assets/javascripts/vue_shared/components/reports/issue_body.js b/app/assets/javascripts/vue_shared/components/reports/issue_body.js new file mode 100644 index 00000000000..f2141e519da --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/reports/issue_body.js @@ -0,0 +1,3 @@ +export const components = {}; + +export const componentNames = {}; diff --git a/app/assets/javascripts/vue_shared/components/reports/issue_status_icon.vue b/app/assets/javascripts/vue_shared/components/reports/issue_status_icon.vue new file mode 100644 index 00000000000..f8189117ac3 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/reports/issue_status_icon.vue @@ -0,0 +1,58 @@ +<script> +import Icon from '~/vue_shared/components/icon.vue'; + +import { + STATUS_FAILED, + STATUS_NEUTRAL, + STATUS_SUCCESS, +} from '~/vue_shared/components/reports/constants'; + +export default { + name: 'IssueStatusIcon', + components: { + Icon, + }, + props: { + // failed || success + status: { + type: String, + required: true, + }, + }, + computed: { + iconName() { + if (this.isStatusFailed) { + return 'status_failed_borderless'; + } else if (this.isStatusSuccess) { + return 'status_success_borderless'; + } + + return 'status_created_borderless'; + }, + isStatusFailed() { + return this.status === STATUS_FAILED; + }, + isStatusSuccess() { + return this.status === STATUS_SUCCESS; + }, + isStatusNeutral() { + return this.status === STATUS_NEUTRAL; + }, + }, +}; +</script> +<template> + <div + :class="{ + failed: isStatusFailed, + success: isStatusSuccess, + neutral: isStatusNeutral, + }" + class="report-block-list-icon" + > + <icon + :name="iconName" + :size="32" + /> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/reports/issues_list.vue b/app/assets/javascripts/vue_shared/components/reports/issues_list.vue index e1e03e39ee0..c01f77c2509 100644 --- a/app/assets/javascripts/vue_shared/components/reports/issues_list.vue +++ b/app/assets/javascripts/vue_shared/components/reports/issues_list.vue @@ -1,5 +1,10 @@ <script> -import IssuesBlock from './report_issues.vue'; +import IssuesBlock from '~/vue_shared/components/reports/report_issues.vue'; +import { + STATUS_SUCCESS, + STATUS_FAILED, + STATUS_NEUTRAL, +} from '~/vue_shared/components/reports/constants'; /** * Renders block of issues @@ -9,6 +14,9 @@ export default { components: { IssuesBlock, }, + success: STATUS_SUCCESS, + failed: STATUS_FAILED, + neutral: STATUS_NEUTRAL, props: { unresolvedIssues: { type: Array, @@ -25,29 +33,10 @@ export default { required: false, default: () => [], }, - allIssues: { - type: Array, - required: false, - default: () => [], - }, - type: { + component: { type: String, - required: true, - }, - }, - data() { - return { - isFullReportVisible: false, - }; - }, - computed: { - unresolvedIssuesStatus() { - return this.type === 'license' ? 'neutral' : 'failed'; - }, - }, - methods: { - openFullReport() { - this.isFullReportVisible = true; + required: false, + default: '', }, }, }; @@ -57,43 +46,26 @@ export default { <issues-block v-if="unresolvedIssues.length" - :type="type" - :status="unresolvedIssuesStatus" + :component="component" :issues="unresolvedIssues" + :status="$options.failed" class="js-mr-code-new-issues" /> <issues-block - v-if="isFullReportVisible" - :type="type" - :issues="allIssues" - class="js-mr-code-all-issues" - status="failed" - /> - - <issues-block v-if="neutralIssues.length" - :type="type" + :component="component" :issues="neutralIssues" + :status="$options.neutral" class="js-mr-code-non-issues" - status="neutral" /> <issues-block v-if="resolvedIssues.length" - :type="type" + :component="component" :issues="resolvedIssues" + :status="$options.success" class="js-mr-code-resolved-issues" - status="success" /> - - <button - v-if="allIssues.length && !isFullReportVisible" - type="button" - class="btn-link btn-blank prepend-left-10 js-expand-full-list break-link" - @click="openFullReport" - > - {{ s__("ciReport|Show complete code vulnerabilities report") }} - </button> </div> </template> diff --git a/app/assets/javascripts/vue_shared/components/reports/report_issues.vue b/app/assets/javascripts/vue_shared/components/reports/report_issues.vue index ecffb02a3a0..2d1f3d82234 100644 --- a/app/assets/javascripts/vue_shared/components/reports/report_issues.vue +++ b/app/assets/javascripts/vue_shared/components/reports/report_issues.vue @@ -1,19 +1,23 @@ <script> -import Icon from '~/vue_shared/components/icon.vue'; +import IssueStatusIcon from '~/vue_shared/components/reports/issue_status_icon.vue'; +import { components, componentNames } from '~/vue_shared/components/reports/issue_body'; export default { name: 'ReportIssues', components: { - Icon, + IssueStatusIcon, + ...components, }, props: { issues: { type: Array, required: true, }, - type: { + component: { type: String, - required: true, + required: false, + default: '', + validator: value => value === '' || Object.values(componentNames).includes(value), }, // failed || success status: { @@ -21,26 +25,6 @@ export default { required: true, }, }, - computed: { - iconName() { - if (this.isStatusFailed) { - return 'status_failed_borderless'; - } else if (this.isStatusSuccess) { - return 'status_success_borderless'; - } - - return 'status_created_borderless'; - }, - isStatusFailed() { - return this.status === 'failed'; - }, - isStatusSuccess() { - return this.status === 'success'; - }, - isStatusNeutral() { - return this.status === 'neutral'; - }, - }, }; </script> <template> @@ -52,20 +36,17 @@ export default { :key="index" class="report-block-list-issue" > - <div - :class="{ - failed: isStatusFailed, - success: isStatusSuccess, - neutral: isStatusNeutral, - }" - class="report-block-list-icon append-right-5" - > - <icon - :name="iconName" - :size="32" - /> - </div> + <issue-status-icon + :status="issue.status || status" + class="append-right-5" + /> + <component + v-if="component" + :is="component" + :issue="issue" + :status="issue.status || status" + /> </li> </ul> </div> diff --git a/app/assets/javascripts/vue_shared/components/reports/report_section.vue b/app/assets/javascripts/vue_shared/components/reports/report_section.vue index d383ed99a0c..0124d8b5bcc 100644 --- a/app/assets/javascripts/vue_shared/components/reports/report_section.vue +++ b/app/assets/javascripts/vue_shared/components/reports/report_section.vue @@ -21,7 +21,7 @@ export default { required: false, default: false, }, - type: { + component: { type: String, required: false, default: '', @@ -59,11 +59,6 @@ export default { required: false, default: () => [], }, - allIssues: { - type: Array, - required: false, - default: () => [], - }, infoText: { type: [String, Boolean], required: false, @@ -142,18 +137,10 @@ export default { </script> <template> <section class="media-section"> - <div - class="media" - > - <status-icon - :status="statusIconName" - /> - <div - class="media-body space-children d-flex" - > - <span - class="js-code-text code-text" - > + <div class="media"> + <status-icon :status="statusIconName" /> + <div class="media-body space-children d-flex flex-align-self-center"> + <span class="js-code-text code-text"> {{ headerText }} <popover @@ -163,10 +150,12 @@ export default { /> </span> + <slot name="actionButtons"></slot> + <button v-if="isCollapsible" type="button" - class="js-collapse-btn btn bt-default float-right btn-sm" + class="js-collapse-btn btn float-right btn-sm" @click="toggleCollapsed" > {{ collapseText }} @@ -183,8 +172,8 @@ export default { <issues-list :unresolved-issues="unresolvedIssues" :resolved-issues="resolvedIssues" - :all-issues="allIssues" - :type="type" + :neutral-issues="neutralIssues" + :component="component" /> </slot> </div> diff --git a/app/assets/javascripts/vue_shared/components/svg_gradient.vue b/app/assets/javascripts/vue_shared/components/svg_gradient.vue new file mode 100644 index 00000000000..b61a1befcd6 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/svg_gradient.vue @@ -0,0 +1,37 @@ +<script> +export default { + props: { + colors: { + type: Array, + required: true, + }, + opacity: { + type: Array, + required: true, + }, + identifierName: { + type: String, + required: true, + }, + }, +}; +</script> +<template> + <svg + height="0" + width="0"> + <defs> + <linearGradient + :id="identifierName"> + <stop + :stop-color="colors[0]" + :stop-opacity="opacity[0]" + offset="0%" /> + <stop + :stop-color="colors[1]" + :stop-opacity="opacity[1]" + offset="100%" /> + </linearGradient> + </defs> + </svg> +</template> diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 637587de597..2d6dba52801 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -370,11 +370,14 @@ img.emoji { margin-right: 10px; } -.alert, -.progress { +.alert { margin-bottom: $gl-padding; } +.progress { + height: 4px; +} + .project-item-select-holder { display: inline-block; position: relative; diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 2097bcebf69..e7e13d35d8e 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -567,9 +567,6 @@ border-bottom: 1px solid $white-normal; .mx-auto { - margin: 8px 0; - text-align: center; - .tanuki-logo, img { height: 36px; diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 56307777a72..a2789021ab4 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -179,6 +179,10 @@ font-weight: inherit; } + a > code { + color: $gl-link-color; + } + dd { margin-left: $gl-padding; } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index efc54196b75..08755b4b545 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -835,3 +835,5 @@ $font-family-monospace: $monospace-font; $input-line-height: 20px; $btn-line-height: 20px; $table-accent-bg: $gray-light; +$card-border-color: $border-color; +$card-cap-bg: $gray-light; diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/page_bundles/ide.scss index 2d76f0ce004..442aef124d3 100644 --- a/app/assets/stylesheets/pages/repo.scss +++ b/app/assets/stylesheets/page_bundles/ide.scss @@ -1,3 +1,6 @@ +@import 'framework/variables'; +@import 'framework/mixins'; + .project-refs-form, .project-refs-target-form { display: inline-block; @@ -74,6 +77,7 @@ .ide-file-icon-holder { display: flex; align-items: center; + color: $theme-gray-700; } .ide-file-changed-icon { @@ -161,12 +165,23 @@ background-color: $white-light; border-bottom-color: $white-light; } + + &:not(.disabled) { + .multi-file-tab { + cursor: pointer; + } + } + + &.disabled { + .multi-file-tab-close { + cursor: default; + } + } } } .multi-file-tab { @include str-truncated(141px); - cursor: pointer; svg { vertical-align: middle; @@ -241,6 +256,38 @@ } } + .is-deleted { + .editor.modified { + .margin-view-overlays, + .lines-content, + .decorationsOverviewRuler { + // !important to override monaco inline styles + display: none !important; + } + } + + .diffOverviewRuler.modified { + // !important to override monaco inline styles + display: none !important; + } + } + + .is-added { + .editor.original { + .margin-view-overlays, + .lines-content, + .decorationsOverviewRuler { + // !important to override monaco inline styles + display: none !important; + } + } + + .diffOverviewRuler.original { + // !important to override monaco inline styles + display: none !important; + } + } + .monaco-diff-editor.vs { .editor.modified { box-shadow: none; @@ -557,16 +604,21 @@ } } -.multi-file-addition, -.multi-file-addition-solid { +.ide-file-addition, +.ide-file-addition-solid { color: $green-500; } -.multi-file-modified, -.multi-file-modified-solid { +.ide-file-modified, +.ide-file-modified-solid { color: $orange-500; } +.ide-file-deletion, +.ide-file-deletion-solid { + color: $red-500; +} + .multi-file-commit-list-collapsed { display: flex; flex-direction: column; @@ -781,18 +833,21 @@ } } -.dragHandle { +.drag-handle { position: absolute; top: 0; bottom: 0; - width: 1px; - background-color: $white-dark; + width: 4px; + + &:hover { + background-color: $white-normal; + } - &.dragright { + &.drag-right { right: 0; } - &.dragleft { + &.drag-left { left: 0; } } @@ -1014,6 +1069,10 @@ .ide-new-btn { margin-left: auto; } + + button { + color: $gl-text-color; + } } .ide-sidebar-branch-title { diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index f030189af06..e5c38a20bf0 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -270,6 +270,7 @@ .block { width: 100%; + word-break: break-word; &:last-child { border-bottom: 1px solid $border-gray-normal; diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index b616357bb8d..591e21243ed 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -16,6 +16,7 @@ svg { vertical-align: middle; + top: -1px; } } diff --git a/app/assets/stylesheets/pages/graph.scss b/app/assets/stylesheets/pages/graph.scss index 84da9180f93..49d8a5d959b 100644 --- a/app/assets/stylesheets/pages/graph.scss +++ b/app/assets/stylesheets/pages/graph.scss @@ -31,3 +31,61 @@ color: $gl-text-red; } } + +.svg-graph-container { + width: 100%; + + .axis-tick { + opacity: 0.4; + } + + .tick-text { + fill: $gl-text-color-secondary; + } + + .x-axis-text { + fill: $theme-gray-900; + } + + .bar-rect { + fill: rgba($blue-500, 0.1); + stroke: $blue-500; + } + + .bar-rect:hover { + fill: rgba($blue-700, 0.3); + } + + .y-axis-label { + line { + stroke: $stat-graph-axis-fill; + } + + text { + font-weight: bold; + font-size: 12px; + fill: $theme-gray-800; + } + } +} + +.svg-graph-container-with-grab { + cursor: grab; + cursor: -webkit-grab; +} + +.svg-graph-container-grabbed { + cursor: grabbing; + cursor: -webkit-grabbing; +} + +@keyframes flickerAnimation { + 0% { opacity: 1; } + 50% { opacity: 0; } + 100% { opacity: 1; } +} + +.animate-flicker { + animation: flickerAnimation 1.5s infinite; + fill: $theme-gray-500; +} diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 797b106de23..d5ae2b673d9 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -834,17 +834,7 @@ } .compare-meter { - &.within_estimate { - .meter-fill { - background: $gl-primary; - } - } - &.over_estimate { - .meter-fill { - background: $red-500; - } - .time-remaining, .compare-value.spent { color: $red-500; @@ -852,18 +842,6 @@ } } - .meter-container { - background: $border-gray-light; - border-radius: 3px; - - .meter-fill { - max-width: 100%; - height: 5px; - border-radius: 3px; - background: $gl-primary; - } - } - .compare-display-container { display: flex; justify-content: space-between; diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss index c1b1d2e028d..8a4a2caa6c9 100644 --- a/app/assets/stylesheets/pages/login.scss +++ b/app/assets/stylesheets/pages/login.scss @@ -237,7 +237,7 @@ } .login-page-broadcast { - margin-top: 50px; + margin-top: 40px; } .navless-container { diff --git a/app/assets/stylesheets/snippets.scss b/app/assets/stylesheets/snippets.scss index 64110f9c3a0..bd777c66b56 100644 --- a/app/assets/stylesheets/snippets.scss +++ b/app/assets/stylesheets/snippets.scss @@ -22,8 +22,8 @@ height: 16px; background-size: cover; - &.gl-snippet-icon-doc_code { background-position: 0 0; } - &.gl-snippet-icon-doc_text { background-position: 0 -16px; } + &.gl-snippet-icon-doc-code { background-position: 0 0; } + &.gl-snippet-icon-doc-text { background-position: 0 -16px; } &.gl-snippet-icon-download { background-position: 0 -32px; } } |