summaryrefslogtreecommitdiff
path: root/app/assets/javascripts
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts')
-rw-r--r--app/assets/javascripts/abuse_reports.js4
-rw-r--r--app/assets/javascripts/behaviors/copy_as_gfm.js (renamed from app/assets/javascripts/copy_as_gfm.js)18
-rw-r--r--app/assets/javascripts/behaviors/index.js2
-rw-r--r--app/assets/javascripts/boards/components/modal/footer.js3
-rw-r--r--app/assets/javascripts/boards/filtered_search_boards.js9
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js8
-rw-r--r--app/assets/javascripts/clusters.js123
-rw-r--r--app/assets/javascripts/clusters/clusters_bundle.js221
-rw-r--r--app/assets/javascripts/clusters/components/application_row.vue185
-rw-r--r--app/assets/javascripts/clusters/components/applications.vue114
-rw-r--r--app/assets/javascripts/clusters/constants.js12
-rw-r--r--app/assets/javascripts/clusters/event_hub.js3
-rw-r--r--app/assets/javascripts/clusters/services/clusters_service.js24
-rw-r--r--app/assets/javascripts/clusters/stores/clusters_store.js68
-rw-r--r--app/assets/javascripts/commits.js4
-rw-r--r--app/assets/javascripts/create_label.js3
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics_store.js4
-rw-r--r--app/assets/javascripts/dispatcher.js19
-rw-r--r--app/assets/javascripts/emoji/support/is_emoji_unicode_supported.js16
-rw-r--r--app/assets/javascripts/emoji/support/unicode_support_map.js21
-rw-r--r--app/assets/javascripts/environments/components/environment_item.vue4
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_utils.js10
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js10
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js15
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js3
-rw-r--r--app/assets/javascripts/gl_dropdown.js2
-rw-r--r--app/assets/javascripts/gl_form.js5
-rw-r--r--app/assets/javascripts/issue.js4
-rw-r--r--app/assets/javascripts/issue_show/components/app.vue17
-rw-r--r--app/assets/javascripts/issue_show/components/edit_actions.vue10
-rw-r--r--app/assets/javascripts/issue_show/components/form.vue8
-rw-r--r--app/assets/javascripts/job.js50
-rw-r--r--app/assets/javascripts/labels_select.js7
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js37
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js5
-rw-r--r--app/assets/javascripts/lib/utils/number_utils.js28
-rw-r--r--app/assets/javascripts/lib/utils/poll.js8
-rw-r--r--app/assets/javascripts/lib/utils/text_markdown.js153
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js228
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js4
-rw-r--r--app/assets/javascripts/main.js4
-rw-r--r--app/assets/javascripts/members.js12
-rw-r--r--app/assets/javascripts/merge_request.js3
-rw-r--r--app/assets/javascripts/milestone_select.js17
-rw-r--r--app/assets/javascripts/monitoring/components/graph.vue8
-rw-r--r--app/assets/javascripts/monitoring/components/graph/legend.vue25
-rw-r--r--app/assets/javascripts/monitoring/components/graph/path.vue12
-rw-r--r--app/assets/javascripts/monitoring/utils/measurements.js8
-rw-r--r--app/assets/javascripts/monitoring/utils/multiple_time_series.js49
-rw-r--r--app/assets/javascripts/notes/components/issue_comment_form.vue3
-rw-r--r--app/assets/javascripts/notes/components/issue_discussion_locked_widget.vue17
-rw-r--r--app/assets/javascripts/pipelines/components/graph/action_component.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/navigation_tabs.vue87
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines.vue163
-rw-r--r--app/assets/javascripts/project.js246
-rw-r--r--app/assets/javascripts/project_avatar.js31
-rw-r--r--app/assets/javascripts/project_import.js17
-rw-r--r--app/assets/javascripts/registry/components/table_registry.vue7
-rw-r--r--app/assets/javascripts/repo/components/repo_file.vue29
-rw-r--r--app/assets/javascripts/repo/components/repo_loading_file.vue43
-rw-r--r--app/assets/javascripts/repo/components/repo_sidebar.vue4
-rw-r--r--app/assets/javascripts/repo/services/index.js7
-rw-r--r--app/assets/javascripts/repo/stores/actions.js22
-rw-r--r--app/assets/javascripts/repo/stores/actions/branch.js8
-rw-r--r--app/assets/javascripts/repo/stores/actions/file.js2
-rw-r--r--app/assets/javascripts/repo/stores/actions/tree.js58
-rw-r--r--app/assets/javascripts/repo/stores/mutation_types.js2
-rw-r--r--app/assets/javascripts/repo/stores/mutations.js7
-rw-r--r--app/assets/javascripts/repo/stores/mutations/tree.js30
-rw-r--r--app/assets/javascripts/repo/stores/state.js1
-rw-r--r--app/assets/javascripts/repo/stores/utils.js37
-rw-r--r--app/assets/javascripts/search_autocomplete.js16
-rw-r--r--app/assets/javascripts/shortcuts_issuable.js5
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue29
-rw-r--r--app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue31
-rw-r--r--app/assets/javascripts/smart_interval.js29
-rw-r--r--app/assets/javascripts/users_select.js7
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js90
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue104
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/dependencies.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js16
-rw-r--r--app/assets/javascripts/vue_shared/components/issue/issue_warning.vue29
-rw-r--r--app/assets/javascripts/vue_shared/components/loading_button.vue14
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue23
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/header.vue36
-rw-r--r--app/assets/javascripts/vue_shared/components/popup_dialog.vue102
-rw-r--r--app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue37
-rw-r--r--app/assets/javascripts/wikis.js3
89 files changed, 1997 insertions, 1012 deletions
diff --git a/app/assets/javascripts/abuse_reports.js b/app/assets/javascripts/abuse_reports.js
index 3de192d56eb..d2d3a257c0d 100644
--- a/app/assets/javascripts/abuse_reports.js
+++ b/app/assets/javascripts/abuse_reports.js
@@ -1,3 +1,5 @@
+import { truncate } from './lib/utils/text_utility';
+
const MAX_MESSAGE_LENGTH = 500;
const MESSAGE_CELL_SELECTOR = '.abuse-reports .message';
@@ -15,7 +17,7 @@ export default class AbuseReports {
if (reportMessage.length > MAX_MESSAGE_LENGTH) {
$messageCellElement.data('original-message', reportMessage);
$messageCellElement.data('message-truncated', 'true');
- $messageCellElement.text(window.gl.text.truncate(reportMessage, MAX_MESSAGE_LENGTH));
+ $messageCellElement.text(truncate(reportMessage, MAX_MESSAGE_LENGTH));
}
}
diff --git a/app/assets/javascripts/copy_as_gfm.js b/app/assets/javascripts/behaviors/copy_as_gfm.js
index 93b0cbf4209..e7dc4ef8304 100644
--- a/app/assets/javascripts/copy_as_gfm.js
+++ b/app/assets/javascripts/behaviors/copy_as_gfm.js
@@ -1,7 +1,8 @@
/* eslint-disable class-methods-use-this, object-shorthand, no-unused-vars, no-use-before-define, no-new, max-len, no-restricted-syntax, guard-for-in, no-continue */
+
import _ from 'underscore';
-import { insertText, getSelectedFragment, nodeMatchesSelector } from './lib/utils/common_utils';
-import { placeholderImage } from './lazy_loader';
+import { insertText, getSelectedFragment, nodeMatchesSelector } from '../lib/utils/common_utils';
+import { placeholderImage } from '../lazy_loader';
const gfmRules = {
// The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert
@@ -284,7 +285,7 @@ const gfmRules = {
},
};
-class CopyAsGFM {
+export class CopyAsGFM {
constructor() {
$(document).on('copy', '.md, .wiki', (e) => { CopyAsGFM.copyAsGFM(e, CopyAsGFM.transformGFMSelection); });
$(document).on('copy', 'pre.code.highlight, .diff-content .line_content', (e) => { CopyAsGFM.copyAsGFM(e, CopyAsGFM.transformCodeSelection); });
@@ -469,7 +470,12 @@ class CopyAsGFM {
}
}
-window.gl = window.gl || {};
-window.gl.CopyAsGFM = CopyAsGFM;
+// Export CopyAsGFM as a global for rspec to access
+// see /spec/features/copy_as_gfm_spec.rb
+if (process.env.NODE_ENV !== 'production') {
+ window.CopyAsGFM = CopyAsGFM;
+}
-new CopyAsGFM();
+export default function initCopyAsGFM() {
+ return new CopyAsGFM();
+}
diff --git a/app/assets/javascripts/behaviors/index.js b/app/assets/javascripts/behaviors/index.js
index 44b2c974b9e..671532394a9 100644
--- a/app/assets/javascripts/behaviors/index.js
+++ b/app/assets/javascripts/behaviors/index.js
@@ -1,5 +1,6 @@
import './autosize';
import './bind_in_out';
+import initCopyAsGFM from './copy_as_gfm';
import './details_behavior';
import installGlEmojiElement from './gl_emoji';
import './quick_submit';
@@ -7,3 +8,4 @@ import './requires_input';
import './toggler_behavior';
installGlEmojiElement();
+initCopyAsGFM();
diff --git a/app/assets/javascripts/boards/components/modal/footer.js b/app/assets/javascripts/boards/components/modal/footer.js
index de9e44cef35..182957113a2 100644
--- a/app/assets/javascripts/boards/components/modal/footer.js
+++ b/app/assets/javascripts/boards/components/modal/footer.js
@@ -3,6 +3,7 @@
import Vue from 'vue';
import Flash from '../../../flash';
import './lists_dropdown';
+import { pluralize } from '../../../lib/utils/text_utility';
const ModalStore = gl.issueBoards.ModalStore;
@@ -21,7 +22,7 @@ gl.issueBoards.ModalFooter = Vue.extend({
submitText() {
const count = ModalStore.selectedCount();
- return `Add ${count > 0 ? count : ''} ${gl.text.pluralize('issue', count)}`;
+ return `Add ${count > 0 ? count : ''} ${pluralize('issue', count)}`;
},
},
methods: {
diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js
index 3f083655f95..184665f395c 100644
--- a/app/assets/javascripts/boards/filtered_search_boards.js
+++ b/app/assets/javascripts/boards/filtered_search_boards.js
@@ -11,7 +11,8 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager {
// Issue boards is slightly different, we handle all the requests async
// instead or reloading the page, we just re-fire the list ajax requests
this.isHandledAsync = true;
- this.cantEdit = cantEdit;
+ this.cantEdit = cantEdit.filter(i => typeof i === 'string');
+ this.cantEditWithValue = cantEdit.filter(i => typeof i === 'object');
}
updateObject(path) {
@@ -42,7 +43,9 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager {
this.filteredSearchInput.dispatchEvent(new Event('input'));
}
- canEdit(tokenName) {
- return this.cantEdit.indexOf(tokenName) === -1;
+ canEdit(tokenName, tokenValue) {
+ if (this.cantEdit.includes(tokenName)) return false;
+ return this.cantEditWithValue.findIndex(token => token.name === tokenName &&
+ token.value === tokenValue) === -1;
}
}
diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js
index ea82958e80d..798d7e0d147 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js
+++ b/app/assets/javascripts/boards/stores/boards_store.js
@@ -14,16 +14,18 @@ gl.issueBoards.BoardsStore = {
},
state: {},
detail: {
- issue: {}
+ issue: {},
},
moving: {
issue: {},
- list: {}
+ list: {},
},
create () {
this.state.lists = [];
this.filter.path = getUrlParamsArray().join('&');
- this.detail = { issue: {} };
+ this.detail = {
+ issue: {},
+ };
},
addList (listObj, defaultAvatar) {
const list = new List(listObj, defaultAvatar);
diff --git a/app/assets/javascripts/clusters.js b/app/assets/javascripts/clusters.js
deleted file mode 100644
index c9fef94efea..00000000000
--- a/app/assets/javascripts/clusters.js
+++ /dev/null
@@ -1,123 +0,0 @@
-/* globals Flash */
-import Visibility from 'visibilityjs';
-import axios from 'axios';
-import setAxiosCsrfToken from './lib/utils/axios_utils';
-import Poll from './lib/utils/poll';
-import { s__ } from './locale';
-import initSettingsPanels from './settings_panels';
-import Flash from './flash';
-
-/**
- * Cluster page has 2 separate parts:
- * Toggle button
- *
- * - Polling status while creating or scheduled
- * -- Update status area with the response result
- */
-
-class ClusterService {
- constructor(options = {}) {
- this.options = options;
- setAxiosCsrfToken();
- }
- fetchData() {
- return axios.get(this.options.endpoint);
- }
-}
-
-export default class Clusters {
- constructor() {
- initSettingsPanels();
-
- const dataset = document.querySelector('.js-edit-cluster-form').dataset;
-
- this.state = {
- statusPath: dataset.statusPath,
- clusterStatus: dataset.clusterStatus,
- clusterStatusReason: dataset.clusterStatusReason,
- toggleStatus: dataset.toggleStatus,
- };
-
- this.service = new ClusterService({ endpoint: this.state.statusPath });
- this.toggleButton = document.querySelector('.js-toggle-cluster');
- this.toggleInput = document.querySelector('.js-toggle-input');
- this.errorContainer = document.querySelector('.js-cluster-error');
- this.successContainer = document.querySelector('.js-cluster-success');
- this.creatingContainer = document.querySelector('.js-cluster-creating');
- this.errorReasonContainer = this.errorContainer.querySelector('.js-error-reason');
-
- this.toggleButton.addEventListener('click', this.toggle.bind(this));
-
- if (this.state.clusterStatus !== 'created') {
- this.updateContainer(this.state.clusterStatus, this.state.clusterStatusReason);
- }
-
- if (this.state.statusPath) {
- this.initPolling();
- }
- }
-
- toggle() {
- this.toggleButton.classList.toggle('checked');
- this.toggleInput.setAttribute('value', this.toggleButton.classList.contains('checked').toString());
- }
-
- initPolling() {
- this.poll = new Poll({
- resource: this.service,
- method: 'fetchData',
- successCallback: data => this.handleSuccess(data),
- errorCallback: () => Clusters.handleError(),
- });
-
- if (!Visibility.hidden()) {
- this.poll.makeRequest();
- } else {
- this.service.fetchData()
- .then(data => this.handleSuccess(data))
- .catch(() => Clusters.handleError());
- }
-
- Visibility.change(() => {
- if (!Visibility.hidden()) {
- this.poll.restart();
- } else {
- this.poll.stop();
- }
- });
- }
-
- static handleError() {
- Flash(s__('ClusterIntegration|Something went wrong on our end.'));
- }
-
- handleSuccess(data) {
- const { status, status_reason } = data.data;
- this.updateContainer(status, status_reason);
- }
-
- hideAll() {
- this.errorContainer.classList.add('hidden');
- this.successContainer.classList.add('hidden');
- this.creatingContainer.classList.add('hidden');
- }
-
- updateContainer(status, error) {
- this.hideAll();
- switch (status) {
- case 'created':
- this.successContainer.classList.remove('hidden');
- break;
- case 'errored':
- this.errorContainer.classList.remove('hidden');
- this.errorReasonContainer.textContent = error;
- break;
- case 'scheduled':
- case 'creating':
- this.creatingContainer.classList.remove('hidden');
- break;
- default:
- this.hideAll();
- }
- }
-}
diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js
new file mode 100644
index 00000000000..dc443475952
--- /dev/null
+++ b/app/assets/javascripts/clusters/clusters_bundle.js
@@ -0,0 +1,221 @@
+import Visibility from 'visibilityjs';
+import Vue from 'vue';
+import { s__, sprintf } from '../locale';
+import Flash from '../flash';
+import Poll from '../lib/utils/poll';
+import initSettingsPanels from '../settings_panels';
+import eventHub from './event_hub';
+import {
+ APPLICATION_INSTALLED,
+ REQUEST_LOADING,
+ REQUEST_SUCCESS,
+ REQUEST_FAILURE,
+} from './constants';
+import ClustersService from './services/clusters_service';
+import ClustersStore from './stores/clusters_store';
+import applications from './components/applications.vue';
+
+/**
+ * Cluster page has 2 separate parts:
+ * Toggle button and applications section
+ *
+ * - Polling status while creating or scheduled
+ * - Update status area with the response result
+ */
+
+export default class Clusters {
+ constructor() {
+ const {
+ statusPath,
+ installHelmPath,
+ installIngressPath,
+ installRunnerPath,
+ clusterStatus,
+ clusterStatusReason,
+ helpPath,
+ } = document.querySelector('.js-edit-cluster-form').dataset;
+
+ this.store = new ClustersStore();
+ this.store.setHelpPath(helpPath);
+ this.store.updateStatus(clusterStatus);
+ this.store.updateStatusReason(clusterStatusReason);
+ this.service = new ClustersService({
+ endpoint: statusPath,
+ installHelmEndpoint: installHelmPath,
+ installIngressEndpoint: installIngressPath,
+ installRunnerEndpoint: installRunnerPath,
+ });
+
+ this.toggle = this.toggle.bind(this);
+ this.installApplication = this.installApplication.bind(this);
+
+ this.toggleButton = document.querySelector('.js-toggle-cluster');
+ this.toggleInput = document.querySelector('.js-toggle-input');
+ this.errorContainer = document.querySelector('.js-cluster-error');
+ this.successContainer = document.querySelector('.js-cluster-success');
+ this.creatingContainer = document.querySelector('.js-cluster-creating');
+ this.errorReasonContainer = this.errorContainer.querySelector('.js-error-reason');
+ this.successApplicationContainer = document.querySelector('.js-cluster-application-notice');
+
+ initSettingsPanels();
+ this.initApplications();
+
+ if (this.store.state.status !== 'created') {
+ this.updateContainer(null, this.store.state.status, this.store.state.statusReason);
+ }
+
+ this.addListeners();
+ if (statusPath) {
+ this.initPolling();
+ }
+ }
+
+ initApplications() {
+ const store = this.store;
+ const el = document.querySelector('#js-cluster-applications');
+
+ this.applications = new Vue({
+ el,
+ components: {
+ applications,
+ },
+ data() {
+ return {
+ state: store.state,
+ };
+ },
+ render(createElement) {
+ return createElement('applications', {
+ props: {
+ applications: this.state.applications,
+ helpPath: this.state.helpPath,
+ },
+ });
+ },
+ });
+ }
+
+ addListeners() {
+ this.toggleButton.addEventListener('click', this.toggle);
+ eventHub.$on('installApplication', this.installApplication);
+ }
+
+ removeListeners() {
+ this.toggleButton.removeEventListener('click', this.toggle);
+ eventHub.$off('installApplication', this.installApplication);
+ }
+
+ initPolling() {
+ this.poll = new Poll({
+ resource: this.service,
+ method: 'fetchData',
+ successCallback: data => this.handleSuccess(data),
+ errorCallback: () => Clusters.handleError(),
+ });
+
+ if (!Visibility.hidden()) {
+ this.poll.makeRequest();
+ } else {
+ this.service.fetchData()
+ .then(data => this.handleSuccess(data))
+ .catch(() => Clusters.handleError());
+ }
+
+ Visibility.change(() => {
+ if (!Visibility.hidden() && !this.destroyed) {
+ this.poll.restart();
+ } else {
+ this.poll.stop();
+ }
+ });
+ }
+
+ static handleError() {
+ Flash(s__('ClusterIntegration|Something went wrong on our end.'));
+ }
+
+ handleSuccess(data) {
+ const prevStatus = this.store.state.status;
+ const prevApplicationMap = Object.assign({}, this.store.state.applications);
+
+ this.store.updateStateFromServer(data.data);
+
+ this.checkForNewInstalls(prevApplicationMap, this.store.state.applications);
+ this.updateContainer(prevStatus, this.store.state.status, this.store.state.statusReason);
+ }
+
+ toggle() {
+ this.toggleButton.classList.toggle('checked');
+ this.toggleInput.setAttribute('value', this.toggleButton.classList.contains('checked').toString());
+ }
+
+ hideAll() {
+ this.errorContainer.classList.add('hidden');
+ this.successContainer.classList.add('hidden');
+ this.creatingContainer.classList.add('hidden');
+ }
+
+ checkForNewInstalls(prevApplicationMap, newApplicationMap) {
+ const appTitles = Object.keys(newApplicationMap)
+ .filter(appId => newApplicationMap[appId].status === APPLICATION_INSTALLED &&
+ prevApplicationMap[appId].status !== APPLICATION_INSTALLED &&
+ prevApplicationMap[appId].status !== null)
+ .map(appId => newApplicationMap[appId].title);
+
+ if (appTitles.length > 0) {
+ const text = sprintf(s__('ClusterIntegration|%{appList} was successfully installed on your cluster'), {
+ appList: appTitles.join(', '),
+ });
+ Flash(text, 'notice', this.successApplicationContainer);
+ }
+ }
+
+ updateContainer(prevStatus, status, error) {
+ this.hideAll();
+
+ // We poll all the time but only want the `created` banner to show when newly created
+ if (this.store.state.status !== 'created' || prevStatus !== this.store.state.status) {
+ switch (status) {
+ case 'created':
+ this.successContainer.classList.remove('hidden');
+ break;
+ case 'errored':
+ this.errorContainer.classList.remove('hidden');
+ this.errorReasonContainer.textContent = error;
+ break;
+ case 'scheduled':
+ case 'creating':
+ this.creatingContainer.classList.remove('hidden');
+ break;
+ default:
+ this.hideAll();
+ }
+ }
+ }
+
+ installApplication(appId) {
+ this.store.updateAppProperty(appId, 'requestStatus', REQUEST_LOADING);
+ this.store.updateAppProperty(appId, 'requestReason', null);
+
+ this.service.installApplication(appId)
+ .then(() => {
+ this.store.updateAppProperty(appId, 'requestStatus', REQUEST_SUCCESS);
+ })
+ .catch(() => {
+ this.store.updateAppProperty(appId, 'requestStatus', REQUEST_FAILURE);
+ this.store.updateAppProperty(appId, 'requestReason', s__('ClusterIntegration|Request to begin installing failed'));
+ });
+ }
+
+ destroy() {
+ this.destroyed = true;
+
+ this.removeListeners();
+
+ if (this.poll) {
+ this.poll.stop();
+ }
+
+ this.applications.$destroy();
+ }
+}
diff --git a/app/assets/javascripts/clusters/components/application_row.vue b/app/assets/javascripts/clusters/components/application_row.vue
new file mode 100644
index 00000000000..872abf03ef1
--- /dev/null
+++ b/app/assets/javascripts/clusters/components/application_row.vue
@@ -0,0 +1,185 @@
+<script>
+import { s__, sprintf } from '../../locale';
+import eventHub from '../event_hub';
+import loadingButton from '../../vue_shared/components/loading_button.vue';
+import {
+ APPLICATION_NOT_INSTALLABLE,
+ APPLICATION_SCHEDULED,
+ APPLICATION_INSTALLABLE,
+ APPLICATION_INSTALLING,
+ APPLICATION_INSTALLED,
+ APPLICATION_ERROR,
+ REQUEST_LOADING,
+ REQUEST_SUCCESS,
+ REQUEST_FAILURE,
+} from '../constants';
+
+export default {
+ props: {
+ id: {
+ type: String,
+ required: true,
+ },
+ title: {
+ type: String,
+ required: true,
+ },
+ titleLink: {
+ type: String,
+ required: false,
+ },
+ description: {
+ type: String,
+ required: true,
+ },
+ status: {
+ type: String,
+ required: false,
+ },
+ statusReason: {
+ type: String,
+ required: false,
+ },
+ requestStatus: {
+ type: String,
+ required: false,
+ },
+ requestReason: {
+ type: String,
+ required: false,
+ },
+ },
+ components: {
+ loadingButton,
+ },
+ computed: {
+ rowJsClass() {
+ return `js-cluster-application-row-${this.id}`;
+ },
+ installButtonLoading() {
+ return !this.status ||
+ this.status === APPLICATION_SCHEDULED ||
+ this.status === APPLICATION_INSTALLING ||
+ this.requestStatus === REQUEST_LOADING;
+ },
+ installButtonDisabled() {
+ // Avoid the potential for the real-time data to say APPLICATION_INSTALLABLE but
+ // we already made a request to install and are just waiting for the real-time
+ // to sync up.
+ return (this.status !== APPLICATION_INSTALLABLE && this.status !== APPLICATION_ERROR) ||
+ this.requestStatus === REQUEST_LOADING ||
+ this.requestStatus === REQUEST_SUCCESS;
+ },
+ installButtonLabel() {
+ let label;
+ if (
+ this.status === APPLICATION_NOT_INSTALLABLE ||
+ this.status === APPLICATION_INSTALLABLE ||
+ this.status === APPLICATION_ERROR
+ ) {
+ label = s__('ClusterIntegration|Install');
+ } else if (this.status === APPLICATION_SCHEDULED || this.status === APPLICATION_INSTALLING) {
+ label = s__('ClusterIntegration|Installing');
+ } else if (this.status === APPLICATION_INSTALLED) {
+ label = s__('ClusterIntegration|Installed');
+ }
+
+ return label;
+ },
+ hasError() {
+ return this.status === APPLICATION_ERROR || this.requestStatus === REQUEST_FAILURE;
+ },
+ generalErrorDescription() {
+ return sprintf(
+ s__('ClusterIntegration|Something went wrong while installing %{title}'), {
+ title: this.title,
+ },
+ );
+ },
+ },
+ methods: {
+ installClicked() {
+ eventHub.$emit('installApplication', this.id);
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ class="gl-responsive-table-row gl-responsive-table-row-col-span"
+ :class="rowJsClass"
+ >
+ <div
+ class="gl-responsive-table-row-layout"
+ role="row"
+ >
+ <a
+ v-if="titleLink"
+ :href="titleLink"
+ target="blank"
+ rel="noopener noreferrer"
+ role="gridcell"
+ class="table-section section-15 section-align-top js-cluster-application-title"
+ >
+ {{ title }}
+ </a>
+ <span
+ v-else
+ class="table-section section-15 section-align-top js-cluster-application-title"
+ >
+ {{ title }}
+ </span>
+ <div
+ class="table-section section-wrap"
+ role="gridcell"
+ >
+ <div v-html="description"></div>
+ </div>
+ <div
+ class="table-section table-button-footer section-15 section-align-top"
+ role="gridcell"
+ >
+ <div class="btn-group table-action-buttons">
+ <loading-button
+ class="js-cluster-application-install-button"
+ :loading="installButtonLoading"
+ :disabled="installButtonDisabled"
+ :label="installButtonLabel"
+ @click="installClicked"
+ />
+ </div>
+ </div>
+ </div>
+ <div
+ v-if="hasError"
+ class="gl-responsive-table-row-layout"
+ role="row"
+ >
+ <div
+ class="alert alert-danger alert-block append-bottom-0 table-section section-100"
+ role="gridcell"
+ >
+ <div>
+ <p class="js-cluster-application-general-error-message">
+ {{ generalErrorDescription }}
+ </p>
+ <ul v-if="statusReason || requestReason">
+ <li
+ v-if="statusReason"
+ class="js-cluster-application-status-error-message"
+ >
+ {{ statusReason }}
+ </li>
+ <li
+ v-if="requestReason"
+ class="js-cluster-application-request-error-message"
+ >
+ {{ requestReason }}
+ </li>
+ </ul>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue
new file mode 100644
index 00000000000..e5ae439d26e
--- /dev/null
+++ b/app/assets/javascripts/clusters/components/applications.vue
@@ -0,0 +1,114 @@
+<script>
+import _ from 'underscore';
+import { s__, sprintf } from '../../locale';
+import applicationRow from './application_row.vue';
+
+export default {
+ props: {
+ applications: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ helpPath: {
+ type: String,
+ required: false,
+ },
+ },
+ components: {
+ applicationRow,
+ },
+ computed: {
+ generalApplicationDescription() {
+ return sprintf(
+ _.escape(s__('ClusterIntegration|Install applications on your cluster. Read more about %{helpLink}')), {
+ helpLink: `<a href="${this.helpPath}">
+ ${_.escape(s__('ClusterIntegration|installing applications'))}
+ </a>`,
+ },
+ false,
+ );
+ },
+ helmTillerDescription() {
+ return _.escape(s__(
+ `ClusterIntegration|Helm streamlines installing and managing Kubernets applications.
+ Tiller runs inside of your Kubernetes Cluster, and manages
+ releases of your charts.`,
+ ));
+ },
+ ingressDescription() {
+ const descriptionParagraph = _.escape(s__(
+ `ClusterIntegration|Ingress gives you a way to route requests to services based on the
+ request host or path, centralizing a number of services into a single entrypoint.`,
+ ));
+
+ const extraCostParagraph = sprintf(
+ _.escape(s__('ClusterIntegration|%{boldNotice} This will add some extra resources like a load balancer, which incur additional costs. See %{pricingLink}')), {
+ boldNotice: `<strong>${_.escape(s__('ClusterIntegration|Note:'))}</strong>`,
+ pricingLink: `<a href="https://cloud.google.com/compute/pricing#lb" target="_blank" rel="noopener noreferrer">
+ ${_.escape(s__('ClusterIntegration|GKE pricing'))}
+ </a>`,
+ },
+ false,
+ );
+
+ return `
+ <p>
+ ${descriptionParagraph}
+ </p>
+ <p class="append-bottom-0">
+ ${extraCostParagraph}
+ </p>
+ `;
+ },
+ gitlabRunnerDescription() {
+ return _.escape(s__(
+ `ClusterIntegration|GitLab Runner is the open source project that is used to run your jobs
+ and send the results back to GitLab.`,
+ ));
+ },
+ },
+};
+</script>
+
+<template>
+ <section class="settings no-animate expanded">
+ <div class="settings-header">
+ <h4>
+ {{ s__('ClusterIntegration|Applications') }}
+ </h4>
+ <p
+ class="append-bottom-0"
+ v-html="generalApplicationDescription"
+ >
+ </p>
+ </div>
+
+ <div class="settings-content">
+ <div class="append-bottom-20">
+ <application-row
+ id="helm"
+ :title="applications.helm.title"
+ title-link="https://docs.helm.sh/"
+ :description="helmTillerDescription"
+ :status="applications.helm.status"
+ :status-reason="applications.helm.statusReason"
+ :request-status="applications.helm.requestStatus"
+ :request-reason="applications.helm.requestReason"
+ />
+ <application-row
+ id="ingress"
+ :title="applications.ingress.title"
+ title-link="https://kubernetes.io/docs/concepts/services-networking/ingress/"
+ :description="ingressDescription"
+ :status="applications.ingress.status"
+ :status-reason="applications.ingress.statusReason"
+ :request-status="applications.ingress.requestStatus"
+ :request-reason="applications.ingress.requestReason"
+ />
+ <!-- NOTE: Don't forget to update `clusters.scss` min-height for this block and uncomment `application_spec` tests -->
+ <!-- Add GitLab Runner row, all other plumbing is complete -->
+ </div>
+ </div>
+ </section>
+</template>
diff --git a/app/assets/javascripts/clusters/constants.js b/app/assets/javascripts/clusters/constants.js
new file mode 100644
index 00000000000..93223aefff8
--- /dev/null
+++ b/app/assets/javascripts/clusters/constants.js
@@ -0,0 +1,12 @@
+// These need to match what is returned from the server
+export const APPLICATION_NOT_INSTALLABLE = 'not_installable';
+export const APPLICATION_INSTALLABLE = 'installable';
+export const APPLICATION_SCHEDULED = 'scheduled';
+export const APPLICATION_INSTALLING = 'installing';
+export const APPLICATION_INSTALLED = 'installed';
+export const APPLICATION_ERROR = 'errored';
+
+// These are only used client-side
+export const REQUEST_LOADING = 'request-loading';
+export const REQUEST_SUCCESS = 'request-success';
+export const REQUEST_FAILURE = 'request-failure';
diff --git a/app/assets/javascripts/clusters/event_hub.js b/app/assets/javascripts/clusters/event_hub.js
new file mode 100644
index 00000000000..0948c2e5352
--- /dev/null
+++ b/app/assets/javascripts/clusters/event_hub.js
@@ -0,0 +1,3 @@
+import Vue from 'vue';
+
+export default new Vue();
diff --git a/app/assets/javascripts/clusters/services/clusters_service.js b/app/assets/javascripts/clusters/services/clusters_service.js
new file mode 100644
index 00000000000..0ac8e68187d
--- /dev/null
+++ b/app/assets/javascripts/clusters/services/clusters_service.js
@@ -0,0 +1,24 @@
+import axios from 'axios';
+import setAxiosCsrfToken from '../../lib/utils/axios_utils';
+
+export default class ClusterService {
+ constructor(options = {}) {
+ setAxiosCsrfToken();
+
+ this.options = options;
+ this.appInstallEndpointMap = {
+ helm: this.options.installHelmEndpoint,
+ ingress: this.options.installIngressEndpoint,
+ runner: this.options.installRunnerEndpoint,
+ };
+ }
+
+ fetchData() {
+ return axios.get(this.options.endpoint);
+ }
+
+ installApplication(appId) {
+ const endpoint = this.appInstallEndpointMap[appId];
+ return axios.post(endpoint);
+ }
+}
diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js
new file mode 100644
index 00000000000..e731cdc3042
--- /dev/null
+++ b/app/assets/javascripts/clusters/stores/clusters_store.js
@@ -0,0 +1,68 @@
+import { s__ } from '../../locale';
+
+export default class ClusterStore {
+ constructor() {
+ this.state = {
+ helpPath: null,
+ status: null,
+ statusReason: null,
+ applications: {
+ helm: {
+ title: s__('ClusterIntegration|Helm Tiller'),
+ status: null,
+ statusReason: null,
+ requestStatus: null,
+ requestReason: null,
+ },
+ ingress: {
+ title: s__('ClusterIntegration|Ingress'),
+ status: null,
+ statusReason: null,
+ requestStatus: null,
+ requestReason: null,
+ },
+ runner: {
+ title: s__('ClusterIntegration|GitLab Runner'),
+ status: null,
+ statusReason: null,
+ requestStatus: null,
+ requestReason: null,
+ },
+ },
+ };
+ }
+
+ setHelpPath(helpPath) {
+ this.state.helpPath = helpPath;
+ }
+
+ updateStatus(status) {
+ this.state.status = status;
+ }
+
+ updateStatusReason(reason) {
+ this.state.statusReason = reason;
+ }
+
+ updateAppProperty(appId, prop, value) {
+ this.state.applications[appId][prop] = value;
+ }
+
+ updateStateFromServer(serverState = {}) {
+ this.state.status = serverState.status;
+ this.state.statusReason = serverState.status_reason;
+ serverState.applications.forEach((serverAppEntry) => {
+ const {
+ name: appId,
+ status,
+ status_reason: statusReason,
+ } = serverAppEntry;
+
+ this.state.applications[appId] = {
+ ...(this.state.applications[appId] || {}),
+ status,
+ statusReason,
+ };
+ });
+ }
+}
diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js
index ae6b8902032..9b952ea7b60 100644
--- a/app/assets/javascripts/commits.js
+++ b/app/assets/javascripts/commits.js
@@ -3,6 +3,8 @@
prefer-template, object-shorthand, prefer-arrow-callback */
/* global Pager */
+import { pluralize } from './lib/utils/text_utility';
+
export default (function () {
const CommitsList = {};
@@ -86,7 +88,7 @@ export default (function () {
// Update commits count in the previous commits header.
commitsCount += Number($(processedData).nextUntil('li.js-commit-header').first().find('li.commit').length);
- $commitsHeadersLast.find('span.commits-count').text(`${commitsCount} ${gl.text.pluralize('commit', commitsCount)}`);
+ $commitsHeadersLast.find('span.commits-count').text(`${commitsCount} ${pluralize('commit', commitsCount)}`);
}
gl.utils.localTimeAgo($processedData.find('.js-timeago'));
diff --git a/app/assets/javascripts/create_label.js b/app/assets/javascripts/create_label.js
index 3bed0678350..9a4c9bfcc80 100644
--- a/app/assets/javascripts/create_label.js
+++ b/app/assets/javascripts/create_label.js
@@ -1,5 +1,6 @@
/* eslint-disable func-names, prefer-arrow-callback */
import Api from './api';
+import { humanize } from './lib/utils/text_utility';
export default class CreateLabelDropdown {
constructor($el, namespacePath, projectPath) {
@@ -107,7 +108,7 @@ export default class CreateLabelDropdown {
errors = label.message;
} else {
errors = Object.keys(label.message).map(key =>
- `${gl.text.humanize(key)} ${label.message[key].join(', ')}`,
+ `${humanize(key)} ${label.message[key].join(', ')}`,
).join('<br/>');
}
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js
index 8bf9ae17de0..a8cd8c20f8f 100644
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js
@@ -1,7 +1,7 @@
/* eslint-disable no-param-reassign */
import { __ } from '../locale';
-import '../lib/utils/text_utility';
+import { dasherize } from '../lib/utils/text_utility';
import DEFAULT_EVENT_OBJECTS from './default_event_objects';
const EMPTY_STAGE_TEXTS = {
@@ -36,7 +36,7 @@ export default {
});
newData.stages.forEach((item) => {
- const stageSlug = gl.text.dasherize(item.name.toLowerCase());
+ const stageSlug = dasherize(item.name.toLowerCase());
item.active = false;
item.isUserAllowed = data.permissions[stageSlug];
item.emptyStageText = EMPTY_STAGE_TEXTS[stageSlug];
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index 760fb0cdf67..d716218d9a4 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -1,4 +1,5 @@
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len */
+import { s__ } from './locale';
/* global ProjectSelect */
import IssuableIndex from './issuable_index';
/* global Milestone */
@@ -19,19 +20,20 @@ import groupsSelect from './groups_select';
import NamespaceSelect from './namespace_select';
/* global NewCommitForm */
/* global NewBranchForm */
-/* global Project */
-/* global ProjectAvatar */
+import Project from './project';
+import projectAvatar from './project_avatar';
/* global MergeRequest */
/* global Compare */
/* global CompareAutocomplete */
/* global ProjectFindFile */
/* global ProjectNew */
/* global ProjectShow */
-/* global ProjectImport */
+import projectImport from './project_import';
import Labels from './labels';
import LabelManager from './label_manager';
/* global Sidebar */
+import Flash from './flash';
import CommitsList from './commits';
import Issue from './issue';
import BindInOut from './behaviors/bind_in_out';
@@ -376,7 +378,7 @@ import Diff from './diff';
initSettingsPanels();
break;
case 'projects:imports:show':
- new ProjectImport();
+ projectImport();
break;
case 'projects:pipelines:new':
new NewBranchForm($('.js-new-pipeline-form'));
@@ -543,9 +545,12 @@ import Diff from './diff';
new DueDateSelectors();
break;
case 'projects:clusters:show':
- import(/* webpackChunkName: "clusters" */ './clusters')
+ import(/* webpackChunkName: "clusters" */ './clusters/clusters_bundle')
.then(cluster => new cluster.default()) // eslint-disable-line new-cap
- .catch(() => {});
+ .catch((err) => {
+ Flash(s__('ClusterIntegration|Problem setting up the cluster JavaScript'));
+ throw err;
+ });
break;
}
switch (path[0]) {
@@ -599,7 +604,7 @@ import Diff from './diff';
break;
case 'projects':
new Project();
- new ProjectAvatar();
+ projectAvatar();
switch (path[1]) {
case 'compare':
new CompareAutocomplete();
diff --git a/app/assets/javascripts/emoji/support/is_emoji_unicode_supported.js b/app/assets/javascripts/emoji/support/is_emoji_unicode_supported.js
index 3fd23efa9f8..e9defb62cf8 100644
--- a/app/assets/javascripts/emoji/support/is_emoji_unicode_supported.js
+++ b/app/assets/javascripts/emoji/support/is_emoji_unicode_supported.js
@@ -7,6 +7,17 @@ function isFlagEmoji(emojiUnicode) {
return emojiUnicode.length === 4 && cp >= flagACodePoint && cp <= flagZCodePoint;
}
+// Tested on mac OS 10.12.6 and Windows 10 FCU, it renders as two separate characters
+const baseFlagCodePoint = 127987; // parseInt('1F3F3', 16)
+const rainbowCodePoint = 127752; // parseInt('1F308', 16)
+function isRainbowFlagEmoji(emojiUnicode) {
+ const characters = Array.from(emojiUnicode);
+ // Length 4 because flags are made of 2 characters which are surrogate pairs
+ return emojiUnicode.length === 4 &&
+ characters[0].codePointAt(0) === baseFlagCodePoint &&
+ characters[1].codePointAt(0) === rainbowCodePoint;
+}
+
// Chrome <57 renders keycaps oddly
// See https://bugs.chromium.org/p/chromium/issues/detail?id=632294
// Same issue on Windows also fixed in Chrome 57, http://i.imgur.com/rQF7woO.png
@@ -57,9 +68,11 @@ function isPersonZwjEmoji(emojiUnicode) {
// in `isEmojiUnicodeSupported` logic
function checkFlagEmojiSupport(unicodeSupportMap, emojiUnicode) {
const isFlagResult = isFlagEmoji(emojiUnicode);
+ const isRainbowFlagResult = isRainbowFlagEmoji(emojiUnicode);
return (
(unicodeSupportMap.flag && isFlagResult) ||
- !isFlagResult
+ (unicodeSupportMap.rainbowFlag && isRainbowFlagResult) ||
+ (!isFlagResult && !isRainbowFlagResult)
);
}
@@ -113,6 +126,7 @@ function isEmojiUnicodeSupported(unicodeSupportMap = {}, emojiUnicode, unicodeVe
export {
isEmojiUnicodeSupported as default,
isFlagEmoji,
+ isRainbowFlagEmoji,
isKeycapEmoji,
isSkinToneComboEmoji,
isHorceRacingSkinToneComboEmoji,
diff --git a/app/assets/javascripts/emoji/support/unicode_support_map.js b/app/assets/javascripts/emoji/support/unicode_support_map.js
index 755381c2f95..c18d07dad43 100644
--- a/app/assets/javascripts/emoji/support/unicode_support_map.js
+++ b/app/assets/javascripts/emoji/support/unicode_support_map.js
@@ -1,5 +1,7 @@
import AccessorUtilities from '../../lib/utils/accessor';
+const GL_EMOJI_VERSION = '0.2.0';
+
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}',
@@ -13,6 +15,7 @@ const unicodeSupportTestMap = {
horseRacing: '\u{1F3C7}\u{1F3FF}',
// US flag, http://emojipedia.org/flags/
flag: '\u{1F1FA}\u{1F1F8}',
+ rainbowFlag: '\u{1F3F3}\u{1F308}',
// http://emojipedia.org/modifiers/
skinToneModifier: [
// spy_tone5
@@ -141,23 +144,31 @@ function generateUnicodeSupportMap(testMap) {
}
export default function getUnicodeSupportMap() {
- let unicodeSupportMap;
- let userAgentFromCache;
-
const isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe();
- if (isLocalStorageAvailable) userAgentFromCache = window.localStorage.getItem('gl-emoji-user-agent');
+ let glEmojiVersionFromCache;
+ let userAgentFromCache;
+ if (isLocalStorageAvailable) {
+ glEmojiVersionFromCache = window.localStorage.getItem('gl-emoji-version');
+ userAgentFromCache = window.localStorage.getItem('gl-emoji-user-agent');
+ }
+ let unicodeSupportMap;
try {
unicodeSupportMap = JSON.parse(window.localStorage.getItem('gl-emoji-unicode-support-map'));
} catch (err) {
// swallow
}
- if (!unicodeSupportMap || userAgentFromCache !== navigator.userAgent) {
+ if (
+ !unicodeSupportMap ||
+ glEmojiVersionFromCache !== GL_EMOJI_VERSION ||
+ userAgentFromCache !== navigator.userAgent
+ ) {
unicodeSupportMap = generateUnicodeSupportMap(unicodeSupportTestMap);
if (isLocalStorageAvailable) {
+ window.localStorage.setItem('gl-emoji-version', GL_EMOJI_VERSION);
window.localStorage.setItem('gl-emoji-user-agent', navigator.userAgent);
window.localStorage.setItem('gl-emoji-unicode-support-map', JSON.stringify(unicodeSupportMap));
}
diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue
index fc0308b81ba..9d25f806c0d 100644
--- a/app/assets/javascripts/environments/components/environment_item.vue
+++ b/app/assets/javascripts/environments/components/environment_item.vue
@@ -2,7 +2,7 @@
import Timeago from 'timeago.js';
import _ from 'underscore';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
-import '../../lib/utils/text_utility';
+import { humanize } from '../../lib/utils/text_utility';
import ActionsComponent from './environment_actions.vue';
import ExternalUrlComponent from './environment_external_url.vue';
import StopComponent from './environment_stop.vue';
@@ -139,7 +139,7 @@ export default {
if (this.hasManualActions) {
return this.model.last_deployment.manual_actions.map((action) => {
const parsedAction = {
- name: gl.text.humanize(action.name),
+ name: humanize(action.name),
play_path: action.play_path,
playable: action.playable,
};
diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js b/app/assets/javascripts/filtered_search/dropdown_utils.js
index 8d711e3213c..cf8a9b0402b 100644
--- a/app/assets/javascripts/filtered_search/dropdown_utils.js
+++ b/app/assets/javascripts/filtered_search/dropdown_utils.js
@@ -147,6 +147,16 @@ class DropdownUtils {
return dataValue !== null;
}
+ static getVisualTokenValues(visualToken) {
+ const tokenName = visualToken && visualToken.querySelector('.name').textContent.trim();
+ let tokenValue = visualToken && visualToken.querySelector('.value') && visualToken.querySelector('.value').textContent.trim();
+ if (tokenName === 'label' && tokenValue) {
+ // remove leading symbol and wrapping quotes
+ tokenValue = tokenValue.replace(/^~("|')?(.*)/, '$2').replace(/("|')$/, '');
+ }
+ return { tokenName, tokenValue };
+ }
+
// Determines the full search query (visual tokens + input)
static getSearchQuery(untilInput = false) {
const container = FilteredSearchContainer.container;
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
index 7b233842d5a..69c57f923b6 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -185,8 +185,8 @@ class FilteredSearchManager {
if (e.keyCode === 8 || e.keyCode === 46) {
const { lastVisualToken } = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
- const sanitizedTokenName = lastVisualToken && lastVisualToken.querySelector('.name').textContent.trim();
- const canEdit = sanitizedTokenName && this.canEdit && this.canEdit(sanitizedTokenName);
+ const { tokenName, tokenValue } = gl.DropdownUtils.getVisualTokenValues(lastVisualToken);
+ const canEdit = tokenName && this.canEdit && this.canEdit(tokenName, tokenValue);
if (this.filteredSearchInput.value === '' && lastVisualToken && canEdit) {
this.filteredSearchInput.value = gl.FilteredSearchVisualTokens.getLastTokenPartial();
gl.FilteredSearchVisualTokens.removeLastTokenPartial();
@@ -336,8 +336,8 @@ class FilteredSearchManager {
let canClearToken = t.classList.contains('js-visual-token');
if (canClearToken) {
- const tokenKey = t.querySelector('.name').textContent.trim();
- canClearToken = this.canEdit && this.canEdit(tokenKey);
+ const { tokenName, tokenValue } = gl.DropdownUtils.getVisualTokenValues(t);
+ canClearToken = this.canEdit && this.canEdit(tokenName, tokenValue);
}
if (canClearToken) {
@@ -469,7 +469,7 @@ class FilteredSearchManager {
}
hasFilteredSearch = true;
- const canEdit = this.canEdit && this.canEdit(sanitizedKey);
+ const canEdit = this.canEdit && this.canEdit(sanitizedKey, sanitizedValue);
gl.FilteredSearchVisualTokens.addFilterVisualToken(
sanitizedKey,
`${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`,
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 d2f92929b8a..6139e81fe6d 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
@@ -38,21 +38,14 @@ class FilteredSearchVisualTokens {
}
static createVisualTokenElementHTML(canEdit = true) {
- let removeTokenMarkup = '';
- if (canEdit) {
- removeTokenMarkup = `
- <div class="remove-token" role="button">
- <i class="fa fa-close"></i>
- </div>
- `;
- }
-
return `
- <div class="selectable" role="button">
+ <div class="${canEdit ? 'selectable' : 'hidden'}" role="button">
<div class="name"></div>
<div class="value-container">
<div class="value"></div>
- ${removeTokenMarkup}
+ <div class="remove-token" role="button">
+ <i class="fa fa-close"></i>
+ </div>
</div>
</div>
`;
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index 5c624b79d45..a642464c920 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -338,7 +338,8 @@ class GfmAutoComplete {
let resultantValue = value;
if (value && !this.setting.skipSpecialCharacterTest) {
const withoutAt = value.substring(1);
- if (withoutAt && /[^\w\d]/.test(withoutAt)) {
+ const regex = value.charAt() === '~' ? /\W|^\d+$/ : /\W/;
+ if (withoutAt && regex.test(withoutAt)) {
resultantValue = `${value.charAt()}"${withoutAt}"`;
}
}
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index c4202f92443..4e7a6e54f90 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -331,7 +331,7 @@ GitLabDropdown = (function() {
if (_this.dropdown.find('.dropdown-toggle-page').length) {
selector = ".dropdown-page-one " + selector;
}
- return $(selector);
+ return $(selector, this.instance.dropdown);
};
})(this),
data: (function(_this) {
diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js
index 48cd43d3348..d0f9e6af0f8 100644
--- a/app/assets/javascripts/gl_form.js
+++ b/app/assets/javascripts/gl_form.js
@@ -2,6 +2,7 @@
import GfmAutoComplete from './gfm_auto_complete';
import dropzoneInput from './dropzone_input';
+import textUtils from './lib/utils/text_markdown';
export default class GLForm {
constructor(form, enableGFM = false) {
@@ -46,7 +47,7 @@ export default class GLForm {
}
// form and textarea event listeners
this.addEventListeners();
- gl.text.init(this.form);
+ textUtils.init(this.form);
// hide discard button
this.form.find('.js-note-discard').hide();
this.form.show();
@@ -85,7 +86,7 @@ export default class GLForm {
clearEventListeners() {
this.textarea.off('focus');
this.textarea.off('blur');
- gl.text.removeListeners(this.form);
+ textUtils.removeListeners(this.form);
}
addEventListeners() {
diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js
index acd5730cf3c..7de07e9403d 100644
--- a/app/assets/javascripts/issue.js
+++ b/app/assets/javascripts/issue.js
@@ -1,6 +1,6 @@
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, no-underscore-dangle, one-var-declaration-per-line, object-shorthand, no-unused-vars, no-new, comma-dangle, consistent-return, quotes, dot-notation, quote-props, prefer-arrow-callback, max-len */
import 'vendor/jquery.waitforimages';
-import '~/lib/utils/text_utility';
+import { addDelimiter } from './lib/utils/text_utility';
import Flash from './flash';
import TaskList from './task_list';
import CreateMergeRequestDropdown from './create_merge_request_dropdown';
@@ -73,7 +73,7 @@ export default class Issue {
let numProjectIssues = Number(projectIssuesCounter.first().text().trim().replace(/[^\d]/, ''));
numProjectIssues = isClosed ? numProjectIssues - 1 : numProjectIssues + 1;
- projectIssuesCounter.text(gl.text.addDelimiter(numProjectIssues));
+ projectIssuesCounter.text(addDelimiter(numProjectIssues));
if (this.createMergeRequestDropdown) {
if (isClosed) {
diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue
index d1aa83ea57f..e8ac8d3b5bb 100644
--- a/app/assets/javascripts/issue_show/components/app.vue
+++ b/app/assets/javascripts/issue_show/components/app.vue
@@ -29,6 +29,11 @@ export default {
required: false,
default: false,
},
+ showDeleteButton: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
issuableRef: {
type: String,
required: true,
@@ -92,6 +97,11 @@ export default {
type: String,
required: true,
},
+ issuableType: {
+ type: String,
+ required: false,
+ default: 'issue',
+ },
},
data() {
const store = new Store({
@@ -157,21 +167,21 @@ export default {
})
.catch(() => {
eventHub.$emit('close.form');
- window.Flash('Error updating issue');
+ window.Flash(`Error updating ${this.issuableType}`);
});
},
deleteIssuable() {
this.service.deleteIssuable()
.then(res => res.json())
.then((data) => {
- // Stop the poll so we don't get 404's with the issue not existing
+ // Stop the poll so we don't get 404's with the issuable not existing
this.poll.stop();
gl.utils.visitUrl(data.web_url);
})
.catch(() => {
eventHub.$emit('close.form');
- window.Flash('Error deleting issue');
+ window.Flash(`Error deleting ${this.issuableType}`);
});
},
},
@@ -223,6 +233,7 @@ export default {
:markdown-preview-path="markdownPreviewPath"
:project-path="projectPath"
:project-namespace="projectNamespace"
+ :show-delete-button="showDeleteButton"
/>
<div v-else>
<title-component
diff --git a/app/assets/javascripts/issue_show/components/edit_actions.vue b/app/assets/javascripts/issue_show/components/edit_actions.vue
index 8c81575fe6f..a539506bce2 100644
--- a/app/assets/javascripts/issue_show/components/edit_actions.vue
+++ b/app/assets/javascripts/issue_show/components/edit_actions.vue
@@ -13,6 +13,11 @@
type: Object,
required: true,
},
+ showDeleteButton: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
data() {
return {
@@ -23,6 +28,9 @@
isSubmitEnabled() {
return this.formState.title.trim() !== '';
},
+ shouldShowDeleteButton() {
+ return this.canDestroy && this.showDeleteButton;
+ },
},
methods: {
closeForm() {
@@ -62,7 +70,7 @@
Cancel
</button>
<button
- v-if="canDestroy"
+ v-if="shouldShowDeleteButton"
class="btn btn-danger pull-right append-right-default"
:class="{ disabled: deleteLoading }"
type="button"
diff --git a/app/assets/javascripts/issue_show/components/form.vue b/app/assets/javascripts/issue_show/components/form.vue
index 28bf6c67ea5..8bb5c86d567 100644
--- a/app/assets/javascripts/issue_show/components/form.vue
+++ b/app/assets/javascripts/issue_show/components/form.vue
@@ -36,6 +36,11 @@
type: String,
required: true,
},
+ showDeleteButton: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
components: {
lockedWarning,
@@ -81,6 +86,7 @@
:markdown-docs-path="markdownDocsPath" />
<edit-actions
:form-state="formState"
- :can-destroy="canDestroy" />
+ :can-destroy="canDestroy"
+ :show-delete-button="showDeleteButton" />
</form>
</template>
diff --git a/app/assets/javascripts/job.js b/app/assets/javascripts/job.js
index c6b5844dff6..cf8fda9a4fa 100644
--- a/app/assets/javascripts/job.js
+++ b/app/assets/javascripts/job.js
@@ -14,8 +14,8 @@ export default class Job {
this.state = this.options.logState;
this.buildStage = this.options.buildStage;
this.$document = $(document);
+ this.$window = $(window);
this.logBytes = 0;
- this.hasBeenScrolled = false;
this.updateDropdown = this.updateDropdown.bind(this);
this.$buildTrace = $('#build-trace');
@@ -54,23 +54,18 @@ export default class Job {
this.scrollThrottled = _.throttle(this.toggleScroll.bind(this), 100);
- $(window)
+ this.$window
.off('scroll')
.on('scroll', () => {
- const contentHeight = this.$buildTraceOutput.height();
- if (contentHeight > this.windowSize) {
- // means the user did not scroll, the content was updated.
- this.windowSize = contentHeight;
- } else {
- // User scrolled
- this.hasBeenScrolled = true;
+ if (!this.isScrolledToBottom()) {
this.toggleScrollAnimation(false);
+ } else if (this.isScrolledToBottom() && !this.isLogComplete) {
+ this.toggleScrollAnimation(true);
}
-
this.scrollThrottled();
});
- $(window)
+ this.$window
.off('resize.build')
.on('resize.build', _.throttle(this.sidebarOnResize.bind(this), 100));
@@ -99,14 +94,14 @@ export default class Job {
// eslint-disable-next-line class-methods-use-this
canScroll() {
- return $(document).height() > $(window).height();
+ return this.$document.height() > this.$window.height();
}
toggleScroll() {
- const currentPosition = $(document).scrollTop();
- const scrollHeight = $(document).height();
+ const currentPosition = this.$document.scrollTop();
+ const scrollHeight = this.$document.height();
- const windowHeight = $(window).height();
+ const windowHeight = this.$window.height();
if (this.canScroll()) {
if (currentPosition > 0 &&
(scrollHeight - currentPosition !== windowHeight)) {
@@ -119,7 +114,7 @@ export default class Job {
this.toggleDisableButton(this.$scrollTopBtn, true);
this.toggleDisableButton(this.$scrollBottomBtn, false);
- } else if (scrollHeight - currentPosition === windowHeight) {
+ } else if (this.isScrolledToBottom()) {
// User is at the bottom of the build log.
this.toggleDisableButton(this.$scrollTopBtn, false);
@@ -131,9 +126,17 @@ export default class Job {
}
}
+ isScrolledToBottom() {
+ const currentPosition = this.$document.scrollTop();
+ const scrollHeight = this.$document.height();
+
+ const windowHeight = this.$window.height();
+ return scrollHeight - currentPosition === windowHeight;
+ }
+
// eslint-disable-next-line class-methods-use-this
scrollDown() {
- $(document).scrollTop($(document).height());
+ this.$document.scrollTop(this.$document.height());
}
scrollToBottom() {
@@ -143,7 +146,7 @@ export default class Job {
}
scrollToTop() {
- $(document).scrollTop(0);
+ this.$document.scrollTop(0);
this.hasBeenScrolled = true;
this.toggleScroll();
}
@@ -174,7 +177,7 @@ export default class Job {
this.state = log.state;
}
- this.windowSize = this.$buildTraceOutput.height();
+ this.isScrollInBottom = this.isScrolledToBottom();
if (log.append) {
this.$buildTraceOutput.append(log.html);
@@ -194,14 +197,9 @@ export default class Job {
} else {
this.$truncatedInfo.addClass('hidden');
}
+ this.isLogComplete = log.complete;
if (!log.complete) {
- if (!this.hasBeenScrolled) {
- this.toggleScrollAnimation(true);
- } else {
- this.toggleScrollAnimation(false);
- }
-
this.timeout = setTimeout(() => {
this.getBuildTrace();
}, 4000);
@@ -218,7 +216,7 @@ export default class Job {
this.$buildRefreshAnimation.remove();
})
.then(() => {
- if (!this.hasBeenScrolled) {
+ if (this.isScrollInBottom) {
this.scrollDown();
}
})
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index 9b35efcb499..f7a1c9f1e40 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -7,7 +7,7 @@ import DropdownUtils from './filtered_search/dropdown_utils';
import CreateLabelDropdown from './create_label';
export default class LabelsSelect {
- constructor(els) {
+ constructor(els, options = {}) {
var _this, $els;
_this = this;
@@ -57,6 +57,7 @@ export default class LabelsSelect {
labelHTMLTemplate = _.template('<% _.each(labels, function(label){ %> <a href="<%- ["",issueURLSplit[1], issueURLSplit[2],""].join("/") %>issues?label_name[]=<%- encodeURIComponent(label.title) %>"> <span class="label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;"> <%- label.title %> </span> </a> <% }); %>');
labelNoneHTMLTemplate = '<span class="no-value">None</span>';
}
+ const handleClick = options.handleClick;
$sidebarLabelTooltip.tooltip();
@@ -390,6 +391,10 @@ export default class LabelsSelect {
.then(fadeOutLoader)
.catch(fadeOutLoader);
}
+ else if (handleClick) {
+ e.preventDefault();
+ handleClick(label);
+ }
else {
if ($dropdown.hasClass('js-multiselect')) {
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 07899777a1e..195e2ca6a78 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -172,7 +172,6 @@ export const getSelectedFragment = () => {
return documentFragment;
};
-// TODO: Update this name, there is a gl.text.insertText function.
export const insertText = (target, text) => {
// Firefox doesn't support `document.execCommand('insertText', false, text)` on textareas
const selectionStart = target.selectionStart;
@@ -311,6 +310,42 @@ export const setParamInURL = (param, value) => {
};
/**
+ * Given a string of query parameters creates an object.
+ *
+ * @example
+ * `scope=all&page=2` -> { scope: 'all', page: '2'}
+ * `scope=all` -> { scope: 'all' }
+ * ``-> {}
+ * @param {String} query
+ * @returns {Object}
+ */
+export const parseQueryStringIntoObject = (query = '') => {
+ if (query === '') return {};
+
+ return query
+ .split('&')
+ .reduce((acc, element) => {
+ const val = element.split('=');
+ Object.assign(acc, {
+ [val[0]]: decodeURIComponent(val[1]),
+ });
+ return acc;
+ }, {});
+};
+
+export const buildUrlWithCurrentLocation = param => (param ? `${window.location.pathname}${param}` : window.location.pathname);
+
+/**
+ * Based on the current location and the string parameters provided
+ * creates a new entry in the history without reloading the page.
+ *
+ * @param {String} param
+ */
+export const historyPushState = (newUrl) => {
+ window.history.pushState({}, document.title, newUrl);
+};
+
+/**
* Converts permission provided as strings to booleans.
*
* @param {String} string
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index 29fc91733b3..5679b8c9a09 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -2,6 +2,7 @@
import timeago from 'timeago.js';
import dateFormat from 'vendor/date.format';
+import { pluralize } from './text_utility';
import {
lang,
@@ -143,9 +144,9 @@ export function timeIntervalInWords(intervalInSeconds) {
let text = '';
if (minutes >= 1) {
- text = `${minutes} ${gl.text.pluralize('minute', minutes)} ${seconds} ${gl.text.pluralize('second', seconds)}`;
+ text = `${minutes} ${pluralize('minute', minutes)} ${seconds} ${pluralize('second', seconds)}`;
} else {
- text = `${seconds} ${gl.text.pluralize('second', seconds)}`;
+ text = `${seconds} ${pluralize('second', seconds)}`;
}
return text;
}
diff --git a/app/assets/javascripts/lib/utils/number_utils.js b/app/assets/javascripts/lib/utils/number_utils.js
index 917a45eb06b..a02c79b787e 100644
--- a/app/assets/javascripts/lib/utils/number_utils.js
+++ b/app/assets/javascripts/lib/utils/number_utils.js
@@ -52,3 +52,31 @@ export function bytesToKiB(number) {
export function bytesToMiB(number) {
return number / (BYTES_IN_KIB * BYTES_IN_KIB);
}
+
+/**
+ * Utility function that calculates GiB of the given bytes.
+ * @param {Number} number
+ * @returns {Number}
+ */
+export function bytesToGiB(number) {
+ return number / (BYTES_IN_KIB * BYTES_IN_KIB * BYTES_IN_KIB);
+}
+
+/**
+ * Port of rails number_to_human_size
+ * Formats the bytes in number into a more understandable
+ * representation (e.g., giving it 1500 yields 1.5 KB).
+ *
+ * @param {Number} size
+ * @returns {String}
+ */
+export function numberToHumanSize(size) {
+ if (size < BYTES_IN_KIB) {
+ return `${size} bytes`;
+ } else if (size < BYTES_IN_KIB * BYTES_IN_KIB) {
+ return `${bytesToKiB(size).toFixed(2)} KiB`;
+ } else if (size < BYTES_IN_KIB * BYTES_IN_KIB * BYTES_IN_KIB) {
+ return `${bytesToMiB(size).toFixed(2)} MiB`;
+ }
+ return `${bytesToGiB(size).toFixed(2)} GiB`;
+}
diff --git a/app/assets/javascripts/lib/utils/poll.js b/app/assets/javascripts/lib/utils/poll.js
index 1485e900945..65a8cf2c891 100644
--- a/app/assets/javascripts/lib/utils/poll.js
+++ b/app/assets/javascripts/lib/utils/poll.js
@@ -60,7 +60,6 @@ export default class Poll {
checkConditions(response) {
const headers = normalizeHeaders(response.headers);
const pollInterval = parseInt(headers[this.intervalHeader], 10);
-
if (pollInterval > 0 && response.status === httpStatusCodes.OK && this.canPoll) {
this.timeoutID = setTimeout(() => {
this.makeRequest();
@@ -102,7 +101,12 @@ export default class Poll {
/**
* Restarts polling after it has been stoped
*/
- restart() {
+ restart(options) {
+ // update data
+ if (options && options.data) {
+ this.options.data = options.data;
+ }
+
this.canPoll = true;
this.makeRequest();
}
diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js
new file mode 100644
index 00000000000..2dc9cf0cc29
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/text_markdown.js
@@ -0,0 +1,153 @@
+/* eslint-disable import/prefer-default-export, func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, quotes, one-var, one-var-declaration-per-line, operator-assignment, no-else-return, prefer-template, prefer-arrow-callback, no-empty, max-len, consistent-return, no-unused-vars, no-return-assign, max-len, vars-on-top */
+
+const textUtils = {};
+
+textUtils.selectedText = function(text, textarea) {
+ return text.substring(textarea.selectionStart, textarea.selectionEnd);
+};
+
+textUtils.lineBefore = function(text, textarea) {
+ var split;
+ split = text.substring(0, textarea.selectionStart).trim().split('\n');
+ return split[split.length - 1];
+};
+
+textUtils.lineAfter = function(text, textarea) {
+ return text.substring(textarea.selectionEnd).trim().split('\n')[0];
+};
+
+textUtils.blockTagText = function(text, textArea, blockTag, selected) {
+ var lineAfter, lineBefore;
+ lineBefore = this.lineBefore(text, textArea);
+ lineAfter = this.lineAfter(text, textArea);
+ if (lineBefore === blockTag && lineAfter === blockTag) {
+ // To remove the block tag we have to select the line before & after
+ if (blockTag != null) {
+ textArea.selectionStart = textArea.selectionStart - (blockTag.length + 1);
+ textArea.selectionEnd = textArea.selectionEnd + (blockTag.length + 1);
+ }
+ return selected;
+ } else {
+ return blockTag + "\n" + selected + "\n" + blockTag;
+ }
+};
+
+textUtils.insertText = function(textArea, text, tag, blockTag, selected, wrap) {
+ var insertText, inserted, selectedSplit, startChar, removedLastNewLine, removedFirstNewLine, currentLineEmpty, lastNewLine;
+ removedLastNewLine = false;
+ removedFirstNewLine = false;
+ currentLineEmpty = false;
+
+ // Remove the first newline
+ if (selected.indexOf('\n') === 0) {
+ removedFirstNewLine = true;
+ selected = selected.replace(/\n+/, '');
+ }
+
+ // Remove the last newline
+ if (textArea.selectionEnd - textArea.selectionStart > selected.replace(/\n$/, '').length) {
+ removedLastNewLine = true;
+ selected = selected.replace(/\n$/, '');
+ }
+
+ selectedSplit = selected.split('\n');
+
+ if (!wrap) {
+ lastNewLine = textArea.value.substr(0, textArea.selectionStart).lastIndexOf('\n');
+
+ // Check whether the current line is empty or consists only of spaces(=handle as empty)
+ if (/^\s*$/.test(textArea.value.substring(lastNewLine, textArea.selectionStart))) {
+ currentLineEmpty = true;
+ }
+ }
+
+ startChar = !wrap && !currentLineEmpty && textArea.selectionStart > 0 ? '\n' : '';
+
+ if (selectedSplit.length > 1 && (!wrap || (blockTag != null && blockTag !== ''))) {
+ if (blockTag != null && blockTag !== '') {
+ insertText = this.blockTagText(text, textArea, blockTag, selected);
+ } else {
+ insertText = selectedSplit.map(function(val) {
+ if (val.indexOf(tag) === 0) {
+ return "" + (val.replace(tag, ''));
+ } else {
+ return "" + tag + val;
+ }
+ }).join('\n');
+ }
+ } else {
+ insertText = "" + startChar + tag + selected + (wrap ? tag : ' ');
+ }
+
+ if (removedFirstNewLine) {
+ insertText = '\n' + insertText;
+ }
+
+ if (removedLastNewLine) {
+ insertText += '\n';
+ }
+
+ if (document.queryCommandSupported('insertText')) {
+ inserted = document.execCommand('insertText', false, insertText);
+ }
+ if (!inserted) {
+ try {
+ document.execCommand("ms-beginUndoUnit");
+ } catch (error) {}
+ textArea.value = this.replaceRange(text, textArea.selectionStart, textArea.selectionEnd, insertText);
+ try {
+ document.execCommand("ms-endUndoUnit");
+ } catch (error) {}
+ }
+ return this.moveCursor(textArea, tag, wrap, removedLastNewLine);
+};
+
+textUtils.moveCursor = function(textArea, tag, wrapped, removedLastNewLine) {
+ var pos;
+ if (!textArea.setSelectionRange) {
+ return;
+ }
+ if (textArea.selectionStart === textArea.selectionEnd) {
+ if (wrapped) {
+ pos = textArea.selectionStart - tag.length;
+ } else {
+ pos = textArea.selectionStart;
+ }
+
+ if (removedLastNewLine) {
+ pos -= 1;
+ }
+
+ return textArea.setSelectionRange(pos, pos);
+ }
+};
+
+textUtils.updateText = function(textArea, tag, blockTag, wrap) {
+ var $textArea, selected, text;
+ $textArea = $(textArea);
+ textArea = $textArea.get(0);
+ text = $textArea.val();
+ selected = this.selectedText(text, textArea);
+ $textArea.focus();
+ return this.insertText(textArea, text, tag, blockTag, selected, wrap);
+};
+
+textUtils.init = function(form) {
+ var self;
+ self = this;
+ return $('.js-md', form).off('click').on('click', function() {
+ var $this;
+ $this = $(this);
+ return self.updateText($this.closest('.md-area').find('textarea'), $this.data('md-tag'), $this.data('md-block'), !$this.data('md-prepend'));
+ });
+};
+
+textUtils.removeListeners = function(form) {
+ return $('.js-md', form).off('click');
+};
+
+textUtils.replaceRange = function(s, start, end, substitute) {
+ return s.substring(0, start) + substitute + s.substring(end);
+};
+
+export default textUtils;
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index f776829f69c..a1475b92c7e 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -1,18 +1,13 @@
-/* eslint-disable import/prefer-default-export, func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, quotes, one-var, one-var-declaration-per-line, operator-assignment, no-else-return, prefer-template, prefer-arrow-callback, no-empty, max-len, consistent-return, no-unused-vars, no-return-assign, max-len, vars-on-top */
-
-import 'vendor/latinise';
-
-var base;
-var w = window;
-if (w.gl == null) {
- w.gl = {};
-}
-if ((base = w.gl).text == null) {
- base.text = {};
-}
-gl.text.addDelimiter = function(text) {
- return text ? text.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") : text;
-};
+/**
+ * Adds a , to a string composed by numbers, at every 3 chars.
+ *
+ * 2333 -> 2,333
+ * 232324 -> 232,324
+ *
+ * @param {String} text
+ * @returns {String}
+ */
+export const addDelimiter = text => (text ? text.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') : text);
/**
* Returns '99+' for numbers bigger than 99.
@@ -20,178 +15,43 @@ gl.text.addDelimiter = function(text) {
* @param {Number} count
* @return {Number|String}
*/
-export function highCountTrim(count) {
- return count > 99 ? '99+' : count;
-}
-
-gl.text.randomString = function() {
- return Math.random().toString(36).substring(7);
-};
-gl.text.replaceRange = function(s, start, end, substitute) {
- return s.substring(0, start) + substitute + s.substring(end);
-};
-gl.text.getTextWidth = function(text, font) {
- /**
- * Uses canvas.measureText to compute and return the width of the given text of given font in pixels.
- *
- * @param {String} text The text to be rendered.
- * @param {String} font The css font descriptor that text is to be rendered with (e.g. "bold 14px verdana").
- *
- * @see http://stackoverflow.com/questions/118241/calculate-text-width-with-javascript/21015393#21015393
- */
- // re-use canvas object for better performance
- var canvas = gl.text.getTextWidth.canvas || (gl.text.getTextWidth.canvas = document.createElement('canvas'));
- var context = canvas.getContext('2d');
- context.font = font;
- return context.measureText(text).width;
-};
-gl.text.selectedText = function(text, textarea) {
- return text.substring(textarea.selectionStart, textarea.selectionEnd);
-};
-gl.text.lineBefore = function(text, textarea) {
- var split;
- split = text.substring(0, textarea.selectionStart).trim().split('\n');
- return split[split.length - 1];
-};
-gl.text.lineAfter = function(text, textarea) {
- return text.substring(textarea.selectionEnd).trim().split('\n')[0];
-};
-gl.text.blockTagText = function(text, textArea, blockTag, selected) {
- var lineAfter, lineBefore;
- lineBefore = this.lineBefore(text, textArea);
- lineAfter = this.lineAfter(text, textArea);
- if (lineBefore === blockTag && lineAfter === blockTag) {
- // To remove the block tag we have to select the line before & after
- if (blockTag != null) {
- textArea.selectionStart = textArea.selectionStart - (blockTag.length + 1);
- textArea.selectionEnd = textArea.selectionEnd + (blockTag.length + 1);
- }
- return selected;
- } else {
- return blockTag + "\n" + selected + "\n" + blockTag;
- }
-};
-gl.text.insertText = function(textArea, text, tag, blockTag, selected, wrap) {
- var insertText, inserted, selectedSplit, startChar, removedLastNewLine, removedFirstNewLine, currentLineEmpty, lastNewLine;
- removedLastNewLine = false;
- removedFirstNewLine = false;
- currentLineEmpty = false;
-
- // Remove the first newline
- if (selected.indexOf('\n') === 0) {
- removedFirstNewLine = true;
- selected = selected.replace(/\n+/, '');
- }
-
- // Remove the last newline
- if (textArea.selectionEnd - textArea.selectionStart > selected.replace(/\n$/, '').length) {
- removedLastNewLine = true;
- selected = selected.replace(/\n$/, '');
- }
-
- selectedSplit = selected.split('\n');
-
- if (!wrap) {
- lastNewLine = textArea.value.substr(0, textArea.selectionStart).lastIndexOf('\n');
-
- // Check whether the current line is empty or consists only of spaces(=handle as empty)
- if (/^\s*$/.test(textArea.value.substring(lastNewLine, textArea.selectionStart))) {
- currentLineEmpty = true;
- }
- }
-
- startChar = !wrap && !currentLineEmpty && textArea.selectionStart > 0 ? '\n' : '';
+export const highCountTrim = count => (count > 99 ? '99+' : count);
- if (selectedSplit.length > 1 && (!wrap || (blockTag != null && blockTag !== ''))) {
- if (blockTag != null && blockTag !== '') {
- insertText = this.blockTagText(text, textArea, blockTag, selected);
- } else {
- insertText = selectedSplit.map(function(val) {
- if (val.indexOf(tag) === 0) {
- return "" + (val.replace(tag, ''));
- } else {
- return "" + tag + val;
- }
- }).join('\n');
- }
- } else {
- insertText = "" + startChar + tag + selected + (wrap ? tag : ' ');
- }
+/**
+ * Converts first char to uppercase and replaces undercores with spaces
+ * @param {String} string
+ * @requires {String}
+ */
+export const humanize = string => string.charAt(0).toUpperCase() + string.replace(/_/g, ' ').slice(1);
- if (removedFirstNewLine) {
- insertText = '\n' + insertText;
- }
+/**
+ * Adds an 's' to the end of the string when count is bigger than 0
+ * @param {String} str
+ * @param {Number} count
+ * @returns {String}
+ */
+export const pluralize = (str, count) => str + (count > 1 || count === 0 ? 's' : '');
- if (removedLastNewLine) {
- insertText += '\n';
- }
+/**
+ * Replaces underscores with dashes
+ * @param {*} str
+ * @returns {String}
+ */
+export const dasherize = str => str.replace(/[_\s]+/g, '-');
- if (document.queryCommandSupported('insertText')) {
- inserted = document.execCommand('insertText', false, insertText);
- }
- if (!inserted) {
- try {
- document.execCommand("ms-beginUndoUnit");
- } catch (error) {}
- textArea.value = this.replaceRange(text, textArea.selectionStart, textArea.selectionEnd, insertText);
- try {
- document.execCommand("ms-endUndoUnit");
- } catch (error) {}
- }
- return this.moveCursor(textArea, tag, wrap, removedLastNewLine);
-};
-gl.text.moveCursor = function(textArea, tag, wrapped, removedLastNewLine) {
- var pos;
- if (!textArea.setSelectionRange) {
- return;
- }
- if (textArea.selectionStart === textArea.selectionEnd) {
- if (wrapped) {
- pos = textArea.selectionStart - tag.length;
- } else {
- pos = textArea.selectionStart;
- }
+/**
+ * Removes accents and converts to lower case
+ * @param {String} str
+ * @returns {String}
+ */
+export const slugify = str => str.trim().toLowerCase();
- if (removedLastNewLine) {
- pos -= 1;
- }
+/**
+ * Truncates given text
+ *
+ * @param {String} string
+ * @param {Number} maxLength
+ * @returns {String}
+ */
+export const truncate = (string, maxLength) => `${string.substr(0, (maxLength - 3))}...`;
- return textArea.setSelectionRange(pos, pos);
- }
-};
-gl.text.updateText = function(textArea, tag, blockTag, wrap) {
- var $textArea, selected, text;
- $textArea = $(textArea);
- textArea = $textArea.get(0);
- text = $textArea.val();
- selected = this.selectedText(text, textArea);
- $textArea.focus();
- return this.insertText(textArea, text, tag, blockTag, selected, wrap);
-};
-gl.text.init = function(form) {
- var self;
- self = this;
- return $('.js-md', form).off('click').on('click', function() {
- var $this;
- $this = $(this);
- return self.updateText($this.closest('.md-area').find('textarea'), $this.data('md-tag'), $this.data('md-block'), !$this.data('md-prepend'));
- });
-};
-gl.text.removeListeners = function(form) {
- return $('.js-md', form).off('click');
-};
-gl.text.humanize = function(string) {
- return string.charAt(0).toUpperCase() + string.replace(/_/g, ' ').slice(1);
-};
-gl.text.pluralize = function(str, count) {
- return str + (count > 1 || count === 0 ? 's' : '');
-};
-gl.text.truncate = function(string, maxLength) {
- return string.substr(0, (maxLength - 3)) + '...';
-};
-gl.text.dasherize = function(str) {
- return str.replace(/[_\s]+/g, '-');
-};
-gl.text.slugify = function(str) {
- return str.trim().toLowerCase().latinise();
-};
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index 1aa63216baf..17236c91490 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -100,6 +100,10 @@ export function visitUrl(url, external = false) {
}
}
+export function redirectTo(url) {
+ return window.location.assign(url);
+}
+
window.gl = window.gl || {};
window.gl.utils = {
...(window.gl.utils || {}),
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 9117f033c9f..0035dd23011 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -30,7 +30,6 @@ import './commit/image_file';
import { handleLocationHash } from './lib/utils/common_utils';
import './lib/utils/datetime_utility';
import './lib/utils/pretty_time';
-import './lib/utils/text_utility';
import './lib/utils/url_utility';
// behaviors
@@ -46,7 +45,6 @@ import './commits';
import './compare';
import './compare_autocomplete';
import './confirm_danger_modal';
-import './copy_as_gfm';
import './copy_to_clipboard';
import Flash, { removeFlashClickListener } from './flash';
import './gl_dropdown';
@@ -71,8 +69,6 @@ import './notifications_dropdown';
import './notifications_form';
import './pager';
import './preview_markdown';
-import './project';
-import './project_avatar';
import './project_find_file';
import './project_import';
import './project_label_subscription';
diff --git a/app/assets/javascripts/members.js b/app/assets/javascripts/members.js
index 6264750a4fb..52315e969d1 100644
--- a/app/assets/javascripts/members.js
+++ b/app/assets/javascripts/members.js
@@ -5,7 +5,6 @@ export default class Members {
}
addListeners() {
- $('.project_member, .group_member').off('ajax:success').on('ajax:success', this.removeRow);
$('.js-member-update-control').off('change').on('change', this.formSubmit.bind(this));
$('.js-edit-member-form').off('ajax:success').on('ajax:success', this.formSuccess.bind(this));
gl.utils.disableButtonIfEmptyField('#user_ids', 'input[name=commit]', 'change');
@@ -33,17 +32,6 @@ export default class Members {
});
});
}
- // eslint-disable-next-line class-methods-use-this
- removeRow(e) {
- const $target = $(e.target);
-
- if ($target.hasClass('btn-remove')) {
- $target.closest('.member')
- .fadeOut(function fadeOutMemberRow() {
- $(this).remove();
- });
- }
- }
formSubmit(e, $el = null) {
const $this = e ? $(e.currentTarget) : $el;
diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js
index af0658eb668..d30ff12bb59 100644
--- a/app/assets/javascripts/merge_request.js
+++ b/app/assets/javascripts/merge_request.js
@@ -5,6 +5,7 @@ import 'vendor/jquery.waitforimages';
import TaskList from './task_list';
import './merge_request_tabs';
import IssuablesHelper from './helpers/issuables_helper';
+import { addDelimiter } from './lib/utils/text_utility';
(function() {
this.MergeRequest = (function() {
@@ -124,7 +125,7 @@ import IssuablesHelper from './helpers/issuables_helper';
const $el = $('.nav-links .js-merge-counter');
const count = Math.max((parseInt($el.text().replace(/[^\d]/, ''), 10) - by), 0);
- $el.text(gl.text.addDelimiter(count));
+ $el.text(addDelimiter(count));
};
MergeRequest.prototype.hideCloseButton = function() {
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index e7d5325a509..74e5a4f1cea 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -5,7 +5,7 @@ import _ from 'underscore';
(function() {
this.MilestoneSelect = (function() {
- function MilestoneSelect(currentProject, els) {
+ function MilestoneSelect(currentProject, els, options = {}) {
var _this, $els;
if (currentProject != null) {
_this = this;
@@ -136,19 +136,26 @@ import _ from 'underscore';
},
opened: function(e) {
const $el = $(e.currentTarget);
- if ($dropdown.hasClass('js-issue-board-sidebar')) {
+ if ($dropdown.hasClass('js-issue-board-sidebar') || options.handleClick) {
selectedMilestone = $dropdown[0].dataset.selected || selectedMilestoneDefault;
}
$('a.is-active', $el).removeClass('is-active');
$(`[data-milestone-id="${selectedMilestone}"] > a`, $el).addClass('is-active');
},
vue: $dropdown.hasClass('js-issue-board-sidebar'),
- clicked: function(options) {
- const { $el, e } = options;
- let selected = options.selectedObj;
+ clicked: function(clickEvent) {
+ const { $el, e } = clickEvent;
+ let selected = clickEvent.selectedObj;
var data, isIssueIndex, isMRIndex, isSelecting, page, boardsStore;
if (!selected) return;
+
+ if (options.handleClick) {
+ e.preventDefault();
+ options.handleClick(selected);
+ return;
+ }
+
page = $('body').attr('data-page');
isIssueIndex = page === 'projects:issues:index';
isMRIndex = (page === page && page === 'projects:merge_requests:index');
diff --git a/app/assets/javascripts/monitoring/components/graph.vue b/app/assets/javascripts/monitoring/components/graph.vue
index 5aa3865f96a..f8782fde927 100644
--- a/app/assets/javascripts/monitoring/components/graph.vue
+++ b/app/assets/javascripts/monitoring/components/graph.vue
@@ -138,7 +138,7 @@
renderAxesPaths() {
this.timeSeries = createTimeSeries(
- this.graphData.queries[0],
+ this.graphData.queries,
this.graphWidth,
this.graphHeight,
this.graphHeightOffset,
@@ -153,8 +153,9 @@
const axisYScale = d3.scale.linear()
.range([this.graphHeight - this.graphHeightOffset, 0]);
- axisXScale.domain(d3.extent(this.timeSeries[0].values, d => d.time));
- axisYScale.domain([0, d3.max(this.timeSeries[0].values.map(d => d.value))]);
+ const allValues = this.timeSeries.reduce((all, { values }) => all.concat(values), []);
+ axisXScale.domain(d3.extent(allValues, d => d.time));
+ axisYScale.domain([0, d3.max(allValues.map(d => d.value))]);
const xAxis = d3.svg.axis()
.scale(axisXScale)
@@ -246,6 +247,7 @@
:key="index"
:generated-line-path="path.linePath"
:generated-area-path="path.areaPath"
+ :line-style="path.lineStyle"
:line-color="path.lineColor"
:area-color="path.areaColor"
/>
diff --git a/app/assets/javascripts/monitoring/components/graph/legend.vue b/app/assets/javascripts/monitoring/components/graph/legend.vue
index 85b6d7f4cbe..440b1b12631 100644
--- a/app/assets/javascripts/monitoring/components/graph/legend.vue
+++ b/app/assets/javascripts/monitoring/components/graph/legend.vue
@@ -79,7 +79,8 @@
},
formatMetricUsage(series) {
- const value = series.values[this.currentDataIndex].value;
+ const value = series.values[this.currentDataIndex] &&
+ series.values[this.currentDataIndex].value;
if (isNaN(value)) {
return '-';
}
@@ -92,6 +93,12 @@
}
return `${this.legendTitle} series ${index + 1} ${this.formatMetricUsage(series)}`;
},
+
+ strokeDashArray(type) {
+ if (type === 'dashed') return '6, 3';
+ if (type === 'dotted') return '3, 3';
+ return null;
+ },
},
mounted() {
this.$nextTick(() => {
@@ -162,13 +169,15 @@
v-for="(series, index) in timeSeries"
:key="index"
:transform="translateLegendGroup(index)">
- <rect
- :fill="series.areaColor"
- :width="measurements.legends.width"
- :height="measurements.legends.height"
- x="20"
- :y="graphHeight - measurements.legendOffset">
- </rect>
+ <line
+ :stroke="series.lineColor"
+ :stroke-width="measurements.legends.height"
+ :stroke-dasharray="strokeDashArray(series.lineStyle)"
+ :x1="measurements.legends.offsetX"
+ :x2="measurements.legends.offsetX + measurements.legends.width"
+ :y1="graphHeight - measurements.legends.offsetY"
+ :y2="graphHeight - measurements.legends.offsetY">
+ </line>
<text
v-if="timeSeries.length > 1"
class="legend-metric-title"
diff --git a/app/assets/javascripts/monitoring/components/graph/path.vue b/app/assets/javascripts/monitoring/components/graph/path.vue
index 043f1bf66bb..5e6d409033a 100644
--- a/app/assets/javascripts/monitoring/components/graph/path.vue
+++ b/app/assets/javascripts/monitoring/components/graph/path.vue
@@ -9,6 +9,10 @@
type: String,
required: true,
},
+ lineStyle: {
+ type: String,
+ required: false,
+ },
lineColor: {
type: String,
required: true,
@@ -18,6 +22,13 @@
required: true,
},
},
+ computed: {
+ strokeDashArray() {
+ if (this.lineStyle === 'dashed') return '3, 1';
+ if (this.lineStyle === 'dotted') return '1, 1';
+ return null;
+ },
+ },
};
</script>
<template>
@@ -34,6 +45,7 @@
:stroke="lineColor"
fill="none"
stroke-width="1"
+ :stroke-dasharray="strokeDashArray"
transform="translate(-5, 20)">
</path>
</g>
diff --git a/app/assets/javascripts/monitoring/utils/measurements.js b/app/assets/javascripts/monitoring/utils/measurements.js
index ee3c45efacc..ee866850e13 100644
--- a/app/assets/javascripts/monitoring/utils/measurements.js
+++ b/app/assets/javascripts/monitoring/utils/measurements.js
@@ -7,15 +7,16 @@ export default {
left: 40,
},
legends: {
- width: 10,
+ width: 15,
height: 3,
+ offsetX: 20,
+ offsetY: 32,
},
backgroundLegend: {
width: 30,
height: 50,
},
axisLabelLineOffset: -20,
- legendOffset: 33,
},
large: { // This covers both md and lg screen sizes
margin: {
@@ -27,13 +28,14 @@ export default {
legends: {
width: 15,
height: 3,
+ offsetX: 20,
+ offsetY: 34,
},
backgroundLegend: {
width: 30,
height: 150,
},
axisLabelLineOffset: 20,
- legendOffset: 36,
},
xTicks: 8,
yTicks: 3,
diff --git a/app/assets/javascripts/monitoring/utils/multiple_time_series.js b/app/assets/javascripts/monitoring/utils/multiple_time_series.js
index 65eec0d8d02..d21a265bd43 100644
--- a/app/assets/javascripts/monitoring/utils/multiple_time_series.js
+++ b/app/assets/javascripts/monitoring/utils/multiple_time_series.js
@@ -11,7 +11,9 @@ const defaultColorPalette = {
const defaultColorOrder = ['blue', 'orange', 'red', 'green', 'purple'];
-export default function createTimeSeries(queryData, graphWidth, graphHeight, graphHeightOffset) {
+const defaultStyleOrder = ['solid', 'dashed', 'dotted'];
+
+function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom, yDom, lineStyle) {
let usedColors = [];
function pickColor(name) {
@@ -31,17 +33,7 @@ export default function createTimeSeries(queryData, graphWidth, graphHeight, gra
return defaultColorPalette[pick];
}
- const maxValues = queryData.result.map((timeSeries, index) => {
- const maxValue = d3.max(timeSeries.values.map(d => d.value));
- return {
- maxValue,
- index,
- };
- });
-
- const maxValueFromSeries = _.max(maxValues, val => val.maxValue);
-
- return queryData.result.map((timeSeries, timeSeriesNumber) => {
+ return query.result.map((timeSeries, timeSeriesNumber) => {
let metricTag = '';
let lineColor = '';
let areaColor = '';
@@ -52,9 +44,9 @@ export default function createTimeSeries(queryData, graphWidth, graphHeight, gra
const timeSeriesScaleY = d3.scale.linear()
.range([graphHeight - graphHeightOffset, 0]);
- timeSeriesScaleX.domain(d3.extent(timeSeries.values, d => d.time));
+ timeSeriesScaleX.domain(xDom);
timeSeriesScaleX.ticks(d3.time.minute, 60);
- timeSeriesScaleY.domain([0, maxValueFromSeries.maxValue]);
+ timeSeriesScaleY.domain(yDom);
const defined = d => !isNaN(d.value) && d.value != null;
@@ -72,10 +64,10 @@ export default function createTimeSeries(queryData, graphWidth, graphHeight, gra
.y1(d => timeSeriesScaleY(d.value));
const timeSeriesMetricLabel = timeSeries.metric[Object.keys(timeSeries.metric)[0]];
- const seriesCustomizationData = queryData.series != null &&
- _.findWhere(queryData.series[0].when,
- { value: timeSeriesMetricLabel });
- if (seriesCustomizationData != null) {
+ const seriesCustomizationData = query.series != null &&
+ _.findWhere(query.series[0].when, { value: timeSeriesMetricLabel });
+
+ if (seriesCustomizationData) {
metricTag = seriesCustomizationData.value || timeSeriesMetricLabel;
[lineColor, areaColor] = pickColor(seriesCustomizationData.color);
} else {
@@ -83,14 +75,35 @@ export default function createTimeSeries(queryData, graphWidth, graphHeight, gra
[lineColor, areaColor] = pickColor();
}
+ if (query.track) {
+ metricTag += ` - ${query.track}`;
+ }
+
return {
linePath: lineFunction(timeSeries.values),
areaPath: areaFunction(timeSeries.values),
timeSeriesScaleX,
values: timeSeries.values,
+ lineStyle,
lineColor,
areaColor,
metricTag,
};
});
}
+
+export default function createTimeSeries(queries, graphWidth, graphHeight, graphHeightOffset) {
+ const allValues = queries.reduce((allQueryResults, query) => allQueryResults.concat(
+ query.result.reduce((allResults, result) => allResults.concat(result.values), []),
+ ), []);
+
+ const xDom = d3.extent(allValues, d => d.time);
+ const yDom = [0, d3.max(allValues.map(d => d.value))];
+
+ return queries.reduce((series, query, index) => {
+ const lineStyle = defaultStyleOrder[index % defaultStyleOrder.length];
+ return series.concat(
+ queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom, yDom, lineStyle),
+ );
+ }, []);
+}
diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue
index db8f85759b2..30e02554b65 100644
--- a/app/assets/javascripts/notes/components/issue_comment_form.vue
+++ b/app/assets/javascripts/notes/components/issue_comment_form.vue
@@ -357,7 +357,8 @@
@click="handleSave(true)"
v-if="canUpdateIssue"
:class="actionButtonClassNames"
- class="btn btn-comment btn-comment-and-close">
+ :disabled="isSubmitting"
+ class="btn btn-comment btn-comment-and-close js-action-button">
{{issueActionButtonTitle}}
</button>
<button
diff --git a/app/assets/javascripts/notes/components/issue_discussion_locked_widget.vue b/app/assets/javascripts/notes/components/issue_discussion_locked_widget.vue
index e73ec2aaf71..64466b04b40 100644
--- a/app/assets/javascripts/notes/components/issue_discussion_locked_widget.vue
+++ b/app/assets/javascripts/notes/components/issue_discussion_locked_widget.vue
@@ -1,18 +1,21 @@
<script>
+ import Icon from '../../vue_shared/components/icon.vue';
+
export default {
- computed: {
- lockIcon() {
- return gl.utils.spriteIcon('lock');
- },
+ component: {
+ Icon,
},
};
-
</script>
<template>
<div class="disabled-comment text-center">
- <span class="issuable-note-warning">
- <span class="icon" v-html="lockIcon"></span>
+ <span class="issuable-note-warning inline">
+ <icon
+ name="lock"
+ :size="16"
+ class="icon">
+ </icon>
<span>This issue is locked. Only <b>project members</b> can comment.</span>
</span>
</div>
diff --git a/app/assets/javascripts/pipelines/components/graph/action_component.vue b/app/assets/javascripts/pipelines/components/graph/action_component.vue
index 547140b1a43..19d8e1f49cf 100644
--- a/app/assets/javascripts/pipelines/components/graph/action_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/action_component.vue
@@ -1,7 +1,7 @@
<script>
import tooltip from '../../../vue_shared/directives/tooltip';
import icon from '../../../vue_shared/components/icon.vue';
-
+ import { dasherize } from '../../../lib/utils/text_utility';
/**
* Renders either a cancel, retry or play icon pointing to the given path.
* TODO: Remove UJS from here and use an async request instead.
@@ -39,7 +39,7 @@
computed: {
cssClass() {
- const actionIconDash = gl.text.dasherize(this.actionIcon);
+ const actionIconDash = dasherize(this.actionIcon);
return `${actionIconDash} js-icon-${actionIconDash}`;
},
},
diff --git a/app/assets/javascripts/pipelines/components/navigation_tabs.vue b/app/assets/javascripts/pipelines/components/navigation_tabs.vue
index 73f7e3a0cad..07befd23500 100644
--- a/app/assets/javascripts/pipelines/components/navigation_tabs.vue
+++ b/app/assets/javascripts/pipelines/components/navigation_tabs.vue
@@ -2,16 +2,8 @@
export default {
name: 'PipelineNavigationTabs',
props: {
- scope: {
- type: String,
- required: true,
- },
- count: {
- type: Object,
- required: true,
- },
- paths: {
- type: Object,
+ tabs: {
+ type: Array,
required: true,
},
},
@@ -23,68 +15,37 @@
// 0 is valid in a badge, but evaluates to false, we need to check for undefined
return count !== undefined;
},
+
+ onTabClick(tab) {
+ this.$emit('onChangeTab', tab.scope);
+ },
},
};
</script>
<template>
<ul class="nav-links scrolling-tabs">
<li
- class="js-pipelines-tab-all"
- :class="{ active: scope === 'all'}">
- <a :href="paths.allPath">
- All
- <span
- v-if="shouldRenderBadge(count.all)"
- class="badge js-totalbuilds-count">
- {{count.all}}
- </span>
- </a>
- </li>
- <li
- class="js-pipelines-tab-pending"
- :class="{ active: scope === 'pending'}">
- <a :href="paths.pendingPath">
- Pending
- <span
- v-if="shouldRenderBadge(count.pending)"
- class="badge">
- {{count.pending}}
- </span>
- </a>
- </li>
- <li
- class="js-pipelines-tab-running"
- :class="{ active: scope === 'running'}">
- <a :href="paths.runningPath">
- Running
- <span
- v-if="shouldRenderBadge(count.running)"
- class="badge">
- {{count.running}}
- </span>
- </a>
- </li>
- <li
- class="js-pipelines-tab-finished"
- :class="{ active: scope === 'finished'}">
- <a :href="paths.finishedPath">
- Finished
+ v-for="(tab, i) in tabs"
+ :key="i"
+ :class="{
+ active: tab.isActive,
+ }"
+ >
+ <a
+ role="button"
+ @click="onTabClick(tab)"
+ :class="`js-pipelines-tab-${tab.scope}`"
+ >
+ {{ tab.name }}
+
<span
- v-if="shouldRenderBadge(count.finished)"
- class="badge">
- {{count.finished}}
+ v-if="shouldRenderBadge(tab.count)"
+ class="badge"
+ >
+ {{tab.count}}
</span>
+
</a>
</li>
- <li
- class="js-pipelines-tab-branches"
- :class="{ active: scope === 'branches'}">
- <a :href="paths.branchesPath">Branches</a>
- </li>
- <li
- class="js-pipelines-tab-tags"
- :class="{ active: scope === 'tags'}">
- <a :href="paths.tagsPath">Tags</a>
- </li>
</ul>
</template>
diff --git a/app/assets/javascripts/pipelines/components/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines.vue
index 3da60e88474..cf241c8ffed 100644
--- a/app/assets/javascripts/pipelines/components/pipelines.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines.vue
@@ -1,10 +1,17 @@
<script>
+ import _ from 'underscore';
import PipelinesService from '../services/pipelines_service';
import pipelinesMixin from '../mixins/pipelines';
import tablePagination from '../../vue_shared/components/table_pagination.vue';
import navigationTabs from './navigation_tabs.vue';
import navigationControls from './nav_controls.vue';
- import { convertPermissionToBoolean, getParameterByName, setParamInURL } from '../../lib/utils/common_utils';
+ import {
+ convertPermissionToBoolean,
+ getParameterByName,
+ historyPushState,
+ buildUrlWithCurrentLocation,
+ parseQueryStringIntoObject,
+ } from '../../lib/utils/common_utils';
export default {
props: {
@@ -41,27 +48,18 @@
autoDevopsPath: pipelinesData.helpAutoDevopsPath,
newPipelinePath: pipelinesData.newPipelinePath,
canCreatePipeline: pipelinesData.canCreatePipeline,
- allPath: pipelinesData.allPath,
- pendingPath: pipelinesData.pendingPath,
- runningPath: pipelinesData.runningPath,
- finishedPath: pipelinesData.finishedPath,
- branchesPath: pipelinesData.branchesPath,
- tagsPath: pipelinesData.tagsPath,
hasCi: pipelinesData.hasCi,
ciLintPath: pipelinesData.ciLintPath,
state: this.store.state,
- apiScope: 'all',
- pagenum: 1,
+ scope: getParameterByName('scope') || 'all',
+ page: getParameterByName('page') || '1',
+ requestData: {},
};
},
computed: {
canCreatePipelineParsed() {
return convertPermissionToBoolean(this.canCreatePipeline);
},
- scope() {
- const scope = getParameterByName('scope');
- return scope === null ? 'all' : scope;
- },
/**
* The empty state should only be rendered when the request is made to fetch all pipelines
@@ -106,46 +104,112 @@
hasCiEnabled() {
return this.hasCi !== undefined;
},
- paths() {
- return {
- allPath: this.allPath,
- pendingPath: this.pendingPath,
- finishedPath: this.finishedPath,
- runningPath: this.runningPath,
- branchesPath: this.branchesPath,
- tagsPath: this.tagsPath,
- };
- },
- pageParameter() {
- return getParameterByName('page') || this.pagenum;
- },
- scopeParameter() {
- return getParameterByName('scope') || this.apiScope;
+
+ tabs() {
+ const { count } = this.state;
+ return [
+ {
+ name: 'All',
+ scope: 'all',
+ count: count.all,
+ isActive: this.scope === 'all',
+ },
+ {
+ name: 'Pending',
+ scope: 'pending',
+ count: count.pending,
+ isActive: this.scope === 'pending',
+ },
+ {
+ name: 'Running',
+ scope: 'running',
+ count: count.running,
+ isActive: this.scope === 'running',
+ },
+ {
+ name: 'Finished',
+ scope: 'finished',
+ count: count.finished,
+ isActive: this.scope === 'finished',
+ },
+ {
+ name: 'Branches',
+ scope: 'branches',
+ isActive: this.scope === 'branches',
+ },
+ {
+ name: 'Tags',
+ scope: 'tags',
+ isActive: this.scope === 'tags',
+ },
+ ];
},
},
created() {
this.service = new PipelinesService(this.endpoint);
- this.requestData = { page: this.pageParameter, scope: this.scopeParameter };
+ this.requestData = { page: this.page, scope: this.scope };
},
methods: {
+ successCallback(resp) {
+ return resp.json().then((response) => {
+ // Because we are polling & the user is interacting verify if the response received
+ // matches the last request made
+ if (_.isEqual(parseQueryStringIntoObject(resp.url.split('?')[1]), this.requestData)) {
+ this.store.storeCount(response.count);
+ this.store.storePagination(resp.headers);
+ this.setCommonData(response.pipelines);
+ }
+ });
+ },
/**
- * Will change the page number and update the URL.
- *
- * @param {Number} pageNumber desired page to go to.
+ * Handles URL and query parameter changes.
+ * When the user uses the pagination or the tabs,
+ * - update URL
+ * - Make API request to the server with new parameters
+ * - Update the polling function
+ * - Update the internal state
*/
- change(pageNumber) {
- const param = setParamInURL('page', pageNumber);
+ updateContent(parameters) {
+ // stop polling
+ this.poll.stop();
+
+ const queryString = Object.keys(parameters).map((parameter) => {
+ const value = parameters[parameter];
+ // update internal state for UI
+ this[parameter] = value;
+ return `${parameter}=${encodeURIComponent(value)}`;
+ }).join('&');
- gl.utils.visitUrl(param);
- return param;
+ // update polling parameters
+ this.requestData = parameters;
+
+ historyPushState(buildUrlWithCurrentLocation(`?${queryString}`));
+
+ this.isLoading = true;
+ // fetch new data
+ return this.service.getPipelines(this.requestData)
+ .then((response) => {
+ this.isLoading = false;
+ this.successCallback(response);
+
+ // restart polling
+ this.poll.restart({ data: this.requestData });
+ })
+ .catch(() => {
+ this.isLoading = false;
+ this.errorCallback();
+
+ // restart polling
+ this.poll.restart();
+ });
},
- successCallback(resp) {
- return resp.json().then((response) => {
- this.store.storeCount(response.count);
- this.store.storePagination(resp.headers);
- this.setCommonData(response.pipelines);
- });
+ onChangeTab(scope) {
+ this.updateContent({ scope, page: '1' });
+ },
+ onChangePage(page) {
+ /* URLS parameters are strings, we need to parse to match types */
+ this.updateContent({ scope: this.scope, page: Number(page).toString() });
},
},
};
@@ -154,7 +218,7 @@
<div class="pipelines-container">
<div
class="top-area scrolling-tabs-container inner-page-scroll-tabs"
- v-if="!isLoading && !shouldRenderEmptyState">
+ v-if="!shouldRenderEmptyState">
<div class="fade-left">
<i
class="fa fa-angle-left"
@@ -167,17 +231,17 @@
aria-hidden="true">
</i>
</div>
+
<navigation-tabs
- :scope="scope"
- :count="state.count"
- :paths="paths"
+ :tabs="tabs"
+ @onChangeTab="onChangeTab"
/>
<navigation-controls
:new-pipeline-path="newPipelinePath"
:has-ci-enabled="hasCiEnabled"
:help-page-path="helpPagePath"
- :ciLintPath="ciLintPath"
+ :ci-lint-path="ciLintPath"
:can-create-pipeline="canCreatePipelineParsed "
/>
</div>
@@ -188,6 +252,7 @@
label="Loading Pipelines"
size="3"
v-if="isLoading"
+ class="prepend-top-20"
/>
<empty-state
@@ -221,8 +286,8 @@
<table-pagination
v-if="shouldRenderPagination"
- :change="change"
- :pageInfo="state.pageInfo"
+ :change="onChangePage"
+ :page-info="state.pageInfo"
/>
</div>
</div>
diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js
index fe6602259e2..ddb78aaeea1 100644
--- a/app/assets/javascripts/project.js
+++ b/app/assets/javascripts/project.js
@@ -1,139 +1,131 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, consistent-return, no-new, prefer-arrow-callback, no-return-assign, one-var, one-var-declaration-per-line, object-shorthand, comma-dangle, no-else-return, newline-per-chained-call, no-shadow, vars-on-top, prefer-template, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, consistent-return, no-new, prefer-arrow-callback, no-return-assign, one-var, one-var-declaration-per-line, object-shorthand, no-else-return, newline-per-chained-call, no-shadow, vars-on-top, prefer-template, max-len */
/* global ProjectSelect */
import Cookies from 'js-cookie';
-(function() {
- this.Project = (function() {
- function Project() {
- const $cloneOptions = $('ul.clone-options-dropdown');
- const $projectCloneField = $('#project_clone');
- const $cloneBtnText = $('a.clone-dropdown-btn span');
+export default class Project {
+ constructor() {
+ const $cloneOptions = $('ul.clone-options-dropdown');
+ const $projectCloneField = $('#project_clone');
+ const $cloneBtnText = $('a.clone-dropdown-btn span');
- const selectedCloneOption = $cloneBtnText.text().trim();
- if (selectedCloneOption.length > 0) {
- $(`a:contains('${selectedCloneOption}')`, $cloneOptions).addClass('is-active');
- }
-
- $('a', $cloneOptions).on('click', (e) => {
- const $this = $(e.currentTarget);
- const url = $this.attr('href');
-
- e.preventDefault();
-
- $('.is-active', $cloneOptions).not($this).removeClass('is-active');
- $this.toggleClass('is-active');
- $projectCloneField.val(url);
- $cloneBtnText.text($this.text());
-
- return $('.clone').text(url);
- });
- // Ref switcher
- this.initRefSwitcher();
- $('.project-refs-select').on('change', function() {
- return $(this).parents('form').submit();
- });
- $('.hide-no-ssh-message').on('click', function(e) {
- Cookies.set('hide_no_ssh_message', 'false');
- $(this).parents('.no-ssh-key-message').remove();
- return e.preventDefault();
- });
- $('.hide-no-password-message').on('click', function(e) {
- Cookies.set('hide_no_password_message', 'false');
- $(this).parents('.no-password-message').remove();
- return e.preventDefault();
- });
- this.projectSelectDropdown();
+ const selectedCloneOption = $cloneBtnText.text().trim();
+ if (selectedCloneOption.length > 0) {
+ $(`a:contains('${selectedCloneOption}')`, $cloneOptions).addClass('is-active');
}
- Project.prototype.projectSelectDropdown = function() {
- new ProjectSelect();
- $('.project-item-select').on('click', (function(_this) {
- return function(e) {
- return _this.changeProject($(e.currentTarget).val());
- };
- })(this));
- };
-
- Project.prototype.changeProject = function(url) {
- return window.location = url;
- };
-
- Project.prototype.initRefSwitcher = function() {
- var refListItem = document.createElement('li');
- var refLink = document.createElement('a');
-
- refLink.href = '#';
-
- return $('.js-project-refs-dropdown').each(function() {
- var $dropdown, selected;
- $dropdown = $(this);
- selected = $dropdown.data('selected');
- return $dropdown.glDropdown({
- data: function(term, callback) {
- return $.ajax({
- url: $dropdown.data('refs-url'),
- data: {
- ref: $dropdown.data('ref'),
- search: term
- },
- dataType: "json"
- }).done(function(refs) {
- return callback(refs);
- });
- },
- selectable: true,
- filterable: true,
- filterRemote: true,
- filterByText: true,
- inputFieldName: $dropdown.data('input-field-name'),
- fieldName: $dropdown.data('field-name'),
- renderRow: function(ref) {
- var li = refListItem.cloneNode(false);
-
- if (ref.header != null) {
- li.className = 'dropdown-header';
- li.textContent = ref.header;
- } else {
- var link = refLink.cloneNode(false);
-
- if (ref === selected) {
- link.className = 'is-active';
- }
-
- link.textContent = ref;
- link.dataset.ref = ref;
-
- li.appendChild(link);
+ $('a', $cloneOptions).on('click', (e) => {
+ const $this = $(e.currentTarget);
+ const url = $this.attr('href');
+
+ e.preventDefault();
+
+ $('.is-active', $cloneOptions).not($this).removeClass('is-active');
+ $this.toggleClass('is-active');
+ $projectCloneField.val(url);
+ $cloneBtnText.text($this.text());
+
+ return $('.clone').text(url);
+ });
+ // Ref switcher
+ Project.initRefSwitcher();
+ $('.project-refs-select').on('change', function() {
+ return $(this).parents('form').submit();
+ });
+ $('.hide-no-ssh-message').on('click', function(e) {
+ Cookies.set('hide_no_ssh_message', 'false');
+ $(this).parents('.no-ssh-key-message').remove();
+ return e.preventDefault();
+ });
+ $('.hide-no-password-message').on('click', function(e) {
+ Cookies.set('hide_no_password_message', 'false');
+ $(this).parents('.no-password-message').remove();
+ return e.preventDefault();
+ });
+ Project.projectSelectDropdown();
+ }
+
+ static projectSelectDropdown () {
+ new ProjectSelect();
+ $('.project-item-select').on('click', e => Project.changeProject($(e.currentTarget).val()));
+ }
+
+ static changeProject(url) {
+ return window.location = url;
+ }
+
+ static initRefSwitcher() {
+ var refListItem = document.createElement('li');
+ var refLink = document.createElement('a');
+
+ refLink.href = '#';
+
+ return $('.js-project-refs-dropdown').each(function() {
+ var $dropdown, selected;
+ $dropdown = $(this);
+ selected = $dropdown.data('selected');
+ return $dropdown.glDropdown({
+ data: function(term, callback) {
+ return $.ajax({
+ url: $dropdown.data('refs-url'),
+ data: {
+ ref: $dropdown.data('ref'),
+ search: term,
+ },
+ dataType: 'json',
+ }).done(function(refs) {
+ return callback(refs);
+ });
+ },
+ selectable: true,
+ filterable: true,
+ filterRemote: true,
+ filterByText: true,
+ inputFieldName: $dropdown.data('input-field-name'),
+ fieldName: $dropdown.data('field-name'),
+ renderRow: function(ref) {
+ var li = refListItem.cloneNode(false);
+
+ if (ref.header != null) {
+ li.className = 'dropdown-header';
+ li.textContent = ref.header;
+ } else {
+ var link = refLink.cloneNode(false);
+
+ if (ref === selected) {
+ link.className = 'is-active';
}
- return li;
- },
- id: function(obj, $el) {
- return $el.attr('data-ref');
- },
- toggleLabel: function(obj, $el) {
- return $el.text().trim();
- },
- clicked: function(options) {
- const { e } = options;
- e.preventDefault();
- if ($('input[name="ref"]').length) {
- var $form = $dropdown.closest('form');
-
- var $visit = $dropdown.data('visit');
- var shouldVisit = $visit ? true : $visit;
- var action = $form.attr('action');
- var divider = action.indexOf('?') === -1 ? '?' : '&';
- if (shouldVisit) {
- gl.utils.visitUrl(`${action}${divider}${$form.serialize()}`);
- }
+ link.textContent = ref;
+ link.dataset.ref = ref;
+
+ li.appendChild(link);
+ }
+
+ return li;
+ },
+ id: function(obj, $el) {
+ return $el.attr('data-ref');
+ },
+ toggleLabel: function(obj, $el) {
+ return $el.text().trim();
+ },
+ clicked: function(options) {
+ const { e } = options;
+ e.preventDefault();
+ if ($('input[name="ref"]').length) {
+ var $form = $dropdown.closest('form');
+
+ var $visit = $dropdown.data('visit');
+ var shouldVisit = $visit ? true : $visit;
+ var action = $form.attr('action');
+ var divider = action.indexOf('?') === -1 ? '?' : '&';
+ if (shouldVisit) {
+ gl.utils.visitUrl(`${action}${divider}${$form.serialize()}`);
}
}
- });
+ },
});
- };
-
- return Project;
- })();
-}).call(window);
+ });
+ }
+}
diff --git a/app/assets/javascripts/project_avatar.js b/app/assets/javascripts/project_avatar.js
index aabdfbf65e2..56627aa155c 100644
--- a/app/assets/javascripts/project_avatar.js
+++ b/app/assets/javascripts/project_avatar.js
@@ -1,20 +1,13 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, no-useless-escape, max-len */
-(function() {
- this.ProjectAvatar = (function() {
- function ProjectAvatar() {
- $('.js-choose-project-avatar-button').bind('click', function() {
- var form;
- form = $(this).closest('form');
- return form.find('.js-project-avatar-input').click();
- });
- $('.js-project-avatar-input').bind('change', function() {
- var filename, form;
- form = $(this).closest('form');
- filename = $(this).val().replace(/^.*[\\\/]/, '');
- return form.find('.js-avatar-filename').text(filename);
- });
- }
+export default function projectAvatar() {
+ $('.js-choose-project-avatar-button').bind('click', function onClickAvatar() {
+ const form = $(this).closest('form');
+ return form.find('.js-project-avatar-input').click();
+ });
- return ProjectAvatar;
- })();
-}).call(window);
+ $('.js-project-avatar-input').bind('change', function onClickAvatarInput() {
+ const form = $(this).closest('form');
+ // eslint-disable-next-line no-useless-escape
+ const filename = $(this).val().replace(/^.*[\\\/]/, '');
+ return form.find('.js-avatar-filename').text(filename);
+ });
+}
diff --git a/app/assets/javascripts/project_import.js b/app/assets/javascripts/project_import.js
index 08334bf1ec5..d2d26d6f67e 100644
--- a/app/assets/javascripts/project_import.js
+++ b/app/assets/javascripts/project_import.js
@@ -1,13 +1,8 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, max-len */
+import { visitUrl } from './lib/utils/url_utility';
-(function() {
- this.ProjectImport = (function() {
- function ProjectImport() {
- setTimeout(function() {
- return gl.utils.visitUrl(location.href);
- }, 5000);
- }
+export default function projectImport() {
+ setTimeout(() => {
+ visitUrl(location.href);
+ }, 5000);
+}
- return ProjectImport;
- })();
-}).call(window);
diff --git a/app/assets/javascripts/registry/components/table_registry.vue b/app/assets/javascripts/registry/components/table_registry.vue
index e917279947e..14d43e135fe 100644
--- a/app/assets/javascripts/registry/components/table_registry.vue
+++ b/app/assets/javascripts/registry/components/table_registry.vue
@@ -8,6 +8,7 @@
import tooltip from '../../vue_shared/directives/tooltip';
import timeagoMixin from '../../vue_shared/mixins/timeago';
import { errorMessages, errorMessagesTypes } from '../constants';
+ import { numberToHumanSize } from '../../lib/utils/number_utils';
export default {
props: {
@@ -41,6 +42,10 @@
return item.layers ? n__('%d layer', '%d layers', item.layers) : '';
},
+ formatSize(size) {
+ return numberToHumanSize(size);
+ },
+
handleDeleteRegistry(registry) {
this.deleteRegistry(registry)
.then(() => this.fetchList({ repo: this.repo }))
@@ -97,7 +102,7 @@
</span>
</td>
<td>
- {{item.size}}
+ {{formatSize(item.size)}}
<template v-if="item.size && item.layers">
&middot;
</template>
diff --git a/app/assets/javascripts/repo/components/repo_file.vue b/app/assets/javascripts/repo/components/repo_file.vue
index 7a23154b340..5be47d568e7 100644
--- a/app/assets/javascripts/repo/components/repo_file.vue
+++ b/app/assets/javascripts/repo/components/repo_file.vue
@@ -1,11 +1,15 @@
<script>
import { mapActions, mapGetters } from 'vuex';
import timeAgoMixin from '../../vue_shared/mixins/timeago';
+ import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue';
export default {
mixins: [
timeAgoMixin,
],
+ components: {
+ skeletonLoadingContainer,
+ },
props: {
file: {
type: Object,
@@ -16,6 +20,9 @@
...mapGetters([
'isCollapsed',
]),
+ isSubmodule() {
+ return this.file.type === 'submodule';
+ },
fileIcon() {
return {
'fa-spinner fa-spin': this.file.loading,
@@ -31,6 +38,9 @@
shortId() {
return this.file.id.substr(0, 8);
},
+ submoduleColSpan() {
+ return !this.isCollapsed && this.isSubmodule ? 3 : 1;
+ },
},
methods: {
...mapActions([
@@ -44,7 +54,10 @@
<tr
class="file"
@click.prevent="clickedTreeRow(file)">
- <td>
+ <td
+ class="multi-file-table-col-name"
+ :colspan="submoduleColSpan"
+ >
<i
class="fa fa-fw file-icon"
:class="fileIcon"
@@ -58,7 +71,7 @@
>
{{ file.name }}
</a>
- <template v-if="file.type === 'submodule' && file.id">
+ <template v-if="isSubmodule && file.id">
@
<span class="commit-sha">
<a
@@ -71,15 +84,20 @@
</template>
</td>
- <template v-if="!isCollapsed">
+ <template v-if="!isCollapsed && !isSubmodule">
<td class="hidden-sm hidden-xs">
<a
+ v-if="file.lastCommit.message"
@click.stop
:href="file.lastCommit.url"
class="commit-message"
>
{{ file.lastCommit.message }}
</a>
+ <skeleton-loading-container
+ v-else
+ :small="true"
+ />
</td>
<td class="commit-update hidden-xs text-right">
@@ -89,6 +107,11 @@
>
{{ timeFormated(file.lastCommit.updatedAt) }}
</span>
+ <skeleton-loading-container
+ v-else
+ class="animation-container-right"
+ :small="true"
+ />
</td>
</template>
</tr>
diff --git a/app/assets/javascripts/repo/components/repo_loading_file.vue b/app/assets/javascripts/repo/components/repo_loading_file.vue
index 1e6c405f292..8fa637d771f 100644
--- a/app/assets/javascripts/repo/components/repo_loading_file.vue
+++ b/app/assets/javascripts/repo/components/repo_loading_file.vue
@@ -1,17 +1,16 @@
<script>
import { mapGetters } from 'vuex';
+ import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue';
export default {
+ components: {
+ skeletonLoadingContainer,
+ },
computed: {
...mapGetters([
'isCollapsed',
]),
},
- methods: {
- lineOfCode(n) {
- return `skeleton-line-${n}`;
- },
- },
};
</script>
@@ -20,37 +19,25 @@
class="loading-file"
aria-label="Loading files"
>
- <td>
- <div
- class="animation-container animation-container-small">
- <div
- v-for="n in 6"
- :key="n"
- :class="lineOfCode(n)">
- </div>
- </div>
+ <td class="multi-file-table-col-name">
+ <skeleton-loading-container
+ :small="true"
+ />
</td>
<template v-if="!isCollapsed">
<td
class="hidden-sm hidden-xs">
- <div class="animation-container">
- <div
- v-for="n in 6"
- :key="n"
- :class="lineOfCode(n)">
- </div>
- </div>
+ <skeleton-loading-container
+ :small="true"
+ />
</td>
<td
class="hidden-xs">
- <div class="animation-container animation-container-small animation-container-right">
- <div
- v-for="n in 6"
- :key="n"
- :class="lineOfCode(n)">
- </div>
- </div>
+ <skeleton-loading-container
+ class="animation-container-right"
+ :small="true"
+ />
</td>
</template>
</tr>
diff --git a/app/assets/javascripts/repo/components/repo_sidebar.vue b/app/assets/javascripts/repo/components/repo_sidebar.vue
index 63c0d70f5c0..9365b09326f 100644
--- a/app/assets/javascripts/repo/components/repo_sidebar.vue
+++ b/app/assets/javascripts/repo/components/repo_sidebar.vue
@@ -57,7 +57,7 @@ export default {
</strong>
</th>
<template v-else>
- <th class="name">
+ <th class="name multi-file-table-col-name">
Name
</th>
<th class="hidden-sm hidden-xs last-commit">
@@ -80,7 +80,7 @@ export default {
/>
<repo-file
v-for="(file, index) in treeList"
- :key="index"
+ :key="file.key"
:file="file"
/>
</tbody>
diff --git a/app/assets/javascripts/repo/services/index.js b/app/assets/javascripts/repo/services/index.js
index dc222ccac01..2fb45dcb03c 100644
--- a/app/assets/javascripts/repo/services/index.js
+++ b/app/assets/javascripts/repo/services/index.js
@@ -30,4 +30,11 @@ export default {
commit(projectId, payload) {
return Api.commitMultiple(projectId, payload);
},
+ getTreeLastCommit(endpoint) {
+ return Vue.http.get(endpoint, {
+ params: {
+ format: 'json',
+ },
+ });
+ },
};
diff --git a/app/assets/javascripts/repo/stores/actions.js b/app/assets/javascripts/repo/stores/actions.js
index ca2f2a5ce7a..120ce96f44d 100644
--- a/app/assets/javascripts/repo/stores/actions.js
+++ b/app/assets/javascripts/repo/stores/actions.js
@@ -3,7 +3,7 @@ import flash from '../../flash';
import service from '../services';
import * as types from './mutation_types';
-export const redirectToUrl = url => gl.utils.visitUrl(url);
+export const redirectToUrl = (_, url) => gl.utils.visitUrl(url);
export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data);
@@ -64,7 +64,7 @@ export const checkCommitStatus = ({ state }) => service.getBranchData(
})
.catch(() => flash('Error checking branch data. Please try again.'));
-export const commitChanges = ({ commit, state, dispatch }, { payload, newMr }) =>
+export const commitChanges = ({ commit, state, dispatch, getters }, { payload, newMr }) =>
service.commit(state.project.id, payload)
.then((data) => {
const { branch } = payload;
@@ -73,12 +73,28 @@ export const commitChanges = ({ commit, state, dispatch }, { payload, newMr }) =
return;
}
+ const lastCommit = {
+ commit_path: `${state.project.url}/commit/${data.id}`,
+ commit: {
+ message: data.message,
+ authored_date: data.committed_date,
+ },
+ };
+
flash(`Your changes have been committed. Commit ${data.short_id} with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`, 'notice');
if (newMr) {
- redirectToUrl(`${state.endpoints.newMergeRequestUrl}${branch}`);
+ dispatch('redirectToUrl', `${state.endpoints.newMergeRequestUrl}${branch}`);
} else {
commit(types.SET_COMMIT_REF, data.id);
+
+ getters.changedFiles.forEach((entry) => {
+ commit(types.SET_LAST_COMMIT_DATA, {
+ entry,
+ lastCommit,
+ });
+ });
+
dispatch('discardAllChanges');
dispatch('closeAllFiles');
dispatch('toggleEditMode');
diff --git a/app/assets/javascripts/repo/stores/actions/branch.js b/app/assets/javascripts/repo/stores/actions/branch.js
index b81a70dfd1e..61d9a5af3e3 100644
--- a/app/assets/javascripts/repo/stores/actions/branch.js
+++ b/app/assets/javascripts/repo/stores/actions/branch.js
@@ -3,16 +3,16 @@ import * as types from '../mutation_types';
import { pushState } from '../utils';
// eslint-disable-next-line import/prefer-default-export
-export const createNewBranch = ({ rootState, commit }, branch) => service.createBranch(
- rootState.project.id,
+export const createNewBranch = ({ state, commit }, branch) => service.createBranch(
+ state.project.id,
{
branch,
- ref: rootState.currentBranch,
+ ref: state.currentBranch,
},
).then(res => res.json())
.then((data) => {
const branchName = data.name;
- const url = location.href.replace(rootState.currentBranch, branchName);
+ const url = location.href.replace(state.currentBranch, branchName);
pushState(url);
diff --git a/app/assets/javascripts/repo/stores/actions/file.js b/app/assets/javascripts/repo/stores/actions/file.js
index afbe0b78a82..5bae4fa826a 100644
--- a/app/assets/javascripts/repo/stores/actions/file.js
+++ b/app/assets/javascripts/repo/stores/actions/file.js
@@ -27,6 +27,8 @@ export const closeFile = ({ commit, state, dispatch }, { file, force = false })
} else if (!state.openFiles.length) {
pushState(file.parentTreeUrl);
}
+
+ dispatch('getLastCommitData');
};
export const setFileActive = ({ commit, state, getters, dispatch }, file) => {
diff --git a/app/assets/javascripts/repo/stores/actions/tree.js b/app/assets/javascripts/repo/stores/actions/tree.js
index 129743c66c2..aa830e946a2 100644
--- a/app/assets/javascripts/repo/stores/actions/tree.js
+++ b/app/assets/javascripts/repo/stores/actions/tree.js
@@ -7,10 +7,11 @@ import {
setPageTitle,
findEntry,
createTemp,
+ createOrMergeEntry,
} from '../utils';
export const getTreeData = (
- { commit, state },
+ { commit, state, dispatch },
{ endpoint = state.endpoints.rootEndpoint, tree = state } = {},
) => {
commit(types.TOGGLE_LOADING, tree);
@@ -24,14 +25,20 @@ export const getTreeData = (
return res.json();
})
.then((data) => {
+ const prevLastCommitPath = tree.lastCommitPath;
if (!state.isInitialRoot) {
commit(types.SET_ROOT, data.path === '/');
}
- commit(types.SET_DIRECTORY_DATA, { data, tree });
+ dispatch('updateDirectoryData', { data, tree });
commit(types.SET_PARENT_TREE_URL, data.parent_tree_url);
+ commit(types.SET_LAST_COMMIT_URL, { tree, url: data.last_commit_path });
commit(types.TOGGLE_LOADING, tree);
+ if (prevLastCommitPath !== null) {
+ dispatch('getLastCommitData', tree);
+ }
+
pushState(endpoint);
})
.catch(() => {
@@ -48,7 +55,7 @@ export const toggleTreeOpen = ({ commit, dispatch }, { endpoint, tree }) => {
pushState(tree.parentTreeUrl);
commit(types.SET_PREVIOUS_URL, tree.parentTreeUrl);
- commit(types.SET_DIRECTORY_DATA, { data, tree });
+ dispatch('updateDirectoryData', { data, tree });
} else {
commit(types.SET_PREVIOUS_URL, endpoint);
dispatch('getTreeData', { endpoint, tree });
@@ -108,3 +115,48 @@ export const createTempTree = ({ state, commit, dispatch }, name) => {
});
}
};
+
+export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = state) => {
+ if (tree.lastCommitPath === null || getters.isCollapsed) return;
+
+ service.getTreeLastCommit(tree.lastCommitPath)
+ .then((res) => {
+ const lastCommitPath = normalizeHeaders(res.headers)['MORE-LOGS-URL'] || null;
+
+ commit(types.SET_LAST_COMMIT_URL, { tree, url: lastCommitPath });
+
+ return res.json();
+ })
+ .then((data) => {
+ data.forEach((lastCommit) => {
+ const entry = findEntry(tree, lastCommit.type, lastCommit.file_name);
+
+ if (entry) {
+ commit(types.SET_LAST_COMMIT_DATA, { entry, lastCommit });
+ }
+ });
+
+ dispatch('getLastCommitData', tree);
+ })
+ .catch(() => flash('Error fetching log data.'));
+};
+
+export const updateDirectoryData = ({ commit, state }, { data, tree }) => {
+ const level = tree.level !== undefined ? tree.level + 1 : 0;
+ const parentTreeUrl = data.parent_tree_url ? `${data.parent_tree_url}${data.path}` : state.endpoints.rootUrl;
+ const createEntry = (entry, type) => createOrMergeEntry({
+ tree,
+ entry,
+ level,
+ type,
+ parentTreeUrl,
+ });
+
+ const formattedData = [
+ ...data.trees.map(t => createEntry(t, 'tree')),
+ ...data.submodules.map(m => createEntry(m, 'submodule')),
+ ...data.blobs.map(b => createEntry(b, 'blob')),
+ ];
+
+ commit(types.SET_DIRECTORY_DATA, { tree, data: formattedData });
+};
diff --git a/app/assets/javascripts/repo/stores/mutation_types.js b/app/assets/javascripts/repo/stores/mutation_types.js
index 4722a7dd0df..bc3390f1506 100644
--- a/app/assets/javascripts/repo/stores/mutation_types.js
+++ b/app/assets/javascripts/repo/stores/mutation_types.js
@@ -4,11 +4,13 @@ export const SET_COMMIT_REF = 'SET_COMMIT_REF';
export const SET_PARENT_TREE_URL = 'SET_PARENT_TREE_URL';
export const SET_ROOT = 'SET_ROOT';
export const SET_PREVIOUS_URL = 'SET_PREVIOUS_URL';
+export const SET_LAST_COMMIT_DATA = 'SET_LAST_COMMIT_DATA';
// Tree mutation types
export const SET_DIRECTORY_DATA = 'SET_DIRECTORY_DATA';
export const TOGGLE_TREE_OPEN = 'TOGGLE_TREE_OPEN';
export const CREATE_TMP_TREE = 'CREATE_TMP_TREE';
+export const SET_LAST_COMMIT_URL = 'SET_LAST_COMMIT_URL';
// File mutation types
export const SET_FILE_DATA = 'SET_FILE_DATA';
diff --git a/app/assets/javascripts/repo/stores/mutations.js b/app/assets/javascripts/repo/stores/mutations.js
index 2f9b038322b..ae2ba5bedf7 100644
--- a/app/assets/javascripts/repo/stores/mutations.js
+++ b/app/assets/javascripts/repo/stores/mutations.js
@@ -48,6 +48,13 @@ export default {
previousUrl,
});
},
+ [types.SET_LAST_COMMIT_DATA](state, { entry, lastCommit }) {
+ Object.assign(entry.lastCommit, {
+ url: lastCommit.commit_path,
+ message: lastCommit.commit.message,
+ updatedAt: lastCommit.commit.authored_date,
+ });
+ },
...fileMutations,
...treeMutations,
...branchMutations,
diff --git a/app/assets/javascripts/repo/stores/mutations/tree.js b/app/assets/javascripts/repo/stores/mutations/tree.js
index 52be2673107..130221c9fda 100644
--- a/app/assets/javascripts/repo/stores/mutations/tree.js
+++ b/app/assets/javascripts/repo/stores/mutations/tree.js
@@ -1,5 +1,4 @@
import * as types from '../mutation_types';
-import * as utils from '../utils';
export default {
[types.TOGGLE_TREE_OPEN](state, tree) {
@@ -8,30 +7,8 @@ export default {
});
},
[types.SET_DIRECTORY_DATA](state, { data, tree }) {
- const level = tree.level !== undefined ? tree.level + 1 : 0;
- const parentTreeUrl = data.parent_tree_url ? `${data.parent_tree_url}${data.path}` : state.endpoints.rootUrl;
-
Object.assign(tree, {
- tree: [
- ...data.trees.map(t => utils.decorateData({
- ...t,
- type: 'tree',
- parentTreeUrl,
- level,
- }, state.project.url)),
- ...data.submodules.map(m => utils.decorateData({
- ...m,
- type: 'submodule',
- parentTreeUrl,
- level,
- }, state.project.url)),
- ...data.blobs.map(b => utils.decorateData({
- ...b,
- type: 'blob',
- parentTreeUrl,
- level,
- }, state.project.url)),
- ],
+ tree: data,
});
},
[types.SET_PARENT_TREE_URL](state, url) {
@@ -39,6 +16,11 @@ export default {
parentTreeUrl: url,
});
},
+ [types.SET_LAST_COMMIT_URL](state, { tree = state, url }) {
+ Object.assign(tree, {
+ lastCommitPath: url,
+ });
+ },
[types.CREATE_TMP_TREE](state, { parent, tmpEntry }) {
parent.tree.push(tmpEntry);
},
diff --git a/app/assets/javascripts/repo/stores/state.js b/app/assets/javascripts/repo/stores/state.js
index aab74754f02..0068834831e 100644
--- a/app/assets/javascripts/repo/stores/state.js
+++ b/app/assets/javascripts/repo/stores/state.js
@@ -8,6 +8,7 @@ export default () => ({
endpoints: {},
isRoot: false,
isInitialRoot: false,
+ lastCommitPath: '',
loading: false,
onTopOfBranch: false,
openFiles: [],
diff --git a/app/assets/javascripts/repo/stores/utils.js b/app/assets/javascripts/repo/stores/utils.js
index 797c2b1e5b9..fae1f4439a9 100644
--- a/app/assets/javascripts/repo/stores/utils.js
+++ b/app/assets/javascripts/repo/stores/utils.js
@@ -1,5 +1,6 @@
export const dataStructure = () => ({
id: '',
+ key: '',
type: '',
name: '',
url: '',
@@ -12,7 +13,12 @@ export const dataStructure = () => ({
opened: false,
active: false,
changed: false,
- lastCommit: {},
+ lastCommitPath: '',
+ lastCommit: {
+ url: '',
+ message: '',
+ updatedAt: '',
+ },
tree_url: '',
blamePath: '',
commitsPath: '',
@@ -27,14 +33,13 @@ export const dataStructure = () => ({
base64: false,
});
-export const decorateData = (entity, projectUrl = '') => {
+export const decorateData = (entity) => {
const {
id,
type,
url,
name,
icon,
- last_commit,
tree_url,
path,
renderError,
@@ -51,6 +56,7 @@ export const decorateData = (entity, projectUrl = '') => {
return {
...dataStructure(),
id,
+ key: `${name}-${type}-${id}`,
type,
name,
url,
@@ -66,12 +72,6 @@ export const decorateData = (entity, projectUrl = '') => {
renderError,
content,
base64,
- // eslint-disable-next-line camelcase
- lastCommit: last_commit ? {
- url: `${projectUrl}/commit/${last_commit.id}`,
- message: last_commit.message,
- updatedAt: last_commit.committed_date,
- } : {},
};
};
@@ -106,3 +106,22 @@ export const createTemp = ({ name, path, type, level, changed, content, base64 }
renderError: base64,
});
};
+
+export const createOrMergeEntry = ({ tree, entry, type, parentTreeUrl, level }) => {
+ const found = findEntry(tree, type, entry.name);
+
+ if (found) {
+ return Object.assign({}, found, {
+ id: entry.id,
+ url: entry.url,
+ tempFile: false,
+ });
+ }
+
+ return decorateData({
+ ...entry,
+ type,
+ parentTreeUrl,
+ level,
+ });
+};
diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js
index f15452ec683..9dec5d7645a 100644
--- a/app/assets/javascripts/search_autocomplete.js
+++ b/app/assets/javascripts/search_autocomplete.js
@@ -162,13 +162,19 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '.
items = [
{
header: "" + name
- }, {
+ }
+ ];
+ const issueItems = [
+ {
text: 'Issues assigned to me',
url: issuesPath + "/?assignee_username=" + userName
}, {
text: "Issues I've created",
url: issuesPath + "/?author_username=" + userName
- }, 'separator', {
+ }
+ ];
+ const mergeRequestItems = [
+ {
text: 'Merge requests assigned to me',
url: mrPath + "/?assignee_username=" + userName
}, {
@@ -176,6 +182,11 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '.
url: mrPath + "/?author_username=" + userName
}
];
+ if (options.issuesDisabled) {
+ items = items.concat(mergeRequestItems);
+ } else {
+ items = items.concat(...issueItems, 'separator', ...mergeRequestItems);
+ }
if (!name) {
items.splice(0, 1);
}
@@ -408,6 +419,7 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '.
gl.projectOptions[projectPath] = {
name: $projectOptionsDataEl.data('name'),
issuesPath: $projectOptionsDataEl.data('issues-path'),
+ issuesDisabled: $projectOptionsDataEl.data('issues-disabled'),
mrPath: $projectOptionsDataEl.data('mr-path')
};
}
diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js
index fc97938e3d1..4f4f606d293 100644
--- a/app/assets/javascripts/shortcuts_issuable.js
+++ b/app/assets/javascripts/shortcuts_issuable.js
@@ -4,6 +4,7 @@
import _ from 'underscore';
import 'mousetrap';
import ShortcutsNavigation from './shortcuts_navigation';
+import { CopyAsGFM } from './behaviors/copy_as_gfm';
export default class ShortcutsIssuable extends ShortcutsNavigation {
constructor(isMergeRequest) {
@@ -33,8 +34,8 @@ export default class ShortcutsIssuable extends ShortcutsNavigation {
return false;
}
- const el = window.gl.CopyAsGFM.transformGFMSelection(documentFragment.cloneNode(true));
- const selected = window.gl.CopyAsGFM.nodeToGFM(el);
+ const el = CopyAsGFM.transformGFMSelection(documentFragment.cloneNode(true));
+ const selected = CopyAsGFM.nodeToGFM(el);
if (selected.trim() === '') {
return false;
diff --git a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
index 22a9a34dda3..6ee4d487c0b 100644
--- a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
@@ -1,10 +1,12 @@
<script>
import Flash from '../../../flash';
import editForm from './edit_form.vue';
+import Icon from '../../../vue_shared/components/icon.vue';
export default {
components: {
editForm,
+ Icon,
},
props: {
isConfidential: {
@@ -26,11 +28,8 @@ export default {
};
},
computed: {
- faEye() {
- const eye = this.isConfidential ? 'fa-eye-slash' : 'fa-eye';
- return {
- [eye]: true,
- };
+ confidentialityIcon() {
+ return this.isConfidential ? 'eye-slash' : 'eye';
},
},
methods: {
@@ -49,7 +48,11 @@ export default {
<template>
<div class="block issuable-sidebar-item confidentiality">
<div class="sidebar-collapsed-icon">
- <i class="fa" :class="faEye" aria-hidden="true"></i>
+ <icon
+ :name="confidentialityIcon"
+ :size="16"
+ aria-hidden="true">
+ </icon>
</div>
<div class="title hide-collapsed">
Confidentiality
@@ -70,11 +73,21 @@ export default {
:update-confidential-attribute="updateConfidentialAttribute"
/>
<div v-if="!isConfidential" class="no-value sidebar-item-value">
- <i class="fa fa-eye sidebar-item-icon"></i>
+ <icon
+ name="eye"
+ :size="16"
+ aria-hidden="true"
+ class="sidebar-item-icon inline">
+ </icon>
Not confidential
</div>
<div v-else class="value sidebar-item-value hide-collapsed">
- <i aria-hidden="true" class="fa fa-eye-slash sidebar-item-icon is-active"></i>
+ <icon
+ name="eye-slash"
+ :size="16"
+ aria-hidden="true"
+ class="sidebar-item-icon inline is-active">
+ </icon>
This issue is confidential
</div>
</div>
diff --git a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
index c4b2900e020..9aff53cf8af 100644
--- a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
+++ b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
@@ -2,6 +2,7 @@
/* global Flash */
import editForm from './edit_form.vue';
import issuableMixin from '../../../vue_shared/mixins/issuable';
+import Icon from '../../../vue_shared/components/icon.vue';
export default {
props: {
@@ -35,11 +36,12 @@ export default {
components: {
editForm,
+ Icon,
},
computed: {
- lockIconClass() {
- return this.isLocked ? 'fa-lock' : 'fa-unlock';
+ lockIcon() {
+ return this.isLocked ? 'lock' : 'lock-open';
},
isLockDialogOpen() {
@@ -66,11 +68,12 @@ export default {
<template>
<div class="block issuable-sidebar-item lock">
<div class="sidebar-collapsed-icon">
- <i
- class="fa"
- :class="lockIconClass"
+ <icon
+ :name="lockIcon"
+ :size="16"
aria-hidden="true"
- ></i>
+ class="sidebar-item-icon is-active">
+ </icon>
</div>
<div class="title hide-collapsed">
@@ -98,10 +101,12 @@ export default {
v-if="isLocked"
class="value sidebar-item-value"
>
- <i
+ <icon
+ name="lock"
+ :size="16"
aria-hidden="true"
- class="fa fa-lock sidebar-item-icon is-active"
- ></i>
+ class="sidebar-item-icon inline is-active">
+ </icon>
{{ __('Locked') }}
</div>
@@ -109,10 +114,12 @@ export default {
v-else
class="no-value sidebar-item-value hide-collapsed"
>
- <i
+ <icon
+ name="lock-open"
+ :size="16"
aria-hidden="true"
- class="fa fa-unlock sidebar-item-icon"
- ></i>
+ class="sidebar-item-icon inline">
+ </icon>
{{ __('Unlocked') }}
</div>
</div>
diff --git a/app/assets/javascripts/smart_interval.js b/app/assets/javascripts/smart_interval.js
index 2bf7a3a5d61..8e931995fc6 100644
--- a/app/assets/javascripts/smart_interval.js
+++ b/app/assets/javascripts/smart_interval.js
@@ -3,9 +3,10 @@
* and controllable by a public API.
*/
-class SmartInterval {
+export default class SmartInterval {
/**
- * @param { function } opts.callback Function to be called on each iteration (required)
+ * @param { function } opts.callback Function that returns a promise, called on each iteration
+ * unless still in progress (required)
* @param { milliseconds } opts.startingInterval `currentInterval` is set to this initially
* @param { milliseconds } opts.maxInterval `currentInterval` will be incremented to this
* @param { milliseconds } opts.hiddenInterval `currentInterval` is set to this
@@ -42,13 +43,16 @@ class SmartInterval {
const cfg = this.cfg;
const state = this.state;
- if (cfg.immediateExecution) {
+ if (cfg.immediateExecution && !this.isLoading) {
cfg.immediateExecution = false;
- cfg.callback();
+ this.triggerCallback();
}
state.intervalId = window.setInterval(() => {
- cfg.callback();
+ if (this.isLoading) {
+ return;
+ }
+ this.triggerCallback();
if (this.getCurrentInterval() === cfg.maxInterval) {
return;
@@ -76,7 +80,7 @@ class SmartInterval {
// start a timer, using the existing interval
resume() {
- this.stopTimer(); // stop exsiting timer, in case timer was not previously stopped
+ this.stopTimer(); // stop existing timer, in case timer was not previously stopped
this.start();
}
@@ -104,6 +108,18 @@ class SmartInterval {
this.initPageUnloadHandling();
}
+ triggerCallback() {
+ this.isLoading = true;
+ this.cfg.callback()
+ .then(() => {
+ this.isLoading = false;
+ })
+ .catch((err) => {
+ this.isLoading = false;
+ throw err;
+ });
+ }
+
initVisibilityChangeHandling() {
// cancel interval when tab no longer shown (prevents cached pages from polling)
document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this));
@@ -154,4 +170,3 @@ class SmartInterval {
}
}
-window.gl.SmartInterval = SmartInterval;
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index a0883b32593..759cc9925f4 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -6,7 +6,7 @@ import _ from 'underscore';
// TODO: remove eventHub hack after code splitting refactor
window.emitSidebarEvent = window.emitSidebarEvent || $.noop;
-function UsersSelect(currentUser, els) {
+function UsersSelect(currentUser, els, options = {}) {
var $els;
this.users = this.users.bind(this);
this.user = this.user.bind(this);
@@ -20,6 +20,8 @@ function UsersSelect(currentUser, els) {
}
}
+ const { handleClick } = options;
+
$els = $(els);
if (!els) {
@@ -442,6 +444,9 @@ function UsersSelect(currentUser, els) {
}
if ($el.closest('.add-issues-modal').length) {
gl.issueBoards.ModalStore.store.filter[$dropdown.data('field-name')] = user.id;
+ } else if (handleClick) {
+ e.preventDefault();
+ handleClick(user, isMarking);
} else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
return Issuable.filterResults($dropdown.closest('form'));
} else if ($dropdown.hasClass('js-filter-submit')) {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js
index 219ff94924e..13e4cb5717e 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js
@@ -1,5 +1,5 @@
import tooltip from '../../vue_shared/directives/tooltip';
-import '../../lib/utils/text_utility';
+import { pluralize } from '../../lib/utils/text_utility';
export default {
name: 'MRWidgetHeader',
@@ -14,7 +14,7 @@ export default {
return this.mr.divergedCommitsCount > 0;
},
commitsText() {
- return gl.text.pluralize('commit', this.mr.divergedCommitsCount);
+ return pluralize('commit', this.mr.divergedCommitsCount);
},
branchNameClipboardData() {
// This supports code in app/assets/javascripts/copy_to_clipboard.js that
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js
deleted file mode 100644
index 029832bdd27..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js
+++ /dev/null
@@ -1,90 +0,0 @@
-import PipelineStage from '../../pipelines/components/stage.vue';
-import ciIcon from '../../vue_shared/components/ci_icon.vue';
-import icon from '../../vue_shared/components/icon.vue';
-
-export default {
- name: 'MRWidgetPipeline',
- props: {
- mr: { type: Object, required: true },
- },
- components: {
- 'pipeline-stage': PipelineStage,
- ciIcon,
- icon,
- },
- computed: {
- hasPipeline() {
- return this.mr.pipeline && Object.keys(this.mr.pipeline).length > 0;
- },
- hasCIError() {
- const { hasCI, ciStatus } = this.mr;
-
- return hasCI && !ciStatus;
- },
- stageText() {
- return this.mr.pipeline.details.stages.length > 1 ? 'stages' : 'stage';
- },
- status() {
- return this.mr.pipeline.details.status || {};
- },
- },
- template: `
- <div
- v-if="hasPipeline || hasCIError"
- class="mr-widget-heading">
- <div class="ci-widget media">
- <template v-if="hasCIError">
- <div class="ci-status-icon ci-status-icon-failed ci-error js-ci-error append-right-10">
- <span
- aria-hidden="true">
- <icon
- name="status_failed"/>
- </span>
- </div>
- <div class="media-body">
- Could not connect to the CI server. Please check your settings and try again
- </div>
- </template>
- <template v-else-if="hasPipeline">
- <div class="ci-status-icon append-right-10">
- <a
- class="icon-link"
- :href="this.status.details_path">
- <ci-icon :status="status" />
- </a>
- </div>
- <div class="media-body">
- <span>
- Pipeline
- <a
- :href="mr.pipeline.path"
- class="pipeline-id">#{{mr.pipeline.id}}</a>
- </span>
- <span class="mr-widget-pipeline-graph">
- <span class="stage-cell">
- <div
- v-if="mr.pipeline.details.stages.length > 0"
- v-for="stage in mr.pipeline.details.stages"
- class="stage-container dropdown js-mini-pipeline-graph">
- <pipeline-stage :stage="stage" />
- </div>
- </span>
- </span>
- <span>
- {{mr.pipeline.details.status.label}} for
- <a
- :href="mr.pipeline.commit.commit_path"
- class="commit-sha js-commit-link">
- {{mr.pipeline.commit.short_id}}</a>.
- </span>
- <span
- v-if="mr.pipeline.coverage"
- class="js-mr-coverage">
- Coverage {{mr.pipeline.coverage}}%
- </span>
- </div>
- </template>
- </div>
- </div>
- `,
-};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
new file mode 100644
index 00000000000..dbc65462377
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
@@ -0,0 +1,104 @@
+<script>
+ import pipelineStage from '../../pipelines/components/stage.vue';
+ import ciIcon from '../../vue_shared/components/ci_icon.vue';
+ import icon from '../../vue_shared/components/icon.vue';
+
+ export default {
+ name: 'MRWidgetPipeline',
+ props: {
+ pipeline: {
+ type: Object,
+ required: true,
+ },
+ // This prop needs to be camelCase, html attributes are case insensive
+ // https://vuejs.org/v2/guide/components.html#camelCase-vs-kebab-case
+ hasCi: {
+ type: Boolean,
+ required: false,
+ },
+ ciStatus: {
+ type: String,
+ required: false,
+ },
+ },
+ components: {
+ pipelineStage,
+ ciIcon,
+ icon,
+ },
+ computed: {
+ hasPipeline() {
+ return this.pipeline && Object.keys(this.pipeline).length > 0;
+ },
+ hasCIError() {
+ return this.hasCi && !this.ciStatus;
+ },
+ status() {
+ return this.pipeline.details &&
+ this.pipeline.details.status ? this.pipeline.details.status : {};
+ },
+ hasStages() {
+ return this.pipeline.details &&
+ this.pipeline.details.stages &&
+ this.pipeline.details.stages.length;
+ },
+ },
+ };
+</script>
+
+<template>
+ <div
+ v-if="hasPipeline || hasCIError"
+ class="mr-widget-heading">
+ <div class="ci-widget media">
+ <template v-if="hasCIError">
+ <div class="ci-status-icon ci-status-icon-failed ci-error js-ci-error append-right-10">
+ <icon name="status_failed" />
+ </div>
+ <div class="media-body">
+ Could not connect to the CI server. Please check your settings and try again
+ </div>
+ </template>
+ <template v-else-if="hasPipeline">
+ <a
+ class="append-right-10"
+ :href="this.status.details_path">
+ <ci-icon :status="status" />
+ </a>
+
+ <div class="media-body">
+ Pipeline
+ <a
+ :href="pipeline.path"
+ class="pipeline-id">
+ #{{pipeline.id}}
+ </a>
+
+ {{pipeline.details.status.label}} for
+
+ <a
+ :href="pipeline.commit.commit_path"
+ class="commit-sha js-commit-link">
+ {{pipeline.commit.short_id}}</a>.
+
+ <span class="mr-widget-pipeline-graph">
+ <span class="stage-cell">
+ <div
+ v-if="hasStages"
+ v-for="(stage, i) in pipeline.details.stages"
+ :key="i"
+ class="stage-container dropdown js-mini-pipeline-graph">
+ <pipeline-stage :stage="stage" />
+ </div>
+ </span>
+ </span>
+
+ <template v-if="pipeline.coverage">
+ Coverage {{pipeline.coverage}}%
+ </template>
+
+ </div>
+ </template>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/dependencies.js b/app/assets/javascripts/vue_merge_request_widget/dependencies.js
index 49340c232c8..5bd8b99420a 100644
--- a/app/assets/javascripts/vue_merge_request_widget/dependencies.js
+++ b/app/assets/javascripts/vue_merge_request_widget/dependencies.js
@@ -13,7 +13,7 @@ export { default as Vue } from 'vue';
export { default as SmartInterval } from '~/smart_interval';
export { default as WidgetHeader } from './components/mr_widget_header';
export { default as WidgetMergeHelp } from './components/mr_widget_merge_help';
-export { default as WidgetPipeline } from './components/mr_widget_pipeline';
+export { default as WidgetPipeline } from './components/mr_widget_pipeline.vue';
export { default as WidgetDeployment } from './components/mr_widget_deployment';
export { default as WidgetRelatedLinks } from './components/mr_widget_related_links';
export { default as MergedState } from './components/states/mr_widget_merged';
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js
index 4f497b204a3..1274db2c4c8 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js
@@ -1,3 +1,4 @@
+import SmartInterval from '~/smart_interval';
import Flash from '../flash';
import {
WidgetHeader,
@@ -60,7 +61,7 @@ export default {
return this.mr.hasCI;
},
shouldRenderRelatedLinks() {
- return this.mr.relatedLinks;
+ return !!this.mr.relatedLinks;
},
shouldRenderDeployments() {
return this.mr.deployments.length;
@@ -81,7 +82,7 @@ export default {
return new MRWidgetService(endpoints);
},
checkStatus(cb) {
- this.service.checkStatus()
+ return this.service.checkStatus()
.then(res => res.json())
.then((res) => {
this.handleNotification(res);
@@ -97,7 +98,7 @@ export default {
});
},
initPolling() {
- this.pollingInterval = new gl.SmartInterval({
+ this.pollingInterval = new SmartInterval({
callback: this.checkStatus,
startingInterval: 10000,
maxInterval: 30000,
@@ -106,7 +107,7 @@ export default {
});
},
initDeploymentsPolling() {
- this.deploymentsInterval = new gl.SmartInterval({
+ this.deploymentsInterval = new SmartInterval({
callback: this.fetchDeployments,
startingInterval: 30000,
maxInterval: 120000,
@@ -121,7 +122,7 @@ export default {
}
},
fetchDeployments() {
- this.service.fetchDeployments()
+ return this.service.fetchDeployments()
.then(res => res.json())
.then((res) => {
if (res.length) {
@@ -235,7 +236,10 @@ export default {
<mr-widget-header :mr="mr" />
<mr-widget-pipeline
v-if="shouldRenderPipelines"
- :mr="mr" />
+ :pipeline="mr.pipeline"
+ :ci-status="mr.ciStatus"
+ :has-ci="mr.hasCI"
+ />
<mr-widget-deployment
v-if="shouldRenderDeployments"
:mr="mr"
diff --git a/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue b/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue
index 16c0a8efcd2..564fc5029af 100644
--- a/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue
+++ b/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue
@@ -1,4 +1,6 @@
<script>
+ import Icon from '../../../vue_shared/components/icon.vue';
+
export default {
props: {
isLocked: {
@@ -14,12 +16,16 @@
},
},
+ components: {
+ Icon,
+ },
+
computed: {
- iconClass() {
- return {
- 'fa-eye-slash': this.isConfidential,
- 'fa-lock': this.isLocked,
- };
+ warningIcon() {
+ if (this.isConfidential) return 'eye-slash';
+ if (this.isLocked) return 'lock';
+
+ return '';
},
isLockedAndConfidential() {
@@ -30,12 +36,13 @@
</script>
<template>
<div class="issuable-note-warning">
- <i
- aria-hidden="true"
- class="fa icon"
- :class="iconClass"
- v-if="!isLockedAndConfidential"
- ></i>
+ <icon
+ :name="warningIcon"
+ :size="16"
+ class="icon inline"
+ aria-hidden="true"
+ v-if="!isLockedAndConfidential">
+ </icon>
<span v-if="isLockedAndConfidential">
{{ __('This issue is confidential and locked.') }}
diff --git a/app/assets/javascripts/vue_shared/components/loading_button.vue b/app/assets/javascripts/vue_shared/components/loading_button.vue
index 6670b554faf..247943f83e6 100644
--- a/app/assets/javascripts/vue_shared/components/loading_button.vue
+++ b/app/assets/javascripts/vue_shared/components/loading_button.vue
@@ -26,10 +26,20 @@ export default {
required: false,
default: false,
},
+ disabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
label: {
type: String,
required: false,
},
+ containerClass: {
+ type: String,
+ required: false,
+ default: 'btn btn-align-content',
+ },
},
components: {
loadingIcon,
@@ -44,10 +54,10 @@ export default {
<template>
<button
- class="btn btn-align-content"
@click="onClick"
type="button"
- :disabled="loading"
+ :class="containerClass"
+ :disabled="loading || disabled"
>
<transition name="fade">
<loading-icon
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index 6511828e982..a873e00d0f3 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -47,8 +47,10 @@
},
},
methods: {
- toggleMarkdownPreview() {
- this.previewMarkdown = !this.previewMarkdown;
+ showPreviewTab() {
+ if (this.previewMarkdown) return;
+
+ this.previewMarkdown = true;
/*
Can't use `$refs` as the component is technically in the parent component
@@ -56,20 +58,22 @@
*/
const text = this.$slots.textarea[0].elm.value;
- if (!this.previewMarkdown) {
- this.markdownPreview = '';
- } else if (text) {
+ if (text) {
this.markdownPreviewLoading = true;
this.$http.post(this.markdownPreviewPath, { text })
.then(resp => resp.json())
- .then((data) => {
- this.renderMarkdown(data);
- })
+ .then(data => this.renderMarkdown(data))
.catch(() => new Flash('Error loading markdown preview'));
} else {
this.renderMarkdown();
}
},
+
+ showWriteTab() {
+ this.markdownPreview = '';
+ this.previewMarkdown = false;
+ },
+
renderMarkdown(data = {}) {
this.markdownPreviewLoading = false;
this.markdownPreview = data.body || 'Nothing to preview.';
@@ -106,7 +110,8 @@
ref="gl-form">
<markdown-header
:preview-markdown="previewMarkdown"
- @toggle-markdown="toggleMarkdownPreview" />
+ @preview-markdown="showPreviewTab"
+ @write-markdown="showWriteTab" />
<div
class="md-write-holder"
v-show="!previewMarkdown">
diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue
index 7541731083b..70f5fc1d664 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue
@@ -18,23 +18,31 @@
icon,
},
methods: {
- toggleMarkdownPreview(e, form) {
- if (form && !form.find('.js-vue-markdown-field').length) {
- return;
- } else if (e.target.blur) {
- e.target.blur();
- }
+ isMarkdownForm(form) {
+ return form && !form.find('.js-vue-markdown-field').length;
+ },
+
+ previewMarkdownTab(event, form) {
+ if (event.target.blur) event.target.blur();
+ if (this.isMarkdownForm(form)) return;
+
+ this.$emit('preview-markdown');
+ },
+
+ writeMarkdownTab(event, form) {
+ if (event.target.blur) event.target.blur();
+ if (this.isMarkdownForm(form)) return;
- this.$emit('toggle-markdown');
+ this.$emit('write-markdown');
},
},
mounted() {
- $(document).on('markdown-preview:show.vue', this.toggleMarkdownPreview);
- $(document).on('markdown-preview:hide.vue', this.toggleMarkdownPreview);
+ $(document).on('markdown-preview:show.vue', this.previewMarkdownTab);
+ $(document).on('markdown-preview:hide.vue', this.writeMarkdownTab);
},
beforeDestroy() {
- $(document).on('markdown-preview:show.vue', this.toggleMarkdownPreview);
- $(document).off('markdown-preview:hide.vue', this.toggleMarkdownPreview);
+ $(document).off('markdown-preview:show.vue', this.previewMarkdownTab);
+ $(document).off('markdown-preview:hide.vue', this.writeMarkdownTab);
},
};
</script>
@@ -44,17 +52,19 @@
<ul class="nav-links clearfix">
<li :class="{ active: !previewMarkdown }">
<a
+ class="js-write-link"
href="#md-write-holder"
tabindex="-1"
- @click.prevent="toggleMarkdownPreview($event)">
+ @click.prevent="writeMarkdownTab($event)">
Write
</a>
</li>
<li :class="{ active: previewMarkdown }">
<a
+ class="js-preview-link"
href="#md-preview-holder"
tabindex="-1"
- @click.prevent="toggleMarkdownPreview($event)">
+ @click.prevent="previewMarkdownTab($event)">
Preview
</a>
</li>
diff --git a/app/assets/javascripts/vue_shared/components/popup_dialog.vue b/app/assets/javascripts/vue_shared/components/popup_dialog.vue
index 9e8c10bdc1a..47efee64c6e 100644
--- a/app/assets/javascripts/vue_shared/components/popup_dialog.vue
+++ b/app/assets/javascripts/vue_shared/components/popup_dialog.vue
@@ -5,17 +5,27 @@ export default {
props: {
title: {
type: String,
- required: true,
+ required: false,
},
text: {
type: String,
required: false,
},
+ hideFooter: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
kind: {
type: String,
required: false,
default: 'primary',
},
+ modalDialogClass: {
+ type: String,
+ required: false,
+ default: '',
+ },
closeKind: {
type: String,
required: false,
@@ -30,6 +40,11 @@ export default {
type: String,
required: true,
},
+ submitDisabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
@@ -57,43 +72,58 @@ export default {
</script>
<template>
-<div
- class="modal popup-dialog"
- role="dialog"
- tabindex="-1">
- <div class="modal-dialog" role="document">
- <div class="modal-content">
- <div class="modal-header">
- <button type="button"
- class="close"
- @click="close"
- aria-label="Close">
- <span aria-hidden="true">&times;</span>
- </button>
- <h4 class="modal-title">{{this.title}}</h4>
- </div>
- <div class="modal-body">
- <slot name="body" :text="text">
- <p>{{text}}</p>
- </slot>
- </div>
- <div class="modal-footer">
- <button
- type="button"
- class="btn"
- :class="btnCancelKindClass"
- @click="close">
- {{ closeButtonLabel }}
- </button>
- <button
- type="button"
- class="btn"
- :class="btnKindClass"
- @click="emitSubmit(true)">
- {{ primaryButtonLabel }}
- </button>
+<div class="modal-open">
+ <div
+ class="modal popup-dialog"
+ role="dialog"
+ tabindex="-1"
+ >
+ <div
+ :class="modalDialogClass"
+ class="modal-dialog"
+ role="document"
+ >
+ <div class="modal-content">
+ <div class="modal-header">
+ <slot name="header">
+ <h4 class="modal-title pull-left">
+ {{this.title}}
+ </h4>
+ <button
+ type="button"
+ class="close pull-right"
+ @click="close"
+ aria-label="Close"
+ >
+ <span aria-hidden="true">&times;</span>
+ </button>
+ </slot>
+ </div>
+ <div class="modal-body">
+ <slot name="body" :text="text">
+ <p>{{this.text}}</p>
+ </slot>
+ </div>
+ <div class="modal-footer" v-if="!hideFooter">
+ <button
+ type="button"
+ class="btn pull-left"
+ :class="btnCancelKindClass"
+ @click="close">
+ {{ closeButtonLabel }}
+ </button>
+ <button
+ type="button"
+ class="btn pull-right"
+ :disabled="submitDisabled"
+ :class="btnKindClass"
+ @click="emitSubmit(true)">
+ {{ primaryButtonLabel }}
+ </button>
+ </div>
</div>
</div>
</div>
+ <div class="modal-backdrop fade in" />
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue b/app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue
new file mode 100644
index 00000000000..b06493e6c66
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue
@@ -0,0 +1,37 @@
+<script>
+ export default {
+ props: {
+ small: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ lines: {
+ type: Number,
+ required: false,
+ default: 6,
+ },
+ },
+ computed: {
+ lineClasses() {
+ return new Array(this.lines).fill().map((_, i) => `skeleton-line-${i + 1}`);
+ },
+ },
+ };
+</script>
+
+<template>
+ <div
+ class="animation-container"
+ :class="{
+ 'animation-container-small': small,
+ }"
+ >
+ <div
+ v-for="(css, index) in lineClasses"
+ :key="index"
+ :class="css"
+ >
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/wikis.js b/app/assets/javascripts/wikis.js
index a0025ddb598..7a865587444 100644
--- a/app/assets/javascripts/wikis.js
+++ b/app/assets/javascripts/wikis.js
@@ -1,4 +1,5 @@
import bp from './breakpoints';
+import { slugify } from './lib/utils/text_utility';
export default class Wikis {
constructor() {
@@ -23,7 +24,7 @@ export default class Wikis {
if (!this.newWikiForm) return;
const slugInput = this.newWikiForm.querySelector('#new_wiki_path');
- const slug = gl.text.slugify(slugInput.value);
+ const slug = slugify(slugInput.value);
if (slug.length > 0) {
const wikisPath = slugInput.getAttribute('data-wikis-path');