diff options
Diffstat (limited to 'app/assets')
73 files changed, 553 insertions, 219 deletions
diff --git a/app/assets/images/ci_favicons/dev/favicon_status_canceled.ico b/app/assets/images/ci_favicons/dev/favicon_status_canceled.ico Binary files differdeleted file mode 100644 index 4af3582b60d..00000000000 --- a/app/assets/images/ci_favicons/dev/favicon_status_canceled.ico +++ /dev/null diff --git a/app/assets/images/ci_favicons/dev/favicon_status_created.ico b/app/assets/images/ci_favicons/dev/favicon_status_created.ico Binary files differdeleted file mode 100644 index 13639da2e8a..00000000000 --- a/app/assets/images/ci_favicons/dev/favicon_status_created.ico +++ /dev/null diff --git a/app/assets/images/ci_favicons/dev/favicon_status_failed.ico b/app/assets/images/ci_favicons/dev/favicon_status_failed.ico Binary files differdeleted file mode 100644 index 5f0e711b104..00000000000 --- a/app/assets/images/ci_favicons/dev/favicon_status_failed.ico +++ /dev/null diff --git a/app/assets/images/ci_favicons/dev/favicon_status_manual.ico b/app/assets/images/ci_favicons/dev/favicon_status_manual.ico Binary files differdeleted file mode 100644 index 8b1168a1267..00000000000 --- a/app/assets/images/ci_favicons/dev/favicon_status_manual.ico +++ /dev/null diff --git a/app/assets/images/ci_favicons/dev/favicon_status_not_found.ico b/app/assets/images/ci_favicons/dev/favicon_status_not_found.ico Binary files differdeleted file mode 100644 index ed19b69e1c5..00000000000 --- a/app/assets/images/ci_favicons/dev/favicon_status_not_found.ico +++ /dev/null diff --git a/app/assets/images/ci_favicons/dev/favicon_status_pending.ico b/app/assets/images/ci_favicons/dev/favicon_status_pending.ico Binary files differdeleted file mode 100644 index 5dfefd4cc5a..00000000000 --- a/app/assets/images/ci_favicons/dev/favicon_status_pending.ico +++ /dev/null diff --git a/app/assets/images/ci_favicons/dev/favicon_status_running.ico b/app/assets/images/ci_favicons/dev/favicon_status_running.ico Binary files differdeleted file mode 100644 index a41539c0e3e..00000000000 --- a/app/assets/images/ci_favicons/dev/favicon_status_running.ico +++ /dev/null diff --git a/app/assets/images/ci_favicons/dev/favicon_status_skipped.ico b/app/assets/images/ci_favicons/dev/favicon_status_skipped.ico Binary files differdeleted file mode 100644 index 2c1ae552b93..00000000000 --- a/app/assets/images/ci_favicons/dev/favicon_status_skipped.ico +++ /dev/null diff --git a/app/assets/images/ci_favicons/dev/favicon_status_success.ico b/app/assets/images/ci_favicons/dev/favicon_status_success.ico Binary files differdeleted file mode 100644 index 70f0ca61eca..00000000000 --- a/app/assets/images/ci_favicons/dev/favicon_status_success.ico +++ /dev/null diff --git a/app/assets/images/ci_favicons/dev/favicon_status_warning.ico b/app/assets/images/ci_favicons/dev/favicon_status_warning.ico Binary files differdeleted file mode 100644 index db289e03eb1..00000000000 --- a/app/assets/images/ci_favicons/dev/favicon_status_warning.ico +++ /dev/null diff --git a/app/assets/images/ci_favicons/favicon_status_canceled.ico b/app/assets/images/ci_favicons/favicon_status_canceled.ico Binary files differdeleted file mode 100644 index 23adcffff50..00000000000 --- a/app/assets/images/ci_favicons/favicon_status_canceled.ico +++ /dev/null diff --git a/app/assets/images/ci_favicons/favicon_status_canceled.png b/app/assets/images/ci_favicons/favicon_status_canceled.png Binary files differnew file mode 100644 index 00000000000..8adaa9c600b --- /dev/null +++ b/app/assets/images/ci_favicons/favicon_status_canceled.png diff --git a/app/assets/images/ci_favicons/favicon_status_created.ico b/app/assets/images/ci_favicons/favicon_status_created.ico Binary files differdeleted file mode 100644 index f9d93b390d8..00000000000 --- a/app/assets/images/ci_favicons/favicon_status_created.ico +++ /dev/null diff --git a/app/assets/images/ci_favicons/favicon_status_created.png b/app/assets/images/ci_favicons/favicon_status_created.png Binary files differnew file mode 100644 index 00000000000..ca788dd0034 --- /dev/null +++ b/app/assets/images/ci_favicons/favicon_status_created.png diff --git a/app/assets/images/ci_favicons/favicon_status_failed.ico b/app/assets/images/ci_favicons/favicon_status_failed.ico Binary files differdeleted file mode 100644 index 28a22ebf724..00000000000 --- a/app/assets/images/ci_favicons/favicon_status_failed.ico +++ /dev/null diff --git a/app/assets/images/ci_favicons/favicon_status_failed.png b/app/assets/images/ci_favicons/favicon_status_failed.png Binary files differnew file mode 100644 index 00000000000..93f1e2772fd --- /dev/null +++ b/app/assets/images/ci_favicons/favicon_status_failed.png diff --git a/app/assets/images/ci_favicons/favicon_status_manual.ico b/app/assets/images/ci_favicons/favicon_status_manual.ico Binary files differdeleted file mode 100644 index dbbf1abf30c..00000000000 --- a/app/assets/images/ci_favicons/favicon_status_manual.ico +++ /dev/null diff --git a/app/assets/images/ci_favicons/favicon_status_manual.png b/app/assets/images/ci_favicons/favicon_status_manual.png Binary files differnew file mode 100644 index 00000000000..c926062c806 --- /dev/null +++ b/app/assets/images/ci_favicons/favicon_status_manual.png diff --git a/app/assets/images/ci_favicons/favicon_status_not_found.ico b/app/assets/images/ci_favicons/favicon_status_not_found.ico Binary files differdeleted file mode 100644 index 49b9b232dd1..00000000000 --- a/app/assets/images/ci_favicons/favicon_status_not_found.ico +++ /dev/null diff --git a/app/assets/images/ci_favicons/favicon_status_not_found.png b/app/assets/images/ci_favicons/favicon_status_not_found.png Binary files differnew file mode 100644 index 00000000000..df3049315a9 --- /dev/null +++ b/app/assets/images/ci_favicons/favicon_status_not_found.png diff --git a/app/assets/images/ci_favicons/favicon_status_pending.ico b/app/assets/images/ci_favicons/favicon_status_pending.ico Binary files differdeleted file mode 100644 index 05962f3f148..00000000000 --- a/app/assets/images/ci_favicons/favicon_status_pending.ico +++ /dev/null diff --git a/app/assets/images/ci_favicons/favicon_status_pending.png b/app/assets/images/ci_favicons/favicon_status_pending.png Binary files differnew file mode 100644 index 00000000000..f7d67d4a230 --- /dev/null +++ b/app/assets/images/ci_favicons/favicon_status_pending.png diff --git a/app/assets/images/ci_favicons/favicon_status_running.ico b/app/assets/images/ci_favicons/favicon_status_running.ico Binary files differdeleted file mode 100644 index 7fa3d4d48d4..00000000000 --- a/app/assets/images/ci_favicons/favicon_status_running.ico +++ /dev/null diff --git a/app/assets/images/ci_favicons/favicon_status_running.png b/app/assets/images/ci_favicons/favicon_status_running.png Binary files differnew file mode 100644 index 00000000000..ff4167c4b20 --- /dev/null +++ b/app/assets/images/ci_favicons/favicon_status_running.png diff --git a/app/assets/images/ci_favicons/favicon_status_skipped.ico b/app/assets/images/ci_favicons/favicon_status_skipped.ico Binary files differdeleted file mode 100644 index b0c26b62068..00000000000 --- a/app/assets/images/ci_favicons/favicon_status_skipped.ico +++ /dev/null diff --git a/app/assets/images/ci_favicons/favicon_status_skipped.png b/app/assets/images/ci_favicons/favicon_status_skipped.png Binary files differnew file mode 100644 index 00000000000..a9c36464b69 --- /dev/null +++ b/app/assets/images/ci_favicons/favicon_status_skipped.png diff --git a/app/assets/images/ci_favicons/favicon_status_success.ico b/app/assets/images/ci_favicons/favicon_status_success.ico Binary files differdeleted file mode 100644 index b150960b5be..00000000000 --- a/app/assets/images/ci_favicons/favicon_status_success.ico +++ /dev/null diff --git a/app/assets/images/ci_favicons/favicon_status_success.png b/app/assets/images/ci_favicons/favicon_status_success.png Binary files differnew file mode 100644 index 00000000000..bcc30c73f5f --- /dev/null +++ b/app/assets/images/ci_favicons/favicon_status_success.png diff --git a/app/assets/images/ci_favicons/favicon_status_warning.ico b/app/assets/images/ci_favicons/favicon_status_warning.ico Binary files differdeleted file mode 100644 index 7e71d71684d..00000000000 --- a/app/assets/images/ci_favicons/favicon_status_warning.ico +++ /dev/null diff --git a/app/assets/images/ci_favicons/favicon_status_warning.png b/app/assets/images/ci_favicons/favicon_status_warning.png Binary files differnew file mode 100644 index 00000000000..6db3b0280f5 --- /dev/null +++ b/app/assets/images/ci_favicons/favicon_status_warning.png diff --git a/app/assets/images/favicon-blue.png b/app/assets/images/favicon-blue.png Binary files differnew file mode 100644 index 00000000000..2229fe79462 --- /dev/null +++ b/app/assets/images/favicon-blue.png diff --git a/app/assets/images/favicon-yellow.ico b/app/assets/images/favicon-yellow.ico Binary files differdeleted file mode 100644 index b650f277fb6..00000000000 --- a/app/assets/images/favicon-yellow.ico +++ /dev/null diff --git a/app/assets/images/favicon-yellow.png b/app/assets/images/favicon-yellow.png Binary files differnew file mode 100644 index 00000000000..2d5289818b4 --- /dev/null +++ b/app/assets/images/favicon-yellow.png diff --git a/app/assets/images/favicon.ico b/app/assets/images/favicon.ico Binary files differdeleted file mode 100644 index 3479cbbb46f..00000000000 --- a/app/assets/images/favicon.ico +++ /dev/null diff --git a/app/assets/images/favicon.png b/app/assets/images/favicon.png Binary files differnew file mode 100644 index 00000000000..845e0ec34a5 --- /dev/null +++ b/app/assets/images/favicon.png diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue index 84a7f277227..0692c96e767 100644 --- a/app/assets/javascripts/boards/components/board_list.vue +++ b/app/assets/javascripts/boards/components/board_list.vue @@ -87,10 +87,46 @@ export default { mounted() { const options = gl.issueBoards.getBoardSortableDefaultOptions({ scroll: true, - group: 'issues', disabled: this.disabled, filter: '.board-list-count, .is-disabled', dataIdAttr: 'data-issue-id', + group: { + name: 'issues', + /** + * Dynamically determine between which containers + * items can be moved or copied as + * Assignee lists (EE feature) require this behavior + */ + pull: (to, from, dragEl, e) => { + // As per Sortable's docs, `to` should provide + // reference to exact sortable container on which + // we're trying to drag element, but either it is + // a library's bug or our markup structure is too complex + // that `to` never points to correct container + // See https://github.com/RubaXa/Sortable/issues/1037 + // + // So we use `e.target` which is always accurate about + // which element we're currently dragging our card upon + // So from there, we can get reference to actual container + // and thus the container type to enable Copy or Move + if (e.target) { + const containerEl = e.target.closest('.js-board-list') || e.target.querySelector('.js-board-list'); + const toBoardType = containerEl.dataset.boardType; + + if (toBoardType) { + const fromBoardType = this.list.type; + + if ((fromBoardType === 'assignee' && toBoardType === 'label') || + (fromBoardType === 'label' && toBoardType === 'assignee')) { + return 'clone'; + } + } + } + + return true; + }, + revertClone: true, + }, onStart: (e) => { const card = this.$refs.issue[e.oldIndex]; @@ -179,10 +215,11 @@ export default { :list="list" v-if="list.type !== 'closed' && showIssueForm"/> <ul - class="board-list" + class="board-list js-board-list" v-show="!loading" ref="list" :data-board="list.id" + :data-board-type="list.type" :class="{ 'is-smaller': showIssueForm }"> <board-card v-for="(issue, index) in issues" diff --git a/app/assets/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue index e8dfd95f7ae..297c9eff38c 100644 --- a/app/assets/javascripts/boards/components/board_new_issue.vue +++ b/app/assets/javascripts/boards/components/board_new_issue.vue @@ -49,11 +49,12 @@ export default { this.error = false; const labels = this.list.label ? [this.list.label] : []; + const assignees = this.list.assignee ? [this.list.assignee] : []; const issue = new ListIssue({ title: this.title, labels, subscribed: true, - assignees: [], + assignees, project_id: this.selectedProject.id, }); @@ -141,4 +142,3 @@ export default { </div> </div> </template> - diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js b/app/assets/javascripts/boards/components/new_list_dropdown.js index 71f49319c36..6dcd4aaec43 100644 --- a/app/assets/javascripts/boards/components/new_list_dropdown.js +++ b/app/assets/javascripts/boards/components/new_list_dropdown.js @@ -56,6 +56,7 @@ gl.issueBoards.newListDropdownInit = () => { filterable: true, selectable: true, multiSelect: true, + containerSelector: '.js-tab-container-labels .dropdown-page-one .dropdown-content', clicked (options) { const { e } = options; const label = options.selectedObj; diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js index 29ab13b8e0b..cdad8d238e3 100644 --- a/app/assets/javascripts/boards/index.js +++ b/app/assets/javascripts/boards/index.js @@ -7,6 +7,7 @@ import Vue from 'vue'; import Flash from '~/flash'; import { __ } from '~/locale'; import '~/vue_shared/models/label'; +import '~/vue_shared/models/assignee'; import FilteredSearchBoards from './filtered_search_boards'; import eventHub from './eventhub'; @@ -15,7 +16,6 @@ import './models/issue'; import './models/list'; import './models/milestone'; import './models/project'; -import './models/assignee'; import './stores/boards_store'; import ModalStore from './stores/modal_store'; import BoardService from './services/board_service'; diff --git a/app/assets/javascripts/boards/models/assignee.js b/app/assets/javascripts/boards/models/assignee.js deleted file mode 100644 index 05dd449e4fd..00000000000 --- a/app/assets/javascripts/boards/models/assignee.js +++ /dev/null @@ -1,12 +0,0 @@ -/* eslint-disable no-unused-vars */ - -class ListAssignee { - constructor(user, defaultAvatar) { - this.id = user.id; - this.name = user.name; - this.username = user.username; - this.avatar = user.avatar_url || defaultAvatar; - } -} - -window.ListAssignee = ListAssignee; diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js index 7144f4190e7..a79dd62e2e4 100644 --- a/app/assets/javascripts/boards/models/list.js +++ b/app/assets/javascripts/boards/models/list.js @@ -1,12 +1,14 @@ /* eslint-disable space-before-function-paren, no-underscore-dangle, class-methods-use-this, consistent-return, no-shadow, no-param-reassign, max-len, no-unused-vars */ /* global ListIssue */ -/* global ListLabel */ + +import ListLabel from '~/vue_shared/models/label'; +import ListAssignee from '~/vue_shared/models/assignee'; import queryData from '../utils/query_data'; const PER_PAGE = 20; class List { - constructor (obj, defaultAvatar) { + constructor(obj, defaultAvatar) { this.id = obj.id; this._uid = this.guid(); this.position = obj.position; @@ -24,6 +26,9 @@ class List { if (obj.label) { this.label = new ListLabel(obj.label); + } else if (obj.user) { + this.assignee = new ListAssignee(obj.user); + this.title = this.assignee.name; } if (this.type !== 'blank' && this.id) { @@ -34,14 +39,25 @@ class List { } guid() { - const s4 = () => Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1); + const s4 = () => + Math.floor((1 + Math.random()) * 0x10000) + .toString(16) + .substring(1); return `${s4()}${s4()}-${s4()}-${s4()}-${s4()}-${s4()}${s4()}${s4()}`; } - save () { + save() { + const entity = this.label || this.assignee; + let entityType = ''; + if (this.label) { + entityType = 'label_id'; + } else { + entityType = 'assignee_id'; + } + return gl.boardService.createList(this.label.id) .then(res => res.data) - .then((data) => { + .then(data => { this.id = data.id; this.type = data.list_type; this.position = data.position; @@ -50,25 +66,23 @@ class List { }); } - destroy () { + destroy() { const index = gl.issueBoards.BoardsStore.state.lists.indexOf(this); gl.issueBoards.BoardsStore.state.lists.splice(index, 1); gl.issueBoards.BoardsStore.updateNewListDropdown(this.id); - gl.boardService.destroyList(this.id) - .catch(() => { - // TODO: handle request error - }); + gl.boardService.destroyList(this.id).catch(() => { + // TODO: handle request error + }); } - update () { - gl.boardService.updateList(this.id, this.position) - .catch(() => { - // TODO: handle request error - }); + update() { + gl.boardService.updateList(this.id, this.position).catch(() => { + // TODO: handle request error + }); } - nextPage () { + nextPage() { if (this.issuesSize > this.issues.length) { if (this.issues.length / PER_PAGE >= 1) { this.page += 1; @@ -78,7 +92,7 @@ class List { } } - getIssues (emptyIssues = true) { + getIssues(emptyIssues = true) { const data = queryData(gl.issueBoards.BoardsStore.filter.path, { page: this.page }); if (this.label && data.label_name) { @@ -89,7 +103,8 @@ class List { this.loading = true; } - return gl.boardService.getIssuesForList(this.id, data) + return gl.boardService + .getIssuesForList(this.id, data) .then(res => res.data) .then((data) => { this.loading = false; @@ -103,11 +118,12 @@ class List { }); } - newIssue (issue) { + newIssue(issue) { this.addIssue(issue, null, 0); this.issuesSize += 1; - return gl.boardService.newIssue(this.id, issue) + return gl.boardService + .newIssue(this.id, issue) .then(res => res.data) .then((data) => { issue.id = data.id; @@ -123,13 +139,13 @@ class List { }); } - createIssues (data) { - data.forEach((issueObj) => { + createIssues(data) { + data.forEach(issueObj => { this.addIssue(new ListIssue(issueObj, this.defaultAvatar)); }); } - addIssue (issue, listFrom, newIndex) { + addIssue(issue, listFrom, newIndex) { let moveBeforeId = null; let moveAfterId = null; @@ -152,6 +168,13 @@ class List { issue.addLabel(this.label); } + if (this.assignee) { + if (listFrom && listFrom.type === 'assignee') { + issue.removeAssignee(listFrom.assignee); + } + issue.addAssignee(this.assignee); + } + if (listFrom) { this.issuesSize += 1; @@ -160,29 +183,29 @@ class List { } } - moveIssue (issue, oldIndex, newIndex, moveBeforeId, moveAfterId) { + moveIssue(issue, oldIndex, newIndex, moveBeforeId, moveAfterId) { this.issues.splice(oldIndex, 1); this.issues.splice(newIndex, 0, issue); - gl.boardService.moveIssue(issue.id, null, null, moveBeforeId, moveAfterId) - .catch(() => { - // TODO: handle request error - }); + gl.boardService.moveIssue(issue.id, null, null, moveBeforeId, moveAfterId).catch(() => { + // TODO: handle request error + }); } updateIssueLabel(issue, listFrom, moveBeforeId, moveAfterId) { - gl.boardService.moveIssue(issue.id, listFrom.id, this.id, moveBeforeId, moveAfterId) + gl.boardService + .moveIssue(issue.id, listFrom.id, this.id, moveBeforeId, moveAfterId) .catch(() => { // TODO: handle request error }); } - findIssue (id) { + findIssue(id) { return this.issues.find(issue => issue.id === id); } - removeIssue (removeIssue) { - this.issues = this.issues.filter((issue) => { + removeIssue(removeIssue) { + this.issues = this.issues.filter(issue => { const matchesRemove = removeIssue.id === issue.id; if (matchesRemove) { diff --git a/app/assets/javascripts/boards/services/board_service.js b/app/assets/javascripts/boards/services/board_service.js index 7c90597f77c..029b0971f2c 100644 --- a/app/assets/javascripts/boards/services/board_service.js +++ b/app/assets/javascripts/boards/services/board_service.js @@ -30,11 +30,13 @@ export default class BoardService { return axios.post(this.listsEndpointGenerate, {}); } - createList(labelId) { + createList(entityId, entityType) { + const list = { + [entityType]: entityId, + }; + return axios.post(this.listsEndpoint, { - list: { - label_id: labelId, - }, + list, }); } diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js index 20e78edf2a2..7dc83843e9b 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js +++ b/app/assets/javascripts/boards/stores/boards_store.js @@ -103,8 +103,15 @@ gl.issueBoards.BoardsStore = { const listLabels = issueLists.map(listIssue => listIssue.label); if (!issueTo) { - // Add to new lists issues if it doesn't already exist - listTo.addIssue(issue, listFrom, newIndex); + // Check if target list assignee is already present in this issue + if ((listTo.type === 'assignee' && listFrom.type === 'assignee') && + issue.findAssignee(listTo.assignee)) { + const targetIssue = listTo.findIssue(issue.id); + targetIssue.removeAssignee(listFrom.assignee); + } else { + // Add to new lists issues if it doesn't already exist + listTo.addIssue(issue, listFrom, newIndex); + } } else { listTo.updateIssueLabel(issue, listFrom); issueTo.removeLabel(listFrom.label); @@ -115,7 +122,11 @@ gl.issueBoards.BoardsStore = { list.removeIssue(issue); }); issue.removeLabels(listLabels); - } else { + } else if (listTo.type === 'backlog' && listFrom.type === 'assignee') { + issue.removeAssignee(listFrom.assignee); + listFrom.removeIssue(issue); + } else if ((listTo.type !== 'label' && listFrom.type === 'assignee') || + (listTo.type !== 'assignee' && listFrom.type === 'label')) { listFrom.removeIssue(issue); } }, @@ -126,11 +137,12 @@ gl.issueBoards.BoardsStore = { list.moveIssue(issue, oldIndex, newIndex, beforeId, afterId); }, findList (key, val, type = 'label') { - return this.state.lists.filter((list) => { - const byType = type ? list['type'] === type : true; + const filteredList = this.state.lists.filter((list) => { + const byType = type ? (list.type === type) || (list.type === 'assignee') : true; return list[key] === val && byType; - })[0]; + }); + return filteredList[0]; }, updateFiltersUrl () { history.pushState(null, null, `?${this.filter.path}`); diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js index 3a4ac09f67c..d90db7b103c 100644 --- a/app/assets/javascripts/clusters/stores/clusters_store.js +++ b/app/assets/javascripts/clusters/stores/clusters_store.js @@ -95,7 +95,7 @@ export default class ClusterStore { this.state.applications.jupyter.hostname = serverAppEntry.hostname || (this.state.applications.ingress.externalIp - ? `jupyter.${this.state.applications.ingress.externalIp}.xip.io` + ? `jupyter.${this.state.applications.ingress.externalIp}.nip.io` : ''); } }); diff --git a/app/assets/javascripts/diff_notes/diff_notes_bundle.js b/app/assets/javascripts/diff_notes/diff_notes_bundle.js index e17daec6a92..d5161ab7df9 100644 --- a/app/assets/javascripts/diff_notes/diff_notes_bundle.js +++ b/app/assets/javascripts/diff_notes/diff_notes_bundle.js @@ -69,9 +69,10 @@ export default () => { gl.diffNotesCompileComponents(); - if (!hasVueMRDiscussionsCookie()) { + const resolveCountAppEl = document.querySelector('#resolve-count-app'); + if (!hasVueMRDiscussionsCookie() && resolveCountAppEl) { new Vue({ - el: '#resolve-count-app', + el: resolveCountAppEl, components: { 'resolve-count': ResolveCount }, diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index 746a06b7c4f..7fbba7e27cb 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -602,7 +602,11 @@ GitLabDropdown = (function() { var selector; selector = '.dropdown-content'; if (this.dropdown.find(".dropdown-toggle-page").length) { - selector = ".dropdown-page-one .dropdown-content"; + if (this.options.containerSelector) { + selector = this.options.containerSelector; + } else { + selector = '.dropdown-page-one .dropdown-content'; + } } return $(selector, this.dropdown).empty(); diff --git a/app/assets/javascripts/ide/components/panes/right.vue b/app/assets/javascripts/ide/components/panes/right.vue index aafd6a15a78..dd7fc8f1e01 100644 --- a/app/assets/javascripts/ide/components/panes/right.vue +++ b/app/assets/javascripts/ide/components/panes/right.vue @@ -5,6 +5,7 @@ import Icon from '../../../vue_shared/components/icon.vue'; import { rightSidebarViews } from '../../constants'; import PipelinesList from '../pipelines/list.vue'; import JobsDetail from '../jobs/detail.vue'; +import ResizablePanel from '../resizable_panel.vue'; export default { directives: { @@ -14,6 +15,7 @@ export default { Icon, PipelinesList, JobsDetail, + ResizablePanel, }, computed: { ...mapState(['rightPane']), @@ -40,12 +42,16 @@ export default { <div class="multi-file-commit-panel ide-right-sidebar" > - <div - class="multi-file-commit-panel-inner" + <resizable-panel v-if="rightPane" + class="multi-file-commit-panel-inner" + :collapsible="false" + :initial-width="350" + :min-size="350" + side="right" > <component :is="rightPane" /> - </div> + </resizable-panel> <nav class="ide-activity-bar"> <ul class="list-unstyled"> <li> diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue index 93453989c08..d365745d78b 100644 --- a/app/assets/javascripts/ide/components/repo_editor.vue +++ b/app/assets/javascripts/ide/components/repo_editor.vue @@ -1,10 +1,8 @@ <script> -/* global monaco */ import { mapState, mapGetters, mapActions } from 'vuex'; import flash from '~/flash'; import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue'; import { activityBarViews, viewerTypes } from '../constants'; -import monacoLoader from '../monaco_loader'; import Editor from '../lib/editor'; import ExternalLink from './external_link.vue'; @@ -50,7 +48,7 @@ export default { // Compare key to allow for files opened in review mode to be cached differently if (oldVal.key !== this.file.key) { - this.initMonaco(); + this.initEditor(); if (this.currentActivityView !== activityBarViews.edit) { this.setFileViewMode({ @@ -84,15 +82,10 @@ export default { this.editor.dispose(); }, mounted() { - if (this.editor && monaco) { - this.initMonaco(); - } else { - monacoLoader(['vs/editor/editor.main'], () => { - this.editor = Editor.create(monaco); - - this.initMonaco(); - }); + if (!this.editor) { + this.editor = Editor.create(); } + this.initEditor(); }, methods: { ...mapActions([ @@ -105,7 +98,7 @@ export default { 'updateViewer', 'removePendingTab', ]), - initMonaco() { + initEditor() { if (this.shouldHideEditor) return; this.editor.clearEditor(); @@ -118,7 +111,7 @@ export default { this.createEditorInstance(); }) .catch(err => { - flash('Error setting up monaco. Please try again.', 'alert', document, null, false, true); + flash('Error setting up editor. Please try again.', 'alert', document, null, false, true); throw err; }); }, diff --git a/app/assets/javascripts/ide/lib/common/model.js b/app/assets/javascripts/ide/lib/common/model.js index e5149b1f3ad..78e6f632728 100644 --- a/app/assets/javascripts/ide/lib/common/model.js +++ b/app/assets/javascripts/ide/lib/common/model.js @@ -1,32 +1,32 @@ +import { editor as monacoEditor, Uri } from 'monaco-editor'; import Disposable from './disposable'; import eventHub from '../../eventhub'; export default class Model { - constructor(monaco, file, head = null) { - this.monaco = monaco; + constructor(file, head = null) { this.disposable = new Disposable(); this.file = file; this.head = head; this.content = file.content !== '' ? file.content : file.raw; this.disposable.add( - (this.originalModel = this.monaco.editor.createModel( + (this.originalModel = monacoEditor.createModel( head ? head.content : this.file.raw, undefined, - new this.monaco.Uri(null, null, `original/${this.path}`), + new Uri(false, false, `original/${this.path}`), )), - (this.model = this.monaco.editor.createModel( + (this.model = monacoEditor.createModel( this.content, undefined, - new this.monaco.Uri(null, null, this.path), + new Uri(false, false, this.path), )), ); if (this.file.mrChange) { this.disposable.add( - (this.baseModel = this.monaco.editor.createModel( + (this.baseModel = monacoEditor.createModel( this.file.baseRaw, undefined, - new this.monaco.Uri(null, null, `target/${this.path}`), + new Uri(false, false, `target/${this.path}`), )), ); } diff --git a/app/assets/javascripts/ide/lib/common/model_manager.js b/app/assets/javascripts/ide/lib/common/model_manager.js index 7f643969480..bd9b8fc3fcc 100644 --- a/app/assets/javascripts/ide/lib/common/model_manager.js +++ b/app/assets/javascripts/ide/lib/common/model_manager.js @@ -3,8 +3,7 @@ import Disposable from './disposable'; import Model from './model'; export default class ModelManager { - constructor(monaco) { - this.monaco = monaco; + constructor() { this.disposable = new Disposable(); this.models = new Map(); } @@ -22,7 +21,7 @@ export default class ModelManager { return this.getModel(file.key); } - const model = new Model(this.monaco, file, head); + const model = new Model(file, head); this.models.set(model.path, model); this.disposable.add(model); diff --git a/app/assets/javascripts/ide/lib/diff/controller.js b/app/assets/javascripts/ide/lib/diff/controller.js index f579424cf33..046e562ba2b 100644 --- a/app/assets/javascripts/ide/lib/diff/controller.js +++ b/app/assets/javascripts/ide/lib/diff/controller.js @@ -1,4 +1,4 @@ -/* global monaco */ +import { Range } from 'monaco-editor'; import { throttle } from 'underscore'; import DirtyDiffWorker from './diff_worker'; import Disposable from '../common/disposable'; @@ -16,7 +16,7 @@ export const getDiffChangeType = change => { }; export const getDecorator = change => ({ - range: new monaco.Range(change.lineNumber, 1, change.endLineNumber, 1), + range: new Range(change.lineNumber, 1, change.endLineNumber, 1), options: { isWholeLine: true, linesDecorationsClassName: `dirty-diff dirty-diff-${getDiffChangeType(change)}`, diff --git a/app/assets/javascripts/ide/lib/editor.js b/app/assets/javascripts/ide/lib/editor.js index 9c3bb9cc17d..02038fcb534 100644 --- a/app/assets/javascripts/ide/lib/editor.js +++ b/app/assets/javascripts/ide/lib/editor.js @@ -1,4 +1,5 @@ import _ from 'underscore'; +import { editor as monacoEditor, KeyCode, KeyMod } from 'monaco-editor'; import store from '../stores'; import DecorationsController from './decorations/controller'; import DirtyDiffController from './diff/controller'; @@ -8,6 +9,11 @@ import editorOptions, { defaultEditorOptions } from './editor_options'; import gitlabTheme from './themes/gl_theme'; import keymap from './keymap.json'; +function setupMonacoTheme() { + monacoEditor.defineTheme(gitlabTheme.themeName, gitlabTheme.monacoTheme); + monacoEditor.setTheme('gitlab'); +} + export const clearDomElement = el => { if (!el || !el.firstChild) return; @@ -17,24 +23,22 @@ export const clearDomElement = el => { }; export default class Editor { - static create(monaco) { - if (this.editorInstance) return this.editorInstance; - - this.editorInstance = new Editor(monaco); - + static create() { + if (!this.editorInstance) { + this.editorInstance = new Editor(); + } return this.editorInstance; } - constructor(monaco) { - this.monaco = monaco; + constructor() { this.currentModel = null; this.instance = null; this.dirtyDiffController = null; this.disposable = new Disposable(); - this.modelManager = new ModelManager(this.monaco); + this.modelManager = new ModelManager(); this.decorationsController = new DecorationsController(this); - this.setupMonacoTheme(); + setupMonacoTheme(); this.debouncedUpdate = _.debounce(() => { this.updateDimensions(); @@ -46,7 +50,7 @@ export default class Editor { clearDomElement(domElement); this.disposable.add( - (this.instance = this.monaco.editor.create(domElement, { + (this.instance = monacoEditor.create(domElement, { ...defaultEditorOptions, })), (this.dirtyDiffController = new DirtyDiffController( @@ -66,7 +70,7 @@ export default class Editor { clearDomElement(domElement); this.disposable.add( - (this.instance = this.monaco.editor.createDiffEditor(domElement, { + (this.instance = monacoEditor.createDiffEditor(domElement, { ...defaultEditorOptions, quickSuggestions: false, occurrencesHighlight: false, @@ -122,17 +126,11 @@ export default class Editor { modified: model.getModel(), }); - this.monaco.editor.createDiffNavigator(this.instance, { + monacoEditor.createDiffNavigator(this.instance, { alwaysRevealFirst: true, }); } - setupMonacoTheme() { - this.monaco.editor.defineTheme(gitlabTheme.themeName, gitlabTheme.monacoTheme); - - this.monaco.editor.setTheme('gitlab'); - } - clearEditor() { if (this.instance) { this.instance.setModel(null); @@ -200,7 +198,7 @@ export default class Editor { const getKeyCode = key => { const monacoKeyMod = key.indexOf('KEY_') === 0; - return monacoKeyMod ? this.monaco.KeyCode[key] : this.monaco.KeyMod[key]; + return monacoKeyMod ? KeyCode[key] : KeyMod[key]; }; keymap.forEach(command => { diff --git a/app/assets/javascripts/ide/monaco_loader.js b/app/assets/javascripts/ide/monaco_loader.js deleted file mode 100644 index 142a220097b..00000000000 --- a/app/assets/javascripts/ide/monaco_loader.js +++ /dev/null @@ -1,16 +0,0 @@ -import monacoContext from 'monaco-editor/dev/vs/loader'; - -monacoContext.require.config({ - paths: { - vs: `${__webpack_public_path__}monaco-editor/vs`, // eslint-disable-line camelcase - }, -}); - -// ignore CDN config and use local assets path for service worker which cannot be cross-domain -const relativeRootPath = (gon && gon.relative_url_root) || ''; -const monacoPath = `${relativeRootPath}/assets/webpack/monaco-editor/vs`; -window.MonacoEnvironment = { getWorkerUrl: () => `${monacoPath}/base/worker/workerMain.js` }; - -// eslint-disable-next-line no-underscore-dangle -window.__monaco_context__ = monacoContext; -export default monacoContext.require; diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index 8b5445d012b..d55d0585031 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -384,6 +384,49 @@ export const backOff = (fn, timeout = 60000) => { }); }; +export const createOverlayIcon = (iconPath, overlayPath) => { + const faviconImage = document.createElement('img'); + + return new Promise((resolve) => { + faviconImage.onload = () => { + const size = 32; + + const canvas = document.createElement('canvas'); + canvas.width = size; + canvas.height = size; + + const context = canvas.getContext('2d'); + context.clearRect(0, 0, size, size); + context.drawImage( + faviconImage, 0, 0, faviconImage.width, faviconImage.height, 0, 0, size, size, + ); + + const overlayImage = document.createElement('img'); + overlayImage.onload = () => { + context.drawImage( + overlayImage, 0, 0, overlayImage.width, overlayImage.height, 0, 0, size, size, + ); + + const faviconWithOverlayUrl = canvas.toDataURL(); + + resolve(faviconWithOverlayUrl); + }; + overlayImage.src = overlayPath; + }; + faviconImage.src = iconPath; + }); +}; + +export const setFaviconOverlay = (overlayPath) => { + const faviconEl = document.getElementById('favicon'); + + if (!faviconEl) { return null; } + + const iconPath = faviconEl.getAttribute('data-original-href'); + + return createOverlayIcon(iconPath, overlayPath).then(faviconWithOverlayUrl => faviconEl.setAttribute('href', faviconWithOverlayUrl)); +}; + export const setFavicon = (faviconPath) => { const faviconEl = document.getElementById('favicon'); if (faviconEl && faviconPath) { @@ -393,8 +436,9 @@ export const setFavicon = (faviconPath) => { export const resetFavicon = () => { const faviconEl = document.getElementById('favicon'); - const originalFavicon = faviconEl ? faviconEl.getAttribute('href') : null; + if (faviconEl) { + const originalFavicon = faviconEl.getAttribute('data-original-href'); faviconEl.setAttribute('href', originalFavicon); } }; @@ -403,10 +447,9 @@ export const setCiStatusFavicon = pageUrl => axios.get(pageUrl) .then(({ data }) => { if (data && data.favicon) { - setFavicon(data.favicon); - } else { - resetFavicon(); + return setFaviconOverlay(data.favicon); } + return resetFavicon(); }) .catch(resetFavicon); diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index 0ff23bbb061..7cca32dc6fa 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -79,37 +79,37 @@ export function getTimeago() { if (!timeagoInstance) { const localeRemaining = function getLocaleRemaining(number, index) { return [ - [s__('Timeago|less than a minute ago'), s__('Timeago|right now')], - [s__('Timeago|less than a minute ago'), s__('Timeago|%s seconds remaining')], - [s__('Timeago|about a minute ago'), s__('Timeago|1 minute remaining')], + [s__('Timeago|just now'), s__('Timeago|right now')], + [s__('Timeago|%s seconds ago'), s__('Timeago|%s seconds remaining')], + [s__('Timeago|1 minute ago'), s__('Timeago|1 minute remaining')], [s__('Timeago|%s minutes ago'), s__('Timeago|%s minutes remaining')], - [s__('Timeago|about an hour ago'), s__('Timeago|1 hour remaining')], - [s__('Timeago|about %s hours ago'), s__('Timeago|%s hours remaining')], - [s__('Timeago|a day ago'), s__('Timeago|1 day remaining')], + [s__('Timeago|1 hour ago'), s__('Timeago|1 hour remaining')], + [s__('Timeago|%s hours ago'), s__('Timeago|%s hours remaining')], + [s__('Timeago|1 day ago'), s__('Timeago|1 day remaining')], [s__('Timeago|%s days ago'), s__('Timeago|%s days remaining')], - [s__('Timeago|a week ago'), s__('Timeago|1 week remaining')], + [s__('Timeago|1 week ago'), s__('Timeago|1 week remaining')], [s__('Timeago|%s weeks ago'), s__('Timeago|%s weeks remaining')], - [s__('Timeago|a month ago'), s__('Timeago|1 month remaining')], + [s__('Timeago|1 month ago'), s__('Timeago|1 month remaining')], [s__('Timeago|%s months ago'), s__('Timeago|%s months remaining')], - [s__('Timeago|a year ago'), s__('Timeago|1 year remaining')], + [s__('Timeago|1 year ago'), s__('Timeago|1 year remaining')], [s__('Timeago|%s years ago'), s__('Timeago|%s years remaining')], ][index]; }; const locale = function getLocale(number, index) { return [ - [s__('Timeago|less than a minute ago'), s__('Timeago|right now')], - [s__('Timeago|less than a minute ago'), s__('Timeago|in %s seconds')], - [s__('Timeago|about a minute ago'), s__('Timeago|in 1 minute')], + [s__('Timeago|just now'), s__('Timeago|right now')], + [s__('Timeago|%s seconds ago'), s__('Timeago|in %s seconds')], + [s__('Timeago|1 minute ago'), s__('Timeago|in 1 minute')], [s__('Timeago|%s minutes ago'), s__('Timeago|in %s minutes')], - [s__('Timeago|about an hour ago'), s__('Timeago|in 1 hour')], - [s__('Timeago|about %s hours ago'), s__('Timeago|in %s hours')], - [s__('Timeago|a day ago'), s__('Timeago|in 1 day')], + [s__('Timeago|1 hour ago'), s__('Timeago|in 1 hour')], + [s__('Timeago|%s hours ago'), s__('Timeago|in %s hours')], + [s__('Timeago|1 day ago'), s__('Timeago|in 1 day')], [s__('Timeago|%s days ago'), s__('Timeago|in %s days')], - [s__('Timeago|a week ago'), s__('Timeago|in 1 week')], + [s__('Timeago|1 week ago'), s__('Timeago|in 1 week')], [s__('Timeago|%s weeks ago'), s__('Timeago|in %s weeks')], - [s__('Timeago|a month ago'), s__('Timeago|in 1 month')], + [s__('Timeago|1 month ago'), s__('Timeago|in 1 month')], [s__('Timeago|%s months ago'), s__('Timeago|in %s months')], - [s__('Timeago|a year ago'), s__('Timeago|in 1 year')], + [s__('Timeago|1 year ago'), s__('Timeago|in 1 year')], [s__('Timeago|%s years ago'), s__('Timeago|in %s years')], ][index]; }; @@ -270,6 +270,17 @@ export const totalDaysInMonth = date => { }; /** + * Returns number of days in a quarter from provided + * months array. + * + * @param {Array} quarter + */ +export const totalDaysInQuarter = quarter => quarter.reduce( + (acc, month) => acc + totalDaysInMonth(month), + 0, +); + +/** * Returns list of Dates referring to Sundays of the month * based on provided date * @@ -309,42 +320,27 @@ export const getSundays = date => { }; /** - * Returns list of Dates representing a timeframe of Months from month of provided date (inclusive) - * up to provided length - * - * For eg; - * If current month is January 2018 and `length` provided is `6` - * Then this method will return list of Date objects as follows; - * - * [ October 2017, November 2017, December 2017, January 2018, February 2018, March 2018 ] - * - * If current month is March 2018 and `length` provided is `3` - * Then this method will return list of Date objects as follows; - * - * [ February 2018, March 2018, April 2018 ] + * Returns list of Dates representing a timeframe of months from startDate and length * + * @param {Date} startDate * @param {Number} length - * @param {Date} date */ -export const getTimeframeWindow = (length, date) => { - if (!length) { +export const getTimeframeWindowFrom = (startDate, length) => { + if (!(startDate instanceof Date) || !length) { return []; } - const currentDate = date instanceof Date ? date : new Date(); - const currentMonthIndex = Math.floor(length / 2); - const timeframe = []; - - // Move date object backward to the first month of timeframe - currentDate.setDate(1); - currentDate.setMonth(currentDate.getMonth() - currentMonthIndex); - - // Iterate and update date for the size of length + // Iterate and set date for the size of length // and push date reference to timeframe list - for (let i = 0; i < length; i += 1) { - timeframe.push(new Date(currentDate.getTime())); - currentDate.setMonth(currentDate.getMonth() + 1); - } + const timeframe = new Array(length) + .fill() + .map( + (val, i) => new Date( + startDate.getFullYear(), + startDate.getMonth() + i, + 1, + ), + ); // Change date of last timeframe item to last date of the month timeframe[length - 1].setDate(totalDaysInMonth(timeframe[length - 1])); @@ -352,6 +348,29 @@ export const getTimeframeWindow = (length, date) => { return timeframe; }; +/** + * Returns count of day within current quarter from provided date + * and array of months for the quarter + * + * Eg; + * If date is 15 Feb 2018 + * and quarter is [Jan, Feb, Mar] + * + * Then 15th Feb is 46th day of the quarter + * Where 31 (days in Jan) + 15 (date of Feb). + * + * @param {Date} date + * @param {Array} quarter + */ +export const dayInQuarter = (date, quarter) => quarter.reduce((acc, month) => { + if (date.getMonth() > month.getMonth()) { + return acc + totalDaysInMonth(month); + } else if (date.getMonth() === month.getMonth()) { + return acc + date.getDate(); + } + return acc + 0; +}, 0); + window.gl = window.gl || {}; window.gl.utils = { ...(window.gl.utils || {}), diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js index dd17544b656..72b72f4247d 100644 --- a/app/assets/javascripts/lib/utils/url_utility.js +++ b/app/assets/javascripts/lib/utils/url_utility.js @@ -85,9 +85,9 @@ export function redirectTo(url) { } export function webIDEUrl(route = undefined) { - let returnUrl = `${gon.relative_url_root}/-/ide/`; + let returnUrl = `${gon.relative_url_root || ''}/-/ide/`; if (route) { - returnUrl += `project${route}`; + returnUrl += `project${route.replace(new RegExp(`^${gon.relative_url_root || ''}`), '')}`; } return returnUrl; } diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 9803bebfd10..c9ce838cd48 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -144,6 +144,7 @@ document.addEventListener('DOMContentLoaded', () => { $body.tooltip({ selector: '.has-tooltip, [data-toggle="tooltip"]', trigger: 'hover', + boundary: 'viewport', placement(tip, el) { return $(el).data('placement') || 'bottom'; }, diff --git a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js index 6c2a785c0af..37ef77c8e43 100644 --- a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js +++ b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js @@ -22,4 +22,18 @@ document.addEventListener('DOMContentLoaded', () => { errorBox: variableListEl.querySelector('.js-ci-variable-error-box'), saveEndpoint: variableListEl.dataset.saveEndpoint, }); + + // hide extra auto devops settings based on data-attributes + const autoDevOpsSettings = document.querySelector('.js-auto-devops-settings'); + const autoDevOpsExtraSettings = document.querySelector('.js-extra-settings'); + + autoDevOpsSettings.addEventListener('click', event => { + const target = event.target; + if (target.classList.contains('js-toggle-extra-settings')) { + autoDevOpsExtraSettings.classList.toggle( + 'hidden', + !!(target.dataset && target.dataset.hideExtraSettings), + ); + } + }); }); 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 6f79310b1cc..0e139cd7f5e 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue @@ -1,8 +1,12 @@ <script> import { parseSeconds, stringifyTime } from '../../../lib/utils/pretty_time'; +import tooltip from '../../../vue_shared/directives/tooltip'; export default { name: 'TimeTrackingComparisonPane', + directives: { + tooltip, + }, props: { timeSpent: { type: Number, @@ -51,17 +55,12 @@ export default { <div class="time-tracking-comparison-pane"> <div class="compare-meter" - data-toggle="tooltip" - data-placement="top" - role="timeRemainingDisplay" - :aria-valuenow="timeRemainingTooltip" :title="timeRemainingTooltip" - :data-original-title="timeRemainingTooltip" + v-tooltip :class="timeRemainingStatusClass" > <div class="meter-container" - role="timeSpentPercent" :aria-valuenow="timeRemainingPercent" > <div diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue index c20d07a169d..098e8178265 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue +++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue @@ -36,7 +36,7 @@ import { notify, SourceBranchRemovalStatus, } from './dependencies'; -import { setFavicon } from '../lib/utils/common_utils'; +import { setFaviconOverlay } from '../lib/utils/common_utils'; export default { el: '#js-vue-mr-widget', @@ -159,8 +159,9 @@ export default { }, setFaviconHelper() { if (this.mr.ciStatusFaviconPath) { - setFavicon(this.mr.ciStatusFaviconPath); + return setFaviconOverlay(this.mr.ciStatusFaviconPath); } + return Promise.resolve(); }, fetchDeployments() { return this.service.fetchDeployments() diff --git a/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js b/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js index 9ffbaae3ea5..9bca1993ccc 100644 --- a/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js +++ b/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js @@ -513,7 +513,7 @@ const fileNameIcons = { 'credits.md': 'credits', 'credits.md.rendered': 'credits', '.flowconfig': 'flow', - 'favicon.ico': 'favicon', + 'favicon.png': 'favicon', 'karma.conf.js': 'karma', 'karma.conf.ts': 'karma', 'karma.conf.coffee': 'karma', diff --git a/app/assets/javascripts/vue_shared/models/assignee.js b/app/assets/javascripts/vue_shared/models/assignee.js new file mode 100644 index 00000000000..4a29b0d0581 --- /dev/null +++ b/app/assets/javascripts/vue_shared/models/assignee.js @@ -0,0 +1,13 @@ +export default class ListAssignee { + constructor(obj, defaultAvatar) { + this.id = obj.id; + this.name = obj.name; + this.username = obj.username; + this.avatar = obj.avatar_url || obj.avatar || defaultAvatar; + this.path = obj.path; + this.state = obj.state; + this.webUrl = obj.web_url || obj.webUrl; + } +} + +window.ListAssignee = ListAssignee; diff --git a/app/assets/stylesheets/bootstrap_migration.scss b/app/assets/stylesheets/bootstrap_migration.scss index 79f580546c3..e3c63ae5e1a 100644 --- a/app/assets/stylesheets/bootstrap_migration.scss +++ b/app/assets/stylesheets/bootstrap_migration.scss @@ -107,7 +107,7 @@ code { background-color: $red-100; border-radius: 3px; - .code & { + .code > & { background-color: inherit; padding: unset; } @@ -118,10 +118,6 @@ code { } } -.code { - padding: 9.5px; -} - table { // Remove any table border lines border-spacing: 0; @@ -233,6 +229,13 @@ table { } } +.card-header { + h3.card-title, + h4.card-title { + margin-top: 0; + } +} + .nav-tabs { // Override bootstrap's default border border-bottom: 0; @@ -261,3 +264,7 @@ pre code { color: $white-light; } } + +input[type=color].form-control { + height: $input-height; +} diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index 0115f542c88..88b174491dd 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -497,6 +497,10 @@ fieldset[disabled] .btn, } } +[readonly] { + cursor: default; +} + .btn-no-padding { padding: 0; } diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index f2ac77819d5..6fbc624dee4 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -139,6 +139,8 @@ } .nav { + flex-wrap: nowrap; + > li:not(.d-none) a { @include media-breakpoint-down(xs) { margin-left: 0; @@ -158,11 +160,12 @@ } .navbar-toggler { + position: relative; right: -10px; border-radius: 0; min-width: 45px; padding: 0; - margin-right: -7px; + margin: $gl-padding-8 -7px $gl-padding-8 0; font-size: 14px; text-align: center; color: currentColor; @@ -186,6 +189,7 @@ display: -webkit-flex; display: flex; padding-right: 10px; + flex-direction: row; } li { @@ -290,6 +294,10 @@ margin: 8px; } } + + .dropdown-menu { + position: absolute; + } } .navbar-sub-nav { diff --git a/app/assets/stylesheets/framework/tables.scss b/app/assets/stylesheets/framework/tables.scss index 10c23f6c407..6e1758d7677 100644 --- a/app/assets/stylesheets/framework/tables.scss +++ b/app/assets/stylesheets/framework/tables.scss @@ -39,6 +39,11 @@ table { &.wide { width: 55%; } + + &.table-th-transparent { + background: none; + color: $gl-text-color-secondary; + } } td { @@ -46,9 +51,86 @@ table { } } } + + &.responsive-table { + @include media-breakpoint-down(sm) { + thead { + display: none; + } + + td { + display: block; + color: $gl-text-color-secondary; + } + + tbody td.responsive-table-cell { + padding: $gl-padding 0; + width: 100%; + display: flex; + text-align: right; + align-items: center; + justify-content: space-between; + + &[data-column]::before { + content: attr(data-column); + display: block; + text-align: left; + padding-right: $gl-padding; + color: $gl-text-color-secondary; + } + + &:not([data-column]) { + flex-direction: row-reverse; + } + } + + tr.responsive-table-border-start, + tr.responsive-table-border-end { + display: block; + border: solid $gl-text-color-quaternary; + padding-left: 0; + padding-right: 0; + + > td { + border-color: $gl-text-color-quaternary; + + &, + &:last-child { + padding-left: $gl-padding; + padding-right: $gl-padding; + } + } + } + + tr.responsive-table-border-start { + border-width: 1px 1px 0; + border-radius: $border-radius-default $border-radius-default 0 0; + padding-top: 0; + padding-bottom: 0; + + > td:first-child { + border-top: 0; // always have the <table> top border + } + + > td:last-child { + border-bottom: 1px solid $gl-text-color-quaternary; + } + } + + tr.responsive-table-border-end { + border-width: 0 1px 1px; + border-radius: 0 0 $border-radius-default $border-radius-default; + margin-bottom: 2 * $gl-padding; + + > :last-child { + border-bottom: 0; + } + } + } + } } -.responsive-table { +.responsive-table:not(table) { @include media-breakpoint-down(sm) { th { width: 100%; diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index dd7374c503e..497261f938f 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -174,11 +174,6 @@ $border-gray-normal-dashed: darken($gray-normal, $darken-border-dashed-factor); $border-gray-dark: darken($white-normal, $darken-border-factor); /* - * Override Bootstrap 4 variables - */ -$secondary: $gray-light; - -/* * UI elements */ $border-color: #e5e5e5; @@ -810,3 +805,14 @@ Prometheus $prometheus-table-row-highlight-color: $theme-gray-100; $priority-label-empty-state-width: 114px; + +/* + * Override Bootstrap 4 variables + */ + +$secondary: $gray-light; +$input-disabled-bg: $gray-light; +$input-border-color: $theme-gray-200; +$input-color: $gl-text-color; +$font-family-sans-serif: $regular_font; +$font-family-monospace: $monospace_font; diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 9213ccd4cdf..f030189af06 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -12,26 +12,22 @@ @keyframes blinking-dots { 0% { background-color: rgba($white-light, 1); - box-shadow: 12px 0 0 0 rgba($white-light, 0.2), - 24px 0 0 0 rgba($white-light, 0.2); + box-shadow: 12px 0 0 0 rgba($white-light, 0.2), 24px 0 0 0 rgba($white-light, 0.2); } 25% { background-color: rgba($white-light, 0.4); - box-shadow: 12px 0 0 0 rgba($white-light, 2), - 24px 0 0 0 rgba($white-light, 0.2); + box-shadow: 12px 0 0 0 rgba($white-light, 2), 24px 0 0 0 rgba($white-light, 0.2); } 75% { background-color: rgba($white-light, 0.4); - box-shadow: 12px 0 0 0 rgba($white-light, 0.2), - 24px 0 0 0 rgba($white-light, 1); + box-shadow: 12px 0 0 0 rgba($white-light, 0.2), 24px 0 0 0 rgba($white-light, 1); } 100% { background-color: rgba($white-light, 1); - box-shadow: 12px 0 0 0 rgba($white-light, 0.2), - 24px 0 0 0 rgba($white-light, 0.2); + box-shadow: 12px 0 0 0 rgba($white-light, 0.2), 24px 0 0 0 rgba($white-light, 0.2); } } @@ -71,6 +67,10 @@ .bash { display: block; } + + &.build-trace-rounded { + border-radius: $border-radius-base; + } } .top-bar { diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index f85f66b9c0b..30428fd198d 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -321,18 +321,17 @@ } .build-failures { + th { + border-top: 0; + } + .build-state { padding: 20px 2px; .build-name { - float: right; font-weight: $gl-font-weight-normal; } - .ci-status-icon-failed svg { - vertical-align: middle; - } - .stage { color: $gl-text-color-secondary; font-weight: $gl-font-weight-normal; @@ -344,6 +343,81 @@ border: 0; line-height: initial; } + + .build-trace-row td { + border-top: 0; + border-bottom-width: 1px; + border-bottom-style: solid; + padding-top: 0; + } + + .build-trace { + width: 100%; + text-align: left; + margin-top: $gl-padding; + } + + .build-name { + width: 196px; + + a { + font-weight: $gl-font-weight-bold; + color: $gl-text-color; + text-decoration: none; + + &:focus, + &:hover { + text-decoration: underline; + } + } + } + + .build-actions { + width: 70px; + text-align: right; + } + + .build-stage { + width: 140px; + } + + .ci-status-icon-failed { + padding: 10px 0 10px 12px; + width: 12px + 24px; // padding-left + svg width + } + + .build-icon svg { + width: 24px; + height: 24px; + vertical-align: middle; + } + + .build-state, + .build-trace-row { + > td:last-child { + padding-right: 0; + } + } + + @include media-breakpoint-down(sm) { + td:empty { + display: none; + } + + .ci-table { + margin-top: 2 * $gl-padding; + } + + .build-trace-container { + padding-top: $gl-padding; + padding-bottom: $gl-padding; + } + + .build-trace { + margin-bottom: 0; + margin-top: 0; + } + } } .pipeline-tab-content { @@ -929,7 +1003,7 @@ button.mini-pipeline-graph-dropdown-toggle { &.dropdown-menu { transform: translate(-80%, 0); - @media(min-width: map-get($grid-breakpoints, md)) { + @media (min-width: map-get($grid-breakpoints, md)) { transform: translate(-50%, 0); right: auto; left: 50%; diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index a35c4ff7c80..5f15795a8e3 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -18,7 +18,8 @@ .file-finder-input:hover, .issuable-search-form:hover, .search-text-input:hover, -.form-control:hover { +.form-control:hover, +:not[readonly] { border-color: lighten($dropdown-input-focus-border, 20%); box-shadow: 0 0 4px lighten($search-input-focus-shadow-color, 20%); } diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss index 16e999341da..1f8e61257a9 100644 --- a/app/assets/stylesheets/pages/settings.scss +++ b/app/assets/stylesheets/pages/settings.scss @@ -127,12 +127,16 @@ color: $gl-danger; } -.service-settings .form-control-label { - padding-top: 0; +.service-settings { + input[type="radio"], + input[type="checkbox"] { + margin-top: 10px; + } } .integration-settings-form { - .card.card-body { + .card.card-body, + .info-well { padding: $gl-padding / 2; box-shadow: none; } diff --git a/app/assets/stylesheets/pages/settings_ci_cd.scss b/app/assets/stylesheets/pages/settings_ci_cd.scss index a355e2dee24..777fdb3581e 100644 --- a/app/assets/stylesheets/pages/settings_ci_cd.scss +++ b/app/assets/stylesheets/pages/settings_ci_cd.scss @@ -16,3 +16,12 @@ .registry-placeholder { min-height: 60px; } + +.auto-devops-card { + margin-bottom: $gl-vert-padding; + + > .card-body { + border-radius: $card-border-radius; + padding: $gl-padding $gl-padding-24; + } +} diff --git a/app/assets/stylesheets/performance_bar.scss b/app/assets/stylesheets/performance_bar.scss index 06ef58531d7..8cdf2275551 100644 --- a/app/assets/stylesheets/performance_bar.scss +++ b/app/assets/stylesheets/performance_bar.scss @@ -15,6 +15,7 @@ color: $perf-bar-text; select { + color: $perf-bar-text; width: 200px; } |