Commit e5c00a8a authored by Denys Mishunov's avatar Denys Mishunov

Merge branch...

Merge branch '235365-migrate-bootstrap-dropdown-to-gitlab-ui-gldropdown-in-app-assets-javascripts' into 'master'

Migrate merge moment dropdown to GitLab UI

See merge request gitlab-org/gitlab!42803
parents a80f2579 ee43299f

Too many changes to show.

To preserve performance only 1000 of 1000+ files are displayed.

...@@ -28,6 +28,7 @@ Personas are described at https://about.gitlab.com/handbook/marketing/product-ma ...@@ -28,6 +28,7 @@ Personas are described at https://about.gitlab.com/handbook/marketing/product-ma
* [Allison (Application Ops)](https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/#allison-application-ops) * [Allison (Application Ops)](https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/#allison-application-ops)
* [Priyanka (Platform Engineer)](https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/#priyanka-platform-engineer) * [Priyanka (Platform Engineer)](https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/#priyanka-platform-engineer)
* [Dana (Data Analyst)](https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/#dana-data-analyst) * [Dana (Data Analyst)](https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/#dana-data-analyst)
* [Eddie (Content Editor)](https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/#eddie-content-editor)
--> -->
### User experience goal ### User experience goal
......
...@@ -186,31 +186,21 @@ RSpec/ExpectChange: ...@@ -186,31 +186,21 @@ RSpec/ExpectChange:
# Offense count: 47 # Offense count: 47
RSpec/ExpectGitlabTracking: RSpec/ExpectGitlabTracking:
Exclude: Exclude:
- 'ee/spec/controllers/groups/analytics/coverage_reports_controller_spec.rb'
- 'ee/spec/controllers/projects/settings/operations_controller_spec.rb' - 'ee/spec/controllers/projects/settings/operations_controller_spec.rb'
- 'ee/spec/controllers/registrations_controller_spec.rb'
- 'ee/spec/requests/api/visual_review_discussions_spec.rb' - 'ee/spec/requests/api/visual_review_discussions_spec.rb'
- 'ee/spec/services/epics/issue_promote_service_spec.rb' - 'ee/spec/services/epics/issue_promote_service_spec.rb'
- 'spec/controllers/groups/registry/repositories_controller_spec.rb' - 'spec/controllers/groups/registry/repositories_controller_spec.rb'
- 'spec/controllers/groups_controller_spec.rb'
- 'spec/controllers/projects/registry/repositories_controller_spec.rb' - 'spec/controllers/projects/registry/repositories_controller_spec.rb'
- 'spec/controllers/projects/registry/tags_controller_spec.rb' - 'spec/controllers/projects/registry/tags_controller_spec.rb'
- 'spec/controllers/projects/settings/operations_controller_spec.rb' - 'spec/controllers/projects/settings/operations_controller_spec.rb'
- 'spec/controllers/registrations_controller_spec.rb' - 'spec/controllers/registrations_controller_spec.rb'
- 'spec/lib/api/helpers_spec.rb' - 'spec/lib/api/helpers_spec.rb'
- 'spec/lib/gitlab/experimentation_spec.rb'
- 'spec/mailers/notify_spec.rb'
- 'spec/models/project_services/prometheus_service_spec.rb'
- 'spec/requests/api/project_container_repositories_spec.rb' - 'spec/requests/api/project_container_repositories_spec.rb'
- 'spec/services/clusters/applications/check_installation_progress_service_spec.rb'
- 'spec/services/issues/zoom_link_service_spec.rb'
- 'spec/support/helpers/snowplow_helpers.rb'
- 'spec/support/shared_examples/controllers/trackable_shared_examples.rb' - 'spec/support/shared_examples/controllers/trackable_shared_examples.rb'
- 'spec/support/shared_examples/requests/api/container_repositories_shared_examples.rb' - 'spec/support/shared_examples/requests/api/container_repositories_shared_examples.rb'
- 'spec/support/shared_examples/requests/api/discussions_shared_examples.rb' - 'spec/support/shared_examples/requests/api/discussions_shared_examples.rb'
- 'spec/support/shared_examples/requests/api/packages_shared_examples.rb' - 'spec/support/shared_examples/requests/api/packages_shared_examples.rb'
- 'spec/support/shared_examples/requests/api/tracking_shared_examples.rb' - 'spec/support/shared_examples/requests/api/tracking_shared_examples.rb'
- 'spec/support/snowplow.rb'
# Offense count: 751 # Offense count: 751
RSpec/ExpectInHook: RSpec/ExpectInHook:
...@@ -1125,33 +1115,6 @@ Rails/SaveBang: ...@@ -1125,33 +1115,6 @@ Rails/SaveBang:
- 'spec/requests/api/labels_spec.rb' - 'spec/requests/api/labels_spec.rb'
- 'spec/requests/api/project_import_spec.rb' - 'spec/requests/api/project_import_spec.rb'
- 'spec/requests/projects/cycle_analytics_events_spec.rb' - 'spec/requests/projects/cycle_analytics_events_spec.rb'
- 'spec/services/auth/container_registry_authentication_service_spec.rb'
- 'spec/services/auto_merge/base_service_spec.rb'
- 'spec/services/auto_merge_service_spec.rb'
- 'spec/services/clusters/update_service_spec.rb'
- 'spec/services/deployments/after_create_service_spec.rb'
- 'spec/services/design_management/generate_image_versions_service_spec.rb'
- 'spec/services/discussions/resolve_service_spec.rb'
- 'spec/services/draft_notes/destroy_service_spec.rb'
- 'spec/services/emails/confirm_service_spec.rb'
- 'spec/services/groups/destroy_service_spec.rb'
- 'spec/services/groups/import_export/import_service_spec.rb'
- 'spec/services/labels/promote_service_spec.rb'
- 'spec/services/notes/create_service_spec.rb'
- 'spec/services/notification_recipients/build_service_spec.rb'
- 'spec/services/notification_service_spec.rb'
- 'spec/services/packages/conan/create_package_file_service_spec.rb'
- 'spec/services/reset_project_cache_service_spec.rb'
- 'spec/services/resource_events/change_milestone_service_spec.rb'
- 'spec/services/system_hooks_service_spec.rb'
- 'spec/services/system_note_service_spec.rb'
- 'spec/services/system_notes/issuables_service_spec.rb'
- 'spec/services/todo_service_spec.rb'
- 'spec/services/todos/destroy/confidential_issue_service_spec.rb'
- 'spec/services/users/destroy_service_spec.rb'
- 'spec/services/users/repair_ldap_blocked_service_spec.rb'
- 'spec/services/verify_pages_domain_service_spec.rb'
- 'spec/sidekiq/cron/job_gem_dependency_spec.rb'
# Offense count: 187 # Offense count: 187
# Cop supports --auto-correct. # Cop supports --auto-correct.
...@@ -1269,11 +1232,10 @@ RSpec/TimecopTravel: ...@@ -1269,11 +1232,10 @@ RSpec/TimecopTravel:
- 'spec/workers/concerns/reenqueuer_spec.rb' - 'spec/workers/concerns/reenqueuer_spec.rb'
- 'spec/lib/gitlab/analytics/cycle_analytics/median_spec.rb' - 'spec/lib/gitlab/analytics/cycle_analytics/median_spec.rb'
# Offense count: 43 # Offense count: 21
Graphql/IDType: Graphql/IDType:
Exclude: Exclude:
- 'ee/app/graphql/ee/mutations/issues/update.rb' - 'ee/app/graphql/ee/mutations/issues/update.rb'
- 'ee/app/graphql/ee/types/boards/board_issue_input_base_type.rb'
- 'ee/app/graphql/mutations/iterations/update.rb' - 'ee/app/graphql/mutations/iterations/update.rb'
- 'ee/app/graphql/resolvers/iterations_resolver.rb' - 'ee/app/graphql/resolvers/iterations_resolver.rb'
- 'app/graphql/mutations/boards/issues/issue_move_list.rb' - 'app/graphql/mutations/boards/issues/issue_move_list.rb'
......
This diff is collapsed.
This diff is collapsed.
dac25a9c19af0a168a7927784295dd12fb5c8075 0ebfb705b79a8baecc1db46f31761f83f4e471f9
13.5.0-pre 13.6.0-pre
\ No newline at end of file
...@@ -2,6 +2,7 @@ import $ from 'jquery'; ...@@ -2,6 +2,7 @@ import $ from 'jquery';
import '../commons/bootstrap'; import '../commons/bootstrap';
import { isInIssuePage } from '../lib/utils/common_utils'; import { isInIssuePage } from '../lib/utils/common_utils';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { add, show, hide } from '~/tooltips';
// Quick Submit behavior // Quick Submit behavior
// //
...@@ -65,18 +66,17 @@ $(document).on( ...@@ -65,18 +66,17 @@ $(document).on(
return; return;
} }
const $this = $(this); const $el = $(this);
const title = isMac() const title = isMac()
? __('You can also press ⌘-Enter') ? __('You can also press \u{2318}-Enter')
: __('You can also press Ctrl-Enter'); : __('You can also press Ctrl-Enter');
$this.tooltip({ add($el, {
container: 'body', triggers: 'manual',
html: true, show: true,
placement: 'top',
title, title,
trigger: 'manual',
}); });
$this.tooltip('show').one('blur click', () => $this.tooltip('hide')); $el.one('blur click', () => hide($el));
show($el);
}, },
); );
/* global DocumentTouch */ /* global DocumentTouch */
import $ from 'jquery';
import sortableConfig from 'ee_else_ce/sortable/sortable_config'; import sortableConfig from 'ee_else_ce/sortable/sortable_config';
export function sortableStart() { export function sortableStart() {
$('.has-tooltip')
.tooltip('hide')
.tooltip('disable');
document.body.classList.add('is-dragging'); document.body.classList.add('is-dragging');
} }
export function sortableEnd() { export function sortableEnd() {
$('.has-tooltip').tooltip('enable');
document.body.classList.remove('is-dragging'); document.body.classList.remove('is-dragging');
} }
......
<script> <script>
import { mapState, mapGetters, mapActions } from 'vuex'; import { mapState, mapGetters, mapActions } from 'vuex';
import { GlSprintf, GlLink } from '@gitlab/ui'; import { GlSprintf, GlLink, GlIcon } from '@gitlab/ui';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import gkeDropdownMixin from './gke_dropdown_mixin'; import gkeDropdownMixin from './gke_dropdown_mixin';
...@@ -10,6 +10,7 @@ export default { ...@@ -10,6 +10,7 @@ export default {
components: { components: {
GlSprintf, GlSprintf,
GlLink, GlLink,
GlIcon,
}, },
mixins: [gkeDropdownMixin], mixins: [gkeDropdownMixin],
props: { props: {
...@@ -178,14 +179,14 @@ export default { ...@@ -178,14 +179,14 @@ export default {
'https://console.cloud.google.com/freetrial?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral' 'https://console.cloud.google.com/freetrial?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral'
" "
target="_blank" target="_blank"
>{{ content }} <i class="fa fa-external-link" aria-hidden="true"></i >{{ content }} <gl-icon name="external-link" aria-hidden="true"
></gl-link> /></gl-link>
</template> </template>
<template #docsLink="{ content }"> <template #docsLink="{ content }">
<gl-link :href="docsUrl" target="_blank" <gl-link :href="docsUrl" target="_blank"
>{{ content }} <i class="fa fa-external-link" aria-hidden="true"></i >{{ content }} <gl-icon name="external-link" aria-hidden="true"
></gl-link> /></gl-link>
</template> </template>
<template #error> <template #error>
......
...@@ -21,6 +21,7 @@ import { ...@@ -21,6 +21,7 @@ import {
updateImageDiffNoteOptimisticResponse, updateImageDiffNoteOptimisticResponse,
toDiffNoteGid, toDiffNoteGid,
extractDesignNoteId, extractDesignNoteId,
getPageLayoutElement,
} from '../../utils/design_management_utils'; } from '../../utils/design_management_utils';
import { import {
updateStoreAfterAddImageDiffNote, updateStoreAfterAddImageDiffNote,
...@@ -38,7 +39,7 @@ import { ...@@ -38,7 +39,7 @@ import {
} from '../../utils/error_messages'; } from '../../utils/error_messages';
import { trackDesignDetailView } from '../../utils/tracking'; import { trackDesignDetailView } from '../../utils/tracking';
import { DESIGNS_ROUTE_NAME } from '../../router/constants'; import { DESIGNS_ROUTE_NAME } from '../../router/constants';
import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../../constants'; import { ACTIVE_DISCUSSION_SOURCE_TYPES, DESIGN_DETAIL_LAYOUT_CLASSLIST } from '../../constants';
const DEFAULT_SCALE = 1; const DEFAULT_SCALE = 1;
...@@ -300,6 +301,22 @@ export default { ...@@ -300,6 +301,22 @@ export default {
this.resolvedDiscussionsExpanded = !this.resolvedDiscussionsExpanded; this.resolvedDiscussionsExpanded = !this.resolvedDiscussionsExpanded;
}, },
}, },
beforeRouteEnter(to, from, next) {
const pageEl = getPageLayoutElement();
if (pageEl) {
pageEl.classList.add(...DESIGN_DETAIL_LAYOUT_CLASSLIST);
}
next();
},
beforeRouteLeave(to, from, next) {
const pageEl = getPageLayoutElement();
if (pageEl) {
pageEl.classList.remove(...DESIGN_DETAIL_LAYOUT_CLASSLIST);
}
next();
},
createImageDiffNoteMutation, createImageDiffNoteMutation,
DESIGNS_ROUTE_NAME, DESIGNS_ROUTE_NAME,
}; };
......
import Vue from 'vue'; import Vue from 'vue';
import VueRouter from 'vue-router'; import VueRouter from 'vue-router';
import routes from './routes'; import routes from './routes';
import { DESIGN_ROUTE_NAME } from './constants';
import { getPageLayoutElement } from '~/design_management/utils/design_management_utils';
import { DESIGN_DETAIL_LAYOUT_CLASSLIST } from '../constants';
Vue.use(VueRouter); Vue.use(VueRouter);
...@@ -13,20 +10,6 @@ export default function createRouter(base) { ...@@ -13,20 +10,6 @@ export default function createRouter(base) {
mode: 'history', mode: 'history',
routes, routes,
}); });
const pageEl = getPageLayoutElement();
router.beforeEach(({ name }, _, next) => {
// apply a fullscreen layout style in Design View (a.k.a design detail)
if (pageEl) {
if (name === DESIGN_ROUTE_NAME) {
pageEl.classList.add(...DESIGN_DETAIL_LAYOUT_CLASSLIST);
} else {
pageEl.classList.remove(...DESIGN_DETAIL_LAYOUT_CLASSLIST);
}
}
next();
});
return router; return router;
} }
...@@ -12,6 +12,7 @@ export default (IssuableTokenKeys, disableTargetBranchFilter = false) => { ...@@ -12,6 +12,7 @@ export default (IssuableTokenKeys, disableTargetBranchFilter = false) => {
tag: __('Yes or No'), tag: __('Yes or No'),
lowercaseValueOnSubmit: true, lowercaseValueOnSubmit: true,
capitalizeTokenValue: true, capitalizeTokenValue: true,
hideNotEqual: true,
}, },
conditions: [ conditions: [
{ {
...@@ -30,20 +31,6 @@ export default (IssuableTokenKeys, disableTargetBranchFilter = false) => { ...@@ -30,20 +31,6 @@ export default (IssuableTokenKeys, disableTargetBranchFilter = false) => {
value: __('No'), value: __('No'),
operator: '=', operator: '=',
}, },
{
url: 'not[wip]=yes',
replacementUrl: 'not[draft]=yes',
tokenKey: 'draft',
value: __('Yes'),
operator: '!=',
},
{
url: 'not[wip]=no',
replacementUrl: 'not[draft]=no',
tokenKey: 'draft',
value: __('No'),
operator: '!=',
},
], ],
}; };
......
...@@ -39,7 +39,7 @@ export default class DropdownOperator extends FilteredSearchDropdown { ...@@ -39,7 +39,7 @@ export default class DropdownOperator extends FilteredSearchDropdown {
this.dispatchInputEvent(); this.dispatchInputEvent();
} }
renderContent(forceShowList = false) { renderContent(forceShowList = false, dropdownName = '') {
const dropdownData = [ const dropdownData = [
{ {
tag: 'equal', tag: 'equal',
...@@ -48,8 +48,9 @@ export default class DropdownOperator extends FilteredSearchDropdown { ...@@ -48,8 +48,9 @@ export default class DropdownOperator extends FilteredSearchDropdown {
help: __('is'), help: __('is'),
}, },
]; ];
const dropdownToken = this.tokenKeys.searchByKey(dropdownName.toLowerCase());
if (gon.features?.notIssuableQueries) { if (gon.features?.notIssuableQueries && !dropdownToken?.hideNotEqual) {
dropdownData.push({ dropdownData.push({
tag: 'not-equal', tag: 'not-equal',
type: 'string', type: 'string',
......
...@@ -83,16 +83,16 @@ export default class FilteredSearchDropdown { ...@@ -83,16 +83,16 @@ export default class FilteredSearchDropdown {
} }
} }
render(forceRenderContent = false, forceShowList = false) { render(forceRenderContent = false, forceShowList = false, hideNotEqual = false) {
this.setAsDropdown(); this.setAsDropdown();
const currentHook = this.getCurrentHook(); const currentHook = this.getCurrentHook();
const firstTimeInitialized = currentHook === null; const firstTimeInitialized = currentHook === null;
if (firstTimeInitialized || forceRenderContent) { if (firstTimeInitialized || forceRenderContent) {
this.renderContent(forceShowList); this.renderContent(forceShowList, hideNotEqual);
} else if (currentHook.list.list.id !== this.dropdown.id) { } else if (currentHook.list.list.id !== this.dropdown.id) {
this.renderContent(forceShowList); this.renderContent(forceShowList, hideNotEqual);
} }
} }
......
...@@ -107,7 +107,7 @@ export default class FilteredSearchDropdownManager { ...@@ -107,7 +107,7 @@ export default class FilteredSearchDropdownManager {
this.mapping[key].reference.setOffset(offset); this.mapping[key].reference.setOffset(offset);
} }
load(key, firstLoad = false) { load(key, firstLoad = false, dropdownKey = '') {
const mappingKey = this.mapping[key]; const mappingKey = this.mapping[key];
const glClass = mappingKey.gl; const glClass = mappingKey.gl;
const { element } = mappingKey; const { element } = mappingKey;
...@@ -141,12 +141,12 @@ export default class FilteredSearchDropdownManager { ...@@ -141,12 +141,12 @@ export default class FilteredSearchDropdownManager {
} }
this.updateDropdownOffset(key); this.updateDropdownOffset(key);
mappingKey.reference.render(firstLoad, forceShowList); mappingKey.reference.render(firstLoad, forceShowList, dropdownKey);
this.currentDropdown = key; this.currentDropdown = key;
} }
loadDropdown(dropdownName = '') { loadDropdown(dropdownName = '', dropdownKey = '') {
let firstLoad = false; let firstLoad = false;
if (!this.droplab) { if (!this.droplab) {
...@@ -155,7 +155,7 @@ export default class FilteredSearchDropdownManager { ...@@ -155,7 +155,7 @@ export default class FilteredSearchDropdownManager {
} }
if (dropdownName === DROPDOWN_TYPE.operator) { if (dropdownName === DROPDOWN_TYPE.operator) {
this.load(dropdownName, firstLoad); this.load(dropdownName, firstLoad, dropdownKey);
return; return;
} }
...@@ -167,7 +167,7 @@ export default class FilteredSearchDropdownManager { ...@@ -167,7 +167,7 @@ export default class FilteredSearchDropdownManager {
if (shouldOpenFilterDropdown || shouldOpenHintDropdown) { if (shouldOpenFilterDropdown || shouldOpenHintDropdown) {
const key = match && match.key ? match.key : DROPDOWN_TYPE.hint; const key = match && match.key ? match.key : DROPDOWN_TYPE.hint;
this.load(key, firstLoad); this.load(key, firstLoad, dropdownKey);
} }
} }
...@@ -200,11 +200,11 @@ export default class FilteredSearchDropdownManager { ...@@ -200,11 +200,11 @@ export default class FilteredSearchDropdownManager {
dropdownToOpen = hasOperator && lastOperatorToken ? dropdownName : DROPDOWN_TYPE.operator; dropdownToOpen = hasOperator && lastOperatorToken ? dropdownName : DROPDOWN_TYPE.operator;
} }
this.loadDropdown(dropdownToOpen); this.loadDropdown(dropdownToOpen, dropdownName);
} else if (lastToken) { } else if (lastToken) {
const lastOperator = FilteredSearchVisualTokens.getLastTokenOperator(); const lastOperator = FilteredSearchVisualTokens.getLastTokenOperator();
// Token has been initialized into an object because it has a value // Token has been initialized into an object because it has a value
this.loadDropdown(lastOperator ? lastToken.key : DROPDOWN_TYPE.operator); this.loadDropdown(lastOperator ? lastToken.key : DROPDOWN_TYPE.operator, lastToken.key);
} else { } else {
this.loadDropdown(DROPDOWN_TYPE.hint); this.loadDropdown(DROPDOWN_TYPE.hint);
} }
......
<script> <script>
import { GlIcon } from '@gitlab/ui'; import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import tooltip from '~/vue_shared/directives/tooltip';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import { COMMON_STR } from '../constants'; import { COMMON_STR } from '../constants';
...@@ -9,7 +8,7 @@ export default { ...@@ -9,7 +8,7 @@ export default {
GlIcon, GlIcon,
}, },
directives: { directives: {
tooltip, GlTooltip: GlTooltipDirective,
}, },
props: { props: {
parentGroup: { parentGroup: {
...@@ -47,12 +46,10 @@ export default { ...@@ -47,12 +46,10 @@ export default {
<div class="controls d-flex justify-content-end"> <div class="controls d-flex justify-content-end">
<a <a
v-if="group.canLeave" v-if="group.canLeave"
v-tooltip v-gl-tooltip.top
:href="group.leavePath" :href="group.leavePath"
:title="leaveBtnTitle" :title="leaveBtnTitle"
:aria-label="leaveBtnTitle" :aria-label="leaveBtnTitle"
data-container="body"
data-placement="bottom"
data-testid="leave-group-btn" data-testid="leave-group-btn"
class="leave-group btn btn-xs no-expand gl-text-gray-500 gl-ml-5" class="leave-group btn btn-xs no-expand gl-text-gray-500 gl-ml-5"
@click.prevent="onLeaveGroup" @click.prevent="onLeaveGroup"
...@@ -61,12 +58,10 @@ export default { ...@@ -61,12 +58,10 @@ export default {
</a> </a>
<a <a
v-if="group.canEdit" v-if="group.canEdit"
v-tooltip v-gl-tooltip.top
:href="group.editPath" :href="group.editPath"
:title="editBtnTitle" :title="editBtnTitle"
:aria-label="editBtnTitle" :aria-label="editBtnTitle"
data-container="body"
data-placement="bottom"
data-testid="edit-group-btn" data-testid="edit-group-btn"
class="edit-group btn btn-xs no-expand gl-text-gray-500 gl-ml-5" class="edit-group btn btn-xs no-expand gl-text-gray-500 gl-ml-5"
> >
......
<script> <script>
import { mapState, mapActions, mapGetters } from 'vuex'; import { mapState, mapActions, mapGetters } from 'vuex';
import tooltip from '~/vue_shared/directives/tooltip';
import CommitFilesList from './commit_sidebar/list.vue'; import CommitFilesList from './commit_sidebar/list.vue';
import EmptyState from './commit_sidebar/empty_state.vue'; import EmptyState from './commit_sidebar/empty_state.vue';
import { stageKeys } from '../constants'; import { stageKeys } from '../constants';
...@@ -10,9 +9,6 @@ export default { ...@@ -10,9 +9,6 @@ export default {
CommitFilesList, CommitFilesList,
EmptyState, EmptyState,
}, },
directives: {
tooltip,
},
computed: { computed: {
...mapState(['changedFiles', 'stagedFiles', 'lastCommitMsg']), ...mapState(['changedFiles', 'stagedFiles', 'lastCommitMsg']),
...mapState('commit', ['commitMessage', 'submitCommitLoading']), ...mapState('commit', ['commitMessage', 'submitCommitLoading']),
......
<script> <script>
import { uniqBy } from 'lodash'; import { uniqBy } from 'lodash';
import { GlIcon } from '@gitlab/ui'; import { GlButton, GlIcon } from '@gitlab/ui';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
export default { export default {
components: { components: {
GlButton,
GlIcon, GlIcon,
UserAvatarLink, UserAvatarLink,
TimeAgoTooltip, TimeAgoTooltip,
...@@ -57,14 +58,15 @@ export default { ...@@ -57,14 +58,15 @@ export default {
tooltip-placement="bottom" tooltip-placement="bottom"
/> />
</div> </div>
<button <gl-button
class="btn btn-link js-replies-text" class="js-replies-text"
category="tertiary"
variant="link"
data-qa-selector="expand_replies_button" data-qa-selector="expand_replies_button"
type="button"
@click="toggle" @click="toggle"
> >
{{ replies.length }} {{ n__('reply', 'replies', replies.length) }} {{ replies.length }} {{ n__('reply', 'replies', replies.length) }}
</button> </gl-button>
{{ __('Last reply by') }} {{ __('Last reply by') }}
<a :href="lastReply.author.path" class="btn btn-link author-link"> <a :href="lastReply.author.path" class="btn btn-link author-link">
{{ lastReply.author.name }} {{ lastReply.author.name }}
......
...@@ -13,7 +13,6 @@ import CIPaginationMixin from '~/vue_shared/mixins/ci_pagination_api_mixin'; ...@@ -13,7 +13,6 @@ import CIPaginationMixin from '~/vue_shared/mixins/ci_pagination_api_mixin';
import PipelinesFilteredSearch from './pipelines_filtered_search.vue'; import PipelinesFilteredSearch from './pipelines_filtered_search.vue';
import { validateParams } from '../../utils'; import { validateParams } from '../../utils';
import { ANY_TRIGGER_AUTHOR, RAW_TEXT_WARNING, FILTER_TAG_IDENTIFIER } from '../../constants'; import { ANY_TRIGGER_AUTHOR, RAW_TEXT_WARNING, FILTER_TAG_IDENTIFIER } from '../../constants';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default { export default {
components: { components: {
...@@ -23,7 +22,7 @@ export default { ...@@ -23,7 +22,7 @@ export default {
PipelinesFilteredSearch, PipelinesFilteredSearch,
GlIcon, GlIcon,
}, },
mixins: [pipelinesMixin, CIPaginationMixin, glFeatureFlagsMixin()], mixins: [pipelinesMixin, CIPaginationMixin],
props: { props: {
store: { store: {
type: Object, type: Object,
...@@ -209,9 +208,6 @@ export default { ...@@ -209,9 +208,6 @@ export default {
}, },
]; ];
}, },
canFilterPipelines() {
return this.glFeatures.filterPipelinesSearch;
},
validatedParams() { validatedParams() {
return validateParams(this.params); return validateParams(this.params);
}, },
...@@ -306,7 +302,6 @@ export default { ...@@ -306,7 +302,6 @@ export default {
</div> </div>
<pipelines-filtered-search <pipelines-filtered-search
v-if="canFilterPipelines"
:project-id="projectId" :project-id="projectId"
:params="validatedParams" :params="validatedParams"
@filterPipelines="filterPipelines" @filterPipelines="filterPipelines"
......
<script>
import { GlModal } from '@gitlab/ui';
import { __ } from '~/locale';
import CodeBlock from '~/vue_shared/components/code_block.vue';
export default {
name: 'TestCaseDetails',
components: {
CodeBlock,
GlModal,
},
props: {
modalId: {
type: String,
required: true,
},
testCase: {
type: Object,
required: true,
validator: ({ classname, formattedTime, name }) =>
Boolean(classname) && Boolean(formattedTime) && Boolean(name),
},
},
text: {
name: __('Name'),
duration: __('Execution time'),
trace: __('System output'),
},
modalCloseButton: {
text: __('Close'),
attributes: [{ variant: 'info' }],
},
};
</script>
<template>
<gl-modal
:modal-id="modalId"
:title="testCase.classname"
:action-primary="$options.modalCloseButton"
>
<div class="gl-display-flex gl-flex-wrap gl-mx-n4 gl-my-3">
<strong class="gl-text-right col-sm-3">{{ $options.text.name }}</strong>
<div class="col-sm-9" data-testid="test-case-name">
{{ testCase.name }}
</div>
</div>
<div class="gl-display-flex gl-flex-wrap gl-mx-n4 gl-my-3">
<strong class="gl-text-right col-sm-3">{{ $options.text.duration }}</strong>
<div class="col-sm-9" data-testid="test-case-duration">
{{ testCase.formattedTime }}
</div>
</div>
<div
v-if="testCase.system_output"
class="gl-display-flex gl-flex-wrap gl-mx-n4 gl-my-3"
data-testid="test-case-trace"
>
<strong class="gl-text-right col-sm-3">{{ $options.text.trace }}</strong>
<div class="col-sm-9">
<code-block :code="testCase.system_output" />
</div>
</div>
</gl-modal>
</template>
...@@ -61,7 +61,7 @@ export default { ...@@ -61,7 +61,7 @@ export default {
<div <div
v-else-if="!isLoading && showTests" v-else-if="!isLoading && showTests"
ref="container" ref="container"
class="tests-detail position-relative" class="position-relative"
data-testid="tests-detail" data-testid="tests-detail"
> >
<transition <transition
...@@ -69,13 +69,13 @@ export default { ...@@ -69,13 +69,13 @@ export default {
@before-enter="beforeEnterTransition" @before-enter="beforeEnterTransition"
@after-leave="afterLeaveTransition" @after-leave="afterLeaveTransition"
> >
<div v-if="showSuite" key="detail" class="w-100 position-absolute slide-enter-to-element"> <div v-if="showSuite" key="detail" class="w-100 slide-enter-to-element">
<test-summary :report="getSelectedSuite" show-back @on-back-click="summaryBackClick" /> <test-summary :report="getSelectedSuite" show-back @on-back-click="summaryBackClick" />
<test-suite-table /> <test-suite-table />
</div> </div>
<div v-else key="summary" class="w-100 position-absolute slide-enter-from-element"> <div v-else key="summary" class="w-100 slide-enter-from-element">
<test-summary :report="testReports" /> <test-summary :report="testReports" />
<test-summary-table @row-click="summaryTableRowClick" /> <test-summary-table @row-click="summaryTableRowClick" />
......
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { GlTooltipDirective, GlFriendlyWrap, GlIcon, GlButton } from '@gitlab/ui'; import { GlModalDirective, GlTooltipDirective, GlFriendlyWrap, GlIcon, GlButton } from '@gitlab/ui';
import { __ } from '~/locale'; import { __ } from '~/locale';
import TestCaseDetails from './test_case_details.vue';
export default { export default {
name: 'TestsSuiteTable', name: 'TestsSuiteTable',
...@@ -9,9 +10,11 @@ export default { ...@@ -9,9 +10,11 @@ export default {
GlIcon, GlIcon,
GlFriendlyWrap, GlFriendlyWrap,
GlButton, GlButton,
TestCaseDetails,
}, },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
GlModalDirective,
}, },
props: { props: {
heading: { heading: {
...@@ -43,7 +46,7 @@ export default { ...@@ -43,7 +46,7 @@ export default {
<div role="rowheader" class="table-section section-20"> <div role="rowheader" class="table-section section-20">
{{ __('Suite') }} {{ __('Suite') }}
</div> </div>
<div role="rowheader" class="table-section section-20"> <div role="rowheader" class="table-section section-40">
{{ __('Name') }} {{ __('Name') }}
</div> </div>
<div role="rowheader" class="table-section section-10"> <div role="rowheader" class="table-section section-10">
...@@ -52,12 +55,12 @@ export default { ...@@ -52,12 +55,12 @@ export default {
<div role="rowheader" class="table-section section-10 text-center"> <div role="rowheader" class="table-section section-10 text-center">
{{ __('Status') }} {{ __('Status') }}
</div> </div>
<div role="rowheader" class="table-section flex-grow-1"> <div role="rowheader" class="table-section section-10">
{{ __('Trace'), }}
</div>
<div role="rowheader" class="table-section section-10 text-right">
{{ __('Duration') }} {{ __('Duration') }}
</div> </div>
<div role="rowheader" class="table-section section-10">
{{ __('Details'), }}
</div>
</div> </div>
<div <div
...@@ -72,7 +75,7 @@ export default { ...@@ -72,7 +75,7 @@ export default {
</div> </div>
</div> </div>
<div class="table-section section-20 section-wrap"> <div class="table-section section-40 section-wrap">
<div role="rowheader" class="table-mobile-header">{{ __('Name') }}</div> <div role="rowheader" class="table-mobile-header">{{ __('Name') }}</div>
<div class="table-mobile-content gl-md-pr-2 gl-overflow-wrap-break"> <div class="table-mobile-content gl-md-pr-2 gl-overflow-wrap-break">
<gl-friendly-wrap :symbols="$options.wrapSymbols" :text="testCase.name" /> <gl-friendly-wrap :symbols="$options.wrapSymbols" :text="testCase.name" />
...@@ -107,24 +110,24 @@ export default { ...@@ -107,24 +110,24 @@ export default {
</div> </div>
</div> </div>
<div class="table-section flex-grow-1">
<div role="rowheader" class="table-mobile-header">{{ __('Trace'), }}</div>
<div class="table-mobile-content">
<pre
v-if="testCase.system_output"
class="build-trace build-trace-rounded text-left"
><code class="bash p-0">{{testCase.system_output}}</code></pre>
</div>
</div>
<div class="table-section section-10 section-wrap"> <div class="table-section section-10 section-wrap">
<div role="rowheader" class="table-mobile-header"> <div role="rowheader" class="table-mobile-header">
{{ __('Duration') }} {{ __('Duration') }}
</div> </div>
<div class="table-mobile-content text-right pr-sm-1"> <div class="table-mobile-content pr-sm-1">
{{ testCase.formattedTime }} {{ testCase.formattedTime }}
</div> </div>
</div> </div>
<div class="table-section section-10 section-wrap">
<div role="rowheader" class="table-mobile-header">{{ __('Details'), }}</div>
<div class="table-mobile-content">
<gl-button v-gl-modal-directive="`test-case-details-${index}`">{{
__('View details')
}}</gl-button>
<test-case-details :modal-id="`test-case-details-${index}`" :test-case="testCase" />
</div>
</div>
</div> </div>
</div> </div>
......
<script> <script>
/* eslint-disable vue/no-v-html */
import { escape } from 'lodash'; import { escape } from 'lodash';
import { GlButton } from '@gitlab/ui'; import { GlSafeHtmlDirective as SafeHtml, GlButton, GlModal, GlModalDirective } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue';
import { s__, sprintf } from '~/locale'; import { s__, sprintf } from '~/locale';
import { deprecatedCreateFlash as Flash } from '~/flash'; import { deprecatedCreateFlash as Flash } from '~/flash';
export default { export default {
components: { components: {
GlModal: DeprecatedModal2, GlModal,
GlButton, GlButton,
}, },
directives: {
GlModalDirective,
SafeHtml,
},
props: { props: {
actionUrl: { actionUrl: {
type: String, type: String,
...@@ -54,6 +56,21 @@ Please update your Git repository remotes as soon as possible.`), ...@@ -54,6 +56,21 @@ Please update your Git repository remotes as soon as possible.`),
false, false,
); );
}, },
primaryProps() {
return {
text: s__('Update username'),
attributes: [
{ variant: 'warning' },
{ category: 'primary' },
{ disabled: this.isRequestPending },
],
};
},
cancelProps() {
return {
text: s__('Cancel'),
};
},
}, },
methods: { methods: {
onConfirm() { onConfirm() {
...@@ -103,22 +120,21 @@ Please update your Git repository remotes as soon as possible.`), ...@@ -103,22 +120,21 @@ Please update your Git repository remotes as soon as possible.`),
<p class="form-text text-muted">{{ path }}</p> <p class="form-text text-muted">{{ path }}</p>
</div> </div>
<gl-button <gl-button
:data-target="`#${$options.modalId}`" v-gl-modal-directive="$options.modalId"
:disabled="isRequestPending || newUsername === username" :disabled="isRequestPending || newUsername === username"
category="primary" category="primary"
variant="warning" variant="warning"
data-toggle="modal" data-testid="username-change-confirmation-modal"
>{{ $options.buttonText }}</gl-button
> >
{{ $options.buttonText }}
</gl-button>
<gl-modal <gl-modal
:id="$options.modalId" :modal-id="$options.modalId"
:header-title-text="s__('Profiles|Change username') + '?'" :title="s__('Profiles|Change username') + '?'"
:footer-primary-button-text="$options.buttonText" :action-primary="primaryProps"
footer-primary-button-variant="warning" :action-cancel="cancelProps"
@submit="onConfirm" @primary="onConfirm"
> >
<span v-html="modalText"></span> <span v-safe-html="modalText"></span>
</gl-modal> </gl-modal>
</div> </div>
</template> </template>
import { LOADING, ERROR, SUCCESS } from '../../constants'; import { LOADING, ERROR, SUCCESS } from '../../constants';
import { sprintf, __, s__, n__ } from '~/locale'; import { sprintf, __, s__, n__ } from '~/locale';
import { spriteIcon } from '~/lib/utils/common_utils';
export const hasCodequalityIssues = state => export const hasCodequalityIssues = state =>
Boolean(state.newIssues?.length || state.resolvedIssues?.length); Boolean(state.newIssues?.length || state.resolvedIssues?.length);
...@@ -48,7 +49,7 @@ export const codequalityPopover = state => { ...@@ -48,7 +49,7 @@ export const codequalityPopover = state => {
s__('ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}'), s__('ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}'),
{ {
linkStartTag: `<a href="${state.helpPath}" target="_blank" rel="noopener noreferrer">`, linkStartTag: `<a href="${state.helpPath}" target="_blank" rel="noopener noreferrer">`,
linkEndTag: '<i class="fa fa-external-link" aria-hidden="true"></i></a>', linkEndTag: `${spriteIcon('external-link', 's16')}</a>`,
}, },
false, false,
), ),
......
...@@ -43,7 +43,15 @@ export default { ...@@ -43,7 +43,15 @@ export default {
return this.filterData.filters.ANY.value; return this.filterData.filters.ANY.value;
}, },
set(filter) { set(filter) {
visitUrl(setUrlParams({ [this.filterData.filterParam]: 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() { selectedFilterText() {
......
...@@ -16,10 +16,6 @@ export default { ...@@ -16,10 +16,6 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
rootPath: {
type: String,
required: true,
},
tooltipPlacement: { tooltipPlacement: {
type: String, type: String,
default: 'bottom', default: 'bottom',
...@@ -76,7 +72,7 @@ export default { ...@@ -76,7 +72,7 @@ export default {
<!-- use d-flex so that slot can be appropriately styled --> <!-- use d-flex so that slot can be appropriately styled -->
<span class="d-flex"> <span class="d-flex">
<assignee-avatar :user="user" :img-size="32" :issuable-type="issuableType" /> <assignee-avatar :user="user" :img-size="32" :issuable-type="issuableType" />
<slot :user="user"></slot> <slot></slot>
</span> </span>
</gl-link> </gl-link>
</template> </template>
...@@ -13,10 +13,6 @@ export default { ...@@ -13,10 +13,6 @@ export default {
type: Array, type: Array,
required: true, required: true,
}, },
rootPath: {
type: String,
required: true,
},
issuableType: { issuableType: {
type: String, type: String,
required: false, required: false,
...@@ -66,22 +62,20 @@ export default { ...@@ -66,22 +62,20 @@ export default {
<template> <template>
<assignee-avatar-link <assignee-avatar-link
v-if="hasOneUser" v-if="hasOneUser"
#default="{ user }"
tooltip-placement="left" tooltip-placement="left"
:tooltip-has-name="false" :tooltip-has-name="false"
:user="firstUser" :user="firstUser"
:root-path="rootPath"
:issuable-type="issuableType" :issuable-type="issuableType"
> >
<div class="ml-2 gl-line-height-normal"> <div class="ml-2 gl-line-height-normal">
<div>{{ user.name }}</div> <div>{{ firstUser.name }}</div>
<div>{{ username }}</div> <div>{{ username }}</div>
</div> </div>
</assignee-avatar-link> </assignee-avatar-link>
<div v-else> <div v-else>
<div class="user-list"> <div class="user-list">
<div v-for="user in uncollapsedUsers" :key="user.id" class="user-item"> <div v-for="user in uncollapsedUsers" :key="user.id" class="user-item">
<assignee-avatar-link :user="user" :root-path="rootPath" :issuable-type="issuableType" /> <assignee-avatar-link :user="user" :issuable-type="issuableType" />
</div> </div>
</div> </div>
<div v-if="renderShowMoreSection" class="user-list-more"> <div v-if="renderShowMoreSection" class="user-list-more">
......
<script> <script>
import { GlForm, GlFormGroup, GlFormInput, GlFormTextarea } from '@gitlab/ui'; import { GlForm, GlFormGroup, GlFormInput, GlFormTextarea } from '@gitlab/ui';
import AccessorUtilities from '~/lib/utils/accessor';
export default { export default {
components: { components: {
...@@ -19,55 +18,25 @@ export default { ...@@ -19,55 +18,25 @@ export default {
required: true, required: true,
}, },
}, },
data() {
return {
editable: {
title: this.title,
description: this.description,
},
};
},
computed: {
editableStorageKey() {
return this.getId('local-storage', 'editable');
},
hasLocalStorage() {
return AccessorUtilities.isLocalStorageAccessSafe();
},
},
mounted() { mounted() {
this.initCachedEditable();
this.preSelect(); this.preSelect();
}, },
methods: { methods: {
getId(type, key) { getId(type, key) {
return `sse-merge-request-meta-${type}-${key}`; return `sse-merge-request-meta-${type}-${key}`;
}, },
initCachedEditable() {
if (this.hasLocalStorage) {
const cachedEditable = JSON.parse(localStorage.getItem(this.editableStorageKey));
if (cachedEditable) {
this.editable = cachedEditable;
}
}
},
preSelect() { preSelect() {
this.$nextTick(() => { this.$nextTick(() => {
this.$refs.title.$el.select(); this.$refs.title.$el.select();
}); });
}, },
resetCachedEditable() { onUpdate(field, value) {
if (this.hasLocalStorage) { const payload = {
window.localStorage.removeItem(this.editableStorageKey); title: this.title,
} description: this.description,
}, [field]: value,
onUpdate() { };
const payload = { ...this.editable };
this.$emit('updateSettings', payload); this.$emit('updateSettings', payload);
if (this.hasLocalStorage) {
window.localStorage.setItem(this.editableStorageKey, JSON.stringify(payload));
}
}, },
}, },
}; };
...@@ -83,9 +52,9 @@ export default { ...@@ -83,9 +52,9 @@ export default {
<gl-form-input <gl-form-input
:id="getId('control', 'title')" :id="getId('control', 'title')"
ref="title" ref="title"
v-model.lazy="editable.title" :value="title"
type="text" type="text"
@input="onUpdate" @input="onUpdate('title', $event)"
/> />
</gl-form-group> </gl-form-group>
...@@ -96,8 +65,8 @@ export default { ...@@ -96,8 +65,8 @@ export default {
> >
<gl-form-textarea <gl-form-textarea
:id="getId('control', 'description')" :id="getId('control', 'description')"
v-model.lazy="editable.description" :value="description"
@input="onUpdate" @input="onUpdate('description', $event)"
/> />
</gl-form-group> </gl-form-group>
</gl-form> </gl-form>
......
<script> <script>
import { GlModal } from '@gitlab/ui'; import { GlModal } from '@gitlab/ui';
import { __, s__, sprintf } from '~/locale'; import { __, s__, sprintf } from '~/locale';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import EditMetaControls from './edit_meta_controls.vue'; import EditMetaControls from './edit_meta_controls.vue';
import { MR_META_LOCAL_STORAGE_KEY } from '../constants';
export default { export default {
components: { components: {
GlModal, GlModal,
EditMetaControls, EditMetaControls,
LocalStorageSync,
}, },
props: { props: {
sourcePath: { sourcePath: {
...@@ -17,6 +21,7 @@ export default { ...@@ -17,6 +21,7 @@ export default {
}, },
data() { data() {
return { return {
clearStorage: false,
mergeRequestMeta: { mergeRequestMeta: {
title: sprintf(s__(`StaticSiteEditor|Update %{sourcePath} file`), { title: sprintf(s__(`StaticSiteEditor|Update %{sourcePath} file`), {
sourcePath: this.sourcePath, sourcePath: this.sourcePath,
...@@ -51,7 +56,7 @@ export default { ...@@ -51,7 +56,7 @@ export default {
}, },
onPrimary() { onPrimary() {
this.$emit('primary', this.mergeRequestMeta); this.$emit('primary', this.mergeRequestMeta);
this.$refs.editMetaControls.resetCachedEditable(); this.clearStorage = true;
}, },
onSecondary() { onSecondary() {
this.hide(); this.hide();
...@@ -60,6 +65,7 @@ export default { ...@@ -60,6 +65,7 @@ export default {
this.mergeRequestMeta = { ...mergeRequestMeta }; this.mergeRequestMeta = { ...mergeRequestMeta };
}, },
}, },
storageKey: MR_META_LOCAL_STORAGE_KEY,
}; };
</script> </script>
...@@ -75,6 +81,12 @@ export default { ...@@ -75,6 +81,12 @@ export default {
@secondary="onSecondary" @secondary="onSecondary"
@hide="() => $emit('hide')" @hide="() => $emit('hide')"
> >
<local-storage-sync
v-model="mergeRequestMeta"
:storage-key="$options.storageKey"
:clear="clearStorage"
as-json
/>
<edit-meta-controls <edit-meta-controls
ref="editMetaControls" ref="editMetaControls"
:title="mergeRequestMeta.title" :title="mergeRequestMeta.title"
......
...@@ -21,3 +21,5 @@ export const TRACKING_ACTION_CREATE_MERGE_REQUEST = 'create_merge_request'; ...@@ -21,3 +21,5 @@ export const TRACKING_ACTION_CREATE_MERGE_REQUEST = 'create_merge_request';
export const TRACKING_ACTION_INITIALIZE_EDITOR = 'initialize_editor'; export const TRACKING_ACTION_INITIALIZE_EDITOR = 'initialize_editor';
export const DEFAULT_IMAGE_UPLOAD_PATH = 'source/images/uploads/'; export const DEFAULT_IMAGE_UPLOAD_PATH = 'source/images/uploads/';
export const MR_META_LOCAL_STORAGE_KEY = 'sse-merge-request-meta-storage-key';
...@@ -108,6 +108,7 @@ export default { ...@@ -108,6 +108,7 @@ export default {
:container="tooltip.container" :container="tooltip.container"
:boundary="tooltip.boundary" :boundary="tooltip.boundary"
:disabled="tooltip.disabled" :disabled="tooltip.disabled"
:show="tooltip.show"
> >
<span v-if="tooltip.html" v-safe-html="tooltip.title"></span> <span v-if="tooltip.html" v-safe-html="tooltip.title"></span>
<span v-else>{{ tooltip.title }}</span> <span v-else>{{ tooltip.title }}</span>
......
...@@ -96,6 +96,12 @@ export const initTooltips = (config = {}) => { ...@@ -96,6 +96,12 @@ export const initTooltips = (config = {}) => {
return invokeBootstrapApi(document.body, config); return invokeBootstrapApi(document.body, config);
}; };
export const add = (elements, config = {}) => {
if (isGlTooltipsEnabled()) {
return addTooltips(elements, config);
}
return invokeBootstrapApi(elements, config);
};
export const dispose = tooltipApiInvoker({ export const dispose = tooltipApiInvoker({
glHandler: element => tooltipsApp().dispose(element), glHandler: element => tooltipsApp().dispose(element),
bsHandler: elements => invokeBootstrapApi(elements, 'dispose'), bsHandler: elements => invokeBootstrapApi(elements, 'dispose'),
......
...@@ -22,11 +22,21 @@ export default { ...@@ -22,11 +22,21 @@ export default {
required: false, required: false,
default: true, default: true,
}, },
clear: {
type: Boolean,
required: false,
default: false,
},
}, },
watch: { watch: {
value(newVal) { value(newVal) {
this.saveValue(this.serialize(newVal)); this.saveValue(this.serialize(newVal));
}, },
clear(newVal) {
if (newVal) {
localStorage.removeItem(this.storageKey);
}
},
}, },
mounted() { mounted() {
// On mount, trigger update if we actually have a localStorageValue // On mount, trigger update if we actually have a localStorageValue
......
...@@ -27,6 +27,10 @@ export default { ...@@ -27,6 +27,10 @@ export default {
RoleDropdown, RoleDropdown,
RemoveGroupLinkModal, RemoveGroupLinkModal,
ExpirationDatepicker, ExpirationDatepicker,
LdapOverrideConfirmationModal: () =>
import(
'ee_component/vue_shared/components/members/ldap/ldap_override_confirmation_modal.vue'
),
}, },
computed: { computed: {
...mapState(['members', 'tableFields']), ...mapState(['members', 'tableFields']),
...@@ -114,5 +118,6 @@ export default { ...@@ -114,5 +118,6 @@ export default {
</template> </template>
</gl-table> </gl-table>
<remove-group-link-modal /> <remove-group-link-modal />
<ldap-override-confirmation-modal />
</div> </div>
</template> </template>
<script> <script>
import $ from 'jquery';
import { GlButton, GlTooltipDirective } from '@gitlab/ui'; import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import Clipboard from 'clipboard'; import Clipboard from 'clipboard';
import { __ } from '~/locale'; import { uniqueId } from 'lodash';
export default { export default {
components: { components: {
...@@ -17,6 +16,11 @@ export default { ...@@ -17,6 +16,11 @@ export default {
required: false, required: false,
default: '', default: '',
}, },
id: {
type: String,
required: false,
default: () => uniqueId('modal-copy-button-'),
},
container: { container: {
type: String, type: String,
required: false, required: false,
...@@ -52,7 +56,6 @@ export default { ...@@ -52,7 +56,6 @@ export default {
default: null, default: null,
}, },
}, },
copySuccessText: __('Copied'),
computed: { computed: {
modalDomId() { modalDomId() {
return this.modalId ? `#${this.modalId}` : ''; return this.modalId ? `#${this.modalId}` : '';
...@@ -68,11 +71,11 @@ export default { ...@@ -68,11 +71,11 @@ export default {
}); });
this.clipboard this.clipboard
.on('success', e => { .on('success', e => {
this.updateTooltip(e.trigger); this.$root.$emit('bv::hide::tooltip', this.id);
this.$emit('success', e); this.$emit('success', e);
// Clear the selection and blur the trigger so it loses its border // Clear the selection and blur the trigger so it loses its border
e.clearSelection(); e.clearSelection();
$(e.trigger).blur(); e.trigger.blur();
}) })
.on('error', e => this.$emit('error', e)); .on('error', e => this.$emit('error', e));
}); });
...@@ -82,29 +85,11 @@ export default { ...@@ -82,29 +85,11 @@ export default {
this.clipboard.destroy(); this.clipboard.destroy();
} }
}, },
methods: {
updateTooltip(target) {
const $target = $(target);
const originalTitle = $target.data('originalTitle');
if ($target.tooltip) {
/**
* The original tooltip will continue staying there unless we remove it by hand.
* $target.tooltip('hide') isn't working.
*/
$('.tooltip').remove();
$target.attr('title', this.$options.copySuccessText);
$target.tooltip('_fixTitle');
$target.tooltip('show');
$target.attr('title', originalTitle);
$target.tooltip('_fixTitle');
}
},
},
}; };
</script> </script>
<template> <template>
<gl-button <gl-button
:id="id"
v-gl-tooltip="{ placement: tooltipPlacement, container: tooltipContainer }" v-gl-tooltip="{ placement: tooltipPlacement, container: tooltipContainer }"
:class="cssClasses" :class="cssClasses"
:data-clipboard-target="target" :data-clipboard-target="target"
......
import createState from 'ee_else_ce/vuex_shared/modules/members/state'; import createState from 'ee_else_ce/vuex_shared/modules/members/state';
import * as actions from './actions'; import mutations from 'ee_else_ce/vuex_shared/modules/members/mutations';
import mutations from './mutations'; import * as actions from 'ee_else_ce/vuex_shared/modules/members/actions';
export default initialState => ({ export default initialState => ({
namespaced: true, namespaced: true,
......
...@@ -101,10 +101,6 @@ ...@@ -101,10 +101,6 @@
content: '\f071'; content: '\f071';
} }
.fa-external-link::before {
content: '\f08e';
}
.fa-spinner::before { .fa-spinner::before {
content: '\f110'; content: '\f110';
} }
......
...@@ -424,7 +424,6 @@ img.emoji { ...@@ -424,7 +424,6 @@ img.emoji {
.w-15p { width: 15%; } .w-15p { width: 15%; }
.w-30p { width: 30%; } .w-30p { width: 30%; }
.w-60p { width: 60%; } .w-60p { width: 60%; }
.w-70p { width: 70%; }
.h-12em { height: 12em; } .h-12em { height: 12em; }
.h-32-px { height: 32px;} .h-32-px { height: 32px;}
......
...@@ -117,12 +117,6 @@ body.modal-open { ...@@ -117,12 +117,6 @@ body.modal-open {
border-bottom-right-radius: $modal-border-radius; border-bottom-right-radius: $modal-border-radius;
} }
} }
@include media-breakpoint-up(sm) {
.modal-dialog {
margin: 64px auto;
}
}
} }
.recaptcha-modal .recaptcha-form { .recaptcha-modal .recaptcha-form {
......
...@@ -453,11 +453,9 @@ ...@@ -453,11 +453,9 @@
h4, h4,
h5, h5,
h6 { h6 {
position: relative;
a.anchor { a.anchor {
left: -16px; float: left;
position: absolute; margin-left: -16px;
text-decoration: none; text-decoration: none;
outline: none; outline: none;
......
...@@ -77,14 +77,6 @@ ...@@ -77,14 +77,6 @@
} }
} }
.issuable-filter-count {
span {
display: block;
margin-bottom: -16px;
padding: 13px 0;
}
}
.issuable-show-labels { .issuable-show-labels {
.gl-label { .gl-label {
margin-bottom: 5px; margin-bottom: 5px;
...@@ -662,12 +654,6 @@ ...@@ -662,12 +654,6 @@
} }
} }
.issuable-form-padding-top {
@include media-breakpoint-up(sm) {
padding-top: 7px;
}
}
.issuable-status-box { .issuable-status-box {
align-self: stretch; align-self: stretch;
display: flex; display: flex;
......
...@@ -67,7 +67,6 @@ ul.related-merge-requests > li { ...@@ -67,7 +67,6 @@ ul.related-merge-requests > li {
} }
} }
.merge-request-ci-status,
.related-merge-requests { .related-merge-requests {
.ci-status-link { .ci-status-link {
display: block; display: block;
...@@ -93,11 +92,6 @@ ul.related-merge-requests > li { ...@@ -93,11 +92,6 @@ ul.related-merge-requests > li {
} }
} }
.issues-footer {
padding-top: $gl-padding;
padding-bottom: 37px;
}
.issues-nav-controls, .issues-nav-controls,
.new-branch-col { .new-branch-col {
font-size: 0; font-size: 0;
......
...@@ -463,8 +463,7 @@ $mr-widget-min-height: 69px; ...@@ -463,8 +463,7 @@ $mr-widget-min-height: 69px;
.mr-list { .mr-list {
.merge-request { .merge-request {
padding: 10px 0 10px 15px; padding: 10px $gl-padding;
position: relative;
display: flex; display: flex;
.issuable-info-container { .issuable-info-container {
...@@ -737,14 +736,6 @@ $mr-widget-min-height: 69px; ...@@ -737,14 +736,6 @@ $mr-widget-min-height: 69px;
border-bottom: 0; border-bottom: 0;
} }
.comments-disabled-notif {
line-height: 28px;
.btn {
margin-left: 5px;
}
}
.mr-version-dropdown, .mr-version-dropdown,
.mr-version-compare-dropdown { .mr-version-compare-dropdown {
margin: 0 7px; margin: 0 7px;
......
...@@ -226,10 +226,6 @@ table { ...@@ -226,10 +226,6 @@ table {
display: none; display: none;
} }
.parallel-comment {
padding: 6px;
}
.error-alert > .alert { .error-alert > .alert {
margin-top: 5px; margin-top: 5px;
margin-bottom: 5px; margin-bottom: 5px;
...@@ -311,31 +307,12 @@ table { ...@@ -311,31 +307,12 @@ table {
} }
} }
.discussion-notes-count {
font-size: 16px;
}
.edit_note {
.markdown-area {
min-height: 140px;
max-height: 500px;
}
.note-form-actions {
background: transparent;
}
}
.comment-toolbar { .comment-toolbar {
padding-top: $gl-padding-top; padding-top: $gl-padding-top;
color: $gl-text-color-secondary; color: $gl-text-color-secondary;
border-top: 1px solid $border-color; border-top: 1px solid $border-color;
} }
.md-helper {
padding-top: 10px;
}
.toolbar-button { .toolbar-button {
padding: 0; padding: 0;
background: none; background: none;
......
...@@ -801,14 +801,6 @@ $note-form-margin-left: 72px; ...@@ -801,14 +801,6 @@ $note-form-margin-left: 72px;
margin: 0 3px; margin: 0 3px;
} }
.note-role-special {
position: relative;
display: inline-block;
color: $gl-text-color-secondary;
font-size: 12px;
text-shadow: 0 0 15px $gl-text-color-inverted;
}
/** /**
* Line note button on the side of diffs * Line note button on the side of diffs
*/ */
......
...@@ -51,43 +51,6 @@ ...@@ -51,43 +51,6 @@
outline: 0; outline: 0;
} }
.flex-users-panel {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
@include media-breakpoint-down(sm) {
display: block;
.flex-project-title {
vertical-align: top;
display: inline-block;
max-width: 90%;
}
}
.flex-project-title {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.badge.badge-pill {
height: 17px;
line-height: 16px;
margin-right: 5px;
padding-top: 1px;
padding-bottom: 1px;
}
.flex-users-form {
flex-wrap: nowrap;
white-space: nowrap;
margin-left: auto;
}
}
.content-list.members-list li { .content-list.members-list li {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
......
...@@ -31,7 +31,7 @@ nav.navbar-collapse.collapse, ...@@ -31,7 +31,7 @@ nav.navbar-collapse.collapse,
.nav, .nav,
.btn, .btn,
ul.notes-form, ul.notes-form,
.merge-request-ci-status .ci-status-link::after, .ci-status-link::after,
.issuable-gutter-toggle, .issuable-gutter-toggle,
.gutter-toggle, .gutter-toggle,
.issuable-details .content-block-small, .issuable-details .content-block-small,
......
...@@ -121,7 +121,7 @@ class ApplicationController < ActionController::Base ...@@ -121,7 +121,7 @@ class ApplicationController < ActionController::Base
end end
def route_not_found def route_not_found
if current_user if current_user || browser.bot.search_engine?
not_found not_found
else else
store_location_for(:user, request.fullpath) unless request.xhr? store_location_for(:user, request.fullpath) unless request.xhr?
......
# frozen_string_literal: true
class JwksController < ActionController::Base # rubocop:disable Rails/ApplicationController
def index
render json: { keys: keys }
end
private
def keys
[
# We keep openid_connect_signing_key so that we can seamlessly
# replace it with ci_jwt_signing_key and remove it on the next release.
# TODO: Remove openid_connect_signing_key in 13.7
# https://gitlab.com/gitlab-org/gitlab/-/issues/221031
Rails.application.secrets.openid_connect_signing_key,
Gitlab::CurrentSettings.ci_jwt_signing_key
].compact.map do |key_data|
OpenSSL::PKey::RSA.new(key_data)
.public_key
.to_jwk
.slice(:kty, :kid, :e, :n)
.merge(use: 'sig', alg: 'RS256')
end
end
end
...@@ -12,7 +12,6 @@ class Projects::PipelinesController < Projects::ApplicationController ...@@ -12,7 +12,6 @@ class Projects::PipelinesController < Projects::ApplicationController
before_action :authorize_create_pipeline!, only: [:new, :create, :config_variables] before_action :authorize_create_pipeline!, only: [:new, :create, :config_variables]
before_action :authorize_update_pipeline!, only: [:retry, :cancel] before_action :authorize_update_pipeline!, only: [:retry, :cancel]
before_action do before_action do
push_frontend_feature_flag(:filter_pipelines_search, project, default_enabled: true)
push_frontend_feature_flag(:dag_pipeline_tab, project, default_enabled: true) push_frontend_feature_flag(:dag_pipeline_tab, project, default_enabled: true)
push_frontend_feature_flag(:pipelines_security_report_summary, project) push_frontend_feature_flag(:pipelines_security_report_summary, project)
push_frontend_feature_flag(:new_pipeline_form, project) push_frontend_feature_flag(:new_pipeline_form, project)
......
...@@ -47,6 +47,8 @@ class Projects::StaticSiteEditorController < Projects::ApplicationController ...@@ -47,6 +47,8 @@ class Projects::StaticSiteEditorController < Projects::ApplicationController
payload.transform_values do |value| payload.transform_values do |value|
if value.is_a?(String) || value.is_a?(Integer) if value.is_a?(String) || value.is_a?(Integer)
value value
elsif value.nil?
''
else else
value.to_json value.to_json
end end
......
...@@ -15,7 +15,7 @@ class ProjectsController < Projects::ApplicationController ...@@ -15,7 +15,7 @@ class ProjectsController < Projects::ApplicationController
around_action :allow_gitaly_ref_name_caching, only: [:index, :show] around_action :allow_gitaly_ref_name_caching, only: [:index, :show]
before_action :whitelist_query_limiting, only: [:create] before_action :whitelist_query_limiting, only: [:create]
before_action :authenticate_user!, except: [:index, :show, :activity, :refs, :resolve] before_action :authenticate_user!, except: [:index, :show, :activity, :refs, :resolve, :unfoldered_environment_names]
before_action :redirect_git_extension, only: [:show] before_action :redirect_git_extension, only: [:show]
before_action :project, except: [:index, :new, :create, :resolve] before_action :project, except: [:index, :new, :create, :resolve]
before_action :repository, except: [:index, :new, :create, :resolve] before_action :repository, except: [:index, :new, :create, :resolve]
......
...@@ -35,6 +35,8 @@ class SearchController < ApplicationController ...@@ -35,6 +35,8 @@ class SearchController < ApplicationController
return unless search_term_valid? return unless search_term_valid?
return if check_single_commit_result?
@search_term = params[:search] @search_term = params[:search]
@scope = search_service.scope @scope = search_service.scope
...@@ -47,8 +49,6 @@ class SearchController < ApplicationController ...@@ -47,8 +49,6 @@ class SearchController < ApplicationController
eager_load_user_status if @scope == 'users' eager_load_user_status if @scope == 'users'
increment_search_counters increment_search_counters
check_single_commit_result
end end
def count def count
...@@ -103,14 +103,23 @@ class SearchController < ApplicationController ...@@ -103,14 +103,23 @@ class SearchController < ApplicationController
@search_objects = @search_objects.eager_load(:status) # rubocop:disable CodeReuse/ActiveRecord @search_objects = @search_objects.eager_load(:status) # rubocop:disable CodeReuse/ActiveRecord
end end
def check_single_commit_result def check_single_commit_result?
if @search_results.single_commit_result? return false if params[:force_search_results]
only_commit = @search_results.objects('commits').first return false unless @project.present?
# download_code project policy grants user the read_commit ability
return false unless Ability.allowed?(current_user, :download_code, @project)
query = params[:search].strip.downcase query = params[:search].strip.downcase
found_by_commit_sha = Commit.valid_hash?(query) && only_commit.sha.start_with?(query) return false unless Commit.valid_hash?(query)
redirect_to project_commit_path(@project, only_commit) if found_by_commit_sha commit = @project.commit_by(oid: query)
end return false unless commit.present?
link = search_path(safe_params.merge(force_search_results: true))
flash[:notice] = html_escape(_("You have been redirected to the only result; see the %{a_start}search results%{a_end} instead.")) % { a_start: "<a href=\"#{link}\"><u>".html_safe, a_end: '</u></a>'.html_safe }
redirect_to project_commit_path(@project, commit)
true
end end
def increment_search_counters def increment_search_counters
......
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
class EnvironmentNamesFinder class EnvironmentNamesFinder
attr_reader :project_or_group, :current_user attr_reader :project_or_group, :current_user
def initialize(project_or_group, current_user) def initialize(project_or_group, current_user = nil)
@project_or_group = project_or_group @project_or_group = project_or_group
@current_user = current_user @current_user = current_user
end end
...@@ -38,7 +38,7 @@ class EnvironmentNamesFinder ...@@ -38,7 +38,7 @@ class EnvironmentNamesFinder
end end
def project_environments def project_environments
if current_user.can?(:read_environment, project_or_group) if Ability.allowed?(current_user, :read_environment, project_or_group)
project_or_group.environments project_or_group.environments
else else
Environment.none Environment.none
......
...@@ -66,6 +66,11 @@ class MergeRequestsFinder < IssuableFinder ...@@ -66,6 +66,11 @@ class MergeRequestsFinder < IssuableFinder
by_source_project_id(items) by_source_project_id(items)
end end
def filter_negated_items(items)
items = super(items)
by_negated_target_branch(items)
end
private private
def by_commit(items) def by_commit(items)
...@@ -98,6 +103,14 @@ class MergeRequestsFinder < IssuableFinder ...@@ -98,6 +103,14 @@ class MergeRequestsFinder < IssuableFinder
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
def by_negated_target_branch(items)
return items unless not_params[:target_branch]
items.where.not(target_branch: not_params[:target_branch])
end
# rubocop: enable CodeReuse/ActiveRecord
def source_project_id def source_project_id
@source_project_id ||= params[:source_project_id].presence @source_project_id ||= params[:source_project_id].presence
end end
......
# frozen_string_literal: true
module Resolvers
module Ci
class RunnerSetupResolver < BaseResolver
type Types::Ci::RunnerSetupType, null: true
argument :platform, GraphQL::STRING_TYPE,
required: true,
description: 'Platform to generate the instructions for'
argument :architecture, GraphQL::STRING_TYPE,
required: true,
description: 'Architecture to generate the instructions for'
argument :project_id, ::Types::GlobalIDType[::Project],
required: false,
description: 'Project to register the runner for'
argument :group_id, ::Types::GlobalIDType[::Group],
required: false,
description: 'Group to register the runner for'
def resolve(platform:, architecture:, **args)
instructions = Gitlab::Ci::RunnerInstructions.new(
{ current_user: current_user, os: platform, arch: architecture }.merge(target_param(args))
)
{
install_instructions: instructions.install_script,
register_instructions: instructions.register_command
}
ensure
raise Gitlab::Graphql::Errors::ResourceNotAvailable, 'User is not authorized to register a runner for the specified resource!' if instructions.errors.include?('Gitlab::Access::AccessDeniedError')
end
private
def target_param(args)
project_param(args[:project_id]) || group_param(args[:group_id]) || {}
end
def project_param(project_id)
return unless project_id
{ project: find_object(project_id) }
end
def group_param(group_id)
return unless group_id
{ group: find_object(group_id) }
end
def find_object(gid)
GlobalID::Locator.locate(gid)
end
end
end
end
# frozen_string_literal: true
module Types
module Ci
# rubocop: disable Graphql/AuthorizeTypes
class RunnerSetupType < BaseObject
graphql_name 'RunnerSetup'
field :install_instructions, GraphQL::STRING_TYPE, null: false,
description: 'Instructions for installing the runner on the specified architecture'
field :register_instructions, GraphQL::STRING_TYPE, null: false,
description: 'Instructions for registering the runner'
end
end
end
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
module Types module Types
# rubocop: disable Graphql/AuthorizeTypes # rubocop: disable Graphql/AuthorizeTypes
class CountableConnectionType < GraphQL::Types::Relay::BaseConnection class CountableConnectionType < GraphQL::Types::Relay::BaseConnection
field :count, Integer, null: false, field :count, GraphQL::INT_TYPE, null: false,
description: 'Total count of collection' description: 'Total count of collection'
def count def count
......
# frozen_string_literal: true
module Types
# rubocop: disable Graphql/AuthorizeTypes
class IssueConnectionType < CountableConnectionType
end
end
Types::IssueConnectionType.prepend_if_ee('::EE::Types::IssueConnectionType')
...@@ -4,7 +4,7 @@ module Types ...@@ -4,7 +4,7 @@ module Types
class IssueType < BaseObject class IssueType < BaseObject
graphql_name 'Issue' graphql_name 'Issue'
connection_type_class(Types::CountableConnectionType) connection_type_class(Types::IssueConnectionType)
implements(Types::Notes::NoteableType) implements(Types::Notes::NoteableType)
implements(Types::CurrentUserTodos) implements(Types::CurrentUserTodos)
...@@ -74,6 +74,10 @@ module Types ...@@ -74,6 +74,10 @@ module Types
description: 'Time estimate of the issue' description: 'Time estimate of the issue'
field :total_time_spent, GraphQL::INT_TYPE, null: false, field :total_time_spent, GraphQL::INT_TYPE, null: false,
description: 'Total time reported as spent on the issue' description: 'Total time reported as spent on the issue'
field :human_time_estimate, GraphQL::STRING_TYPE, null: true,
description: 'Human-readable time estimate of the issue'
field :human_total_time_spent, GraphQL::STRING_TYPE, null: true,
description: 'Human-readable total time reported as spent on the issue'
field :closed_at, Types::TimeType, null: true, field :closed_at, Types::TimeType, null: true,
description: 'Timestamp of when the issue was closed' description: 'Timestamp of when the issue was closed'
......
...@@ -84,6 +84,10 @@ module Types ...@@ -84,6 +84,10 @@ module Types
null: true, description: 'Supported runner platforms', null: true, description: 'Supported runner platforms',
resolver: Resolvers::Ci::RunnerPlatformsResolver resolver: Resolvers::Ci::RunnerPlatformsResolver
field :runner_setup, Types::Ci::RunnerSetupType, null: true,
description: 'Get runner setup instructions',
resolver: Resolvers::Ci::RunnerSetupResolver
def design_management def design_management
DesignManagementObject.new(nil) DesignManagementObject.new(nil)
end end
......
...@@ -196,7 +196,7 @@ module CommitsHelper ...@@ -196,7 +196,7 @@ module CommitsHelper
return unless external_url return unless external_url
link_to(external_url, class: 'btn btn-file-option has-tooltip', target: '_blank', rel: 'noopener noreferrer', title: "View on #{environment.formatted_external_url}", data: { container: 'body' }) do link_to(external_url, class: 'btn btn-file-option has-tooltip', target: '_blank', rel: 'noopener noreferrer', title: "View on #{environment.formatted_external_url}", data: { container: 'body' }) do
icon('external-link') sprite_icon('external-link')
end end
end end
......
...@@ -129,8 +129,7 @@ module DropdownsHelper ...@@ -129,8 +129,7 @@ module DropdownsHelper
end end
def dropdown_loading def dropdown_loading
content_tag :div, class: "dropdown-loading" do spinner = loading_icon(container: true, size: "md", css_class: "gl-mt-7")
icon('spinner spin') content_tag(:div, spinner, class: "dropdown-loading")
end
end end
end end
...@@ -89,15 +89,6 @@ module IconsHelper ...@@ -89,15 +89,6 @@ module IconsHelper
sprite_icon(name, css_class: css_class) sprite_icon(name, css_class: css_class)
end end
def spinner(text = nil, visible = false)
css_class = ['loading']
css_class << 'hide' unless visible
content_tag :div, class: css_class.join(' ') do
icon('spinner spin') + text
end
end
def boolean_to_icon(value) def boolean_to_icon(value)
if value if value
sprite_icon('check', css_class: 'cgreen') sprite_icon('check', css_class: 'cgreen')
......
# frozen_string_literal: true # frozen_string_literal: true
module SearchHelper module SearchHelper
SEARCH_PERMITTED_PARAMS = [:search, :scope, :project_id, :group_id, :repository_ref, :snippets, :sort, :state, :confidential].freeze SEARCH_PERMITTED_PARAMS = [
:search,
:scope,
:project_id,
:group_id,
:repository_ref,
:snippets,
:sort,
:state,
:confidential,
:force_search_results
].freeze
def search_autocomplete_opts(term) def search_autocomplete_opts(term)
return unless current_user return unless current_user
...@@ -9,7 +20,8 @@ module SearchHelper ...@@ -9,7 +20,8 @@ module SearchHelper
resources_results = [ resources_results = [
recent_items_autocomplete(term), recent_items_autocomplete(term),
groups_autocomplete(term), groups_autocomplete(term),
projects_autocomplete(term) projects_autocomplete(term),
issue_autocomplete(term)
].flatten ].flatten
search_pattern = Regexp.new(Regexp.escape(term), "i") search_pattern = Regexp.new(Regexp.escape(term), "i")
...@@ -172,6 +184,24 @@ module SearchHelper ...@@ -172,6 +184,24 @@ module SearchHelper
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
def issue_autocomplete(term)
return [] unless @project.present? && current_user && term =~ /\A#{Issue.reference_prefix}\d+\z/
iid = term.sub(Issue.reference_prefix, '').to_i
issue = @project.issues.find_by_iid(iid)
return [] unless issue && Ability.allowed?(current_user, :read_issue, issue)
[
{
category: 'In this project',
id: issue.id,
label: search_result_sanitize("#{issue.title} (#{issue.to_reference})"),
url: issue_path(issue),
avatar_url: issue.project.avatar_url || ''
}
]
end
# Autocomplete results for the current user's projects # Autocomplete results for the current user's projects
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def projects_autocomplete(term, limit = 5) def projects_autocomplete(term, limit = 5)
......
...@@ -35,13 +35,6 @@ module ServicesHelper ...@@ -35,13 +35,6 @@ module ServicesHelper
"#{event}_events" "#{event}_events"
end end
def service_save_button(disabled: false)
button_tag(class: 'btn btn-success', type: 'submit', disabled: disabled, data: { qa_selector: 'save_changes_button' }) do
icon('spinner spin', class: 'hidden js-btn-spinner') +
content_tag(:span, 'Save changes', class: 'js-btn-label')
end
end
def scoped_integrations_path def scoped_integrations_path
if @project.present? if @project.present?
project_settings_integrations_path(@project) project_settings_integrations_path(@project)
......
...@@ -9,7 +9,6 @@ class ApplicationSetting < ApplicationRecord ...@@ -9,7 +9,6 @@ class ApplicationSetting < ApplicationRecord
ignore_column :namespace_storage_size_limit, remove_with: '13.5', remove_after: '2020-09-22' ignore_column :namespace_storage_size_limit, remove_with: '13.5', remove_after: '2020-09-22'
ignore_column :instance_statistics_visibility_private, remove_with: '13.6', remove_after: '2020-10-22' ignore_column :instance_statistics_visibility_private, remove_with: '13.6', remove_after: '2020-10-22'
ignore_column :snowplow_iglu_registry_url, remove_with: '13.6', remove_after: '2020-11-22'
INSTANCE_REVIEW_MIN_USERS = 50 INSTANCE_REVIEW_MIN_USERS = 50
GRAFANA_URL_ERROR_MESSAGE = 'Please check your Grafana URL setting in ' \ GRAFANA_URL_ERROR_MESSAGE = 'Please check your Grafana URL setting in ' \
...@@ -385,6 +384,9 @@ class ApplicationSetting < ApplicationRecord ...@@ -385,6 +384,9 @@ class ApplicationSetting < ApplicationRecord
validates :raw_blob_request_limit, validates :raw_blob_request_limit,
numericality: { only_integer: true, greater_than_or_equal_to: 0 } numericality: { only_integer: true, greater_than_or_equal_to: 0 }
validates :ci_jwt_signing_key,
rsa_key: true, allow_nil: true
attr_encrypted :asset_proxy_secret_key, attr_encrypted :asset_proxy_secret_key,
mode: :per_attribute_iv, mode: :per_attribute_iv,
key: Settings.attr_encrypted_db_key_base_truncated, key: Settings.attr_encrypted_db_key_base_truncated,
...@@ -410,6 +412,7 @@ class ApplicationSetting < ApplicationRecord ...@@ -410,6 +412,7 @@ class ApplicationSetting < ApplicationRecord
attr_encrypted :recaptcha_site_key, encryption_options_base_truncated_aes_256_gcm attr_encrypted :recaptcha_site_key, encryption_options_base_truncated_aes_256_gcm
attr_encrypted :slack_app_secret, encryption_options_base_truncated_aes_256_gcm attr_encrypted :slack_app_secret, encryption_options_base_truncated_aes_256_gcm
attr_encrypted :slack_app_verification_token, encryption_options_base_truncated_aes_256_gcm attr_encrypted :slack_app_verification_token, encryption_options_base_truncated_aes_256_gcm
attr_encrypted :ci_jwt_signing_key, encryption_options_base_truncated_aes_256_gcm
before_validation :ensure_uuid! before_validation :ensure_uuid!
......
# frozen_string_literal: true # frozen_string_literal: true
# The BulkImport import model links together all the models required to for a # The BulkImport model links all models required for a bulk import of groups and
# bulk import of groups and projects to a GitLab instance, and associates these # projects to a GitLab instance. It associates the import with the responsible
# with the user that initiated the import. # user.
class BulkImport < ApplicationRecord class BulkImport < ApplicationRecord
belongs_to :user, optional: false belongs_to :user, optional: false
......
# frozen_string_literal: true # frozen_string_literal: true
# The BulkImport::Entity represents a Group or Project that is going to be # The BulkImport::Entity represents a Group or Project to be imported during the
# imported during the bulk import process. An entity is nested under the a # bulk import process. An entity is nested under the parent group when it is not
# parent group when it is not a top level group. # a top level group.
# #
# A full bulk import entity structure might look like this, where the links are # A full bulk import entity structure might look like this, where the links are
# parents: # parents:
...@@ -15,8 +15,8 @@ ...@@ -15,8 +15,8 @@
# | | # | |
# ProjectEntity Project # ProjectEntity Project
# #
# The tree structure of the entities will result in the same structure for the # The tree structure of the entities results in the same structure for imported
# imported Groups and Projects. # Groups and Projects.
class BulkImports::Entity < ApplicationRecord class BulkImports::Entity < ApplicationRecord
self.table_name = 'bulk_import_entities' self.table_name = 'bulk_import_entities'
......
...@@ -1059,7 +1059,7 @@ module Ci ...@@ -1059,7 +1059,7 @@ module Ci
jwt = Gitlab::Ci::Jwt.for_build(self) jwt = Gitlab::Ci::Jwt.for_build(self)
variables.append(key: 'CI_JOB_JWT', value: jwt, public: false, masked: true) variables.append(key: 'CI_JOB_JWT', value: jwt, public: false, masked: true)
rescue OpenSSL::PKey::RSAError => e rescue OpenSSL::PKey::RSAError, Gitlab::Ci::Jwt::NoSigningKeyError => e
Gitlab::ErrorTracking.track_exception(e) Gitlab::ErrorTracking.track_exception(e)
end end
end end
......
...@@ -205,13 +205,8 @@ class CommitStatus < ApplicationRecord ...@@ -205,13 +205,8 @@ class CommitStatus < ApplicationRecord
# 'rspec:linux: 1/10' => 'rspec:linux' # 'rspec:linux: 1/10' => 'rspec:linux'
common_name = name.to_s.gsub(%r{\d+[\s:\/\\]+\d+\s*}, '') common_name = name.to_s.gsub(%r{\d+[\s:\/\\]+\d+\s*}, '')
if ::Gitlab::Ci::Features.one_dimensional_matrix_enabled?
# 'rspec:linux: [aws, max memory]' => 'rspec:linux', 'rspec:linux: [aws]' => 'rspec:linux' # 'rspec:linux: [aws, max memory]' => 'rspec:linux', 'rspec:linux: [aws]' => 'rspec:linux'
common_name.gsub!(%r{: \[.*\]\s*\z}, '') common_name.gsub!(%r{: \[.*\]\s*\z}, '')
else
# 'rspec:linux: [aws, max memory]' => 'rspec:linux', 'rspec:linux: [aws]' => 'rspec:linux: [aws]'
common_name.gsub!(%r{: \[.*, .*\]\s*\z}, '')
end
common_name.strip! common_name.strip!
common_name common_name
......
...@@ -21,6 +21,7 @@ class GroupMember < Member ...@@ -21,6 +21,7 @@ class GroupMember < Member
scope :of_groups, ->(groups) { where(source_id: groups.select(:id)) } scope :of_groups, ->(groups) { where(source_id: groups.select(:id)) }
scope :of_ldap_type, -> { where(ldap: true) } scope :of_ldap_type, -> { where(ldap: true) }
scope :count_users_by_group_id, -> { group(:source_id).count } scope :count_users_by_group_id, -> { group(:source_id).count }
scope :with_user, -> (user) { where(user: user) }
after_create :update_two_factor_requirement, unless: :invite? after_create :update_two_factor_requirement, unless: :invite?
after_destroy :update_two_factor_requirement, unless: :invite? after_destroy :update_two_factor_requirement, unless: :invite?
......
...@@ -284,7 +284,8 @@ class Namespace < ApplicationRecord ...@@ -284,7 +284,8 @@ class Namespace < ApplicationRecord
# that belongs to this namespace # that belongs to this namespace
def all_projects def all_projects
if Feature.enabled?(:recursive_approach_for_all_projects) if Feature.enabled?(:recursive_approach_for_all_projects)
Project.where(namespace: self_and_descendants) namespace = user? ? self : self_and_descendants
Project.where(namespace: namespace)
else else
Project.inside_path(full_path) Project.inside_path(full_path)
end end
......
# frozen_string_literal: true
module Packages
module Debian
# Returns .deb file metadata
class ExtractDebMetadataService
CommandFailedError = Class.new(StandardError)
def initialize(file_path)
@file_path = file_path
end
def execute
unless success?
raise CommandFailedError, "The `#{cmd}` command failed (status: #{result.status}) with the following error:\n#{result.stderr}"
end
sections = ParseDebian822Service.new(result.stdout).execute
sections.each_value.first
end
private
def cmd
@cmd ||= begin
dpkg_deb_path = Gitlab.config.packages.dpkg_deb_path
[dpkg_deb_path, '--field', @file_path]
end
end
def result
@result ||= Gitlab::Popen.popen_with_detail(cmd)
end
def success?
result.status&.exitstatus == 0
end
end
end
end
# frozen_string_literal: true
module Packages
module Debian
# Parse String as Debian RFC822 control data format
# https://manpages.debian.org/unstable/dpkg-dev/deb822.5
class ParseDebian822Service
InvalidDebian822Error = Class.new(StandardError)
def initialize(input)
@input = input
end
def execute
output = {}
@input.each_line('', chomp: true) do |block|
section = {}
section_name, field = nil
block.each_line(chomp: true) do |line|
next if comment_line?(line)
if continuation_line?(line)
raise InvalidDebian822Error, "Parse error. Unexpected continuation line" if field.nil?
section[field] += "\n"
section[field] += line[1..] unless paragraph_separator?(line)
elsif match = match_section_line(line)
section_name = match[:name] if section_name.nil?
field = match[:field].to_sym
raise InvalidDebian822Error, "Duplicate field '#{field}' in section '#{section_name}'" if section.include?(field)
section[field] = match[:value]
else
raise InvalidDebian822Error, "Parse error on line #{line}"
end
end
raise InvalidDebian822Error, "Duplicate section '#{section_name}'" if output[section_name]
output[section_name] = section
end
output
end
private
def comment_line?(line)
line.match?(/^#/)
end
def continuation_line?(line)
line.match?(/^ /)
end
def paragraph_separator?(line)
line == ' .'
end
def match_section_line(line)
line.match(/(?<name>(?<field>^\S+):\s*(?<value>.*))/)
end
end
end
end
...@@ -62,7 +62,9 @@ class SearchService ...@@ -62,7 +62,9 @@ class SearchService
end end
def search_objects(preload_method = nil) def search_objects(preload_method = nil)
@search_objects ||= redact_unauthorized_results(search_results.objects(scope, page: params[:page], per_page: per_page, preload_method: preload_method)) @search_objects ||= redact_unauthorized_results(
search_results.objects(scope, page: page, per_page: per_page, preload_method: preload_method)
)
end end
def search_highlight def search_highlight
...@@ -71,6 +73,10 @@ class SearchService ...@@ -71,6 +73,10 @@ class SearchService
private private
def page
[1, params[:page].to_i].max
end
def per_page def per_page
per_page_param = params[:per_page].to_i per_page_param = params[:per_page].to_i
......
# frozen_string_literal: true
# RsaKeyValidator
#
# Custom validator for RSA private keys.
#
# class Project < ActiveRecord::Base
# validates :signing_key, rsa_key: true
# end
#
class RsaKeyValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
unless valid_rsa_key?(value)
record.errors.add(attribute, "is not a valid RSA key")
end
end
private
def valid_rsa_key?(value)
return false unless value
OpenSSL::PKey::RSA.new(value)
rescue OpenSSL::PKey::RSAError
false
end
end
...@@ -14,4 +14,4 @@ ...@@ -14,4 +14,4 @@
'kubernetes-integration-help-path' => help_page_path('user/project/clusters/index'), 'kubernetes-integration-help-path' => help_page_path('user/project/clusters/index'),
'account-and-external-ids-help-path' => help_page_path('user/project/clusters/add_remove_clusters.md', anchor: 'new-eks-cluster'), 'account-and-external-ids-help-path' => help_page_path('user/project/clusters/add_remove_clusters.md', anchor: 'new-eks-cluster'),
'create-role-arn-help-path' => help_page_path('user/project/clusters/add_remove_clusters.md', anchor: 'new-eks-cluster'), 'create-role-arn-help-path' => help_page_path('user/project/clusters/add_remove_clusters.md', anchor: 'new-eks-cluster'),
'external-link-icon' => icon('external-link') } } 'external-link-icon' => sprite_icon('external-link') } }
= javascript_include_tag 'https://apis.google.com/js/api.js' = javascript_include_tag 'https://apis.google.com/js/api.js'
- external_link_icon = icon('external-link') - external_link_icon = sprite_icon('external-link')
- zones_link_url = 'https://cloud.google.com/compute/docs/regions-zones/regions-zones' - zones_link_url = 'https://cloud.google.com/compute/docs/regions-zones/regions-zones'
- machine_type_link_url = 'https://cloud.google.com/compute/docs/machine-types' - machine_type_link_url = 'https://cloud.google.com/compute/docs/machine-types'
- pricing_link_url = 'https://cloud.google.com/compute/pricing#machinetype' - pricing_link_url = 'https://cloud.google.com/compute/pricing#machinetype'
......
- @hide_top_links = true - @hide_top_links = true
- page_title _("Projects") - page_title _("Projects")
- header_title _("Projects"), dashboard_projects_path - header_title _("Projects"), dashboard_projects_path
- page_canonical_link explore_projects_url
= render_dashboard_gold_trial(current_user) = render_dashboard_gold_trial(current_user)
......
...@@ -272,7 +272,6 @@ ...@@ -272,7 +272,6 @@
.feature-highlight.js-feature-highlight{ disabled: true, .feature-highlight.js-feature-highlight{ disabled: true,
data: { trigger: 'manual', data: { trigger: 'manual',
container: 'body', container: 'body',
toggle: 'popover',
placement: 'right', placement: 'right',
highlight: UserCalloutsHelper::GKE_CLUSTER_INTEGRATION, highlight: UserCalloutsHelper::GKE_CLUSTER_INTEGRATION,
highlight_priority: UserCallout.feature_names[:GKE_CLUSTER_INTEGRATION], highlight_priority: UserCallout.feature_names[:GKE_CLUSTER_INTEGRATION],
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
= succeed ':' do = succeed ':' do
= link_to note.author_name, user_url(note.author) = link_to note.author_name, user_url(note.author)
- if discussion.nil? - if discussion.nil?
commented = link_to 'commented', target_url
- else - else
- if note.start_of_discussion? - if note.start_of_discussion?
started a new started a new
......
...@@ -34,4 +34,5 @@ ...@@ -34,4 +34,5 @@
%div{ id: dom_id(@project) } %div{ id: dom_id(@project) }
%ol#commits-list.list-unstyled.content_list %ol#commits-list.list-unstyled.content_list
= render 'commits', project: @project, ref: @ref = render 'commits', project: @project, ref: @ref
= spinner .loading.hide
= loading_icon(size: "lg")
...@@ -37,10 +37,10 @@ ...@@ -37,10 +37,10 @@
.detail-page-header-actions.js-issuable-actions.js-issuable-buttons{ data: { "action": "close-reopen" } } .detail-page-header-actions.js-issuable-actions.js-issuable-buttons{ data: { "action": "close-reopen" } }
.clearfix.issue-btn-group.dropdown .clearfix.issue-btn-group.dropdown
%button.btn.btn-default.float-left.d-md-none.d-lg-none.d-xl-none{ type: "button", data: { toggle: "dropdown" } } %button.btn.btn-default.float-left.d-md-none{ type: "button", data: { toggle: "dropdown" } }
Options Options
= icon('caret-down') = icon('caret-down')
.dropdown-menu.dropdown-menu-right.d-lg-none.d-xl-none .dropdown-menu.dropdown-menu-right.d-lg-none
%ul %ul
- unless current_user == @issue.author - unless current_user == @issue.author
%li= link_to 'Report abuse', new_abuse_report_path(user_id: @issue.author.id, ref_url: issue_url(@issue)) %li= link_to 'Report abuse', new_abuse_report_path(user_id: @issue.author.id, ref_url: issue_url(@issue))
...@@ -58,9 +58,9 @@ ...@@ -58,9 +58,9 @@
= render 'shared/issuable/close_reopen_button', issuable: @issue, can_update: can_update_issue, can_reopen: can_reopen_issue, warn_before_close: defined?(@issue.blocked?) && @issue.blocked? = render 'shared/issuable/close_reopen_button', issuable: @issue, can_update: can_update_issue, can_reopen: can_reopen_issue, warn_before_close: defined?(@issue.blocked?) && @issue.blocked?
- if can_report_spam - if can_report_spam
= link_to 'Submit as spam', mark_as_spam_project_issue_path(@project, @issue), method: :post, class: 'd-none d-sm-none d-md-block gl-button btn btn-grouped btn-spam', title: 'Submit as spam' = link_to 'Submit as spam', mark_as_spam_project_issue_path(@project, @issue), method: :post, class: 'd-none d-md-block gl-button btn btn-grouped btn-spam', title: 'Submit as spam'
- if can_create_issue - if can_create_issue
= link_to new_project_issue_path(@project), class: 'd-none d-sm-none d-md-block gl-button btn btn-grouped btn-success btn-inverted', title: 'New issue', id: 'new_issue_link' do = link_to new_project_issue_path(@project), class: 'd-none d-md-block gl-button btn btn-grouped btn-success btn-inverted', title: 'New issue', id: 'new_issue_link' do
New issue New issue
.issue-details.issuable-details .issue-details.issuable-details
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
To install this service, To install this service,
= link_to "#{Gitlab.config.mattermost.host}/select_team", target: '__blank' do = link_to "#{Gitlab.config.mattermost.host}/select_team", target: '__blank' do
join a team join a team
= icon('external-link') = sprite_icon('external-link')
and try again. and try again.
%hr %hr
.clearfix .clearfix
......
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
To create a team, To create a team,
= link_to "#{Gitlab.config.mattermost.host}/create_team" do = link_to "#{Gitlab.config.mattermost.host}/create_team" do
use Mattermost's interface use Mattermost's interface
= icon('external-link') = sprite_icon('external-link')
or ask your Mattermost system administrator. or ask your Mattermost system administrator.
%hr %hr
%h4 Command trigger word %h4 Command trigger word
...@@ -38,7 +38,7 @@ ...@@ -38,7 +38,7 @@
Reserved: Reserved:
= link_to 'https://docs.mattermost.com/help/messaging/executing-commands.html#built-in-commands', target: '__blank' do = link_to 'https://docs.mattermost.com/help/messaging/executing-commands.html#built-in-commands', target: '__blank' do
see list of built-in slash commands see list of built-in slash commands
= icon('external-link') = sprite_icon('external-link')
%hr %hr
.clearfix .clearfix
.float-right .float-right
......
---
title: Apply GitLab UI button styles to buttons in app/views/invites directory
merge_request: 44289
author: Lakshit
type: other
---
title: Apply GitLab UI button styles to buttons in app/views/admin/jobs directory
merge_request: 44291
author: Lakshit
type: other
---
title: Apply GitLab UI button styles to buttons in app/views/projects/services/mattermost_slash_commands
merge_request: 44293
author: Lakshit
type: other
---
title: Apply gl-button class to projects/issues/export_csv directory
merge_request: 44106
author: Lakshit
type: other
---
title: Apply GitLab UI button styles to buttons in app/views/projects/commits directory
merge_request: 44331
author: Lakshit
type: other
---
title: Apply GitLab UI button styles to buttons in app/views/projects/compare directory
merge_request: 44342
author: Lakshit
type: other
---
title: Apply GitLab UI button styles to buttons in app/views/sherlock/file_samples
merge_request: 44109
author: Lakshit
type: other
---
title: Apply GitLab UI button styles to buttons in app/views/shared/wikis directory
merge_request: 44338
author: Lakshit
type: other
---
title: Add gl-button class to app/views/projects/deployments
merge_request: 44203
author: Lakshit
type: other
---
title: Fix the maven md5 upload endpoint
merge_request: 45271
author:
type: fixed
---
title: Add file name column to CI unit test report
merge_request: 43338
author:
type: added
---
title: Fix displaying a message when design copying is in progress
merge_request: 43749
author:
type: fixed
---
title: Copy designs to new issue when issue is moved
merge_request: 41714
author:
type: added
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment