Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-11-09 15:09:24 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-11-09 15:09:24 +0300
commit0ab6d56c15ebf4a12981556c7d3bc53d9b62cdb9 (patch)
tree31f28e85bb24de18240f3ddeaf5c3367e4510c3a /app
parent079ad2772f2b78f56b26730307cc73d1376fa6d6 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue18
-rw-r--r--app/assets/javascripts/alerts_settings/components/alerts_settings_form_new.vue26
-rw-r--r--app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue31
-rw-r--r--app/assets/javascripts/alerts_settings/constants.js9
-rw-r--r--app/assets/javascripts/alerts_settings/index.js18
-rw-r--r--app/assets/javascripts/alerts_settings/utils/error_messages.js8
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_mermaid.js2
-rw-r--r--app/assets/javascripts/boards/components/board_assignee_dropdown.vue8
-rw-r--r--app/assets/javascripts/boards/components/board_column.vue4
-rw-r--r--app/assets/javascripts/boards/components/board_settings_sidebar.vue10
-rw-r--r--app/assets/javascripts/boards/components/new_list_dropdown.js3
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_sidebar_due_date.vue2
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue2
-rw-r--r--app/assets/javascripts/boards/queries/board_list_destroy.mutation.graphql5
-rw-r--r--app/assets/javascripts/boards/stores/actions.js27
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js7
-rw-r--r--app/assets/javascripts/boards/stores/getters.js6
-rw-r--r--app/assets/javascripts/boards/stores/mutation_types.js5
-rw-r--r--app/assets/javascripts/boards/stores/mutations.js13
-rw-r--r--app/assets/javascripts/graphql_shared/utils.js2
-rw-r--r--app/assets/javascripts/ide/components/ide.vue70
-rw-r--r--app/assets/javascripts/ide/components/ide_side_bar.vue10
-rw-r--r--app/assets/javascripts/ide/components/ide_tree.vue2
-rw-r--r--app/assets/javascripts/ide/components/ide_tree_list.vue7
-rw-r--r--app/assets/javascripts/issue_show/components/app.vue16
-rw-r--r--app/assets/javascripts/issue_show/issue.js2
-rw-r--r--app/assets/javascripts/notes/components/discussion_actions.vue2
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue8
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_item.vue6
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue2
-rw-r--r--app/assets/javascripts/search/dropdown_filter/components/dropdown_filter.vue108
-rw-r--r--app/assets/javascripts/search/dropdown_filter/constants/confidential_filter_data.js36
-rw-r--r--app/assets/javascripts/search/dropdown_filter/constants/state_filter_data.js42
-rw-r--r--app/assets/javascripts/search/dropdown_filter/index.js38
-rw-r--r--app/assets/javascripts/search/index.js8
-rw-r--r--app/assets/javascripts/search/sidebar/components/app.vue41
-rw-r--r--app/assets/javascripts/search/sidebar/components/confidentiality_filter.vue1
-rw-r--r--app/assets/javascripts/search/sidebar/components/status_filter.vue1
-rw-r--r--app/assets/javascripts/search/sidebar/index.js23
-rw-r--r--app/assets/javascripts/search/store/actions.js9
-rw-r--r--app/assets/javascripts/static_site_editor/components/edit_area.vue10
-rw-r--r--app/assets/javascripts/static_site_editor/graphql/queries/app_data.query.graphql2
-rw-r--r--app/assets/javascripts/static_site_editor/graphql/typedefs.graphql2
-rw-r--r--app/assets/javascripts/static_site_editor/index.js3
-rw-r--r--app/assets/javascripts/static_site_editor/pages/home.vue2
-rw-r--r--app/assets/javascripts/static_site_editor/services/renderers/render_image.js73
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer.js4
-rw-r--r--app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss4
-rw-r--r--app/assets/stylesheets/page_bundles/ide.scss8
-rw-r--r--app/assets/stylesheets/page_bundles/ide_themes/_dark.scss1
-rw-r--r--app/assets/stylesheets/page_bundles/ide_themes/_monokai.scss66
-rw-r--r--app/assets/stylesheets/page_bundles/ide_themes/_solarized-dark.scss1
-rw-r--r--app/assets/stylesheets/page_bundles/ide_themes/_solarized-light.scss57
-rw-r--r--app/assets/stylesheets/pages/issuable.scss6
-rw-r--r--app/controllers/profiles/personal_access_tokens_controller.rb8
-rw-r--r--app/controllers/search_controller.rb4
-rw-r--r--app/helpers/operations_helper.rb3
-rw-r--r--app/policies/user_policy.rb1
-rw-r--r--app/serializers/note_entity.rb4
-rw-r--r--app/services/personal_access_tokens/create_service.rb27
-rw-r--r--app/services/personal_access_tokens/revoke_service.rb11
-rw-r--r--app/services/resource_access_tokens/create_service.rb4
-rw-r--r--app/views/search/_results.html.haml39
-rw-r--r--app/views/search/results/_filters.html.haml6
-rw-r--r--app/workers/remove_expired_members_worker.rb10
66 files changed, 567 insertions, 429 deletions
diff --git a/app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue b/app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue
index f24c52f61da..9420480e35a 100644
--- a/app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue
+++ b/app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue
@@ -58,6 +58,11 @@ export default {
required: false,
default: false,
},
+ currentIntegration: {
+ type: Object,
+ required: false,
+ default: null,
+ },
},
fields: [
{
@@ -82,17 +87,16 @@ export default {
integrationToDelete: integrationToDeleteDefault,
};
},
- computed: {
- tbodyTrClass() {
- return {
- [bodyTrClass]: this.integrations.length,
- };
- },
- },
mounted() {
this.trackPageViews();
},
methods: {
+ tbodyTrClass(item) {
+ return {
+ [bodyTrClass]: this.integrations.length,
+ 'gl-bg-blue-50': item?.id === this.currentIntegration?.id,
+ };
+ },
trackPageViews() {
const { category, action } = trackAlertIntegrationsViewsOptions;
Tracking.event(category, action);
diff --git a/app/assets/javascripts/alerts_settings/components/alerts_settings_form_new.vue b/app/assets/javascripts/alerts_settings/components/alerts_settings_form_new.vue
index 946da8ef34c..a08100f3938 100644
--- a/app/assets/javascripts/alerts_settings/components/alerts_settings_form_new.vue
+++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_form_new.vue
@@ -33,6 +33,9 @@ export default {
step1: {
label: s__('AlertSettings|1. Select integration type'),
help: s__('AlertSettings|Learn more about our upcoming %{linkStart}integrations%{linkEnd}'),
+ enterprise: s__(
+ 'AlertSettings|In free versions of GitLab, only one integration for each type can be added. %{linkStart}Upgrade your subscription%{linkEnd} to add additional integrations.',
+ ),
},
step2: {
label: s__('AlertSettings|2. Name integration'),
@@ -107,6 +110,10 @@ export default {
required: false,
default: null,
},
+ canAddIntegration: {
+ type: Boolean,
+ required: true,
+ },
},
data() {
return {
@@ -236,15 +243,24 @@ export default {
>
<gl-form-select
v-model="selectedIntegration"
- :disabled="currentIntegration !== null"
+ :disabled="currentIntegration !== null || !canAddIntegration"
:options="options"
@change="integrationTypeSelect"
/>
- <alert-settings-form-help-block
- :message="$options.i18n.integrationFormSteps.step1.help"
- link="https://gitlab.com/groups/gitlab-org/-/epics/4390"
- />
+ <div class="gl-my-4">
+ <alert-settings-form-help-block
+ :message="$options.i18n.integrationFormSteps.step1.help"
+ link="https://gitlab.com/groups/gitlab-org/-/epics/4390"
+ />
+ </div>
+
+ <div v-if="!canAddIntegration" class="gl-my-4" data-testid="multi-integrations-not-supported">
+ <alert-settings-form-help-block
+ :message="$options.i18n.integrationFormSteps.step1.enterprise"
+ link="https://about.gitlab.com/pricing"
+ />
+ </div>
</gl-form-group>
<gl-collapse v-model="formVisible" class="gl-mt-3">
<gl-form-group
diff --git a/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue b/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue
index e9e7b1407bc..57fc1984990 100644
--- a/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue
+++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue
@@ -19,6 +19,12 @@ import {
updateStoreAfterIntegrationDelete,
updateStoreAfterIntegrationAdd,
} from '../utils/cache_updates';
+import {
+ DELETE_INTEGRATION_ERROR,
+ ADD_INTEGRATION_ERROR,
+ RESET_INTEGRATION_TOKEN_ERROR,
+ UPDATE_INTEGRATION_ERROR,
+} from '../utils/error_messages';
export default {
typeSet,
@@ -44,6 +50,9 @@ export default {
projectPath: {
default: '',
},
+ multiIntegrations: {
+ default: false,
+ },
},
apollo: {
integrations: {
@@ -91,6 +100,9 @@ export default {
},
];
},
+ canAddIntegration() {
+ return this.multiIntegrations || this.integrations?.list?.length < 2;
+ },
},
methods: {
createNewIntegration({ type, variables }) {
@@ -121,8 +133,8 @@ export default {
type: FLASH_TYPES.SUCCESS,
});
})
- .catch(err => {
- createFlash({ message: err });
+ .catch(() => {
+ createFlash({ message: ADD_INTEGRATION_ERROR });
})
.finally(() => {
this.isUpdating = false;
@@ -151,8 +163,8 @@ export default {
type: FLASH_TYPES.SUCCESS,
});
})
- .catch(err => {
- createFlash({ message: err });
+ .catch(() => {
+ createFlash({ message: UPDATE_INTEGRATION_ERROR });
})
.finally(() => {
this.isUpdating = false;
@@ -187,8 +199,8 @@ export default {
});
},
)
- .catch(err => {
- createFlash({ message: err });
+ .catch(() => {
+ createFlash({ message: RESET_INTEGRATION_TOKEN_ERROR });
})
.finally(() => {
this.isUpdating = false;
@@ -222,9 +234,8 @@ export default {
type: FLASH_TYPES.SUCCESS,
});
})
- .catch(err => {
- this.errored = true;
- createFlash({ message: err });
+ .catch(() => {
+ createFlash({ message: DELETE_INTEGRATION_ERROR });
})
.finally(() => {
this.isUpdating = false;
@@ -242,6 +253,7 @@ export default {
<integrations-list
:integrations="glFeatures.httpIntegrationsList ? integrations.list : intergrationsOptionsOld"
:loading="loading"
+ :current-integration="currentIntegration"
@edit-integration="editIntegration"
@delete-integration="deleteIntegration"
/>
@@ -249,6 +261,7 @@ export default {
v-if="glFeatures.httpIntegrationsList"
:loading="isUpdating"
:current-integration="currentIntegration"
+ :can-add-integration="canAddIntegration"
@create-new-integration="createNewIntegration"
@update-integration="updateIntegration"
@reset-token="resetToken"
diff --git a/app/assets/javascripts/alerts_settings/constants.js b/app/assets/javascripts/alerts_settings/constants.js
index 9cf2f356e0a..19eaccf05fc 100644
--- a/app/assets/javascripts/alerts_settings/constants.js
+++ b/app/assets/javascripts/alerts_settings/constants.js
@@ -57,15 +57,6 @@ export const typeSet = {
prometheus: 'PROMETHEUS',
};
-export const defaultFormState = {
- name: '',
- active: false,
- token: '',
- url: '',
- apiUrl: '',
- integrationTestPayload: { json: null, error: null },
-};
-
export const integrationToDeleteDefault = { id: null, name: '' };
export const JSON_VALIDATE_DELAY = 250;
diff --git a/app/assets/javascripts/alerts_settings/index.js b/app/assets/javascripts/alerts_settings/index.js
index 2ae0dd447a1..a9d109b3f3e 100644
--- a/app/assets/javascripts/alerts_settings/index.js
+++ b/app/assets/javascripts/alerts_settings/index.js
@@ -29,19 +29,16 @@ export default el => {
opsgenieMvcEnabled,
opsgenieMvcTargetUrl,
projectPath,
+ multiIntegrations,
} = el.dataset;
- const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(
- {},
- {
- cacheConfig: {},
- },
- ),
- });
+ const resolvers = {};
- apolloProvider.clients.defaultClient.cache.writeData({
- data: {},
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(resolvers, {
+ cacheConfig: {},
+ assumeImmutableResults: true,
+ }),
});
return new Vue({
@@ -70,6 +67,7 @@ export default el => {
opsgenieMvcIsAvailable: parseBoolean(opsgenieMvcAvailable),
},
projectPath,
+ multiIntegrations: parseBoolean(multiIntegrations),
},
apolloProvider,
components: {
diff --git a/app/assets/javascripts/alerts_settings/utils/error_messages.js b/app/assets/javascripts/alerts_settings/utils/error_messages.js
index 2e6058fc81a..7df5d444a53 100644
--- a/app/assets/javascripts/alerts_settings/utils/error_messages.js
+++ b/app/assets/javascripts/alerts_settings/utils/error_messages.js
@@ -7,3 +7,11 @@ export const DELETE_INTEGRATION_ERROR = s__(
export const ADD_INTEGRATION_ERROR = s__(
'AlertsIntegrations|The integration could not be added. Please try again.',
);
+
+export const UPDATE_INTEGRATION_ERROR = s__(
+ 'AlertsIntegrations|The current integration could not be updated. Please try again.',
+);
+
+export const RESET_INTEGRATION_TOKEN_ERROR = s__(
+ 'AlertsIntegrations|The integration token could not be reset. Please try again.',
+);
diff --git a/app/assets/javascripts/behaviors/markdown/render_mermaid.js b/app/assets/javascripts/behaviors/markdown/render_mermaid.js
index cb0e6345059..233c5f84340 100644
--- a/app/assets/javascripts/behaviors/markdown/render_mermaid.js
+++ b/app/assets/javascripts/behaviors/markdown/render_mermaid.js
@@ -25,7 +25,7 @@ function importMermaidModule() {
return import(/* webpackChunkName: 'mermaid' */ 'mermaid')
.then(mermaid => {
let theme = 'neutral';
- const ideDarkThemes = ['dark', 'solarized-dark'];
+ const ideDarkThemes = ['dark', 'solarized-dark', 'monokai'];
if (
ideDarkThemes.includes(window.gon?.user_color_scheme) &&
diff --git a/app/assets/javascripts/boards/components/board_assignee_dropdown.vue b/app/assets/javascripts/boards/components/board_assignee_dropdown.vue
index a04b1361d4e..b2c3b6aa8ab 100644
--- a/app/assets/javascripts/boards/components/board_assignee_dropdown.vue
+++ b/app/assets/javascripts/boards/components/board_assignee_dropdown.vue
@@ -26,7 +26,7 @@ export default {
data() {
return {
participants: [],
- selected: this.$store.getters.getActiveIssue.assignees,
+ selected: this.$store.getters.activeIssue.assignees,
};
},
apollo: {
@@ -34,7 +34,7 @@ export default {
query: getIssueParticipants,
variables() {
return {
- id: `gid://gitlab/Issue/${this.getActiveIssue.iid}`,
+ id: `gid://gitlab/Issue/${this.activeIssue.iid}`,
};
},
update(data) {
@@ -43,7 +43,7 @@ export default {
},
},
computed: {
- ...mapGetters(['getActiveIssue']),
+ ...mapGetters(['activeIssue']),
assigneeText() {
return n__('Assignee', '%d Assignees', this.selected.length);
},
@@ -88,7 +88,7 @@ export default {
<template>
<board-editable-item :title="assigneeText" @close="saveAssignees">
<template #collapsed>
- <issuable-assignees :users="getActiveIssue.assignees" />
+ <issuable-assignees :users="activeIssue.assignees" />
</template>
<template #default>
diff --git a/app/assets/javascripts/boards/components/board_column.vue b/app/assets/javascripts/boards/components/board_column.vue
index 7a468abddf1..c8b713b0c5a 100644
--- a/app/assets/javascripts/boards/components/board_column.vue
+++ b/app/assets/javascripts/boards/components/board_column.vue
@@ -46,7 +46,7 @@ export default {
};
},
computed: {
- ...mapGetters(['getIssues']),
+ ...mapGetters(['getIssuesByList']),
showBoardListAndBoardInfo() {
return this.list.type !== ListType.promotion;
},
@@ -58,7 +58,7 @@ export default {
if (!this.glFeatures.graphqlBoardLists) {
return this.list.issues;
}
- return this.getIssues(this.list.id);
+ return this.getIssuesByList(this.list.id);
},
shouldFetchIssues() {
return this.glFeatures.graphqlBoardLists && this.list.type !== ListType.blank;
diff --git a/app/assets/javascripts/boards/components/board_settings_sidebar.vue b/app/assets/javascripts/boards/components/board_settings_sidebar.vue
index 06319df6ea5..80070b25bd0 100644
--- a/app/assets/javascripts/boards/components/board_settings_sidebar.vue
+++ b/app/assets/javascripts/boards/components/board_settings_sidebar.vue
@@ -69,14 +69,18 @@ export default {
eventHub.$off('sidebar.closeAll', this.unsetActiveId);
},
methods: {
- ...mapActions(['unsetActiveId']),
+ ...mapActions(['unsetActiveId', 'removeList']),
showScopedLabels(label) {
return boardsStore.scopedLabels.enabled && isScopedLabel(label);
},
deleteBoard() {
// eslint-disable-next-line no-alert
- if (window.confirm(__('Are you sure you want to delete this list?'))) {
- this.activeList.destroy();
+ if (window.confirm(__('Are you sure you want to remove this list?'))) {
+ if (this.shouldUseGraphQL) {
+ this.removeList(this.activeId);
+ } else {
+ this.activeList.destroy();
+ }
this.unsetActiveId();
}
},
diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js b/app/assets/javascripts/boards/components/new_list_dropdown.js
index c8926c5ef2a..47eee5306da 100644
--- a/app/assets/javascripts/boards/components/new_list_dropdown.js
+++ b/app/assets/javascripts/boards/components/new_list_dropdown.js
@@ -7,6 +7,7 @@ import { deprecatedCreateFlash as flash } from '~/flash';
import CreateLabelDropdown from '../../create_label';
import boardsStore from '../stores/boards_store';
import { fullLabelId } from '../boards_util';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import store from '~/boards/stores';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
@@ -61,7 +62,7 @@ export default function initNewListDropdown() {
const active = boardsStore.findListByLabelId(label.id);
const $li = $('<li />');
const $a = $('<a />', {
- class: active ? `is-active js-board-list-${active.id}` : '',
+ class: active ? `is-active js-board-list-${getIdFromGraphQLId(active.id)}` : '',
text: label.title,
href: '#',
});
diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_due_date.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_due_date.vue
index 97fe7572493..19e6f8a2269 100644
--- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_due_date.vue
+++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_due_date.vue
@@ -18,7 +18,7 @@ export default {
};
},
computed: {
- ...mapGetters({ issue: 'getActiveIssue' }),
+ ...mapGetters({ issue: 'activeIssue' }),
hasDueDate() {
return this.issue.dueDate != null;
},
diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue
index 0f063c7582e..31094939733 100644
--- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue
+++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue
@@ -21,7 +21,7 @@ export default {
},
inject: ['labelsFetchPath', 'labelsManagePath', 'labelsFilterBasePath'],
computed: {
- ...mapGetters({ issue: 'getActiveIssue' }),
+ ...mapGetters({ issue: 'activeIssue' }),
selectedLabels() {
const { labels = [] } = this.issue;
diff --git a/app/assets/javascripts/boards/queries/board_list_destroy.mutation.graphql b/app/assets/javascripts/boards/queries/board_list_destroy.mutation.graphql
new file mode 100644
index 00000000000..ef3fd36e980
--- /dev/null
+++ b/app/assets/javascripts/boards/queries/board_list_destroy.mutation.graphql
@@ -0,0 +1,5 @@
+mutation DestroyBoardList($listId: ID!) {
+ destroyBoardList(input: { listId: $listId }) {
+ errors
+ }
+}
diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js
index df5b84a974a..bbc7559cd86 100644
--- a/app/assets/javascripts/boards/stores/actions.js
+++ b/app/assets/javascripts/boards/stores/actions.js
@@ -18,6 +18,7 @@ import boardLabelsQuery from '../queries/board_labels.query.graphql';
import createBoardListMutation from '../queries/board_list_create.mutation.graphql';
import updateBoardListMutation from '../queries/board_list_update.mutation.graphql';
import issueMoveListMutation from '../queries/issue_move_list.mutation.graphql';
+import destroyBoardListMutation from '../queries/board_list_destroy.mutation.graphql';
import updateAssignees from '~/vue_shared/components/sidebar/queries/updateAssignees.mutation.graphql';
import issueSetLabels from '../queries/issue_set_labels.mutation.graphql';
import issueSetDueDate from '../queries/issue_set_due_date.mutation.graphql';
@@ -212,8 +213,26 @@ export default {
});
},
- deleteList: () => {
- notImplemented();
+ removeList: ({ state, commit }, listId) => {
+ const listsBackup = { ...state.boardLists };
+
+ commit(types.REMOVE_LIST, listId);
+
+ return gqlClient
+ .mutate({
+ mutation: destroyBoardListMutation,
+ variables: {
+ listId,
+ },
+ })
+ .then(({ data: { destroyBoardList: { errors } } }) => {
+ if (errors.length > 0) {
+ commit(types.REMOVE_LIST_FAILURE, listsBackup);
+ }
+ })
+ .catch(() => {
+ commit(types.REMOVE_LIST_FAILURE, listsBackup);
+ });
},
fetchIssuesForList: ({ state, commit }, { listId, fetchNext = false }) => {
@@ -324,7 +343,7 @@ export default {
},
setActiveIssueLabels: async ({ commit, getters }, input) => {
- const activeIssue = getters.getActiveIssue;
+ const { activeIssue } = getters;
const { data } = await gqlClient.mutate({
mutation: issueSetLabels,
variables: {
@@ -349,7 +368,7 @@ export default {
},
setActiveIssueDueDate: async ({ commit, getters }, input) => {
- const activeIssue = getters.getActiveIssue;
+ const { activeIssue } = getters;
const { data } = await gqlClient.mutate({
mutation: issueSetDueDate,
variables: {
diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js
index d1a5db1bcc5..337b2897fe9 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js
+++ b/app/assets/javascripts/boards/stores/boards_store.js
@@ -1,7 +1,6 @@
/* eslint-disable no-shadow, no-param-reassign,consistent-return */
/* global List */
/* global ListIssue */
-import $ from 'jquery';
import { sortBy, pick } from 'lodash';
import Vue from 'vue';
import Cookies from 'js-cookie';
@@ -119,8 +118,12 @@ const boardsStore = {
// https://gitlab.com/gitlab-org/gitlab-foss/issues/30821
});
},
+
updateNewListDropdown(listId) {
- $(`.js-board-list-${listId}`).removeClass('is-active');
+ // eslint-disable-next-line no-unused-expressions
+ document
+ .querySelector(`.js-board-list-${getIdFromGraphQLId(listId)}`)
+ ?.classList.remove('is-active');
},
shouldAddBlankState() {
// Decide whether to add the blank state
diff --git a/app/assets/javascripts/boards/stores/getters.js b/app/assets/javascripts/boards/stores/getters.js
index 89a3b14b262..f717b4101ab 100644
--- a/app/assets/javascripts/boards/stores/getters.js
+++ b/app/assets/javascripts/boards/stores/getters.js
@@ -2,7 +2,7 @@ import { find } from 'lodash';
import { inactiveId } from '../constants';
export default {
- getLabelToggleState: state => (state.isShowingLabels ? 'on' : 'off'),
+ labelToggleState: state => (state.isShowingLabels ? 'on' : 'off'),
isSidebarOpen: state => state.activeId !== inactiveId,
isSwimlanesOn: state => {
if (!gon?.features?.boardsWithSwimlanes && !gon?.features?.swimlanes) {
@@ -15,12 +15,12 @@ export default {
return state.issues[id] || {};
},
- getIssues: (state, getters) => listId => {
+ getIssuesByList: (state, getters) => listId => {
const listIssueIds = state.issuesByListId[listId] || [];
return listIssueIds.map(id => getters.getIssueById(id));
},
- getActiveIssue: state => {
+ activeIssue: state => {
return state.issues[state.activeId] || {};
},
diff --git a/app/assets/javascripts/boards/stores/mutation_types.js b/app/assets/javascripts/boards/stores/mutation_types.js
index d5ddba710a9..29468105b5c 100644
--- a/app/assets/javascripts/boards/stores/mutation_types.js
+++ b/app/assets/javascripts/boards/stores/mutation_types.js
@@ -12,9 +12,8 @@ export const RECEIVE_ADD_LIST_SUCCESS = 'RECEIVE_ADD_LIST_SUCCESS';
export const RECEIVE_ADD_LIST_ERROR = 'RECEIVE_ADD_LIST_ERROR';
export const MOVE_LIST = 'MOVE_LIST';
export const UPDATE_LIST_FAILURE = 'UPDATE_LIST_FAILURE';
-export const REQUEST_REMOVE_LIST = 'REQUEST_REMOVE_LIST';
-export const RECEIVE_REMOVE_LIST_SUCCESS = 'RECEIVE_REMOVE_LIST_SUCCESS';
-export const RECEIVE_REMOVE_LIST_ERROR = 'RECEIVE_REMOVE_LIST_ERROR';
+export const REMOVE_LIST = 'REMOVE_LIST';
+export const REMOVE_LIST_FAILURE = 'REMOVE_LIST_FAILURE';
export const REQUEST_ISSUES_FOR_LIST = 'REQUEST_ISSUES_FOR_LIST';
export const RECEIVE_ISSUES_FOR_LIST_FAILURE = 'RECEIVE_ISSUES_FOR_LIST_FAILURE';
export const RECEIVE_ISSUES_FOR_LIST_SUCCESS = 'RECEIVE_ISSUES_FOR_LIST_SUCCESS';
diff --git a/app/assets/javascripts/boards/stores/mutations.js b/app/assets/javascripts/boards/stores/mutations.js
index 361bb94abe0..eb2003a6ed3 100644
--- a/app/assets/javascripts/boards/stores/mutations.js
+++ b/app/assets/javascripts/boards/stores/mutations.js
@@ -93,16 +93,13 @@ export default {
Vue.set(state, 'boardLists', backupList);
},
- [mutationTypes.REQUEST_REMOVE_LIST]: () => {
- notImplemented();
+ [mutationTypes.REMOVE_LIST]: (state, listId) => {
+ Vue.delete(state.boardLists, listId);
},
- [mutationTypes.RECEIVE_REMOVE_LIST_SUCCESS]: () => {
- notImplemented();
- },
-
- [mutationTypes.RECEIVE_REMOVE_LIST_ERROR]: () => {
- notImplemented();
+ [mutationTypes.REMOVE_LIST_FAILURE](state, listsBackup) {
+ state.error = s__('Boards|An error occurred while removing the list. Please try again.');
+ state.boardLists = listsBackup;
},
[mutationTypes.REQUEST_ISSUES_FOR_LIST]: (state, { listId, fetchNext }) => {
diff --git a/app/assets/javascripts/graphql_shared/utils.js b/app/assets/javascripts/graphql_shared/utils.js
index 7a81d5963f2..5487aeb9391 100644
--- a/app/assets/javascripts/graphql_shared/utils.js
+++ b/app/assets/javascripts/graphql_shared/utils.js
@@ -7,7 +7,7 @@
* @returns {Number}
*/
export const getIdFromGraphQLId = (gid = '') =>
- parseInt((gid || '').replace(/gid:\/\/gitlab\/.*\//g, ''), 10) || null;
+ parseInt(`${gid}`.replace(/gid:\/\/gitlab\/.*\//g, ''), 10) || null;
export const MutationOperationMode = {
Append: 'APPEND',
diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue
index 54365df2119..e1d2895831a 100644
--- a/app/assets/javascripts/ide/components/ide.vue
+++ b/app/assets/javascripts/ide/components/ide.vue
@@ -1,6 +1,5 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
-import { GlButton, GlLoadingIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import {
WEBIDE_MARK_APP_START,
@@ -14,15 +13,8 @@ import {
import { performanceMarkAndMeasure } from '~/performance/utils';
import { modalTypes } from '../constants';
import eventHub from '../eventhub';
-import FindFile from '~/vue_shared/components/file_finder/index.vue';
-import NewModal from './new_dropdown/modal.vue';
import IdeSidebar from './ide_side_bar.vue';
-import RepoTabs from './repo_tabs.vue';
-import IdeStatusBar from './ide_status_bar.vue';
import RepoEditor from './repo_editor.vue';
-import RightPane from './panes/right.vue';
-import ErrorMessage from './error_message.vue';
-import CommitEditorHeader from './commit_sidebar/editor_header.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { measurePerformance } from '../utils';
@@ -43,19 +35,24 @@ eventHub.$on(WEBIDE_MEASURE_FILE_AFTER_INTERACTION, () =>
export default {
components: {
- NewModal,
IdeSidebar,
- RepoTabs,
- IdeStatusBar,
RepoEditor,
- FindFile,
- ErrorMessage,
- CommitEditorHeader,
- GlButton,
- GlLoadingIcon,
- RightPane,
+ 'error-message': () => import('./error_message.vue'),
+ 'gl-button': () => import('@gitlab/ui/src/components/base/button/button.vue'),
+ 'gl-loading-icon': () => import('@gitlab/ui/src/components/base/loading_icon/loading_icon.vue'),
+ 'commit-editor-header': () => import('./commit_sidebar/editor_header.vue'),
+ 'repo-tabs': () => import('./repo_tabs.vue'),
+ 'ide-status-bar': () => import('./ide_status_bar.vue'),
+ 'find-file': () => import('~/vue_shared/components/file_finder/index.vue'),
+ 'right-pane': () => import('./panes/right.vue'),
+ 'new-modal': () => import('./new_dropdown/modal.vue'),
},
mixins: [glFeatureFlagsMixin()],
+ data() {
+ return {
+ loadDeferred: false,
+ };
+ },
computed: {
...mapState([
'openFiles',
@@ -107,6 +104,9 @@ export default {
createNewFile() {
this.$refs.newModal.open(modalTypes.blob);
},
+ loadDeferredComponents() {
+ this.loadDeferred = true;
+ },
},
};
</script>
@@ -118,19 +118,23 @@ export default {
>
<error-message v-if="errorMessage" :message="errorMessage" />
<div class="ide-view flex-grow d-flex">
- <find-file
- v-show="fileFindVisible"
- :files="allBlobs"
- :visible="fileFindVisible"
- :loading="loading"
- @toggle="toggleFileFinder"
- @click="openFile"
- />
- <ide-sidebar />
+ <template v-if="loadDeferred">
+ <find-file
+ v-show="fileFindVisible"
+ :files="allBlobs"
+ :visible="fileFindVisible"
+ :loading="loading"
+ @toggle="toggleFileFinder"
+ @click="openFile"
+ />
+ </template>
+ <ide-sidebar @tree-ready="loadDeferredComponents" />
<div class="multi-file-edit-pane">
<template v-if="activeFile">
- <commit-editor-header v-if="isCommitModeActive" :active-file="activeFile" />
- <repo-tabs v-else :active-file="activeFile" :files="openFiles" :viewer="viewer" />
+ <template v-if="loadDeferred">
+ <commit-editor-header v-if="isCommitModeActive" :active-file="activeFile" />
+ <repo-tabs v-else :active-file="activeFile" :files="openFiles" :viewer="viewer" />
+ </template>
<repo-editor :file="activeFile" class="multi-file-edit-pane-content" />
</template>
<template v-else>
@@ -177,9 +181,13 @@ export default {
</div>
</template>
</div>
- <right-pane v-if="currentProjectId" />
+ <template v-if="loadDeferred">
+ <right-pane v-if="currentProjectId" />
+ </template>
</div>
- <ide-status-bar />
- <new-modal ref="newModal" />
+ <template v-if="loadDeferred">
+ <ide-status-bar />
+ <new-modal ref="newModal" />
+ </template>
</article>
</template>
diff --git a/app/assets/javascripts/ide/components/ide_side_bar.vue b/app/assets/javascripts/ide/components/ide_side_bar.vue
index 53dfc133fc8..99215d6c3f1 100644
--- a/app/assets/javascripts/ide/components/ide_side_bar.vue
+++ b/app/assets/javascripts/ide/components/ide_side_bar.vue
@@ -4,21 +4,19 @@ import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
import IdeTree from './ide_tree.vue';
import ResizablePanel from './resizable_panel.vue';
import ActivityBar from './activity_bar.vue';
-import RepoCommitSection from './repo_commit_section.vue';
import CommitForm from './commit_sidebar/form.vue';
-import IdeReview from './ide_review.vue';
import IdeProjectHeader from './ide_project_header.vue';
-import { SIDEBAR_INIT_WIDTH } from '../constants';
+import { SIDEBAR_INIT_WIDTH, leftSidebarViews } from '../constants';
export default {
components: {
GlSkeletonLoading,
ResizablePanel,
ActivityBar,
- RepoCommitSection,
IdeTree,
+ [leftSidebarViews.review.name]: () => import('./ide_review.vue'),
+ [leftSidebarViews.commit.name]: () => import('./repo_commit_section.vue'),
CommitForm,
- IdeReview,
IdeProjectHeader,
},
computed: {
@@ -49,7 +47,7 @@ export default {
<div class="multi-file-commit-panel-inner" data-testid="ide-side-bar-inner">
<div class="multi-file-commit-panel-inner-content">
<keep-alive>
- <component :is="currentActivityView" />
+ <component :is="currentActivityView" @tree-ready="$emit('tree-ready')" />
</keep-alive>
</div>
<commit-form />
diff --git a/app/assets/javascripts/ide/components/ide_tree.vue b/app/assets/javascripts/ide/components/ide_tree.vue
index 56fcb6c2600..e563de6659a 100644
--- a/app/assets/javascripts/ide/components/ide_tree.vue
+++ b/app/assets/javascripts/ide/components/ide_tree.vue
@@ -51,7 +51,7 @@ export default {
</script>
<template>
- <ide-tree-list>
+ <ide-tree-list @tree-ready="$emit('tree-ready')">
<template #header>
{{ __('Edit') }}
<div class="ide-tree-actions ml-auto d-flex" data-testid="ide-root-actions">
diff --git a/app/assets/javascripts/ide/components/ide_tree_list.vue b/app/assets/javascripts/ide/components/ide_tree_list.vue
index d7ff1b8316f..e7e94f5b5da 100644
--- a/app/assets/javascripts/ide/components/ide_tree_list.vue
+++ b/app/assets/javascripts/ide/components/ide_tree_list.vue
@@ -32,6 +32,13 @@ export default {
return !this.currentTree || this.currentTree.loading;
},
},
+ watch: {
+ showLoading(newVal) {
+ if (!newVal) {
+ this.$emit('tree-ready');
+ }
+ },
+ },
beforeCreate() {
performanceMarkAndMeasure({ mark: WEBIDE_MARK_TREE_START });
},
diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue
index 96cb024c768..61e5db0970a 100644
--- a/app/assets/javascripts/issue_show/components/app.vue
+++ b/app/assets/javascripts/issue_show/components/app.vue
@@ -136,6 +136,16 @@ export default {
type: String,
required: true,
},
+ isConfidential: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ isLocked: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
issuableType: {
type: String,
required: false,
@@ -453,6 +463,12 @@ export default {
<gl-icon :name="statusIcon" class="gl-display-block d-sm-none gl-h-6!" />
<span class="gl-display-none d-sm-block">{{ statusText }}</span>
</p>
+ <span v-if="isLocked" data-testid="locked" class="issuable-warning-icon">
+ <gl-icon name="lock" :aria-label="__('Locked')" />
+ </span>
+ <span v-if="isConfidential" data-testid="confidential" class="issuable-warning-icon">
+ <gl-icon name="eye-slash" :aria-label="__('Confidential')" />
+ </span>
<p
class="gl-font-weight-bold gl-overflow-hidden gl-white-space-nowrap gl-text-overflow-ellipsis gl-my-0"
:title="state.titleText"
diff --git a/app/assets/javascripts/issue_show/issue.js b/app/assets/javascripts/issue_show/issue.js
index fc9e8e051bb..b5cd466596e 100644
--- a/app/assets/javascripts/issue_show/issue.js
+++ b/app/assets/javascripts/issue_show/issue.js
@@ -17,6 +17,8 @@ export function initIssuableApp(issuableData, store) {
return createElement(IssuableApp, {
props: {
...issuableData,
+ isConfidential: this.getNoteableData?.confidential,
+ isLocked: this.getNoteableData?.discussion_locked,
issuableStatus: this.getNoteableData?.state,
},
});
diff --git a/app/assets/javascripts/notes/components/discussion_actions.vue b/app/assets/javascripts/notes/components/discussion_actions.vue
index 878a748e99a..0272790a75d 100644
--- a/app/assets/javascripts/notes/components/discussion_actions.vue
+++ b/app/assets/javascripts/notes/components/discussion_actions.vue
@@ -45,7 +45,7 @@ export default {
return this.discussion.notes.filter(x => x.resolvable);
},
userCanResolveDiscussion() {
- return this.resolvableNotes.every(note => note.current_user && note.current_user.can_resolve);
+ return this.resolvableNotes.every(note => note.current_user?.can_resolve_discussion);
},
},
};
diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue
index 4b3f23e742d..43f17c5d65c 100644
--- a/app/assets/javascripts/notes/components/note_form.vue
+++ b/app/assets/javascripts/notes/components/note_form.vue
@@ -121,7 +121,13 @@ export default {
return this.withBatchComments && this.noteId === '' && !this.discussion.for_commit;
},
showResolveDiscussionToggle() {
- return (this.discussion?.id && this.discussion.resolvable) || this.isDraft;
+ if (!this.discussion?.notes) return false;
+
+ return (
+ this.discussion?.notes
+ .filter(n => n.resolvable)
+ .some(n => n.current_user?.can_resolve_discussion) || this.isDraft
+ );
},
noteHash() {
if (this.noteId) {
diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue
index 1775e83978e..9be53fe60f2 100644
--- a/app/assets/javascripts/notes/components/noteable_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -141,6 +141,8 @@ export default {
canResolve() {
if (this.glFeatures.removeResolveNote && !this.discussionRoot) return false;
+ if (this.glFeatures.removeResolveNote) return this.note.current_user.can_resolve_discussion;
+
return (
this.note.current_user.can_resolve ||
(this.note.isDraft && this.note.discussion_id !== null)
diff --git a/app/assets/javascripts/pipelines/components/graph/job_item.vue b/app/assets/javascripts/pipelines/components/graph/job_item.vue
index 09f40601fbf..4ed0aae0d1e 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_item.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_item.vue
@@ -132,24 +132,26 @@ export default {
<div class="ci-job-component" data-qa-selector="job_item_container">
<gl-link
v-if="status.has_details"
- v-gl-tooltip="{ boundary, placement: 'bottom' }"
+ v-gl-tooltip="{ boundary, placement: 'bottom', customClass: 'gl-pointer-events-none' }"
:href="status.details_path"
:title="tooltipText"
:class="jobClasses"
class="js-pipeline-graph-job-link qa-job-link menu-item"
data-testid="job-with-link"
@click.stop="hideTooltips"
+ @mouseout="hideTooltips"
>
<job-name-component :name="job.name" :status="job.status" />
</gl-link>
<div
v-else
- v-gl-tooltip="{ boundary, placement: 'bottom' }"
+ v-gl-tooltip="{ boundary, placement: 'bottom', customClass: 'gl-pointer-events-none' }"
:title="tooltipText"
:class="jobClasses"
class="js-job-component-tooltip non-details-job-component"
data-testid="job-without-link"
+ @mouseout="hideTooltips"
>
<job-name-component :name="job.name" :status="job.status" />
</div>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue
index 00ccf59e37c..9ee427d01fd 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue
@@ -337,7 +337,7 @@ export default {
:message="emptyTabMessage"
/>
- <div v-else-if="stateToRender === $options.stateMap.tableList" class="table-holder">
+ <div v-else-if="stateToRender === $options.stateMap.tableList">
<pipelines-table-component
:pipelines="state.pipelines"
:pipeline-schedule-url="pipelineScheduleUrl"
diff --git a/app/assets/javascripts/search/dropdown_filter/components/dropdown_filter.vue b/app/assets/javascripts/search/dropdown_filter/components/dropdown_filter.vue
deleted file mode 100644
index 08619fa2066..00000000000
--- a/app/assets/javascripts/search/dropdown_filter/components/dropdown_filter.vue
+++ /dev/null
@@ -1,108 +0,0 @@
-<script>
-import { mapState } from 'vuex';
-import { GlDropdown, GlDropdownItem, GlDropdownDivider } from '@gitlab/ui';
-import { setUrlParams, visitUrl } from '~/lib/utils/url_utility';
-import { sprintf, s__ } from '~/locale';
-
-export default {
- name: 'DropdownFilter',
- components: {
- GlDropdown,
- GlDropdownItem,
- GlDropdownDivider,
- },
- props: {
- filterData: {
- type: Object,
- required: true,
- },
- },
- computed: {
- ...mapState(['query']),
- scope() {
- return this.query.scope;
- },
- supportedScopes() {
- return Object.values(this.filterData.scopes);
- },
- initialFilter() {
- return this.query[this.filterData.filterParam];
- },
- filter() {
- return this.initialFilter || this.filterData.filters.ANY.value;
- },
- filtersArray() {
- return this.filterData.filterByScope[this.scope];
- },
- selectedFilter: {
- get() {
- if (this.filtersArray.some(({ value }) => value === this.filter)) {
- return this.filter;
- }
-
- return this.filterData.filters.ANY.value;
- },
- set(filter) {
- // we need to remove the pagination cursor to ensure the
- // relevancy of the new results
-
- visitUrl(
- setUrlParams({
- page: null,
- [this.filterData.filterParam]: filter,
- }),
- );
- },
- },
- selectedFilterText() {
- const f = this.filtersArray.find(({ value }) => value === this.selectedFilter);
- if (!f || f === this.filterData.filters.ANY) {
- return sprintf(s__('Any %{header}'), { header: this.filterData.header });
- }
-
- return f.label;
- },
- showDropdown() {
- return this.supportedScopes.includes(this.scope);
- },
- },
- methods: {
- dropDownItemClass(filter) {
- return {
- 'gl-border-b-solid gl-border-b-gray-100 gl-border-b-1 gl-pb-2! gl-mb-2':
- filter === this.filterData.filters.ANY,
- };
- },
- isFilterSelected(filter) {
- return filter === this.selectedFilter;
- },
- handleFilterChange(filter) {
- this.selectedFilter = filter;
- },
- },
-};
-</script>
-
-<template>
- <gl-dropdown
- v-if="showDropdown"
- :text="selectedFilterText"
- class="col-3 gl-pt-4 gl-pl-0 gl-pr-0 gl-mr-4"
- menu-class="gl-w-full! gl-pl-0"
- >
- <header class="gl-text-center gl-font-weight-bold gl-font-lg">
- {{ filterData.header }}
- </header>
- <gl-dropdown-divider />
- <gl-dropdown-item
- v-for="f in filtersArray"
- :key="f.value"
- :is-check-item="true"
- :is-checked="isFilterSelected(f.value)"
- :class="dropDownItemClass(f)"
- @click="handleFilterChange(f.value)"
- >
- {{ f.label }}
- </gl-dropdown-item>
- </gl-dropdown>
-</template>
diff --git a/app/assets/javascripts/search/dropdown_filter/constants/confidential_filter_data.js b/app/assets/javascripts/search/dropdown_filter/constants/confidential_filter_data.js
deleted file mode 100644
index b29daca89cb..00000000000
--- a/app/assets/javascripts/search/dropdown_filter/constants/confidential_filter_data.js
+++ /dev/null
@@ -1,36 +0,0 @@
-import { __ } from '~/locale';
-
-const header = __('Confidentiality');
-
-const filters = {
- ANY: {
- label: __('Any'),
- value: null,
- },
- CONFIDENTIAL: {
- label: __('Confidential'),
- value: 'yes',
- },
- NOT_CONFIDENTIAL: {
- label: __('Not confidential'),
- value: 'no',
- },
-};
-
-const scopes = {
- ISSUES: 'issues',
-};
-
-const filterByScope = {
- [scopes.ISSUES]: [filters.ANY, filters.CONFIDENTIAL, filters.NOT_CONFIDENTIAL],
-};
-
-const filterParam = 'confidential';
-
-export default {
- header,
- filters,
- scopes,
- filterByScope,
- filterParam,
-};
diff --git a/app/assets/javascripts/search/dropdown_filter/constants/state_filter_data.js b/app/assets/javascripts/search/dropdown_filter/constants/state_filter_data.js
deleted file mode 100644
index 0b93aa0be29..00000000000
--- a/app/assets/javascripts/search/dropdown_filter/constants/state_filter_data.js
+++ /dev/null
@@ -1,42 +0,0 @@
-import { __ } from '~/locale';
-
-const header = __('Status');
-
-const filters = {
- ANY: {
- label: __('Any'),
- value: 'all',
- },
- OPEN: {
- label: __('Open'),
- value: 'opened',
- },
- CLOSED: {
- label: __('Closed'),
- value: 'closed',
- },
- MERGED: {
- label: __('Merged'),
- value: 'merged',
- },
-};
-
-const scopes = {
- ISSUES: 'issues',
- MERGE_REQUESTS: 'merge_requests',
-};
-
-const filterByScope = {
- [scopes.ISSUES]: [filters.ANY, filters.OPEN, filters.CLOSED],
- [scopes.MERGE_REQUESTS]: [filters.ANY, filters.OPEN, filters.MERGED, filters.CLOSED],
-};
-
-const filterParam = 'state';
-
-export default {
- header,
- filters,
- scopes,
- filterByScope,
- filterParam,
-};
diff --git a/app/assets/javascripts/search/dropdown_filter/index.js b/app/assets/javascripts/search/dropdown_filter/index.js
deleted file mode 100644
index e5e0745d990..00000000000
--- a/app/assets/javascripts/search/dropdown_filter/index.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import Vue from 'vue';
-import Translate from '~/vue_shared/translate';
-import DropdownFilter from './components/dropdown_filter.vue';
-import stateFilterData from './constants/state_filter_data';
-import confidentialFilterData from './constants/confidential_filter_data';
-
-Vue.use(Translate);
-
-const mountDropdownFilter = (store, { id, filterData }) => {
- const el = document.getElementById(id);
-
- if (!el) return false;
-
- return new Vue({
- el,
- store,
- render(createElement) {
- return createElement(DropdownFilter, {
- props: {
- filterData,
- },
- });
- },
- });
-};
-
-const dropdownFilters = [
- {
- id: 'js-search-filter-by-state',
- filterData: stateFilterData,
- },
- {
- id: 'js-search-filter-by-confidential',
- filterData: confidentialFilterData,
- },
-];
-
-export default store => [...dropdownFilters].map(filter => mountDropdownFilter(store, filter));
diff --git a/app/assets/javascripts/search/index.js b/app/assets/javascripts/search/index.js
index 275d6351adc..7508b3c9a55 100644
--- a/app/assets/javascripts/search/index.js
+++ b/app/assets/javascripts/search/index.js
@@ -1,17 +1,11 @@
import { queryToObject } from '~/lib/utils/url_utility';
import createStore from './store';
-import initDropdownFilters from './dropdown_filter';
import { initSidebar } from './sidebar';
import initGroupFilter from './group_filter';
export default () => {
const store = createStore({ query: queryToObject(window.location.search) });
- if (gon.features.searchFacets) {
- initSidebar(store);
- } else {
- initDropdownFilters(store);
- }
-
+ initSidebar(store);
initGroupFilter(store);
};
diff --git a/app/assets/javascripts/search/sidebar/components/app.vue b/app/assets/javascripts/search/sidebar/components/app.vue
new file mode 100644
index 00000000000..0c50f93d381
--- /dev/null
+++ b/app/assets/javascripts/search/sidebar/components/app.vue
@@ -0,0 +1,41 @@
+<script>
+import { mapActions, mapState } from 'vuex';
+import { GlButton, GlLink } from '@gitlab/ui';
+import StatusFilter from './status_filter.vue';
+import ConfidentialityFilter from './confidentiality_filter.vue';
+
+export default {
+ name: 'GlobalSearchSidebar',
+ components: {
+ GlButton,
+ GlLink,
+ StatusFilter,
+ ConfidentialityFilter,
+ },
+ computed: {
+ ...mapState(['query']),
+ showReset() {
+ return this.query.state || this.query.confidential;
+ },
+ },
+ methods: {
+ ...mapActions(['applyQuery', 'resetQuery']),
+ },
+};
+</script>
+
+<template>
+ <form
+ class="gl-display-flex gl-flex-direction-column col-md-3 gl-mr-4 gl-mb-6 gl-mb gl-mt-5"
+ @submit.prevent="applyQuery"
+ >
+ <status-filter />
+ <confidentiality-filter />
+ <div class="gl-display-flex gl-align-items-center gl-mt-3">
+ <gl-button variant="success" type="submit">{{ __('Apply') }}</gl-button>
+ <gl-link v-if="showReset" class="gl-ml-auto" @click="resetQuery">{{
+ __('Reset filters')
+ }}</gl-link>
+ </div>
+ </form>
+</template>
diff --git a/app/assets/javascripts/search/sidebar/components/confidentiality_filter.vue b/app/assets/javascripts/search/sidebar/components/confidentiality_filter.vue
index f8938e799aa..38dccb9675d 100644
--- a/app/assets/javascripts/search/sidebar/components/confidentiality_filter.vue
+++ b/app/assets/javascripts/search/sidebar/components/confidentiality_filter.vue
@@ -21,5 +21,6 @@ export default {
<template>
<div v-if="showDropdown">
<radio-filter :filter-data="$options.confidentialFilterData" />
+ <hr class="gl-my-5 gl-border-gray-100" />
</div>
</template>
diff --git a/app/assets/javascripts/search/sidebar/components/status_filter.vue b/app/assets/javascripts/search/sidebar/components/status_filter.vue
index 876123ccc52..5cec2090906 100644
--- a/app/assets/javascripts/search/sidebar/components/status_filter.vue
+++ b/app/assets/javascripts/search/sidebar/components/status_filter.vue
@@ -21,5 +21,6 @@ export default {
<template>
<div v-if="showDropdown">
<radio-filter :filter-data="$options.stateFilterData" />
+ <hr class="gl-my-5 gl-border-gray-100" />
</div>
</template>
diff --git a/app/assets/javascripts/search/sidebar/index.js b/app/assets/javascripts/search/sidebar/index.js
index b19016edf3d..6419e8ac2c6 100644
--- a/app/assets/javascripts/search/sidebar/index.js
+++ b/app/assets/javascripts/search/sidebar/index.js
@@ -1,12 +1,11 @@
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
-import StatusFilter from './components/status_filter.vue';
-import ConfidentialityFilter from './components/confidentiality_filter.vue';
+import GlobalSearchSidebar from './components/app.vue';
Vue.use(Translate);
-const mountRadioFilters = (store, { id, component }) => {
- const el = document.getElementById(id);
+export const initSidebar = store => {
+ const el = document.getElementById('js-search-sidebar');
if (!el) return false;
@@ -14,21 +13,7 @@ const mountRadioFilters = (store, { id, component }) => {
el,
store,
render(createElement) {
- return createElement(component);
+ return createElement(GlobalSearchSidebar);
},
});
};
-
-const radioFilters = [
- {
- id: 'js-search-filter-by-state',
- component: StatusFilter,
- },
- {
- id: 'js-search-filter-by-confidential',
- component: ConfidentialityFilter,
- },
-];
-
-export const initSidebar = store =>
- [...radioFilters].map(filter => mountRadioFilters(store, filter));
diff --git a/app/assets/javascripts/search/store/actions.js b/app/assets/javascripts/search/store/actions.js
index 722ed2eec26..447278aa223 100644
--- a/app/assets/javascripts/search/store/actions.js
+++ b/app/assets/javascripts/search/store/actions.js
@@ -1,6 +1,7 @@
import Api from '~/api';
import createFlash from '~/flash';
import { __ } from '~/locale';
+import { visitUrl, setUrlParams } from '~/lib/utils/url_utility';
import * as types from './mutation_types';
export const fetchGroups = ({ commit }, search) => {
@@ -18,3 +19,11 @@ export const fetchGroups = ({ commit }, search) => {
export const setQuery = ({ commit }, { key, value }) => {
commit(types.SET_QUERY, { key, value });
};
+
+export const applyQuery = ({ state }) => {
+ visitUrl(setUrlParams({ ...state.query, page: null }));
+};
+
+export const resetQuery = ({ state }) => {
+ visitUrl(setUrlParams({ ...state.query, page: null, state: null, confidential: null }));
+};
diff --git a/app/assets/javascripts/static_site_editor/components/edit_area.vue b/app/assets/javascripts/static_site_editor/components/edit_area.vue
index 034c8dea012..09bc176c399 100644
--- a/app/assets/javascripts/static_site_editor/components/edit_area.vue
+++ b/app/assets/javascripts/static_site_editor/components/edit_area.vue
@@ -37,6 +37,14 @@ export default {
required: false,
default: '',
},
+ branch: {
+ type: String,
+ required: true,
+ },
+ baseUrl: {
+ type: String,
+ required: true,
+ },
mounts: {
type: Array,
required: true,
@@ -75,7 +83,7 @@ export default {
return this.editorMode === EDITOR_TYPES.wysiwyg;
},
customRenderers() {
- const imageRenderer = renderImage.build(this.mounts, this.project);
+ const imageRenderer = renderImage.build(this.mounts, this.project, this.branch, this.baseUrl);
return {
image: [imageRenderer],
};
diff --git a/app/assets/javascripts/static_site_editor/graphql/queries/app_data.query.graphql b/app/assets/javascripts/static_site_editor/graphql/queries/app_data.query.graphql
index 4842dfcda49..e422a4b6036 100644
--- a/app/assets/javascripts/static_site_editor/graphql/queries/app_data.query.graphql
+++ b/app/assets/javascripts/static_site_editor/graphql/queries/app_data.query.graphql
@@ -6,6 +6,8 @@ query appData {
sourcePath
username
returnUrl
+ branch
+ baseUrl
mounts {
source
target
diff --git a/app/assets/javascripts/static_site_editor/graphql/typedefs.graphql b/app/assets/javascripts/static_site_editor/graphql/typedefs.graphql
index fdac87cd91c..00af6c10359 100644
--- a/app/assets/javascripts/static_site_editor/graphql/typedefs.graphql
+++ b/app/assets/javascripts/static_site_editor/graphql/typedefs.graphql
@@ -26,6 +26,8 @@ type AppData {
returnUrl: String
sourcePath: String!
username: String!
+ branch: String!
+ baseUrl: String!
mounts: [Mount]!
imageUploadPath: String!
}
diff --git a/app/assets/javascripts/static_site_editor/index.js b/app/assets/javascripts/static_site_editor/index.js
index dd684429794..b58564388de 100644
--- a/app/assets/javascripts/static_site_editor/index.js
+++ b/app/assets/javascripts/static_site_editor/index.js
@@ -9,6 +9,7 @@ const initStaticSiteEditor = el => {
isSupportedContent,
path: sourcePath,
baseUrl,
+ branch,
namespace,
project,
mergeRequestsIllustrationPath,
@@ -27,6 +28,8 @@ const initStaticSiteEditor = el => {
hasSubmittedChanges: false,
project: `${namespace}/${project}`,
mounts: JSON.parse(mounts), // NOTE that the object in 'mounts' is a JSON string from the data attribute, so it must be parsed into an object.
+ branch,
+ baseUrl,
returnUrl,
sourcePath,
username,
diff --git a/app/assets/javascripts/static_site_editor/pages/home.vue b/app/assets/javascripts/static_site_editor/pages/home.vue
index 3e2e6e3b3ee..b61e0968d7d 100644
--- a/app/assets/javascripts/static_site_editor/pages/home.vue
+++ b/app/assets/javascripts/static_site_editor/pages/home.vue
@@ -139,6 +139,8 @@ export default {
:saving-changes="isSavingChanges"
:return-url="appData.returnUrl"
:mounts="appData.mounts"
+ :branch="appData.branch"
+ :base-url="appData.baseUrl"
:project="appData.project"
:image-root="appData.imageUploadPath"
@submit="onPrepareSubmit"
diff --git a/app/assets/javascripts/static_site_editor/services/renderers/render_image.js b/app/assets/javascripts/static_site_editor/services/renderers/render_image.js
index 38304a1c57f..5e5e5d1c860 100644
--- a/app/assets/javascripts/static_site_editor/services/renderers/render_image.js
+++ b/app/assets/javascripts/static_site_editor/services/renderers/render_image.js
@@ -1,29 +1,80 @@
+import { isAbsolute, getBaseURL, joinPaths } from '~/lib/utils/url_utility';
+
const canRender = ({ type }) => type === 'image';
-// NOTE: the `metadata` is not used yet, but will be used in a follow-up iteration
-// To be removed with the next iteration of https://gitlab.com/gitlab-org/gitlab/-/issues/218531
-// eslint-disable-next-line no-unused-vars
let metadata;
-const render = (node, { skipChildren }) => {
- skipChildren();
+const isRelativeToCurrentDirectory = basePath => !basePath.startsWith('/');
+
+const extractSourceDirectory = url => {
+ const sourceDir = /^(.+)\/([^/]+)$/.exec(url); // Extracts the base path and fileName from an image path
+ return sourceDir || [null, null, url]; // If no source directory was extracted it means only a fileName was specified (e.g. url='file.png')
+};
+
+const parseCurrentDirectory = basePath => {
+ const baseUrl = decodeURIComponent(metadata.baseUrl);
+ const sourceDirectory = extractSourceDirectory(baseUrl)[1];
+ const currentDirectory = sourceDirectory.split(`/-/sse/${metadata.branch}`)[1];
+
+ return joinPaths(currentDirectory, basePath);
+};
+
+// For more context around this logic, please see the following comment:
+// https://gitlab.com/gitlab-org/gitlab/-/issues/241166#note_409413500
+const generateSourceDirectory = basePath => {
+ let sourceDir = '';
+ let defaultSourceDir = '';
+
+ if (!basePath || isRelativeToCurrentDirectory(basePath)) {
+ return parseCurrentDirectory(basePath);
+ }
+
+ if (!metadata.mounts.length) {
+ return basePath;
+ }
- // To be removed with the next iteration of https://gitlab.com/gitlab-org/gitlab/-/issues/218531
- // TODO resolve relative paths
+ metadata.mounts.forEach(({ source, target }) => {
+ const hasTarget = target !== '';
+
+ if (hasTarget && basePath.includes(target)) {
+ sourceDir = source;
+ } else if (!hasTarget) {
+ defaultSourceDir = joinPaths(source, basePath);
+ }
+ });
+
+ return sourceDir || defaultSourceDir;
+};
+
+const resolveFullPath = originalSrc => {
+ if (isAbsolute(originalSrc)) {
+ return originalSrc;
+ }
+
+ const sourceDirectory = extractSourceDirectory(originalSrc);
+ const [, basePath, fileName] = sourceDirectory;
+ const sourceDir = generateSourceDirectory(basePath);
+
+ return joinPaths(getBaseURL(), metadata.project, '/-/raw/', metadata.branch, sourceDir, fileName);
+};
+
+const render = ({ destination: originalSrc, firstChild }, { skipChildren }) => {
+ skipChildren();
return {
type: 'openTag',
tagName: 'img',
selfClose: true,
attributes: {
- src: node.destination,
- alt: node.firstChild.literal,
+ 'data-original-src': !isAbsolute(originalSrc) ? originalSrc : '',
+ src: resolveFullPath(originalSrc),
+ alt: firstChild.literal,
},
};
};
-const build = (mounts, project) => {
- metadata = { mounts, project };
+const build = (mounts = [], project, branch, baseUrl) => {
+ metadata = { mounts, project, branch, baseUrl };
return { canRender, render };
};
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer.js
index 2bce691e793..9744e25a8e1 100644
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer.js
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer.js
@@ -99,6 +99,10 @@ const buildHTMLToMarkdownRender = (baseRenderer, formattingPreferences = {}) =>
? `\n\n${node.innerText}\n\n`
: baseRenderer.convert(node, subContent);
},
+ IMG(node) {
+ const { originalSrc } = node.dataset;
+ return `![${node.alt}](${originalSrc || node.src})`;
+ },
};
};
diff --git a/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss b/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss
index 14274c0747a..52cc7d3449e 100644
--- a/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss
+++ b/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss
@@ -151,7 +151,7 @@
border-color: var(--ide-border-color-alt, $gray-100);
code {
- background-color: var(--ide-border-color, inherit);
+ background-color: var(--ide-empty-state-background, inherit);
}
}
@@ -427,7 +427,7 @@
}
.md table:not(.code) tbody {
- background-color: var(--ide-border-color, $white);
+ background-color: var(--ide-empty-state-background, $white);
}
.animation-container {
diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss
index 54cd267b993..15cc10d1532 100644
--- a/app/assets/stylesheets/page_bundles/ide.scss
+++ b/app/assets/stylesheets/page_bundles/ide.scss
@@ -5,7 +5,9 @@
@import './ide_theme_overrides';
@import './ide_themes/dark';
+@import './ide_themes/solarized-light';
@import './ide_themes/solarized-dark';
+@import './ide_themes/monokai';
$search-list-icon-width: 18px;
$ide-activity-bar-width: 60px;
@@ -176,11 +178,11 @@ $ide-commit-header-height: 48px;
height: 100%;
overflow: auto;
padding: $gl-padding;
- background-color: var(--ide-border-color, transparent);
+ background-color: var(--ide-empty-state-background, transparent);
}
.file-container {
- background-color: var(--ide-border-color, $gray-darker);
+ background-color: var(--ide-empty-state-background, $gray-darker);
display: flex;
height: 100%;
align-items: center;
@@ -491,7 +493,7 @@ $ide-commit-header-height: 48px;
height: 100vh;
align-items: center;
justify-content: center;
- background-color: var(--ide-border-color, transparent);
+ background-color: var(--ide-empty-state-background, transparent);
}
.ide {
diff --git a/app/assets/stylesheets/page_bundles/ide_themes/_dark.scss b/app/assets/stylesheets/page_bundles/ide_themes/_dark.scss
index 5f31d554c19..c7aae77c412 100644
--- a/app/assets/stylesheets/page_bundles/ide_themes/_dark.scss
+++ b/app/assets/stylesheets/page_bundles/ide_themes/_dark.scss
@@ -12,6 +12,7 @@
--ide-highlight-background: #252526;
--ide-link-color: #428fdc;
--ide-footer-background: #060606;
+ --ide-empty-state-background: var(--ide-border-color);
--ide-input-border: #868686;
--ide-input-background: transparent;
diff --git a/app/assets/stylesheets/page_bundles/ide_themes/_monokai.scss b/app/assets/stylesheets/page_bundles/ide_themes/_monokai.scss
new file mode 100644
index 00000000000..f53ace0b6c2
--- /dev/null
+++ b/app/assets/stylesheets/page_bundles/ide_themes/_monokai.scss
@@ -0,0 +1,66 @@
+// -------
+// Please see `app/assets/stylesheets/page_bundles/ide_themes/README.md` for a guide on contributing new themes
+// -------
+.ide.theme-monokai {
+ --ide-border-color: #1a1a18;
+ --ide-border-color-alt: #3f4237;
+ --ide-highlight-accent: #fff;
+ --ide-text-color: #ccc;
+ --ide-text-color-secondary: #b7b7b7;
+ --ide-background: #282822;
+ --ide-background-hover: #2d2d2d;
+ --ide-highlight-background: #1f1f1d;
+ --ide-link-color: #428fdc;
+ --ide-footer-background: #404338;
+ --ide-empty-state-background: #1a1a18;
+
+ --ide-input-border: #7d8175;
+ --ide-input-background: transparent;
+ --ide-input-color: #fff;
+
+ --ide-btn-default-background: transparent;
+ --ide-btn-default-border: #7d8175;
+ --ide-btn-default-hover-border: #b5bda5;
+ --ide-btn-default-hover-border-width: 2px;
+ --ide-btn-default-focus-box-shadow: 0 0 0 1px #bfbfbf;
+
+ --ide-btn-primary-background: #1068bf;
+ --ide-btn-primary-border: #428fdc;
+ --ide-btn-primary-hover-border: #63a6e9;
+ --ide-btn-primary-hover-border-width: 2px;
+ --ide-btn-primary-focus-box-shadow: 0 0 0 1px #63a6e9;
+
+ --ide-btn-success-background: #217645;
+ --ide-btn-success-border: #108548;
+ --ide-btn-success-hover-border: #2da160;
+ --ide-btn-success-hover-border-width: 2px;
+ --ide-btn-success-focus-box-shadow: 0 0 0 1px #2da160;
+
+ // Danger styles should be the same as default styles in dark theme
+ --ide-btn-danger-secondary-background: var(--ide-btn-default-background);
+ --ide-btn-danger-secondary-border: var(--ide-btn-default-border);
+ --ide-btn-danger-secondary-hover-border: var(--ide-btn-default-hover-border);
+ --ide-btn-danger-secondary-hover-border-width: var(--ide-btn-default-hover-border-width);
+ --ide-btn-danger-secondary-focus-box-shadow: var(--ide-btn-default-focus-box-shadow);
+
+ --ide-btn-disabled-background: transparent;
+ --ide-btn-disabled-border: rgba(223, 223, 223, 0.24);
+ --ide-btn-disabled-hover-border: rgba(223, 223, 223, 0.24);
+ --ide-btn-disabled-hover-border-width: 1px;
+ --ide-btn-disabled-focus-box-shadow: 0 0 0 0 transparent;
+ --ide-btn-disabled-color: rgba(145, 145, 145, 0.48);
+
+ --ide-dropdown-background: #36382f;
+ --ide-dropdown-hover-background: #404338;
+
+ --ide-dropdown-btn-hover-border: #b5bda5;
+ --ide-dropdown-btn-hover-background: #3f4237;
+
+ --ide-file-row-btn-hover-background: #404338;
+
+ --ide-diff-insert: rgba(155, 185, 85, 0.2);
+ --ide-diff-remove: rgba(255, 0, 0, 0.2);
+
+ --ide-animation-gradient-1: #404338;
+ --ide-animation-gradient-2: #36382f;
+}
diff --git a/app/assets/stylesheets/page_bundles/ide_themes/_solarized-dark.scss b/app/assets/stylesheets/page_bundles/ide_themes/_solarized-dark.scss
index 83c55310063..1906b3ca938 100644
--- a/app/assets/stylesheets/page_bundles/ide_themes/_solarized-dark.scss
+++ b/app/assets/stylesheets/page_bundles/ide_themes/_solarized-dark.scss
@@ -12,6 +12,7 @@
--ide-highlight-background: #003240;
--ide-link-color: #73b9ff;
--ide-footer-background: var(--ide-highlight-background);
+ --ide-empty-state-background: var(--ide-border-color);
--ide-input-border: #d8d8d8;
--ide-input-background: transparent;
diff --git a/app/assets/stylesheets/page_bundles/ide_themes/_solarized-light.scss b/app/assets/stylesheets/page_bundles/ide_themes/_solarized-light.scss
new file mode 100644
index 00000000000..315a0ae6202
--- /dev/null
+++ b/app/assets/stylesheets/page_bundles/ide_themes/_solarized-light.scss
@@ -0,0 +1,57 @@
+// -------
+// Please see `app/assets/stylesheets/page_bundles/ide_themes/README.md` for a guide on contributing new themes
+// -------
+.ide.theme-solarized-light {
+ --ide-border-color: #dfd7bf;
+ --ide-border-color-alt: #dfd7bf;
+ --ide-highlight-accent: #5c4e21;
+ --ide-text-color: #616161;
+ --ide-text-color-secondary: #526f76;
+ --ide-background: #efe8d3;
+ --ide-background-hover: #ded6be;
+ --ide-highlight-background: #fef6e1;
+ --ide-link-color: #955800;
+ --ide-footer-background: #efe8d3;
+ --ide-empty-state-background: #fef6e1;
+
+ --ide-input-border: #c0b9a4;
+ --ide-input-background: transparent;
+
+ --ide-btn-default-background: transparent;
+ --ide-btn-default-border: #c0b9a4;
+ --ide-btn-default-hover-border: #c0b9a4;
+
+ --ide-btn-primary-background: #b16802;
+ --ide-btn-primary-border: #a35f00;
+ --ide-btn-primary-hover-border: #955800;
+ --ide-btn-primary-hover-border-width: 2px;
+ --ide-btn-primary-focus-box-shadow: 0 0 0 1px #dd8101;
+
+ --ide-btn-danger-secondary-background: transparent;
+
+ --ide-btn-disabled-background: transparent;
+ --ide-btn-disabled-border: rgba(192, 185, 64, 0.48);
+ --ide-btn-disabled-hover-border: rgba(192, 185, 64, 0.48);
+ --ide-btn-disabled-hover-border-width: 1px;
+ --ide-btn-disabled-focus-box-shadow: transparent;
+ --ide-btn-disabled-color: rgba(82, 82, 82, 0.48);
+
+ --ide-dropdown-background: #fef6e1;
+ --ide-dropdown-hover-background: #efe8d3;
+
+ --ide-dropdown-btn-hover-border: #dfd7bf;
+ --ide-dropdown-btn-hover-background: #efe8d3;
+
+ --ide-file-row-btn-hover-background: #ded6be;
+
+ --ide-animation-gradient-1: #d3cbb3;
+ --ide-animation-gradient-2: #efe8d3;
+
+ .ide-empty-state,
+ .ide-sidebar,
+ .ide-commit-empty-state {
+ img {
+ filter: sepia(1) brightness(0.7);
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 00d32b75628..cc4827f75d4 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -1,16 +1,12 @@
.issuable-warning-icon {
background-color: $orange-50;
border-radius: $border-radius-default;
+ color: $orange-600;
width: $issuable-warning-size;
height: $issuable-warning-size;
text-align: center;
margin-right: $issuable-warning-icon-margin;
line-height: $gl-line-height-24;
-
- .icon {
- fill: $orange-600;
- vertical-align: text-bottom;
- }
}
.limit-container-width {
diff --git a/app/controllers/profiles/personal_access_tokens_controller.rb b/app/controllers/profiles/personal_access_tokens_controller.rb
index b005347c43a..a45205c5da7 100644
--- a/app/controllers/profiles/personal_access_tokens_controller.rb
+++ b/app/controllers/profiles/personal_access_tokens_controller.rb
@@ -9,9 +9,13 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
end
def create
- @personal_access_token = finder.build(personal_access_token_params)
+ result = ::PersonalAccessTokens::CreateService.new(
+ current_user: current_user, target_user: current_user, params: personal_access_token_params
+ ).execute
- if @personal_access_token.save
+ @personal_access_token = result.payload[:personal_access_token]
+
+ if result.success?
PersonalAccessToken.redis_store!(current_user.id, @personal_access_token.token)
redirect_to profile_personal_access_tokens_path, notice: _("Your new personal access token has been created.")
else
diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb
index 0f149c24a59..4b21edc98d5 100644
--- a/app/controllers/search_controller.rb
+++ b/app/controllers/search_controller.rb
@@ -24,10 +24,6 @@ class SearchController < ApplicationController
search_term_present && !params[:project_id].present?
end
- before_action do
- push_frontend_feature_flag(:search_facets)
- end
-
layout 'search'
feature_category :global_search
diff --git a/app/helpers/operations_helper.rb b/app/helpers/operations_helper.rb
index 3e802156c8f..8105fce10cf 100644
--- a/app/helpers/operations_helper.rb
+++ b/app/helpers/operations_helper.rb
@@ -30,7 +30,8 @@ module OperationsHelper
'alerts_setup_url' => help_page_path('operations/incident_management/alert_integrations.md', anchor: 'generic-http-endpoint'),
'alerts_usage_url' => project_alert_management_index_path(@project),
'disabled' => disabled.to_s,
- 'project_path' => @project.full_path
+ 'project_path' => @project.full_path,
+ 'multi_integrations' => 'false'
}
end
diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb
index 61030b6d5e6..70e8fb32064 100644
--- a/app/policies/user_policy.rb
+++ b/app/policies/user_policy.rb
@@ -27,6 +27,7 @@ class UserPolicy < BasePolicy
rule { default }.enable :read_user_profile
rule { (private_profile | blocked_user) & ~(user_is_self | admin) }.prevent :read_user_profile
rule { user_is_self | admin }.enable :disable_two_factor
+ rule { (user_is_self | admin) & ~blocked }.enable :create_user_personal_access_token
end
UserPolicy.prepend_if_ee('EE::UserPolicy')
diff --git a/app/serializers/note_entity.rb b/app/serializers/note_entity.rb
index ef305195e22..cf7a71a11d0 100644
--- a/app/serializers/note_entity.rb
+++ b/app/serializers/note_entity.rb
@@ -34,6 +34,10 @@ class NoteEntity < API::Entities::Note
expose :can_resolve do |note|
note.resolvable? && can?(current_user, :resolve_note, note)
end
+
+ expose :can_resolve_discussion do |note|
+ note.discussion.resolvable? && note.discussion.can_resolve?(current_user)
+ end
end
expose :suggestions, using: SuggestionEntity
diff --git a/app/services/personal_access_tokens/create_service.rb b/app/services/personal_access_tokens/create_service.rb
index ff9bb7d6802..93a0135669f 100644
--- a/app/services/personal_access_tokens/create_service.rb
+++ b/app/services/personal_access_tokens/create_service.rb
@@ -2,23 +2,30 @@
module PersonalAccessTokens
class CreateService < BaseService
- def initialize(current_user, params = {})
+ def initialize(current_user:, target_user:, params: {})
@current_user = current_user
+ @target_user = target_user
@params = params.dup
+ @ip_address = @params.delete(:ip_address)
end
def execute
- personal_access_token = current_user.personal_access_tokens.create(params.slice(*allowed_params))
+ return ServiceResponse.error(message: 'Not permitted to create') unless creation_permitted?
- if personal_access_token.persisted?
- ServiceResponse.success(payload: { personal_access_token: personal_access_token })
+ token = target_user.personal_access_tokens.create(params.slice(*allowed_params))
+
+ if token.persisted?
+ log_event(token)
+ ServiceResponse.success(payload: { personal_access_token: token })
else
- ServiceResponse.error(message: personal_access_token.errors.full_messages.to_sentence)
+ ServiceResponse.error(message: token.errors.full_messages.to_sentence, payload: { personal_access_token: token })
end
end
private
+ attr_reader :target_user, :ip_address
+
def allowed_params
[
:name,
@@ -27,5 +34,15 @@ module PersonalAccessTokens
:expires_at
]
end
+
+ def creation_permitted?
+ Ability.allowed?(current_user, :create_user_personal_access_token, target_user)
+ end
+
+ def log_event(token)
+ log_info("PAT CREATION: created_by: '#{current_user.username}', created_for: '#{token.user.username}', token_id: '#{token.id}'")
+ end
end
end
+
+PersonalAccessTokens::CreateService.prepend_if_ee('EE::PersonalAccessTokens::CreateService')
diff --git a/app/services/personal_access_tokens/revoke_service.rb b/app/services/personal_access_tokens/revoke_service.rb
index 17405002d8d..34d542acab1 100644
--- a/app/services/personal_access_tokens/revoke_service.rb
+++ b/app/services/personal_access_tokens/revoke_service.rb
@@ -4,16 +4,17 @@ module PersonalAccessTokens
class RevokeService
attr_reader :token, :current_user, :group
- def initialize(current_user = nil, params = { token: nil, group: nil })
+ def initialize(current_user = nil, token: nil, group: nil )
@current_user = current_user
- @token = params[:token]
- @group = params[:group]
+ @token = token
+ @group = group
end
def execute
return ServiceResponse.error(message: 'Not permitted to revoke') unless revocation_permitted?
if token.revoke!
+ log_event
ServiceResponse.success(message: success_message)
else
ServiceResponse.error(message: error_message)
@@ -33,6 +34,10 @@ module PersonalAccessTokens
def revocation_permitted?
Ability.allowed?(current_user, :revoke_token, token)
end
+
+ def log_event
+ Gitlab::AppLogger.info("PAT REVOCATION: revoked_by: '#{current_user.username}', revoked_for: '#{token.user.username}', token_id: '#{token.id}'")
+ end
end
end
diff --git a/app/services/resource_access_tokens/create_service.rb b/app/services/resource_access_tokens/create_service.rb
index c2b4773a505..70e09be9407 100644
--- a/app/services/resource_access_tokens/create_service.rb
+++ b/app/services/resource_access_tokens/create_service.rb
@@ -83,7 +83,9 @@ module ResourceAccessTokens
end
def create_personal_access_token(user)
- PersonalAccessTokens::CreateService.new(user, personal_access_token_params).execute
+ PersonalAccessTokens::CreateService.new(
+ current_user: user, target_user: user, params: personal_access_token_params
+ ).execute
end
def personal_access_token_params
diff --git a/app/views/search/_results.html.haml b/app/views/search/_results.html.haml
index 607e759928c..3af4437a63a 100644
--- a/app/views/search/_results.html.haml
+++ b/app/views/search/_results.html.haml
@@ -1,7 +1,10 @@
- if @search_objects.to_a.empty?
- = render partial: "search/results/filters"
- = render partial: "search/results/empty"
- = render_if_exists 'shared/promotions/promote_advanced_search'
+ .gl-display-md-flex
+ - if %w(issues merge_requests).include?(@scope)
+ #js-search-sidebar.gl-display-flex.gl-flex-direction-column.col-md-3.gl-mr-4{ }
+ .gl-w-full
+ = render partial: "search/results/empty"
+ = render_if_exists 'shared/promotions/promote_advanced_search'
- else
.search-results-status
.row-content-block.gl-display-flex
@@ -24,19 +27,21 @@
.gl-display-md-flex.gl-flex-direction-column
= render partial: 'search/sort_dropdown'
= render_if_exists 'shared/promotions/promote_advanced_search'
- = render partial: "search/results/filters"
- .results.gl-mt-3
- - if @scope == 'commits'
- %ul.content-list.commit-list
- = render partial: "search/results/commit", collection: @search_objects
- - else
- .search-results
- - if @scope == 'projects'
- .term
- = render 'shared/projects/list', projects: @search_objects, pipeline_status: false
- - else
- = render_if_exists partial: "search/results/#{@scope.singularize}", collection: @search_objects
+ .results.gl-display-md-flex.gl-mt-3
+ - if %w(issues merge_requests).include?(@scope)
+ #js-search-sidebar{ }
+ .gl-w-full
+ - if @scope == 'commits'
+ %ul.content-list.commit-list
+ = render partial: "search/results/commit", collection: @search_objects
+ - else
+ .search-results
+ - if @scope == 'projects'
+ .term
+ = render 'shared/projects/list', projects: @search_objects, pipeline_status: false
+ - else
+ = render_if_exists partial: "search/results/#{@scope.singularize}", collection: @search_objects
- - if @scope != 'projects'
- = paginate_collection(@search_objects)
+ - if @scope != 'projects'
+ = paginate_collection(@search_objects)
diff --git a/app/views/search/results/_filters.html.haml b/app/views/search/results/_filters.html.haml
deleted file mode 100644
index 2356a6e1f2c..00000000000
--- a/app/views/search/results/_filters.html.haml
+++ /dev/null
@@ -1,6 +0,0 @@
-.d-lg-flex.align-items-end
- #js-search-filter-by-state{ 'v-cloak': true }
- #js-search-filter-by-confidential{ 'v-cloak': true }
-
- - if %w(issues merge_requests).include?(@scope)
- %hr.gl-mt-4.gl-mb-4
diff --git a/app/workers/remove_expired_members_worker.rb b/app/workers/remove_expired_members_worker.rb
index f56a6cd9fa2..35844fdf297 100644
--- a/app/workers/remove_expired_members_worker.rb
+++ b/app/workers/remove_expired_members_worker.rb
@@ -7,11 +7,19 @@ class RemoveExpiredMembersWorker # rubocop:disable Scalability/IdempotentWorker
feature_category :authentication_and_authorization
worker_resource_boundary :cpu
+ # rubocop: disable CodeReuse/ActiveRecord
def perform
- Member.expired.find_each do |member|
+ Member.expired.preload(:user).find_each do |member|
Members::DestroyService.new.execute(member, skip_authorization: true)
+
+ expired_user = member.user
+
+ if expired_user.project_bot?
+ Users::DestroyService.new(nil).execute(expired_user, skip_authorization: true)
+ end
rescue => ex
logger.error("Expired Member ID=#{member.id} cannot be removed - #{ex}")
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
end