summaryrefslogtreecommitdiff
path: root/app/assets
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets')
-rw-r--r--app/assets/javascripts/ci_variable_list/store/mutations.js4
-rw-r--r--app/assets/javascripts/ide/stores/mutation_types.js1
-rw-r--r--app/assets/javascripts/ide/stores/mutations.js9
-rw-r--r--app/assets/javascripts/ide/stores/utils.js7
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard_header.vue27
-rw-r--r--app/assets/javascripts/monitoring/monitoring_app.js5
-rw-r--r--app/assets/javascripts/monitoring/pages/dashboard_page.vue11
-rw-r--r--app/assets/javascripts/monitoring/router/constants.js1
-rw-r--r--app/assets/javascripts/monitoring/router/routes.js9
-rw-r--r--app/assets/javascripts/monitoring/stores/actions.js4
-rw-r--r--app/assets/javascripts/monitoring/stores/mutation_types.js2
-rw-r--r--app/assets/javascripts/monitoring/stores/mutations.js4
-rw-r--r--app/assets/javascripts/pages/projects/edit/index.js5
-rw-r--r--app/assets/javascripts/pages/projects/issues/service_desk/filtered_search.js30
-rw-r--r--app/assets/javascripts/pages/projects/issues/service_desk/index.js11
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue215
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue160
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue169
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/event_hub.js3
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/index.js41
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/services/service_desk_service.js27
-rw-r--r--app/assets/javascripts/repository/components/web_ide_link.vue47
-rw-r--r--app/assets/javascripts/repository/index.js21
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js3
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_identifier_instance_text.js40
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/constants.js1
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue23
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue19
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/getters.js9
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/state.js1
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss4
-rw-r--r--app/assets/stylesheets/pages/service_desk.scss7
35 files changed, 747 insertions, 191 deletions
diff --git a/app/assets/javascripts/ci_variable_list/store/mutations.js b/app/assets/javascripts/ci_variable_list/store/mutations.js
index 8903b742537..961cecee298 100644
--- a/app/assets/javascripts/ci_variable_list/store/mutations.js
+++ b/app/assets/javascripts/ci_variable_list/store/mutations.js
@@ -74,7 +74,7 @@ export default {
variable_type: displayText.variableText,
key: '',
secret_value: '',
- protected: false,
+ protected_variable: false,
masked: false,
environment_scope: displayText.allEnvironmentsText,
};
@@ -103,7 +103,7 @@ export default {
},
[types.SET_VARIABLE_PROTECTED](state) {
- state.variable.protected = true;
+ state.variable.protected_variable = true;
},
[types.UPDATE_VARIABLE_KEY](state, key) {
diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js
index d94adc3760f..ae119c2b1fd 100644
--- a/app/assets/javascripts/ide/stores/mutation_types.js
+++ b/app/assets/javascripts/ide/stores/mutation_types.js
@@ -1,6 +1,5 @@
export const SET_INITIAL_DATA = 'SET_INITIAL_DATA';
export const TOGGLE_LOADING = 'TOGGLE_LOADING';
-export const SET_LAST_COMMIT_DATA = 'SET_LAST_COMMIT_DATA';
export const SET_LAST_COMMIT_MSG = 'SET_LAST_COMMIT_MSG';
export const SET_RESIZING_STATUS = 'SET_RESIZING_STATUS';
export const SET_EMPTY_STATE_SVGS = 'SET_EMPTY_STATE_SVGS';
diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js
index e827aacac13..c64839e5019 100644
--- a/app/assets/javascripts/ide/stores/mutations.js
+++ b/app/assets/javascripts/ide/stores/mutations.js
@@ -34,15 +34,6 @@ export default {
panelResizing: resizing,
});
},
- [types.SET_LAST_COMMIT_DATA](state, { entry, lastCommit }) {
- Object.assign(entry.lastCommit, {
- id: lastCommit.commit.id,
- url: lastCommit.commit_path,
- message: lastCommit.commit.message,
- author: lastCommit.commit.author_name,
- updatedAt: lastCommit.commit.authored_date,
- });
- },
[types.SET_LAST_COMMIT_MSG](state, lastCommitMsg) {
Object.assign(state, {
lastCommitMsg,
diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js
index 1c5fe9fe9a5..f074e6880d0 100644
--- a/app/assets/javascripts/ide/stores/utils.js
+++ b/app/assets/javascripts/ide/stores/utils.js
@@ -25,13 +25,6 @@ export const dataStructure = () => ({
changed: false,
staged: false,
lastCommitSha: '',
- lastCommit: {
- id: '',
- url: '',
- message: '',
- updatedAt: '',
- author: '',
- },
rawPath: '',
binary: false,
raw: '',
diff --git a/app/assets/javascripts/monitoring/components/dashboard_header.vue b/app/assets/javascripts/monitoring/components/dashboard_header.vue
index ae8c586ff8c..fe6ca3a2a07 100644
--- a/app/assets/javascripts/monitoring/components/dashboard_header.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard_header.vue
@@ -127,6 +127,7 @@ export default {
'projectPath',
'canAccessOperationsSettings',
'operationsSettingsPath',
+ 'currentDashboard',
]),
...mapGetters('monitoringDashboard', ['selectedDashboard', 'filteredEnvironments']),
isOutOfTheBoxDashboard() {
@@ -164,11 +165,14 @@ export default {
methods: {
...mapActions('monitoringDashboard', ['filterEnvironments', 'toggleStarredValue']),
selectDashboard(dashboard) {
- const params = {
- dashboard: encodeURIComponent(dashboard.path),
- };
-
- redirectTo(mergeUrlParams(params, window.location.href));
+ // Once the sidebar See metrics link is updated to the new URL,
+ // this sort of hardcoding will not be necessary.
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/229277
+ const baseURL = `${this.projectPath}/-/metrics`;
+ const dashboardPath = encodeURIComponent(
+ dashboard.out_of_the_box_dashboard ? dashboard.path : dashboard.display_name,
+ );
+ redirectTo(`${baseURL}/${dashboardPath}`);
},
debouncedEnvironmentsSearch: debounce(function environmentsSearchOnInput(searchTerm) {
this.filterEnvironments(searchTerm);
@@ -193,6 +197,17 @@ export default {
submitCustomMetricsForm() {
this.$refs.customMetricsForm.submit();
},
+ getEnvironmentPath(environment) {
+ // Once the sidebar See metrics link is updated to the new URL,
+ // this sort of hardcoding will not be necessary.
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/229277
+ const baseURL = `${this.projectPath}/-/metrics`;
+ const dashboardPath = encodeURIComponent(this.currentDashboard || '');
+ // The environment_metrics_spec.rb requires the URL to not have
+ // slashes. Hence, this additional check.
+ const url = dashboardPath ? `${baseURL}/${dashboardPath}` : baseURL;
+ return mergeUrlParams({ environment }, url);
+ },
},
modalIds: {
addMetric: 'addMetric',
@@ -255,7 +270,7 @@ export default {
:key="environment.id"
:active="environment.name === currentEnvironmentName"
active-class="is-active"
- :href="environment.metrics_path"
+ :href="getEnvironmentPath(environment.id)"
>{{ environment.name }}</gl-dropdown-item
>
</div>
diff --git a/app/assets/javascripts/monitoring/monitoring_app.js b/app/assets/javascripts/monitoring/monitoring_app.js
index 6e7df9efbb1..307154c9a84 100644
--- a/app/assets/javascripts/monitoring/monitoring_app.js
+++ b/app/assets/javascripts/monitoring/monitoring_app.js
@@ -1,6 +1,5 @@
import Vue from 'vue';
import { GlToast } from '@gitlab/ui';
-import { getParameterValues } from '~/lib/utils/url_utility';
import { createStore } from './stores';
import createRouter from './router';
import { stateAndPropsFromDataset } from './utils';
@@ -11,11 +10,9 @@ export default (props = {}) => {
const el = document.getElementById('prometheus-graphs');
if (el && el.dataset) {
- const [encodedDashboard] = getParameterValues('dashboard');
- const currentDashboard = encodedDashboard ? decodeURIComponent(encodedDashboard) : null;
const { metricsDashboardBasePath, ...dataset } = el.dataset;
- const { initState, dataProps } = stateAndPropsFromDataset({ currentDashboard, ...dataset });
+ const { initState, dataProps } = stateAndPropsFromDataset(dataset);
const store = createStore(initState);
const router = createRouter(metricsDashboardBasePath);
diff --git a/app/assets/javascripts/monitoring/pages/dashboard_page.vue b/app/assets/javascripts/monitoring/pages/dashboard_page.vue
index 519a20d7be3..df0e2d7f8f6 100644
--- a/app/assets/javascripts/monitoring/pages/dashboard_page.vue
+++ b/app/assets/javascripts/monitoring/pages/dashboard_page.vue
@@ -1,4 +1,5 @@
<script>
+import { mapActions } from 'vuex';
import Dashboard from '../components/dashboard.vue';
export default {
@@ -11,6 +12,16 @@ export default {
required: true,
},
},
+ created() {
+ // This is to support the older URL <project>/-/environments/:env_id/metrics?dashboard=:path
+ // and the new format <project>/-/metrics/:dashboardPath
+ const encodedDashboard = this.$route.query.dashboard || this.$route.params.dashboard;
+ const currentDashboard = encodedDashboard ? decodeURIComponent(encodedDashboard) : null;
+ this.setCurrentDashboard({ currentDashboard });
+ },
+ methods: {
+ ...mapActions('monitoringDashboard', ['setCurrentDashboard']),
+ },
};
</script>
<template>
diff --git a/app/assets/javascripts/monitoring/router/constants.js b/app/assets/javascripts/monitoring/router/constants.js
index acfcd03f928..fedfebe33e9 100644
--- a/app/assets/javascripts/monitoring/router/constants.js
+++ b/app/assets/javascripts/monitoring/router/constants.js
@@ -1,3 +1,4 @@
export const BASE_DASHBOARD_PAGE = 'dashboard';
+export const CUSTOM_DASHBOARD_PAGE = 'custom_dashboard';
export default {};
diff --git a/app/assets/javascripts/monitoring/router/routes.js b/app/assets/javascripts/monitoring/router/routes.js
index 1e0cc1715a7..4b82791178a 100644
--- a/app/assets/javascripts/monitoring/router/routes.js
+++ b/app/assets/javascripts/monitoring/router/routes.js
@@ -1,6 +1,6 @@
import DashboardPage from '../pages/dashboard_page.vue';
-import { BASE_DASHBOARD_PAGE } from './constants';
+import { BASE_DASHBOARD_PAGE, CUSTOM_DASHBOARD_PAGE } from './constants';
/**
* Because the cluster health page uses the dashboard
@@ -12,7 +12,12 @@ import { BASE_DASHBOARD_PAGE } from './constants';
export default [
{
name: BASE_DASHBOARD_PAGE,
- path: '*',
+ path: '/',
+ component: DashboardPage,
+ },
+ {
+ name: CUSTOM_DASHBOARD_PAGE,
+ path: '/:dashboard(.*)',
component: DashboardPage,
},
];
diff --git a/app/assets/javascripts/monitoring/stores/actions.js b/app/assets/javascripts/monitoring/stores/actions.js
index cac04faae98..3da3aa2cb58 100644
--- a/app/assets/javascripts/monitoring/stores/actions.js
+++ b/app/assets/javascripts/monitoring/stores/actions.js
@@ -97,6 +97,10 @@ export const clearExpandedPanel = ({ commit }) => {
});
};
+export const setCurrentDashboard = ({ commit }, { currentDashboard }) => {
+ commit(types.SET_CURRENT_DASHBOARD, currentDashboard);
+};
+
// All Data
/**
diff --git a/app/assets/javascripts/monitoring/stores/mutation_types.js b/app/assets/javascripts/monitoring/stores/mutation_types.js
index e1fa037c5bb..d408628fc4d 100644
--- a/app/assets/javascripts/monitoring/stores/mutation_types.js
+++ b/app/assets/javascripts/monitoring/stores/mutation_types.js
@@ -9,6 +9,8 @@ export const REQUEST_DASHBOARD_STARRING = 'REQUEST_DASHBOARD_STARRING';
export const RECEIVE_DASHBOARD_STARRING_SUCCESS = 'RECEIVE_DASHBOARD_STARRING_SUCCESS';
export const RECEIVE_DASHBOARD_STARRING_FAILURE = 'RECEIVE_DASHBOARD_STARRING_FAILURE';
+export const SET_CURRENT_DASHBOARD = 'SET_CURRENT_DASHBOARD';
+
// Annotations
export const RECEIVE_ANNOTATIONS_SUCCESS = 'RECEIVE_ANNOTATIONS_SUCCESS';
export const RECEIVE_ANNOTATIONS_FAILURE = 'RECEIVE_ANNOTATIONS_FAILURE';
diff --git a/app/assets/javascripts/monitoring/stores/mutations.js b/app/assets/javascripts/monitoring/stores/mutations.js
index 28c6b14a029..744441c8935 100644
--- a/app/assets/javascripts/monitoring/stores/mutations.js
+++ b/app/assets/javascripts/monitoring/stores/mutations.js
@@ -97,6 +97,10 @@ export default {
state.isUpdatingStarredValue = false;
},
+ [types.SET_CURRENT_DASHBOARD](state, currentDashboard) {
+ state.currentDashboard = currentDashboard;
+ },
+
/**
* Deployments and environments
*/
diff --git a/app/assets/javascripts/pages/projects/edit/index.js b/app/assets/javascripts/pages/projects/edit/index.js
index 63762e414df..e65c18c07a9 100644
--- a/app/assets/javascripts/pages/projects/edit/index.js
+++ b/app/assets/javascripts/pages/projects/edit/index.js
@@ -8,6 +8,8 @@ import initFilePickers from '~/file_pickers';
import initProjectLoadingSpinner from '../shared/save_project_loader';
import initProjectPermissionsSettings from '../shared/permissions';
import initProjectRemoveModal from '~/projects/project_remove_modal';
+import UserCallout from '~/user_callout';
+import initServiceDesk from '~/projects/settings_service_desk';
document.addEventListener('DOMContentLoaded', () => {
initFilePickers();
@@ -16,6 +18,9 @@ document.addEventListener('DOMContentLoaded', () => {
initProjectRemoveModal();
mountBadgeSettings(PROJECT_BADGE);
+ new UserCallout({ className: 'js-service-desk-callout' }); // eslint-disable-line no-new
+ initServiceDesk();
+
initProjectLoadingSpinner();
initProjectPermissionsSettings();
setupTransferEdit('.js-project-transfer-form', 'select.select2');
diff --git a/app/assets/javascripts/pages/projects/issues/service_desk/filtered_search.js b/app/assets/javascripts/pages/projects/issues/service_desk/filtered_search.js
new file mode 100644
index 00000000000..72003b61c8a
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/issues/service_desk/filtered_search.js
@@ -0,0 +1,30 @@
+/* eslint-disable class-methods-use-this */
+import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys';
+import FilteredSearchManager from 'ee_else_ce/filtered_search/filtered_search_manager';
+
+const AUTHOR_PARAM_KEY = 'author_username';
+
+export default class FilteredSearchServiceDesk extends FilteredSearchManager {
+ constructor(supportBotData) {
+ super({
+ page: 'service_desk',
+ filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys,
+ });
+
+ this.supportBotData = supportBotData;
+ }
+
+ canEdit(tokenName) {
+ return tokenName !== 'author';
+ }
+
+ modifyUrlParams(paramsArray) {
+ const supportBotParamPair = `${AUTHOR_PARAM_KEY}=${this.supportBotData.username}`;
+ const onlyValidParams = paramsArray.filter(param => param.indexOf(AUTHOR_PARAM_KEY) === -1);
+
+ // unshift ensures author param is always first token element
+ onlyValidParams.unshift(supportBotParamPair);
+
+ return onlyValidParams;
+ }
+}
diff --git a/app/assets/javascripts/pages/projects/issues/service_desk/index.js b/app/assets/javascripts/pages/projects/issues/service_desk/index.js
new file mode 100644
index 00000000000..56054f5fc80
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/issues/service_desk/index.js
@@ -0,0 +1,11 @@
+import FilteredSearchServiceDesk from './filtered_search';
+
+document.addEventListener('DOMContentLoaded', () => {
+ const supportBotData = JSON.parse(
+ document.querySelector('.js-service-desk-issues').dataset.supportBot,
+ );
+
+ const filteredSearchManager = new FilteredSearchServiceDesk(supportBotData);
+
+ filteredSearchManager.setup();
+});
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue
index 4efabcb7df3..5ef1f959b2c 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue
@@ -1,12 +1,19 @@
<script>
-import { GlSprintf, GlLink } from '@gitlab/ui';
+import { GlFormRadio, GlFormRadioGroup, GlLink, GlSprintf } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
import { getWeekdayNames } from '~/lib/utils/datetime_utility';
+const KEY_EVERY_DAY = 'everyDay';
+const KEY_EVERY_WEEK = 'everyWeek';
+const KEY_EVERY_MONTH = 'everyMonth';
+const KEY_CUSTOM = 'custom';
+
export default {
components: {
- GlSprintf,
+ GlFormRadio,
+ GlFormRadioGroup,
GlLink,
+ GlSprintf,
},
props: {
initialCronInterval: {
@@ -22,6 +29,7 @@ export default {
randomWeekDayIndex: this.generateRandomWeekDayIndex(),
randomDay: this.generateRandomDay(),
inputNameAttribute: 'schedule[cron]',
+ radioValue: this.initialCronInterval ? KEY_CUSTOM : KEY_EVERY_DAY,
cronInterval: this.initialCronInterval,
cronSyntaxUrl: 'https://en.wikipedia.org/wiki/Cron',
};
@@ -29,14 +37,11 @@ export default {
computed: {
cronIntervalPresets() {
return {
- everyDay: `0 ${this.randomHour} * * *`,
- everyWeek: `0 ${this.randomHour} * * ${this.randomWeekDayIndex}`,
- everyMonth: `0 ${this.randomHour} ${this.randomDay} * *`,
+ [KEY_EVERY_DAY]: `0 ${this.randomHour} * * *`,
+ [KEY_EVERY_WEEK]: `0 ${this.randomHour} * * ${this.randomWeekDayIndex}`,
+ [KEY_EVERY_MONTH]: `0 ${this.randomHour} ${this.randomDay} * *`,
};
},
- intervalIsPreset() {
- return Object.values(this.cronIntervalPresets).includes(this.cronInterval);
- },
formattedTime() {
if (this.randomHour > 12) {
return `${this.randomHour - 12}:00pm`;
@@ -45,24 +50,36 @@ export default {
}
return `${this.randomHour}:00am`;
},
+ radioOptions() {
+ return [
+ {
+ value: KEY_EVERY_DAY,
+ text: sprintf(s__(`Every day (at %{time})`), { time: this.formattedTime }),
+ },
+ {
+ value: KEY_EVERY_WEEK,
+ text: sprintf(s__('Every week (%{weekday} at %{time})'), {
+ weekday: this.weekday,
+ time: this.formattedTime,
+ }),
+ },
+ {
+ value: KEY_EVERY_MONTH,
+ text: sprintf(s__('Every month (Day %{day} at %{time})'), {
+ day: this.randomDay,
+ time: this.formattedTime,
+ }),
+ },
+ {
+ value: KEY_CUSTOM,
+ text: s__('PipelineScheduleIntervalPattern|Custom (%{linkStart}Cron syntax%{linkEnd})'),
+ link: this.cronSyntaxUrl,
+ },
+ ];
+ },
weekday() {
return getWeekdayNames()[this.randomWeekDayIndex];
},
- everyDayText() {
- return sprintf(s__(`Every day (at %{time})`), { time: this.formattedTime });
- },
- everyWeekText() {
- return sprintf(s__('Every week (%{weekday} at %{time})'), {
- weekday: this.weekday,
- time: this.formattedTime,
- });
- },
- everyMonthText() {
- return sprintf(s__('Every month (Day %{day} at %{time})'), {
- day: this.randomDay,
- time: this.formattedTime,
- });
- },
},
watch: {
cronInterval() {
@@ -72,38 +89,18 @@ export default {
gl.pipelineScheduleFieldErrors.updateFormValidityState();
});
},
- },
- // If at the mounting stage the default is still an empty string, we
- // know we are not editing an existing field so we update it so
- // that the default is the first radio option
- mounted() {
- if (this.cronInterval === '') {
- this.cronInterval = this.cronIntervalPresets.everyDay;
- }
+ radioValue: {
+ immediate: true,
+ handler(val) {
+ if (val !== KEY_CUSTOM) {
+ this.cronInterval = this.cronIntervalPresets[val];
+ }
+ },
+ },
},
methods: {
- setCustomInput(e) {
- if (!this.isEditingCustom) {
- this.isEditingCustom = true;
- this.$refs.customInput.click();
- // Because we need to manually trigger the click on the radio btn,
- // it will add a space to update the v-model. If the user is typing
- // and the space is added, it will feel very unituitive so we reset
- // the value to the original
- this.cronInterval = e.target.value;
- }
- if (this.intervalIsPreset) {
- this.isEditingCustom = false;
- }
- },
- toggleCustomInput(shouldEnable) {
- this.isEditingCustom = shouldEnable;
-
- if (shouldEnable) {
- // We need to change the value so other radios don't remain selected
- // because the model (cronInterval) hasn't changed. The server trims it.
- this.cronInterval = `${this.cronInterval} `;
- }
+ onCustomInput() {
+ this.radioValue = KEY_CUSTOM;
},
generateRandomHour() {
return Math.floor(Math.random() * 23);
@@ -119,89 +116,33 @@ export default {
</script>
<template>
- <div class="interval-pattern-form-group">
- <div class="cron-preset-radio-input">
- <input
- id="every-day"
- v-model="cronInterval"
- :name="inputNameAttribute"
- :value="cronIntervalPresets.everyDay"
- class="label-bold"
- type="radio"
- @click="toggleCustomInput(false)"
- />
-
- <label class="label-bold" for="every-day">
- {{ everyDayText }}
- </label>
- </div>
-
- <div class="cron-preset-radio-input">
- <input
- id="every-week"
- v-model="cronInterval"
- :name="inputNameAttribute"
- :value="cronIntervalPresets.everyWeek"
- class="label-bold"
- type="radio"
- @click="toggleCustomInput(false)"
- />
-
- <label class="label-bold" for="every-week">
- {{ everyWeekText }}
- </label>
- </div>
-
- <div class="cron-preset-radio-input">
- <input
- id="every-month"
- v-model="cronInterval"
- :name="inputNameAttribute"
- :value="cronIntervalPresets.everyMonth"
- class="label-bold"
- type="radio"
- @click="toggleCustomInput(false)"
- />
-
- <label class="label-bold" for="every-month">
- {{ everyMonthText }}
- </label>
- </div>
-
- <div class="cron-preset-radio-input">
- <input
- id="custom"
- ref="customInput"
- v-model="cronInterval"
- :name="inputNameAttribute"
- :value="cronInterval"
- class="label-bold"
- type="radio"
- @click="toggleCustomInput(true)"
- />
-
- <label for="custom"> {{ s__('PipelineSheduleIntervalPattern|Custom') }} </label>
-
- <gl-sprintf :message="__('(%{linkStart}Cron syntax%{linkEnd})')">
- <template #link="{content}">
- <gl-link :href="cronSyntaxUrl" target="_blank" class="gl-font-sm">
- {{ content }}
- </gl-link>
- </template>
- </gl-sprintf>
- </div>
-
- <div class="cron-interval-input-wrapper">
- <input
- id="schedule_cron"
- v-model="cronInterval"
- :placeholder="__('Define a custom pattern with cron syntax')"
- :name="inputNameAttribute"
- class="form-control inline cron-interval-input"
- type="text"
- required="true"
- @input="setCustomInput"
- />
- </div>
+ <div>
+ <gl-form-radio-group v-model="radioValue" :name="inputNameAttribute">
+ <gl-form-radio
+ v-for="option in radioOptions"
+ :key="option.value"
+ :value="option.value"
+ :data-testid="option.value"
+ >
+ <gl-sprintf v-if="option.link" :message="option.text">
+ <template #link="{content}">
+ <gl-link :href="option.link" target="_blank" class="gl-font-sm">
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ <template v-else>{{ option.text }}</template>
+ </gl-form-radio>
+ </gl-form-radio-group>
+ <input
+ id="schedule_cron"
+ v-model="cronInterval"
+ :placeholder="__('Define a custom pattern with cron syntax')"
+ :name="inputNameAttribute"
+ class="form-control inline cron-interval-input"
+ type="text"
+ required="true"
+ @input="onCustomInput"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue
new file mode 100644
index 00000000000..d61569fcd6e
--- /dev/null
+++ b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue
@@ -0,0 +1,160 @@
+<script>
+import { GlAlert } from '@gitlab/ui';
+import { __ } from '~/locale';
+import ServiceDeskSetting from './service_desk_setting.vue';
+import ServiceDeskService from '../services/service_desk_service';
+import eventHub from '../event_hub';
+
+export default {
+ name: 'ServiceDeskRoot',
+ components: {
+ GlAlert,
+ ServiceDeskSetting,
+ },
+ props: {
+ initialIsEnabled: {
+ type: Boolean,
+ required: true,
+ },
+ endpoint: {
+ type: String,
+ required: true,
+ },
+ initialIncomingEmail: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ selectedTemplate: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ outgoingName: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ projectKey: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ templates: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ },
+
+ data() {
+ return {
+ isEnabled: this.initialIsEnabled,
+ incomingEmail: this.initialIncomingEmail,
+ isTemplateSaving: false,
+ isAlertShowing: false,
+ alertVariant: 'danger',
+ alertMessage: '',
+ };
+ },
+
+ created() {
+ eventHub.$on('serviceDeskEnabledCheckboxToggled', this.onEnableToggled);
+ eventHub.$on('serviceDeskTemplateSave', this.onSaveTemplate);
+
+ this.service = new ServiceDeskService(this.endpoint);
+
+ if (this.isEnabled && !this.incomingEmail) {
+ this.fetchIncomingEmail();
+ }
+ },
+
+ beforeDestroy() {
+ eventHub.$off('serviceDeskEnabledCheckboxToggled', this.onEnableToggled);
+ eventHub.$off('serviceDeskTemplateSave', this.onSaveTemplate);
+ },
+
+ methods: {
+ fetchIncomingEmail() {
+ this.service
+ .fetchIncomingEmail()
+ .then(({ data }) => {
+ const email = data.service_desk_address;
+ if (!email) {
+ throw new Error(__("Response didn't include `service_desk_address`"));
+ }
+
+ this.incomingEmail = email;
+ })
+ .catch(() =>
+ this.showAlert(__('An error occurred while fetching the Service Desk address.')),
+ );
+ },
+
+ onEnableToggled(isChecked) {
+ this.isEnabled = isChecked;
+ this.incomingEmail = '';
+
+ this.service
+ .toggleServiceDesk(isChecked)
+ .then(({ data }) => {
+ const email = data.service_desk_address;
+ if (isChecked && !email) {
+ throw new Error(__("Response didn't include `service_desk_address`"));
+ }
+
+ this.incomingEmail = email;
+ })
+ .catch(() => {
+ const message = isChecked
+ ? __('An error occurred while enabling Service Desk.')
+ : __('An error occurred while disabling Service Desk.');
+
+ this.showAlert(message);
+ });
+ },
+
+ onSaveTemplate({ selectedTemplate, outgoingName, projectKey }) {
+ this.isTemplateSaving = true;
+ this.service
+ .updateTemplate({ selectedTemplate, outgoingName, projectKey }, this.isEnabled)
+ .then(() => this.showAlert(__('Template was successfully saved.'), 'success'))
+ .catch(() =>
+ this.showAlert(
+ __('An error occurred while saving the template. Please check if the template exists.'),
+ ),
+ )
+ .finally(() => {
+ this.isTemplateSaving = false;
+ });
+ },
+
+ showAlert(message, variant = 'danger') {
+ this.isAlertShowing = true;
+ this.alertMessage = message;
+ this.alertVariant = variant;
+ },
+
+ onDismiss() {
+ this.isAlertShowing = false;
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-alert v-if="isAlertShowing" class="mb-3" :variant="alertVariant" @dismiss="onDismiss">
+ {{ alertMessage }}
+ </gl-alert>
+ <service-desk-setting
+ :is-enabled="isEnabled"
+ :incoming-email="incomingEmail"
+ :initial-selected-template="selectedTemplate"
+ :initial-outgoing-name="outgoingName"
+ :initial-project-key="projectKey"
+ :templates="templates"
+ :is-template-saving="isTemplateSaving"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue
new file mode 100644
index 00000000000..43c20fea43e
--- /dev/null
+++ b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue
@@ -0,0 +1,169 @@
+<script>
+import { GlDeprecatedButton, GlFormSelect, GlToggle, GlLoadingIcon } from '@gitlab/ui';
+import { __ } from '~/locale';
+import tooltip from '~/vue_shared/directives/tooltip';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import eventHub from '../event_hub';
+
+export default {
+ name: 'ServiceDeskSetting',
+ directives: {
+ tooltip,
+ },
+ components: {
+ ClipboardButton,
+ GlDeprecatedButton,
+ GlFormSelect,
+ GlToggle,
+ GlLoadingIcon,
+ },
+ mixins: [glFeatureFlagsMixin()],
+ props: {
+ isEnabled: {
+ type: Boolean,
+ required: true,
+ },
+ incomingEmail: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ initialSelectedTemplate: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ initialOutgoingName: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ initialProjectKey: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ templates: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ isTemplateSaving: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ selectedTemplate: this.initialSelectedTemplate,
+ outgoingName: this.initialOutgoingName || __('GitLab Support Bot'),
+ projectKey: this.initialProjectKey,
+ };
+ },
+ computed: {
+ templateOptions() {
+ return [''].concat(this.templates);
+ },
+ hasProjectKeySupport() {
+ return Boolean(this.glFeatures.serviceDeskCustomAddress);
+ },
+ },
+ methods: {
+ onCheckboxToggle(isChecked) {
+ eventHub.$emit('serviceDeskEnabledCheckboxToggled', isChecked);
+ },
+ onSaveTemplate() {
+ eventHub.$emit('serviceDeskTemplateSave', {
+ selectedTemplate: this.selectedTemplate,
+ outgoingName: this.outgoingName,
+ projectKey: this.projectKey,
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-toggle
+ id="service-desk-checkbox"
+ :value="isEnabled"
+ class="d-inline-block align-middle mr-1"
+ label="Service desk"
+ label-position="left"
+ @change="onCheckboxToggle"
+ />
+ <label class="align-middle" for="service-desk-checkbox">
+ {{ __('Activate Service Desk') }}
+ </label>
+ <div v-if="isEnabled" class="row mt-3">
+ <div class="col-md-9 mb-0">
+ <strong id="incoming-email-describer" class="d-block mb-1">
+ {{ __('Forward external support email address to') }}
+ </strong>
+ <template v-if="incomingEmail">
+ <div class="input-group">
+ <input
+ ref="service-desk-incoming-email"
+ type="text"
+ class="form-control incoming-email h-auto"
+ :placeholder="__('Incoming email')"
+ :aria-label="__('Incoming email')"
+ aria-describedby="incoming-email-describer"
+ :value="incomingEmail"
+ disabled="true"
+ />
+ <div class="input-group-append">
+ <clipboard-button
+ :title="__('Copy')"
+ :text="incomingEmail"
+ css-class="btn qa-clipboard-button"
+ />
+ </div>
+ </div>
+ </template>
+ <template v-else>
+ <gl-loading-icon :inline="true" />
+ <span class="sr-only">{{ __('Fetching incoming email') }}</span>
+ </template>
+
+ <label for="service-desk-template-select" class="mt-3">
+ {{ __('Template to append to all Service Desk issues') }}
+ </label>
+ <gl-form-select
+ id="service-desk-template-select"
+ v-model="selectedTemplate"
+ :options="templateOptions"
+ />
+ <label for="service-desk-email-from-name" class="mt-3">
+ {{ __('Email display name') }}
+ </label>
+ <input id="service-desk-email-from-name" v-model.trim="outgoingName" class="form-control" />
+ <span class="form-text text-muted">
+ {{ __('Emails sent from Service Desk will have this name') }}
+ </span>
+ <template v-if="hasProjectKeySupport">
+ <label for="service-desk-project-suffix" class="mt-3">
+ {{ __('Project name suffix') }}
+ </label>
+ <input id="service-desk-project-suffix" v-model.trim="projectKey" class="form-control" />
+ <span class="form-text text-muted mb-3">
+ {{
+ __(
+ 'Project name suffix is a user-defined string which will be appended to the project path, and will form the Service Desk email address.',
+ )
+ }}
+ </span>
+ </template>
+ <gl-deprecated-button
+ variant="success"
+ :disabled="isTemplateSaving"
+ @click="onSaveTemplate"
+ >{{ __('Save template') }}</gl-deprecated-button
+ >
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/projects/settings_service_desk/event_hub.js b/app/assets/javascripts/projects/settings_service_desk/event_hub.js
new file mode 100644
index 00000000000..e31806ad199
--- /dev/null
+++ b/app/assets/javascripts/projects/settings_service_desk/event_hub.js
@@ -0,0 +1,3 @@
+import createEventHub from '~/helpers/event_hub_factory';
+
+export default createEventHub();
diff --git a/app/assets/javascripts/projects/settings_service_desk/index.js b/app/assets/javascripts/projects/settings_service_desk/index.js
new file mode 100644
index 00000000000..15c077de72e
--- /dev/null
+++ b/app/assets/javascripts/projects/settings_service_desk/index.js
@@ -0,0 +1,41 @@
+import Vue from 'vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import ServiceDeskRoot from './components/service_desk_root.vue';
+
+export default () => {
+ const serviceDeskRootElement = document.querySelector('.js-service-desk-setting-root');
+ if (serviceDeskRootElement) {
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: serviceDeskRootElement,
+ components: {
+ ServiceDeskRoot,
+ },
+ data() {
+ const { dataset } = serviceDeskRootElement;
+ return {
+ initialIsEnabled: parseBoolean(dataset.enabled),
+ endpoint: dataset.endpoint,
+ incomingEmail: dataset.incomingEmail,
+ selectedTemplate: dataset.selectedTemplate,
+ outgoingName: dataset.outgoingName,
+ projectKey: dataset.projectKey,
+ templates: JSON.parse(dataset.templates),
+ };
+ },
+ render(createElement) {
+ return createElement('service-desk-root', {
+ props: {
+ initialIsEnabled: this.initialIsEnabled,
+ endpoint: this.endpoint,
+ initialIncomingEmail: this.incomingEmail,
+ selectedTemplate: this.selectedTemplate,
+ outgoingName: this.outgoingName,
+ projectKey: this.projectKey,
+ templates: this.templates,
+ },
+ });
+ },
+ });
+ }
+};
diff --git a/app/assets/javascripts/projects/settings_service_desk/services/service_desk_service.js b/app/assets/javascripts/projects/settings_service_desk/services/service_desk_service.js
new file mode 100644
index 00000000000..d707763c64e
--- /dev/null
+++ b/app/assets/javascripts/projects/settings_service_desk/services/service_desk_service.js
@@ -0,0 +1,27 @@
+import axios from '~/lib/utils/axios_utils';
+
+class ServiceDeskService {
+ constructor(endpoint) {
+ this.endpoint = endpoint;
+ }
+
+ fetchIncomingEmail() {
+ return axios.get(this.endpoint);
+ }
+
+ toggleServiceDesk(enable) {
+ return axios.put(this.endpoint, { service_desk_enabled: enable });
+ }
+
+ updateTemplate({ selectedTemplate, outgoingName, projectKey = '' }, isEnabled) {
+ const body = {
+ issue_template_key: selectedTemplate,
+ outgoing_name: outgoingName,
+ project_key: projectKey,
+ service_desk_enabled: isEnabled,
+ };
+ return axios.put(this.endpoint, body);
+ }
+}
+
+export default ServiceDeskService;
diff --git a/app/assets/javascripts/repository/components/web_ide_link.vue b/app/assets/javascripts/repository/components/web_ide_link.vue
new file mode 100644
index 00000000000..6549d5a3878
--- /dev/null
+++ b/app/assets/javascripts/repository/components/web_ide_link.vue
@@ -0,0 +1,47 @@
+<script>
+import TreeActionLink from './tree_action_link.vue';
+import { __ } from '~/locale';
+import { webIDEUrl } from '~/lib/utils/url_utility';
+
+export default {
+ components: {
+ TreeActionLink,
+ },
+ props: {
+ projectPath: {
+ type: String,
+ required: true,
+ },
+ refSha: {
+ type: String,
+ required: true,
+ },
+ canPushCode: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ forkPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ computed: {
+ showLinkToFork() {
+ return !this.canPushCode && this.forkPath;
+ },
+ text() {
+ return this.showLinkToFork ? __('Edit fork in Web IDE') : __('Web IDE');
+ },
+ path() {
+ const path = this.showLinkToFork ? this.forkPath : this.projectPath;
+ return webIDEUrl(`/${path}/edit/${this.refSha}/-/${this.$route.params.path || ''}`);
+ },
+ },
+};
+</script>
+
+<template>
+ <tree-action-link :path="path" :text="text" data-qa-selector="web_ide_button" />
+</template>
diff --git a/app/assets/javascripts/repository/index.js b/app/assets/javascripts/repository/index.js
index 01db4e363ba..4f80ab4ff5d 100644
--- a/app/assets/javascripts/repository/index.js
+++ b/app/assets/javascripts/repository/index.js
@@ -4,18 +4,26 @@ import App from './components/app.vue';
import Breadcrumbs from './components/breadcrumbs.vue';
import LastCommit from './components/last_commit.vue';
import TreeActionLink from './components/tree_action_link.vue';
+import WebIdeLink from './components/web_ide_link.vue';
import DirectoryDownloadLinks from './components/directory_download_links.vue';
import apolloProvider from './graphql';
import { setTitle } from './utils/title';
import { updateFormAction } from './utils/dom';
import { parseBoolean } from '../lib/utils/common_utils';
-import { webIDEUrl } from '../lib/utils/url_utility';
import { __ } from '../locale';
export default function setupVueRepositoryList() {
const el = document.getElementById('js-tree-list');
const { dataset } = el;
- const { projectPath, projectShortPath, ref, escapedRef, fullName } = dataset;
+ const {
+ canPushCode,
+ projectPath,
+ projectShortPath,
+ forkPath,
+ ref,
+ escapedRef,
+ fullName,
+ } = dataset;
const router = createRouter(projectPath, escapedRef);
apolloProvider.clients.defaultClient.cache.writeData({
@@ -117,11 +125,12 @@ export default function setupVueRepositoryList() {
el: webIdeLinkEl,
router,
render(h) {
- return h(TreeActionLink, {
+ return h(WebIdeLink, {
props: {
- path: webIDEUrl(`/${projectPath}/edit/${ref}/-/${this.$route.params.path || ''}`),
- text: __('Web IDE'),
- cssClass: 'qa-web-ide-button',
+ projectPath,
+ refSha: ref,
+ forkPath,
+ canPushCode: parseBoolean(canPushCode),
},
});
},
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js
index 34cb74efabe..70d29b5b3df 100644
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js
@@ -1,6 +1,7 @@
import renderBlockHtml from './renderers/render_html_block';
import renderKramdownList from './renderers/render_kramdown_list';
import renderKramdownText from './renderers/render_kramdown_text';
+import renderIdentifierInstanceText from './renderers/render_identifier_instance_text';
import renderIdentifierParagraph from './renderers/render_identifier_paragraph';
import renderEmbeddedRubyText from './renderers/render_embedded_ruby_text';
import renderFontAwesomeHtmlInline from './renderers/render_font_awesome_html_inline';
@@ -9,7 +10,7 @@ const htmlInlineRenderers = [renderFontAwesomeHtmlInline];
const htmlBlockRenderers = [renderBlockHtml];
const listRenderers = [renderKramdownList];
const paragraphRenderers = [renderIdentifierParagraph];
-const textRenderers = [renderKramdownText, renderEmbeddedRubyText];
+const textRenderers = [renderKramdownText, renderEmbeddedRubyText, renderIdentifierInstanceText];
const executeRenderer = (renderers, node, context) => {
const availableRenderer = renderers.find(renderer => renderer.canRender(node, context));
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token.js
index 6937d2acb47..5b89390932c 100644
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token.js
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token.js
@@ -32,6 +32,8 @@ export const buildUneditableCloseTokens = (token, tagType = TAG_TYPES.block) =>
// Complete helpers (open plus close)
+export const buildTextToken = content => buildToken('text', null, { content });
+
export const buildUneditableTokens = token => {
return [...buildUneditableOpenTokens(token), buildUneditableCloseToken()];
};
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_identifier_instance_text.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_identifier_instance_text.js
new file mode 100644
index 00000000000..a9c3dfcd728
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_identifier_instance_text.js
@@ -0,0 +1,40 @@
+import { buildTextToken, buildUneditableInlineTokens } from './build_uneditable_token';
+
+/*
+Use case examples:
+- Majority: two bracket pairs, back-to-back, each with content (including spaces)
+ - `[environment terraform plans][terraform]`
+ - `[an issue labelled `~"master:broken"`][broken-master-issues]`
+- Minority: two bracket pairs the latter being empty or only one pair with content (including spaces)
+ - `[this link][]`
+ - `[this link]`
+
+Regexp notes:
+ - `(?:\[.+?\]){1}`: Always one bracket pair with content (including spaces)
+ - `(?:\[\]|\[.+?\])?`: Optional second pair that may or may not contain content (including spaces)
+ - `(?!:)`: Never followed by a `:` which is reserved for identifier definition syntax (`[identifier]: /the-link`)
+ - Each of the three parts is non-captured, but the match as a whole is captured
+*/
+const identifierInstanceRegex = /((?:\[.+?\]){1}(?:\[\]|\[.+?\])?(?!:))/g;
+
+const isIdentifierInstance = literal => {
+ // Reset lastIndex as global flag in regexp are stateful (https://stackoverflow.com/a/11477448)
+ identifierInstanceRegex.lastIndex = 0;
+ return identifierInstanceRegex.test(literal);
+};
+
+const canRender = ({ literal }) => isIdentifierInstance(literal);
+
+const tokenize = text => {
+ const matches = text.split(identifierInstanceRegex);
+ const tokens = matches.map(match => {
+ const token = buildTextToken(match);
+ return isIdentifierInstance(match) ? buildUneditableInlineTokens(token) : token;
+ });
+
+ return tokens.flat();
+};
+
+const render = (_, { origin }) => tokenize(origin().content);
+
+export default { canRender, render };
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/constants.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/constants.js
index e94e7d46f85..746e38e98e8 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/constants.js
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/constants.js
@@ -1,6 +1,7 @@
export const DropdownVariant = {
Sidebar: 'sidebar',
Standalone: 'standalone',
+ Embedded: 'embedded',
};
export const LIST_BUFFER_SIZE = 5;
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue
index f45c14f8344..cf77aa37d14 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue
@@ -8,12 +8,16 @@ export default {
GlIcon,
},
computed: {
- ...mapGetters(['dropdownButtonText', 'isDropdownVariantStandalone']),
+ ...mapGetters([
+ 'dropdownButtonText',
+ 'isDropdownVariantStandalone',
+ 'isDropdownVariantEmbedded',
+ ]),
},
methods: {
...mapActions(['toggleDropdownContents']),
handleButtonClick(e) {
- if (this.isDropdownVariantStandalone) {
+ if (this.isDropdownVariantStandalone || this.isDropdownVariantEmbedded) {
this.toggleDropdownContents();
e.stopPropagation();
}
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue
index ba8d8391952..94671f8a109 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue
@@ -88,12 +88,16 @@ export default {
@click.prevent="handleColorClick(color)"
/>
</div>
- <div class="color-input-container d-flex">
+ <div class="color-input-container gl-display-flex">
<span
class="dropdown-label-color-preview position-relative position-relative d-inline-block"
:style="{ backgroundColor: selectedColor }"
></span>
- <gl-form-input v-model.trim="selectedColor" :placeholder="__('Use custom color #FF0000')" />
+ <gl-form-input
+ v-model.trim="selectedColor"
+ class="gl-rounded-top-left-none gl-rounded-bottom-left-none"
+ :placeholder="__('Use custom color #FF0000')"
+ />
</div>
</div>
<div class="dropdown-actions clearfix pt-2 px-2">
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue
index af16088b6b9..ef506d00d9a 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue
@@ -36,7 +36,7 @@ export default {
'footerCreateLabelTitle',
'footerManageLabelTitle',
]),
- ...mapGetters(['selectedLabelsList', 'isDropdownVariantSidebar']),
+ ...mapGetters(['selectedLabelsList', 'isDropdownVariantSidebar', 'isDropdownVariantEmbedded']),
visibleLabels() {
if (this.searchKey) {
return this.labels.filter(label =>
@@ -126,16 +126,19 @@ export default {
<div class="labels-select-contents-list js-labels-list" @keydown="handleKeyDown">
<gl-loading-icon
v-if="labelsFetchInProgress"
- class="labels-fetch-loading position-absolute d-flex align-items-center w-100 h-100"
+ class="labels-fetch-loading position-absolute gl-display-flex gl-align-items-center w-100 h-100"
size="md"
/>
- <div v-if="isDropdownVariantSidebar" class="dropdown-title d-flex align-items-center pt-0 pb-2">
+ <div
+ v-if="isDropdownVariantSidebar || isDropdownVariantEmbedded"
+ class="dropdown-title gl-display-flex gl-align-items-center gl-pt-0 gl-pb-3!"
+ >
<span class="flex-grow-1">{{ labelsListTitle }}</span>
<gl-button
:aria-label="__('Close')"
variant="link"
size="small"
- class="dropdown-header-button p-0"
+ class="dropdown-header-button gl-p-0!"
icon="close"
@click="toggleDropdownContents"
/>
@@ -165,17 +168,21 @@ export default {
</li>
</smart-virtual-list>
</div>
- <div v-if="isDropdownVariantSidebar" class="dropdown-footer">
+ <div v-if="isDropdownVariantSidebar || isDropdownVariantEmbedded" class="dropdown-footer">
<ul class="list-unstyled">
<li v-if="allowLabelCreate">
<gl-link
- class="d-flex w-100 flex-row text-break-word label-item"
+ class="gl-display-flex w-100 flex-row text-break-word label-item"
@click="toggleDropdownContentsCreateView"
- >{{ footerCreateLabelTitle }}</gl-link
>
+ {{ footerCreateLabelTitle }}
+ </gl-link>
</li>
<li>
- <gl-link :href="labelsManagePath" class="d-flex flex-row text-break-word label-item">
+ <gl-link
+ :href="labelsManagePath"
+ class="gl-display-flex flex-row text-break-word label-item"
+ >
{{ footerManageLabelTitle }}
</gl-link>
</li>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue
index f38b66fdfdf..258a87e62b9 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue
@@ -74,6 +74,11 @@ export default {
required: false,
default: '',
},
+ dropdownButtonText: {
+ type: String,
+ required: false,
+ default: __('Label'),
+ },
labelsListTitle: {
type: String,
required: false,
@@ -97,7 +102,11 @@ export default {
},
computed: {
...mapState(['showDropdownButton', 'showDropdownContents']),
- ...mapGetters(['isDropdownVariantSidebar', 'isDropdownVariantStandalone']),
+ ...mapGetters([
+ 'isDropdownVariantSidebar',
+ 'isDropdownVariantStandalone',
+ 'isDropdownVariantEmbedded',
+ ]),
dropdownButtonVisible() {
return this.isDropdownVariantSidebar ? this.showDropdownButton : true;
},
@@ -116,6 +125,7 @@ export default {
allowLabelCreate: this.allowLabelCreate,
allowMultiselect: this.allowMultiselect,
allowScopedLabels: this.allowScopedLabels,
+ dropdownButtonText: this.dropdownButtonText,
selectedLabels: this.selectedLabels,
labelsFetchPath: this.labelsFetchPath,
labelsManagePath: this.labelsManagePath,
@@ -200,7 +210,10 @@ export default {
<template>
<div
class="labels-select-wrapper position-relative"
- :class="{ 'is-standalone': isDropdownVariantStandalone }"
+ :class="{
+ 'is-standalone': isDropdownVariantStandalone,
+ 'is-embedded': isDropdownVariantEmbedded,
+ }"
>
<template v-if="isDropdownVariantSidebar">
<dropdown-value-collapsed
@@ -221,7 +234,7 @@ export default {
ref="dropdownContents"
/>
</template>
- <template v-if="isDropdownVariantStandalone">
+ <template v-if="isDropdownVariantStandalone || isDropdownVariantEmbedded">
<dropdown-button v-show="dropdownButtonVisible" />
<dropdown-contents
v-if="dropdownButtonVisible && showDropdownContents"
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/getters.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/getters.js
index c39222959a9..e035a866048 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/getters.js
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/getters.js
@@ -13,7 +13,7 @@ export const dropdownButtonText = (state, getters) => {
: state.selectedLabels;
if (!selectedLabels.length) {
- return __('Label');
+ return state.dropdownButtonText || __('Label');
} else if (selectedLabels.length > 1) {
return sprintf(s__('LabelSelect|%{firstLabelName} +%{remainingLabelCount} more'), {
firstLabelName: selectedLabels[0].title,
@@ -44,5 +44,12 @@ export const isDropdownVariantSidebar = state => state.variant === DropdownVaria
*/
export const isDropdownVariantStandalone = state => state.variant === DropdownVariant.Standalone;
+/**
+ * Returns boolean representing whether dropdown variant
+ * is `embedded`
+ * @param {object} state
+ */
+export const isDropdownVariantEmbedded = state => state.variant === DropdownVariant.Embedded;
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/state.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/state.js
index 6a6c0b4c0ee..3f3358d4805 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/state.js
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/state.js
@@ -6,6 +6,7 @@ export default () => ({
labelsCreateTitle: '',
footerCreateLabelTitle: '',
footerManageLabelTitle: '',
+ dropdownButtonText: '',
// Paths
namespace: '',
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 9da0b0da598..32c276ea6d2 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -1089,6 +1089,10 @@ header.header-content .dropdown-menu.frequent-items-dropdown-menu {
.dropdown-label-color-preview {
border: 1px solid $gray-100;
border-right: 0;
+
+ &[style] {
+ border-color: transparent;
+ }
}
}
}
diff --git a/app/assets/stylesheets/pages/service_desk.scss b/app/assets/stylesheets/pages/service_desk.scss
new file mode 100644
index 00000000000..34ab5eb1b74
--- /dev/null
+++ b/app/assets/stylesheets/pages/service_desk.scss
@@ -0,0 +1,7 @@
+.service-desk-issues {
+ .non-empty-state {
+ text-align: left;
+ padding-bottom: $gl-padding-top;
+ border-bottom: 1px solid $border-color;
+ }
+}