diff options
Diffstat (limited to 'app')
79 files changed, 945 insertions, 275 deletions
diff --git a/app/assets/javascripts/autosave.js b/app/assets/javascripts/autosave.js index 8630b18a73f..cfab6c40b34 100644 --- a/app/assets/javascripts/autosave.js +++ b/app/assets/javascripts/autosave.js @@ -1,8 +1,11 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-param-reassign, quotes, prefer-template, no-var, one-var, no-unused-vars, one-var-declaration-per-line, no-void, consistent-return, no-empty, max-len */ +import AccessorUtilities from './lib/utils/accessor'; window.Autosave = (function() { function Autosave(field, key) { this.field = field; + this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe(); + if (key.join != null) { key = key.join("/"); } @@ -17,16 +20,12 @@ window.Autosave = (function() { } Autosave.prototype.restore = function() { - var e, text; - if (window.localStorage == null) { - return; - } - try { - text = window.localStorage.getItem(this.key); - } catch (error) { - e = error; - return; - } + var text; + + if (!this.isLocalStorageAvailable) return; + + text = window.localStorage.getItem(this.key); + if ((text != null ? text.length : void 0) > 0) { this.field.val(text); } @@ -35,27 +34,22 @@ window.Autosave = (function() { Autosave.prototype.save = function() { var text; - if (window.localStorage == null) { - return; - } text = this.field.val(); - if ((text != null ? text.length : void 0) > 0) { - try { - return window.localStorage.setItem(this.key, text); - } catch (error) {} - } else { - return this.reset(); + + if (this.isLocalStorageAvailable && (text != null ? text.length : void 0) > 0) { + return window.localStorage.setItem(this.key, text); } + + return this.reset(); }; Autosave.prototype.reset = function() { - if (window.localStorage == null) { - return; - } - try { - return window.localStorage.removeItem(this.key); - } catch (error) {} + if (!this.isLocalStorageAvailable) return; + + return window.localStorage.removeItem(this.key); }; return Autosave; })(); + +export default window.Autosave; diff --git a/app/assets/javascripts/behaviors/gl_emoji/unicode_support_map.js b/app/assets/javascripts/behaviors/gl_emoji/unicode_support_map.js index aa522e20c36..257df55e54f 100644 --- a/app/assets/javascripts/behaviors/gl_emoji/unicode_support_map.js +++ b/app/assets/javascripts/behaviors/gl_emoji/unicode_support_map.js @@ -1,3 +1,5 @@ +import AccessorUtilities from '../../lib/utils/accessor'; + const unicodeSupportTestMap = { // man, student (emojione does not have any of these yet), http://emojipedia.org/emoji-zwj-sequences/ // occupationZwj: '\u{1F468}\u{200D}\u{1F393}', @@ -140,16 +142,25 @@ function generateUnicodeSupportMap(testMap) { function getUnicodeSupportMap() { let unicodeSupportMap; - const userAgentFromCache = window.localStorage.getItem('gl-emoji-user-agent'); + let userAgentFromCache; + + const isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe(); + + if (isLocalStorageAvailable) userAgentFromCache = window.localStorage.getItem('gl-emoji-user-agent'); + try { unicodeSupportMap = JSON.parse(window.localStorage.getItem('gl-emoji-unicode-support-map')); } catch (err) { // swallow } + if (!unicodeSupportMap || userAgentFromCache !== navigator.userAgent) { unicodeSupportMap = generateUnicodeSupportMap(unicodeSupportTestMap); - window.localStorage.setItem('gl-emoji-user-agent', navigator.userAgent); - window.localStorage.setItem('gl-emoji-unicode-support-map', JSON.stringify(unicodeSupportMap)); + + if (isLocalStorageAvailable) { + window.localStorage.setItem('gl-emoji-user-agent', navigator.userAgent); + window.localStorage.setItem('gl-emoji-unicode-support-map', JSON.stringify(unicodeSupportMap)); + } } return unicodeSupportMap; diff --git a/app/assets/javascripts/blob/balsamiq/balsamiq_viewer.js b/app/assets/javascripts/blob/balsamiq/balsamiq_viewer.js new file mode 100644 index 00000000000..cdbfe36ca1c --- /dev/null +++ b/app/assets/javascripts/blob/balsamiq/balsamiq_viewer.js @@ -0,0 +1,114 @@ +/* global Flash */ + +import sqljs from 'sql.js'; +import { template as _template } from 'underscore'; + +const PREVIEW_TEMPLATE = _template(` + <div class="panel panel-default"> + <div class="panel-heading"><%- name %></div> + <div class="panel-body"> + <img class="img-thumbnail" src="data:image/png;base64,<%- image %>"/> + </div> + </div> +`); + +class BalsamiqViewer { + constructor(viewer) { + this.viewer = viewer; + this.endpoint = this.viewer.dataset.endpoint; + } + + loadFile() { + const xhr = new XMLHttpRequest(); + + xhr.open('GET', this.endpoint, true); + xhr.responseType = 'arraybuffer'; + + xhr.onload = this.renderFile.bind(this); + xhr.onerror = BalsamiqViewer.onError; + + xhr.send(); + } + + renderFile(loadEvent) { + const container = document.createElement('ul'); + + this.initDatabase(loadEvent.target.response); + + const previews = this.getPreviews(); + previews.forEach((preview) => { + const renderedPreview = this.renderPreview(preview); + + container.appendChild(renderedPreview); + }); + + container.classList.add('list-inline'); + container.classList.add('previews'); + + this.viewer.appendChild(container); + } + + initDatabase(data) { + const previewBinary = new Uint8Array(data); + + this.database = new sqljs.Database(previewBinary); + } + + getPreviews() { + const thumbnails = this.database.exec('SELECT * FROM thumbnails'); + + return thumbnails[0].values.map(BalsamiqViewer.parsePreview); + } + + getResource(resourceID) { + const resources = this.database.exec(`SELECT * FROM resources WHERE id = '${resourceID}'`); + + return resources[0]; + } + + renderPreview(preview) { + const previewElement = document.createElement('li'); + + previewElement.classList.add('preview'); + previewElement.innerHTML = this.renderTemplate(preview); + + return previewElement; + } + + renderTemplate(preview) { + const resource = this.getResource(preview.resourceID); + const name = BalsamiqViewer.parseTitle(resource); + const image = preview.image; + + const template = PREVIEW_TEMPLATE({ + name, + image, + }); + + return template; + } + + static parsePreview(preview) { + return JSON.parse(preview[1]); + } + + /* + * resource = { + * columns: ['ID', 'BRANCHID', 'ATTRIBUTES', 'DATA'], + * values: [['id', 'branchId', 'attributes', 'data']], + * } + * + * 'attributes' being a JSON string containing the `name` property. + */ + static parseTitle(resource) { + return JSON.parse(resource.values[0][2]).name; + } + + static onError() { + const flash = new Flash('Balsamiq file could not be loaded.'); + + return flash; + } +} + +export default BalsamiqViewer; diff --git a/app/assets/javascripts/blob/balsamiq_viewer.js b/app/assets/javascripts/blob/balsamiq_viewer.js new file mode 100644 index 00000000000..1dacf84470f --- /dev/null +++ b/app/assets/javascripts/blob/balsamiq_viewer.js @@ -0,0 +1,6 @@ +import BalsamiqViewer from './balsamiq/balsamiq_viewer'; + +document.addEventListener('DOMContentLoaded', () => { + const balsamiqViewer = new BalsamiqViewer(document.getElementById('js-balsamiq-viewer')); + balsamiqViewer.loadFile(); +}); diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index b87c57c38fe..b16ff2a0221 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -252,6 +252,7 @@ const ShortcutsBlob = require('./shortcuts_blob'); } break; case 'projects:pipelines:builds': + case 'projects:pipelines:failures': case 'projects:pipelines:show': const { controllerAction } = document.querySelector('.js-pipeline-container').dataset; const pipelineStatusUrl = `${document.querySelector('.js-pipeline-tab-link a').getAttribute('href')}/status.json`; diff --git a/app/assets/javascripts/droplab/constants.js b/app/assets/javascripts/droplab/constants.js index 8883ed9aa14..868d47e91b3 100644 --- a/app/assets/javascripts/droplab/constants.js +++ b/app/assets/javascripts/droplab/constants.js @@ -3,11 +3,14 @@ const DATA_DROPDOWN = 'data-dropdown'; const SELECTED_CLASS = 'droplab-item-selected'; const ACTIVE_CLASS = 'droplab-item-active'; const IGNORE_CLASS = 'droplab-item-ignore'; +// Matches `{{anything}}` and `{{ everything }}`. +const TEMPLATE_REGEX = /\{\{(.+?)\}\}/g; export { DATA_TRIGGER, DATA_DROPDOWN, SELECTED_CLASS, ACTIVE_CLASS, + TEMPLATE_REGEX, IGNORE_CLASS, }; diff --git a/app/assets/javascripts/droplab/drop_down.js b/app/assets/javascripts/droplab/drop_down.js index 1fb4d63923c..de3927d683c 100644 --- a/app/assets/javascripts/droplab/drop_down.js +++ b/app/assets/javascripts/droplab/drop_down.js @@ -94,7 +94,7 @@ Object.assign(DropDown.prototype, { }, renderChildren: function(data) { - var html = utils.t(this.templateString, data); + var html = utils.template(this.templateString, data); var template = document.createElement('div'); template.innerHTML = html; diff --git a/app/assets/javascripts/droplab/utils.js b/app/assets/javascripts/droplab/utils.js index c149a33a1e9..4da7344604e 100644 --- a/app/assets/javascripts/droplab/utils.js +++ b/app/assets/javascripts/droplab/utils.js @@ -1,19 +1,19 @@ /* eslint-disable */ -import { DATA_TRIGGER, DATA_DROPDOWN } from './constants'; +import { template as _template } from 'underscore'; +import { DATA_TRIGGER, DATA_DROPDOWN, TEMPLATE_REGEX } from './constants'; const utils = { toCamelCase(attr) { return this.camelize(attr.split('-').slice(1).join(' ')); }, - t(s, d) { - for (const p in d) { - if (Object.prototype.hasOwnProperty.call(d, p)) { - s = s.replace(new RegExp(`{{${p}}}`, 'g'), d[p]); - } - } - return s; + template(templateString, data) { + const template = _template(templateString, { + escape: TEMPLATE_REGEX, + }); + + return template(data); }, camelize(str) { diff --git a/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.js b/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.js index 9126422b335..15052dbd362 100644 --- a/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.js +++ b/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.js @@ -8,6 +8,11 @@ export default { type: Array, required: true, }, + isLocalStorageAvailable: { + type: Boolean, + required: false, + default: true, + }, }, computed: { @@ -47,7 +52,12 @@ export default { template: ` <div> - <ul v-if="hasItems"> + <div + v-if="!isLocalStorageAvailable" + class="dropdown-info-note"> + This feature requires local storage to be enabled + </div> + <ul v-else-if="hasItems"> <li v-for="(item, index) in processedItems" :key="index"> diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js b/app/assets/javascripts/filtered_search/dropdown_hint.js index 3e7a892756c..5e9434fd48f 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js @@ -62,7 +62,7 @@ class DropdownHint extends gl.FilteredSearchDropdown { Object.assign({ icon: `fa-${icon}`, hint, - tag: `<${tag}>`, + tag: `<${tag}>`, }, type && { type }), ); } diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js index 36af0674ac6..9fea563370f 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js @@ -1,5 +1,3 @@ -/* global Flash */ - import FilteredSearchContainer from './container'; import RecentSearchesRoot from './recent_searches_root'; import RecentSearchesStore from './stores/recent_searches_store'; @@ -15,7 +13,9 @@ class FilteredSearchManager { this.tokensContainer = this.container.querySelector('.tokens-container'); this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys; - this.recentSearchesStore = new RecentSearchesStore(); + this.recentSearchesStore = new RecentSearchesStore({ + isLocalStorageAvailable: RecentSearchesService.isAvailable(), + }); let recentSearchesKey = 'issue-recent-searches'; if (page === 'merge_requests') { recentSearchesKey = 'merge-request-recent-searches'; @@ -24,9 +24,10 @@ class FilteredSearchManager { // Fetch recent searches from localStorage this.fetchingRecentSearchesPromise = this.recentSearchesService.fetch() - .catch(() => { + .catch((error) => { + if (error.name === 'RecentSearchesServiceError') return undefined; // eslint-disable-next-line no-new - new Flash('An error occured while parsing recent searches'); + new window.Flash('An error occured while parsing recent searches'); // Gracefully fail to empty array return []; }) diff --git a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js index 453ecccc6fc..59ce0587e1e 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js +++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js @@ -183,6 +183,9 @@ class FilteredSearchVisualTokens { static moveInputToTheRight() { const input = FilteredSearchContainer.container.querySelector('.filtered-search'); + + if (!input) return; + const inputLi = input.parentElement; const tokenContainer = FilteredSearchContainer.container.querySelector('.tokens-container'); diff --git a/app/assets/javascripts/filtered_search/recent_searches_root.js b/app/assets/javascripts/filtered_search/recent_searches_root.js index 4e38409e12a..b2e6f63aacf 100644 --- a/app/assets/javascripts/filtered_search/recent_searches_root.js +++ b/app/assets/javascripts/filtered_search/recent_searches_root.js @@ -29,12 +29,15 @@ class RecentSearchesRoot { } render() { + const state = this.store.state; this.vm = new Vue({ el: this.wrapperElement, - data: this.store.state, + data() { return state; }, template: ` <recent-searches-dropdown-content - :items="recentSearches" /> + :items="recentSearches" + :is-local-storage-available="isLocalStorageAvailable" + /> `, components: { 'recent-searches-dropdown-content': RecentSearchesDropdownContent, diff --git a/app/assets/javascripts/filtered_search/services/recent_searches_service.js b/app/assets/javascripts/filtered_search/services/recent_searches_service.js index 3e402d5aed0..a056dea928d 100644 --- a/app/assets/javascripts/filtered_search/services/recent_searches_service.js +++ b/app/assets/javascripts/filtered_search/services/recent_searches_service.js @@ -1,9 +1,17 @@ +import RecentSearchesServiceError from './recent_searches_service_error'; +import AccessorUtilities from '../../lib/utils/accessor'; + class RecentSearchesService { constructor(localStorageKey = 'issuable-recent-searches') { this.localStorageKey = localStorageKey; } fetch() { + if (!RecentSearchesService.isAvailable()) { + const error = new RecentSearchesServiceError(); + return Promise.reject(error); + } + const input = window.localStorage.getItem(this.localStorageKey); let searches = []; @@ -19,8 +27,14 @@ class RecentSearchesService { } save(searches = []) { + if (!RecentSearchesService.isAvailable()) return; + window.localStorage.setItem(this.localStorageKey, JSON.stringify(searches)); } + + static isAvailable() { + return AccessorUtilities.isLocalStorageAccessSafe(); + } } export default RecentSearchesService; diff --git a/app/assets/javascripts/filtered_search/services/recent_searches_service_error.js b/app/assets/javascripts/filtered_search/services/recent_searches_service_error.js new file mode 100644 index 00000000000..5917b223d63 --- /dev/null +++ b/app/assets/javascripts/filtered_search/services/recent_searches_service_error.js @@ -0,0 +1,11 @@ +class RecentSearchesServiceError { + constructor(message) { + this.name = 'RecentSearchesServiceError'; + this.message = message || 'Recent Searches Service is unavailable'; + } +} + +// Can't use `extends` for builtin prototypes and get true inheritance yet +RecentSearchesServiceError.prototype = Error.prototype; + +export default RecentSearchesServiceError; diff --git a/app/assets/javascripts/issue_show/actions/tasks.js b/app/assets/javascripts/issue_show/actions/tasks.js new file mode 100644 index 00000000000..0740a9f559c --- /dev/null +++ b/app/assets/javascripts/issue_show/actions/tasks.js @@ -0,0 +1,27 @@ +export default (newStateData, tasks) => { + const $tasks = $('#task_status'); + const $tasksShort = $('#task_status_short'); + const $issueableHeader = $('.issuable-header'); + const tasksStates = { newState: null, currentState: null }; + + if ($tasks.length === 0) { + if (!(newStateData.task_status.indexOf('0 of 0') === 0)) { + $issueableHeader.append(`<span id="task_status">${newStateData.task_status}</span>`); + } else { + $issueableHeader.append('<span id="task_status"></span>'); + } + } else { + tasksStates.newState = newStateData.task_status.indexOf('0 of 0') === 0; + tasksStates.currentState = tasks.indexOf('0 of 0') === 0; + } + + if ($tasks.length !== 0 && !tasksStates.newState) { + $tasks.text(newStateData.task_status); + $tasksShort.text(newStateData.task_status); + } else if (tasksStates.currentState) { + $issueableHeader.append(`<span id="task_status">${newStateData.task_status}</span>`); + } else if (tasksStates.newState) { + $tasks.remove(); + $tasksShort.remove(); + } +}; diff --git a/app/assets/javascripts/issue_show/index.js b/app/assets/javascripts/issue_show/index.js index 4d491e70d83..eb20a597bb5 100644 --- a/app/assets/javascripts/issue_show/index.js +++ b/app/assets/javascripts/issue_show/index.js @@ -1,16 +1,16 @@ import Vue from 'vue'; -import IssueTitle from './issue_title.vue'; +import IssueTitle from './issue_title_description.vue'; import '../vue_shared/vue_resource_interceptor'; (() => { const issueTitleData = document.querySelector('.issue-title-data').dataset; - const { initialTitle, endpoint } = issueTitleData; + const { canUpdateTasksClass, endpoint } = issueTitleData; const vm = new Vue({ el: '.issue-title-entrypoint', render: createElement => createElement(IssueTitle, { props: { - initialTitle, + canUpdateTasksClass, endpoint, }, }), diff --git a/app/assets/javascripts/issue_show/issue_title.vue b/app/assets/javascripts/issue_show/issue_title.vue deleted file mode 100644 index 00b0e56030a..00000000000 --- a/app/assets/javascripts/issue_show/issue_title.vue +++ /dev/null @@ -1,80 +0,0 @@ -<script> -import Visibility from 'visibilityjs'; -import Poll from './../lib/utils/poll'; -import Service from './services/index'; - -export default { - props: { - initialTitle: { required: true, type: String }, - endpoint: { required: true, type: String }, - }, - data() { - const resource = new Service(this.$http, this.endpoint); - - const poll = new Poll({ - resource, - method: 'getTitle', - successCallback: (res) => { - this.renderResponse(res); - }, - errorCallback: (err) => { - if (process.env.NODE_ENV !== 'production') { - // eslint-disable-next-line no-console - console.error('ISSUE SHOW TITLE REALTIME ERROR', err); - } else { - throw new Error(err); - } - }, - }); - - return { - poll, - timeoutId: null, - title: this.initialTitle, - }; - }, - methods: { - renderResponse(res) { - const body = JSON.parse(res.body); - this.triggerAnimation(body); - }, - triggerAnimation(body) { - const { title } = body; - - /** - * since opacity is changed, even if there is no diff for Vue to update - * we must check the title even on a 304 to ensure no visual change - */ - if (this.title === title) return; - - this.$el.style.opacity = 0; - - this.timeoutId = setTimeout(() => { - this.title = title; - - this.$el.style.transition = 'opacity 0.2s ease'; - this.$el.style.opacity = 1; - - clearTimeout(this.timeoutId); - }, 100); - }, - }, - created() { - if (!Visibility.hidden()) { - this.poll.makeRequest(); - } - - Visibility.change(() => { - if (!Visibility.hidden()) { - this.poll.restart(); - } else { - this.poll.stop(); - } - }); - }, -}; -</script> - -<template> - <h2 class="title" v-html="title"></h2> -</template> diff --git a/app/assets/javascripts/issue_show/issue_title_description.vue b/app/assets/javascripts/issue_show/issue_title_description.vue new file mode 100644 index 00000000000..dc3ba2550c5 --- /dev/null +++ b/app/assets/javascripts/issue_show/issue_title_description.vue @@ -0,0 +1,180 @@ +<script> +import Visibility from 'visibilityjs'; +import Poll from './../lib/utils/poll'; +import Service from './services/index'; +import tasks from './actions/tasks'; + +export default { + props: { + endpoint: { + required: true, + type: String, + }, + canUpdateTasksClass: { + required: true, + type: String, + }, + }, + data() { + const resource = new Service(this.$http, this.endpoint); + + const poll = new Poll({ + resource, + method: 'getTitle', + successCallback: (res) => { + this.renderResponse(res); + }, + errorCallback: (err) => { + throw new Error(err); + }, + }); + + return { + poll, + apiData: {}, + tasks: '0 of 0', + title: null, + titleText: '', + titleFlag: { + pre: true, + pulse: false, + }, + description: null, + descriptionText: '', + descriptionChange: false, + descriptionFlag: { + pre: true, + pulse: false, + }, + timeAgoEl: $('.issue_edited_ago'), + titleEl: document.querySelector('title'), + }; + }, + methods: { + updateFlag(key, toggle) { + this[key].pre = toggle; + this[key].pulse = !toggle; + }, + renderResponse(res) { + this.apiData = res.json(); + this.triggerAnimation(); + }, + updateTaskHTML() { + tasks(this.apiData, this.tasks); + }, + elementsToVisualize(noTitleChange, noDescriptionChange) { + if (!noTitleChange) { + this.titleText = this.apiData.title_text; + this.updateFlag('titleFlag', true); + } + + if (!noDescriptionChange) { + // only change to true when we need to bind TaskLists the html of description + this.descriptionChange = true; + this.updateTaskHTML(); + this.tasks = this.apiData.task_status; + this.updateFlag('descriptionFlag', true); + } + }, + setTabTitle() { + const currentTabTitleScope = this.titleEl.innerText.split('·'); + currentTabTitleScope[0] = `${this.titleText} (#${this.apiData.issue_number}) `; + this.titleEl.innerText = currentTabTitleScope.join('·'); + }, + animate(title, description) { + this.title = title; + this.description = description; + this.setTabTitle(); + + this.$nextTick(() => { + this.updateFlag('titleFlag', false); + this.updateFlag('descriptionFlag', false); + }); + }, + triggerAnimation() { + // always reset to false before checking the change + this.descriptionChange = false; + + const { title, description } = this.apiData; + this.descriptionText = this.apiData.description_text; + + const noTitleChange = this.title === title; + const noDescriptionChange = this.description === description; + + /** + * since opacity is changed, even if there is no diff for Vue to update + * we must check the title/description even on a 304 to ensure no visual change + */ + if (noTitleChange && noDescriptionChange) return; + + this.elementsToVisualize(noTitleChange, noDescriptionChange); + this.animate(title, description); + }, + updateEditedTimeAgo() { + const toolTipTime = gl.utils.formatDate(this.apiData.updated_at); + this.timeAgoEl.attr('datetime', this.apiData.updated_at); + this.timeAgoEl.attr('title', toolTipTime).tooltip('fixTitle'); + }, + }, + created() { + if (!Visibility.hidden()) { + this.poll.makeRequest(); + } + + Visibility.change(() => { + if (!Visibility.hidden()) { + this.poll.restart(); + } else { + this.poll.stop(); + } + }); + }, + updated() { + // if new html is injected (description changed) - bind TaskList and call renderGFM + if (this.descriptionChange) { + this.updateEditedTimeAgo(); + + $(this.$refs['issue-content-container-gfm-entry']).renderGFM(); + + const tl = new gl.TaskList({ + dataType: 'issue', + fieldName: 'description', + selector: '.detail-page-description', + }); + + return tl && null; + } + + return null; + }, +}; +</script> + +<template> + <div> + <h2 + class="title" + :class="{ 'issue-realtime-pre-pulse': titleFlag.pre, 'issue-realtime-trigger-pulse': titleFlag.pulse }" + ref="issue-title" + v-html="title" + > + </h2> + <div + class="description is-task-list-enabled" + :class="canUpdateTasksClass" + v-if="description" + > + <div + class="wiki" + :class="{ 'issue-realtime-pre-pulse': descriptionFlag.pre, 'issue-realtime-trigger-pulse': descriptionFlag.pulse }" + v-html="description" + ref="issue-content-container-gfm-entry" + > + </div> + <textarea + class="hidden js-task-list-field" + v-if="descriptionText" + >{{descriptionText}}</textarea> + </div> + </div> +</template> diff --git a/app/assets/javascripts/lib/utils/accessor.js b/app/assets/javascripts/lib/utils/accessor.js new file mode 100644 index 00000000000..1d18992af63 --- /dev/null +++ b/app/assets/javascripts/lib/utils/accessor.js @@ -0,0 +1,47 @@ +function isPropertyAccessSafe(base, property) { + let safe; + + try { + safe = !!base[property]; + } catch (error) { + safe = false; + } + + return safe; +} + +function isFunctionCallSafe(base, functionName, ...args) { + let safe = true; + + try { + base[functionName](...args); + } catch (error) { + safe = false; + } + + return safe; +} + +function isLocalStorageAccessSafe() { + let safe; + + const TEST_KEY = 'isLocalStorageAccessSafe'; + const TEST_VALUE = 'true'; + + safe = isPropertyAccessSafe(window, 'localStorage'); + if (!safe) return safe; + + safe = isFunctionCallSafe(window.localStorage, 'setItem', TEST_KEY, TEST_VALUE); + + if (safe) window.localStorage.removeItem(TEST_KEY); + + return safe; +} + +const AccessorUtilities = { + isPropertyAccessSafe, + isFunctionCallSafe, + isLocalStorageAccessSafe, +}; + +export default AccessorUtilities; diff --git a/app/assets/javascripts/raven/index.js b/app/assets/javascripts/raven/index.js new file mode 100644 index 00000000000..5325e495815 --- /dev/null +++ b/app/assets/javascripts/raven/index.js @@ -0,0 +1,16 @@ +import RavenConfig from './raven_config'; + +const index = function index() { + RavenConfig.init({ + sentryDsn: gon.sentry_dsn, + currentUserId: gon.current_user_id, + whitelistUrls: [gon.gitlab_url], + isProduction: process.env.NODE_ENV, + }); + + return RavenConfig; +}; + +index(); + +export default index; diff --git a/app/assets/javascripts/raven/raven_config.js b/app/assets/javascripts/raven/raven_config.js new file mode 100644 index 00000000000..c7fe1cacf49 --- /dev/null +++ b/app/assets/javascripts/raven/raven_config.js @@ -0,0 +1,100 @@ +import Raven from 'raven-js'; + +const IGNORE_ERRORS = [ + // Random plugins/extensions + 'top.GLOBALS', + // See: http://blog.errorception.com/2012/03/tale-of-unfindable-js-error. html + 'originalCreateNotification', + 'canvas.contentDocument', + 'MyApp_RemoveAllHighlights', + 'http://tt.epicplay.com', + 'Can\'t find variable: ZiteReader', + 'jigsaw is not defined', + 'ComboSearch is not defined', + 'http://loading.retry.widdit.com/', + 'atomicFindClose', + // Facebook borked + 'fb_xd_fragment', + // ISP "optimizing" proxy - `Cache-Control: no-transform` seems to + // reduce this. (thanks @acdha) + // See http://stackoverflow.com/questions/4113268 + 'bmi_SafeAddOnload', + 'EBCallBackMessageReceived', + // See http://toolbar.conduit.com/Developer/HtmlAndGadget/Methods/JSInjection.aspx + 'conduitPage', +]; + +const IGNORE_URLS = [ + // Facebook flakiness + /graph\.facebook\.com/i, + // Facebook blocked + /connect\.facebook\.net\/en_US\/all\.js/i, + // Woopra flakiness + /eatdifferent\.com\.woopra-ns\.com/i, + /static\.woopra\.com\/js\/woopra\.js/i, + // Chrome extensions + /extensions\//i, + /^chrome:\/\//i, + // Other plugins + /127\.0\.0\.1:4001\/isrunning/i, // Cacaoweb + /webappstoolbarba\.texthelp\.com\//i, + /metrics\.itunes\.apple\.com\.edgesuite\.net\//i, +]; + +const SAMPLE_RATE = 95; + +const RavenConfig = { + IGNORE_ERRORS, + IGNORE_URLS, + SAMPLE_RATE, + init(options = {}) { + this.options = options; + + this.configure(); + this.bindRavenErrors(); + if (this.options.currentUserId) this.setUser(); + }, + + configure() { + Raven.config(this.options.sentryDsn, { + whitelistUrls: this.options.whitelistUrls, + environment: this.options.isProduction ? 'production' : 'development', + ignoreErrors: this.IGNORE_ERRORS, + ignoreUrls: this.IGNORE_URLS, + shouldSendCallback: this.shouldSendSample.bind(this), + }).install(); + }, + + setUser() { + Raven.setUserContext({ + id: this.options.currentUserId, + }); + }, + + bindRavenErrors() { + window.$(document).on('ajaxError.raven', this.handleRavenErrors); + }, + + handleRavenErrors(event, req, config, err) { + const error = err || req.statusText; + const responseText = req.responseText || 'Unknown response text'; + + Raven.captureMessage(error, { + extra: { + type: config.type, + url: config.url, + data: config.data, + status: req.status, + response: responseText, + error, + event, + }, + }); + }, + + shouldSendSample() { + return Math.random() * 100 <= this.SAMPLE_RATE; + }, +}; + +export default RavenConfig; diff --git a/app/assets/javascripts/signin_tabs_memoizer.js b/app/assets/javascripts/signin_tabs_memoizer.js index d811d1cd53a..2587facc582 100644 --- a/app/assets/javascripts/signin_tabs_memoizer.js +++ b/app/assets/javascripts/signin_tabs_memoizer.js @@ -1,5 +1,7 @@ /* eslint no-param-reassign: ["error", { "props": false }]*/ /* eslint no-new: "off" */ +import AccessorUtilities from './lib/utils/accessor'; + ((global) => { /** * Memorize the last selected tab after reloading a page. @@ -9,6 +11,8 @@ constructor({ currentTabKey = 'current_signin_tab', tabSelector = 'ul.nav-tabs' } = {}) { this.currentTabKey = currentTabKey; this.tabSelector = tabSelector; + this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe(); + this.bootstrap(); } @@ -37,11 +41,15 @@ } saveData(val) { - localStorage.setItem(this.currentTabKey, val); + if (!this.isLocalStorageAvailable) return undefined; + + return window.localStorage.setItem(this.currentTabKey, val); } readData() { - return localStorage.getItem(this.currentTabKey); + if (!this.isLocalStorageAvailable) return null; + + return window.localStorage.getItem(this.currentTabKey); } } diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index c197bf6b9f5..1dd0e5ab581 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -162,6 +162,18 @@ &.code { padding: 0; } + + .list-inline.previews { + display: flex; + flex-wrap: wrap; + justify-content: center; + align-content: flex-start; + align-items: baseline; + + .preview { + padding: $gl-padding; + } + } } } diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss index 52bbb753af3..d29944207c5 100644 --- a/app/assets/stylesheets/pages/cycle_analytics.scss +++ b/app/assets/stylesheets/pages/cycle_analytics.scss @@ -3,6 +3,25 @@ margin: 24px auto 0; position: relative; + .landing { + margin-top: 10px; + + .inner-content { + white-space: normal; + + h4, + p { + margin: 7px 0 0; + max-width: 480px; + padding: 0 $gl-padding; + + @media (max-width: $screen-sm-min) { + margin: 0 auto; + } + } + } + } + .col-headers { ul { margin: 0; diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index b18bbc329c3..ad3b6e0344b 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -18,6 +18,15 @@ } } +.issue-realtime-pre-pulse { + opacity: 0; +} + +.issue-realtime-trigger-pulse { + transition: opacity $fade-in-duration linear; + opacity: 1; +} + .check-all-holder { line-height: 36px; float: left; diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index e1ef0b029a5..c10588ac58e 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -116,7 +116,7 @@ } .manage-labels-list { - > li:not(.empty-message) { + > li:not(.empty-message):not(.is-not-draggable) { background-color: $white-light; cursor: move; cursor: -webkit-grab; diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index cfea52c6e57..69c328d09ff 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -130,12 +130,6 @@ ul.notes { } .note-header { - padding-bottom: 8px; - padding-right: 20px; - - @media (min-width: $screen-sm-min) { - padding-right: 0; - } @media (max-width: $screen-xs-min) { .inline { @@ -384,10 +378,15 @@ ul.notes { .note-header { display: flex; justify-content: space-between; + + @media (max-width: $screen-xs-max) { + flex-flow: row wrap; + } } .note-header-info { min-width: 0; + padding-bottom: 5px; } .note-headline-light { @@ -435,6 +434,11 @@ ul.notes { margin-left: 10px; color: $gray-darkest; + @media (max-width: $screen-xs-max) { + float: none; + margin-left: 0; + } + .note-action-button { margin-left: 8px; } diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 9115d26c779..530a6f3c6a1 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -273,6 +273,7 @@ .stage-container { display: inline-block; position: relative; + vertical-align: middle; height: 22px; margin: 3px 6px 3px 0; @@ -316,6 +317,32 @@ } } +.build-failures { + .build-state { + padding: 20px 2px; + + .build-name { + float: right; + font-weight: 500; + } + + .ci-status-icon-failed svg { + vertical-align: middle; + } + + .stage { + color: $gl-text-color-secondary; + font-weight: 500; + vertical-align: middle; + } + } + + .build-log { + border: none; + line-height: initial; + } +} + // Pipeline graph .pipeline-graph { width: 100%; diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss index a39815319f3..de652a79369 100644 --- a/app/assets/stylesheets/pages/todos.scss +++ b/app/assets/stylesheets/pages/todos.scss @@ -54,8 +54,9 @@ background-color: $white-light; &:hover { - border-color: $white-dark; + border-color: $white-normal; background-color: $gray-light; + border-top: 1px solid transparent; .todo-avatar, .todo-item { diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 643993d035e..152d7baad49 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -133,6 +133,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :signup_enabled, :sentry_dsn, :sentry_enabled, + :clientside_sentry_dsn, + :clientside_sentry_enabled, :send_user_confirmation_email, :shared_runners_enabled, :shared_runners_text, diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb index c32038d07bf..a57d9e6e6c0 100644 --- a/app/controllers/concerns/notes_actions.rb +++ b/app/controllers/concerns/notes_actions.rb @@ -65,6 +65,15 @@ module NotesActions private + def note_html(note) + render_to_string( + "shared/notes/_note", + layout: false, + formats: [:html], + locals: { note: note } + ) + end + def note_json(note) attrs = { commands_changes: note.commands_changes @@ -98,6 +107,41 @@ module NotesActions attrs end + def diff_discussion_html(discussion) + return unless discussion.diff_discussion? + + if params[:view] == 'parallel' + template = "discussions/_parallel_diff_discussion" + locals = + if params[:line_type] == 'old' + { discussions_left: [discussion], discussions_right: nil } + else + { discussions_left: nil, discussions_right: [discussion] } + end + else + template = "discussions/_diff_discussion" + locals = { discussions: [discussion] } + end + + render_to_string( + template, + layout: false, + formats: [:html], + locals: locals + ) + end + + def discussion_html(discussion) + return if discussion.individual_note? + + render_to_string( + "discussions/_discussion", + layout: false, + formats: [:html], + locals: { discussion: discussion } + ) + end + def authorize_admin_note! return access_denied! unless can?(current_user, :admin_note, note) end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 2cb38fd953d..bcd23d61519 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -201,7 +201,16 @@ class Projects::IssuesController < Projects::ApplicationController def rendered_title Gitlab::PollingInterval.set_header(response, interval: 3_000) - render json: { title: view_context.markdown_field(@issue, :title) } + + render json: { + title: view_context.markdown_field(@issue, :title), + title_text: @issue.title, + description: view_context.markdown_field(@issue, :description), + description_text: @issue.description, + task_status: @issue.task_status, + issue_number: @issue.iid, + updated_at: @issue.updated_at, + } end def create_merge_request diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index 37f51b2ebe3..41a13f6f577 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -62,50 +62,6 @@ class Projects::NotesController < Projects::ApplicationController end alias_method :awardable, :note - def note_html(note) - render_to_string( - "shared/notes/_note", - layout: false, - formats: [:html], - locals: { note: note } - ) - end - - def discussion_html(discussion) - return if discussion.individual_note? - - render_to_string( - "discussions/_discussion", - layout: false, - formats: [:html], - locals: { discussion: discussion } - ) - end - - def diff_discussion_html(discussion) - return unless discussion.diff_discussion? - - if params[:view] == 'parallel' - template = "discussions/_parallel_diff_discussion" - locals = - if params[:line_type] == 'old' - { discussions_left: [discussion], discussions_right: nil } - else - { discussions_left: nil, discussions_right: [discussion] } - end - else - template = "discussions/_diff_discussion" - locals = { discussions: [discussion] } - end - - render_to_string( - template, - layout: false, - formats: [:html], - locals: locals - ) - end - def finder_params params.merge(last_fetched_at: last_fetched_at) end diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index 2908036607a..f9adedcb074 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -1,6 +1,6 @@ class Projects::PipelinesController < Projects::ApplicationController before_action :pipeline, except: [:index, :new, :create, :charts] - before_action :commit, only: [:show, :builds] + before_action :commit, only: [:show, :builds, :failures] before_action :authorize_read_pipeline! before_action :authorize_create_pipeline!, only: [:new, :create] before_action :authorize_update_pipeline!, only: [:retry, :cancel] @@ -69,10 +69,14 @@ class Projects::PipelinesController < Projects::ApplicationController end def builds - respond_to do |format| - format.html do - render 'show' - end + render_show + end + + def failures + if @pipeline.statuses.latest.failed.present? + render_show + else + redirect_to pipeline_path(@pipeline) end end @@ -125,6 +129,14 @@ class Projects::PipelinesController < Projects::ApplicationController private + def render_show + respond_to do |format| + format.html do + render 'show' + end + end + end + def create_params params.require(:pipeline).permit(:ref) end diff --git a/app/controllers/snippets/notes_controller.rb b/app/controllers/snippets/notes_controller.rb index 3c4ddc1680d..f9496787b15 100644 --- a/app/controllers/snippets/notes_controller.rb +++ b/app/controllers/snippets/notes_controller.rb @@ -13,15 +13,6 @@ class Snippets::NotesController < ApplicationController end alias_method :awardable, :note - def note_html(note) - render_to_string( - "shared/notes/_note", - layout: false, - formats: [:html], - locals: { note: note } - ) - end - def project nil end diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb index afed27a41d1..19e07e3ab86 100644 --- a/app/controllers/snippets_controller.rb +++ b/app/controllers/snippets_controller.rb @@ -64,6 +64,7 @@ class SnippetsController < ApplicationController blob = @snippet.blob override_max_blob_size(blob) + @note = Note.new(noteable: @snippet) @noteable = @snippet @discussions = @snippet.discussions diff --git a/app/helpers/builds_helper.rb b/app/helpers/builds_helper.rb index 2fcb7a59fc3..2eb2c6c7389 100644 --- a/app/helpers/builds_helper.rb +++ b/app/helpers/builds_helper.rb @@ -1,4 +1,16 @@ module BuildsHelper + def build_summary(build, skip: false) + if build.has_trace? + if skip + link_to "View job trace", pipeline_build_url(build.pipeline, build) + else + build.trace.html(last_lines: 10).html_safe + end + else + "No job trace" + end + end + def sidebar_build_class(build, current_build) build_class = '' build_class += ' active' if build.id === current_build.id diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index 88f9a691a17..3769830de2a 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -123,7 +123,11 @@ module GitlabRoutingHelper end def preview_markdown_path(project, *args) - preview_markdown_namespace_project_path(project.namespace, project, *args) + if @snippet.is_a?(PersonalSnippet) + preview_markdown_snippet_path(@snippet) + else + preview_markdown_namespace_project_path(project.namespace, project, *args) + end end def toggle_subscription_path(entity, *args) diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb index 08180883eb9..52403640c05 100644 --- a/app/helpers/notes_helper.rb +++ b/app/helpers/notes_helper.rb @@ -76,4 +76,47 @@ module NotesHelper namespace_project_commit_path(discussion.project.namespace, discussion.project, discussion.noteable, anchor: anchor) end end + + def notes_url + if @snippet.is_a?(PersonalSnippet) + snippet_notes_path(@snippet) + else + namespace_project_noteable_notes_path( + namespace_id: @project.namespace, + project_id: @project, + target_id: @noteable.id, + target_type: @noteable.class.name.underscore + ) + end + end + + def note_url(note) + if note.noteable.is_a?(PersonalSnippet) + snippet_note_path(note.noteable, note) + else + namespace_project_note_path(@project.namespace, @project, note) + end + end + + def form_resources + if @snippet.is_a?(PersonalSnippet) + [@note] + else + [@project.namespace.becomes(Namespace), @project, @note] + end + end + + def new_form_url + return nil unless @snippet.is_a?(PersonalSnippet) + + snippet_notes_path(@snippet) + end + + def can_create_note? + if @snippet.is_a?(PersonalSnippet) + can?(current_user, :comment_personal_snippet, @snippet) + else + can?(current_user, :create_note, @project) + end + end end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index cf042717c95..54f01f8637e 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -62,6 +62,10 @@ class ApplicationSetting < ActiveRecord::Base presence: true, if: :sentry_enabled + validates :clientside_sentry_dsn, + presence: true, + if: :clientside_sentry_enabled + validates :akismet_api_key, presence: true, if: :akismet_enabled diff --git a/app/models/blob.rb b/app/models/blob.rb index a4fae22a0c4..eaf0b713122 100644 --- a/app/models/blob.rb +++ b/app/models/blob.rb @@ -26,6 +26,7 @@ class Blob < SimpleDelegator BlobViewer::Image, BlobViewer::Sketch, + BlobViewer::Balsamiq, BlobViewer::Video, diff --git a/app/models/blob_viewer/balsamiq.rb b/app/models/blob_viewer/balsamiq.rb new file mode 100644 index 00000000000..f982521db99 --- /dev/null +++ b/app/models/blob_viewer/balsamiq.rb @@ -0,0 +1,12 @@ +module BlobViewer + class Balsamiq < Base + include Rich + include ClientSide + + self.partial_name = 'balsamiq' + self.extensions = %w(bmpr) + self.binary = true + self.switcher_icon = 'file-image-o' + self.switcher_title = 'preview' + end +end diff --git a/app/services/notes/build_service.rb b/app/services/notes/build_service.rb index ea7cacc956c..abf25bb778b 100644 --- a/app/services/notes/build_service.rb +++ b/app/services/notes/build_service.rb @@ -3,8 +3,8 @@ module Notes def execute in_reply_to_discussion_id = params.delete(:in_reply_to_discussion_id) - if project && in_reply_to_discussion_id.present? - discussion = project.notes.find_discussion(in_reply_to_discussion_id) + if in_reply_to_discussion_id.present? + discussion = find_discussion(in_reply_to_discussion_id) unless discussion note = Note.new @@ -21,5 +21,19 @@ module Notes note end + + def find_discussion(discussion_id) + if project + project.notes.find_discussion(discussion_id) + else + # only PersonalSnippets can have discussions without project association + discussion = Note.find_discussion(discussion_id) + noteable = discussion.noteable + + return nil unless noteable.is_a?(PersonalSnippet) && can?(current_user, :comment_personal_snippet, noteable) + + discussion + end + end end end diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 0dc1103eece..4b6628169ef 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -394,8 +394,6 @@ %fieldset %legend Error Reporting and Logging - %p - These settings require a restart to take effect. .form-group .col-sm-offset-2.col-sm-10 .checkbox @@ -403,6 +401,7 @@ = f.check_box :sentry_enabled Enable Sentry .help-block + %p This setting requires a restart to take effect. Sentry is an error reporting and logging tool which is currently not shipped with GitLab, get it here: %a{ href: 'https://getsentry.com', target: '_blank', rel: 'noopener noreferrer' } https://getsentry.com @@ -411,6 +410,21 @@ .col-sm-10 = f.text_field :sentry_dsn, class: 'form-control' + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :clientside_sentry_enabled do + = f.check_box :clientside_sentry_enabled + Enable Clientside Sentry + .help-block + Sentry can also be used for reporting and logging clientside exceptions. + %a{ href: 'https://sentry.io/for/javascript/', target: '_blank', rel: 'noopener noreferrer' } https://sentry.io/for/javascript/ + + .form-group + = f.label :clientside_sentry_dsn, 'Clientside Sentry DSN', class: 'control-label col-sm-2' + .col-sm-10 + = f.text_field :clientside_sentry_dsn, class: 'form-control' + %fieldset %legend Repository Storage .form-group diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index 19473b6ab27..afcc2b6e4f3 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -28,9 +28,12 @@ = stylesheet_link_tag "application", media: "all" = stylesheet_link_tag "print", media: "print" + = Gon::Base.render_data + = webpack_bundle_tag "runtime" = webpack_bundle_tag "common" = webpack_bundle_tag "main" + = webpack_bundle_tag "raven" if current_application_settings.clientside_sentry_enabled - if content_for?(:page_specific_javascripts) = yield :page_specific_javascripts diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index dc926a615c7..7e011ac3e75 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -2,8 +2,6 @@ %html{ lang: I18n.locale, class: "#{page_class}" } = render "layouts/head" %body{ class: @body_class, data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}" } } - = Gon::Base.render_data - = render "layouts/header/default", title: header_title = render 'layouts/page', sidebar: sidebar, nav: nav diff --git a/app/views/layouts/devise.html.haml b/app/views/layouts/devise.html.haml index 3368a9beb29..52fb46eb8c9 100644 --- a/app/views/layouts/devise.html.haml +++ b/app/views/layouts/devise.html.haml @@ -3,7 +3,6 @@ = render "layouts/head" %body.ui_charcoal.login-page.application.navless{ data: { page: body_data_page } } .page-wrap - = Gon::Base.render_data = render "layouts/header/empty" = render "layouts/broadcast" .container.navless-container diff --git a/app/views/layouts/devise_empty.html.haml b/app/views/layouts/devise_empty.html.haml index 7466423a934..ed6731bde95 100644 --- a/app/views/layouts/devise_empty.html.haml +++ b/app/views/layouts/devise_empty.html.haml @@ -2,7 +2,6 @@ %html{ lang: "en" } = render "layouts/head" %body.ui_charcoal.login-page.application.navless - = Gon::Base.render_data = render "layouts/header/empty" = render "layouts/broadcast" .container.navless-container diff --git a/app/views/layouts/snippets.html.haml b/app/views/layouts/snippets.html.haml index 02ca3ee7a28..98b75cea03f 100644 --- a/app/views/layouts/snippets.html.haml +++ b/app/views/layouts/snippets.html.haml @@ -1,3 +1,9 @@ - header_title "Snippets", snippets_path +- content_for :page_specific_javascripts do + - if @snippet&.persisted? && current_user + :javascript + window.uploads_path = "#{upload_path('personal_snippet', @snippet)}"; + window.preview_markdown_path = "#{preview_markdown_snippet_path(@snippet)}"; + = render template: "layouts/application" diff --git a/app/views/projects/blob/viewers/_balsamiq.html.haml b/app/views/projects/blob/viewers/_balsamiq.html.haml new file mode 100644 index 00000000000..28670e7de97 --- /dev/null +++ b/app/views/projects/blob/viewers/_balsamiq.html.haml @@ -0,0 +1,4 @@ +- content_for :page_specific_javascripts do + = page_specific_javascript_bundle_tag('balsamiq_viewer') + +.file-content.balsamiq-viewer#js-balsamiq-viewer{ data: { endpoint: blob_raw_url } } diff --git a/app/views/projects/boards/components/_board.html.haml b/app/views/projects/boards/components/_board.html.haml index 5a4eaf92b16..bc5c727bf0d 100644 --- a/app/views/projects/boards/components/_board.html.haml +++ b/app/views/projects/boards/components/_board.html.haml @@ -13,8 +13,8 @@ %button.btn.btn-small.btn-default.pull-right.has-tooltip{ type: "button", "@click" => "showNewIssueForm", "v-if" => 'list.type !== "closed"', - "aria-label" => "Add an issue", - "title" => "Add an issue", + "aria-label" => "New issue", + "title" => "New issue", data: { placement: "top", container: "body" } } = icon("plus") - if can?(current_user, :admin_list, @project) diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml index 16d2646cb4e..6051ea2f1ce 100644 --- a/app/views/projects/commit/show.html.haml +++ b/app/views/projects/commit/show.html.haml @@ -13,7 +13,7 @@ .block-connector = render "projects/diffs/diffs", diffs: @diffs, environment: @environment - = render "projects/notes/notes_with_form" + = render "shared/notes/notes_with_form" - if can_collaborate_with_project? - %w(revert cherry-pick).each do |type| = render "projects/commit/change", type: type, commit: @commit, title: @commit.title diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml index 819f29d3ca5..b158a81471c 100644 --- a/app/views/projects/cycle_analytics/show.html.haml +++ b/app/views/projects/cycle_analytics/show.html.haml @@ -9,17 +9,17 @@ #cycle-analytics{ class: container_class, "v-cloak" => "true", data: { request_path: project_cycle_analytics_path(@project) } } - if @cycle_analytics_no_data - .bordered-box.landing.content-block{ "v-if" => "!isOverviewDialogDismissed" } - = icon("times", class: "dismiss-icon", "@click" => "dismissOverviewDialog()") - .row - .col-sm-3.col-xs-12.svg-container - = custom_icon('icon_cycle_analytics_splash') - .col-sm-8.col-xs-12.inner-content - %h4 - {{ __('Introducing Cycle Analytics') }} - %p - {{ __('Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.') }} - + .landing.content-block{ "v-if" => "!isOverviewDialogDismissed" } + %button.dismiss-button{ type: 'button', 'aria-label': 'Dismiss Cycle Analytics introduction box' } + = icon("times", "@click" => "dismissOverviewDialog()") + .svg-container + = custom_icon('icon_cycle_analytics_splash') + .inner-content + %h4 + {{ __('Introducing Cycle Analytics') }} + %p + {{ __('Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.') }} + %p = link_to _('Read more'), help_page_path('user/project/cycle_analytics'), target: '_blank', class: 'btn' = icon("spinner spin", "v-show" => "isLoading") .wrapper{ "v-show" => "!isLoading && !hasError" } diff --git a/app/views/projects/group_links/_index.html.haml b/app/views/projects/group_links/_index.html.haml index b6116dbec41..debb0214d06 100644 --- a/app/views/projects/group_links/_index.html.haml +++ b/app/views/projects/group_links/_index.html.haml @@ -6,11 +6,9 @@ %p Projects can be stored in only one group at once. However you can share a project with other groups here. .col-lg-9 - %h5.prepend-top-0 - Set a group to share = form_tag namespace_project_group_links_path(@project.namespace, @project), class: 'js-requires-input', method: :post do .form-group - = label_tag :link_group_id, "Group", class: "label-light" + = label_tag :link_group_id, "Select a group to share with", class: "label-light" = groups_select_tag(:link_group_id, data: { skip_groups: @skip_groups }, required: true) .form-group = label_tag :link_group_access, "Max access level", class: "label-light" diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml index 5d4e593e4ef..4dfda54feb5 100644 --- a/app/views/projects/issues/_discussion.html.haml +++ b/app/views/projects/issues/_discussion.html.haml @@ -4,4 +4,4 @@ = link_to 'Close issue', issue_path(@issue, issue: {state_event: :close}, format: 'json'), data: {no_turbolink: true, original_text: "Close issue", alternative_text: "Comment & close issue"}, class: "btn btn-nr btn-close btn-comment js-note-target-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' #notes - = render 'projects/notes/notes_with_form' + = render 'shared/notes/notes_with_form' diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 1418ad73553..9084883eb3e 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -51,16 +51,11 @@ .issue-details.issuable-details .detail-page-description.content-block - .issue-title-data.hidden{ "data" => { "initial-title" => markdown_field(@issue, :title), - "endpoint" => rendered_title_namespace_project_issue_path(@project.namespace, @project, @issue), + .issue-title-data.hidden{ "data" => { "endpoint" => rendered_title_namespace_project_issue_path(@project.namespace, @project, @issue), + "can-update-tasks-class" => can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : '', } } .issue-title-entrypoint - - if @issue.description.present? - .description{ class: can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : '' } - .wiki - = markdown_field(@issue, :description) - %textarea.hidden.js-task-list-field - = @issue.description + = edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue_edited_ago') #merge-requests{ data: { url: referenced_merge_requests_namespace_project_issue_url(@project.namespace, @project, @issue) } } diff --git a/app/views/projects/merge_requests/_discussion.html.haml b/app/views/projects/merge_requests/_discussion.html.haml index 15b5a51c1d0..2e6420db212 100644 --- a/app/views/projects/merge_requests/_discussion.html.haml +++ b/app/views/projects/merge_requests/_discussion.html.haml @@ -8,4 +8,4 @@ %button.btn.btn-nr.btn-default.append-right-10.js-comment-resolve-button{ "v-if" => "showButton", type: "submit", data: { project_path: "#{project_path(@merge_request.project)}" } } {{ buttonText }} -#notes= render "projects/notes/notes_with_form" +#notes= render "shared/notes/notes_with_form" diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml index 2e978fda624..9a95b2a82ff 100644 --- a/app/views/projects/milestones/_form.html.haml +++ b/app/views/projects/milestones/_form.html.haml @@ -11,7 +11,7 @@ .col-sm-10 = render layout: 'projects/md_preview', locals: { url: preview_markdown_path(@project) } do = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: 'Write milestone description...' - = render 'projects/notes/hints' + = render 'shared/notes/hints' .clearfix .error-alert = render "shared/milestones/form_dates", f: f diff --git a/app/views/projects/notes/_edit.html.haml b/app/views/projects/notes/_edit.html.haml deleted file mode 100644 index f1e251d65b7..00000000000 --- a/app/views/projects/notes/_edit.html.haml +++ /dev/null @@ -1,3 +0,0 @@ -.original-note-content.hidden{ data: { post_url: namespace_project_note_path(@project.namespace, @project, note), target_id: note.noteable.id, target_type: note.noteable.class.name.underscore } } - #{note.note} -%textarea.hidden.js-task-list-field.original-task-list{ data: {update_url: namespace_project_note_path(@project.namespace, @project, note) } }= note.note diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml index d7cefb8613e..1aa48bf9813 100644 --- a/app/views/projects/pipelines/_with_tabs.html.haml +++ b/app/views/projects/pipelines/_with_tabs.html.haml @@ -1,3 +1,5 @@ +- failed_builds = @pipeline.statuses.latest.failed + .tabs-holder %ul.pipelines-tabs.nav-links.no-top.no-bottom %li.js-pipeline-tab-link @@ -7,8 +9,11 @@ = link_to builds_namespace_project_pipeline_path(@project.namespace, @project, @pipeline), data: {target: 'div#js-tab-builds', action: 'builds', toggle: 'tab' }, class: 'builds-tab' do Jobs %span.badge.js-builds-counter= pipeline.statuses.count - - + - if failed_builds.present? + %li.js-failures-tab-link + = link_to failures_namespace_project_pipeline_path(@project.namespace, @project, @pipeline), data: {target: 'div#js-tab-failures', action: 'failures', toggle: 'tab' }, class: 'failures-tab' do + Failed Jobs + %span.badge.js-failures-counter= failed_builds.count .tab-content #js-tab-pipeline.tab-pane @@ -39,3 +44,13 @@ %th Coverage %th = render partial: "projects/stage/stage", collection: pipeline.stages, as: :stage + - if failed_builds.present? + #js-tab-failures.build-failures.tab-pane + - failed_builds.each_with_index do |build, index| + .build-state + %span.ci-status-icon-failed= custom_icon('icon_status_failed') + %span.stage + = build.stage.titleize + %span.build-name + = link_to build.name, pipeline_build_url(pipeline, build) + %pre.build-log= build_summary(build, skip: index >= 10) diff --git a/app/views/projects/project_members/_index.html.haml b/app/views/projects/project_members/_index.html.haml index f83521052ed..d080b6c83d4 100644 --- a/app/views/projects/project_members/_index.html.haml +++ b/app/views/projects/project_members/_index.html.haml @@ -18,7 +18,7 @@ = render "projects/project_members/new_project_member" = render 'shared/members/requests', membership_source: @project, requesters: @requesters - .append-bottom-default.clearfix + .clearfix %h5.member.existing-title Existing members and groups - if @group_links.any? diff --git a/app/views/projects/releases/edit.html.haml b/app/views/projects/releases/edit.html.haml index faa24a3c88e..93ee9382a6e 100644 --- a/app/views/projects/releases/edit.html.haml +++ b/app/views/projects/releases/edit.html.haml @@ -13,7 +13,7 @@ = form_for(@release, method: :put, url: namespace_project_tag_release_path(@project.namespace, @project, @tag.name), html: { class: 'form-horizontal common-note-form release-form js-quick-submit' }) do |f| = render layout: 'projects/md_preview', locals: { url: preview_markdown_path(@project), referenced_users: true } do = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: "Write your release notes or drag files here..." - = render 'projects/notes/hints' + = render 'shared/notes/hints' .error-alert .prepend-top-default = f.submit 'Save changes', class: 'btn btn-save' diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml index 7a175f63eeb..aab1c043e66 100644 --- a/app/views/projects/snippets/show.html.haml +++ b/app/views/projects/snippets/show.html.haml @@ -9,4 +9,4 @@ .row-content-block.top-block.content-component-block = render 'award_emoji/awards_block', awardable: @snippet, inline: true - #notes= render "projects/notes/notes_with_form" + #notes= render "shared/notes/notes_with_form" diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml index a6894b9adc0..7c607d2956b 100644 --- a/app/views/projects/tags/new.html.haml +++ b/app/views/projects/tags/new.html.haml @@ -30,7 +30,7 @@ .col-sm-10 = render layout: 'projects/md_preview', locals: { url: preview_markdown_path(@project), referenced_users: true } do = render 'projects/zen', attr: :release_description, classes: 'note-textarea', placeholder: "Write your release notes or drag files here..." - = render 'projects/notes/hints' + = render 'shared/notes/hints' .help-block Optionally, add release notes to the tag. They will be stored in the GitLab database and displayed on the tags page. .form-actions = button_tag 'Create tag', class: 'btn btn-create', tabindex: 3 diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml index 00869aff27b..6cb7c1e9c4d 100644 --- a/app/views/projects/wikis/_form.html.haml +++ b/app/views/projects/wikis/_form.html.haml @@ -14,7 +14,7 @@ .col-sm-10 = render layout: 'projects/md_preview', locals: { url: namespace_project_wiki_preview_markdown_path(@project.namespace, @project, @page.slug) } do = render 'projects/zen', f: f, attr: :content, classes: 'note-textarea', placeholder: 'Write your content or drag files here...' - = render 'projects/notes/hints' + = render 'shared/notes/hints' .clearfix .error-alert diff --git a/app/views/shared/issuable/form/_description.html.haml b/app/views/shared/issuable/form/_description.html.haml index cbc7125c0d5..7ef0ae96be2 100644 --- a/app/views/shared/issuable/form/_description.html.haml +++ b/app/views/shared/issuable/form/_description.html.haml @@ -17,6 +17,6 @@ classes: 'note-textarea', placeholder: "Write a comment or drag your files here...", supports_slash_commands: supports_slash_commands - = render 'projects/notes/hints', supports_slash_commands: supports_slash_commands + = render 'shared/notes/hints', supports_slash_commands: supports_slash_commands .clearfix .error-alert diff --git a/app/views/shared/members/_requests.html.haml b/app/views/shared/members/_requests.html.haml index 10050adfda5..92f6e7428ae 100644 --- a/app/views/shared/members/_requests.html.haml +++ b/app/views/shared/members/_requests.html.haml @@ -1,5 +1,5 @@ - if requesters.any? - .panel.panel-default + .panel.panel-default.prepend-top-default .panel-heading Users requesting access to %strong= membership_source.name diff --git a/app/views/shared/milestones/_labels_tab.html.haml b/app/views/shared/milestones/_labels_tab.html.haml index 33f93dccd3c..a26b3b8009e 100644 --- a/app/views/shared/milestones/_labels_tab.html.haml +++ b/app/views/shared/milestones/_labels_tab.html.haml @@ -2,7 +2,7 @@ - labels.each do |label| - options = { milestone_title: @milestone.title, label_name: label.title } - %li + %li.is-not-draggable %span.label-row %span.label-name = link_to milestones_label_path(options) do @@ -10,10 +10,8 @@ %span.prepend-description-left = markdown_field(label, :description) - .pull-info-right - %span.append-right-20 - = link_to milestones_label_path(options.merge(state: 'opened')) do - - pluralize milestone_issues_by_label_count(@milestone, label, state: :opened), 'open issue' - %span.append-right-20 - = link_to milestones_label_path(options.merge(state: 'closed')) do - - pluralize milestone_issues_by_label_count(@milestone, label, state: :closed), 'closed issue' + .pull-right.hidden-xs.hidden-sm.hidden-md + = link_to milestones_label_path(options.merge(state: 'opened')), class: 'btn btn-transparent btn-action' do + - pluralize milestone_issues_by_label_count(@milestone, label, state: :opened), 'open issue' + = link_to milestones_label_path(options.merge(state: 'closed')), class: 'btn btn-transparent btn-action' do + - pluralize milestone_issues_by_label_count(@milestone, label, state: :closed), 'closed issue' diff --git a/app/views/projects/notes/_comment_button.html.haml b/app/views/shared/notes/_comment_button.html.haml index 29cf5825292..29cf5825292 100644 --- a/app/views/projects/notes/_comment_button.html.haml +++ b/app/views/shared/notes/_comment_button.html.haml diff --git a/app/views/shared/notes/_edit.html.haml b/app/views/shared/notes/_edit.html.haml new file mode 100644 index 00000000000..4a020865828 --- /dev/null +++ b/app/views/shared/notes/_edit.html.haml @@ -0,0 +1,3 @@ +.original-note-content.hidden{ data: { post_url: note_url(note), target_id: note.noteable.id, target_type: note.noteable.class.name.underscore } } + #{note.note} +%textarea.hidden.js-task-list-field.original-task-list{ data: {update_url: note_url(note) } }= note.note diff --git a/app/views/projects/notes/_edit_form.html.haml b/app/views/shared/notes/_edit_form.html.haml index 3867072225f..8923e5602a4 100644 --- a/app/views/projects/notes/_edit_form.html.haml +++ b/app/views/shared/notes/_edit_form.html.haml @@ -4,7 +4,7 @@ = hidden_field_tag :target_type, '', class: 'js-form-target-type' = render layout: 'projects/md_preview', locals: { url: preview_markdown_path(project), referenced_users: true } do = render 'projects/zen', attr: 'note[note]', classes: 'note-textarea js-note-text js-task-list-field', placeholder: "Write a comment or drag your files here..." - = render 'projects/notes/hints' + = render 'shared/notes/hints' .note-form-actions.clearfix .settings-message.note-edit-warning.js-finish-edit-warning diff --git a/app/views/projects/notes/_form.html.haml b/app/views/shared/notes/_form.html.haml index 46f785fefca..eaf50bc2115 100644 --- a/app/views/projects/notes/_form.html.haml +++ b/app/views/shared/notes/_form.html.haml @@ -4,7 +4,7 @@ - else - preview_url = preview_markdown_path(@project) -= form_for [@project.namespace.becomes(Namespace), @project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new-note js-new-note-form js-quick-submit common-note-form", "data-noteable-iid" => @note.noteable.try(:iid), }, authenticity_token: true do |f| += form_for form_resources, url: new_form_url, remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new-note js-new-note-form js-quick-submit common-note-form", "data-noteable-iid" => @note.noteable.try(:iid), }, authenticity_token: true do |f| = hidden_field_tag :view, diff_view = hidden_field_tag :line_type = hidden_field_tag :merge_request_diff_head_sha, @note.noteable.try(:diff_head_sha) @@ -28,11 +28,11 @@ classes: 'note-textarea js-note-text', placeholder: "Write a comment or drag your files here...", supports_slash_commands: supports_slash_commands - = render 'projects/notes/hints', supports_slash_commands: supports_slash_commands + = render 'shared/notes/hints', supports_slash_commands: supports_slash_commands .error-alert .note-form-actions.clearfix - = render partial: 'projects/notes/comment_button' + = render partial: 'shared/notes/comment_button' = yield(:note_actions) diff --git a/app/views/projects/notes/_hints.html.haml b/app/views/shared/notes/_hints.html.haml index 81d97eabe65..81d97eabe65 100644 --- a/app/views/projects/notes/_hints.html.haml +++ b/app/views/shared/notes/_hints.html.haml diff --git a/app/views/shared/notes/_note.html.haml b/app/views/shared/notes/_note.html.haml index 9657b4eea82..071c48fa2e4 100644 --- a/app/views/shared/notes/_note.html.haml +++ b/app/views/shared/notes/_note.html.haml @@ -42,10 +42,7 @@ = note.redacted_note_html = edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true) - if note_editable - - if note.for_personal_snippet? - = render 'snippets/notes/edit', note: note - - else - = render 'projects/notes/edit', note: note + = render 'shared/notes/edit', note: note .note-awards = render 'award_emoji/awards_block', awardable: note, inline: false - if note.system diff --git a/app/views/projects/notes/_notes_with_form.html.haml b/app/views/shared/notes/_notes_with_form.html.haml index 2a66addb08a..9930cbd96d7 100644 --- a/app/views/projects/notes/_notes_with_form.html.haml +++ b/app/views/shared/notes/_notes_with_form.html.haml @@ -1,18 +1,18 @@ %ul#notes-list.notes.main-notes-list.timeline = render "shared/notes/notes" -= render 'projects/notes/edit_form', project: @project += render 'shared/notes/edit_form', project: @project %ul.notes.notes-form.timeline %li.timeline-entry .flash-container.timeline-content - - if can? current_user, :create_note, @project + - if can_create_note? .timeline-icon.hidden-xs.hidden-sm %a.author_link{ href: user_path(current_user) } = image_tag avatar_icon(current_user), alt: current_user.to_reference, class: 'avatar s40' .timeline-content.timeline-content-form - = render "projects/notes/form", view: diff_view + = render "shared/notes/form", view: diff_view - elsif !current_user .disabled-comment.text-center .disabled-comment-text.inline @@ -23,4 +23,4 @@ to post a comment :javascript - var notes = new Notes("#{namespace_project_noteable_notes_path(namespace_id: @project.namespace, project_id: @project, target_id: @noteable.id, target_type: @noteable.class.name.underscore)}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}, "#{diff_view}") + var notes = new Notes("#{notes_url}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}, "#{diff_view}") diff --git a/app/views/snippets/notes/_edit.html.haml b/app/views/snippets/notes/_edit.html.haml deleted file mode 100644 index e69de29bb2d..00000000000 --- a/app/views/snippets/notes/_edit.html.haml +++ /dev/null diff --git a/app/views/snippets/notes/_notes.html.haml b/app/views/snippets/notes/_notes.html.haml deleted file mode 100644 index f07d6b8c126..00000000000 --- a/app/views/snippets/notes/_notes.html.haml +++ /dev/null @@ -1,2 +0,0 @@ -%ul#notes-list.notes.main-notes-list.timeline - = render "projects/notes/notes" diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml index 98287cba5b4..51dbbc32cc9 100644 --- a/app/views/snippets/show.html.haml +++ b/app/views/snippets/show.html.haml @@ -2,11 +2,11 @@ = render 'shared/snippets/header' -%article.file-holder.snippet-file-content - = render 'shared/snippets/blob' +.personal-snippets + %article.file-holder.snippet-file-content + = render 'shared/snippets/blob' -.row-content-block.top-block.content-component-block - = render 'award_emoji/awards_block', awardable: @snippet, inline: true + .row-content-block.top-block.content-component-block + = render 'award_emoji/awards_block', awardable: @snippet, inline: true -%ul#notes-list.notes.main-notes-list.timeline - #notes= render 'shared/notes/notes' + #notes= render "shared/notes/notes_with_form" |