Commit b51951ef authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents d9509ce7 be408943
......@@ -54,6 +54,7 @@ export function formatListIssues(listIssues) {
const listIssue = {
...i,
id,
fullId: i.id,
labels: i.labels?.nodes || [],
assignees: i.assignees?.nodes || [],
};
......
<script>
import { GlIcon, GlLink, GlPopover, GlLoadingIcon } from '@gitlab/ui';
import { blockingIssuablesQueries, issuableTypes } from '~/boards/constants';
import { IssueType } from '~/graphql_shared/constants';
import { TYPE_ISSUE } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { truncate } from '~/lib/utils/text_utility';
import { __, n__, s__, sprintf } from '~/locale';
......@@ -13,7 +13,7 @@ export default {
},
},
graphQLIdType: {
[issuableTypes.issue]: IssueType,
[issuableTypes.issue]: TYPE_ISSUE,
},
referenceFormatter: {
[issuableTypes.issue]: (r) => r.split('/')[1],
......
......@@ -82,7 +82,9 @@ export default {
class="boards-sidebar gl-absolute"
@close="handleClose"
>
<template #header>{{ __('Issue details') }}</template>
<template #header>
<h2 class="gl-my-0 gl-font-size-h2 gl-line-height-24">{{ __('Issue details') }}</h2>
</template>
<template #default>
<board-sidebar-title />
<sidebar-assignees-widget
......
......@@ -2,6 +2,7 @@
import { GlModal, GlAlert } from '@gitlab/ui';
import { mapGetters, mapActions, mapState } from 'vuex';
import ListLabel from '~/boards/models/label';
import { TYPE_ITERATION, TYPE_MILESTONE, TYPE_USER } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { getParameterByName } from '~/lib/utils/common_utils';
import { visitUrl } from '~/lib/utils/url_utility';
......@@ -188,21 +189,19 @@ export default {
};
},
issueBoardScopeMutationVariables() {
/* eslint-disable @gitlab/require-i18n-strings */
return {
weight: this.board.weight,
assigneeId: this.board.assignee?.id
? convertToGraphQLId('User', this.board.assignee.id)
? convertToGraphQLId(TYPE_USER, this.board.assignee.id)
: null,
milestoneId:
this.board.milestone?.id || this.board.milestone?.id === 0
? convertToGraphQLId('Milestone', this.board.milestone.id)
? convertToGraphQLId(TYPE_MILESTONE, this.board.milestone.id)
: null,
iterationId: this.board.iteration_id
? convertToGraphQLId('Iteration', this.board.iteration_id)
? convertToGraphQLId(TYPE_ITERATION, this.board.iteration_id)
: null,
};
/* eslint-enable @gitlab/require-i18n-strings */
},
boardScopeMutationVariables() {
return {
......
......@@ -60,22 +60,6 @@ export default {
},
},
methods: {
updateGlobalTodoCount(additionalTodoCount) {
const currentCount = parseInt(document.querySelector('.js-todos-count').innerText, 10);
const todoToggleEvent = new CustomEvent('todo:toggle', {
detail: {
count: Math.max(currentCount + additionalTodoCount, 0),
},
});
document.dispatchEvent(todoToggleEvent);
},
incrementGlobalTodoCount() {
this.updateGlobalTodoCount(1);
},
decrementGlobalTodoCount() {
this.updateGlobalTodoCount(-1);
},
createTodo() {
this.todoLoading = true;
return this.$apollo
......@@ -92,9 +76,6 @@ export default {
}
},
})
.then(() => {
this.incrementGlobalTodoCount();
})
.catch((err) => {
this.$emit('error', Error(CREATE_DESIGN_TODO_ERROR));
throw err;
......@@ -130,9 +111,6 @@ export default {
}
},
})
.then(() => {
this.decrementGlobalTodoCount();
})
.catch((err) => {
this.$emit('error', Error(DELETE_DESIGN_TODO_ERROR));
throw err;
......
/* eslint-disable @gitlab/require-i18n-strings */
export const IssueType = 'Issue';
export const TYPE_CI_RUNNER = 'Ci::Runner';
export const TYPE_GROUP = 'Group';
export const TYPE_ISSUE = 'Issue';
export const TYPE_ITERATION = 'Iteration';
export const TYPE_ITERATIONS_CADENCE = 'Iterations::Cadence';
export const TYPE_MERGE_REQUEST = 'MergeRequest';
export const TYPE_MILESTONE = 'Milestone';
export const TYPE_SCANNER_PROFILE = 'DastScannerProfile';
export const TYPE_SITE_PROFILE = 'DastSiteProfile';
export const TYPE_USER = 'User';
export const TYPE_VULNERABILITY = 'Vulnerability';
......@@ -17,11 +17,6 @@ export const MutationOperationMode = {
Replace: 'REPLACE',
};
/**
* Possible GraphQL entity types.
*/
export const TYPE_GROUP = 'Group';
/**
* Ids generated by GraphQL endpoints are usually in the format
* gid://gitlab/Groups/123. This method takes a type and an id
......
......@@ -11,6 +11,7 @@ import {
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import getIssuesQuery from 'ee_else_ce/issues_list/queries/get_issues.query.graphql';
import createFlash from '~/flash';
import { TYPE_USER } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue';
import IssuableByEmail from '~/issuable/components/issuable_by_email.vue';
......@@ -268,7 +269,7 @@ export default {
if (gon.current_user_id) {
preloadedAuthors.push({
id: convertToGraphQLId('User', gon.current_user_id), // eslint-disable-line @gitlab/require-i18n-strings
id: convertToGraphQLId(TYPE_USER, gon.current_user_id),
name: gon.current_user_fullname,
username: gon.current_username,
avatar_url: gon.current_user_avatar_url,
......
......@@ -105,7 +105,7 @@ export default {
return this.pipeline;
}
return unwrapPipelineData(this.pipelineProjectPath, data);
return unwrapPipelineData(this.pipelineProjectPath, JSON.parse(JSON.stringify(data)));
},
error(err) {
this.reportFailure({ type: LOAD_FAILURE, skipSentry: true });
......
......@@ -118,7 +118,7 @@ export default {
return this.currentPipeline;
}
return unwrapPipelineData(projectPath, data);
return unwrapPipelineData(projectPath, JSON.parse(JSON.stringify(data)));
},
result() {
this.loadingPipelineId = null;
......
......@@ -5,6 +5,7 @@ export const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(
{},
{
assumeImmutableResults: true,
useGet: true,
},
),
......
......@@ -4,8 +4,6 @@ export const RUNNER_PAGE_SIZE = 20;
export const I18N_DETAILS_TITLE = s__('Runners|Runner #%{runner_id}');
export const RUNNER_ENTITY_TYPE = 'Ci::Runner';
export const RUNNER_TAG_BADGE_VARIANT = 'info';
export const RUNNER_TAG_BG_CLASS = 'gl-bg-blue-100';
......
<script>
import { TYPE_CI_RUNNER } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import RunnerTypeAlert from '../components/runner_type_alert.vue';
import RunnerTypeBadge from '../components/runner_type_badge.vue';
import RunnerUpdateForm from '../components/runner_update_form.vue';
import { I18N_DETAILS_TITLE, RUNNER_ENTITY_TYPE } from '../constants';
import { I18N_DETAILS_TITLE } from '../constants';
import getRunnerQuery from '../graphql/get_runner.query.graphql';
export default {
......@@ -31,7 +32,7 @@ export default {
query: getRunnerQuery,
variables() {
return {
id: convertToGraphQLId(RUNNER_ENTITY_TYPE, this.runnerId),
id: convertToGraphQLId(TYPE_CI_RUNNER, this.runnerId),
};
},
},
......
<script>
import { GlLoadingIcon, GlTable } from '@gitlab/ui';
import createFlash from '~/flash';
import { TYPE_ISSUE, TYPE_MERGE_REQUEST } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { formatDate, parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility';
import { __ } from '~/locale';
......@@ -52,8 +53,7 @@ export default {
return this.issuableType === 'issue';
},
getGraphQLEntityType() {
// eslint-disable-next-line @gitlab/require-i18n-strings
return this.isIssue() ? 'Issue' : 'MergeRequest';
return this.isIssue() ? TYPE_ISSUE : TYPE_MERGE_REQUEST;
},
extractTimelogs(data) {
const timelogs = data?.issuable?.timelogs?.nodes || [];
......
<script>
import { GlTooltipDirective } from '@gitlab/ui';
import { produce } from 'immer';
import createFlash from '~/flash';
import { __, sprintf } from '~/locale';
import { todoQueries, TodoMutationTypes, todoMutations } from '~/sidebar/constants';
import TodoButton from '~/vue_shared/components/todo_button.vue';
export default {
components: {
TodoButton,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
issuableId: {
type: String,
required: true,
},
issuableIid: {
type: String,
required: true,
},
fullPath: {
type: String,
required: true,
},
issuableType: {
required: true,
type: String,
},
},
data() {
return {
loading: false,
};
},
apollo: {
todoId: {
query() {
return todoQueries[this.issuableType].query;
},
variables() {
return {
fullPath: this.fullPath,
iid: String(this.issuableIid),
};
},
update(data) {
return data.workspace?.issuable?.currentUserTodos.nodes[0]?.id;
},
result({ data }) {
const currentUserTodos = data.workspace?.issuable?.currentUserTodos?.nodes ?? [];
this.todoId = currentUserTodos[0]?.id;
this.$emit('todoUpdated', currentUserTodos.length > 0);
},
error() {
createFlash({
message: sprintf(__('Something went wrong while setting %{issuableType} to-do item.'), {
issuableType: this.issuableType,
}),
});
},
},
},
computed: {
todoIdQuery() {
return todoQueries[this.issuableType].query;
},
todoIdQueryVariables() {
return {
fullPath: this.fullPath,
iid: String(this.issuableIid),
};
},
isLoading() {
return this.$apollo.queries?.todoId?.loading || this.loading;
},
hasTodo() {
return Boolean(this.todoId);
},
todoMutationType() {
if (this.hasTodo) {
return TodoMutationTypes.MarkDone;
}
return TodoMutationTypes.Create;
},
},
methods: {
toggleTodo() {
this.loading = true;
this.$apollo
.mutate({
mutation: todoMutations[this.todoMutationType],
variables: {
input: {
targetId: !this.hasTodo ? this.issuableId : undefined,
id: this.hasTodo ? this.todoId : undefined,
},
},
update: (
store,
{
data: {
todoMutation: { todo },
},
},
) => {
const queryProps = {
query: this.todoIdQuery,
variables: this.todoIdQueryVariables,
};
const sourceData = store.readQuery(queryProps);
const data = produce(sourceData, (draftState) => {
draftState.workspace.issuable.currentUserTodos.nodes = this.hasTodo ? [] : [todo];
});
store.writeQuery({
data,
...queryProps,
});
},
})
.then(
({
data: {
todoMutation: { errors },
},
}) => {
if (errors.length) {
createFlash({
message: errors[0],
});
}
},
)
.catch(() => {
createFlash({
message: sprintf(__('Something went wrong while setting %{issuableType} to-do item.'), {
issuableType: this.issuableType,
}),
});
})
.finally(() => {
this.loading = false;
});
},
},
};
</script>
<template>
<div data-testid="sidebar-todo">
<todo-button
:issuable-type="issuableType"
:issuable-id="issuableId"
:is-todo="hasTodo"
:loading="isLoading"
size="small"
@click.stop.prevent="toggleTodo"
/>
</div>
</template>
......@@ -4,6 +4,7 @@ import epicDueDateQuery from '~/sidebar/queries/epic_due_date.query.graphql';
import epicParticipantsQuery from '~/sidebar/queries/epic_participants.query.graphql';
import epicStartDateQuery from '~/sidebar/queries/epic_start_date.query.graphql';
import epicSubscribedQuery from '~/sidebar/queries/epic_subscribed.query.graphql';
import epicTodoQuery from '~/sidebar/queries/epic_todo.query.graphql';
import issuableAssigneesSubscription from '~/sidebar/queries/issuable_assignees.subscription.graphql';
import issueConfidentialQuery from '~/sidebar/queries/issue_confidential.query.graphql';
import issueDueDateQuery from '~/sidebar/queries/issue_due_date.query.graphql';
......@@ -13,6 +14,8 @@ import issueTimeTrackingQuery from '~/sidebar/queries/issue_time_tracking.query.
import mergeRequestReferenceQuery from '~/sidebar/queries/merge_request_reference.query.graphql';
import mergeRequestSubscribed from '~/sidebar/queries/merge_request_subscribed.query.graphql';
import mergeRequestTimeTrackingQuery from '~/sidebar/queries/merge_request_time_tracking.query.graphql';
import todoCreateMutation from '~/sidebar/queries/todo_create.mutation.graphql';
import todoMarkDoneMutation from '~/sidebar/queries/todo_mark_done.mutation.graphql';
import updateEpicConfidentialMutation from '~/sidebar/queries/update_epic_confidential.mutation.graphql';
import updateEpicDueDateMutation from '~/sidebar/queries/update_epic_due_date.mutation.graphql';
import updateEpicStartDateMutation from '~/sidebar/queries/update_epic_start_date.mutation.graphql';
......@@ -189,3 +192,19 @@ export const issuableAttributesQueries = {
list: milestonesQueries,
},
};
export const todoQueries = {
[IssuableType.Epic]: {
query: epicTodoQuery,
},
};
export const TodoMutationTypes = {
Create: 'create',
MarkDone: 'mark-done',
};
export const todoMutations = {
[TodoMutationTypes.Create]: todoCreateMutation,
[TodoMutationTypes.MarkDone]: todoMarkDoneMutation,
};
query epicTodos($fullPath: ID!, $iid: ID) {
workspace: group(fullPath: $fullPath) {
__typename
issuable: epic(iid: $iid) {
__typename
id
currentUserTodos(state: pending) {
nodes {
id
}
}
}
}
}
mutation issuableTodoCreate($input: TodoCreateInput!) {
todoMutation: todoCreate(input: $input) {
__typename
todo {
id
}
errors
}
}
mutation issuableTodoMarkDone($input: TodoMarkDoneInput!) {
todoMutation: todoMarkDone(input: $input) {
__typename
todo {
id
}
errors
}
}
import sidebarDetailsIssueQuery from 'ee_else_ce/sidebar/queries/sidebarDetails.query.graphql';
import { TYPE_USER } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import createGqClient, { fetchPolicies } from '~/lib/graphql';
import axios from '~/lib/utils/axios_utils';
......@@ -88,7 +89,7 @@ export default class SidebarService {
return gqClient.mutate({
mutation: reviewerRereviewMutation,
variables: {
userId: convertToGraphQLId('User', `${userId}`), // eslint-disable-line @gitlab/require-i18n-strings
userId: convertToGraphQLId(TYPE_USER, `${userId}`),
projectPath: this.fullPath,
iid: this.iid.toString(),
},
......
......@@ -18,11 +18,39 @@ export default {
return this.isTodo ? __('Mark as done') : __('Add a to do');
},
},
methods: {
updateGlobalTodoCount(additionalTodoCount) {
const countContainer = document.querySelector('.js-todos-count');
if (countContainer === null) return;
const currentCount = parseInt(countContainer.innerText, 10);
const todoToggleEvent = new CustomEvent('todo:toggle', {
detail: {
count: Math.max(currentCount + additionalTodoCount, 0),
},
});
document.dispatchEvent(todoToggleEvent);
},
incrementGlobalTodoCount() {
this.updateGlobalTodoCount(1);
},
decrementGlobalTodoCount() {
this.updateGlobalTodoCount(-1);
},
onToggle(event) {
if (this.isTodo) {
this.decrementGlobalTodoCount();
} else {
this.incrementGlobalTodoCount();
}
this.$emit('click', event);
},
},
};
</script>
<template>
<gl-button v-bind="$attrs" :aria-label="buttonLabel" @click="$emit('click', $event)">
<gl-button v-bind="$attrs" :aria-label="buttonLabel" @click="onToggle($event)">
{{ buttonLabel }}
</gl-button>
</template>
......@@ -472,6 +472,10 @@
.sidebar-collapsed-icon {
display: none;
}
.gl-drawer-header {
align-items: flex-start;
}
}
.board-header-collapsed-info-icon:hover {
......
......@@ -87,7 +87,7 @@ class Wiki
end
def create_wiki_repository
repository.create_if_not_exists
change_head_to_default_branch if repository.create_if_not_exists
raise CouldNotCreateWikiError unless repository_exists?
rescue StandardError => err
......@@ -174,6 +174,7 @@ class Wiki
commit = commit_details(:created, message, title)
wiki.write_page(title, format.to_sym, content, commit)
repository.expire_status_cache if repository.empty?
after_wiki_activity
true
......@@ -248,7 +249,9 @@ class Wiki
override :default_branch
def default_branch
wiki.class.default_ref
return 'master' if Feature.disabled?(:wiki_uses_default_branch, user, default_enabled: :yaml)
super || wiki.class.default_ref(container)
end
def wiki_base_path
......@@ -320,6 +323,10 @@ class Wiki
false
end
def change_head_to_default_branch
repository.raw_repository.write_ref('HEAD', "refs/heads/#{default_branch}")
end
end
Wiki.prepend_mod_with('Wiki')
---
name: wiki_uses_default_branch
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/64891
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/334754
milestone: '14.1'
type: development
group: group::editor
default_enabled: false
......@@ -65,6 +65,9 @@ To use this feature, define a [CI/CD variable](../../ci/variables/index.md#custo
demonstrates how you might use a pre-clone step to seed the build
directory.
NOTE:
The `CI_PRE_CLONE_SCRIPT` variable does not work on Windows runners.
### `config.toml`
The full contents of our `config.toml` are:
......
......@@ -7,7 +7,8 @@ import {
GlTooltipDirective,
} from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import { convertToGraphQLId, TYPE_GROUP } from '~/graphql_shared/utils';
import { TYPE_GROUP } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import {
DEBOUNCE_DELAY,
DEVOPS_ADOPTION_GROUP_DROPDOWN_TEXT,
......
import Vue from 'vue';
import { convertToGraphQLId, TYPE_GROUP } from '~/graphql_shared/utils';
import { TYPE_GROUP } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import DevopsAdoptionApp from './components/devops_adoption_app.vue';
import { createApolloProvider } from './graphql';
......
......@@ -67,6 +67,7 @@ export function formatListEpics(listEpics) {
const listEpic = {
...i,
id,
fullId: i.id,
labels: i.labels?.nodes || [],
assignees: i.assignees?.nodes || [],
};
......
......@@ -10,10 +10,12 @@ import SidebarConfidentialityWidget from '~/sidebar/components/confidential/side
import SidebarDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue';
import SidebarParticipantsWidget from '~/sidebar/components/participants/sidebar_participants_widget.vue';
import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
import SidebarTodoWidget from '~/sidebar/components/todo_toggle/sidebar_todo_widget.vue';
export default {
components: {
GlDrawer,
SidebarTodoWidget,
BoardSidebarLabelsSelect,
BoardSidebarTitle,
SidebarConfidentialityWidget,
......@@ -52,7 +54,15 @@ export default {
:open="isSidebarOpen"
@close="handleClose"
>
<template #header>{{ __('Epic details') }}</template>
<template #header>
<h2 class="gl-mt-0 gl-mb-3 gl-font-size-h2 gl-line-height-24">{{ __('Epic details') }}</h2>
<sidebar-todo-widget
:issuable-id="activeBoardItem.fullId"
:issuable-iid="activeBoardItem.iid"
:full-path="fullPath"
:issuable-type="issuableType"
/>
</template>
<template #default>
<board-sidebar-title data-testid="sidebar-title" />
<sidebar-date-widget
......
......@@ -10,6 +10,7 @@ import {
GlFormSelect,
GlFormTextarea,
} from '@gitlab/ui';
import { TYPE_ITERATIONS_CADENCE } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { s__, __ } from '~/locale';
import createCadence from '../queries/cadence_create.mutation.graphql';
......@@ -130,7 +131,7 @@ export default {
},
variables() {
const id = this.isEdit
? convertToGraphQLId('Iterations::Cadence', this.cadenceId)
? convertToGraphQLId(TYPE_ITERATIONS_CADENCE, this.cadenceId)
: undefined;
const groupPath = this.isEdit ? undefined : this.groupPath;
......
......@@ -10,6 +10,7 @@ import {
GlLoadingIcon,
} from '@gitlab/ui';
import BurnCharts from 'ee/burndown_chart/components/burn_charts.vue';
import { TYPE_ITERATION } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { formatDate } from '~/lib/utils/datetime_utility';
import { __ } from '~/locale';
......@@ -39,15 +40,13 @@ export default {
apollo: {
iteration: {
query,
/* eslint-disable @gitlab/require-i18n-strings */
variables() {
return {
fullPath: this.fullPath,
id: convertToGraphQLId('Iteration', this.iterationId),
id: convertToGraphQLId(TYPE_ITERATION, this.iterationId),
isGroup: this.namespaceType === Namespace.Group,
};
},
/* eslint-enable @gitlab/require-i18n-strings */
update(data) {
return data[this.namespaceType]?.iterations?.nodes[0] || {};
},
......
......@@ -10,6 +10,7 @@ import {
GlLoadingIcon,
} from '@gitlab/ui';
import BurnCharts from 'ee/burndown_chart/components/burn_charts.vue';
import { TYPE_ITERATION } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { formatDate } from '~/lib/utils/datetime_utility';
import { __ } from '~/locale';
......@@ -46,15 +47,13 @@ export default {
apollo: {
iteration: {
query,
/* eslint-disable @gitlab/require-i18n-strings */
variables() {
return {
fullPath: this.fullPath,
id: convertToGraphQLId('Iteration', this.iterationId),
id: convertToGraphQLId(TYPE_ITERATION, this.iterationId),
isGroup: this.namespaceType === Namespace.Group,
};
},
/* eslint-enable @gitlab/require-i18n-strings */
update(data) {
return data[this.namespaceType]?.iterations?.nodes[0] || {};
},
......
......@@ -17,6 +17,7 @@ import * as Sentry from '@sentry/browser';
import { SCAN_TYPE } from 'ee/security_configuration/dast_scanner_profiles/constants';
import { DAST_SITE_VALIDATION_STATUS } from 'ee/security_configuration/dast_site_validation/constants';
import { initFormField } from 'ee/security_configuration/utils';
import { TYPE_SCANNER_PROFILE, TYPE_SITE_PROFILE } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { serializeFormObject } from '~/lib/utils/forms';
import { redirectTo, queryToObject } from '~/lib/utils/url_utility';
......@@ -34,8 +35,6 @@ import {
ERROR_MESSAGES,
SCANNER_PROFILES_QUERY,
SITE_PROFILES_QUERY,
TYPE_SITE_PROFILE,
TYPE_SCANNER_PROFILE,
} from '../settings';
import ScannerProfileSelector from './profile_selector/scanner_profile_selector.vue';
import SiteProfileSelector from './profile_selector/site_profile_selector.vue';
......
......@@ -27,6 +27,3 @@ export const SITE_PROFILES_QUERY = {
fetchQuery: dastSiteProfilesQuery,
fetchError: ERROR_FETCH_SITE_PROFILES,
};
export const TYPE_SITE_PROFILE = 'DastSiteProfile';
export const TYPE_SCANNER_PROFILE = 'DastScannerProfile';
<script>
import { GlButton } from '@gitlab/ui';
import vulnerabilityExternalIssueLinkCreate from 'ee/vue_shared/security_reports/graphql/vulnerabilityExternalIssueLinkCreate.mutation.graphql';
import { TYPE_VULNERABILITY } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { s__ } from '~/locale';
......@@ -33,18 +34,17 @@ export default {
async createJiraIssue() {
this.isLoading = true;
try {
/* eslint-disable @gitlab/require-i18n-strings */
const { data } = await this.$apollo.mutate({
mutation: vulnerabilityExternalIssueLinkCreate,
variables: {
input: {
externalTracker: 'JIRA',
linkType: 'CREATED',
id: convertToGraphQLId('Vulnerability', this.vulnerabilityId),
id: convertToGraphQLId(TYPE_VULNERABILITY, this.vulnerabilityId),
},
},
});
/* eslint-enable @gitlab/require-i18n-strings */
const { errors } = data.vulnerabilityExternalIssueLinkCreate;
if (errors.length > 0) {
......
......@@ -10,7 +10,7 @@ module Gitlab
def content
strong_memoize(:content) do
next unless available?
next unless pipeline_configuration_full_path
next unless pipeline_configuration_full_path.present?
path_file, path_project = pipeline_configuration_full_path.split('@', 2)
YAML.dump('include' => [{ 'project' => path_project, 'file' => path_file }])
......
......@@ -81,6 +81,33 @@ RSpec.describe 'Epic boards sidebar', :js do
end
end
context 'todo' do
it 'creates todo when clicking button' do
click_card(card)
wait_for_requests
page.within '[data-testid="sidebar-todo"]' do
click_button 'Add a to do'
wait_for_requests
expect(page).to have_content 'Mark as done'
end
end
it 'marks a todo as done' do
click_card(card)
wait_for_requests
page.within '[data-testid="sidebar-todo"]' do
click_button 'Add a to do'
wait_for_requests
click_button 'Mark as done'
wait_for_requests
expect(page).to have_content 'Add a to do'
end
end
end
context 'start date' do
it 'edits fixed start date' do
click_card(card)
......
......@@ -66,6 +66,7 @@ describe('formatListEpics', () => {
1: {
assignees: [],
id: 1,
fullId: 'gid://gitlab/Epic/1',
labels: [mockLabel],
title: 'epic title',
},
......
......@@ -4,7 +4,11 @@ exports[`ee/BoardContentSidebar matches the snapshot 1`] = `
<div
class="boards-sidebar gl-absolute"
>
Issue details
<h2
class="gl-my-0 gl-font-size-h2 gl-line-height-24"
>
Issue details
</h2>
<boardsidebartitle-stub />
<sidebarassigneeswidget-stub
......
......@@ -12,7 +12,8 @@ import SidebarConfidentialityWidget from '~/sidebar/components/confidential/side
import SidebarDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue';
import SidebarParticipantsWidget from '~/sidebar/components/participants/sidebar_participants_widget.vue';
import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
import { mockEpic } from '../mock_data';
import SidebarTodoWidget from '~/sidebar/components/todo_toggle/sidebar_todo_widget.vue';
import { mockFormattedBoardEpic } from '../mock_data';
describe('EpicBoardContentSidebar', () => {
let wrapper;
......@@ -22,14 +23,14 @@ describe('EpicBoardContentSidebar', () => {
store = new Vuex.Store({
state: {
sidebarType: ISSUABLE,
boardItems: { [mockEpic.id]: mockEpic },
activeId: mockEpic.id,
boardItems: { [mockFormattedBoardEpic.id]: mockFormattedBoardEpic },
activeId: mockFormattedBoardEpic.id,
issuableType: 'epic',
fullPath: 'gitlab-org',
},
getters: {
activeBoardItem: () => {
return mockEpic;
return mockFormattedBoardEpic;
},
isSidebarOpen: () => true,
...mockGetters,
......@@ -86,6 +87,10 @@ describe('EpicBoardContentSidebar', () => {
expect(wrapper.findComponent(GlDrawer).props('open')).toBe(true);
});
it('renders SidebarTodoWidget', () => {
expect(wrapper.findComponent(SidebarTodoWidget).exists()).toBe(true);
});
it('renders BoardSidebarLabelsSelect', () => {
expect(wrapper.findComponent(BoardSidebarLabelsSelect).exists()).toBe(true);
});
......@@ -127,7 +132,7 @@ describe('EpicBoardContentSidebar', () => {
expect(toggleBoardItem).toHaveBeenCalledTimes(1);
expect(toggleBoardItem).toHaveBeenCalledWith(expect.any(Object), {
boardItem: mockEpic,
boardItem: mockFormattedBoardEpic,
sidebarType: ISSUABLE,
});
});
......
......@@ -240,6 +240,22 @@ export const mockEpic = {
labels: [],
};
export const mockFormattedBoardEpic = {
fullId: 'gid://gitlab/Epic/41',
id: 41,
iid: '1',
title: 'Epic title',
state: 'opened',
webUrl: '/groups/gitlab-org/-/epics/1',
group: { fullPath: 'gitlab-org' },
descendantCounts: {
openedIssues: 3,
closedIssues: 2,
},
issues: [mockIssue],
labels: [],
};
export const mockEpics = [
{
id: 'gid://gitlab/Epic/41',
......
......@@ -4,11 +4,7 @@ require 'spec_helper'
RSpec.describe Gitlab::ImportExport::WikiRepoSaver do
let_it_be(:user) { create(:user) }
let_it_be(:group) do
create(:group, :wiki_repo).tap do |g|
g.add_owner(user)
end
end
let_it_be(:group) { create(:group) }
let(:exportable) { group }
let(:shared) { Gitlab::ImportExport::Shared.new(group) }
......@@ -16,15 +12,19 @@ RSpec.describe Gitlab::ImportExport::WikiRepoSaver do
subject { described_class.new(exportable: exportable, shared: shared) }
describe 'bundles a group wiki Git repo' do
let!(:group_wiki) { GroupWiki.new(group, user) }
let_it_be(:group_wiki) do
create(:group_wiki, user: user).tap do |wiki|
wiki.create_page('index', 'test content')
end
end
let(:group) { group_wiki.group }
let(:export_path) { "#{Dir.tmpdir}/group_tree_saver_spec" }
before do
allow_next_instance_of(Gitlab::ImportExport) do |instance|
allow(instance).to receive(:storage_path).and_return(export_path)
end
group_wiki.create_page('index', 'test content')
end
after do
......@@ -37,8 +37,6 @@ RSpec.describe Gitlab::ImportExport::WikiRepoSaver do
end
context 'when the repo is empty' do
let!(:group) { create(:group) }
it 'bundles the repo successfully' do
expect(subject.save).to be true
end
......
......@@ -3,12 +3,12 @@
require 'spec_helper'
RSpec.describe Elastic::Latest::ProjectWikiClassProxy, :elastic do
let_it_be(:project) { create(:project, :wiki_repo) }
let(:project) { create(:project, :wiki_repo) }
subject { described_class.new(project.wiki.repository) }
describe '#elastic_search_as_wiki_page' do
let_it_be(:page) { create(:wiki_page, wiki: project.wiki) }
let!(:page) { create(:wiki_page, wiki: project.wiki) }
before do
stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true)
......
......@@ -8,9 +8,6 @@ RSpec.describe ::Gitlab::Ci::Pipeline::Chain::Config::Content do
let(:content) { nil }
let(:source) { :push }
let(:command) { Gitlab::Ci::Pipeline::Chain::Command.new(project: project, content: content, source: source) }
subject { described_class.new(pipeline, command) }
let(:content_result) do
<<~EOY
---
......@@ -20,6 +17,8 @@ RSpec.describe ::Gitlab::Ci::Pipeline::Chain::Config::Content do
EOY
end
subject { described_class.new(pipeline, command) }
shared_examples 'does not include compliance pipeline configuration content' do
it do
subject.perform!
......@@ -30,26 +29,56 @@ RSpec.describe ::Gitlab::Ci::Pipeline::Chain::Config::Content do
end
end
context 'when project has compliance pipeline configuration defined' do
context 'when project has compliance label defined' do
let(:project) { create(:project, ci_config_path: ci_config_path) }
let(:compliance_group) { create(:group, :private, name: "compliance") }
let(:compliance_project) { create(:project, namespace: compliance_group, name: "hippa") }
let(:framework) { create(:compliance_framework, namespace_id: compliance_group.id, pipeline_configuration_full_path: ".compliance-gitlab-ci.yml@compliance/hippa") }
let!(:framework_project_setting) { create(:compliance_framework_project_setting, project: project, framework_id: framework.id) }
context 'when feature is available' do
before do
stub_feature_flags(ff_evaluate_group_level_compliance_pipeline: true)
stub_licensed_features(evaluate_group_level_compliance_pipeline: true)
end
it 'includes compliance pipeline configuration content' do
subject.perform!
context 'when compliance pipeline configuration is defined' do
let(:framework) do
create(:compliance_framework,
namespace: compliance_group,
pipeline_configuration_full_path: ".compliance-gitlab-ci.yml@compliance/hippa")
end
let!(:framework_project_setting) do
create(:compliance_framework_project_setting, project: project, compliance_management_framework: framework)
end
expect(pipeline.config_source).to eq 'compliance_source'
expect(pipeline.pipeline_config.content).to eq(content_result)
expect(command.config_content).to eq(content_result)
it 'includes compliance pipeline configuration content' do
subject.perform!
expect(pipeline.config_source).to eq 'compliance_source'
expect(pipeline.pipeline_config.content).to eq(content_result)
expect(command.config_content).to eq(content_result)
end
end
context 'when compliance pipeline configuration is not defined' do
let(:framework) { create(:compliance_framework, namespace: compliance_group) }
let!(:framework_project_setting) do
create(:compliance_framework_project_setting, project: project, compliance_management_framework: framework)
end
it_behaves_like 'does not include compliance pipeline configuration content'
end
context 'when compliance pipeline configuration is empty' do
let(:framework) do
create(:compliance_framework, namespace: compliance_group, pipeline_configuration_full_path: '')
end
let!(:framework_project_setting) do
create(:compliance_framework_project_setting, project: project, compliance_management_framework: framework)
end
it_behaves_like 'does not include compliance pipeline configuration content'
end
end
......
......@@ -33,8 +33,8 @@ RSpec.describe Gitlab::Elastic::ProjectSearchResults, :elastic, :clean_gitlab_re
end
describe "search", :sidekiq_inline do
let_it_be(:project) { create(:project, :public, :repository, :wiki_repo) }
let_it_be(:private_project) { create(:project, :repository, :wiki_repo) }
let(:project) { create(:project, :public, :repository, :wiki_repo) }
let(:private_project) { create(:project, :repository, :wiki_repo) }
before do
[project, private_project].each do |p|
......
......@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe ProjectWiki, :elastic do
let_it_be(:project) { create(:project, :wiki_repo) }
let(:project) { create(:project, :wiki_repo) }
before do
stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true)
......
......@@ -54,8 +54,11 @@ module Gitlab
attr_reader :repository
def self.default_ref
'master'
# TODO remove argument when issue
# https://gitlab.com/gitlab-org/gitlab/-/issues/329190
# is closed.
def self.default_ref(container = nil)
Gitlab::DefaultBranch.value(object: container)
end
# Initialize with a Gitlab::Git::Repository instance
......
......@@ -51,6 +51,10 @@ module Sidebars
end
def labels_menu_item
unless can?(context.current_user, :read_label, context.project)
return ::Sidebars::NilMenuItem.new(item_id: :labels)
end
::Sidebars::MenuItem.new(
title: _('Labels'),
link: project_labels_path(context.project),
......@@ -60,6 +64,10 @@ module Sidebars
end
def members_menu_item
unless can?(context.current_user, :read_project_member, context.project)
return ::Sidebars::NilMenuItem.new(item_id: :members)
end
::Sidebars::MenuItem.new(
title: _('Members'),
link: project_project_members_path(context.project),
......
......@@ -30351,6 +30351,9 @@ msgstr ""
msgid "Something went wrong while setting %{issuableType} notifications."
msgstr ""
msgid "Something went wrong while setting %{issuableType} to-do item."
msgstr ""
msgid "Something went wrong while stopping this environment. Please try again."
msgstr ""
......
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import SidebarTodoWidget from '~/sidebar/components/todo_toggle/sidebar_todo_widget.vue';
import epicTodoQuery from '~/sidebar/queries/epic_todo.query.graphql';
import TodoButton from '~/vue_shared/components/todo_button.vue';
import { todosResponse, noTodosResponse } from '../../mock_data';
jest.mock('~/flash');
Vue.use(VueApollo);
describe('Sidebar Todo Widget', () => {
let wrapper;
let fakeApollo;
const findTodoButton = () => wrapper.findComponent(TodoButton);
const createComponent = ({
todosQueryHandler = jest.fn().mockResolvedValue(noTodosResponse),
} = {}) => {
fakeApollo = createMockApollo([[epicTodoQuery, todosQueryHandler]]);
wrapper = shallowMount(SidebarTodoWidget, {
apolloProvider: fakeApollo,
provide: {
canUpdate: true,
},
propsData: {
fullPath: 'group',
issuableIid: '1',
issuableId: 'gid://gitlab/Epic/4',
issuableType: 'epic',
},
});
};
afterEach(() => {
wrapper.destroy();
fakeApollo = null;
});
describe('when user does not have a todo for the issuable', () => {
beforeEach(() => {
createComponent();
return waitForPromises();
});
it('passes false isTodo prop to Todo button component', () => {
expect(findTodoButton().props('isTodo')).toBe(false);
});
it('emits `todoUpdated` event with a `false` payload', () => {
expect(wrapper.emitted('todoUpdated')).toEqual([[false]]);
});
});
describe('when user has a todo for the issuable', () => {
beforeEach(() => {
createComponent({
todosQueryHandler: jest.fn().mockResolvedValue(todosResponse),
});
return waitForPromises();
});
it('passes true isTodo prop to Todo button component', () => {
expect(findTodoButton().props('isTodo')).toBe(true);
});
it('emits `todoUpdated` event with a `true` payload', () => {
expect(wrapper.emitted('todoUpdated')).toEqual([[true]]);
});
});
it('displays a flash message when query is rejected', async () => {
createComponent({
todosQueryHandler: jest.fn().mockRejectedValue('Houston, we have a problem'),
});
await waitForPromises();
expect(createFlash).toHaveBeenCalled();
});
});
......@@ -609,4 +609,38 @@ export const issuableTimeTrackingResponse = {
},
};
export const todosResponse = {
data: {
workspace: {
__typename: 'Group',
issuable: {
__typename: 'Epic',
id: 'gid://gitlab/Epic/4',
currentUserTodos: {
nodes: [
{
id: 'gid://gitlab/Todo/433',
},
],
},
},
},
},
};
export const noTodosResponse = {
data: {
workspace: {
__typename: 'Group',
issuable: {
__typename: 'Epic',
id: 'gid://gitlab/Epic/4',
currentUserTodos: {
nodes: [],
},
},
},
},
};
export default mockData;
......@@ -4,6 +4,7 @@ import TodoButton from '~/vue_shared/components/todo_button.vue';
describe('Todo Button', () => {
let wrapper;
let dispatchEventSpy;
const createComponent = (props = {}, mountFn = shallowMount) => {
wrapper = mountFn(TodoButton, {
......@@ -13,8 +14,17 @@ describe('Todo Button', () => {
});
};
beforeEach(() => {
dispatchEventSpy = jest.spyOn(document, 'dispatchEvent');
jest.spyOn(document, 'querySelector').mockReturnValue({
innerText: 2,
});
});
afterEach(() => {
wrapper.destroy();
dispatchEventSpy = null;
jest.clearAllMocks();
});
it('renders GlButton', () => {
......@@ -30,6 +40,16 @@ describe('Todo Button', () => {
expect(wrapper.emitted().click).toBeTruthy();
});
it('calls dispatchDocumentEvent to update global To-Do counter correctly', () => {
createComponent({}, mount);
wrapper.find(GlButton).trigger('click');
const dispatchedEvent = dispatchEventSpy.mock.calls[0][0];
expect(dispatchEventSpy).toHaveBeenCalledTimes(1);
expect(dispatchedEvent.detail).toEqual({ count: 1 });
expect(dispatchedEvent.type).toBe('todo:toggle');
});
it.each`
label | isTodo
${'Mark as done'} | ${true}
......
......@@ -209,11 +209,10 @@ RSpec.describe Gitlab::ProjectSearchResults do
describe 'wiki search' do
let(:project) { create(:project, :public, :wiki_repo) }
let(:wiki) { build(:project_wiki, project: project) }
before do
wiki.create_page('Files/Title', 'Content')
wiki.create_page('CHANGELOG', 'Files example')
project.wiki.create_page('Files/Title', 'Content')
project.wiki.create_page('CHANGELOG', 'Files example')
end
it_behaves_like 'general blob search', 'wiki', 'wiki_blobs' do
......
......@@ -4,12 +4,11 @@ require 'spec_helper'
RSpec.describe Gitlab::WikiFileFinder do
describe '#find' do
let(:project) { create(:project, :public, :wiki_repo) }
let(:wiki) { build(:project_wiki, project: project) }
before do
wiki.create_page('Files/Title', 'Content')
wiki.create_page('CHANGELOG', 'Files example')
let_it_be(:project) do
create(:project, :public, :wiki_repo).tap do |project|
project.wiki.create_page('Files/Title', 'Content')
project.wiki.create_page('CHANGELOG', 'Files example')
end
end
it_behaves_like 'file finder' do
......
......@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Sidebars::Projects::Menus::ProjectInformationMenu do
let_it_be(:project) { create(:project, :repository) }
let_it_be_with_reload(:project) { create(:project, :repository) }
let(:user) { project.owner }
let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project) }
......@@ -21,12 +21,43 @@ RSpec.describe Sidebars::Projects::Menus::ProjectInformationMenu do
let(:item_id) { :labels }
specify { is_expected.not_to be_nil }
context 'when merge requests are disabled' do
before do
project.project_feature.update_attribute(:merge_requests_access_level, Featurable::DISABLED)
end
specify { is_expected.not_to be_nil }
end
context 'when issues are disabled' do
before do
project.project_feature.update_attribute(:issues_access_level, Featurable::DISABLED)
end
specify { is_expected.not_to be_nil }
end
context 'when merge requests and issues are disabled' do
before do
project.project_feature.update_attribute(:merge_requests_access_level, Featurable::DISABLED)
project.project_feature.update_attribute(:issues_access_level, Featurable::DISABLED)
end
specify { is_expected.to be_nil }
end
end
describe 'Members' do
let(:item_id) { :members }
specify { is_expected.not_to be_nil }
describe 'when the user does not have access' do
let(:user) { nil }
specify { is_expected.to be_nil }
end
end
end
end
......@@ -5,11 +5,12 @@ require 'spec_helper'
RSpec.describe Git::WikiPushService, services: true do
include RepoHelpers
let_it_be(:current_user) { create(:user) }
let_it_be(:key_id) { create(:key, user: current_user).shell_id }
let_it_be(:wiki) { create(:project_wiki) }
let_it_be(:current_user) { wiki.container.default_owner }
let_it_be(:git_wiki) { wiki.wiki }
let_it_be(:repository) { wiki.repository }
let(:wiki) { create(:project_wiki, user: current_user) }
let(:git_wiki) { wiki.wiki }
let(:repository) { wiki.repository }
describe '#execute' do
it 'executes model-specific callbacks' do
......
......@@ -2,6 +2,7 @@
RSpec.shared_examples 'wiki model' do
let_it_be(:user) { create(:user, :commit_email) }
let(:wiki_container) { raise NotImplementedError }
let(:wiki_container_without_repo) { raise NotImplementedError }
let(:wiki_lfs_enabled) { false }
......@@ -536,4 +537,94 @@ RSpec.shared_examples 'wiki model' do
expect(subject.hook_attrs.keys).to contain_exactly(:web_url, :git_ssh_url, :git_http_url, :path_with_namespace, :default_branch)
end
end
describe '#default_branch' do
subject { wiki.default_branch }
before do
allow(Gitlab::DefaultBranch).to receive(:value).and_return('main')
end
shared_examples 'feature flag wiki_uses_default_branch is disabled' do
it 'returns "master"' do
stub_feature_flags(wiki_uses_default_branch: false)
expect(subject).to eq 'master'
end
end
context 'when repository is not created' do
let(:wiki_container) { wiki_container_without_repo }
it 'returns the instance default branch' do
expect(subject).to eq 'main'
end
it_behaves_like 'feature flag wiki_uses_default_branch is disabled'
end
context 'when repository is empty' do
let(:wiki_container) { wiki_container_without_repo }
before do
wiki.repository.create_if_not_exists
end
it 'returns the instance default branch' do
expect(subject).to eq 'main'
end
it_behaves_like 'feature flag wiki_uses_default_branch is disabled'
end
context 'when repository is not empty' do
it 'returns the repository default branch' do
wiki.create_page('index', 'test content')
expect(subject).to eq wiki.repository.root_ref
end
end
end
describe '#create_wiki_repository' do
subject { wiki.create_wiki_repository }
context 'when repository is not created' do
let(:wiki_container) { wiki_container_without_repo }
let(:head_path) { Rails.root.join(TestEnv.repos_path, "#{wiki.disk_path}.git", 'HEAD') }
let(:default_branch) { 'foo' }
it 'changes the HEAD reference to the default branch' do
expect(wiki.empty?).to eq true
allow(Gitlab::CurrentSettings).to receive(:default_branch_name).and_return(default_branch)
subject
expect(File.read(head_path).squish).to eq "ref: refs/heads/#{default_branch}"
end
end
context 'when repository is empty' do
let(:wiki_container) { wiki_container_without_repo }
it 'does nothing' do
wiki.repository.create_if_not_exists
expect(wiki).not_to receive(:change_head_to_default_branch)
subject
end
end
context 'when repository is not empty' do
it 'does nothing' do
wiki.create_page('index', 'test content')
expect(wiki).not_to receive(:change_head_to_default_branch)
subject
end
end
end
end
......@@ -62,7 +62,7 @@ RSpec.describe 'search/_results' do
let_it_be(:merge_request) { create(:merge_request, title: '*', source_project: project, target_project: project) }
let_it_be(:milestone) { create(:milestone, title: '*', project: project) }
let_it_be(:note) { create(:discussion_note_on_issue, project: project, note: '*') }
let_it_be(:wiki_blob) { create(:wiki_page, project: project, content: '*') }
let_it_be(:wiki_blob) { create(:wiki_page, wiki: project.wiki, content: '*') }
let_it_be(:user) { create(:admin) }
%w[issues merge_requests].each do |search_scope|
......
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