Commit 53e31ad3 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents a24b1218 397f75b8
......@@ -312,7 +312,7 @@ gem 'gitlab-pg_query', '~> 1.3', require: 'pg_query'
gem 'premailer-rails', '~> 1.10.3'
# LabKit: Tracing and Correlation
gem 'gitlab-labkit', '0.13.1'
gem 'gitlab-labkit', '0.13.3'
# I18n
gem 'ruby_parser', '~> 3.15', require: false
......
......@@ -434,9 +434,10 @@ GEM
fog-json (~> 1.2.0)
mime-types
ms_rest_azure (~> 0.12.0)
gitlab-labkit (0.13.1)
gitlab-labkit (0.13.3)
actionpack (>= 5.0.0, < 6.1.0)
activesupport (>= 5.0.0, < 6.1.0)
gitlab-pg_query (~> 1.3)
grpc (~> 1.19)
jaeger-client (~> 1.1)
opentracing (~> 0.4)
......@@ -1352,7 +1353,7 @@ DEPENDENCIES
github-markup (~> 1.7.0)
gitlab-chronic (~> 0.10.5)
gitlab-fog-azure-rm (~> 1.0)
gitlab-labkit (= 0.13.1)
gitlab-labkit (= 0.13.3)
gitlab-license (~> 1.0)
gitlab-mail_room (~> 0.0.7)
gitlab-markup (~> 1.7.1)
......
@import 'mixins_and_variables_and_functions';
@mixin inset-border-1-red-500($important: false) {
box-shadow: inset 0 0 0 $gl-border-size-1 $red-500 if-important($important);
}
.timezone-dropdown {
.dropdown-menu {
@include gl-w-full;
}
.gl-new-dropdown-item-text-primary {
@include gl-overflow-hidden;
@include gl-text-overflow-ellipsis;
}
}
.modal-footer {
@include gl-bg-gray-10;
}
.invalid-dropdown {
.gl-dropdown-toggle {
@include inset-border-1-red-500;
&:hover {
@include inset-border-1-red-500(true);
}
}
}
......@@ -45,7 +45,7 @@ module Registrations
end
def update_params
params.require(:user).permit(:role, :setup_for_company)
params.require(:user).permit(:role, :other_role, :setup_for_company)
end
def requires_confirmation?(user)
......
# frozen_string_literal: true
module Ci
module PipelineSchedulesHelper
def timezone_data
ActiveSupport::TimeZone.all.map do |timezone|
{
name: timezone.name,
offset: timezone.now.utc_offset,
identifier: timezone.tzinfo.identifier
}
end
end
end
end
# frozen_string_literal: true
module TimeZoneHelper
def timezone_data
ActiveSupport::TimeZone.all.map do |timezone|
{
identifier: timezone.tzinfo.identifier,
name: timezone.name,
abbr: timezone.tzinfo.strftime('%Z'),
offset: timezone.now.utc_offset,
formatted_offset: timezone.now.formatted_offset
}
end
end
end
......@@ -289,6 +289,7 @@ class User < ApplicationRecord
delegate :path, to: :namespace, allow_nil: true, prefix: true
delegate :job_title, :job_title=, to: :user_detail, allow_nil: true
delegate :other_role, :other_role=, to: :user_detail, allow_nil: true
delegate :bio, :bio=, :bio_html, to: :user_detail, allow_nil: true
delegate :webauthn_xid, :webauthn_xid=, to: :user_detail, allow_nil: true
......
......@@ -14,8 +14,16 @@
.row
.form-group.col-sm-12
= f.label :role, _('Role'), class: 'label-bold'
= f.select :role, ::User.roles.keys.map { |role| [role.titleize, role] }, {}, class: 'form-control', autofocus: true
.form-text.gl-text-gray-500.gl-mt-3= _('This will help us personalize your onboarding experience.')
= f.select :role, ::User.roles.keys.map { |role| [role.titleize, role] }, {}, class: 'form-control js-user-role-dropdown', autofocus: true
- if Feature.enabled?(:user_other_role_details)
.row
.form-group.col-sm-12.js-other-role-group{ class: ("hidden") }
= f.label :other_role, _('What is your job title? (optional)'), class: 'form-check-label gl-mb-3'
= f.text_field :other_role, class: 'form-control'
- else
.row
.form-group.col-sm-12
.form-text.gl-text-gray-500.gl-mt-0.gl-line-height-normal.gl-px-1= _('This will help us personalize your onboarding experience.')
= render_if_exists "registrations/welcome/setup_for_company", f: f
.row
.form-group.col-sm-12.gl-mb-0
......
---
title: Add other role column in user details table
merge_request: 45635
author:
type: added
---
title: Avoid creating wiki empty repo when not present in export files
merge_request: 48890
author:
type: changed
......@@ -204,6 +204,7 @@ module Gitlab
config.assets.precompile << "page_bundles/wiki.css"
config.assets.precompile << "page_bundles/xterm.css"
config.assets.precompile << "page_bundles/alert_management_settings.css"
config.assets.precompile << "page_bundles/oncall_schedules.css"
config.assets.precompile << "lazy_bundles/cropper.css"
config.assets.precompile << "lazy_bundles/select2.css"
config.assets.precompile << "performance_bar.css"
......
---
name: user_other_role_details
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45635
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/255170
milestone: '13.7'
type: development
group: group::conversion
default_enabled: false
# frozen_string_literal: true
class AddOtherRoleToUserDetails < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
unless column_exists?(:user_details, :other_role)
with_lock_retries do
add_column :user_details, :other_role, :text
end
end
add_text_limit :user_details, :other_role, 100
end
def down
with_lock_retries do
remove_column :user_details, :other_role
end
end
end
70fae11d6a73ea8b2ad75c574716f48e9cc78a58ae23db48e74840646fd46672
\ No newline at end of file
......@@ -16961,7 +16961,9 @@ CREATE TABLE user_details (
bio_html text,
cached_markdown_version integer,
webauthn_xid text,
CONSTRAINT check_245664af82 CHECK ((char_length(webauthn_xid) <= 100))
other_role text,
CONSTRAINT check_245664af82 CHECK ((char_length(webauthn_xid) <= 100)),
CONSTRAINT check_b132136b01 CHECK ((char_length(other_role) <= 100))
);
CREATE SEQUENCE user_details_user_id_seq
......
<script>
import { isEqual, isEmpty } from 'lodash';
import {
GlModal,
GlForm,
GlFormGroup,
GlFormInput,
GlDropdown,
GlDropdownItem,
GlSearchBoxByType,
GlAlert,
} from '@gitlab/ui';
import { s__, __ } from '~/locale';
import createOncallScheduleMutation from '../graphql/create_oncall_schedule.mutation.graphql';
export const i18n = {
selectTimezone: s__('OnCallSchedules|Select timezone'),
search: __('Search'),
noResults: __('No matching results'),
cancel: __('Cancel'),
addSchedule: s__('OnCallSchedules|Add schedule'),
fields: {
name: {
title: __('Name'),
validation: {
empty: __("Can't be empty"),
},
},
description: { title: __('Description (optional)') },
timezone: {
title: __('Timezone'),
description: s__(
'OnCallSchedules|Sets the default timezone for the schedule, for all participants',
),
validation: {
empty: __("Can't be empty"),
},
},
},
errorMsg: s__('OnCallSchedules|Failed to add schedule'),
};
export default {
i18n,
inject: ['projectPath', 'timezones'],
components: {
GlModal,
GlForm,
GlFormGroup,
GlFormInput,
GlDropdown,
GlDropdownItem,
GlSearchBoxByType,
GlAlert,
},
props: {
modalId: {
type: String,
required: true,
},
},
data() {
return {
loading: false,
tzSearchTerm: '',
form: {
name: '',
description: '',
timezone: {},
},
error: null,
};
},
computed: {
actionsProps() {
return {
primary: {
text: i18n.addSchedule,
attributes: [
{ variant: 'info' },
{ loading: this.loading },
{ disabled: this.isFormInvalid },
],
},
cancel: {
text: i18n.cancel,
},
};
},
filteredTimezones() {
const lowerCaseTzSearchTerm = this.tzSearchTerm.toLowerCase();
return this.timezones.filter(tz =>
this.getFormattedTimezone(tz)
.toLowerCase()
.includes(lowerCaseTzSearchTerm),
);
},
noResults() {
return !this.filteredTimezones.length;
},
selectedTimezone() {
return isEmpty(this.form.timezone)
? i18n.selectTimezone
: this.getFormattedTimezone(this.form.timezone);
},
isNameInvalid() {
return !this.form.name.length;
},
isTimezoneInvalid() {
return isEmpty(this.form.timezone);
},
isFormInvalid() {
return this.isNameInvalid || this.isTimezoneInvalid;
},
},
methods: {
createSchedule() {
this.loading = true;
this.$apollo
.mutate({
mutation: createOncallScheduleMutation,
variables: {
oncallScheduleCreateInput: {
projectPath: this.projectPath,
...this.form,
timezone: this.form.timezone.identifier,
},
},
})
.then(({ data: { oncallScheduleCreate: { errors: [error] } } }) => {
if (error) {
throw error;
}
this.$refs.createScheduleModal.hide();
})
.catch(error => {
this.error = error;
})
.finally(() => {
this.loading = false;
});
},
setSelectedTimezone(tz) {
this.form.timezone = tz;
},
getFormattedTimezone(tz) {
return __(`(UTC${tz.formatted_offset}) ${tz.abbr} ${tz.name}`);
},
isTimezoneSelected(tz) {
return isEqual(tz, this.form.timezone);
},
hideErrorAlert() {
this.error = null;
},
},
};
</script>
<template>
<gl-modal
ref="createScheduleModal"
:modal-id="modalId"
size="sm"
:title="$options.i18n.addSchedule"
:action-primary="actionsProps.primary"
:action-cancel="actionsProps.cancel"
@primary.prevent="createSchedule"
>
<gl-alert v-if="error" variant="danger" class="gl-mt-n3 gl-mb-3" @dismiss="hideErrorAlert">
{{ error || $options.i18n.errorMsg }}
</gl-alert>
<gl-form>
<gl-form-group
:label="$options.i18n.fields.name.title"
:invalid-feedback="$options.i18n.fields.name.validation.empty"
label-size="sm"
label-for="schedule-name"
>
<gl-form-input id="schedule-name" v-model="form.name" :state="!isNameInvalid" />
</gl-form-group>
<gl-form-group
:label="$options.i18n.fields.description.title"
label-size="sm"
label-for="schedule-description"
>
<gl-form-input id="schedule-description" v-model="form.description" />
</gl-form-group>
<gl-form-group
:label="$options.i18n.fields.timezone.title"
label-size="sm"
label-for="schedule-timezone"
:description="$options.i18n.fields.timezone.description"
:state="!isTimezoneInvalid"
:invalid-feedback="$options.i18n.fields.timezone.validation.empty"
>
<gl-dropdown
id="schedule-timezone"
:text="selectedTimezone"
class="timezone-dropdown gl-w-full"
:header-text="$options.i18n.selectTimezone"
:class="{ 'invalid-dropdown': isTimezoneInvalid }"
>
<gl-search-box-by-type v-model.trim="tzSearchTerm" />
<gl-dropdown-item
v-for="tz in filteredTimezones"
:key="getFormattedTimezone(tz)"
:is-checked="isTimezoneSelected(tz)"
is-check-item
@click="setSelectedTimezone(tz)"
>
<span class="gl-white-space-nowrap"> {{ getFormattedTimezone(tz) }}</span>
</gl-dropdown-item>
<gl-dropdown-item v-if="noResults">
{{ $options.i18n.noResults }}
</gl-dropdown-item>
</gl-dropdown>
</gl-form-group>
</gl-form>
</gl-modal>
</template>
<script>
import { GlEmptyState, GlButton } from '@gitlab/ui';
import { GlEmptyState, GlButton, GlModalDirective } from '@gitlab/ui';
import AddScheduleModal from './add_schedule_modal.vue';
import { s__ } from '~/locale';
const addScheduleModalId = 'addScheduleModal';
export const i18n = {
emptyState: {
title: s__('OnCallSchedules|Create on-call schedules in GitLab'),
......@@ -12,27 +15,33 @@ export const i18n = {
export default {
i18n,
addScheduleModalId,
inject: ['emptyOncallSchedulesSvgPath'],
components: {
GlEmptyState,
GlButton,
AddScheduleModal,
},
methods: {
createSchedule() {},
directives: {
GlModal: GlModalDirective,
},
methods: {},
};
</script>
<template>
<gl-empty-state
:title="$options.i18n.emptyState.title"
:description="$options.i18n.emptyState.description"
:svg-path="emptyOncallSchedulesSvgPath"
>
<template #actions>
<gl-button variant="info" @click="createSchedule">{{
$options.i18n.emptyState.button
}}</gl-button>
</template>
</gl-empty-state>
<div>
<gl-empty-state
:title="$options.i18n.emptyState.title"
:description="$options.i18n.emptyState.description"
:svg-path="emptyOncallSchedulesSvgPath"
>
<template #actions>
<gl-button v-gl-modal="$options.addScheduleModalId" variant="info">
{{ $options.i18n.emptyState.button }}
</gl-button>
</template>
</gl-empty-state>
<add-schedule-modal :modal-id="$options.addScheduleModalId" />
</div>
</template>
mutation oncallScheduleCreate($oncallScheduleCreateInput: OncallScheduleCreateInput!) {
oncallScheduleCreate(input: $oncallScheduleCreateInput) {
errors
oncallSchedule {
iid
name
description
timezone
}
}
}
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import OnCallSchedulesWrapper from './components/oncall_schedules_wrapper.vue';
import createDefaultClient from '~/lib/graphql';
Vue.use(VueApollo);
export default () => {
const el = document.querySelector('#js-oncall_schedule');
if (!el) return null;
const { emptyOncallSchedulesSvgPath } = el.dataset;
const { projectPath, emptyOncallSchedulesSvgPath, timezones } = el.dataset;
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
return new Vue({
el,
apolloProvider,
provide: {
projectPath,
emptyOncallSchedulesSvgPath,
timezones: JSON.parse(timezones),
},
render(createElement) {
return createElement(OnCallSchedulesWrapper);
......
import Vue from 'vue';
import 'ee/registrations/welcome/other_role';
import { parseBoolean } from '~/lib/utils/common_utils';
import {
STEPS,
......
const role = document.querySelector('.js-user-role-dropdown');
const otherRoleGroup = document.querySelector('.js-other-role-group');
role.addEventListener('change', () => {
const enableOtherRole = role.value === 'other';
otherRoleGroup.classList.toggle('hidden', !enableOtherRole);
});
role.dispatchEvent(new Event('change'));
......@@ -17,7 +17,13 @@ export default {
};
},
computed: {
...mapGetters(['totalAmount', 'name', 'usersPresent']),
...mapGetters([
'totalAmount',
'name',
'usersPresent',
'isGroupSelected',
'isSelectedGroupPresent',
]),
titleWithName() {
return sprintf(this.$options.i18n.title, { name: this.name });
},
......@@ -33,7 +39,10 @@ export default {
};
</script>
<template>
<div class="order-summary d-flex flex-column flex-grow-1 gl-mt-2 mt-lg-5">
<div
v-if="!isGroupSelected || isSelectedGroupPresent"
class="order-summary gl-display-flex gl-flex-direction-column gl-flex-grow-1 gl-mt-2 mt-lg-5"
>
<div class="d-lg-none">
<div @click="toggleCollapse">
<h4 class="d-flex justify-content-between gl-font-lg" :class="{ 'gl-mb-7': !collapsed }">
......
......@@ -45,9 +45,14 @@ export const vat = (state, getters) => state.taxRate * getters.totalExVat;
export const totalAmount = (_, getters) => getters.totalExVat + getters.vat;
export const name = (state, getters) => {
if (state.isSetupForCompany && state.organizationName) return state.organizationName;
else if (getters.isGroupSelected) return getters.selectedGroupName;
else if (state.isSetupForCompany) return s__('Checkout|Your organization');
if (state.isSetupForCompany && state.organizationName) {
return state.organizationName;
} else if (getters.isGroupSelected && getters.isSelectedGroupPresent) {
return getters.selectedGroupName;
} else if (state.isSetupForCompany) {
return s__('Checkout|Your organization');
}
return state.fullName;
};
......@@ -56,9 +61,20 @@ export const usersPresent = state => state.numberOfUsers > 0;
export const isGroupSelected = state =>
state.selectedGroup !== null && state.selectedGroup !== NEW_GROUP;
export const isSelectedGroupPresent = (state, getters) => {
return (
getters.isGroupSelected && state.groupData.some(group => group.value === state.selectedGroup)
);
};
export const selectedGroupUsers = (state, getters) => {
if (!getters.isGroupSelected) return 1;
return state.groupData.find(group => group.value === state.selectedGroup).numberOfUsers;
if (!getters.isGroupSelected) {
return 1;
} else if (getters.isSelectedGroupPresent) {
return state.groupData.find(group => group.value === state.selectedGroup).numberOfUsers;
}
return null;
};
export const selectedGroupName = (state, getters) => {
......
......@@ -2,9 +2,11 @@
module IncidentManagement
module OncallScheduleHelper
def oncall_schedule_data
def oncall_schedule_data(project)
{
'empty-oncall-schedules-svg-path' => image_path('illustrations/empty-state/empty-on-call.svg')
'project-path' => project.full_path,
'empty-oncall-schedules-svg-path' => image_path('illustrations/empty-state/empty-on-call.svg'),
'timezones' => timezone_data.to_json
}
end
end
......
- page_title _('On-call schedules')
- add_page_specific_style 'page_bundles/oncall_schedules'
#js-oncall_schedule{ data: oncall_schedule_data }
#js-oncall_schedule{ data: oncall_schedule_data(@project) }
---
title: Fix the user experience when the user is unauthorized or trying to subscribe
for a non-existing group
merge_request: 48626
author:
type: fixed
......@@ -75,6 +75,71 @@ RSpec.describe 'Signup on EE' do
end
end
context 'when the user_other_role_details feature flag is disabled' do
before do
stub_feature_flags(user_other_role_details: false)
end
context 'collects no collect a job title' do
it 'proceeds to the next step without collecting other_role' do
fill_in_signup_form
click_button "Register"
select 'Other', from: 'user_role'
expect(page).not_to have_field('What is your job title? (optional)')
choose 'user_setup_for_company_false'
click_button 'Get started!'
user = User.find_by_username!(new_user[:username])
expect(user.other_role).to be_blank
end
end
end
context 'when the user selects existing role' do
let_it_be(:job_title) { 'Guardian of the galaxy' }
it 'has the job title box' do
expect(page).not_to have_field('What is your job title? (optional)')
end
it 'proceeds to the next step' do
fill_in_signup_form
click_button "Register"
select 'Software Developer', from: 'user_role'
choose 'user_setup_for_company_false'
click_button 'Get started!'
user = User.find_by_username!(new_user[:username])
expect(user.other_role).to be_blank
end
end
context 'when the user selects other role' do
let_it_be(:job_title) { 'Guardian of the galaxy' }
it 'has the job title box' do
expect(page).not_to have_field('What is your job title? (optional)')
end
it 'proceeds to the next step' do
fill_in_signup_form
click_button "Register"
select 'Other', from: 'user_role'
expect(page).to have_field('What is your job title? (optional)')
choose 'user_setup_for_company_false'
fill_in 'What is your job title? (optional)', with: job_title
click_button 'Get started!'
user = User.find_by_username!(new_user[:username])
expect(user.other_role).to eq(job_title)
end
end
it 'redirects to step 2 of the signup process, sets the role and setup for company and redirects back' do
fill_in_signup_form
click_button 'Register'
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AddScheduleModal renders modal layout 1`] = `
<gl-modal-stub
actioncancel="[object Object]"
actionprimary="[object Object]"
modalclass=""
modalid="modalId"
size="sm"
title="Add schedule"
titletag="h4"
>
<!---->
<gl-form-stub>
<gl-form-group-stub
invalid-feedback="Can't be empty"
label="Name"
label-for="schedule-name"
label-size="sm"
>
<gl-form-input-stub
id="schedule-name"
value=""
/>
</gl-form-group-stub>
<gl-form-group-stub
label="Description (optional)"
label-for="schedule-description"
label-size="sm"
>
<gl-form-input-stub
id="schedule-description"
value=""
/>
</gl-form-group-stub>
<gl-form-group-stub
description="Sets the default timezone for the schedule, for all participants"
invalid-feedback="Can't be empty"
label="Timezone"
label-for="schedule-timezone"
label-size="sm"
>
<gl-dropdown-stub
category="primary"
class="timezone-dropdown gl-w-full invalid-dropdown"
headertext="Select timezone"
id="schedule-timezone"
size="medium"
text="Select timezone"
variant="default"
>
<gl-search-box-by-type-stub
clearbuttontitle="Clear"
value=""
/>
<gl-dropdown-item-stub
avatarurl=""
iconcolor=""
iconname=""
iconrightarialabel=""
iconrightname=""
ischeckitem="true"
secondarytext=""
>
<span
class="gl-white-space-nowrap"
>
(UTC-12:00) -12 International Date Line West
</span>
</gl-dropdown-item-stub>
<gl-dropdown-item-stub
avatarurl=""
iconcolor=""
iconname=""
iconrightarialabel=""
iconrightname=""
ischeckitem="true"
secondarytext=""
>
<span
class="gl-white-space-nowrap"
>
(UTC-11:00) SST American Samoa
</span>
</gl-dropdown-item-stub>
<gl-dropdown-item-stub
avatarurl=""
iconcolor=""
iconname=""
iconrightarialabel=""
iconrightname=""
ischeckitem="true"
secondarytext=""
>
<span
class="gl-white-space-nowrap"
>
(UTC-11:00) SST Midway Island
</span>
</gl-dropdown-item-stub>
<gl-dropdown-item-stub
avatarurl=""
iconcolor=""
iconname=""
iconrightarialabel=""
iconrightname=""
ischeckitem="true"
secondarytext=""
>
<span
class="gl-white-space-nowrap"
>
(UTC-10:00) HST Hawaii
</span>
</gl-dropdown-item-stub>
<!---->
</gl-dropdown-stub>
</gl-form-group-stub>
</gl-form-stub>
</gl-modal-stub>
`;
import { shallowMount } from '@vue/test-utils';
import { GlSearchBoxByType, GlDropdown, GlDropdownItem, GlModal, GlAlert } from '@gitlab/ui';
import waitForPromises from 'helpers/wait_for_promises';
import AddScheduleModal, { i18n } from 'ee/oncall_schedules/components/add_schedule_modal.vue';
import mockTimezones from './mocks/mockTimezones.json';
describe('AddScheduleModal', () => {
let wrapper;
const projectPath = 'group/project';
const mutate = jest.fn();
const mockHideModal = jest.fn();
function mountComponent() {
wrapper = shallowMount(AddScheduleModal, {
propsData: {
modalId: 'modalId',
},
provide: {
projectPath,
timezones: mockTimezones,
},
mocks: {
$apollo: {
mutate,
},
},
stubs: {
GlFormGroup: false,
},
});
wrapper.vm.$refs.createScheduleModal.hide = mockHideModal;
}
beforeEach(() => {
mountComponent();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const findModal = () => wrapper.find(GlModal);
const findAlert = () => wrapper.find(GlAlert);
const findTimezoneDropdown = () => wrapper.find(GlDropdown);
const findDropdownOptions = () => wrapper.findAll(GlDropdownItem);
const findTimezoneSearchBox = () => wrapper.find(GlSearchBoxByType);
it('renders modal layout', () => {
expect(wrapper.element).toMatchSnapshot();
});
describe('Timezone select', () => {
it('has options based on provided BE data', () => {
expect(findDropdownOptions().length).toBe(mockTimezones.length);
});
it('formats each option', () => {
findDropdownOptions().wrappers.forEach((option, index) => {
const tz = mockTimezones[index];
const expectedValue = `(UTC${tz.formatted_offset}) ${tz.abbr} ${tz.name}`;
expect(option.text()).toBe(expectedValue);
});
});
describe('timezones filtering', () => {
it('should filter options based on search term', async () => {
const searchTerm = 'Hawaii';
findTimezoneSearchBox().vm.$emit('input', searchTerm);
await wrapper.vm.$nextTick();
const options = findDropdownOptions();
expect(options.length).toBe(1);
expect(options.at(0).text()).toContain(searchTerm);
});
it('should display no results item when there are no filter matches', async () => {
const searchTerm = 'someUnexistentTZ';
findTimezoneSearchBox().vm.$emit('input', searchTerm);
await wrapper.vm.$nextTick();
const options = findDropdownOptions();
expect(options.length).toBe(1);
expect(options.at(0).text()).toContain(i18n.noResults);
});
});
it('should add a checkmark to the selected option', async () => {
const selectedTZOption = findDropdownOptions().at(0);
selectedTZOption.vm.$emit('click');
await wrapper.vm.$nextTick();
expect(selectedTZOption.attributes('ischecked')).toBe('true');
});
});
describe('Schedule create', () => {
it('makes a request with form data to create a schedule', () => {
mutate.mockResolvedValueOnce({});
findModal().vm.$emit('primary', { preventDefault: jest.fn() });
expect(mutate).toHaveBeenCalledWith({
mutation: expect.any(Object),
variables: { oncallScheduleCreateInput: expect.objectContaining({ projectPath }) },
});
});
it('hides the modal on successful schedule creation', async () => {
mutate.mockResolvedValueOnce({ data: { oncallScheduleCreate: { errors: [] } } });
findModal().vm.$emit('primary', { preventDefault: jest.fn() });
await waitForPromises();
expect(mockHideModal).toHaveBeenCalled();
});
it("doesn't hide a modal and shows error alert on fail", async () => {
const error = 'some error';
mutate.mockResolvedValueOnce({ data: { oncallScheduleCreate: { errors: [error] } } });
findModal().vm.$emit('primary', { preventDefault: jest.fn() });
await waitForPromises();
const alert = findAlert();
expect(mockHideModal).not.toHaveBeenCalled();
expect(alert.exists()).toBe(true);
expect(alert.text()).toContain(error);
});
});
describe('Form validation', () => {
describe('Timezone select', () => {
it('has red border when nothing selected', () => {
expect(findTimezoneDropdown().classes()).toContain('invalid-dropdown');
});
it("doesn't have a red border when there is selected opeion", async () => {
findDropdownOptions()
.at(1)
.vm.$emit('click');
await wrapper.vm.$nextTick();
expect(findTimezoneDropdown().classes()).not.toContain('invalid-dropdown');
});
});
});
});
[
{
"identifier": "Etc/GMT+12",
"name": "International Date Line West",
"abbr": "-12",
"formatted_offset": "-12:00"
},
{
"identifier": "Pacific/Pago_Pago",
"name": "American Samoa",
"abbr": "SST",
"formatted_offset": "-11:00"
},
{
"identifier": "Pacific/Midway",
"name": "Midway Island",
"abbr": "SST",
"formatted_offset": "-11:00"
},
{
"identifier": "Pacific/Honolulu",
"name": "Hawaii",
"abbr": "HST",
"formatted_offset": "-10:00"
}
]
......@@ -105,11 +105,28 @@ describe('Subscriptions Getters', () => {
).toBe('My organization');
});
it('returns the organization name when a group is selected but does not exist', () => {
expect(
getters.name(
{ isSetupForCompany: true },
{
isGroupSelected: true,
isSelectedGroupPresent: false,
selectedGroupName: 'Selected group',
},
),
).toBe('Your organization');
});
it('returns the selected group name a group is selected', () => {
expect(
getters.name(
{ isSetupForCompany: true },
{ isGroupSelected: true, selectedGroupName: 'Selected group' },
{
isGroupSelected: true,
isSelectedGroupPresent: true,
selectedGroupName: 'Selected group',
},
),
).toBe('Selected group');
});
......@@ -161,16 +178,54 @@ describe('Subscriptions Getters', () => {
).toBe(1);
});
it('returns `null` when a group is selected, but not present', () => {
expect(
getters.selectedGroupUsers(
{ groupData: [{ numberOfUsers: 3, value: 123 }], selectedGroup: 123 },
{ isGroupSelected: true, isSelectedGroupPresent: false },
),
).toBe(null);
});
it('returns the number of users of the selected group when a group is selected', () => {
expect(
getters.selectedGroupUsers(
{ groupData: [{ numberOfUsers: 3, value: 123 }], selectedGroup: 123 },
{ isGroupSelected: true },
{ isGroupSelected: true, isSelectedGroupPresent: true },
),
).toBe(3);
});
});
describe('isSelectedGroupPresent', () => {
it('returns false when group is not selected', () => {
expect(
getters.isSelectedGroupPresent(
{ groupData: [{ numberOfUsers: 3, value: 123 }], selectedGroup: null },
{ isGroupSelected: false },
),
).toBe(false);
});
it('returns false when group is selected, but not present', () => {
expect(
getters.isSelectedGroupPresent(
{ groupData: [{ numberOfUsers: 3, value: 123 }], selectedGroup: 321 },
{ isGroupSelected: true },
),
).toBe(false);
});
it('returns true when group is selected and is present', () => {
expect(
getters.isSelectedGroupPresent(
{ groupData: [{ numberOfUsers: 3, value: 123 }], selectedGroup: 123 },
{ isGroupSelected: true },
),
).toBe(true);
});
});
describe('selectedGroupName', () => {
it('returns null when no group is selected', () => {
expect(
......
......@@ -6,11 +6,13 @@ RSpec.describe IncidentManagement::OncallScheduleHelper do
let_it_be(:project) { create(:project) }
describe '#oncall_schedule_data' do
subject(:data) { helper.oncall_schedule_data }
subject(:data) { helper.oncall_schedule_data(project) }
it 'returns on-call schedule data' do
is_expected.to eq(
'empty-oncall-schedules-svg-path' => helper.image_path('illustrations/empty-state/empty-on-call.svg')
'project-path' => project.full_path,
'empty-oncall-schedules-svg-path' => helper.image_path('illustrations/empty-state/empty-on-call.svg'),
'timezones' => helper.timezone_data.to_json
)
end
end
......
......@@ -6,12 +6,14 @@ RSpec.describe 'registrations/welcome/show' do
using RSpec::Parameterized::TableSyntax
let_it_be(:user) { User.new }
let_it_be(:user_other_role_details_enabled) { false }
before do
allow(view).to receive(:current_user).and_return(user)
allow(view).to receive(:redirect_path).and_return(redirect_path)
allow(view).to receive(:onboarding_issues_experiment_enabled?).and_return(onboarding_issues_experiment_enabled)
allow(Gitlab).to receive(:com?).and_return(true)
stub_feature_flags(user_other_role_details: user_other_role_details_enabled)
render
end
......@@ -49,5 +51,14 @@ RSpec.describe 'registrations/welcome/show' do
else
it { is_expected.not_to have_selector('#progress-bar') }
end
context 'feature flag other_role_details is enabled' do
let_it_be(:user_other_role_details_enabled) { true }
it 'has a text field for other role' do
is_expected.not_to have_selector('input[type="hidden"][name="user[other_role]"]', visible: false)
is_expected.to have_selector('input[type="text"][name="user[other_role]"]')
end
end
end
end
......@@ -79,10 +79,9 @@ module Gitlab
end
def wiki_restorer
Gitlab::ImportExport::WikiRestorer.new(path_to_bundle: wiki_repo_path,
Gitlab::ImportExport::RepoRestorer.new(path_to_bundle: wiki_repo_path,
shared: shared,
project: ProjectWiki.new(project),
wiki_enabled: project.wiki_enabled?)
project: ProjectWiki.new(project))
end
def design_repo_restorer
......
# frozen_string_literal: true
module Gitlab
module ImportExport
class WikiRestorer < RepoRestorer
def initialize(project:, shared:, path_to_bundle:, wiki_enabled:)
super(project: project, shared: shared, path_to_bundle: path_to_bundle)
@project = project
@wiki_enabled = wiki_enabled
end
def restore
project.wiki if create_empty_wiki?
super
end
private
attr_accessor :project, :wiki_enabled
def create_empty_wiki?
!File.exist?(path_to_bundle) && wiki_enabled
end
end
end
end
......@@ -4881,6 +4881,9 @@ msgstr ""
msgid "Can't apply this suggestion."
msgstr ""
msgid "Can't be empty"
msgstr ""
msgid "Can't create snippet: %{err}"
msgstr ""
......@@ -9348,6 +9351,9 @@ msgstr ""
msgid "Description"
msgstr ""
msgid "Description (optional)"
msgstr ""
msgid "Description parsed with %{link_start}GitLab Flavored Markdown%{link_end}"
msgstr ""
......@@ -19033,12 +19039,24 @@ msgstr ""
msgid "OnCallSchedules|Add a schedule"
msgstr ""
msgid "OnCallSchedules|Add schedule"
msgstr ""
msgid "OnCallSchedules|Create on-call schedules in GitLab"
msgstr ""
msgid "OnCallSchedules|Failed to add schedule"
msgstr ""
msgid "OnCallSchedules|Route alerts directly to specific members of your team"
msgstr ""
msgid "OnCallSchedules|Select timezone"
msgstr ""
msgid "OnCallSchedules|Sets the default timezone for the schedule, for all participants"
msgstr ""
msgid "OnDemandScans|Could not fetch scanner profiles. Please refresh the page, or try again later."
msgstr ""
......@@ -28436,6 +28454,9 @@ msgstr ""
msgid "Timeout connecting to the Google API. Please try again."
msgstr ""
msgid "Timezone"
msgstr ""
msgid "Time|hr"
msgid_plural "Time|hrs"
msgstr[0] ""
......@@ -30649,6 +30670,9 @@ msgstr ""
msgid "What is squashing?"
msgstr ""
msgid "What is your job title? (optional)"
msgstr ""
msgid "What's new at GitLab"
msgstr ""
......
......@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe 'Freeze Periods (JavaScript fixtures)' do
include JavaScriptFixturesHelpers
include Ci::PipelineSchedulesHelper
include TimeZoneHelper
let_it_be(:admin) { create(:admin) }
let_it_be(:project) { create(:project, :repository, path: 'freeze-periods-project') }
......@@ -40,10 +40,12 @@ RSpec.describe 'Freeze Periods (JavaScript fixtures)' do
end
end
describe Ci::PipelineSchedulesHelper, '(JavaScript fixtures)' do
describe TimeZoneHelper, '(JavaScript fixtures)' do
let(:response) { timezone_data.to_json }
it 'api/freeze-periods/timezone_data.json' do
# Looks empty but does things
# More info: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/38525/diffs#note_391048415
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Ci::PipelineSchedulesHelper, :aggregate_failures do
describe '#timezone_data' do
subject { helper.timezone_data }
it 'matches schema' do
expect(subject).not_to be_empty
subject.each_with_index do |timzone_hash, i|
expect(timzone_hash.keys).to contain_exactly(:name, :offset, :identifier), "Failed at index #{i}"
end
end
it 'formats for display' do
first_timezone = ActiveSupport::TimeZone.all[0]
expect(subject[0][:name]).to eq(first_timezone.name)
expect(subject[0][:offset]).to eq(first_timezone.now.utc_offset)
expect(subject[0][:identifier]).to eq(first_timezone.tzinfo.identifier)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe TimeZoneHelper, :aggregate_failures do
describe '#timezone_data' do
subject(:timezone_data) { helper.timezone_data }
it 'matches schema' do
expect(timezone_data).not_to be_empty
timezone_data.each_with_index do |timezone_hash, i|
expect(timezone_hash.keys).to contain_exactly(
:identifier,
:name,
:abbr,
:offset,
:formatted_offset
), "Failed at index #{i}"
end
end
it 'formats for display' do
tz = ActiveSupport::TimeZone.all[0]
expect(timezone_data[0]).to eq(
identifier: tz.tzinfo.identifier,
name: tz.name,
abbr: tz.tzinfo.strftime('%Z'),
offset: tz.now.utc_offset,
formatted_offset: tz.now.formatted_offset
)
end
end
end
......@@ -48,7 +48,6 @@ RSpec.describe Gitlab::ImportExport::Importer do
[
Gitlab::ImportExport::AvatarRestorer,
Gitlab::ImportExport::RepoRestorer,
Gitlab::ImportExport::WikiRestorer,
Gitlab::ImportExport::UploadsRestorer,
Gitlab::ImportExport::LfsRestorer,
Gitlab::ImportExport::StatisticsRestorer,
......@@ -65,6 +64,20 @@ RSpec.describe Gitlab::ImportExport::Importer do
end
end
it 'calls RepoRestorer with project and wiki' do
wiki_repo_path = File.join(shared.export_path, Gitlab::ImportExport.wiki_repo_bundle_filename)
repo_path = File.join(shared.export_path, Gitlab::ImportExport.project_bundle_filename)
restorer = double(Gitlab::ImportExport::RepoRestorer)
expect(Gitlab::ImportExport::RepoRestorer).to receive(:new).with(path_to_bundle: repo_path, shared: shared, project: project).and_return(restorer)
expect(Gitlab::ImportExport::RepoRestorer).to receive(:new).with(path_to_bundle: wiki_repo_path, shared: shared, project: ProjectWiki.new(project)).and_return(restorer)
expect(Gitlab::ImportExport::RepoRestorer).to receive(:new).and_call_original
expect(restorer).to receive(:restore).and_return(true).twice
importer.execute
end
context 'with sample_data_template' do
it 'initializes the Sample::TreeRestorer' do
project.create_or_update_import_data(data: { sample_data: true })
......
......@@ -5,35 +5,42 @@ require 'spec_helper'
RSpec.describe Gitlab::ImportExport::RepoRestorer do
include GitHelpers
let_it_be(:project_with_repo) do
create(:project, :repository, :wiki_repo, name: 'test-repo-restorer', path: 'test-repo-restorer').tap do |p|
p.wiki.create_page('page', 'foobar', :markdown, 'created page')
end
end
let!(:project) { create(:project) }
let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
let(:shared) { project.import_export_shared }
before do
allow(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
bundler.save
end
after do
FileUtils.rm_rf(export_path)
end
describe 'bundle a project Git repo' do
let(:user) { create(:user) }
let!(:project_with_repo) { create(:project, :repository, name: 'test-repo-restorer', path: 'test-repo-restorer') }
let!(:project) { create(:project) }
let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
let(:shared) { project.import_export_shared }
let(:bundler) { Gitlab::ImportExport::RepoSaver.new(project: project_with_repo, shared: shared) }
let(:bundle_path) { File.join(shared.export_path, Gitlab::ImportExport.project_bundle_filename) }
subject { described_class.new(path_to_bundle: bundle_path, shared: shared, project: project) }
before do
allow_next_instance_of(Gitlab::ImportExport) do |instance|
allow(instance).to receive(:storage_path).and_return(export_path)
end
bundler.save
end
after do
FileUtils.rm_rf(export_path)
Gitlab::GitalyClient::StorageSettings.allow_disk_access do
FileUtils.rm_rf(project_with_repo.repository.path_to_repo)
FileUtils.rm_rf(project.repository.path_to_repo)
end
Gitlab::Shell.new.remove_repository(project.repository_storage, project.disk_path)
end
it 'restores the repo successfully' do
expect(project.repository.exists?).to be false
expect(subject.restore).to be_truthy
expect(project.repository.empty?).to be false
end
context 'when the repository already exists' do
......@@ -53,4 +60,35 @@ RSpec.describe Gitlab::ImportExport::RepoRestorer do
end
end
end
describe 'restore a wiki Git repo' do
let(:bundler) { Gitlab::ImportExport::WikiRepoSaver.new(project: project_with_repo, shared: shared) }
let(:bundle_path) { File.join(shared.export_path, Gitlab::ImportExport.wiki_repo_bundle_filename) }
subject { described_class.new(path_to_bundle: bundle_path, shared: shared, project: ProjectWiki.new(project)) }
after do
Gitlab::Shell.new.remove_repository(project.wiki.repository_storage, project.wiki.disk_path)
end
it 'restores the wiki repo successfully' do
expect(project.wiki_repository_exists?).to be false
subject.restore
project.wiki.repository.expire_status_cache
expect(project.wiki_repository_exists?).to be true
end
describe 'no wiki in the bundle' do
let!(:project_without_wiki) { create(:project) }
let(:bundler) { Gitlab::ImportExport::WikiRepoSaver.new(project: project_without_wiki, shared: shared) }
it 'does not creates an empty wiki' do
expect(subject.restore).to be true
expect(project.wiki_repository_exists?).to be false
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::ImportExport::WikiRestorer do
describe 'restore a wiki Git repo' do
let!(:project_with_wiki) { create(:project, :wiki_repo) }
let!(:project_without_wiki) { create(:project) }
let!(:project) { create(:project) }
let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
let(:shared) { project.import_export_shared }
let(:bundler) { Gitlab::ImportExport::WikiRepoSaver.new(project: project_with_wiki, shared: shared) }
let(:bundle_path) { File.join(shared.export_path, Gitlab::ImportExport.project_bundle_filename) }
let(:restorer) do
described_class.new(path_to_bundle: bundle_path,
shared: shared,
project: project.wiki,
wiki_enabled: true)
end
before do
allow(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
bundler.save
end
after do
FileUtils.rm_rf(export_path)
Gitlab::Shell.new.remove_repository(project_with_wiki.wiki.repository_storage, project_with_wiki.wiki.disk_path)
Gitlab::Shell.new.remove_repository(project.wiki.repository_storage, project.wiki.disk_path)
end
it 'restores the wiki repo successfully' do
expect(restorer.restore).to be true
end
describe "no wiki in the bundle" do
let(:bundler) { Gitlab::ImportExport::WikiRepoSaver.new(project: project_without_wiki, shared: shared) }
it 'creates an empty wiki' do
expect(restorer.restore).to be true
expect(project.wiki_repository_exists?).to be true
end
end
end
end
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