Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
bf593ae68b
commit
7073275386
|
|
@ -1,12 +1,10 @@
|
||||||
<script>
|
<script>
|
||||||
import { GlTooltipDirective, GlDeprecatedButton } from '@gitlab/ui';
|
import { GlTooltipDirective, GlButton } from '@gitlab/ui';
|
||||||
import Icon from '~/vue_shared/components/icon.vue';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ResolveWithIssueButton',
|
name: 'ResolveWithIssueButton',
|
||||||
components: {
|
components: {
|
||||||
Icon,
|
GlButton,
|
||||||
GlDeprecatedButton,
|
|
||||||
},
|
},
|
||||||
directives: {
|
directives: {
|
||||||
GlTooltip: GlTooltipDirective,
|
GlTooltip: GlTooltipDirective,
|
||||||
|
|
@ -22,13 +20,12 @@ export default {
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="btn-group" role="group">
|
<div class="btn-group" role="group">
|
||||||
<gl-deprecated-button
|
<gl-button
|
||||||
v-gl-tooltip
|
v-gl-tooltip
|
||||||
:href="url"
|
:href="url"
|
||||||
:title="s__('MergeRequests|Resolve this thread in a new issue')"
|
:title="s__('MergeRequests|Resolve this thread in a new issue')"
|
||||||
class="new-issue-for-discussion discussion-create-issue-btn"
|
class="new-issue-for-discussion discussion-create-issue-btn"
|
||||||
>
|
icon="issue-new"
|
||||||
<icon name="issue-new" />
|
/>
|
||||||
</gl-deprecated-button>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,6 @@ import {
|
||||||
commentLineOptions,
|
commentLineOptions,
|
||||||
formatLineRange,
|
formatLineRange,
|
||||||
} from './multiline_comment_utils';
|
} from './multiline_comment_utils';
|
||||||
import MultilineCommentForm from './multiline_comment_form.vue';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'NoteableNote',
|
name: 'NoteableNote',
|
||||||
|
|
@ -34,7 +33,6 @@ export default {
|
||||||
noteActions,
|
noteActions,
|
||||||
NoteBody,
|
NoteBody,
|
||||||
TimelineEntryItem,
|
TimelineEntryItem,
|
||||||
MultilineCommentForm,
|
|
||||||
},
|
},
|
||||||
mixins: [noteable, resolvable, glFeatureFlagsMixin()],
|
mixins: [noteable, resolvable, glFeatureFlagsMixin()],
|
||||||
props: {
|
props: {
|
||||||
|
|
@ -147,14 +145,16 @@ export default {
|
||||||
return getEndLineNumber(this.lineRange);
|
return getEndLineNumber(this.lineRange);
|
||||||
},
|
},
|
||||||
showMultiLineComment() {
|
showMultiLineComment() {
|
||||||
if (!this.glFeatures.multilineComments || !this.discussionRoot) return false;
|
if (
|
||||||
if (this.isEditing) return true;
|
!this.glFeatures.multilineComments ||
|
||||||
|
!this.discussionRoot ||
|
||||||
|
this.startLineNumber.length === 0 ||
|
||||||
|
this.endLineNumber.length === 0
|
||||||
|
)
|
||||||
|
return false;
|
||||||
|
|
||||||
return this.line && this.startLineNumber !== this.endLineNumber;
|
return this.line && this.startLineNumber !== this.endLineNumber;
|
||||||
},
|
},
|
||||||
showMultilineCommentForm() {
|
|
||||||
return Boolean(this.isEditing && this.note.position && this.diffFile && this.line);
|
|
||||||
},
|
|
||||||
commentLineOptions() {
|
commentLineOptions() {
|
||||||
const sideA = this.line.type === 'new' ? 'right' : 'left';
|
const sideA = this.line.type === 'new' ? 'right' : 'left';
|
||||||
const sideB = sideA === 'left' ? 'right' : 'left';
|
const sideB = sideA === 'left' ? 'right' : 'left';
|
||||||
|
|
@ -344,28 +344,19 @@ export default {
|
||||||
:data-note-id="note.id"
|
:data-note-id="note.id"
|
||||||
class="note note-wrapper qa-noteable-note-item"
|
class="note note-wrapper qa-noteable-note-item"
|
||||||
>
|
>
|
||||||
<div v-if="showMultiLineComment" data-testid="multiline-comment">
|
<div
|
||||||
<multiline-comment-form
|
v-if="showMultiLineComment"
|
||||||
v-if="showMultilineCommentForm"
|
data-testid="multiline-comment"
|
||||||
v-model="commentLineStart"
|
class="gl-mb-3 gl-text-gray-700 gl-border-gray-200 gl-border-b-solid gl-border-b-1 gl-pb-3"
|
||||||
:line="line"
|
>
|
||||||
:comment-line-options="commentLineOptions"
|
<gl-sprintf :message="__('Comment on lines %{startLine} to %{endLine}')">
|
||||||
:line-range="note.position.line_range"
|
<template #startLine>
|
||||||
class="gl-mb-3 gl-text-gray-700 gl-pb-3"
|
<span :class="getLineClasses(startLineNumber)">{{ startLineNumber }}</span>
|
||||||
/>
|
</template>
|
||||||
<div
|
<template #endLine>
|
||||||
v-else
|
<span :class="getLineClasses(endLineNumber)">{{ endLineNumber }}</span>
|
||||||
class="gl-mb-3 gl-text-gray-700 gl-border-gray-200 gl-border-b-solid gl-border-b-1 gl-pb-3"
|
</template>
|
||||||
>
|
</gl-sprintf>
|
||||||
<gl-sprintf :message="__('Comment on lines %{startLine} to %{endLine}')">
|
|
||||||
<template #startLine>
|
|
||||||
<span :class="getLineClasses(startLineNumber)">{{ startLineNumber }}</span>
|
|
||||||
</template>
|
|
||||||
<template #endLine>
|
|
||||||
<span :class="getLineClasses(endLineNumber)">{{ endLineNumber }}</span>
|
|
||||||
</template>
|
|
||||||
</gl-sprintf>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-once class="timeline-icon">
|
<div v-once class="timeline-icon">
|
||||||
<user-avatar-link
|
<user-avatar-link
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import { mapState, mapActions, mapGetters } from 'vuex';
|
||||||
import { GlButton, GlFormInput, GlFormGroup } from '@gitlab/ui';
|
import { GlButton, GlFormInput, GlFormGroup } from '@gitlab/ui';
|
||||||
import { __, sprintf } from '~/locale';
|
import { __, sprintf } from '~/locale';
|
||||||
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
|
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
|
||||||
import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
|
|
||||||
import { BACK_URL_PARAM } from '~/releases/constants';
|
import { BACK_URL_PARAM } from '~/releases/constants';
|
||||||
import { getParameterByName } from '~/lib/utils/common_utils';
|
import { getParameterByName } from '~/lib/utils/common_utils';
|
||||||
import AssetLinksForm from './asset_links_form.vue';
|
import AssetLinksForm from './asset_links_form.vue';
|
||||||
|
|
@ -22,9 +21,6 @@ export default {
|
||||||
MilestoneCombobox,
|
MilestoneCombobox,
|
||||||
TagField,
|
TagField,
|
||||||
},
|
},
|
||||||
directives: {
|
|
||||||
autofocusonshow,
|
|
||||||
},
|
|
||||||
mixins: [glFeatureFlagsMixin()],
|
mixins: [glFeatureFlagsMixin()],
|
||||||
computed: {
|
computed: {
|
||||||
...mapState('detail', [
|
...mapState('detail', [
|
||||||
|
|
@ -40,9 +36,9 @@ export default {
|
||||||
'manageMilestonesPath',
|
'manageMilestonesPath',
|
||||||
'projectId',
|
'projectId',
|
||||||
]),
|
]),
|
||||||
...mapGetters('detail', ['isValid']),
|
...mapGetters('detail', ['isValid', 'isExistingRelease']),
|
||||||
showForm() {
|
showForm() {
|
||||||
return !this.isFetchingRelease && !this.fetchError;
|
return Boolean(!this.isFetchingRelease && !this.fetchError && this.release);
|
||||||
},
|
},
|
||||||
subtitleText() {
|
subtitleText() {
|
||||||
return sprintf(
|
return sprintf(
|
||||||
|
|
@ -86,6 +82,9 @@ export default {
|
||||||
showAssetLinksForm() {
|
showAssetLinksForm() {
|
||||||
return this.glFeatures.releaseAssetLinkEditing;
|
return this.glFeatures.releaseAssetLinkEditing;
|
||||||
},
|
},
|
||||||
|
saveButtonLabel() {
|
||||||
|
return this.isExistingRelease ? __('Save changes') : __('Create release');
|
||||||
|
},
|
||||||
isSaveChangesDisabled() {
|
isSaveChangesDisabled() {
|
||||||
return this.isUpdatingRelease || !this.isValid;
|
return this.isUpdatingRelease || !this.isValid;
|
||||||
},
|
},
|
||||||
|
|
@ -102,13 +101,17 @@ export default {
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
created() {
|
mounted() {
|
||||||
this.fetchRelease();
|
// eslint-disable-next-line promise/catch-or-return
|
||||||
|
this.initializeRelease().then(() => {
|
||||||
|
// Focus the first non-disabled input element
|
||||||
|
this.$el.querySelector('input:enabled').focus();
|
||||||
|
});
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions('detail', [
|
...mapActions('detail', [
|
||||||
'fetchRelease',
|
'initializeRelease',
|
||||||
'updateRelease',
|
'saveRelease',
|
||||||
'updateReleaseTitle',
|
'updateReleaseTitle',
|
||||||
'updateReleaseNotes',
|
'updateReleaseNotes',
|
||||||
'updateReleaseMilestones',
|
'updateReleaseMilestones',
|
||||||
|
|
@ -119,7 +122,7 @@ export default {
|
||||||
<template>
|
<template>
|
||||||
<div class="d-flex flex-column">
|
<div class="d-flex flex-column">
|
||||||
<p class="pt-3 js-subtitle-text" v-html="subtitleText"></p>
|
<p class="pt-3 js-subtitle-text" v-html="subtitleText"></p>
|
||||||
<form v-if="showForm" @submit.prevent="updateRelease()">
|
<form v-if="showForm" @submit.prevent="saveRelease()">
|
||||||
<tag-field />
|
<tag-field />
|
||||||
<gl-form-group>
|
<gl-form-group>
|
||||||
<label for="release-title">{{ __('Release title') }}</label>
|
<label for="release-title">{{ __('Release title') }}</label>
|
||||||
|
|
@ -127,8 +130,6 @@ export default {
|
||||||
id="release-title"
|
id="release-title"
|
||||||
ref="releaseTitleInput"
|
ref="releaseTitleInput"
|
||||||
v-model="releaseTitle"
|
v-model="releaseTitle"
|
||||||
v-autofocusonshow
|
|
||||||
autofocus
|
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
/>
|
/>
|
||||||
|
|
@ -162,8 +163,8 @@ export default {
|
||||||
data-supports-quick-actions="false"
|
data-supports-quick-actions="false"
|
||||||
:aria-label="__('Release notes')"
|
:aria-label="__('Release notes')"
|
||||||
:placeholder="__('Write your release notes or drag your files here…')"
|
:placeholder="__('Write your release notes or drag your files here…')"
|
||||||
@keydown.meta.enter="updateRelease()"
|
@keydown.meta.enter="saveRelease()"
|
||||||
@keydown.ctrl.enter="updateRelease()"
|
@keydown.ctrl.enter="saveRelease()"
|
||||||
></textarea>
|
></textarea>
|
||||||
</template>
|
</template>
|
||||||
</markdown-field>
|
</markdown-field>
|
||||||
|
|
@ -178,10 +179,11 @@ export default {
|
||||||
category="primary"
|
category="primary"
|
||||||
variant="success"
|
variant="success"
|
||||||
type="submit"
|
type="submit"
|
||||||
:aria-label="__('Save changes')"
|
|
||||||
:disabled="isSaveChangesDisabled"
|
:disabled="isSaveChangesDisabled"
|
||||||
>{{ __('Save changes') }}</gl-button
|
data-testid="submit-button"
|
||||||
>
|
>
|
||||||
|
{{ saveButtonLabel }}
|
||||||
|
</gl-button>
|
||||||
<gl-button :href="cancelPath" class="js-cancel-button">{{ __('Cancel') }}</gl-button>
|
<gl-button :href="cancelPath" class="js-cancel-button">{{ __('Cancel') }}</gl-button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
||||||
|
|
@ -3,125 +3,48 @@ import api from '~/api';
|
||||||
import createFlash from '~/flash';
|
import createFlash from '~/flash';
|
||||||
import { s__ } from '~/locale';
|
import { s__ } from '~/locale';
|
||||||
import { redirectTo } from '~/lib/utils/url_utility';
|
import { redirectTo } from '~/lib/utils/url_utility';
|
||||||
import {
|
import { releaseToApiJson, apiJsonToRelease } from '~/releases/util';
|
||||||
convertObjectPropsToCamelCase,
|
|
||||||
convertObjectPropsToSnakeCase,
|
|
||||||
} from '~/lib/utils/common_utils';
|
|
||||||
|
|
||||||
export const requestRelease = ({ commit }) => commit(types.REQUEST_RELEASE);
|
export const initializeRelease = ({ commit, dispatch, getters }) => {
|
||||||
export const receiveReleaseSuccess = ({ commit }, data) =>
|
if (getters.isExistingRelease) {
|
||||||
commit(types.RECEIVE_RELEASE_SUCCESS, data);
|
// When editing an existing release,
|
||||||
export const receiveReleaseError = ({ commit }, error) => {
|
// fetch the release object from the API
|
||||||
commit(types.RECEIVE_RELEASE_ERROR, error);
|
return dispatch('fetchRelease');
|
||||||
createFlash(s__('Release|Something went wrong while getting the release details'));
|
}
|
||||||
|
|
||||||
|
// When creating a new release, initialize the
|
||||||
|
// store with an empty release object
|
||||||
|
commit(types.INITIALIZE_EMPTY_RELEASE);
|
||||||
|
return Promise.resolve();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchRelease = ({ dispatch, state }) => {
|
export const fetchRelease = ({ commit, state }) => {
|
||||||
dispatch('requestRelease');
|
commit(types.REQUEST_RELEASE);
|
||||||
|
|
||||||
return api
|
return api
|
||||||
.release(state.projectId, state.tagName)
|
.release(state.projectId, state.tagName)
|
||||||
.then(({ data }) => {
|
.then(({ data }) => {
|
||||||
const release = {
|
commit(types.RECEIVE_RELEASE_SUCCESS, apiJsonToRelease(data));
|
||||||
...data,
|
|
||||||
milestones: data.milestones || [],
|
|
||||||
};
|
|
||||||
|
|
||||||
dispatch('receiveReleaseSuccess', convertObjectPropsToCamelCase(release, { deep: true }));
|
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
dispatch('receiveReleaseError', error);
|
commit(types.RECEIVE_RELEASE_ERROR, error);
|
||||||
|
createFlash(s__('Release|Something went wrong while getting the release details'));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateReleaseTagName = ({ commit }, tagName) =>
|
export const updateReleaseTagName = ({ commit }, tagName) =>
|
||||||
commit(types.UPDATE_RELEASE_TAG_NAME, tagName);
|
commit(types.UPDATE_RELEASE_TAG_NAME, tagName);
|
||||||
|
|
||||||
export const updateCreateFrom = ({ commit }, createFrom) =>
|
export const updateCreateFrom = ({ commit }, createFrom) =>
|
||||||
commit(types.UPDATE_CREATE_FROM, createFrom);
|
commit(types.UPDATE_CREATE_FROM, createFrom);
|
||||||
|
|
||||||
export const updateReleaseTitle = ({ commit }, title) => commit(types.UPDATE_RELEASE_TITLE, title);
|
export const updateReleaseTitle = ({ commit }, title) => commit(types.UPDATE_RELEASE_TITLE, title);
|
||||||
|
|
||||||
export const updateReleaseNotes = ({ commit }, notes) => commit(types.UPDATE_RELEASE_NOTES, notes);
|
export const updateReleaseNotes = ({ commit }, notes) => commit(types.UPDATE_RELEASE_NOTES, notes);
|
||||||
|
|
||||||
export const updateReleaseMilestones = ({ commit }, milestones) =>
|
export const updateReleaseMilestones = ({ commit }, milestones) =>
|
||||||
commit(types.UPDATE_RELEASE_MILESTONES, milestones);
|
commit(types.UPDATE_RELEASE_MILESTONES, milestones);
|
||||||
|
|
||||||
export const requestUpdateRelease = ({ commit }) => commit(types.REQUEST_UPDATE_RELEASE);
|
|
||||||
export const receiveUpdateReleaseSuccess = ({ commit, state, rootState }) => {
|
|
||||||
commit(types.RECEIVE_UPDATE_RELEASE_SUCCESS);
|
|
||||||
redirectTo(
|
|
||||||
rootState.featureFlags.releaseShowPage ? state.release._links.self : state.releasesPagePath,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
export const receiveUpdateReleaseError = ({ commit }, error) => {
|
|
||||||
commit(types.RECEIVE_UPDATE_RELEASE_ERROR, error);
|
|
||||||
createFlash(s__('Release|Something went wrong while saving the release details'));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateRelease = ({ dispatch, state, getters }) => {
|
|
||||||
dispatch('requestUpdateRelease');
|
|
||||||
|
|
||||||
const { release } = state;
|
|
||||||
const milestones = release.milestones ? release.milestones.map(milestone => milestone.title) : [];
|
|
||||||
|
|
||||||
const updatedRelease = convertObjectPropsToSnakeCase(
|
|
||||||
{
|
|
||||||
name: release.name,
|
|
||||||
description: release.description,
|
|
||||||
milestones,
|
|
||||||
},
|
|
||||||
{ deep: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
api
|
|
||||||
.updateRelease(state.projectId, state.tagName, updatedRelease)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Currently, we delete all existing links and then
|
|
||||||
* recreate new ones on each edit. This is because the
|
|
||||||
* REST API doesn't support bulk updating of Release links,
|
|
||||||
* and updating individual links can lead to validation
|
|
||||||
* race conditions (in particular, the "URLs must be unique")
|
|
||||||
* constraint.
|
|
||||||
*
|
|
||||||
* This isn't ideal since this is no longer an atomic
|
|
||||||
* operation - parts of it can fail while others succeed,
|
|
||||||
* leaving the Release in an inconsistent state.
|
|
||||||
*
|
|
||||||
* This logic should be refactored to use GraphQL once
|
|
||||||
* https://gitlab.com/gitlab-org/gitlab/-/issues/208702
|
|
||||||
* is closed.
|
|
||||||
*/
|
|
||||||
|
|
||||||
.then(() => {
|
|
||||||
// Delete all links currently associated with this Release
|
|
||||||
return Promise.all(
|
|
||||||
getters.releaseLinksToDelete.map(l =>
|
|
||||||
api.deleteReleaseLink(state.projectId, release.tagName, l.id),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
// Create a new link for each link in the form
|
|
||||||
return Promise.all(
|
|
||||||
getters.releaseLinksToCreate.map(l =>
|
|
||||||
api.createReleaseLink(
|
|
||||||
state.projectId,
|
|
||||||
release.tagName,
|
|
||||||
convertObjectPropsToSnakeCase(l, { deep: true }),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.then(() => dispatch('receiveUpdateReleaseSuccess'))
|
|
||||||
.catch(error => {
|
|
||||||
dispatch('receiveUpdateReleaseError', error);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const navigateToReleasesPage = ({ state }) => {
|
|
||||||
redirectTo(state.releasesPagePath);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const addEmptyAssetLink = ({ commit }) => {
|
export const addEmptyAssetLink = ({ commit }) => {
|
||||||
commit(types.ADD_EMPTY_ASSET_LINK);
|
commit(types.ADD_EMPTY_ASSET_LINK);
|
||||||
};
|
};
|
||||||
|
|
@ -141,3 +64,95 @@ export const updateAssetLinkType = ({ commit }, { linkIdToUpdate, newType }) =>
|
||||||
export const removeAssetLink = ({ commit }, linkIdToRemove) => {
|
export const removeAssetLink = ({ commit }, linkIdToRemove) => {
|
||||||
commit(types.REMOVE_ASSET_LINK, linkIdToRemove);
|
commit(types.REMOVE_ASSET_LINK, linkIdToRemove);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const receiveSaveReleaseSuccess = ({ commit, state, rootState }, release) => {
|
||||||
|
commit(types.RECEIVE_SAVE_RELEASE_SUCCESS);
|
||||||
|
redirectTo(rootState.featureFlags.releaseShowPage ? release._links.self : state.releasesPagePath);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const saveRelease = ({ commit, dispatch, getters }) => {
|
||||||
|
commit(types.REQUEST_SAVE_RELEASE);
|
||||||
|
|
||||||
|
dispatch(getters.isExistingRelease ? 'updateRelease' : 'createRelease');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createRelease = ({ commit, dispatch, state, getters }) => {
|
||||||
|
const apiJson = releaseToApiJson(
|
||||||
|
{
|
||||||
|
...state.release,
|
||||||
|
assets: {
|
||||||
|
links: getters.releaseLinksToCreate,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
state.createFrom,
|
||||||
|
);
|
||||||
|
|
||||||
|
return api
|
||||||
|
.createRelease(state.projectId, apiJson)
|
||||||
|
.then(({ data }) => {
|
||||||
|
dispatch('receiveSaveReleaseSuccess', apiJsonToRelease(data));
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
commit(types.RECEIVE_SAVE_RELEASE_ERROR, error);
|
||||||
|
createFlash(s__('Release|Something went wrong while creating a new release'));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateRelease = ({ commit, dispatch, state, getters }) => {
|
||||||
|
const apiJson = releaseToApiJson({
|
||||||
|
...state.release,
|
||||||
|
assets: {
|
||||||
|
links: getters.releaseLinksToCreate,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
let updatedRelease = null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
api
|
||||||
|
.updateRelease(state.projectId, state.tagName, apiJson)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Currently, we delete all existing links and then
|
||||||
|
* recreate new ones on each edit. This is because the
|
||||||
|
* REST API doesn't support bulk updating of Release links,
|
||||||
|
* and updating individual links can lead to validation
|
||||||
|
* race conditions (in particular, the "URLs must be unique")
|
||||||
|
* constraint.
|
||||||
|
*
|
||||||
|
* This isn't ideal since this is no longer an atomic
|
||||||
|
* operation - parts of it can fail while others succeed,
|
||||||
|
* leaving the Release in an inconsistent state.
|
||||||
|
*
|
||||||
|
* This logic should be refactored to use GraphQL once
|
||||||
|
* https://gitlab.com/gitlab-org/gitlab/-/issues/208702
|
||||||
|
* is closed.
|
||||||
|
*/
|
||||||
|
.then(({ data }) => {
|
||||||
|
// Save this response since we need it later in the Promise chain
|
||||||
|
updatedRelease = data;
|
||||||
|
|
||||||
|
// Delete all links currently associated with this Release
|
||||||
|
return Promise.all(
|
||||||
|
getters.releaseLinksToDelete.map(l =>
|
||||||
|
api.deleteReleaseLink(state.projectId, state.release.tagName, l.id),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
// Create a new link for each link in the form
|
||||||
|
return Promise.all(
|
||||||
|
apiJson.assets.links.map(l =>
|
||||||
|
api.createReleaseLink(state.projectId, state.release.tagName, l),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
dispatch('receiveSaveReleaseSuccess', apiJsonToRelease(updatedRelease));
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
commit(types.RECEIVE_SAVE_RELEASE_ERROR, error);
|
||||||
|
createFlash(s__('Release|Something went wrong while saving the release details'));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { hasContent } from '~/lib/utils/text_utility';
|
||||||
* `false` if the app is creating a new release.
|
* `false` if the app is creating a new release.
|
||||||
*/
|
*/
|
||||||
export const isExistingRelease = state => {
|
export const isExistingRelease = state => {
|
||||||
return Boolean(state.originalRelease);
|
return Boolean(state.tagName);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
export const INITIALIZE_EMPTY_RELEASE = 'INITIALIZE_EMPTY_RELEASE';
|
||||||
|
|
||||||
export const REQUEST_RELEASE = 'REQUEST_RELEASE';
|
export const REQUEST_RELEASE = 'REQUEST_RELEASE';
|
||||||
export const RECEIVE_RELEASE_SUCCESS = 'RECEIVE_RELEASE_SUCCESS';
|
export const RECEIVE_RELEASE_SUCCESS = 'RECEIVE_RELEASE_SUCCESS';
|
||||||
export const RECEIVE_RELEASE_ERROR = 'RECEIVE_RELEASE_ERROR';
|
export const RECEIVE_RELEASE_ERROR = 'RECEIVE_RELEASE_ERROR';
|
||||||
|
|
@ -8,9 +10,9 @@ export const UPDATE_RELEASE_TITLE = 'UPDATE_RELEASE_TITLE';
|
||||||
export const UPDATE_RELEASE_NOTES = 'UPDATE_RELEASE_NOTES';
|
export const UPDATE_RELEASE_NOTES = 'UPDATE_RELEASE_NOTES';
|
||||||
export const UPDATE_RELEASE_MILESTONES = 'UPDATE_RELEASE_MILESTONES';
|
export const UPDATE_RELEASE_MILESTONES = 'UPDATE_RELEASE_MILESTONES';
|
||||||
|
|
||||||
export const REQUEST_UPDATE_RELEASE = 'REQUEST_UPDATE_RELEASE';
|
export const REQUEST_SAVE_RELEASE = 'REQUEST_SAVE_RELEASE';
|
||||||
export const RECEIVE_UPDATE_RELEASE_SUCCESS = 'RECEIVE_UPDATE_RELEASE_SUCCESS';
|
export const RECEIVE_SAVE_RELEASE_SUCCESS = 'RECEIVE_SAVE_RELEASE_SUCCESS';
|
||||||
export const RECEIVE_UPDATE_RELEASE_ERROR = 'RECEIVE_UPDATE_RELEASE_ERROR';
|
export const RECEIVE_SAVE_RELEASE_ERROR = 'RECEIVE_SAVE_RELEASE_ERROR';
|
||||||
|
|
||||||
export const ADD_EMPTY_ASSET_LINK = 'ADD_EMPTY_ASSET_LINK';
|
export const ADD_EMPTY_ASSET_LINK = 'ADD_EMPTY_ASSET_LINK';
|
||||||
export const UPDATE_ASSET_LINK_URL = 'UPDATE_ASSET_LINK_URL';
|
export const UPDATE_ASSET_LINK_URL = 'UPDATE_ASSET_LINK_URL';
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,18 @@ const findReleaseLink = (release, id) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
[types.INITIALIZE_EMPTY_RELEASE](state) {
|
||||||
|
state.release = {
|
||||||
|
tagName: null,
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
milestones: [],
|
||||||
|
assets: {
|
||||||
|
links: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
[types.REQUEST_RELEASE](state) {
|
[types.REQUEST_RELEASE](state) {
|
||||||
state.isFetchingRelease = true;
|
state.isFetchingRelease = true;
|
||||||
},
|
},
|
||||||
|
|
@ -39,14 +51,14 @@ export default {
|
||||||
state.release.milestones = milestones;
|
state.release.milestones = milestones;
|
||||||
},
|
},
|
||||||
|
|
||||||
[types.REQUEST_UPDATE_RELEASE](state) {
|
[types.REQUEST_SAVE_RELEASE](state) {
|
||||||
state.isUpdatingRelease = true;
|
state.isUpdatingRelease = true;
|
||||||
},
|
},
|
||||||
[types.RECEIVE_UPDATE_RELEASE_SUCCESS](state) {
|
[types.RECEIVE_SAVE_RELEASE_SUCCESS](state) {
|
||||||
state.updateError = undefined;
|
state.updateError = undefined;
|
||||||
state.isUpdatingRelease = false;
|
state.isUpdatingRelease = false;
|
||||||
},
|
},
|
||||||
[types.RECEIVE_UPDATE_RELEASE_ERROR](state, error) {
|
[types.RECEIVE_SAVE_RELEASE_ERROR](state, error) {
|
||||||
state.updateError = error;
|
state.updateError = error;
|
||||||
state.isUpdatingRelease = false;
|
state.isUpdatingRelease = false;
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
import {
|
||||||
|
convertObjectPropsToCamelCase,
|
||||||
|
convertObjectPropsToSnakeCase,
|
||||||
|
} from '~/lib/utils/common_utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a release object into a JSON object that can sent to the public
|
||||||
|
* API to create or update a release.
|
||||||
|
* @param {Object} release The release object to convert
|
||||||
|
* @param {string} createFrom The ref to create a new tag from, if necessary
|
||||||
|
*/
|
||||||
|
export const releaseToApiJson = (release, createFrom = null) => {
|
||||||
|
const milestones = release.milestones ? release.milestones.map(milestone => milestone.title) : [];
|
||||||
|
|
||||||
|
return convertObjectPropsToSnakeCase(
|
||||||
|
{
|
||||||
|
tagName: release.tagName,
|
||||||
|
ref: createFrom,
|
||||||
|
name: release.name,
|
||||||
|
description: release.description,
|
||||||
|
milestones,
|
||||||
|
assets: release.assets,
|
||||||
|
},
|
||||||
|
{ deep: true },
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a JSON release object returned by the Release API
|
||||||
|
* into the structure this Vue application can work with.
|
||||||
|
* @param {Object} json The JSON object received from the release API
|
||||||
|
*/
|
||||||
|
export const apiJsonToRelease = json => {
|
||||||
|
const release = convertObjectPropsToCamelCase(json, { deep: true });
|
||||||
|
|
||||||
|
release.milestones = release.milestones || [];
|
||||||
|
|
||||||
|
return release;
|
||||||
|
};
|
||||||
|
|
@ -1,26 +1,79 @@
|
||||||
|
/**
|
||||||
|
* The purpose of this file is to modify Markdown source such that templated code (embedded ruby currently) can be temporarily wrapped and unwrapped in codeblocks:
|
||||||
|
* 1. `wrap()`: temporarily wrap in codeblocks (useful for a WYSIWYG editing experience)
|
||||||
|
* 2. `unwrap()`: undo the temporarily wrapped codeblocks (useful for Markdown editing experience and saving edits)
|
||||||
|
*
|
||||||
|
* Without this `templater`, the templated code is otherwise interpreted as Markdown content resulting in loss of spacing, indentation, escape characters, etc.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
const ticks = '```';
|
const ticks = '```';
|
||||||
const marker = 'sse';
|
const marker = 'sse';
|
||||||
const prefix = `${ticks} ${marker}\n`; // Space intentional due to https://github.com/nhn/tui.editor/blob/6bcec75c69028570d93d973aa7533090257eaae0/libs/to-mark/src/renderer.gfm.js#L26
|
const wrapPrefix = `${ticks} ${marker}\n`; // Space intentional due to https://github.com/nhn/tui.editor/blob/6bcec75c69028570d93d973aa7533090257eaae0/libs/to-mark/src/renderer.gfm.js#L26
|
||||||
const postfix = `\n${ticks}`;
|
const wrapPostfix = `\n${ticks}`;
|
||||||
const flagPrefix = `${marker}-${Date.now()}`;
|
const markPrefix = `${marker}-${Date.now()}`;
|
||||||
const template = `.| |\\t|\\n(?!(\\n|${flagPrefix}))`;
|
|
||||||
const templatedRegex = new RegExp(`(^${prefix}(${template})+?${postfix}$)`, 'gm');
|
|
||||||
|
|
||||||
const nonErbMarkupRegex = new RegExp(`^((<(?!%).+>){1}(${template})+(</.+>){1})$`, 'gm');
|
const reHelpers = {
|
||||||
const embeddedRubyBlockRegex = new RegExp(`(^<%(${template})+%>$)`, 'gm');
|
template: `.| |\\t|\\n(?!(\\n|${markPrefix}))`,
|
||||||
const embeddedRubyInlineRegex = new RegExp(`(^.*[<|<]%(${template})+$)`, 'gm');
|
openTag: '<[a-zA-Z]+.*?>',
|
||||||
|
closeTag: '</.+>',
|
||||||
|
};
|
||||||
|
const reTemplated = new RegExp(`(^${wrapPrefix}(${reHelpers.template})+?${wrapPostfix}$)`, 'gm');
|
||||||
|
const rePreexistingCodeBlocks = new RegExp(`(^${ticks}.*\\n(.|\\s)+?${ticks}$)`, 'gm');
|
||||||
|
const reHtmlMarkup = new RegExp(
|
||||||
|
`^((${reHelpers.openTag}){1}(${reHelpers.template})*(${reHelpers.closeTag}){1})$`,
|
||||||
|
'gm',
|
||||||
|
);
|
||||||
|
const reEmbeddedRubyBlock = new RegExp(`(^<%(${reHelpers.template})+%>$)`, 'gm');
|
||||||
|
const reEmbeddedRubyInline = new RegExp(`(^.*[<|<]%(${reHelpers.template})+$)`, 'gm');
|
||||||
|
|
||||||
// Order is intentional (general to specific) where HTML markup is flagged first, then ERB blocks, then inline ERB
|
const patternGroups = {
|
||||||
// Order in combo with the `flag()` algorithm is used to mitigate potential duplicate pattern matches (ERB nested in HTML for example)
|
ignore: [rePreexistingCodeBlocks],
|
||||||
const orderedPatterns = [nonErbMarkupRegex, embeddedRubyBlockRegex, embeddedRubyInlineRegex];
|
// Order is intentional (general to specific) where HTML markup is marked first, then ERB blocks, then inline ERB
|
||||||
|
// Order in combo with the `mark()` algorithm is used to mitigate potential duplicate pattern matches (ERB nested in HTML for example)
|
||||||
|
allow: [reHtmlMarkup, reEmbeddedRubyBlock, reEmbeddedRubyInline],
|
||||||
|
};
|
||||||
|
|
||||||
|
const mark = (source, groups) => {
|
||||||
|
let text = source;
|
||||||
|
let id = 0;
|
||||||
|
const hash = {};
|
||||||
|
|
||||||
|
Object.entries(groups).forEach(([groupKey, group]) => {
|
||||||
|
group.forEach(pattern => {
|
||||||
|
const matches = text.match(pattern);
|
||||||
|
if (matches) {
|
||||||
|
matches.forEach(match => {
|
||||||
|
const key = `${markPrefix}-${groupKey}-${id}`;
|
||||||
|
text = text.replace(match, key);
|
||||||
|
hash[key] = match;
|
||||||
|
id += 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return { text, hash };
|
||||||
|
};
|
||||||
|
|
||||||
|
const unmark = (text, hash) => {
|
||||||
|
let source = text;
|
||||||
|
|
||||||
|
Object.entries(hash).forEach(([key, value]) => {
|
||||||
|
const newVal = key.includes('ignore') ? value : `${wrapPrefix}${value}${wrapPostfix}`;
|
||||||
|
source = source.replace(key, newVal);
|
||||||
|
});
|
||||||
|
|
||||||
|
return source;
|
||||||
|
};
|
||||||
|
|
||||||
const unwrap = source => {
|
const unwrap = source => {
|
||||||
let text = source;
|
let text = source;
|
||||||
const matches = text.match(templatedRegex);
|
const matches = text.match(reTemplated);
|
||||||
|
|
||||||
if (matches) {
|
if (matches) {
|
||||||
matches.forEach(match => {
|
matches.forEach(match => {
|
||||||
const initial = match.replace(`${prefix}`, '').replace(`${postfix}`, '');
|
const initial = match.replace(`${wrapPrefix}`, '').replace(`${wrapPostfix}`, '');
|
||||||
text = text.replace(match, initial);
|
text = text.replace(match, initial);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -28,35 +81,9 @@ const unwrap = source => {
|
||||||
return text;
|
return text;
|
||||||
};
|
};
|
||||||
|
|
||||||
const flag = (source, patterns) => {
|
|
||||||
let text = source;
|
|
||||||
let id = 0;
|
|
||||||
const hash = {};
|
|
||||||
|
|
||||||
patterns.forEach(pattern => {
|
|
||||||
const matches = text.match(pattern);
|
|
||||||
if (matches) {
|
|
||||||
matches.forEach(match => {
|
|
||||||
const key = `${flagPrefix}${id}`;
|
|
||||||
text = text.replace(match, key);
|
|
||||||
hash[key] = match;
|
|
||||||
id += 1;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return { text, hash };
|
|
||||||
};
|
|
||||||
|
|
||||||
const wrap = source => {
|
const wrap = source => {
|
||||||
const { text, hash } = flag(unwrap(source), orderedPatterns);
|
const { text, hash } = mark(unwrap(source), patternGroups);
|
||||||
|
return unmark(text, hash);
|
||||||
let wrappedSource = text;
|
|
||||||
Object.entries(hash).forEach(([key, value]) => {
|
|
||||||
wrappedSource = wrappedSource.replace(key, `${prefix}${value}${postfix}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
return wrappedSource;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default { wrap, unwrap };
|
export default { wrap, unwrap };
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Mutations
|
||||||
|
module Boards
|
||||||
|
module Issues
|
||||||
|
class IssueMoveList < Mutations::Issues::Base
|
||||||
|
graphql_name 'IssueMoveList'
|
||||||
|
|
||||||
|
argument :board_id, GraphQL::ID_TYPE,
|
||||||
|
required: true,
|
||||||
|
loads: Types::BoardType,
|
||||||
|
description: 'Global ID of the board that the issue is in'
|
||||||
|
|
||||||
|
argument :project_path, GraphQL::ID_TYPE,
|
||||||
|
required: true,
|
||||||
|
description: 'Project the issue to mutate is in'
|
||||||
|
|
||||||
|
argument :iid, GraphQL::STRING_TYPE,
|
||||||
|
required: true,
|
||||||
|
description: 'IID of the issue to mutate'
|
||||||
|
|
||||||
|
argument :from_list_id, GraphQL::ID_TYPE,
|
||||||
|
required: false,
|
||||||
|
description: 'ID of the board list that the issue will be moved from'
|
||||||
|
|
||||||
|
argument :to_list_id, GraphQL::ID_TYPE,
|
||||||
|
required: false,
|
||||||
|
description: 'ID of the board list that the issue will be moved to'
|
||||||
|
|
||||||
|
argument :move_before_id, GraphQL::ID_TYPE,
|
||||||
|
required: false,
|
||||||
|
description: 'ID of issue before which the current issue will be positioned at'
|
||||||
|
|
||||||
|
argument :move_after_id, GraphQL::ID_TYPE,
|
||||||
|
required: false,
|
||||||
|
description: 'ID of issue after which the current issue will be positioned at'
|
||||||
|
|
||||||
|
def ready?(**args)
|
||||||
|
if move_arguments(args).blank?
|
||||||
|
raise Gitlab::Graphql::Errors::ArgumentError,
|
||||||
|
'At least one of the arguments fromListId, toListId, afterId or beforeId is required'
|
||||||
|
end
|
||||||
|
|
||||||
|
if move_list_arguments(args).one?
|
||||||
|
raise Gitlab::Graphql::Errors::ArgumentError,
|
||||||
|
'Both fromListId and toListId must be present'
|
||||||
|
end
|
||||||
|
|
||||||
|
super
|
||||||
|
end
|
||||||
|
|
||||||
|
def resolve(board:, **args)
|
||||||
|
raise_resource_not_available_error! unless board
|
||||||
|
authorize_board!(board)
|
||||||
|
|
||||||
|
issue = authorized_find!(project_path: args[:project_path], iid: args[:iid])
|
||||||
|
move_params = { id: issue.id, board_id: board.id }.merge(move_arguments(args))
|
||||||
|
|
||||||
|
move_issue(board, issue, move_params)
|
||||||
|
|
||||||
|
{
|
||||||
|
issue: issue.reset,
|
||||||
|
errors: issue.errors.full_messages
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def move_issue(board, issue, move_params)
|
||||||
|
service = ::Boards::Issues::MoveService.new(board.resource_parent, current_user, move_params)
|
||||||
|
|
||||||
|
service.execute(issue)
|
||||||
|
end
|
||||||
|
|
||||||
|
def move_list_arguments(args)
|
||||||
|
args.slice(:from_list_id, :to_list_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def move_arguments(args)
|
||||||
|
args.slice(:from_list_id, :to_list_id, :move_after_id, :move_before_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def authorize_board!(board)
|
||||||
|
return if Ability.allowed?(current_user, :read_board, board.resource_parent)
|
||||||
|
|
||||||
|
raise_resource_not_available_error!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -14,6 +14,7 @@ module Types
|
||||||
mount_mutation Mutations::AwardEmojis::Add
|
mount_mutation Mutations::AwardEmojis::Add
|
||||||
mount_mutation Mutations::AwardEmojis::Remove
|
mount_mutation Mutations::AwardEmojis::Remove
|
||||||
mount_mutation Mutations::AwardEmojis::Toggle
|
mount_mutation Mutations::AwardEmojis::Toggle
|
||||||
|
mount_mutation Mutations::Boards::Issues::IssueMoveList
|
||||||
mount_mutation Mutations::Branches::Create, calls_gitaly: true
|
mount_mutation Mutations::Branches::Create, calls_gitaly: true
|
||||||
mount_mutation Mutations::Commits::Create, calls_gitaly: true
|
mount_mutation Mutations::Commits::Create, calls_gitaly: true
|
||||||
mount_mutation Mutations::Discussions::ToggleResolve
|
mount_mutation Mutations::Discussions::ToggleResolve
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,8 @@ module TriggerableHooks
|
||||||
merge_request_hooks: :merge_requests_events,
|
merge_request_hooks: :merge_requests_events,
|
||||||
job_hooks: :job_events,
|
job_hooks: :job_events,
|
||||||
pipeline_hooks: :pipeline_events,
|
pipeline_hooks: :pipeline_events,
|
||||||
wiki_page_hooks: :wiki_page_events
|
wiki_page_hooks: :wiki_page_events,
|
||||||
|
deployment_hooks: :deployment_events
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
|
|
||||||
|
|
@ -148,6 +148,7 @@ class Deployment < ApplicationRecord
|
||||||
|
|
||||||
def execute_hooks
|
def execute_hooks
|
||||||
deployment_data = Gitlab::DataBuilder::Deployment.build(self)
|
deployment_data = Gitlab::DataBuilder::Deployment.build(self)
|
||||||
|
project.execute_hooks(deployment_data, :deployment_hooks) if Feature.enabled?(:deployment_webhooks, project)
|
||||||
project.execute_services(deployment_data, :deployment_hooks)
|
project.execute_services(deployment_data, :deployment_hooks)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,8 @@ class ProjectHook < WebHook
|
||||||
:merge_request_hooks,
|
:merge_request_hooks,
|
||||||
:job_hooks,
|
:job_hooks,
|
||||||
:pipeline_hooks,
|
:pipeline_hooks,
|
||||||
:wiki_page_hooks
|
:wiki_page_hooks,
|
||||||
|
:deployment_hooks
|
||||||
]
|
]
|
||||||
|
|
||||||
belongs_to :project
|
belongs_to :project
|
||||||
|
|
|
||||||
|
|
@ -75,8 +75,6 @@ module Git
|
||||||
end
|
end
|
||||||
|
|
||||||
def merge_request_branches_for(changes)
|
def merge_request_branches_for(changes)
|
||||||
return if Feature.disabled?(:refresh_only_existing_merge_requests_on_push, default_enabled: true)
|
|
||||||
|
|
||||||
@merge_requests_branches ||= MergeRequests::PushedBranchesService.new(project, current_user, changes: changes).execute
|
@merge_requests_branches ||= MergeRequests::PushedBranchesService.new(project, current_user, changes: changes).execute
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,23 @@ let presets = [
|
||||||
useBuiltIns: 'usage',
|
useBuiltIns: 'usage',
|
||||||
corejs: { version: 3, proposals: true },
|
corejs: { version: 3, proposals: true },
|
||||||
modules: false,
|
modules: false,
|
||||||
|
/**
|
||||||
|
* This list of browsers is a conservative first definition, based on
|
||||||
|
* https://docs.gitlab.com/ee/install/requirements.html#supported-web-browsers
|
||||||
|
* with the following reasoning:
|
||||||
|
*
|
||||||
|
* - Edge: Pick the last two major version before the Chrome switch
|
||||||
|
* - Rest: We should support the latest ESR of Firefox: 68, because it used quite a lot.
|
||||||
|
* For the rest, pick browser versions that have a similar age to Firefox 68.
|
||||||
|
*
|
||||||
|
* See also this follow-up epic:
|
||||||
|
* https://gitlab.com/groups/gitlab-org/-/epics/3957
|
||||||
|
*/
|
||||||
targets: {
|
targets: {
|
||||||
ie: '11',
|
chrome: '73',
|
||||||
|
edge: '17',
|
||||||
|
firefox: '68',
|
||||||
|
safari: '12',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
@ -22,6 +37,8 @@ const plugins = [
|
||||||
'@babel/plugin-proposal-class-properties',
|
'@babel/plugin-proposal-class-properties',
|
||||||
'@babel/plugin-proposal-json-strings',
|
'@babel/plugin-proposal-json-strings',
|
||||||
'@babel/plugin-proposal-private-methods',
|
'@babel/plugin-proposal-private-methods',
|
||||||
|
// See: https://gitlab.com/gitlab-org/gitlab/-/issues/229146
|
||||||
|
'@babel/plugin-transform-arrow-functions',
|
||||||
'lodash',
|
'lodash',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: GraphQL mutation to move issue within board lists
|
||||||
|
merge_request: 38309
|
||||||
|
author:
|
||||||
|
type: added
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Add pre-processing step so preexisting codeblocks are preserved prior to flagging content as code in the static site editor's WYSIWYG mode.
|
||||||
|
merge_request: 38834
|
||||||
|
author:
|
||||||
|
type: added
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Fix multiline comment rendering
|
||||||
|
merge_request: 38721
|
||||||
|
author:
|
||||||
|
type: fixed
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Remove Internet Explorer 11 from babel transpilation
|
||||||
|
merge_request: 36840
|
||||||
|
author:
|
||||||
|
type: removed
|
||||||
|
|
@ -15,7 +15,7 @@ is generally stable and can handle many requests, so it is an acceptable
|
||||||
trade off to have only a single instance. See the [reference architectures](../reference_architectures/index.md)
|
trade off to have only a single instance. See the [reference architectures](../reference_architectures/index.md)
|
||||||
page for an overview of GitLab scaling options.
|
page for an overview of GitLab scaling options.
|
||||||
|
|
||||||
## Set up a standalone Redis instance
|
## Set up the standalone Redis instance
|
||||||
|
|
||||||
The steps below are the minimum necessary to configure a Redis server with
|
The steps below are the minimum necessary to configure a Redis server with
|
||||||
Omnibus GitLab:
|
Omnibus GitLab:
|
||||||
|
|
@ -28,36 +28,49 @@ Omnibus GitLab:
|
||||||
1. Edit `/etc/gitlab/gitlab.rb` and add the contents:
|
1. Edit `/etc/gitlab/gitlab.rb` and add the contents:
|
||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
## Enable Redis
|
## Enable Redis and disable all other services
|
||||||
redis['enable'] = true
|
## https://docs.gitlab.com/omnibus/roles/
|
||||||
|
roles ['redis_master_role']
|
||||||
## Disable all other services
|
|
||||||
sidekiq['enable'] = false
|
|
||||||
gitlab_workhorse['enable'] = false
|
|
||||||
puma['enable'] = false
|
|
||||||
postgresql['enable'] = false
|
|
||||||
nginx['enable'] = false
|
|
||||||
prometheus['enable'] = false
|
|
||||||
alertmanager['enable'] = false
|
|
||||||
pgbouncer_exporter['enable'] = false
|
|
||||||
gitlab_exporter['enable'] = false
|
|
||||||
gitaly['enable'] = false
|
|
||||||
|
|
||||||
|
## Redis configuration
|
||||||
redis['bind'] = '0.0.0.0'
|
redis['bind'] = '0.0.0.0'
|
||||||
redis['port'] = 6379
|
redis['port'] = 6379
|
||||||
redis['password'] = 'SECRET_PASSWORD_HERE'
|
redis['password'] = '<redis_password>'
|
||||||
|
|
||||||
gitlab_rails['enable'] = false
|
## Disable automatic database migrations
|
||||||
|
## Only the primary GitLab application server should handle migrations
|
||||||
|
gitlab_rails['auto_migrate'] = false
|
||||||
```
|
```
|
||||||
|
|
||||||
1. [Reconfigure Omnibus GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect.
|
1. [Reconfigure Omnibus GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect.
|
||||||
1. Note the Redis node's IP address or hostname, port, and
|
1. Note the Redis node's IP address or hostname, port, and
|
||||||
Redis password. These will be necessary when configuring the GitLab
|
Redis password. These will be necessary when [configuring the GitLab
|
||||||
application servers later.
|
application servers](#set-up-the-gitlab-rails-application-instance).
|
||||||
|
|
||||||
[Advanced configuration options](https://docs.gitlab.com/omnibus/settings/redis.html)
|
[Advanced configuration options](https://docs.gitlab.com/omnibus/settings/redis.html)
|
||||||
are supported and can be added if needed.
|
are supported and can be added if needed.
|
||||||
|
|
||||||
|
## Set up the GitLab Rails application instance
|
||||||
|
|
||||||
|
On the instance where GitLab is installed:
|
||||||
|
|
||||||
|
1. Edit the `/etc/gitlab/gitlab.rb` file and add the following contents:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
## Disable Redis
|
||||||
|
redis['enable'] = false
|
||||||
|
|
||||||
|
gitlab_rails['redis_host'] = 'redis.example.com'
|
||||||
|
gitlab_rails['redis_port'] = 6379
|
||||||
|
|
||||||
|
## Required if Redis authentication is configured on the Redis node
|
||||||
|
gitlab_rails['redis_password'] = '<redis_password>'
|
||||||
|
```
|
||||||
|
|
||||||
|
1. Save your changes to `/etc/gitlab/gitlab.rb`.
|
||||||
|
|
||||||
|
1. [Reconfigure Omnibus GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect.
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
See the [Redis troubleshooting guide](troubleshooting.md).
|
See the [Redis troubleshooting guide](troubleshooting.md).
|
||||||
|
|
|
||||||
|
|
@ -6667,6 +6667,71 @@ type IssueEdge {
|
||||||
node: Issue
|
node: Issue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
Autogenerated input type of IssueMoveList
|
||||||
|
"""
|
||||||
|
input IssueMoveListInput {
|
||||||
|
"""
|
||||||
|
Global ID of the board that the issue is in
|
||||||
|
"""
|
||||||
|
boardId: ID!
|
||||||
|
|
||||||
|
"""
|
||||||
|
A unique identifier for the client performing the mutation.
|
||||||
|
"""
|
||||||
|
clientMutationId: String
|
||||||
|
|
||||||
|
"""
|
||||||
|
ID of the board list that the issue will be moved from
|
||||||
|
"""
|
||||||
|
fromListId: ID
|
||||||
|
|
||||||
|
"""
|
||||||
|
IID of the issue to mutate
|
||||||
|
"""
|
||||||
|
iid: String!
|
||||||
|
|
||||||
|
"""
|
||||||
|
ID of issue after which the current issue will be positioned at
|
||||||
|
"""
|
||||||
|
moveAfterId: ID
|
||||||
|
|
||||||
|
"""
|
||||||
|
ID of issue before which the current issue will be positioned at
|
||||||
|
"""
|
||||||
|
moveBeforeId: ID
|
||||||
|
|
||||||
|
"""
|
||||||
|
Project the issue to mutate is in
|
||||||
|
"""
|
||||||
|
projectPath: ID!
|
||||||
|
|
||||||
|
"""
|
||||||
|
ID of the board list that the issue will be moved to
|
||||||
|
"""
|
||||||
|
toListId: ID
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
Autogenerated return type of IssueMoveList
|
||||||
|
"""
|
||||||
|
type IssueMoveListPayload {
|
||||||
|
"""
|
||||||
|
A unique identifier for the client performing the mutation.
|
||||||
|
"""
|
||||||
|
clientMutationId: String
|
||||||
|
|
||||||
|
"""
|
||||||
|
Errors encountered during execution of the mutation.
|
||||||
|
"""
|
||||||
|
errors: [String!]!
|
||||||
|
|
||||||
|
"""
|
||||||
|
The issue after mutation
|
||||||
|
"""
|
||||||
|
issue: Issue
|
||||||
|
}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Check permissions for the current user on a issue
|
Check permissions for the current user on a issue
|
||||||
"""
|
"""
|
||||||
|
|
@ -8971,6 +9036,7 @@ type Mutation {
|
||||||
epicAddIssue(input: EpicAddIssueInput!): EpicAddIssuePayload
|
epicAddIssue(input: EpicAddIssueInput!): EpicAddIssuePayload
|
||||||
epicSetSubscription(input: EpicSetSubscriptionInput!): EpicSetSubscriptionPayload
|
epicSetSubscription(input: EpicSetSubscriptionInput!): EpicSetSubscriptionPayload
|
||||||
epicTreeReorder(input: EpicTreeReorderInput!): EpicTreeReorderPayload
|
epicTreeReorder(input: EpicTreeReorderInput!): EpicTreeReorderPayload
|
||||||
|
issueMoveList(input: IssueMoveListInput!): IssueMoveListPayload
|
||||||
issueSetAssignees(input: IssueSetAssigneesInput!): IssueSetAssigneesPayload
|
issueSetAssignees(input: IssueSetAssigneesInput!): IssueSetAssigneesPayload
|
||||||
issueSetConfidential(input: IssueSetConfidentialInput!): IssueSetConfidentialPayload
|
issueSetConfidential(input: IssueSetConfidentialInput!): IssueSetConfidentialPayload
|
||||||
issueSetDueDate(input: IssueSetDueDateInput!): IssueSetDueDatePayload
|
issueSetDueDate(input: IssueSetDueDateInput!): IssueSetDueDatePayload
|
||||||
|
|
|
||||||
|
|
@ -18428,6 +18428,176 @@
|
||||||
"enumValues": null,
|
"enumValues": null,
|
||||||
"possibleTypes": null
|
"possibleTypes": null
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"kind": "INPUT_OBJECT",
|
||||||
|
"name": "IssueMoveListInput",
|
||||||
|
"description": "Autogenerated input type of IssueMoveList",
|
||||||
|
"fields": null,
|
||||||
|
"inputFields": [
|
||||||
|
{
|
||||||
|
"name": "projectPath",
|
||||||
|
"description": "Project the issue to mutate is in",
|
||||||
|
"type": {
|
||||||
|
"kind": "NON_NULL",
|
||||||
|
"name": null,
|
||||||
|
"ofType": {
|
||||||
|
"kind": "SCALAR",
|
||||||
|
"name": "ID",
|
||||||
|
"ofType": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultValue": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "iid",
|
||||||
|
"description": "IID of the issue to mutate",
|
||||||
|
"type": {
|
||||||
|
"kind": "NON_NULL",
|
||||||
|
"name": null,
|
||||||
|
"ofType": {
|
||||||
|
"kind": "SCALAR",
|
||||||
|
"name": "String",
|
||||||
|
"ofType": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultValue": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "boardId",
|
||||||
|
"description": "Global ID of the board that the issue is in",
|
||||||
|
"type": {
|
||||||
|
"kind": "NON_NULL",
|
||||||
|
"name": null,
|
||||||
|
"ofType": {
|
||||||
|
"kind": "SCALAR",
|
||||||
|
"name": "ID",
|
||||||
|
"ofType": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultValue": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "fromListId",
|
||||||
|
"description": "ID of the board list that the issue will be moved from",
|
||||||
|
"type": {
|
||||||
|
"kind": "SCALAR",
|
||||||
|
"name": "ID",
|
||||||
|
"ofType": null
|
||||||
|
},
|
||||||
|
"defaultValue": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "toListId",
|
||||||
|
"description": "ID of the board list that the issue will be moved to",
|
||||||
|
"type": {
|
||||||
|
"kind": "SCALAR",
|
||||||
|
"name": "ID",
|
||||||
|
"ofType": null
|
||||||
|
},
|
||||||
|
"defaultValue": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "moveBeforeId",
|
||||||
|
"description": "ID of issue before which the current issue will be positioned at",
|
||||||
|
"type": {
|
||||||
|
"kind": "SCALAR",
|
||||||
|
"name": "ID",
|
||||||
|
"ofType": null
|
||||||
|
},
|
||||||
|
"defaultValue": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "moveAfterId",
|
||||||
|
"description": "ID of issue after which the current issue will be positioned at",
|
||||||
|
"type": {
|
||||||
|
"kind": "SCALAR",
|
||||||
|
"name": "ID",
|
||||||
|
"ofType": null
|
||||||
|
},
|
||||||
|
"defaultValue": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "clientMutationId",
|
||||||
|
"description": "A unique identifier for the client performing the mutation.",
|
||||||
|
"type": {
|
||||||
|
"kind": "SCALAR",
|
||||||
|
"name": "String",
|
||||||
|
"ofType": null
|
||||||
|
},
|
||||||
|
"defaultValue": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"interfaces": null,
|
||||||
|
"enumValues": null,
|
||||||
|
"possibleTypes": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "OBJECT",
|
||||||
|
"name": "IssueMoveListPayload",
|
||||||
|
"description": "Autogenerated return type of IssueMoveList",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "clientMutationId",
|
||||||
|
"description": "A unique identifier for the client performing the mutation.",
|
||||||
|
"args": [
|
||||||
|
|
||||||
|
],
|
||||||
|
"type": {
|
||||||
|
"kind": "SCALAR",
|
||||||
|
"name": "String",
|
||||||
|
"ofType": null
|
||||||
|
},
|
||||||
|
"isDeprecated": false,
|
||||||
|
"deprecationReason": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "errors",
|
||||||
|
"description": "Errors encountered during execution of the mutation.",
|
||||||
|
"args": [
|
||||||
|
|
||||||
|
],
|
||||||
|
"type": {
|
||||||
|
"kind": "NON_NULL",
|
||||||
|
"name": null,
|
||||||
|
"ofType": {
|
||||||
|
"kind": "LIST",
|
||||||
|
"name": null,
|
||||||
|
"ofType": {
|
||||||
|
"kind": "NON_NULL",
|
||||||
|
"name": null,
|
||||||
|
"ofType": {
|
||||||
|
"kind": "SCALAR",
|
||||||
|
"name": "String",
|
||||||
|
"ofType": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"isDeprecated": false,
|
||||||
|
"deprecationReason": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "issue",
|
||||||
|
"description": "The issue after mutation",
|
||||||
|
"args": [
|
||||||
|
|
||||||
|
],
|
||||||
|
"type": {
|
||||||
|
"kind": "OBJECT",
|
||||||
|
"name": "Issue",
|
||||||
|
"ofType": null
|
||||||
|
},
|
||||||
|
"isDeprecated": false,
|
||||||
|
"deprecationReason": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"inputFields": null,
|
||||||
|
"interfaces": [
|
||||||
|
|
||||||
|
],
|
||||||
|
"enumValues": null,
|
||||||
|
"possibleTypes": null
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"kind": "OBJECT",
|
"kind": "OBJECT",
|
||||||
"name": "IssuePermissions",
|
"name": "IssuePermissions",
|
||||||
|
|
@ -26040,6 +26210,33 @@
|
||||||
"isDeprecated": false,
|
"isDeprecated": false,
|
||||||
"deprecationReason": null
|
"deprecationReason": null
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "issueMoveList",
|
||||||
|
"description": null,
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"name": "input",
|
||||||
|
"description": null,
|
||||||
|
"type": {
|
||||||
|
"kind": "NON_NULL",
|
||||||
|
"name": null,
|
||||||
|
"ofType": {
|
||||||
|
"kind": "INPUT_OBJECT",
|
||||||
|
"name": "IssueMoveListInput",
|
||||||
|
"ofType": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultValue": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": {
|
||||||
|
"kind": "OBJECT",
|
||||||
|
"name": "IssueMoveListPayload",
|
||||||
|
"ofType": null
|
||||||
|
},
|
||||||
|
"isDeprecated": false,
|
||||||
|
"deprecationReason": null
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "issueSetAssignees",
|
"name": "issueSetAssignees",
|
||||||
"description": null,
|
"description": null,
|
||||||
|
|
|
||||||
|
|
@ -995,6 +995,16 @@ Represents a Group Member
|
||||||
| `webUrl` | String! | Web URL of the issue |
|
| `webUrl` | String! | Web URL of the issue |
|
||||||
| `weight` | Int | Weight of the issue |
|
| `weight` | Int | Weight of the issue |
|
||||||
|
|
||||||
|
## IssueMoveListPayload
|
||||||
|
|
||||||
|
Autogenerated return type of IssueMoveList
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | ---- | ---------- |
|
||||||
|
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
|
||||||
|
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
|
||||||
|
| `issue` | Issue | The issue after mutation |
|
||||||
|
|
||||||
## IssuePermissions
|
## IssuePermissions
|
||||||
|
|
||||||
Check permissions for the current user on a issue
|
Check permissions for the current user on a issue
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,8 @@ the `author` field. GitLab team members **should not**.
|
||||||
- Any user-facing change **should** have a changelog entry. Example: "GitLab now
|
- Any user-facing change **should** have a changelog entry. Example: "GitLab now
|
||||||
uses system fonts for all text."
|
uses system fonts for all text."
|
||||||
- Performance improvements **should** have a changelog entry.
|
- Performance improvements **should** have a changelog entry.
|
||||||
|
- Changes that need to be documented in the Telemetry [Event Dictionary](telemetry/event_dictionary.md)
|
||||||
|
also require a changelog entry.
|
||||||
- _Any_ contribution from a community member, no matter how small, **may** have
|
- _Any_ contribution from a community member, no matter how small, **may** have
|
||||||
a changelog entry regardless of these guidelines if the contributor wants one.
|
a changelog entry regardless of these guidelines if the contributor wants one.
|
||||||
Example: "Fixed a typo on the search results page."
|
Example: "Fixed a typo on the search results page."
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,10 @@
|
||||||
|
---
|
||||||
|
stage: Verify
|
||||||
|
group: Continuous Integration
|
||||||
|
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||||
|
type: index, concepts, howto
|
||||||
|
---
|
||||||
|
|
||||||
# CI/CD development documentation
|
# CI/CD development documentation
|
||||||
|
|
||||||
Development guides that are specific to CI/CD are listed here.
|
Development guides that are specific to CI/CD are listed here.
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,10 @@
|
||||||
|
---
|
||||||
|
stage: Release
|
||||||
|
group: Progressive Delivery
|
||||||
|
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||||
|
type: index, concepts, howto
|
||||||
|
---
|
||||||
|
|
||||||
# Development guide for GitLab CI/CD templates
|
# Development guide for GitLab CI/CD templates
|
||||||
|
|
||||||
This document explains how to develop [GitLab CI/CD templates](../../ci/examples/README.md).
|
This document explains how to develop [GitLab CI/CD templates](../../ci/examples/README.md).
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
---
|
---
|
||||||
redirect_to: 'documentation/styleguide.md'
|
redirect_to: 'documentation/styleguide.md'
|
||||||
---
|
---
|
||||||
|
|
||||||
|
This document was moved to [another location](documentation/styleguide.md).
|
||||||
|
|
|
||||||
|
|
@ -1 +1,5 @@
|
||||||
|
---
|
||||||
|
redirect_to: 'feature_flags/index.md'
|
||||||
|
---
|
||||||
|
|
||||||
This document was moved to [another location](feature_flags/index.md).
|
This document was moved to [another location](feature_flags/index.md).
|
||||||
|
|
|
||||||
|
|
@ -7093,6 +7093,9 @@ msgstr ""
|
||||||
msgid "Create project label"
|
msgid "Create project label"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Create release"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Create requirement"
|
msgid "Create requirement"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
@ -20099,6 +20102,9 @@ msgstr ""
|
||||||
msgid "Releases|New Release"
|
msgid "Releases|New Release"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Release|Something went wrong while creating a new release"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Release|Something went wrong while getting the release details"
|
msgid "Release|Something went wrong while getting the release details"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,8 +42,8 @@
|
||||||
"@babel/plugin-syntax-import-meta": "^7.10.1",
|
"@babel/plugin-syntax-import-meta": "^7.10.1",
|
||||||
"@babel/preset-env": "^7.10.1",
|
"@babel/preset-env": "^7.10.1",
|
||||||
"@gitlab/at.js": "1.5.5",
|
"@gitlab/at.js": "1.5.5",
|
||||||
"@gitlab/svgs": "1.157.0",
|
"@gitlab/svgs": "1.158.0",
|
||||||
"@gitlab/ui": "18.1.0",
|
"@gitlab/ui": "18.3.0",
|
||||||
"@gitlab/visual-review-tools": "1.6.1",
|
"@gitlab/visual-review-tools": "1.6.1",
|
||||||
"@rails/actioncable": "^6.0.3-1",
|
"@rails/actioncable": "^6.0.3-1",
|
||||||
"@sentry/browser": "^5.10.2",
|
"@sentry/browser": "^5.10.2",
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { GlDeprecatedButton } from '@gitlab/ui';
|
import { GlButton } from '@gitlab/ui';
|
||||||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||||
import { TEST_HOST } from 'spec/test_constants';
|
import { TEST_HOST } from 'spec/test_constants';
|
||||||
import ResolveWithIssueButton from '~/notes/components/discussion_resolve_with_issue_button.vue';
|
import ResolveWithIssueButton from '~/notes/components/discussion_resolve_with_issue_button.vue';
|
||||||
|
|
@ -23,7 +23,7 @@ describe('ResolveWithIssueButton', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('it should have a link with the provided link property as href', () => {
|
it('it should have a link with the provided link property as href', () => {
|
||||||
const button = wrapper.find(GlDeprecatedButton);
|
const button = wrapper.find(GlButton);
|
||||||
|
|
||||||
expect(button.attributes().href).toBe(url);
|
expect(button.attributes().href).toBe(url);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -83,18 +83,34 @@ describe('issue_note', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render multiline comment if editing discussion root', () => {
|
it('should only render if it has everything it needs', () => {
|
||||||
wrapper.setProps({ discussionRoot: true });
|
const position = {
|
||||||
wrapper.vm.isEditing = true;
|
line_range: {
|
||||||
|
start: {
|
||||||
return wrapper.vm.$nextTick().then(() => {
|
line_code: 'abc_1_1',
|
||||||
expect(findMultilineComment().exists()).toBe(true);
|
type: null,
|
||||||
|
old_line: '',
|
||||||
|
new_line: '',
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
line_code: 'abc_2_2',
|
||||||
|
type: null,
|
||||||
|
old_line: '2',
|
||||||
|
new_line: '2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const line = {
|
||||||
|
line_code: 'abc_1_1',
|
||||||
|
type: null,
|
||||||
|
old_line: '1',
|
||||||
|
new_line: '1',
|
||||||
|
};
|
||||||
|
wrapper.setProps({
|
||||||
|
note: { ...note, position },
|
||||||
|
discussionRoot: true,
|
||||||
|
line,
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
it('should only render multiline comment form if it has everything it needs', () => {
|
|
||||||
wrapper.setProps({ line: { line_code: '' } });
|
|
||||||
wrapper.vm.isEditing = true;
|
|
||||||
|
|
||||||
return wrapper.vm.$nextTick().then(() => {
|
return wrapper.vm.$nextTick().then(() => {
|
||||||
expect(findMultilineComment().exists()).toBe(false);
|
expect(findMultilineComment().exists()).toBe(false);
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,8 @@ describe('Release edit/new component', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
actions = {
|
actions = {
|
||||||
fetchRelease: jest.fn(),
|
initializeRelease: jest.fn(),
|
||||||
updateRelease: jest.fn(),
|
saveRelease: jest.fn(),
|
||||||
addEmptyAssetLink: jest.fn(),
|
addEmptyAssetLink: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -64,6 +64,8 @@ describe('Release edit/new component', () => {
|
||||||
glFeatures: featureFlags,
|
glFeatures: featureFlags,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
wrapper.element.querySelectorAll('input').forEach(input => jest.spyOn(input, 'focus'));
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|
@ -87,8 +89,18 @@ describe('Release edit/new component', () => {
|
||||||
factory();
|
factory();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls fetchRelease when the component is created', () => {
|
it('calls initializeRelease when the component is created', () => {
|
||||||
expect(actions.fetchRelease).toHaveBeenCalledTimes(1);
|
expect(actions.initializeRelease).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('focuses the first non-disabled input element once the page is shown', () => {
|
||||||
|
const firstEnabledInput = wrapper.element.querySelector('input:enabled');
|
||||||
|
const allInputs = wrapper.element.querySelectorAll('input');
|
||||||
|
|
||||||
|
allInputs.forEach(input => {
|
||||||
|
const expectedFocusCalls = input === firstEnabledInput ? 1 : 0;
|
||||||
|
expect(input.focus).toHaveBeenCalledTimes(expectedFocusCalls);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders the description text at the top of the page', () => {
|
it('renders the description text at the top of the page', () => {
|
||||||
|
|
@ -109,9 +121,9 @@ describe('Release edit/new component', () => {
|
||||||
expect(findSubmitButton().attributes('type')).toBe('submit');
|
expect(findSubmitButton().attributes('type')).toBe('submit');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls updateRelease when the form is submitted', () => {
|
it('calls saveRelease when the form is submitted', () => {
|
||||||
wrapper.find('form').trigger('submit');
|
wrapper.find('form').trigger('submit');
|
||||||
expect(actions.updateRelease).toHaveBeenCalledTimes(1);
|
expect(actions.saveRelease).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -143,6 +155,34 @@ describe('Release edit/new component', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('when creating a new release', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
factory({
|
||||||
|
store: {
|
||||||
|
modules: {
|
||||||
|
detail: {
|
||||||
|
getters: {
|
||||||
|
isExistingRelease: () => false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders the submit button with the text "Create release"', () => {
|
||||||
|
expect(findSubmitButton().text()).toBe('Create release');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when editing an existing release', () => {
|
||||||
|
beforeEach(factory);
|
||||||
|
|
||||||
|
it('renders the submit button with the text "Save changes"', () => {
|
||||||
|
expect(findSubmitButton().text()).toBe('Save changes');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('asset links form', () => {
|
describe('asset links form', () => {
|
||||||
const findAssetLinksForm = () => wrapper.find(AssetLinksForm);
|
const findAssetLinksForm = () => wrapper.find(AssetLinksForm);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,14 +9,14 @@ describe('releases/components/tag_field', () => {
|
||||||
let store;
|
let store;
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
|
||||||
const createComponent = ({ originalRelease }) => {
|
const createComponent = ({ tagName }) => {
|
||||||
store = createStore({
|
store = createStore({
|
||||||
modules: {
|
modules: {
|
||||||
detail: createDetailModule({}),
|
detail: createDetailModule({}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
store.state.detail.originalRelease = originalRelease;
|
store.state.detail.tagName = tagName;
|
||||||
|
|
||||||
wrapper = shallowMount(TagField, { store });
|
wrapper = shallowMount(TagField, { store });
|
||||||
};
|
};
|
||||||
|
|
@ -31,8 +31,7 @@ describe('releases/components/tag_field', () => {
|
||||||
|
|
||||||
describe('when an existing release is being edited', () => {
|
describe('when an existing release is being edited', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const originalRelease = { name: 'Version 1.0' };
|
createComponent({ tagName: 'v1.0' });
|
||||||
createComponent({ originalRelease });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders the TagFieldExisting component', () => {
|
it('renders the TagFieldExisting component', () => {
|
||||||
|
|
@ -46,7 +45,7 @@ describe('releases/components/tag_field', () => {
|
||||||
|
|
||||||
describe('when a new release is being created', () => {
|
describe('when a new release is being created', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
createComponent({ originalRelease: null });
|
createComponent({ tagName: null });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders the TagFieldNew component', () => {
|
it('renders the TagFieldNew component', () => {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import MockAdapter from 'axios-mock-adapter';
|
import MockAdapter from 'axios-mock-adapter';
|
||||||
import testAction from 'helpers/vuex_action_helper';
|
import testAction from 'helpers/vuex_action_helper';
|
||||||
import { cloneDeep, merge } from 'lodash';
|
import { cloneDeep } from 'lodash';
|
||||||
import * as actions from '~/releases/stores/modules/detail/actions';
|
import * as actions from '~/releases/stores/modules/detail/actions';
|
||||||
import * as types from '~/releases/stores/modules/detail/mutation_types';
|
import * as types from '~/releases/stores/modules/detail/mutation_types';
|
||||||
import { release as originalRelease } from '../../../mock_data';
|
import { release as originalRelease } from '../../../mock_data';
|
||||||
|
|
@ -10,7 +10,9 @@ import createFlash from '~/flash';
|
||||||
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
|
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
|
||||||
import { redirectTo } from '~/lib/utils/url_utility';
|
import { redirectTo } from '~/lib/utils/url_utility';
|
||||||
import api from '~/api';
|
import api from '~/api';
|
||||||
|
import httpStatus from '~/lib/utils/http_status';
|
||||||
import { ASSET_LINK_TYPE } from '~/releases/constants';
|
import { ASSET_LINK_TYPE } from '~/releases/constants';
|
||||||
|
import { releaseToApiJson, apiJsonToRelease } from '~/releases/util';
|
||||||
|
|
||||||
jest.mock('~/flash', () => jest.fn());
|
jest.mock('~/flash', () => jest.fn());
|
||||||
|
|
||||||
|
|
@ -25,15 +27,26 @@ describe('Release detail actions', () => {
|
||||||
let mock;
|
let mock;
|
||||||
let error;
|
let error;
|
||||||
|
|
||||||
|
const setupState = (updates = {}) => {
|
||||||
|
const getters = {
|
||||||
|
isExistingRelease: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
...createState({
|
||||||
|
projectId: '18',
|
||||||
|
tagName: release.tag_name,
|
||||||
|
releasesPagePath: 'path/to/releases/page',
|
||||||
|
markdownDocsPath: 'path/to/markdown/docs',
|
||||||
|
markdownPreviewPath: 'path/to/markdown/preview',
|
||||||
|
updateReleaseApiDocsPath: 'path/to/api/docs',
|
||||||
|
}),
|
||||||
|
...getters,
|
||||||
|
...updates,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
state = createState({
|
|
||||||
projectId: '18',
|
|
||||||
tagName: 'v1.3',
|
|
||||||
releasesPagePath: 'path/to/releases/page',
|
|
||||||
markdownDocsPath: 'path/to/markdown/docs',
|
|
||||||
markdownPreviewPath: 'path/to/markdown/preview',
|
|
||||||
updateReleaseApiDocsPath: 'path/to/api/docs',
|
|
||||||
});
|
|
||||||
release = cloneDeep(originalRelease);
|
release = cloneDeep(originalRelease);
|
||||||
mock = new MockAdapter(axios);
|
mock = new MockAdapter(axios);
|
||||||
gon.api_version = 'v4';
|
gon.api_version = 'v4';
|
||||||
|
|
@ -45,302 +58,424 @@ describe('Release detail actions', () => {
|
||||||
mock.restore();
|
mock.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('requestRelease', () => {
|
describe('when creating a new release', () => {
|
||||||
it(`commits ${types.REQUEST_RELEASE}`, () =>
|
|
||||||
testAction(actions.requestRelease, undefined, state, [{ type: types.REQUEST_RELEASE }]));
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('receiveReleaseSuccess', () => {
|
|
||||||
it(`commits ${types.RECEIVE_RELEASE_SUCCESS}`, () =>
|
|
||||||
testAction(actions.receiveReleaseSuccess, release, state, [
|
|
||||||
{ type: types.RECEIVE_RELEASE_SUCCESS, payload: release },
|
|
||||||
]));
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('receiveReleaseError', () => {
|
|
||||||
it(`commits ${types.RECEIVE_RELEASE_ERROR}`, () =>
|
|
||||||
testAction(actions.receiveReleaseError, error, state, [
|
|
||||||
{ type: types.RECEIVE_RELEASE_ERROR, payload: error },
|
|
||||||
]));
|
|
||||||
|
|
||||||
it('shows a flash with an error message', () => {
|
|
||||||
actions.receiveReleaseError({ commit: jest.fn() }, error);
|
|
||||||
|
|
||||||
expect(createFlash).toHaveBeenCalledTimes(1);
|
|
||||||
expect(createFlash).toHaveBeenCalledWith(
|
|
||||||
'Something went wrong while getting the release details',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('fetchRelease', () => {
|
|
||||||
let getReleaseUrl;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
state.projectId = '18';
|
setupState({ isExistingRelease: false });
|
||||||
state.tagName = 'v1.3';
|
|
||||||
getReleaseUrl = `/api/v4/projects/${state.projectId}/releases/${state.tagName}`;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`dispatches requestRelease and receiveReleaseSuccess with the camel-case'd release object`, () => {
|
describe('initializeRelease', () => {
|
||||||
mock.onGet(getReleaseUrl).replyOnce(200, release);
|
it(`commits ${types.INITIALIZE_EMPTY_RELEASE}`, () => {
|
||||||
|
testAction(actions.initializeRelease, undefined, state, [
|
||||||
return testAction(
|
{ type: types.INITIALIZE_EMPTY_RELEASE },
|
||||||
actions.fetchRelease,
|
|
||||||
undefined,
|
|
||||||
state,
|
|
||||||
[],
|
|
||||||
[
|
|
||||||
{ type: 'requestRelease' },
|
|
||||||
{
|
|
||||||
type: 'receiveReleaseSuccess',
|
|
||||||
payload: convertObjectPropsToCamelCase(release, { deep: true }),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`dispatches requestRelease and receiveReleaseError with an error object`, () => {
|
|
||||||
mock.onGet(getReleaseUrl).replyOnce(500);
|
|
||||||
|
|
||||||
return testAction(
|
|
||||||
actions.fetchRelease,
|
|
||||||
undefined,
|
|
||||||
state,
|
|
||||||
[],
|
|
||||||
[{ type: 'requestRelease' }, { type: 'receiveReleaseError', payload: expect.anything() }],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('updateReleaseTagName', () => {
|
|
||||||
it(`commits ${types.UPDATE_RELEASE_TAG_NAME} with the updated tag name`, () => {
|
|
||||||
const newTag = 'updated-tag-name';
|
|
||||||
return testAction(actions.updateReleaseTagName, newTag, state, [
|
|
||||||
{ type: types.UPDATE_RELEASE_TAG_NAME, payload: newTag },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('updateCreateFrom', () => {
|
|
||||||
it(`commits ${types.UPDATE_CREATE_FROM} with the updated ref`, () => {
|
|
||||||
const newRef = 'my-feature-branch';
|
|
||||||
return testAction(actions.updateCreateFrom, newRef, state, [
|
|
||||||
{ type: types.UPDATE_CREATE_FROM, payload: newRef },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('updateReleaseTitle', () => {
|
|
||||||
it(`commits ${types.UPDATE_RELEASE_TITLE} with the updated release title`, () => {
|
|
||||||
const newTitle = 'The new release title';
|
|
||||||
return testAction(actions.updateReleaseTitle, newTitle, state, [
|
|
||||||
{ type: types.UPDATE_RELEASE_TITLE, payload: newTitle },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('updateReleaseNotes', () => {
|
|
||||||
it(`commits ${types.UPDATE_RELEASE_NOTES} with the updated release notes`, () => {
|
|
||||||
const newReleaseNotes = 'The new release notes';
|
|
||||||
return testAction(actions.updateReleaseNotes, newReleaseNotes, state, [
|
|
||||||
{ type: types.UPDATE_RELEASE_NOTES, payload: newReleaseNotes },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('updateAssetLinkUrl', () => {
|
|
||||||
it(`commits ${types.UPDATE_ASSET_LINK_URL} with the updated link URL`, () => {
|
|
||||||
const params = {
|
|
||||||
linkIdToUpdate: 2,
|
|
||||||
newUrl: 'https://example.com/updated',
|
|
||||||
};
|
|
||||||
|
|
||||||
return testAction(actions.updateAssetLinkUrl, params, state, [
|
|
||||||
{ type: types.UPDATE_ASSET_LINK_URL, payload: params },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('updateAssetLinkName', () => {
|
|
||||||
it(`commits ${types.UPDATE_ASSET_LINK_NAME} with the updated link name`, () => {
|
|
||||||
const params = {
|
|
||||||
linkIdToUpdate: 2,
|
|
||||||
newName: 'Updated link name',
|
|
||||||
};
|
|
||||||
|
|
||||||
return testAction(actions.updateAssetLinkName, params, state, [
|
|
||||||
{ type: types.UPDATE_ASSET_LINK_NAME, payload: params },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('updateAssetLinkType', () => {
|
|
||||||
it(`commits ${types.UPDATE_ASSET_LINK_TYPE} with the updated link type`, () => {
|
|
||||||
const params = {
|
|
||||||
linkIdToUpdate: 2,
|
|
||||||
newType: ASSET_LINK_TYPE.RUNBOOK,
|
|
||||||
};
|
|
||||||
|
|
||||||
return testAction(actions.updateAssetLinkType, params, state, [
|
|
||||||
{ type: types.UPDATE_ASSET_LINK_TYPE, payload: params },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('removeAssetLink', () => {
|
|
||||||
it(`commits ${types.REMOVE_ASSET_LINK} with the ID of the asset link to remove`, () => {
|
|
||||||
const idToRemove = 2;
|
|
||||||
return testAction(actions.removeAssetLink, idToRemove, state, [
|
|
||||||
{ type: types.REMOVE_ASSET_LINK, payload: idToRemove },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('updateReleaseMilestones', () => {
|
|
||||||
it(`commits ${types.UPDATE_RELEASE_MILESTONES} with the updated release milestones`, () => {
|
|
||||||
const newReleaseMilestones = ['v0.0', 'v0.1'];
|
|
||||||
return testAction(actions.updateReleaseMilestones, newReleaseMilestones, state, [
|
|
||||||
{ type: types.UPDATE_RELEASE_MILESTONES, payload: newReleaseMilestones },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('requestUpdateRelease', () => {
|
|
||||||
it(`commits ${types.REQUEST_UPDATE_RELEASE}`, () =>
|
|
||||||
testAction(actions.requestUpdateRelease, undefined, state, [
|
|
||||||
{ type: types.REQUEST_UPDATE_RELEASE },
|
|
||||||
]));
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('receiveUpdateReleaseSuccess', () => {
|
|
||||||
it(`commits ${types.RECEIVE_UPDATE_RELEASE_SUCCESS}`, () =>
|
|
||||||
testAction(actions.receiveUpdateReleaseSuccess, undefined, { ...state, featureFlags: {} }, [
|
|
||||||
{ type: types.RECEIVE_UPDATE_RELEASE_SUCCESS },
|
|
||||||
]));
|
|
||||||
|
|
||||||
it('redirects to the releases page if releaseShowPage feature flag is enabled', () => {
|
|
||||||
const rootState = { featureFlags: { releaseShowPage: true } };
|
|
||||||
const updatedState = merge({}, state, {
|
|
||||||
releasesPagePath: 'path/to/releases/page',
|
|
||||||
release: {
|
|
||||||
_links: {
|
|
||||||
self: 'path/to/self',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
actions.receiveUpdateReleaseSuccess({ commit: jest.fn(), state: updatedState, rootState });
|
|
||||||
|
|
||||||
expect(redirectTo).toHaveBeenCalledTimes(1);
|
|
||||||
expect(redirectTo).toHaveBeenCalledWith(updatedState.release._links.self);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when the releaseShowPage feature flag is disabled', () => {});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('receiveUpdateReleaseError', () => {
|
|
||||||
it(`commits ${types.RECEIVE_UPDATE_RELEASE_ERROR}`, () =>
|
|
||||||
testAction(actions.receiveUpdateReleaseError, error, state, [
|
|
||||||
{ type: types.RECEIVE_UPDATE_RELEASE_ERROR, payload: error },
|
|
||||||
]));
|
|
||||||
|
|
||||||
it('shows a flash with an error message', () => {
|
|
||||||
actions.receiveUpdateReleaseError({ commit: jest.fn() }, error);
|
|
||||||
|
|
||||||
expect(createFlash).toHaveBeenCalledTimes(1);
|
|
||||||
expect(createFlash).toHaveBeenCalledWith(
|
|
||||||
'Something went wrong while saving the release details',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('updateRelease', () => {
|
|
||||||
let getters;
|
|
||||||
let dispatch;
|
|
||||||
let callOrder;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
state.release = convertObjectPropsToCamelCase(release);
|
|
||||||
state.projectId = '18';
|
|
||||||
state.tagName = state.release.tagName;
|
|
||||||
|
|
||||||
getters = {
|
|
||||||
releaseLinksToDelete: [{ id: '1' }, { id: '2' }],
|
|
||||||
releaseLinksToCreate: [{ id: 'new-link-1' }, { id: 'new-link-2' }],
|
|
||||||
};
|
|
||||||
|
|
||||||
dispatch = jest.fn();
|
|
||||||
|
|
||||||
callOrder = [];
|
|
||||||
jest.spyOn(api, 'updateRelease').mockImplementation(() => {
|
|
||||||
callOrder.push('updateRelease');
|
|
||||||
return Promise.resolve();
|
|
||||||
});
|
|
||||||
jest.spyOn(api, 'deleteReleaseLink').mockImplementation(() => {
|
|
||||||
callOrder.push('deleteReleaseLink');
|
|
||||||
return Promise.resolve();
|
|
||||||
});
|
|
||||||
jest.spyOn(api, 'createReleaseLink').mockImplementation(() => {
|
|
||||||
callOrder.push('createReleaseLink');
|
|
||||||
return Promise.resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('dispatches requestUpdateRelease and receiveUpdateReleaseSuccess', () => {
|
|
||||||
return actions.updateRelease({ dispatch, state, getters }).then(() => {
|
|
||||||
expect(dispatch.mock.calls).toEqual([
|
|
||||||
['requestUpdateRelease'],
|
|
||||||
['receiveUpdateReleaseSuccess'],
|
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('dispatches requestUpdateRelease and receiveUpdateReleaseError with an error object', () => {
|
describe('saveRelease', () => {
|
||||||
jest.spyOn(api, 'updateRelease').mockRejectedValue(error);
|
it(`commits ${types.REQUEST_SAVE_RELEASE} and then dispatched "createRelease"`, () => {
|
||||||
|
testAction(
|
||||||
|
actions.saveRelease,
|
||||||
|
undefined,
|
||||||
|
state,
|
||||||
|
[{ type: types.REQUEST_SAVE_RELEASE }],
|
||||||
|
[{ type: 'createRelease' }],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
return actions.updateRelease({ dispatch, state, getters }).then(() => {
|
describe('when editing an existing release', () => {
|
||||||
expect(dispatch.mock.calls).toEqual([
|
beforeEach(setupState);
|
||||||
['requestUpdateRelease'],
|
|
||||||
['receiveUpdateReleaseError', error],
|
describe('initializeRelease', () => {
|
||||||
]);
|
it('dispatches "fetchRelease"', () => {
|
||||||
|
testAction(actions.initializeRelease, undefined, state, [], [{ type: 'fetchRelease' }]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('updates the Release, then deletes all existing links, and then recreates new links', () => {
|
describe('saveRelease', () => {
|
||||||
return actions.updateRelease({ dispatch, state, getters }).then(() => {
|
it(`commits ${types.REQUEST_SAVE_RELEASE} and then dispatched "updateRelease"`, () => {
|
||||||
expect(callOrder).toEqual([
|
testAction(
|
||||||
'updateRelease',
|
actions.saveRelease,
|
||||||
'deleteReleaseLink',
|
undefined,
|
||||||
'deleteReleaseLink',
|
state,
|
||||||
'createReleaseLink',
|
[{ type: types.REQUEST_SAVE_RELEASE }],
|
||||||
'createReleaseLink',
|
[{ type: 'updateRelease' }],
|
||||||
]);
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
expect(api.updateRelease.mock.calls).toEqual([
|
describe('actions that behave the same whether creating a new release or editing an existing release', () => {
|
||||||
[
|
beforeEach(setupState);
|
||||||
state.projectId,
|
|
||||||
state.tagName,
|
|
||||||
{
|
|
||||||
name: state.release.name,
|
|
||||||
description: state.release.description,
|
|
||||||
milestones: state.release.milestones.map(milestone => milestone.title),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect(api.deleteReleaseLink).toHaveBeenCalledTimes(getters.releaseLinksToDelete.length);
|
describe('fetchRelease', () => {
|
||||||
getters.releaseLinksToDelete.forEach(link => {
|
let getReleaseUrl;
|
||||||
expect(api.deleteReleaseLink).toHaveBeenCalledWith(
|
|
||||||
state.projectId,
|
beforeEach(() => {
|
||||||
state.tagName,
|
getReleaseUrl = `/api/v4/projects/${state.projectId}/releases/${state.tagName}`;
|
||||||
link.id,
|
});
|
||||||
);
|
|
||||||
|
describe('when the network request to the Release API is successful', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mock.onGet(getReleaseUrl).replyOnce(httpStatus.OK, release);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(api.createReleaseLink).toHaveBeenCalledTimes(getters.releaseLinksToCreate.length);
|
it(`commits ${types.REQUEST_RELEASE} and then commits ${types.RECEIVE_RELEASE_SUCCESS} with the converted release object`, () => {
|
||||||
getters.releaseLinksToCreate.forEach(link => {
|
return testAction(actions.fetchRelease, undefined, state, [
|
||||||
expect(api.createReleaseLink).toHaveBeenCalledWith(state.projectId, state.tagName, link);
|
{
|
||||||
|
type: types.REQUEST_RELEASE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: types.RECEIVE_RELEASE_SUCCESS,
|
||||||
|
payload: apiJsonToRelease(release, { deep: true }),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the network request to the Release API fails', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mock.onGet(getReleaseUrl).replyOnce(httpStatus.INTERNAL_SERVER_ERROR);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`commits ${types.REQUEST_RELEASE} and then commits ${types.RECEIVE_RELEASE_ERROR} with an error object`, () => {
|
||||||
|
return testAction(actions.fetchRelease, undefined, state, [
|
||||||
|
{
|
||||||
|
type: types.REQUEST_RELEASE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: types.RECEIVE_RELEASE_ERROR,
|
||||||
|
payload: expect.any(Error),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`shows a flash message`, () => {
|
||||||
|
return actions.fetchRelease({ commit: jest.fn(), state }).then(() => {
|
||||||
|
expect(createFlash).toHaveBeenCalledTimes(1);
|
||||||
|
expect(createFlash).toHaveBeenCalledWith(
|
||||||
|
'Something went wrong while getting the release details',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('updateReleaseTagName', () => {
|
||||||
|
it(`commits ${types.UPDATE_RELEASE_TAG_NAME} with the updated tag name`, () => {
|
||||||
|
const newTag = 'updated-tag-name';
|
||||||
|
return testAction(actions.updateReleaseTagName, newTag, state, [
|
||||||
|
{ type: types.UPDATE_RELEASE_TAG_NAME, payload: newTag },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('updateCreateFrom', () => {
|
||||||
|
it(`commits ${types.UPDATE_CREATE_FROM} with the updated ref`, () => {
|
||||||
|
const newRef = 'my-feature-branch';
|
||||||
|
return testAction(actions.updateCreateFrom, newRef, state, [
|
||||||
|
{ type: types.UPDATE_CREATE_FROM, payload: newRef },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('updateReleaseTitle', () => {
|
||||||
|
it(`commits ${types.UPDATE_RELEASE_TITLE} with the updated release title`, () => {
|
||||||
|
const newTitle = 'The new release title';
|
||||||
|
return testAction(actions.updateReleaseTitle, newTitle, state, [
|
||||||
|
{ type: types.UPDATE_RELEASE_TITLE, payload: newTitle },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('updateReleaseNotes', () => {
|
||||||
|
it(`commits ${types.UPDATE_RELEASE_NOTES} with the updated release notes`, () => {
|
||||||
|
const newReleaseNotes = 'The new release notes';
|
||||||
|
return testAction(actions.updateReleaseNotes, newReleaseNotes, state, [
|
||||||
|
{ type: types.UPDATE_RELEASE_NOTES, payload: newReleaseNotes },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('updateReleaseMilestones', () => {
|
||||||
|
it(`commits ${types.UPDATE_RELEASE_MILESTONES} with the updated release milestones`, () => {
|
||||||
|
const newReleaseMilestones = ['v0.0', 'v0.1'];
|
||||||
|
return testAction(actions.updateReleaseMilestones, newReleaseMilestones, state, [
|
||||||
|
{ type: types.UPDATE_RELEASE_MILESTONES, payload: newReleaseMilestones },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('addEmptyAssetLink', () => {
|
||||||
|
it(`commits ${types.ADD_EMPTY_ASSET_LINK}`, () => {
|
||||||
|
return testAction(actions.addEmptyAssetLink, undefined, state, [
|
||||||
|
{ type: types.ADD_EMPTY_ASSET_LINK },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('updateAssetLinkUrl', () => {
|
||||||
|
it(`commits ${types.UPDATE_ASSET_LINK_URL} with the updated link URL`, () => {
|
||||||
|
const params = {
|
||||||
|
linkIdToUpdate: 2,
|
||||||
|
newUrl: 'https://example.com/updated',
|
||||||
|
};
|
||||||
|
|
||||||
|
return testAction(actions.updateAssetLinkUrl, params, state, [
|
||||||
|
{ type: types.UPDATE_ASSET_LINK_URL, payload: params },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('updateAssetLinkName', () => {
|
||||||
|
it(`commits ${types.UPDATE_ASSET_LINK_NAME} with the updated link name`, () => {
|
||||||
|
const params = {
|
||||||
|
linkIdToUpdate: 2,
|
||||||
|
newName: 'Updated link name',
|
||||||
|
};
|
||||||
|
|
||||||
|
return testAction(actions.updateAssetLinkName, params, state, [
|
||||||
|
{ type: types.UPDATE_ASSET_LINK_NAME, payload: params },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('updateAssetLinkType', () => {
|
||||||
|
it(`commits ${types.UPDATE_ASSET_LINK_TYPE} with the updated link type`, () => {
|
||||||
|
const params = {
|
||||||
|
linkIdToUpdate: 2,
|
||||||
|
newType: ASSET_LINK_TYPE.RUNBOOK,
|
||||||
|
};
|
||||||
|
|
||||||
|
return testAction(actions.updateAssetLinkType, params, state, [
|
||||||
|
{ type: types.UPDATE_ASSET_LINK_TYPE, payload: params },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('removeAssetLink', () => {
|
||||||
|
it(`commits ${types.REMOVE_ASSET_LINK} with the ID of the asset link to remove`, () => {
|
||||||
|
const idToRemove = 2;
|
||||||
|
return testAction(actions.removeAssetLink, idToRemove, state, [
|
||||||
|
{ type: types.REMOVE_ASSET_LINK, payload: idToRemove },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('receiveSaveReleaseSuccess', () => {
|
||||||
|
it(`commits ${types.RECEIVE_SAVE_RELEASE_SUCCESS}`, () =>
|
||||||
|
testAction(actions.receiveSaveReleaseSuccess, undefined, { ...state, featureFlags: {} }, [
|
||||||
|
{ type: types.RECEIVE_SAVE_RELEASE_SUCCESS },
|
||||||
|
]));
|
||||||
|
|
||||||
|
describe('when the releaseShowPage feature flag is enabled', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
const rootState = { featureFlags: { releaseShowPage: true } };
|
||||||
|
actions.receiveSaveReleaseSuccess({ commit: jest.fn(), state, rootState }, release);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("redirects to the release's dedicated page", () => {
|
||||||
|
expect(redirectTo).toHaveBeenCalledTimes(1);
|
||||||
|
expect(redirectTo).toHaveBeenCalledWith(release._links.self);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the releaseShowPage feature flag is disabled', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
const rootState = { featureFlags: { releaseShowPage: false } };
|
||||||
|
actions.receiveSaveReleaseSuccess({ commit: jest.fn(), state, rootState }, release);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("redirects to the project's main Releases page", () => {
|
||||||
|
expect(redirectTo).toHaveBeenCalledTimes(1);
|
||||||
|
expect(redirectTo).toHaveBeenCalledWith(state.releasesPagePath);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createRelease', () => {
|
||||||
|
let createReleaseUrl;
|
||||||
|
let releaseLinksToCreate;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const camelCasedRelease = convertObjectPropsToCamelCase(release);
|
||||||
|
|
||||||
|
releaseLinksToCreate = camelCasedRelease.assets.links.slice(0, 1);
|
||||||
|
|
||||||
|
setupState({
|
||||||
|
release: camelCasedRelease,
|
||||||
|
releaseLinksToCreate,
|
||||||
|
});
|
||||||
|
|
||||||
|
createReleaseUrl = `/api/v4/projects/${state.projectId}/releases`;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the network request to the Release API is successful', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
const expectedRelease = releaseToApiJson({
|
||||||
|
...state.release,
|
||||||
|
assets: {
|
||||||
|
links: releaseLinksToCreate,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
mock.onPost(createReleaseUrl, expectedRelease).replyOnce(httpStatus.CREATED, release);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`dispatches "receiveSaveReleaseSuccess" with the converted release object`, () => {
|
||||||
|
return testAction(
|
||||||
|
actions.createRelease,
|
||||||
|
undefined,
|
||||||
|
state,
|
||||||
|
[],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
type: 'receiveSaveReleaseSuccess',
|
||||||
|
payload: apiJsonToRelease(release, { deep: true }),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the network request to the Release API fails', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mock.onPost(createReleaseUrl).replyOnce(httpStatus.INTERNAL_SERVER_ERROR);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`commits ${types.RECEIVE_SAVE_RELEASE_ERROR} with an error object`, () => {
|
||||||
|
return testAction(actions.createRelease, undefined, state, [
|
||||||
|
{
|
||||||
|
type: types.RECEIVE_SAVE_RELEASE_ERROR,
|
||||||
|
payload: expect.any(Error),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`shows a flash message`, () => {
|
||||||
|
return actions
|
||||||
|
.createRelease({ commit: jest.fn(), dispatch: jest.fn(), state, getters: {} })
|
||||||
|
.then(() => {
|
||||||
|
expect(createFlash).toHaveBeenCalledTimes(1);
|
||||||
|
expect(createFlash).toHaveBeenCalledWith(
|
||||||
|
'Something went wrong while creating a new release',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('updateRelease', () => {
|
||||||
|
let getters;
|
||||||
|
let dispatch;
|
||||||
|
let commit;
|
||||||
|
let callOrder;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
getters = {
|
||||||
|
releaseLinksToDelete: [{ id: '1' }, { id: '2' }],
|
||||||
|
releaseLinksToCreate: [{ id: 'new-link-1' }, { id: 'new-link-2' }],
|
||||||
|
};
|
||||||
|
|
||||||
|
setupState({
|
||||||
|
release: convertObjectPropsToCamelCase(release),
|
||||||
|
...getters,
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch = jest.fn();
|
||||||
|
commit = jest.fn();
|
||||||
|
|
||||||
|
callOrder = [];
|
||||||
|
jest.spyOn(api, 'updateRelease').mockImplementation(() => {
|
||||||
|
callOrder.push('updateRelease');
|
||||||
|
return Promise.resolve({ data: release });
|
||||||
|
});
|
||||||
|
jest.spyOn(api, 'deleteReleaseLink').mockImplementation(() => {
|
||||||
|
callOrder.push('deleteReleaseLink');
|
||||||
|
return Promise.resolve();
|
||||||
|
});
|
||||||
|
jest.spyOn(api, 'createReleaseLink').mockImplementation(() => {
|
||||||
|
callOrder.push('createReleaseLink');
|
||||||
|
return Promise.resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the network request to the Release API is successful', () => {
|
||||||
|
it('dispatches receiveSaveReleaseSuccess', () => {
|
||||||
|
return actions.updateRelease({ commit, dispatch, state, getters }).then(() => {
|
||||||
|
expect(dispatch.mock.calls).toEqual([
|
||||||
|
['receiveSaveReleaseSuccess', apiJsonToRelease(release)],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates the Release, then deletes all existing links, and then recreates new links', () => {
|
||||||
|
return actions.updateRelease({ dispatch, state, getters }).then(() => {
|
||||||
|
expect(callOrder).toEqual([
|
||||||
|
'updateRelease',
|
||||||
|
'deleteReleaseLink',
|
||||||
|
'deleteReleaseLink',
|
||||||
|
'createReleaseLink',
|
||||||
|
'createReleaseLink',
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(api.updateRelease.mock.calls).toEqual([
|
||||||
|
[
|
||||||
|
state.projectId,
|
||||||
|
state.tagName,
|
||||||
|
releaseToApiJson({
|
||||||
|
...state.release,
|
||||||
|
assets: {
|
||||||
|
links: getters.releaseLinksToCreate,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(api.deleteReleaseLink).toHaveBeenCalledTimes(
|
||||||
|
getters.releaseLinksToDelete.length,
|
||||||
|
);
|
||||||
|
getters.releaseLinksToDelete.forEach(link => {
|
||||||
|
expect(api.deleteReleaseLink).toHaveBeenCalledWith(
|
||||||
|
state.projectId,
|
||||||
|
state.tagName,
|
||||||
|
link.id,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(api.createReleaseLink).toHaveBeenCalledTimes(
|
||||||
|
getters.releaseLinksToCreate.length,
|
||||||
|
);
|
||||||
|
getters.releaseLinksToCreate.forEach(link => {
|
||||||
|
expect(api.createReleaseLink).toHaveBeenCalledWith(
|
||||||
|
state.projectId,
|
||||||
|
state.tagName,
|
||||||
|
link,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the network request to the Release API fails', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.spyOn(api, 'updateRelease').mockRejectedValue(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('dispatches requestUpdateRelease and receiveUpdateReleaseError with an error object', () => {
|
||||||
|
return actions.updateRelease({ commit, dispatch, state, getters }).then(() => {
|
||||||
|
expect(commit.mock.calls).toEqual([[types.RECEIVE_SAVE_RELEASE_ERROR, error]]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows a flash message', () => {
|
||||||
|
return actions.updateRelease({ commit, dispatch, state, getters }).then(() => {
|
||||||
|
expect(createFlash).toHaveBeenCalledTimes(1);
|
||||||
|
expect(createFlash).toHaveBeenCalledWith(
|
||||||
|
'Something went wrong while saving the release details',
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,13 @@ import * as getters from '~/releases/stores/modules/detail/getters';
|
||||||
describe('Release detail getters', () => {
|
describe('Release detail getters', () => {
|
||||||
describe('isExistingRelease', () => {
|
describe('isExistingRelease', () => {
|
||||||
it('returns true if the release is an existing release that already exists in the database', () => {
|
it('returns true if the release is an existing release that already exists in the database', () => {
|
||||||
const state = { originalRelease: { name: 'The first release' } };
|
const state = { tagName: 'test-tag-name' };
|
||||||
|
|
||||||
expect(getters.isExistingRelease(state)).toBe(true);
|
expect(getters.isExistingRelease(state)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns false if the release is a new release that has not yet been saved to the database', () => {
|
it('returns false if the release is a new release that has not yet been saved to the database', () => {
|
||||||
const state = { originalRelease: null };
|
const state = { tagName: null };
|
||||||
|
|
||||||
expect(getters.isExistingRelease(state)).toBe(false);
|
expect(getters.isExistingRelease(state)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,22 @@ describe('Release detail mutations', () => {
|
||||||
release = convertObjectPropsToCamelCase(originalRelease);
|
release = convertObjectPropsToCamelCase(originalRelease);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe(`${types.INITIALIZE_EMPTY_RELEASE}`, () => {
|
||||||
|
it('set state.release to an empty release object', () => {
|
||||||
|
mutations[types.INITIALIZE_EMPTY_RELEASE](state);
|
||||||
|
|
||||||
|
expect(state.release).toEqual({
|
||||||
|
tagName: null,
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
milestones: [],
|
||||||
|
assets: {
|
||||||
|
links: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe(`${types.REQUEST_RELEASE}`, () => {
|
describe(`${types.REQUEST_RELEASE}`, () => {
|
||||||
it('set state.isFetchingRelease to true', () => {
|
it('set state.isFetchingRelease to true', () => {
|
||||||
mutations[types.REQUEST_RELEASE](state);
|
mutations[types.REQUEST_RELEASE](state);
|
||||||
|
|
@ -96,17 +112,17 @@ describe('Release detail mutations', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe(`${types.REQUEST_UPDATE_RELEASE}`, () => {
|
describe(`${types.REQUEST_SAVE_RELEASE}`, () => {
|
||||||
it('set state.isUpdatingRelease to true', () => {
|
it('set state.isUpdatingRelease to true', () => {
|
||||||
mutations[types.REQUEST_UPDATE_RELEASE](state);
|
mutations[types.REQUEST_SAVE_RELEASE](state);
|
||||||
|
|
||||||
expect(state.isUpdatingRelease).toBe(true);
|
expect(state.isUpdatingRelease).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe(`${types.RECEIVE_UPDATE_RELEASE_SUCCESS}`, () => {
|
describe(`${types.RECEIVE_SAVE_RELEASE_SUCCESS}`, () => {
|
||||||
it('handles a successful response from the server', () => {
|
it('handles a successful response from the server', () => {
|
||||||
mutations[types.RECEIVE_UPDATE_RELEASE_SUCCESS](state, release);
|
mutations[types.RECEIVE_SAVE_RELEASE_SUCCESS](state, release);
|
||||||
|
|
||||||
expect(state.updateError).toBeUndefined();
|
expect(state.updateError).toBeUndefined();
|
||||||
|
|
||||||
|
|
@ -114,10 +130,10 @@ describe('Release detail mutations', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe(`${types.RECEIVE_UPDATE_RELEASE_ERROR}`, () => {
|
describe(`${types.RECEIVE_SAVE_RELEASE_ERROR}`, () => {
|
||||||
it('handles an unsuccessful response from the server', () => {
|
it('handles an unsuccessful response from the server', () => {
|
||||||
const error = { message: 'An error occurred!' };
|
const error = { message: 'An error occurred!' };
|
||||||
mutations[types.RECEIVE_UPDATE_RELEASE_ERROR](state, error);
|
mutations[types.RECEIVE_SAVE_RELEASE_ERROR](state, error);
|
||||||
|
|
||||||
expect(state.isUpdatingRelease).toBe(false);
|
expect(state.isUpdatingRelease).toBe(false);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
import { releaseToApiJson, apiJsonToRelease } from '~/releases/util';
|
||||||
|
|
||||||
|
describe('releases/util.js', () => {
|
||||||
|
describe('releaseToApiJson', () => {
|
||||||
|
it('converts a release JavaScript object into JSON that the Release API can accept', () => {
|
||||||
|
const release = {
|
||||||
|
tagName: 'tag-name',
|
||||||
|
name: 'Release name',
|
||||||
|
description: 'Release description',
|
||||||
|
milestones: [{ id: 1, title: '13.2' }, { id: 2, title: '13.3' }],
|
||||||
|
assets: {
|
||||||
|
links: [{ url: 'https://gitlab.example.com/link', linkType: 'other' }],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const expectedJson = {
|
||||||
|
tag_name: 'tag-name',
|
||||||
|
ref: null,
|
||||||
|
name: 'Release name',
|
||||||
|
description: 'Release description',
|
||||||
|
milestones: ['13.2', '13.3'],
|
||||||
|
assets: {
|
||||||
|
links: [{ url: 'https://gitlab.example.com/link', link_type: 'other' }],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(releaseToApiJson(release)).toEqual(expectedJson);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when createFrom is provided', () => {
|
||||||
|
it('adds the provided createFrom ref to the JSON as a "ref" property', () => {
|
||||||
|
const createFrom = 'main';
|
||||||
|
|
||||||
|
const release = {};
|
||||||
|
|
||||||
|
const expectedJson = {
|
||||||
|
ref: createFrom,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(releaseToApiJson(release, createFrom)).toMatchObject(expectedJson);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when release.milestones is falsy', () => {
|
||||||
|
it('includes a "milestone" property in the returned result as an empty array', () => {
|
||||||
|
const release = {};
|
||||||
|
|
||||||
|
const expectedJson = {
|
||||||
|
milestones: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(releaseToApiJson(release)).toMatchObject(expectedJson);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('apiJsonToRelease', () => {
|
||||||
|
it('converts JSON received from the Release API into an object usable by the Vue application', () => {
|
||||||
|
const json = {
|
||||||
|
tag_name: 'tag-name',
|
||||||
|
assets: {
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
link_type: 'other',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const expectedRelease = {
|
||||||
|
tagName: 'tag-name',
|
||||||
|
assets: {
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
linkType: 'other',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
milestones: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(apiJsonToRelease(json)).toEqual(expectedRelease);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -30,6 +30,15 @@ Below this line is a block of HTML.
|
||||||
<h1>Heading</h1>
|
<h1>Heading</h1>
|
||||||
<p>Some paragraph...</p>
|
<p>Some paragraph...</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
Below this line is a codeblock of the same HTML that should be ignored and preserved.
|
||||||
|
|
||||||
|
\`\`\` html
|
||||||
|
<div>
|
||||||
|
<h1>Heading</h1>
|
||||||
|
<p>Some paragraph...</p>
|
||||||
|
</div>
|
||||||
|
\`\`\`
|
||||||
`;
|
`;
|
||||||
const sourceTemplated = `Below this line is a simple ERB (single-line erb block) example.
|
const sourceTemplated = `Below this line is a simple ERB (single-line erb block) example.
|
||||||
|
|
||||||
|
|
@ -69,6 +78,15 @@ Below this line is a block of HTML.
|
||||||
<p>Some paragraph...</p>
|
<p>Some paragraph...</p>
|
||||||
</div>
|
</div>
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
|
Below this line is a codeblock of the same HTML that should be ignored and preserved.
|
||||||
|
|
||||||
|
\`\`\` html
|
||||||
|
<div>
|
||||||
|
<h1>Heading</h1>
|
||||||
|
<p>Some paragraph...</p>
|
||||||
|
</div>
|
||||||
|
\`\`\`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
it.each`
|
it.each`
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe Mutations::Boards::Issues::IssueMoveList do
|
||||||
|
let_it_be(:group) { create(:group, :public) }
|
||||||
|
let_it_be(:project) { create(:project, group: group) }
|
||||||
|
let_it_be(:board) { create(:board, group: group) }
|
||||||
|
let_it_be(:user) { create(:user) }
|
||||||
|
let_it_be(:guest) { create(:user) }
|
||||||
|
let_it_be(:development) { create(:label, project: project, name: 'Development') }
|
||||||
|
let_it_be(:testing) { create(:label, project: project, name: 'Testing') }
|
||||||
|
let_it_be(:list1) { create(:list, board: board, label: development, position: 0) }
|
||||||
|
let_it_be(:list2) { create(:list, board: board, label: testing, position: 1) }
|
||||||
|
let_it_be(:issue1) { create(:labeled_issue, project: project, labels: [development]) }
|
||||||
|
let_it_be(:existing_issue1) { create(:labeled_issue, project: project, labels: [testing], relative_position: 10) }
|
||||||
|
let_it_be(:existing_issue2) { create(:labeled_issue, project: project, labels: [testing], relative_position: 50) }
|
||||||
|
|
||||||
|
let(:current_user) { user }
|
||||||
|
let(:mutation) { described_class.new(object: nil, context: { current_user: current_user }, field: nil) }
|
||||||
|
let(:params) { { board: board, project_path: project.full_path, iid: issue1.iid } }
|
||||||
|
let(:move_params) do
|
||||||
|
{
|
||||||
|
from_list_id: list1.id,
|
||||||
|
to_list_id: list2.id,
|
||||||
|
move_before_id: existing_issue2.id,
|
||||||
|
move_after_id: existing_issue1.id
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
before_all do
|
||||||
|
group.add_maintainer(user)
|
||||||
|
group.add_guest(guest)
|
||||||
|
end
|
||||||
|
|
||||||
|
subject do
|
||||||
|
mutation.resolve(params.merge(move_params))
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#ready?' do
|
||||||
|
it 'raises an error if required arguments are missing' do
|
||||||
|
expect { mutation.ready?(params) }
|
||||||
|
.to raise_error(Gitlab::Graphql::Errors::ArgumentError, "At least one of the arguments " \
|
||||||
|
"fromListId, toListId, afterId or beforeId is required")
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'raises an error if only one of fromListId and toListId is present' do
|
||||||
|
expect { mutation.ready?(params.merge(from_list_id: list1.id)) }
|
||||||
|
.to raise_error(Gitlab::Graphql::Errors::ArgumentError,
|
||||||
|
'Both fromListId and toListId must be present'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#resolve' do
|
||||||
|
context 'when user have access to resources' do
|
||||||
|
it 'moves and repositions issue' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(issue1.reload.labels).to eq([testing])
|
||||||
|
expect(issue1.relative_position).to be < existing_issue2.relative_position
|
||||||
|
expect(issue1.relative_position).to be > existing_issue1.relative_position
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user have no access to resources' do
|
||||||
|
shared_examples 'raises a resource not available error' do
|
||||||
|
it { expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user cannot update issue' do
|
||||||
|
let(:current_user) { guest }
|
||||||
|
|
||||||
|
it_behaves_like 'raises a resource not available error'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user cannot access board' do
|
||||||
|
let(:board) { create(:board, group: create(:group, :private)) }
|
||||||
|
|
||||||
|
it_behaves_like 'raises a resource not available error'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when passing board_id as nil' do
|
||||||
|
let(:board) { nil }
|
||||||
|
|
||||||
|
it_behaves_like 'raises a resource not available error'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Reposition and move issue within board lists' do
|
||||||
|
include GraphqlHelpers
|
||||||
|
|
||||||
|
let_it_be(:group) { create(:group, :private) }
|
||||||
|
let_it_be(:project) { create(:project, group: group) }
|
||||||
|
let_it_be(:board) { create(:board, group: group) }
|
||||||
|
let_it_be(:user) { create(:user) }
|
||||||
|
let_it_be(:development) { create(:label, project: project, name: 'Development') }
|
||||||
|
let_it_be(:testing) { create(:label, project: project, name: 'Testing') }
|
||||||
|
let_it_be(:list1) { create(:list, board: board, label: development, position: 0) }
|
||||||
|
let_it_be(:list2) { create(:list, board: board, label: testing, position: 1) }
|
||||||
|
let_it_be(:existing_issue1) { create(:labeled_issue, project: project, labels: [testing], relative_position: 10) }
|
||||||
|
let_it_be(:existing_issue2) { create(:labeled_issue, project: project, labels: [testing], relative_position: 50) }
|
||||||
|
let_it_be(:issue1) { create(:labeled_issue, project: project, labels: [development]) }
|
||||||
|
|
||||||
|
let(:mutation_class) { Mutations::Boards::Issues::IssueMoveList }
|
||||||
|
let(:mutation_name) { mutation_class.graphql_name }
|
||||||
|
let(:mutation_result_identifier) { mutation_name.camelize(:lower) }
|
||||||
|
let(:current_user) { user }
|
||||||
|
let(:params) { { board_id: board.to_global_id.to_s, project_path: project.full_path, iid: issue1.iid.to_s } }
|
||||||
|
let(:issue_move_params) do
|
||||||
|
{
|
||||||
|
from_list_id: list1.id,
|
||||||
|
to_list_id: list2.id
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
before_all do
|
||||||
|
group.add_maintainer(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_examples 'returns an error' do
|
||||||
|
it 'fails with error' do
|
||||||
|
message = "The resource that you are attempting to access does not exist or you don't have "\
|
||||||
|
"permission to perform this action"
|
||||||
|
|
||||||
|
post_graphql_mutation(mutation(params), current_user: current_user)
|
||||||
|
|
||||||
|
expect(graphql_errors).to include(a_hash_including('message' => message))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user has access to resources' do
|
||||||
|
context 'when repositioning an issue' do
|
||||||
|
let(:issue_move_params) { { move_after_id: existing_issue1.id, move_before_id: existing_issue2.id } }
|
||||||
|
|
||||||
|
it 'repositions an issue' do
|
||||||
|
post_graphql_mutation(mutation(params), current_user: current_user)
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:success)
|
||||||
|
response_issue = json_response['data'][mutation_result_identifier]['issue']
|
||||||
|
expect(response_issue['iid']).to eq(issue1.iid.to_s)
|
||||||
|
expect(response_issue['relativePosition']).to be > existing_issue1.relative_position
|
||||||
|
expect(response_issue['relativePosition']).to be < existing_issue2.relative_position
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when moving an issue to a different list' do
|
||||||
|
let(:issue_move_params) { { from_list_id: list1.id, to_list_id: list2.id } }
|
||||||
|
|
||||||
|
it 'moves issue to a different list' do
|
||||||
|
post_graphql_mutation(mutation(params), current_user: current_user)
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:success)
|
||||||
|
response_issue = json_response['data'][mutation_result_identifier]['issue']
|
||||||
|
expect(response_issue['iid']).to eq(issue1.iid.to_s)
|
||||||
|
expect(response_issue['labels']['edges'][0]['node']['title']).to eq(testing.title)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user has no access to resources' do
|
||||||
|
context 'the user is not allowed to update the issue' do
|
||||||
|
let(:current_user) { create(:user) }
|
||||||
|
|
||||||
|
it_behaves_like 'returns an error'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the user can not read board' do
|
||||||
|
let(:board) { create(:board, group: create(:group, :private)) }
|
||||||
|
|
||||||
|
it_behaves_like 'returns an error'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def mutation(additional_params = {})
|
||||||
|
graphql_mutation(mutation_name, issue_move_params.merge(additional_params),
|
||||||
|
<<-QL.strip_heredoc
|
||||||
|
clientMutationId
|
||||||
|
issue {
|
||||||
|
iid,
|
||||||
|
relativePosition
|
||||||
|
labels {
|
||||||
|
edges {
|
||||||
|
node{
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
errors
|
||||||
|
QL
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -190,18 +190,6 @@ RSpec.describe Git::ProcessRefChangesService do
|
||||||
|
|
||||||
subject.execute
|
subject.execute
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'refresh_only_existing_merge_requests_on_push disabled' do
|
|
||||||
before do
|
|
||||||
stub_feature_flags(refresh_only_existing_merge_requests_on_push: false)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'refreshes all merge requests' do
|
|
||||||
expect(UpdateMergeRequestsWorker).to receive(:perform_async).exactly(3).times
|
|
||||||
|
|
||||||
subject.execute
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,5 +49,29 @@ RSpec.describe Deployments::FinishedWorker do
|
||||||
|
|
||||||
expect(ProjectServiceWorker).not_to have_received(:perform_async)
|
expect(ProjectServiceWorker).not_to have_received(:perform_async)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'execute webhooks' do
|
||||||
|
deployment = create(:deployment)
|
||||||
|
project = deployment.project
|
||||||
|
web_hook = create(:project_hook, deployment_events: true, project: project)
|
||||||
|
|
||||||
|
expect_next_instance_of(WebHookService, web_hook, an_instance_of(Hash), "deployment_hooks") do |service|
|
||||||
|
expect(service).to receive(:async_execute)
|
||||||
|
end
|
||||||
|
|
||||||
|
worker.perform(deployment.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not execute webhooks if feature flag is disabled' do
|
||||||
|
stub_feature_flags(deployment_webhooks: false)
|
||||||
|
|
||||||
|
deployment = create(:deployment)
|
||||||
|
project = deployment.project
|
||||||
|
create(:project_hook, deployment_events: true, project: project)
|
||||||
|
|
||||||
|
expect(WebHookService).not_to receive(:new)
|
||||||
|
|
||||||
|
worker.perform(deployment.id)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
16
yarn.lock
16
yarn.lock
|
|
@ -843,15 +843,15 @@
|
||||||
eslint-plugin-vue "^6.2.1"
|
eslint-plugin-vue "^6.2.1"
|
||||||
vue-eslint-parser "^7.0.0"
|
vue-eslint-parser "^7.0.0"
|
||||||
|
|
||||||
"@gitlab/svgs@1.157.0":
|
"@gitlab/svgs@1.158.0":
|
||||||
version "1.157.0"
|
version "1.158.0"
|
||||||
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.157.0.tgz#ada33c2b706836a2f5baa2c539f1348791d74859"
|
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.158.0.tgz#300d416184a2b0e05f15a96547f726e1825b08a1"
|
||||||
integrity sha512-H07Rn4Cy2QW+wnadvuFBSIWrtn8l4hGFLn62f1fT0iYZy58zb/q5/FsShxk9cSKnZYNkXp8I4Nnk/4R7y1MEOw==
|
integrity sha512-5OJl+7TsXN9PJhY6/uwi+mTwmDZa9n/6119rf77orQ/joFYUypaYhBmy/1TcKVPsy5Zs6KCxE1kmGsfoXc1TYA==
|
||||||
|
|
||||||
"@gitlab/ui@18.1.0":
|
"@gitlab/ui@18.3.0":
|
||||||
version "18.1.0"
|
version "18.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-18.1.0.tgz#36c1e292cae47d1580d2a3918fe5dd16893e2219"
|
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-18.3.0.tgz#c582eca1a0a851823700dabc7f4456feef882d9a"
|
||||||
integrity sha512-oXKTJ07hMFYxXZiJOgbNzVCpz/ooz0rY7D3ISG9ocawGVFVjrwLj41wgNtOzYAnQntxUcgvxNeBt3X6SS/zeTg==
|
integrity sha512-H0I3ExZJIqDd9rFDzyZwUerS3ZHDxRf2wHmAzMzK9smq/kr8aL5Pvb2E0KPcgDsVhGQCt7coCBN5NI0p+kf8oQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/standalone" "^7.0.0"
|
"@babel/standalone" "^7.0.0"
|
||||||
"@gitlab/vue-toasted" "^1.3.0"
|
"@gitlab/vue-toasted" "^1.3.0"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue