Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-12-07 15:15:03 +00:00
parent 6dd9e3644e
commit 3a0f6ebaa9
91 changed files with 308 additions and 180 deletions

View File

@ -1,5 +1,6 @@
<script>
import { GlAlert, GlButton, GlLoadingIcon, GlTable, GlTooltipDirective } from '@gitlab/ui';
import { parseBoolean } from '~/lib/utils/common_utils';
import { s__, __ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import getGroupContactsQuery from './queries/get_group_contacts.query.graphql';
@ -21,7 +22,6 @@ export default {
return {
contacts: [],
error: false,
errorMessages: [],
};
},
apollo: {
@ -49,6 +49,9 @@ export default {
showNewForm() {
return this.$route.path.startsWith('/new');
},
canCreateNew() {
return parseBoolean(this.canAdminCrmContact);
},
},
methods: {
extractContacts(data) {
@ -60,17 +63,11 @@ export default {
this.$router.push({ path: '/new' });
},
hideNewForm() {
hideNewForm(success) {
if (success) this.$toast.show(s__('Crm|Contact has been added'));
this.$router.replace({ path: '/' });
},
handleError(errors) {
this.error = true;
if (errors) this.errorMessages = errors;
},
dismissError() {
this.error = false;
this.errorMessages = [];
},
getIssuesPath(path, value) {
return `${path}?scope=all&state=opened&crm_contact_id=${value}`;
},
@ -108,9 +105,8 @@ export default {
<template>
<div>
<gl-alert v-if="error" variant="danger" class="gl-mt-6" @dismiss="dismissError">
<div v-if="errorMessages.length == 0">{{ $options.i18n.errorText }}</div>
<div v-for="(message, index) in errorMessages" :key="index">{{ message }}</div>
<gl-alert v-if="error" variant="danger" class="gl-mt-6" @dismiss="error = false">
{{ $options.i18n.errorText }}
</gl-alert>
<div
class="gl-display-flex gl-align-items-baseline gl-flex-direction-row gl-justify-content-space-between gl-mt-6"
@ -120,7 +116,7 @@ export default {
</h2>
<div class="gl-display-none gl-md-display-flex gl-align-items-center gl-justify-content-end">
<gl-button
v-if="canAdminCrmContact"
v-if="canCreateNew"
variant="confirm"
data-testid="new-contact-button"
@click="displayNewForm"
@ -129,7 +125,7 @@ export default {
</gl-button>
</div>
</div>
<new-contact-form v-if="showNewForm" @close="hideNewForm" @error="handleError" />
<new-contact-form v-if="showNewForm" :drawer-open="showNewForm" @close="hideNewForm" />
<gl-loading-icon v-if="isLoading" class="gl-mt-5" size="lg" />
<gl-table
v-else

View File

@ -1,5 +1,5 @@
<script>
import { GlButton, GlFormGroup, GlFormInput } from '@gitlab/ui';
import { GlAlert, GlButton, GlDrawer, GlFormGroup, GlFormInput } from '@gitlab/ui';
import { produce } from 'immer';
import { __, s__ } from '~/locale';
import { convertToGraphQLId } from '~/graphql_shared/utils';
@ -9,11 +9,19 @@ import getGroupContactsQuery from './queries/get_group_contacts.query.graphql';
export default {
components: {
GlAlert,
GlButton,
GlDrawer,
GlFormGroup,
GlFormInput,
},
inject: ['groupFullPath', 'groupId'],
props: {
drawerOpen: {
type: Boolean,
required: true,
},
},
data() {
return {
firstName: '',
@ -22,6 +30,7 @@ export default {
email: '',
description: '',
submitting: false,
errorMessages: [],
};
},
computed: {
@ -48,24 +57,21 @@ export default {
update: this.updateCache,
})
.then(({ data }) => {
if (data.customerRelationsContactCreate.errors.length === 0) this.close();
if (data.customerRelationsContactCreate.errors.length === 0) this.close(true);
this.submitting = false;
})
.catch(() => {
this.error();
this.errorMessages = [__('Something went wrong. Please try again.')];
this.submitting = false;
});
},
close() {
this.$emit('close');
},
error(errors = null) {
this.$emit('error', errors);
close(success) {
this.$emit('close', success);
},
updateCache(store, { data: { customerRelationsContactCreate } }) {
if (customerRelationsContactCreate.errors.length > 0) {
this.error(customerRelationsContactCreate.errors);
this.errorMessages = customerRelationsContactCreate.errors;
return;
}
@ -90,6 +96,15 @@ export default {
data,
});
},
getDrawerHeaderHeight() {
const wrapperEl = document.querySelector('.content-wrapper');
if (wrapperEl) {
return `${wrapperEl.offsetTop}px`;
}
return '';
},
},
i18n: {
buttonLabel: s__('Crm|Create new contact'),
@ -99,12 +114,28 @@ export default {
email: s__('Crm|Email'),
phone: s__('Crm|Phone number (optional)'),
description: s__('Crm|Description (optional)'),
title: s__('Crm|New Contact'),
},
};
</script>
<template>
<div class="col-md-4">
<gl-drawer
class="gl-drawer-responsive"
:open="drawerOpen"
:header-height="getDrawerHeaderHeight()"
@close="close(false)"
>
<template #title>
<h4>{{ $options.i18n.title }}</h4>
</template>
<gl-alert v-if="errorMessages.length" variant="danger" @dismiss="errorMessages = []">
<ul class="gl-mb-0! gl-ml-5">
<li v-for="error in errorMessages" :key="error">
{{ error }}
</li>
</ul>
</gl-alert>
<form @submit.prevent="save">
<gl-form-group :label="$options.i18n.firstName" label-for="contact-first-name">
<gl-form-input id="contact-first-name" v-model="firstName" />
@ -121,7 +152,10 @@ export default {
<gl-form-group :label="$options.i18n.description" label-for="contact-description">
<gl-form-input id="contact-description" v-model="description" />
</gl-form-group>
<div class="form-actions">
<span class="gl-float-right">
<gl-button data-testid="cancel-button" @click="close(false)">
{{ $options.i18n.cancel }}
</gl-button>
<gl-button
variant="confirm"
:disabled="invalid"
@ -130,11 +164,7 @@ export default {
type="submit"
>{{ $options.i18n.buttonLabel }}</gl-button
>
<gl-button data-testid="cancel-button" @click="close">
{{ $options.i18n.cancel }}
</gl-button>
</div>
</span>
</form>
<div class="gl-pb-5"></div>
</div>
</gl-drawer>
</template>

View File

@ -1,3 +1,4 @@
import { GlToast } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import VueRouter from 'vue-router';
@ -6,6 +7,7 @@ import CrmContactsRoot from './components/contacts_root.vue';
Vue.use(VueApollo);
Vue.use(VueRouter);
Vue.use(GlToast);
export default () => {
const el = document.getElementById('js-crm-contacts-app');

View File

@ -4,7 +4,7 @@ import $ from 'jquery';
import { property } from 'lodash';
import issueableEventHub from '~/issues_list/eventhub';
import LabelsSelect from '~/labels_select';
import LabelsSelect from '~/labels/labels_select';
import MilestoneSelect from '~/milestones/milestone_select';
import initIssueStatusSelect from './init_issue_status_select';
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';

View File

@ -1,8 +1,8 @@
/* eslint-disable no-new */
import { getSidebarOptions } from '~/sidebar/mount_sidebar';
import IssuableContext from './issuable_context';
import Sidebar from './right_sidebar';
import IssuableContext from '~/issuable/issuable_context';
import Sidebar from '~/right_sidebar';
export default () => {
const sidebarOptEl = document.querySelector('.js-sidebar-options');

View File

@ -1,8 +1,8 @@
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import $ from 'jquery';
import Cookies from 'js-cookie';
import { loadCSSFile } from './lib/utils/css_utils';
import UsersSelect from './users_select';
import { loadCSSFile } from '~/lib/utils/css_utils';
import UsersSelect from '~/users_select';
export default class IssuableContext {
constructor(currentUser) {

View File

@ -1,14 +1,14 @@
import $ from 'jquery';
import Pikaday from 'pikaday';
import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete';
import Autosave from './autosave';
import AutoWidthDropdownSelect from './issuable/auto_width_dropdown_select';
import { loadCSSFile } from './lib/utils/css_utils';
import { parsePikadayDate, pikadayToString } from './lib/utils/datetime_utility';
import { select2AxiosTransport } from './lib/utils/select2_utils';
import { queryToObject, objectToQuery } from './lib/utils/url_utility';
import UsersSelect from './users_select';
import ZenMode from './zen_mode';
import Autosave from '~/autosave';
import AutoWidthDropdownSelect from '~/issuable/auto_width_dropdown_select';
import { loadCSSFile } from '~/lib/utils/css_utils';
import { parsePikadayDate, pikadayToString } from '~/lib/utils/datetime_utility';
import { select2AxiosTransport } from '~/lib/utils/select2_utils';
import { queryToObject, objectToQuery } from '~/lib/utils/url_utility';
import UsersSelect from '~/users_select';
import ZenMode from '~/zen_mode';
const MR_SOURCE_BRANCH = 'merge_request[source_branch]';
const MR_TARGET_BRANCH = 'merge_request[target_branch]';

View File

@ -1,4 +1,4 @@
import issuableInitBulkUpdateSidebar from '~/issuable_bulk_update_sidebar/issuable_init_bulk_update_sidebar';
import issuableInitBulkUpdateSidebar from '~/issuable/bulk_update_sidebar/issuable_init_bulk_update_sidebar';
export default class IssuableIndex {
constructor(pagePrefix = 'issuable_') {

View File

@ -3,7 +3,7 @@ import { GlButton, GlDropdown, GlDropdownItem, GlLink, GlModal } from '@gitlab/u
import { mapActions, mapGetters, mapState } from 'vuex';
import createFlash, { FLASH_TYPES } from '~/flash';
import { EVENT_ISSUABLE_VUE_APP_CHANGE } from '~/issuable/constants';
import { IssuableType } from '~/issuable_show/constants';
import { IssuableType } from '~/vue_shared/issuable/show/constants';
import { IssuableStatus, IssueStateEvent } from '~/issues/show/constants';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
import { visitUrl } from '~/lib/utils/url_utility';

View File

@ -499,7 +499,7 @@ export default {
async handleBulkUpdateClick() {
if (!this.hasInitBulkEdit) {
const initBulkUpdateSidebar = await import(
'~/issuable_bulk_update_sidebar/issuable_init_bulk_update_sidebar'
'~/issuable/bulk_update_sidebar/issuable_init_bulk_update_sidebar'
);
initBulkUpdateSidebar.default.init('issuable_');

View File

@ -1,8 +1,8 @@
/* eslint-disable func-names */
import $ from 'jquery';
import Api from './api';
import { humanize } from './lib/utils/text_utility';
import Api from '~/api';
import { humanize } from '~/lib/utils/text_utility';
export default class CreateLabelDropdown {
constructor($el, namespacePath, projectPath) {

View File

@ -1,8 +1,8 @@
import $ from 'jquery';
import { __ } from '~/locale';
import { fixTitle, hide } from '~/tooltips';
import createFlash from './flash';
import axios from './lib/utils/axios_utils';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
const tooltipTitles = {
group: __('Unsubscribe at group level'),

View File

@ -3,9 +3,9 @@
import $ from 'jquery';
import Sortable from 'sortablejs';
import { dispose } from '~/tooltips';
import createFlash from './flash';
import axios from './lib/utils/axios_utils';
import { __ } from './locale';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
export default class LabelManager {
constructor({ togglePriorityButton, prioritizedLabels, otherLabels } = {}) {

View File

@ -4,12 +4,12 @@
import $ from 'jquery';
import { difference, isEqual, escape, sortBy, template, union } from 'lodash';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
import IssuableBulkUpdateActions from '~/issuable_bulk_update_sidebar/issuable_bulk_update_actions';
import IssuableBulkUpdateActions from '~/issuable/bulk_update_sidebar/issuable_bulk_update_actions';
import { isScopedLabel } from '~/lib/utils/common_utils';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { sprintf, __ } from '~/locale';
import CreateLabelDropdown from './create_label';
import createFlash from './flash';
import axios from './lib/utils/axios_utils';
import { sprintf, __ } from './locale';
export default class LabelsSelect {
constructor(els, options = {}) {

View File

@ -1,8 +1,8 @@
import $ from 'jquery';
import { fixTitle } from '~/tooltips';
import createFlash from './flash';
import axios from './lib/utils/axios_utils';
import { __ } from './locale';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
const tooltipTitles = {
group: {

View File

@ -1,5 +1,5 @@
import Vue from 'vue';
import initIssuableSidebar from '../init_issuable_sidebar';
import initIssuableSidebar from '~/issuable/init_issuable_sidebar';
import MergeConflictsResolverApp from './merge_conflict_resolver_app.vue';
import { createStore } from './store';

View File

@ -86,8 +86,8 @@ export default {
<template>
<div>
<div class="gl-display-flex gl-justify-content-space-between gl-mb-3">
<gl-form-checkbox v-if="!hiddenDelete" v-model="selectAll" class="gl-ml-2">
<div class="gl-display-flex gl-justify-content-space-between gl-mb-3 gl-align-items-center">
<gl-form-checkbox v-if="!hiddenDelete" v-model="selectAll" class="gl-ml-2 gl-pt-2">
<span class="gl-font-weight-bold">{{ title }}</span>
</gl-form-checkbox>

View File

@ -1,3 +1,3 @@
import Labels from '../../../../labels';
import Labels from '~/labels/labels';
new Labels(); // eslint-disable-line no-new

View File

@ -1,3 +1,3 @@
import Labels from '../../../../labels';
import Labels from '~/labels/labels';
new Labels(); // eslint-disable-line no-new

View File

@ -1,5 +1,5 @@
import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys';
import issuableInitBulkUpdateSidebar from '~/issuable_bulk_update_sidebar/issuable_init_bulk_update_sidebar';
import issuableInitBulkUpdateSidebar from '~/issuable/bulk_update_sidebar/issuable_init_bulk_update_sidebar';
import { mountIssuablesListApp, mountIssuesListApp } from '~/issues_list';
import initManualOrdering from '~/manual_ordering';
import { FILTERED_SEARCH } from '~/pages/constants';

View File

@ -1,4 +1,4 @@
import Labels from 'ee_else_ce/labels';
import Labels from 'ee_else_ce/labels/labels';
// eslint-disable-next-line no-new
new Labels();

View File

@ -1,5 +1,5 @@
import initDeleteLabelModal from '~/delete_label_modal';
import initLabels from '~/init_labels';
import initDeleteLabelModal from '~/labels/delete_label_modal';
import initLabels from '~/labels/init_labels';
initLabels();
initDeleteLabelModal();

View File

@ -1,4 +1,4 @@
import Labels from 'ee_else_ce/labels';
import Labels from 'ee_else_ce/labels/labels';
// eslint-disable-next-line no-new
new Labels();

View File

@ -1,6 +1,6 @@
import addExtraTokensForMergeRequests from 'ee_else_ce/filtered_search/add_extra_tokens_for_merge_requests';
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
import issuableInitBulkUpdateSidebar from '~/issuable_bulk_update_sidebar/issuable_init_bulk_update_sidebar';
import issuableInitBulkUpdateSidebar from '~/issuable/bulk_update_sidebar/issuable_init_bulk_update_sidebar';
import { FILTERED_SEARCH } from '~/pages/constants';
import initFilteredSearch from '~/pages/search/init_filtered_search';
import projectSelect from '~/project_select';

View File

@ -1,12 +1,12 @@
/* eslint-disable no-new */
import $ from 'jquery';
import IssuableForm from 'ee_else_ce/issuable_form';
import IssuableForm from 'ee_else_ce/issuable/issuable_form';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import GLForm from '~/gl_form';
import initSuggestions from '~/issues/suggestions';
import initIssuableTypeSelector from '~/issues/type_selector';
import LabelsSelect from '~/labels_select';
import LabelsSelect from '~/labels/labels_select';
import MilestoneSelect from '~/milestones/milestone_select';
import IssuableTemplateSelectors from '~/templates/issuable_template_selectors';

View File

@ -2,7 +2,7 @@ import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import initCsvImportExportButtons from '~/issuable/init_csv_import_export_buttons';
import initIssuableByEmail from '~/issuable/init_issuable_by_email';
import IssuableIndex from '~/issuable_index';
import IssuableIndex from '~/issuable/issuable_index';
import { mountIssuablesListApp, mountIssuesListApp, mountJiraIssuesListApp } from '~/issues_list';
import initManualOrdering from '~/manual_ordering';
import { FILTERED_SEARCH } from '~/pages/constants';

View File

@ -1,7 +1,7 @@
import loadAwardsHandler from '~/awards_handler';
import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
import initIssuableSidebar from '~/init_issuable_sidebar';
import { IssuableType } from '~/issuable_show/constants';
import initIssuableSidebar from '~/issuable/init_issuable_sidebar';
import { IssuableType } from '~/vue_shared/issuable/show/constants';
import Issue from '~/issue';
import { initIncidentApp, initIncidentHeaderActions } from '~/issues/show/incident';
import { initIssuableApp, initIssueHeaderActions } from '~/issues/show/issue';

View File

@ -1,3 +1,3 @@
import Labels from 'ee_else_ce/labels';
import Labels from 'ee_else_ce/labels/labels';
new Labels(); // eslint-disable-line no-new

View File

@ -1,6 +1,6 @@
import Vue from 'vue';
import initDeleteLabelModal from '~/delete_label_modal';
import initLabels from '~/init_labels';
import initDeleteLabelModal from '~/labels/delete_label_modal';
import initLabels from '~/labels/init_labels';
import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import Translate from '~/vue_shared/translate';
import PromoteLabelModal from '../components/promote_label_modal.vue';

View File

@ -1,4 +1,4 @@
import Labels from 'ee_else_ce/labels';
import Labels from 'ee_else_ce/labels/labels';
// eslint-disable-next-line no-new
new Labels();

View File

@ -3,7 +3,7 @@ import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
import initCsvImportExportButtons from '~/issuable/init_csv_import_export_buttons';
import initIssuableByEmail from '~/issuable/init_issuable_by_email';
import IssuableIndex from '~/issuable_index';
import IssuableIndex from '~/issuable/issuable_index';
import { FILTERED_SEARCH } from '~/pages/constants';
import { ISSUABLE_INDEX } from '~/pages/projects/constants';
import initFilteredSearch from '~/pages/search/init_filtered_search';

View File

@ -1,11 +1,11 @@
/* eslint-disable no-new */
import $ from 'jquery';
import IssuableForm from 'ee_else_ce/issuable_form';
import IssuableForm from 'ee_else_ce/issuable/issuable_form';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import Diff from '~/diff';
import GLForm from '~/gl_form';
import LabelsSelect from '~/labels_select';
import LabelsSelect from '~/labels/labels_select';
import MilestoneSelect from '~/milestones/milestone_select';
import IssuableTemplateSelectors from '~/templates/issuable_template_selectors';

View File

@ -3,7 +3,7 @@ import VueApollo from 'vue-apollo';
import loadAwardsHandler from '~/awards_handler';
import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
import { initPipelineCountListener } from '~/commit/pipelines/utils';
import initIssuableSidebar from '~/init_issuable_sidebar';
import initIssuableSidebar from '~/issuable/init_issuable_sidebar';
import StatusBox from '~/issuable/components/status_box.vue';
import createDefaultClient from '~/lib/graphql';
import initSourcegraph from '~/sourcegraph';

View File

@ -57,7 +57,7 @@ export default {
<div class="gl-display-flex gl-align-items-center gl-py-3">
<div
v-if="$slots['left-action']"
class="gl-w-7 gl-display-none gl-sm-display-flex gl-justify-content-start gl-pl-2"
class="gl-w-7 gl-display-flex gl-justify-content-start gl-pl-2"
>
<slot name="left-action"></slot>
</div>
@ -105,7 +105,7 @@ export default {
</div>
<div
v-if="$slots['right-action']"
class="gl-w-9 gl-display-none gl-sm-display-flex gl-justify-content-end gl-pr-1"
class="gl-w-9 gl-display-flex gl-justify-content-end gl-pr-1"
>
<slot name="right-action"></slot>
</div>

View File

@ -174,3 +174,30 @@ body {
min-height: 0;
}
}
.gl-drawer-responsive {
// Both width & min-width
// are defined as per Pajamas
// See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/44902#note_429056182
width: 28%;
min-width: 400px;
padding-left: $gl-padding;
padding-right: $gl-padding;
box-shadow: none;
background-color: $gray-10;
border-left: 1px solid $gray-100;
@include media-breakpoint-down(sm) {
min-width: unset;
width: 100%;
}
// These overrides should not happen here,
// we should ideally have support for custom
// header and body classes in `GlDrawer`.
.gl-drawer-header,
.gl-drawer-body > * {
padding-left: 0;
padding-right: 0;
}
}

View File

@ -33,7 +33,7 @@ module TimeZoneHelper
end
end
def local_time_instance(timezone)
def local_timezone_instance(timezone)
return Time.zone if timezone.blank?
ActiveSupport::TimeZone.new(timezone) || Time.zone
@ -42,7 +42,7 @@ module TimeZoneHelper
def local_time(timezone)
return if timezone.blank?
time_zone_instance = local_time_instance(timezone)
time_zone_instance = local_timezone_instance(timezone)
time_zone_instance.now.strftime("%-l:%M %p")
end
end

View File

@ -1,9 +1,10 @@
# frozen_string_literal: true
class NamespacePolicy < ::Namespaces::UserNamespacePolicy
class NamespacePolicy < BasePolicy
# NamespacePolicy has been traditionally for user namespaces.
# So these policies have been moved into Namespaces::UserNamespacePolicy.
# Once the user namespace conversion is complete, we can look at
# either removing this file or locating common namespace policy items
# here.
# See https://gitlab.com/groups/gitlab-org/-/epics/6689 for details
end

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module Namespaces
class ProjectNamespacePolicy < BasePolicy
class ProjectNamespacePolicy < NamespacePolicy
# For now users are not granted any permissions on project namespace
# as it's completely hidden to them. When we start using project
# namespaces in queries, we will have to extend this policy.

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module Namespaces
class UserNamespacePolicy < BasePolicy
class UserNamespacePolicy < ::NamespacePolicy
rule { anonymous }.prevent_all
condition(:personal_project, scope: :subject) { @subject.kind == 'user' }

View File

@ -2,7 +2,7 @@
.row.d-none.d-sm-flex
.col-12.calendar-block.gl-my-3
.user-calendar.light{ data: { calendar_path: user_calendar_path(@user, :json), calendar_activities_path: user_calendar_activities_path, utc_offset: local_time_instance(@user.timezone).now.utc_offset } }
.user-calendar.light{ data: { calendar_path: user_calendar_path(@user, :json), calendar_activities_path: user_calendar_activities_path, utc_offset: local_timezone_instance(@user.timezone).now.utc_offset } }
.gl-spinner.gl-spinner-md.gl-my-8
.user-calendar-error.invisible
= _('There was an error loading users activity calendar.')

View File

@ -7,7 +7,7 @@
%li
%span.light.js-localtime{ :data => { :datetime => event.created_at.utc.strftime('%Y-%m-%dT%H:%M:%SZ'), :toggle => 'tooltip', :placement => 'top' } }
= sprite_icon('clock', css_class: 'gl-vertical-align-text-bottom')
= event.created_at.to_time.in_time_zone(local_time_instance(@user.timezone)).strftime('%-I:%M%P')
= event.created_at.to_time.in_time_zone(local_timezone_instance(@user.timezone)).strftime('%-I:%M%P')
- if event.visible_to_user?(current_user)
- if event.push_action?
#{event.action_name} #{event.ref_type}

View File

@ -4,5 +4,5 @@ introduced_by_url: https://dev.gitlab.org/gitlab/gitlabhq/-/merge_requests/3126
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/29875
milestone: '12.0'
type: development
group: group::pipeline execution
group: group::pipeline authoring
default_enabled: true

View File

@ -4,5 +4,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/50922
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/296772
milestone: '13.8'
type: development
group: group::pipeline execution
group: group::testing
default_enabled: true

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
class CreateCalendarEventsIndexSynchronously < Gitlab::Database::Migration[1.0]
disable_ddl_transaction!
INDEX_NAME = 'index_events_author_id_project_id_action_target_type_created_at'
def up
add_concurrent_index :events, [:author_id, :project_id, :action, :target_type, :created_at], name: INDEX_NAME
end
def down
remove_concurrent_index_by_name :events, INDEX_NAME
end
end

View File

@ -0,0 +1,18 @@
# frozen_string_literal: true
# See https://docs.gitlab.com/ee/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class RemoveOldCalendarEventsIndex < Gitlab::Database::Migration[1.0]
disable_ddl_transaction!
OLD_INDEX_NAME = 'index_events_on_author_id_and_project_id'
def up
remove_concurrent_index_by_name :events, OLD_INDEX_NAME
end
def down
add_concurrent_index :events, [:author_id, :project_id], name: OLD_INDEX_NAME
end
end

View File

@ -0,0 +1 @@
403592fda1d82ed3c3fb8d5315593b67954a4ecbc368d9bcd5eedc75bb3c9821

View File

@ -0,0 +1 @@
ba1c0d20e21ef51278109d0eaeb23f1c541eb5eb9aeb9a92583ee6de83c68918

View File

@ -26018,14 +26018,14 @@ CREATE INDEX index_et_errors_on_project_id_and_status_first_seen_at_id_desc ON e
CREATE INDEX index_et_errors_on_project_id_and_status_last_seen_at_id_desc ON error_tracking_errors USING btree (project_id, status, last_seen_at DESC, id DESC);
CREATE INDEX index_events_author_id_project_id_action_target_type_created_at ON events USING btree (author_id, project_id, action, target_type, created_at);
CREATE INDEX index_events_on_action ON events USING btree (action);
CREATE INDEX index_events_on_author_id_and_created_at ON events USING btree (author_id, created_at);
CREATE INDEX index_events_on_author_id_and_created_at_merge_requests ON events USING btree (author_id, created_at) WHERE ((target_type)::text = 'MergeRequest'::text);
CREATE INDEX index_events_on_author_id_and_project_id ON events USING btree (author_id, project_id);
CREATE INDEX index_events_on_created_at_and_id ON events USING btree (created_at, id) WHERE (created_at > '2021-08-27 00:00:00+00'::timestamp with time zone);
CREATE INDEX index_events_on_group_id_partial ON events USING btree (group_id) WHERE (group_id IS NOT NULL);

View File

@ -69,6 +69,8 @@ a single URL used by all Geo sites, including the primary.
is using the secondary proxying and set the `URL` field to the single URL.
Make sure the primary site is also using this URL.
In Kubernetes, you can use the same domain under `global.hosts.domain` as for the primary site.
## Disable Geo proxying
You can disable the secondary proxying on each Geo site, separately, by following these steps with Omnibus-based packages:
@ -121,18 +123,22 @@ for details.
## Limitations
The asynchronous Geo replication can cause unexpected issues when secondary proxying is used, for accelerated
data types that may be replicated to the Geo secondaries with a delay.
- When secondary proxying is used, the asynchronous Geo replication can cause unexpected issues for accelerated
data types that may be replicated to the Geo secondaries with a delay.
For example, we found a potential issue where
[Replication lag introduces read-your-own-write inconsistencies](https://gitlab.com/gitlab-org/gitlab/-/issues/345267).
If the replication lag is high enough, this can result in Git reads receiving stale data when hitting a secondary.
For example, we found a potential issue where
[replication lag introduces read-after-write inconsistencies](https://gitlab.com/gitlab-org/gitlab/-/issues/345267).
If the replication lag is high enough, this can result in Git reads receiving stale data when hitting a secondary.
Non-Rails requests are not proxied, so other services may need to use a separate, non-unified URL to ensure requests
are always sent to the primary. These services include:
- Non-Rails requests are not proxied, so other services may need to use a separate, non-unified URL to ensure requests
are always sent to the primary. These services include:
- GitLab Container Registry - [can be configured to use a separate domain](../../packages/container_registry.md#configure-container-registry-under-its-own-domain).
- GitLab Pages - should always use a separate domain, as part of [the prerequisites for running GitLab Pages](../../pages/index.md#prerequisites).
- GitLab Container Registry - [can be configured to use a separate domain](../../packages/container_registry.md#configure-container-registry-under-its-own-domain).
- GitLab Pages - should always use a separate domain, as part of [the prerequisites for running GitLab Pages](../../pages/index.md#prerequisites).
- With a unified URL, Let's Encrypt can't generate certificates unless it can reach both IPs through the same domain.
To use TLS certificates with Let's Encrypt, you can manually point the domain to one of the Geo sites, generate
the certificate, then copy it to all other sites.
## Features accelerated by secondary Geo sites

View File

@ -10,7 +10,7 @@ module Gitlab
def initialize(contributor, current_user = nil)
@contributor = contributor
@contributor_time_instance = local_time_instance(contributor.timezone)
@contributor_time_instance = local_timezone_instance(contributor.timezone).now
@current_user = current_user
@projects = if @contributor.include_private_contributions?
ContributedProjectsFinder.new(@contributor).execute(@contributor)
@ -24,18 +24,20 @@ module Gitlab
return {} if @projects.empty?
return @activity_dates if @activity_dates.present?
date_interval = "INTERVAL '#{@contributor_time_instance.now.utc_offset} seconds'"
start_time = @contributor_time_instance.years_ago(1).beginning_of_day
end_time = @contributor_time_instance.end_of_day
date_interval = "INTERVAL '#{@contributor_time_instance.utc_offset} seconds'"
# Can't use Event.contributions here because we need to check 3 different
# project_features for the (currently) 3 different contribution types
date_from = @contributor_time_instance.now.years_ago(1)
repo_events = event_created_at(date_from, :repository)
repo_events = events_created_between(start_time, end_time, :repository)
.where(action: :pushed)
issue_events = event_created_at(date_from, :issues)
issue_events = events_created_between(start_time, end_time, :issues)
.where(action: [:created, :closed], target_type: "Issue")
mr_events = event_created_at(date_from, :merge_requests)
mr_events = events_created_between(start_time, end_time, :merge_requests)
.where(action: [:merged, :created, :closed], target_type: "MergeRequest")
note_events = event_created_at(date_from, :merge_requests)
note_events = events_created_between(start_time, end_time, :merge_requests)
.where(action: :commented)
events = Event
@ -54,7 +56,7 @@ module Gitlab
def events_by_date(date)
return Event.none unless can_read_cross_project?
date_in_time_zone = date.in_time_zone(@contributor_time_instance)
date_in_time_zone = date.in_time_zone(@contributor_time_instance.time_zone)
Event.contributions.where(author_id: contributor.id)
.where(created_at: date_in_time_zone.beginning_of_day..date_in_time_zone.end_of_day)
@ -64,11 +66,11 @@ module Gitlab
# rubocop: enable CodeReuse/ActiveRecord
def starting_year
@contributor_time_instance.now.years_ago(1).year
@contributor_time_instance.years_ago(1).year
end
def starting_month
@contributor_time_instance.today.month
@contributor_time_instance.month
end
private
@ -78,9 +80,7 @@ module Gitlab
end
# rubocop: disable CodeReuse/ActiveRecord
def event_created_at(date_from, feature)
t = Event.arel_table
def events_created_between(start_time, end_time, feature)
# re-running the contributed projects query in each union is expensive, so
# use IN(project_ids...) instead. It's the intersection of two users so
# the list will be (relatively) short
@ -89,24 +89,22 @@ module Gitlab
# no need to check feature access of current user, if the contributor opted-in
# to show all private events anyway - otherwise they would get filtered out again
authed_projects = if @contributor.include_private_contributions?
@contributed_project_ids.join(",")
@contributed_project_ids
else
ProjectFeature
.with_feature_available_for_user(feature, current_user)
.where(project_id: @contributed_project_ids)
.reorder(nil)
.select(:project_id)
.to_sql
end
conditions = t[:created_at].gteq(date_from.beginning_of_day)
.and(t[:created_at].lteq(@contributor_time_instance.today.end_of_day))
.and(t[:author_id].eq(contributor.id))
Event.reorder(nil)
.select(:created_at)
.where(conditions)
.where("events.project_id in (#{authed_projects})") # rubocop:disable GitlabSecurity/SqlInjection
.where(
author_id: contributor.id,
created_at: start_time..end_time,
events: { project_id: authed_projects }
)
end
# rubocop: enable CodeReuse/ActiveRecord
end

View File

@ -10202,6 +10202,9 @@ msgstr ""
msgid "Critical vulnerabilities present"
msgstr ""
msgid "Crm|Contact has been added"
msgstr ""
msgid "Crm|Create new contact"
msgstr ""
@ -10220,6 +10223,9 @@ msgstr ""
msgid "Crm|Last name"
msgstr ""
msgid "Crm|New Contact"
msgstr ""
msgid "Crm|New contact"
msgstr ""

View File

@ -122,16 +122,6 @@ describe('Customer relations contacts root app', () => {
expect(findError().exists()).toBe(true);
});
it('should exist when new contact form emits error', async () => {
router.replace({ path: '/new' });
mountComponent();
findNewContactForm().vm.$emit('error');
await waitForPromises();
expect(findError().exists()).toBe(true);
});
});
describe('on successful load', () => {

View File

@ -1,3 +1,4 @@
import { GlAlert } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
@ -21,6 +22,7 @@ describe('Customer relations contacts root app', () => {
const findCreateNewContactButton = () => wrapper.findByTestId('create-new-contact-button');
const findCancelButton = () => wrapper.findByTestId('cancel-button');
const findForm = () => wrapper.find('form');
const findError = () => wrapper.findComponent(GlAlert);
const mountComponent = ({ mountFunction = shallowMountExtended } = {}) => {
fakeApollo = createMockApollo([[createContactMutation, queryHandler]]);
@ -32,6 +34,7 @@ describe('Customer relations contacts root app', () => {
wrapper = mountFunction(NewContactForm, {
provide: { groupId: 26, groupFullPath: 'flightjs' },
apolloProvider: fakeApollo,
propsData: { drawerOpen: true },
});
};
@ -83,26 +86,25 @@ describe('Customer relations contacts root app', () => {
});
describe('when query fails', () => {
it('should emit error on reject', async () => {
it('should show error on reject', async () => {
queryHandler = jest.fn().mockRejectedValue('ERROR');
mountComponent();
findForm().trigger('submit');
await waitForPromises();
expect(wrapper.emitted().error).toBeTruthy();
expect(findError().exists()).toBe(true);
});
it('should emit error on error response', async () => {
it('should show error on error response', async () => {
queryHandler = jest.fn().mockResolvedValue(createContactMutationErrorResponse);
mountComponent();
findForm().trigger('submit');
await waitForPromises();
expect(wrapper.emitted().error[0][0]).toEqual(
createContactMutationErrorResponse.data.customerRelationsContactCreate.errors,
);
expect(findError().exists()).toBe(true);
expect(findError().text()).toBe('Phone is invalid.');
});
});
});

View File

@ -1,7 +1,7 @@
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import StatusSelect from '~/issuable_bulk_update_sidebar/components/status_select.vue';
import { ISSUE_STATUS_SELECT_OPTIONS } from '~/issuable_bulk_update_sidebar/constants';
import StatusSelect from '~/issuable/bulk_update_sidebar/components/status_select.vue';
import { ISSUE_STATUS_SELECT_OPTIONS } from '~/issuable/bulk_update_sidebar/constants';
describe('StatusSelect', () => {
let wrapper;

View File

@ -1,6 +1,6 @@
import $ from 'jquery';
import IssuableForm from '~/issuable_form';
import IssuableForm from '~/issuable/issuable_form';
function createIssuable() {
const instance = new IssuableForm($(document.createElement('form')));

View File

@ -1,5 +1,5 @@
import issuableInitBulkUpdateSidebar from '~/issuable_bulk_update_sidebar/issuable_init_bulk_update_sidebar';
import IssuableIndex from '~/issuable_index';
import issuableInitBulkUpdateSidebar from '~/issuable/bulk_update_sidebar/issuable_init_bulk_update_sidebar';
import IssuableIndex from '~/issuable/issuable_index';
describe('Issuable', () => {
describe('initBulkUpdate', () => {

View File

@ -2,7 +2,7 @@ import { GlButton, GlDropdown, GlDropdownItem, GlLink, GlModal } from '@gitlab/u
import { createLocalVue, shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import createFlash, { FLASH_TYPES } from '~/flash';
import { IssuableType } from '~/issuable_show/constants';
import { IssuableType } from '~/vue_shared/issuable/show/constants';
import HeaderActions from '~/issues/show/components/header_actions.vue';
import { IssuableStatus, IssueStateEvent } from '~/issues/show/constants';
import promoteToEpicMutation from '~/issues/show/queries/promote_to_epic.mutation.graphql';

View File

@ -1,5 +1,5 @@
import { TEST_HOST } from 'helpers/test_constants';
import initDeleteLabelModal from '~/delete_label_modal';
import initDeleteLabelModal from '~/labels/delete_label_modal';
describe('DeleteLabelModal', () => {
const buttons = [

View File

@ -1,5 +1,5 @@
import $ from 'jquery';
import LabelsSelect from '~/labels_select';
import LabelsSelect from '~/labels/labels_select';
const mockUrl = '/foo/bar/url';

View File

@ -86,7 +86,7 @@ exports[`packages_list_row renders 1`] = `
</div>
<div
class="gl-w-9 gl-display-none gl-sm-display-flex gl-justify-content-end gl-pr-1"
class="gl-w-9 gl-display-flex gl-justify-content-end gl-pr-1"
>
<gl-button-stub
aria-label="Remove package"

View File

@ -92,7 +92,7 @@ exports[`packages_list_row renders 1`] = `
</div>
<div
class="gl-w-9 gl-display-none gl-sm-display-flex gl-justify-content-end gl-pr-1"
class="gl-w-9 gl-display-flex gl-justify-content-end gl-pr-1"
>
<gl-button-stub
aria-label="Remove package"

View File

@ -1,11 +1,11 @@
import { shallowMount } from '@vue/test-utils';
import { useFakeDate } from 'helpers/fake_date';
import IssuableBody from '~/issuable_show/components/issuable_body.vue';
import IssuableBody from '~/vue_shared/issuable/show/components/issuable_body.vue';
import IssuableDescription from '~/issuable_show/components/issuable_description.vue';
import IssuableEditForm from '~/issuable_show/components/issuable_edit_form.vue';
import IssuableTitle from '~/issuable_show/components/issuable_title.vue';
import IssuableDescription from '~/vue_shared/issuable/show/components/issuable_description.vue';
import IssuableEditForm from '~/vue_shared/issuable/show/components/issuable_edit_form.vue';
import IssuableTitle from '~/vue_shared/issuable/show/components/issuable_title.vue';
import TaskList from '~/task_list';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';

View File

@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import $ from 'jquery';
import IssuableDescription from '~/issuable_show/components/issuable_description.vue';
import IssuableDescription from '~/vue_shared/issuable/show/components/issuable_description.vue';
import { mockIssuable } from '../mock_data';

View File

@ -1,8 +1,8 @@
import { GlFormInput } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import IssuableEditForm from '~/issuable_show/components/issuable_edit_form.vue';
import IssuableEventHub from '~/issuable_show/event_hub';
import IssuableEditForm from '~/vue_shared/issuable/show/components/issuable_edit_form.vue';
import IssuableEventHub from '~/vue_shared/issuable/show/event_hub';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import { mockIssuableShowProps, mockIssuable } from '../mock_data';

View File

@ -2,7 +2,7 @@ import { GlIcon, GlAvatarLabeled } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import IssuableHeader from '~/issuable_show/components/issuable_header.vue';
import IssuableHeader from '~/vue_shared/issuable/show/components/issuable_header.vue';
import { mockIssuableShowProps, mockIssuable } from '../mock_data';

View File

@ -1,8 +1,8 @@
import { shallowMount } from '@vue/test-utils';
import IssuableBody from '~/issuable_show/components/issuable_body.vue';
import IssuableHeader from '~/issuable_show/components/issuable_header.vue';
import IssuableShowRoot from '~/issuable_show/components/issuable_show_root.vue';
import IssuableBody from '~/vue_shared/issuable/show/components/issuable_body.vue';
import IssuableHeader from '~/vue_shared/issuable/show/components/issuable_header.vue';
import IssuableShowRoot from '~/vue_shared/issuable/show/components/issuable_show_root.vue';
import IssuableSidebar from '~/vue_shared/issuable/sidebar/components/issuable_sidebar_root.vue';

View File

@ -2,7 +2,7 @@ import { GlIcon, GlButton, GlIntersectionObserver } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import IssuableTitle from '~/issuable_show/components/issuable_title.vue';
import IssuableTitle from '~/vue_shared/issuable/show/components/issuable_title.vue';
import { mockIssuableShowProps, mockIssuable } from '../mock_data';

View File

@ -125,7 +125,7 @@ RSpec.describe TimeZoneHelper, :aggregate_failures do
end
end
describe '#local_time_instance' do
describe '#local_timezone_instance' do
let_it_be(:timezone) { 'UTC' }
before do
@ -134,25 +134,25 @@ RSpec.describe TimeZoneHelper, :aggregate_failures do
context 'when timezone is `nil`' do
it 'returns the system timezone instance' do
expect(helper.local_time_instance(nil).name).to eq(timezone)
expect(helper.local_timezone_instance(nil).name).to eq(timezone)
end
end
context 'when timezone is blank' do
it 'returns the system timezone instance' do
expect(helper.local_time_instance('').name).to eq(timezone)
expect(helper.local_timezone_instance('').name).to eq(timezone)
end
end
context 'when a valid timezone is passed' do
it 'returns the local time instance' do
expect(helper.local_time_instance('America/Los_Angeles').name).to eq('America/Los_Angeles')
expect(helper.local_timezone_instance('America/Los_Angeles').name).to eq('America/Los_Angeles')
end
end
context 'when an invalid timezone is passed' do
it 'returns the system timezone instance' do
expect(helper.local_time_instance('Foo/Bar').name).to eq(timezone)
expect(helper.local_timezone_instance('Foo/Bar').name).to eq(timezone)
end
end
end

View File

@ -146,6 +146,7 @@ RSpec.describe Gitlab::ContributionsCalendar do
create_event(public_project, today, 10)
create_event(public_project, today, 16)
create_event(public_project, today, 23)
create_event(public_project, tomorrow, 1)
end
it "renders correct event counts within the UTC timezone" do
@ -158,14 +159,14 @@ RSpec.describe Gitlab::ContributionsCalendar do
it "renders correct event counts within the Sydney timezone" do
Time.use_zone('UTC') do
contributor.timezone = 'Sydney'
expect(calendar.activity_dates).to eq(today => 3, tomorrow => 2)
expect(calendar.activity_dates).to eq(today => 3, tomorrow => 3)
end
end
it "renders correct event counts within the US Central timezone" do
Time.use_zone('UTC') do
contributor.timezone = 'Central Time (US & Canada)'
expect(calendar.activity_dates).to eq(yesterday => 2, today => 3)
expect(calendar.activity_dates).to eq(yesterday => 2, today => 4)
end
end
end

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe NamespacePolicy do
RSpec.describe Namespaces::ProjectNamespacePolicy do
let_it_be(:parent) { create(:namespace) }
let_it_be(:project) { create(:project, namespace: parent) }
let_it_be(:namespace) { project.project_namespace }
@ -37,7 +37,7 @@ RSpec.describe NamespacePolicy do
let_it_be(:current_user) { create(:admin) }
context 'when admin mode is enabled', :enable_admin_mode do
it { is_expected.to be_allowed(*permissions) }
it { is_expected.to be_disallowed(*permissions) }
end
context 'when admin mode is disabled' do

View File

@ -209,6 +209,8 @@ RSpec.describe API::Ci::JobArtifacts do
end
it 'returns specific job artifacts' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(response.headers.to_h).to include(download_headers)
expect(response.body).to match_file(job.artifacts_file.file.file)
@ -220,18 +222,48 @@ RSpec.describe API::Ci::JobArtifacts do
context 'when artifacts are stored locally' do
let(:job) { create(:ci_build, :artifacts, pipeline: pipeline) }
before do
get api("/projects/#{project.id}/jobs/#{job.id}/artifacts", api_user)
end
subject { get api("/projects/#{project.id}/jobs/#{job.id}/artifacts", api_user) }
context 'authorized user' do
it_behaves_like 'downloads artifact'
end
context 'when job token is used' do
let(:other_job) { create(:ci_build, :running, user: user) }
subject { get api("/projects/#{project.id}/jobs/#{job.id}/artifacts", job_token: other_job.token) }
before do
stub_licensed_features(cross_project_pipelines: true)
end
it_behaves_like 'downloads artifact'
context 'when job token scope is enabled' do
before do
other_job.project.ci_cd_settings.update!(job_token_scope_enabled: true)
end
it 'does not allow downloading artifacts' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
context 'when project is added to the job token scope' do
let!(:link) { create(:ci_job_token_project_scope_link, source_project: other_job.project, target_project: job.project) }
it_behaves_like 'downloads artifact'
end
end
end
context 'unauthorized user' do
let(:api_user) { nil }
it 'does not return specific job artifacts' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end

View File

@ -19,13 +19,15 @@ module ApiHelpers
# => "/api/v2/issues?foo=bar&private_token=..."
#
# Returns the relative path to the requested API resource
def api(path, user = nil, version: API::API.version, personal_access_token: nil, oauth_access_token: nil)
def api(path, user = nil, version: API::API.version, personal_access_token: nil, oauth_access_token: nil, job_token: nil)
full_path = "/api/#{version}#{path}"
if oauth_access_token
query_string = "access_token=#{oauth_access_token.token}"
elsif personal_access_token
query_string = "private_token=#{personal_access_token.token}"
elsif job_token
query_string = "job_token=#{job_token}"
elsif user
personal_access_token = create(:personal_access_token, user: user)
query_string = "private_token=#{personal_access_token.token}"