Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
266421c289
commit
8bf1146557
|
|
@ -50,23 +50,6 @@ Layout/SpaceInsideParens:
|
|||
- 'spec/serializers/analytics_build_entity_spec.rb'
|
||||
- 'spec/services/bulk_imports/create_service_spec.rb'
|
||||
- 'spec/services/jira_connect_installations/proxy_lifecycle_event_service_spec.rb'
|
||||
- 'spec/support/helpers/database/partitioning_helpers.rb'
|
||||
- 'spec/support/helpers/dependency_proxy_helpers.rb'
|
||||
- 'spec/support/helpers/javascript_fixtures_helpers.rb'
|
||||
- 'spec/support/helpers/kubernetes_helpers.rb'
|
||||
- 'spec/support/shared_contexts/services/projects/container_repository/delete_tags_service_shared_context.rb'
|
||||
- 'spec/support/shared_examples/controllers/destroy_hook_shared_examples.rb'
|
||||
- 'spec/support/shared_examples/features/project_features_apply_to_issuables_shared_examples.rb'
|
||||
- 'spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb'
|
||||
- 'spec/support/shared_examples/finders/packages/debian/distributions_finder_shared_examples.rb'
|
||||
- 'spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb'
|
||||
- 'spec/support/shared_examples/lib/gitlab/position_formatters_shared_examples.rb'
|
||||
- 'spec/support/shared_examples/lib/gitlab/sidekiq_middleware/strategy_shared_examples.rb'
|
||||
- 'spec/support/shared_examples/mailers/notify_shared_examples.rb'
|
||||
- 'spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb'
|
||||
- 'spec/support/shared_examples/requests/api/composer_packages_shared_examples.rb'
|
||||
- 'spec/support/shared_examples/requests/releases_shared_examples.rb'
|
||||
- 'spec/support/shared_examples/serializers/environment_serializer_shared_examples.rb'
|
||||
- 'spec/tasks/gitlab/db_rake_spec.rb'
|
||||
- 'spec/validators/devise_email_validator_spec.rb'
|
||||
- 'spec/views/dashboard/projects/_blank_state_welcome.html.haml_spec.rb'
|
||||
|
|
|
|||
|
|
@ -130,6 +130,7 @@ export default {
|
|||
isUpdating: false,
|
||||
groupsAndProjectsWithAccess: { groups: [], projects: [] },
|
||||
projectName: '',
|
||||
namespaceToEdit: null,
|
||||
namespaceToRemove: null,
|
||||
};
|
||||
},
|
||||
|
|
@ -234,6 +235,10 @@ export default {
|
|||
refetchGroupsAndProjects() {
|
||||
this.$apollo.queries.groupsAndProjectsWithAccess.refetch();
|
||||
},
|
||||
showNamespaceForm(namespace, showFormFn) {
|
||||
this.namespaceToEdit = namespace;
|
||||
showFormFn();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -286,6 +291,7 @@ export default {
|
|||
:description="$options.i18n.cardHeaderDescription"
|
||||
:toggle-text="$options.i18n.addGroupOrProject"
|
||||
class="gl-mt-5"
|
||||
@hideForm="namespaceToEdit = null"
|
||||
>
|
||||
<template #count>
|
||||
<gl-loading-icon v-if="isAllowlistLoading" data-testid="count-loading-icon" />
|
||||
|
|
@ -308,29 +314,37 @@ export default {
|
|||
</template>
|
||||
|
||||
<template #form="{ hideForm }">
|
||||
<namespace-form @saved="refetchGroupsAndProjects" @close="hideForm" />
|
||||
<namespace-form
|
||||
:namespace="namespaceToEdit"
|
||||
@saved="refetchGroupsAndProjects"
|
||||
@close="hideForm"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<token-access-table
|
||||
:items="allowlist"
|
||||
:loading="isAllowlistLoading"
|
||||
:show-policies="isJobTokenPoliciesEnabled"
|
||||
@removeItem="namespaceToRemove = $event"
|
||||
/>
|
||||
<confirm-action-modal
|
||||
v-if="namespaceToRemove"
|
||||
modal-id="inbound-token-access-remove-confirm-modal"
|
||||
:title="removeNamespaceModalTitle"
|
||||
:action-fn="removeItem"
|
||||
:action-text="$options.i18n.removeNamespaceModalActionText"
|
||||
@close="namespaceToRemove = null"
|
||||
>
|
||||
<gl-sprintf :message="$options.i18n.removeNamespaceModalText">
|
||||
<template #namespace>
|
||||
<code>{{ namespaceToRemove.fullPath }}</code>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</confirm-action-modal>
|
||||
<template #default="{ showForm }">
|
||||
<token-access-table
|
||||
:items="allowlist"
|
||||
:loading="isAllowlistLoading"
|
||||
:show-policies="isJobTokenPoliciesEnabled"
|
||||
@editItem="showNamespaceForm($event, showForm)"
|
||||
@removeItem="namespaceToRemove = $event"
|
||||
/>
|
||||
|
||||
<confirm-action-modal
|
||||
v-if="namespaceToRemove"
|
||||
modal-id="inbound-token-access-remove-confirm-modal"
|
||||
:title="removeNamespaceModalTitle"
|
||||
:action-fn="removeItem"
|
||||
:action-text="$options.i18n.removeNamespaceModalActionText"
|
||||
@close="namespaceToRemove = null"
|
||||
>
|
||||
<gl-sprintf :message="$options.i18n.removeNamespaceModalText">
|
||||
<template #namespace>
|
||||
<code>{{ namespaceToRemove.fullPath }}</code>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</confirm-action-modal>
|
||||
</template>
|
||||
</crud-component>
|
||||
</template>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,14 +1,22 @@
|
|||
<script>
|
||||
import { GlFormGroup, GlButton, GlFormInput } from '@gitlab/ui';
|
||||
import { s__ } from '~/locale';
|
||||
import { s__, __ } from '~/locale';
|
||||
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import addNamespaceMutation from '../graphql/mutations/inbound_add_group_or_project_ci_job_token_scope.mutation.graphql';
|
||||
import editNamespaceMutation from '../graphql/mutations/edit_namespace_job_token_scope.mutation.graphql';
|
||||
import PoliciesSelector from './policies_selector.vue';
|
||||
|
||||
export default {
|
||||
components: { GlFormGroup, GlButton, GlFormInput, PoliciesSelector },
|
||||
mixins: [glFeatureFlagsMixin()],
|
||||
inject: ['fullPath'],
|
||||
props: {
|
||||
namespace: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
targetPath: '',
|
||||
|
|
@ -18,6 +26,27 @@ export default {
|
|||
isSaving: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isPathInputDisabled() {
|
||||
// Disable the path if the form is currently saving or if we're editing a namespace.
|
||||
return this.isSaving || Boolean(this.namespace);
|
||||
},
|
||||
saveButtonText() {
|
||||
return this.namespace ? __('Save') : __('Add');
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
namespace: {
|
||||
immediate: true,
|
||||
handler() {
|
||||
// Update the local data when the namespace changes. This will happen if the form is open and
|
||||
// the user tries to edit another namespace.
|
||||
this.targetPath = this.namespace?.fullPath ?? '';
|
||||
this.defaultPermissions = this.namespace?.defaultPermissions ?? true;
|
||||
this.jobTokenPolicies = this.namespace?.jobTokenPolicies ?? [];
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async saveNamespace() {
|
||||
try {
|
||||
|
|
@ -31,9 +60,10 @@ export default {
|
|||
variables.jobTokenPolicies = this.defaultPermissions ? [] : this.jobTokenPolicies;
|
||||
}
|
||||
|
||||
const response = await this.$apollo.mutate({ mutation: addNamespaceMutation, variables });
|
||||
const mutation = this.namespace ? editNamespaceMutation : addNamespaceMutation;
|
||||
const response = await this.$apollo.mutate({ mutation, variables });
|
||||
|
||||
const error = response.data.ciJobTokenScopeAddGroupOrProject.errors[0];
|
||||
const error = response.data.saveNamespace.errors[0];
|
||||
if (error) {
|
||||
this.errorMessage = error;
|
||||
} else {
|
||||
|
|
@ -70,7 +100,7 @@ export default {
|
|||
autofocus
|
||||
:state="!errorMessage"
|
||||
:placeholder="fullPath"
|
||||
:disabled="isSaving"
|
||||
:disabled="isPathInputDisabled"
|
||||
@input="errorMessage = ''"
|
||||
/>
|
||||
</gl-form-group>
|
||||
|
|
@ -89,10 +119,10 @@ export default {
|
|||
variant="confirm"
|
||||
:disabled="!targetPath"
|
||||
:loading="isSaving"
|
||||
data-testid="add-button"
|
||||
data-testid="submit-button"
|
||||
@click="saveNamespace"
|
||||
>
|
||||
{{ __('Add') }}
|
||||
{{ saveButtonText }}
|
||||
</gl-button>
|
||||
<gl-button
|
||||
class="gl-ml-3"
|
||||
|
|
|
|||
|
|
@ -64,6 +64,15 @@ export default {
|
|||
getPolicies(policyKeys) {
|
||||
return policyKeys?.map((key) => JOB_TOKEN_POLICIES[key]);
|
||||
},
|
||||
hasJobTokenPolicies(item) {
|
||||
return Boolean(item.jobTokenPolicies?.length);
|
||||
},
|
||||
isCurrentProject(item) {
|
||||
return item.fullPath === this.fullPath;
|
||||
},
|
||||
shouldShowEditButton(item) {
|
||||
return this.showPolicies && !this.isCurrentProject(item);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -99,7 +108,7 @@ export default {
|
|||
<span v-if="item.defaultPermissions">
|
||||
{{ s__('CICD|Default (user membership and role)') }}</span
|
||||
>
|
||||
<span v-else-if="item.jobTokenPolicies && !item.jobTokenPolicies.length">
|
||||
<span v-else-if="!hasJobTokenPolicies(item)">
|
||||
{{ s__('CICD|No resources selected (minimal access only)') }}</span
|
||||
>
|
||||
<ul v-else class="gl-m-0 gl-list-none gl-p-0 gl-leading-20">
|
||||
|
|
@ -113,12 +122,22 @@ export default {
|
|||
</template>
|
||||
|
||||
<template #cell(actions)="{ item }">
|
||||
<gl-button
|
||||
v-if="item.fullPath !== fullPath"
|
||||
icon="remove"
|
||||
:aria-label="__('Remove access')"
|
||||
@click="$emit('removeItem', item)"
|
||||
/>
|
||||
<div class="gl-flex gl-gap-2">
|
||||
<gl-button
|
||||
v-if="shouldShowEditButton(item)"
|
||||
icon="pencil"
|
||||
:aria-label="__('Edit')"
|
||||
data-testid="token-access-table-edit-button"
|
||||
@click="$emit('editItem', item)"
|
||||
/>
|
||||
<gl-button
|
||||
v-if="!isCurrentProject(item)"
|
||||
icon="remove"
|
||||
:aria-label="__('Remove access')"
|
||||
data-testid="token-access-table-remove-button"
|
||||
@click="$emit('removeItem', item)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</gl-table>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
mutation editNamespaceJobTokenScope(
|
||||
$projectPath: ID!
|
||||
$targetPath: ID!
|
||||
$defaultPermissions: Boolean!
|
||||
$jobTokenPolicies: [CiJobTokenScopePolicies!]!
|
||||
) {
|
||||
saveNamespace: ciJobTokenScopeUpdatePolicies(
|
||||
input: {
|
||||
projectPath: $projectPath
|
||||
targetPath: $targetPath
|
||||
defaultPermissions: $defaultPermissions
|
||||
jobTokenPolicies: $jobTokenPolicies
|
||||
}
|
||||
) {
|
||||
errors
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@ mutation inboundAddGroupOrProjectCIJobTokenScope(
|
|||
$defaultPermissions: Boolean
|
||||
$jobTokenPolicies: [CiJobTokenScopePolicies!]
|
||||
) {
|
||||
ciJobTokenScopeAddGroupOrProject(
|
||||
saveNamespace: ciJobTokenScopeAddGroupOrProject(
|
||||
input: {
|
||||
projectPath: $projectPath
|
||||
targetPath: $targetPath
|
||||
|
|
|
|||
|
|
@ -265,7 +265,7 @@ export default {
|
|||
<span v-else-if="$scopedSlots.empty" class="gl-text-subtle" data-testid="crud-empty">
|
||||
<slot name="empty"></slot>
|
||||
</span>
|
||||
<slot v-else></slot>
|
||||
<slot v-else :show-form="showForm"></slot>
|
||||
|
||||
<div
|
||||
v-if="$scopedSlots.pagination"
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ class Projects::IssuesController < Projects::ApplicationController
|
|||
include IssuesCalendar
|
||||
include RecordUserLastActivity
|
||||
|
||||
ISSUES_EXCEPT_ACTIONS = %i[index calendar new create bulk_update import_csv export_csv service_desk].freeze
|
||||
ISSUES_EXCEPT_ACTIONS = %i[index calendar new create bulk_update import_csv export_csv service_desk can_create_branch].freeze
|
||||
SET_ISSUABLES_INDEX_ONLY_ACTIONS = %i[index calendar service_desk].freeze
|
||||
|
||||
prepend_before_action(only: [:index]) { authenticate_sessionless_user!(:rss) }
|
||||
|
|
@ -234,11 +234,11 @@ class Projects::IssuesController < Projects::ApplicationController
|
|||
def can_create_branch
|
||||
can_create = current_user &&
|
||||
can?(current_user, :push_code, @project) &&
|
||||
@issue.can_be_worked_on?
|
||||
issue.can_be_worked_on?
|
||||
|
||||
respond_to do |format|
|
||||
format.json do
|
||||
render json: { can_create_branch: can_create, suggested_branch_name: @issue.suggested_branch_name }
|
||||
render json: { can_create_branch: can_create, suggested_branch_name: issue.suggested_branch_name }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -11,8 +11,10 @@ DETAILS:
|
|||
**Offering:** GitLab.com, Self-managed, GitLab Dedicated
|
||||
**Status:** Beta
|
||||
|
||||
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/9290) in GitLab 17.2 with several feature flags. Disabled by default. Your administrator must have [enabled the new look for epics](../../user/group/epics/epic_work_items.md#enable-and-disable-the-new-look-for-epics). This feature is an [experiment](../../policy/development_stages_support.md#experiment).
|
||||
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/9290) in GitLab 17.2 [with a flag](../../administration/feature_flags.md) named `work_item_epics`. Disabled by default. Your administrator must have [enabled the new look for epics](../../user/group/epics/epic_work_items.md). This feature is in [beta](../../policy/development_stages_support.md#beta).
|
||||
> - Listing epics using the [GraphQL API](reference/index.md) [introduced](https://gitlab.com/groups/gitlab-org/-/epics/12852) in GitLab 17.4.
|
||||
> - [Enabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/470685) in GitLab 17.6.
|
||||
> - [Enabled by default on self-managed and GitLab Dedicated](https://gitlab.com/gitlab-org/gitlab/-/issues/468310) in GitLab 17.7.
|
||||
|
||||
In GitLab 17.2, we introduced [epics as work items](../../user/group/epics/epic_work_items.md).
|
||||
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ These requirements are documented in the `Required permission` column in the fol
|
|||
|:-----|:------------|:------------------|:---------|:--------------|:---------|
|
||||
| [`admin_merge_request`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/128302) | | Allows approval of merge requests. | GitLab [16.4](https://gitlab.com/gitlab-org/gitlab/-/issues/412708) | | |
|
||||
| [`admin_protected_branch`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/162208) | | Create, read, update, and delete protected branches for a project. | GitLab [17.4](https://gitlab.com/gitlab-org/gitlab/-/issues/448823) | | |
|
||||
| [`admin_push_rules`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/147872) | | Configure push rules for repositories at the group or project level. | GitLab [16.11](https://gitlab.com/gitlab-org/gitlab/-/issues/421786) | `custom_ability_admin_push_rules` | |
|
||||
| [`admin_push_rules`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/147872) | | Configure push rules for repositories at the group or project level. | GitLab [16.11](https://gitlab.com/gitlab-org/gitlab/-/issues/421786) | | |
|
||||
| [`read_code`](https://gitlab.com/gitlab-org/gitlab/-/issues/376180) | | Allows read-only access to the source code in the user interface. Does not allow users to edit or download repository archives, clone or pull repositories, view source code in an IDE, or view merge requests for private projects. You can download individual files because read-only access inherently grants the ability to make a local copy of the file. | GitLab [15.7](https://gitlab.com/gitlab-org/gitlab/-/issues/20277) | `customizable_roles` | GitLab [15.9](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/110810) |
|
||||
|
||||
## System access
|
||||
|
|
|
|||
|
|
@ -8,33 +8,23 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
DETAILS:
|
||||
**Tier:** Premium, Ultimate
|
||||
**Offering:** Self-managed
|
||||
**Status:** Experiment
|
||||
**Offering:** GitLab.com, Self-managed, GitLab Dedicated
|
||||
**Status:** Beta
|
||||
|
||||
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/9290) in GitLab 17.2 with [several feature flags](#enable-and-disable-the-new-look-for-epics). Disabled by default. This feature is an [experiment](../../../policy/development_stages_support.md#experiment).
|
||||
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/9290) in GitLab 17.2 [with a flag](../../../administration/feature_flags.md) named `work_item_epics`. Disabled by default. This feature is in [beta](../../../policy/development_stages_support.md#beta).
|
||||
> - Listing epics using the [GraphQL API](../../../api/graphql/reference/index.md) [introduced](https://gitlab.com/groups/gitlab-org/-/epics/12852) in GitLab 17.4.
|
||||
> - [Enabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/470685) in GitLab 17.6.
|
||||
> - [Enabled by default on self-managed and GitLab Dedicated](https://gitlab.com/gitlab-org/gitlab/-/issues/468310) in GitLab 17.7.
|
||||
|
||||
FLAG:
|
||||
The availability of this feature is controlled by a feature flag.
|
||||
For more information, see the history.
|
||||
This feature is available for testing, but not ready for production use.
|
||||
|
||||
DISCLAIMER:
|
||||
This page contains information related to upcoming products, features, and functionality.
|
||||
It is important to note that the information presented is for informational purposes only.
|
||||
Please do not rely on this information for purchasing or planning purposes.
|
||||
The development, release, and timing of any products, features, or functionality may be subject
|
||||
to change or delay and remain at the sole discretion of GitLab Inc.
|
||||
<!-- When epics as work items are generally available and `work_item_epics` flag is removed,
|
||||
incorporate this content into epics/index.md and redirect this page there -->
|
||||
|
||||
WARNING:
|
||||
This project is still in the experimental stage and could result in corruption or loss of production data.
|
||||
If you would like to enable this feature with no consequences, you are strongly advised to do so in a test environment.
|
||||
|
||||
<!-- When epics as work items are generally available, incorporate this content into epics/index.md and redirect
|
||||
this page there -->
|
||||
|
||||
We're working on changing how epics look by migrating them to a unified framework for work items to better
|
||||
We have changed how epics look by migrating them to a unified framework for work items to better
|
||||
meet the product needs of our Agile Planning offering.
|
||||
|
||||
For more information, see [epic 9290](https://gitlab.com/groups/gitlab-org/-/epics/9290) and the
|
||||
|
|
@ -43,39 +33,30 @@ following blog posts:
|
|||
- [First look: The new Agile planning experience in GitLab](https://about.gitlab.com/blog/2024/06/18/first-look-the-new-agile-planning-experience-in-gitlab/) (June 2024)
|
||||
- [Unveiling a new epic experience for improved Agile planning](https://about.gitlab.com/blog/2024/07/03/unveiling-a-new-epic-experience-for-improved-agile-planning/) (July 2024)
|
||||
|
||||
## Enable and disable the new look for epics
|
||||
## Troubleshooting
|
||||
|
||||
To try out this change on GitLab self-managed, run the following Rake task.
|
||||
The task performs a database verification to ensure data consistency and might take a few minutes.
|
||||
If the consistency check passes, the Rake task enables the `work_item_epics` feature flag.
|
||||
If you run into any issues while navigating your data in the new experience, there are a couple
|
||||
of ways you can try to resolve it.
|
||||
|
||||
If the check fails, the feature flag is not enabled. Inconsistencies are logged in the `epic_work_item_sync.log` file.
|
||||
Failed background migrations or invalid imports can cause data inconsistencies. These inconsistencies will be resolved when work item epics become generally available.
|
||||
### Access the old experience
|
||||
|
||||
**To enable:**
|
||||
You can temporarily load the old experience by editing URL to include `force_legacy_view=true` parameter,
|
||||
for example, `https://gitlab.com/groups/gitlab-org/-/epics/9290?force_legacy_view=true`. Use this parameter to do any comparison
|
||||
between old and new experience to provide details while opening support request.
|
||||
|
||||
```shell
|
||||
# omnibus-gitlab
|
||||
sudo gitlab-rake gitlab:work_items:epics:enable
|
||||
### Disable the new experience
|
||||
|
||||
# installation from source
|
||||
bundle exec rake gitlab:work_items:epics:enable RAILS_ENV=production
|
||||
```
|
||||
DETAILS:
|
||||
**Offering:** Self-managed
|
||||
|
||||
**To disable:**
|
||||
|
||||
```shell
|
||||
# omnibus-gitlab
|
||||
sudo gitlab-rake gitlab:work_items:epics:disable
|
||||
|
||||
# installation from source
|
||||
bundle exec rake gitlab:work_items:epics:disable RAILS_ENV=production
|
||||
```
|
||||
We don't recommend disabling this change, because we'd like your feedback on what you don't like about it.
|
||||
If you have to disable the new experience to unblock your workflow, disable the `work_item_epics`
|
||||
[feature flag](../../../administration/feature_flags.md#how-to-enable-and-disable-features-behind-flags).
|
||||
|
||||
## Feedback
|
||||
|
||||
If you run into any issues while trying out this change, you can use
|
||||
[feedback issue](https://gitlab.com/gitlab-org/gitlab/-/issues/463598) to provide more details.
|
||||
[feedback issue](https://gitlab.com/gitlab-org/gitlab/-/issues/494462) to provide more details.
|
||||
|
||||
## Related topics
|
||||
|
||||
|
|
|
|||
|
|
@ -437,7 +437,8 @@ You can always view the issues assigned to the epic if they are in the group's c
|
|||
It's possible because the visibility setting of a project must be the same as or less restrictive than
|
||||
of its parent group.
|
||||
|
||||
<!-- When epics as work items are generally available, use the info below in the main body. -->
|
||||
<!-- When epics as work items are generally available and feature flag `work_item_epics` is removed,
|
||||
use the info below in the main body. -->
|
||||
|
||||
If your administrator [enabled the new look for epics](epic_work_items.md), this section is named
|
||||
**Child items**.
|
||||
|
|
@ -456,7 +457,8 @@ To see the number of open and closed epics and issues:
|
|||
The numbers reflect all child issues and epics associated with the epic, including those you might
|
||||
not have permission to view.
|
||||
|
||||
<!-- When epics as work items are generally available, use the info below in the main body. -->
|
||||
<!-- When epics as work items are generally available and feature flag `work_item_epics` is removed,
|
||||
use the info below in the main body. -->
|
||||
|
||||
If your administrator [enabled the new look for epics](epic_work_items.md), this section is named
|
||||
**Child items**.
|
||||
|
|
@ -475,7 +477,8 @@ To see the completed and total weight of child issues:
|
|||
The weights and progress reflect all issues associated with the epic, including issues you might
|
||||
not have permission to view.
|
||||
|
||||
<!-- When epics as work items are generally available, use the info below in the main body. -->
|
||||
<!-- When epics as work items are generally available and feature flag `work_item_epics` is removed,
|
||||
use the info below in the main body. -->
|
||||
|
||||
If your administrator [enabled the new look for epics](epic_work_items.md), this section is named
|
||||
**Child items**.
|
||||
|
|
@ -563,7 +566,8 @@ To add an existing issue to an epic:
|
|||
If there are multiple issues to be added, press <kbd>Space</kbd> and repeat this step.
|
||||
1. Select **Add**.
|
||||
|
||||
<!-- When epics as work items are generally available, use the info below in the main body. -->
|
||||
<!-- When epics as work items are generally available and feature flag `work_item_epics` is removed,
|
||||
use the info below in the main body. -->
|
||||
|
||||
If your administrator [enabled the new look for epics](epic_work_items.md), this section is named
|
||||
**Child items**.
|
||||
|
|
@ -595,7 +599,8 @@ To create an issue from an epic:
|
|||
|
||||
The new issue is assigned to the epic.
|
||||
|
||||
<!-- When epics as work items are generally available, use the info below in the main body. -->
|
||||
<!-- When epics as work items are generally available and feature flag `work_item_epics` is removed,
|
||||
use the info below in the main body. -->
|
||||
|
||||
If your administrator [enabled the new look for epics](epic_work_items.md), this section is named
|
||||
**Child items**.
|
||||
|
|
@ -635,7 +640,8 @@ To reorder issues assigned to an epic:
|
|||
1. Go to the **Child issues and epics** section.
|
||||
1. Drag issues into the desired order.
|
||||
|
||||
<!-- When epics as work items are generally available, use the info below in the main body. -->
|
||||
<!-- When epics as work items are generally available and feature flag `work_item_epics` is removed,
|
||||
use the info below in the main body. -->
|
||||
|
||||
If your administrator [enabled the new look for epics](epic_work_items.md), this section is named
|
||||
**Child items**.
|
||||
|
|
@ -660,7 +666,8 @@ To move an issue to another epic:
|
|||
1. Go to the **Child issues and epics** section.
|
||||
1. Drag issues into the desired parent epic in the visible hierarchy.
|
||||
|
||||
<!-- When epics as work items are generally available, use the info below in the main body. -->
|
||||
<!-- When epics as work items are generally available and feature flag `work_item_epics` is removed,
|
||||
use the info below in the main body. -->
|
||||
|
||||
If your administrator [enabled the new look for epics](epic_work_items.md), this section is named
|
||||
**Child items**.
|
||||
|
|
@ -682,7 +689,8 @@ DETAILS:
|
|||
|
||||
You can add any epic that belongs to a group or subgroup of the parent epic's group.
|
||||
New child epics appear at the top of the list of epics in the **Child issues and epics** section.
|
||||
<!-- When epics as work items are generally available, use the info below in the main body. -->
|
||||
<!-- When epics as work items are generally available and feature flag `work_item_epics` is removed,
|
||||
use the info below in the main body. -->
|
||||
If your administrator [enabled the new look for epics](epic_work_items.md), this section is named
|
||||
**Child items**.
|
||||
|
||||
|
|
@ -742,7 +750,8 @@ To view child epics from the parent:
|
|||
|
||||
- In an epic, in the **Child issues and epics** section, select **Roadmap view**.
|
||||
|
||||
<!-- When epics as work items are generally available, use the info below in the main body. -->
|
||||
<!-- When epics as work items are generally available and feature flag `work_item_epics` is removed,
|
||||
use the info below in the main body. -->
|
||||
|
||||
If your administrator [enabled the new look for epics](epic_work_items.md), this section is named
|
||||
**Child items**.
|
||||
|
|
@ -764,7 +773,8 @@ To add a new epic as child epic:
|
|||
1. Enter a title for the new epic.
|
||||
1. Select **Create epic**.
|
||||
|
||||
<!-- When epics as work items are generally available, use the info below in the main body. -->
|
||||
<!-- When epics as work items are generally available and feature flag `work_item_epics` is removed,
|
||||
use the info below in the main body. -->
|
||||
|
||||
If your administrator [enabled the new look for epics](epic_work_items.md), this section is named
|
||||
**Child items**.
|
||||
|
|
@ -781,7 +791,8 @@ To add an existing epic as child epic:
|
|||
If there are multiple epics to be added, press <kbd>Space</kbd> and repeat this step.
|
||||
1. Select **Add**.
|
||||
|
||||
<!-- When epics as work items are generally available, use the info below in the main body. -->
|
||||
<!-- When epics as work items are generally available and feature flag `work_item_epics` is removed,
|
||||
use the info below in the main body. -->
|
||||
|
||||
If your administrator [enabled the new look for epics](epic_work_items.md), this section is named
|
||||
**Child items**.
|
||||
|
|
@ -804,7 +815,8 @@ To move child epics to another epic:
|
|||
1. Go to the **Child issues and epics** section.
|
||||
1. Drag epics into the desired parent epic.
|
||||
|
||||
<!-- When epics as work items are generally available, use the info below in the main body. -->
|
||||
<!-- When epics as work items are generally available and feature flag `work_item_epics` is removed,
|
||||
use the info below in the main body. -->
|
||||
|
||||
If your administrator [enabled the new look for epics](epic_work_items.md), this section is named
|
||||
**Child items**.
|
||||
|
|
@ -825,7 +837,8 @@ To reorder child epics assigned to an epic:
|
|||
1. Go to the **Child issues and epics** section.
|
||||
1. Drag epics into the desired order.
|
||||
|
||||
<!-- When epics as work items are generally available, use the info below in the main body. -->
|
||||
<!-- When epics as work items are generally available and feature flag `work_item_epics` is removed,
|
||||
use the info below in the main body. -->
|
||||
|
||||
If your administrator [enabled the new look for epics](epic_work_items.md), this section is named
|
||||
**Child items**.
|
||||
|
|
|
|||
|
|
@ -45525,6 +45525,12 @@ msgstr ""
|
|||
msgid "ProtectedEnvironment|Approvers"
|
||||
msgstr ""
|
||||
|
||||
msgid "ProtectedEnvironment|Environment %{environmentName} is protected."
|
||||
msgstr ""
|
||||
|
||||
msgid "ProtectedEnvironment|Environment %{environmentName} is unprotected."
|
||||
msgstr ""
|
||||
|
||||
msgid "ProtectedEnvironment|Environment '%{environment_name}' is already protected"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -24,8 +24,13 @@ RSpec.describe 'Thread Comments Commit', :js, feature_category: :source_code_man
|
|||
expect(page).to have_css('.js-note-emoji')
|
||||
end
|
||||
|
||||
it 'adds award to the correct note', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/207973' do
|
||||
it 'adds award to the correct note' do
|
||||
wait_for_requests
|
||||
|
||||
find("#note_#{commit_discussion_note2.id} .js-note-emoji").click
|
||||
|
||||
wait_for_requests
|
||||
|
||||
first('.emoji-menu .js-emoji-btn').click
|
||||
|
||||
wait_for_requests
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { GlAlert, GlLoadingIcon, GlFormRadioGroup } from '@gitlab/ui';
|
|||
import Vue, { nextTick } from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import { createAlert } from '~/alert';
|
||||
import InboundTokenAccess from '~/token_access/components/inbound_token_access.vue';
|
||||
|
|
@ -15,6 +15,9 @@ import inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery from '~/token_access/
|
|||
import getCiJobTokenScopeAllowlistQuery from '~/token_access/graphql/queries/get_ci_job_token_scope_allowlist.query.graphql';
|
||||
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
|
||||
import ConfirmActionModal from '~/vue_shared/components/confirm_action_modal.vue';
|
||||
import TokenAccessTable from '~/token_access/components/token_access_table.vue';
|
||||
import CrudComponent from '~/vue_shared/components/crud_component.vue';
|
||||
import { stubComponent } from 'helpers/stub_component';
|
||||
import {
|
||||
inboundJobTokenScopeEnabledResponse,
|
||||
inboundJobTokenScopeDisabledResponse,
|
||||
|
|
@ -57,8 +60,6 @@ describe('TokenAccess component', () => {
|
|||
|
||||
const findRadioGroup = () => wrapper.findComponent(GlFormRadioGroup);
|
||||
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
|
||||
const findRemoveProjectBtnAt = (i) =>
|
||||
wrapper.findAllByRole('button', { name: 'Remove access' }).at(i);
|
||||
const findToggleFormBtn = () => wrapper.findByTestId('crud-form-toggle');
|
||||
const findTokenDisabledAlert = () => wrapper.findComponent(GlAlert);
|
||||
const findNamespaceForm = () => wrapper.findComponent(NamespaceForm);
|
||||
|
|
@ -67,24 +68,29 @@ describe('TokenAccess component', () => {
|
|||
const findGroupCount = () => wrapper.findByTestId('group-count');
|
||||
const findProjectCount = () => wrapper.findByTestId('project-count');
|
||||
const findConfirmActionModal = () => wrapper.findComponent(ConfirmActionModal);
|
||||
const findTokenAccessTable = () => wrapper.findComponent(TokenAccessTable);
|
||||
|
||||
const createComponent = (requestHandlers, mountFn = shallowMountExtended, provide = {}) => {
|
||||
wrapper = mountFn(InboundTokenAccess, {
|
||||
const createComponent = (
|
||||
requestHandlers,
|
||||
{ addPoliciesToCiJobToken = false, enforceAllowlist = false, stubs = {} } = {},
|
||||
) => {
|
||||
wrapper = shallowMountExtended(InboundTokenAccess, {
|
||||
provide: {
|
||||
fullPath: projectPath,
|
||||
enforceAllowlist: false,
|
||||
glFeatures: { addPoliciesToCiJobToken: false },
|
||||
...provide,
|
||||
enforceAllowlist,
|
||||
glFeatures: { addPoliciesToCiJobToken },
|
||||
},
|
||||
apolloProvider: createMockApollo(requestHandlers),
|
||||
mocks: {
|
||||
$toast: {
|
||||
show: mockToastShow,
|
||||
},
|
||||
$toast: { show: mockToastShow },
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: createMockDirective('gl-tooltip'),
|
||||
},
|
||||
stubs: {
|
||||
CrudComponent: stubComponent(CrudComponent),
|
||||
...stubs,
|
||||
},
|
||||
});
|
||||
|
||||
return waitForPromises();
|
||||
|
|
@ -311,7 +317,7 @@ describe('TokenAccess component', () => {
|
|||
inboundGroupsAndProjectsWithScopeResponseHandler,
|
||||
],
|
||||
],
|
||||
mountExtended,
|
||||
{ stubs: { CrudComponent } },
|
||||
),
|
||||
);
|
||||
|
||||
|
|
@ -344,42 +350,25 @@ describe('TokenAccess component', () => {
|
|||
});
|
||||
|
||||
describe.each`
|
||||
type | index | mutation | handler
|
||||
${'group'} | ${0} | ${inboundRemoveGroupCIJobTokenScopeMutation} | ${inboundRemoveGroupSuccessHandler}
|
||||
${'project'} | ${1} | ${inboundRemoveProjectCIJobTokenScopeMutation} | ${inboundRemoveProjectSuccessHandler}
|
||||
`('remove $type', ({ type, index, mutation, handler }) => {
|
||||
type | mutation | handler
|
||||
${'Group'} | ${inboundRemoveGroupCIJobTokenScopeMutation} | ${inboundRemoveGroupSuccessHandler}
|
||||
${'Project'} | ${inboundRemoveProjectCIJobTokenScopeMutation} | ${inboundRemoveProjectSuccessHandler}
|
||||
`('remove $type', ({ type, mutation, handler }) => {
|
||||
describe('when remove button is clicked', () => {
|
||||
beforeEach(async () => {
|
||||
await createComponent(
|
||||
[
|
||||
[
|
||||
inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery,
|
||||
inboundGroupsAndProjectsWithScopeResponseHandler,
|
||||
],
|
||||
[mutation, handler],
|
||||
],
|
||||
mountExtended,
|
||||
);
|
||||
await createComponent([[mutation, handler]]);
|
||||
|
||||
return findRemoveProjectBtnAt(index).trigger('click');
|
||||
findTokenAccessTable().vm.$emit('removeItem', { fullPath: 'full/path' });
|
||||
});
|
||||
|
||||
it('shows remove confirmation modal', () => {
|
||||
expect(findConfirmActionModal().props()).toMatchObject({
|
||||
title: `Remove root/ci-${type}`,
|
||||
title: `Remove full/path`,
|
||||
actionFn: wrapper.vm.removeItem,
|
||||
actionText: 'Remove group or project',
|
||||
});
|
||||
});
|
||||
|
||||
describe('when confirmation modal calls the action', () => {
|
||||
beforeEach(() => findConfirmActionModal().vm.performAction());
|
||||
|
||||
it(`calls remove ${type} mutation`, () => {
|
||||
expect(handler).toHaveBeenCalledWith({ projectPath, targetPath: expect.any(String) });
|
||||
});
|
||||
});
|
||||
|
||||
describe('after confirmation modal closes', () => {
|
||||
beforeEach(() => findConfirmActionModal().vm.$emit('close'));
|
||||
|
||||
|
|
@ -391,18 +380,9 @@ describe('TokenAccess component', () => {
|
|||
|
||||
describe('when there is a mutation error', () => {
|
||||
beforeEach(async () => {
|
||||
await createComponent(
|
||||
[
|
||||
[
|
||||
inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery,
|
||||
inboundGroupsAndProjectsWithScopeResponseHandler,
|
||||
],
|
||||
[mutation, failureHandler],
|
||||
],
|
||||
mountExtended,
|
||||
);
|
||||
await createComponent([[mutation, failureHandler]]);
|
||||
|
||||
return findRemoveProjectBtnAt(index).trigger('click');
|
||||
findTokenAccessTable().vm.$emit('removeItem', { fullPath: 'full/path', __typename: type });
|
||||
});
|
||||
|
||||
it('returns an error', async () => {
|
||||
|
|
@ -420,9 +400,8 @@ describe('TokenAccess component', () => {
|
|||
inboundGroupsAndProjectsWithScopeResponseHandler,
|
||||
],
|
||||
];
|
||||
const provide = { enforceAllowlist: true };
|
||||
|
||||
return createComponent(requestHandlers, shallowMountExtended, provide);
|
||||
return createComponent(requestHandlers, { enforceAllowlist: true });
|
||||
});
|
||||
|
||||
it('hides alert, options, and submit button', () => {
|
||||
|
|
@ -441,7 +420,7 @@ describe('TokenAccess component', () => {
|
|||
],
|
||||
];
|
||||
|
||||
return createComponent(requestHandlers, mountExtended);
|
||||
return createComponent(requestHandlers, { stubs: { CrudComponent } });
|
||||
});
|
||||
|
||||
describe('when allowlist query is loaded', () => {
|
||||
|
|
@ -496,7 +475,7 @@ describe('TokenAccess component', () => {
|
|||
${true} | ${0} | ${1}
|
||||
${false} | ${1} | ${0}
|
||||
`(
|
||||
'when addPoliciestoCiJobToken is $addPoliciesToCiJobToken',
|
||||
'when addPoliciesToCiJobToken feature flag is $addPoliciesToCiJobToken',
|
||||
({ addPoliciesToCiJobToken, oldQueryCallCount, newQueryCallCount }) => {
|
||||
const oldQueryHandler = jest.fn();
|
||||
const newQueryHandler = jest.fn();
|
||||
|
|
@ -507,8 +486,7 @@ describe('TokenAccess component', () => {
|
|||
[inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery, oldQueryHandler],
|
||||
[getCiJobTokenScopeAllowlistQuery, newQueryHandler],
|
||||
],
|
||||
mountExtended,
|
||||
{ glFeatures: { addPoliciesToCiJobToken } },
|
||||
{ addPoliciesToCiJobToken },
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -521,4 +499,27 @@ describe('TokenAccess component', () => {
|
|||
});
|
||||
},
|
||||
);
|
||||
|
||||
describe('editing an allowlist item', () => {
|
||||
const item = {};
|
||||
|
||||
beforeEach(async () => {
|
||||
await createComponent([], { stubs: { CrudComponent } });
|
||||
findTokenAccessTable().vm.$emit('editItem', item);
|
||||
});
|
||||
|
||||
it('shows the form with the namespace', () => {
|
||||
expect(findNamespaceForm().props('namespace')).toBe(item);
|
||||
});
|
||||
|
||||
describe('when form is closed', () => {
|
||||
beforeEach(() => findNamespaceForm().vm.$emit('close'));
|
||||
|
||||
it('clears the selected namespace', async () => {
|
||||
await findToggleFormBtn().vm.$emit('click');
|
||||
|
||||
expect(findNamespaceForm().props('namespace')).toBe(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -197,9 +197,9 @@ export const inboundGroupsAndProjectsWithScopeResponse = {
|
|||
},
|
||||
};
|
||||
|
||||
export const getAddNamespaceHandler = (error) =>
|
||||
export const getSaveNamespaceHandler = (error) =>
|
||||
jest.fn().mockResolvedValue({
|
||||
data: { ciJobTokenScopeAddGroupOrProject: { errors: error ? [error] : [] } },
|
||||
data: { saveNamespace: { errors: error ? [error] : [] } },
|
||||
});
|
||||
|
||||
export const inboundRemoveNamespaceSuccess = {
|
||||
|
|
|
|||
|
|
@ -5,24 +5,32 @@ import createMockApollo from 'helpers/mock_apollo_helper';
|
|||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import NamespaceForm from '~/token_access/components/namespace_form.vue';
|
||||
import addNamespaceMutation from '~/token_access/graphql/mutations/inbound_add_group_or_project_ci_job_token_scope.mutation.graphql';
|
||||
import editNamespaceMutation from '~/token_access/graphql/mutations/edit_namespace_job_token_scope.mutation.graphql';
|
||||
import { stubComponent } from 'helpers/stub_component';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import PoliciesSelector from '~/token_access/components/policies_selector.vue';
|
||||
import { getAddNamespaceHandler } from './mock_data';
|
||||
import { getSaveNamespaceHandler } from './mock_data';
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
||||
describe('Namespace form component', () => {
|
||||
let wrapper;
|
||||
|
||||
const defaultAddMutationHandler = getAddNamespaceHandler();
|
||||
const defaultAddMutationHandler = getSaveNamespaceHandler();
|
||||
const defaultEditMutationHandler = getSaveNamespaceHandler();
|
||||
|
||||
const createWrapper = ({
|
||||
namespace,
|
||||
addMutationHandler = defaultAddMutationHandler,
|
||||
editMutationHandler = defaultEditMutationHandler,
|
||||
addPoliciesToCiJobToken = true,
|
||||
} = {}) => {
|
||||
wrapper = shallowMountExtended(NamespaceForm, {
|
||||
apolloProvider: createMockApollo([[addNamespaceMutation, addMutationHandler]]),
|
||||
apolloProvider: createMockApollo([
|
||||
[addNamespaceMutation, addMutationHandler],
|
||||
[editNamespaceMutation, editMutationHandler],
|
||||
]),
|
||||
propsData: { namespace },
|
||||
provide: { fullPath: 'full/path', glFeatures: { addPoliciesToCiJobToken } },
|
||||
stubs: {
|
||||
GlFormInput: stubComponent(GlFormInput, {
|
||||
|
|
@ -34,7 +42,7 @@ describe('Namespace form component', () => {
|
|||
|
||||
const findFormGroup = () => wrapper.findComponent(GlFormGroup);
|
||||
const findFormInput = () => wrapper.findComponent(GlFormInput);
|
||||
const findAddButton = () => wrapper.findByTestId('add-button');
|
||||
const findSubmitButton = () => wrapper.findByTestId('submit-button');
|
||||
const findCancelButton = () => wrapper.findByTestId('cancel-button');
|
||||
const findPoliciesSelector = () => wrapper.findComponent(PoliciesSelector);
|
||||
|
||||
|
|
@ -84,10 +92,10 @@ describe('Namespace form component', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('Add button', () => {
|
||||
describe('Submit button', () => {
|
||||
it('shows button', () => {
|
||||
expect(findAddButton().text()).toBe('Add');
|
||||
expect(findAddButton().props()).toMatchObject({
|
||||
expect(findSubmitButton().text()).toBe('Add');
|
||||
expect(findSubmitButton().props()).toMatchObject({
|
||||
variant: 'confirm',
|
||||
disabled: true,
|
||||
loading: false,
|
||||
|
|
@ -114,12 +122,12 @@ describe('Namespace form component', () => {
|
|||
});
|
||||
|
||||
it('enables Save button', () => {
|
||||
expect(findAddButton().props('disabled')).toBe(false);
|
||||
expect(findSubmitButton().props('disabled')).toBe(false);
|
||||
});
|
||||
|
||||
describe('when the save button is clicked', () => {
|
||||
beforeEach(() => {
|
||||
findAddButton().vm.$emit('click');
|
||||
findSubmitButton().vm.$emit('click');
|
||||
});
|
||||
|
||||
it('runs save mutation', () => {
|
||||
|
|
@ -140,8 +148,8 @@ describe('Namespace form component', () => {
|
|||
expect(findPoliciesSelector().props('disabled')).toBe(true);
|
||||
});
|
||||
|
||||
it('disables Add button', () => {
|
||||
expect(findAddButton().props('loading')).toBe(true);
|
||||
it('disables submit button', () => {
|
||||
expect(findSubmitButton().props('loading')).toBe(true);
|
||||
});
|
||||
|
||||
it('disables Cancel button', () => {
|
||||
|
|
@ -164,8 +172,8 @@ describe('Namespace form component', () => {
|
|||
expect(findPoliciesSelector().props('disabled')).toBe(false);
|
||||
});
|
||||
|
||||
it('enables Add button', () => {
|
||||
expect(findAddButton().props('loading')).toBe(false);
|
||||
it('enables submit button', () => {
|
||||
expect(findSubmitButton().props('loading')).toBe(false);
|
||||
});
|
||||
|
||||
it('enables Cancel button', () => {
|
||||
|
|
@ -178,12 +186,12 @@ describe('Namespace form component', () => {
|
|||
|
||||
describe.each`
|
||||
phrase | addMutationHandler
|
||||
${'when the mutation response contains an error'} | ${getAddNamespaceHandler('some error')}
|
||||
${'when the mutation response contains an error'} | ${getSaveNamespaceHandler('some error')}
|
||||
${'when the mutation throws an error'} | ${jest.fn().mockRejectedValue(new Error('some error'))}
|
||||
`('$phrase', ({ addMutationHandler }) => {
|
||||
beforeEach(() => {
|
||||
createWrapper({ addMutationHandler });
|
||||
findAddButton().vm.$emit('click');
|
||||
findSubmitButton().vm.$emit('click');
|
||||
|
||||
return waitForPromises();
|
||||
});
|
||||
|
|
@ -200,8 +208,8 @@ describe('Namespace form component', () => {
|
|||
expect(findFormInput().props('disabled')).toBe(false);
|
||||
});
|
||||
|
||||
it('enables Add button', () => {
|
||||
expect(findAddButton().props('loading')).toBe(false);
|
||||
it('enables submit button', () => {
|
||||
expect(findSubmitButton().props('loading')).toBe(false);
|
||||
});
|
||||
|
||||
it('enables Cancel button', () => {
|
||||
|
|
@ -211,7 +219,7 @@ describe('Namespace form component', () => {
|
|||
describe.each`
|
||||
phrase | actionFn
|
||||
${'when the namespace input is changed'} | ${() => findFormInput().vm.$emit('input', 'gitlab2')}
|
||||
${'when the Add button is clicked'} | ${() => findAddButton().vm.$emit('click')}
|
||||
${'when the submit button is clicked'} | ${() => findSubmitButton().vm.$emit('click')}
|
||||
`('$phrase', ({ actionFn }) => {
|
||||
beforeEach(() => {
|
||||
actionFn();
|
||||
|
|
@ -238,7 +246,7 @@ describe('Namespace form component', () => {
|
|||
describe('when namespace is saved', () => {
|
||||
it('calls mutation without defaultPermissions or jobTokenPolicies', () => {
|
||||
findFormInput().vm.$emit('input', 'gitlab');
|
||||
findAddButton().vm.$emit('click');
|
||||
findSubmitButton().vm.$emit('click');
|
||||
|
||||
expect(defaultAddMutationHandler).toHaveBeenCalledWith({
|
||||
projectPath: 'full/path',
|
||||
|
|
@ -247,4 +255,43 @@ describe('Namespace form component', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('editing a namespace', () => {
|
||||
beforeEach(() =>
|
||||
createWrapper({
|
||||
namespace: {
|
||||
fullPath: 'namespace/path',
|
||||
defaultPermissions: false,
|
||||
jobTokenPolicies: ['READ_JOBS'],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
describe('path input', () => {
|
||||
it('disables the input', () => {
|
||||
expect(findFormInput().props('disabled')).toBe(true);
|
||||
});
|
||||
|
||||
it('shows the namespace full path', () => {
|
||||
expect(findFormInput().attributes('value')).toBe('namespace/path');
|
||||
});
|
||||
});
|
||||
|
||||
it('passes expected values to the policies selector', () => {
|
||||
expect(findPoliciesSelector().props()).toMatchObject({
|
||||
isDefaultPermissionsSelected: false,
|
||||
jobTokenPolicies: ['READ_JOBS'],
|
||||
});
|
||||
});
|
||||
|
||||
it('shows "Save" for the submit button text', () => {
|
||||
expect(findSubmitButton().text()).toBe('Save');
|
||||
});
|
||||
|
||||
it('calls the edit mutation when the submit button is clicked', () => {
|
||||
findSubmitButton().vm.$emit('click');
|
||||
|
||||
expect(defaultEditMutationHandler).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { GlButton, GlTable, GlLoadingIcon } from '@gitlab/ui';
|
||||
import { GlTable, GlLoadingIcon } from '@gitlab/ui';
|
||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import TokenAccessTable from '~/token_access/components/token_access_table.vue';
|
||||
import { mockGroups, mockProjects } from './mock_data';
|
||||
|
|
@ -14,7 +14,8 @@ describe('Token access table', () => {
|
|||
};
|
||||
|
||||
const findTable = () => wrapper.findComponent(GlTable);
|
||||
const findDeleteButton = () => wrapper.findComponent(GlButton);
|
||||
const findEditButton = () => wrapper.findByTestId('token-access-table-edit-button');
|
||||
const findRemoveButton = () => wrapper.findByTestId('token-access-table-remove-button');
|
||||
const findAllTableRows = () => findTable().findAll('tbody tr');
|
||||
const findIcon = () => wrapper.findByTestId('token-access-icon');
|
||||
const findProjectAvatar = () => wrapper.findByTestId('token-access-avatar');
|
||||
|
|
@ -38,8 +39,8 @@ describe('Token access table', () => {
|
|||
expect(findAllTableRows(type)).toHaveLength(items.length);
|
||||
});
|
||||
|
||||
it('delete button emits event with correct item to delete', async () => {
|
||||
await findDeleteButton().trigger('click');
|
||||
it('remove button emits event with correct item to remove', async () => {
|
||||
await findRemoveButton().trigger('click');
|
||||
|
||||
expect(wrapper.emitted('removeItem')).toEqual([[items[0]]]);
|
||||
});
|
||||
|
|
@ -53,6 +54,30 @@ describe('Token access table', () => {
|
|||
expect(findName(type).text()).toBe(items[0].fullPath);
|
||||
expect(findName(type).attributes('href')).toBe(items[0].webUrl);
|
||||
});
|
||||
|
||||
describe('edit button', () => {
|
||||
it('shows button', () => {
|
||||
expect(findEditButton().props('icon')).toBe('pencil');
|
||||
});
|
||||
|
||||
it('emits editItem event when button is clicked', () => {
|
||||
findEditButton().vm.$emit('click');
|
||||
|
||||
expect(wrapper.emitted('editItem')[0][0]).toBe(items[0]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when item is the current project', () => {
|
||||
beforeEach(() => createComponent({ items: [mockProjects.at(-1)] }));
|
||||
|
||||
it('does not show edit button', () => {
|
||||
expect(findEditButton().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('does not show remove button', () => {
|
||||
expect(findRemoveButton().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when table is loading', () => {
|
||||
|
|
@ -85,14 +110,18 @@ describe('Token access table', () => {
|
|||
});
|
||||
|
||||
describe('when showPolicies prop is false', () => {
|
||||
it('does not show policies column', () => {
|
||||
createComponent({ showPolicies: false, items: [] });
|
||||
beforeEach(() => createComponent({ showPolicies: false, items: mockGroups }));
|
||||
|
||||
it('does not show policies column', () => {
|
||||
const tableFieldKeys = findTable()
|
||||
.props('fields')
|
||||
.map(({ key }) => key);
|
||||
|
||||
expect(tableFieldKeys).not.toContain('policies');
|
||||
});
|
||||
|
||||
it('does not show edit button', () => {
|
||||
expect(findEditButton().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -230,4 +230,15 @@ describe('CRUD Component', () => {
|
|||
expect(wrapper.emitted('collapsed')).toStrictEqual([[]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('default slot', () => {
|
||||
it('passes the showForm function to the default slot', () => {
|
||||
const defaultSlot = jest.fn();
|
||||
createComponent({}, { default: defaultSlot });
|
||||
|
||||
expect(defaultSlot).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ showForm: wrapper.vm.showForm }),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ module Database
|
|||
SQL
|
||||
end
|
||||
|
||||
def find_partition_definition(partition, schema: )
|
||||
def find_partition_definition(partition, schema:)
|
||||
connection.select_one(<<~SQL)
|
||||
select
|
||||
parent_class.relname as base_table,
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ module DependencyProxyHelpers
|
|||
manifest_url = registry.manifest_url(image, tag)
|
||||
|
||||
stub_full_request(manifest_url, method: :head)
|
||||
.to_return(status: status, body: body, headers: headers )
|
||||
.to_return(status: status, body: body, headers: headers)
|
||||
end
|
||||
|
||||
def stub_blob_download(image, blob_sha, status = 200, body = '123456')
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ module JavaScriptFixturesHelpers
|
|||
path = Rails.root / base / query_path
|
||||
queries = Gitlab::Graphql::Queries.find(path)
|
||||
if queries.length == 1
|
||||
query = queries.first.text(mode: Gitlab.ee? ? :ee : :ce )
|
||||
query = queries.first.text(mode: Gitlab.ee? ? :ee : :ce)
|
||||
inflate_query_with_typenames(query)
|
||||
else
|
||||
raise "Could not find query file at #{path}, please check your query_path" % path
|
||||
|
|
|
|||
|
|
@ -154,7 +154,7 @@ module KubernetesHelpers
|
|||
minor: min_version.to_s
|
||||
})
|
||||
|
||||
WebMock.stub_request( :get, service.api_url + '/version')
|
||||
WebMock.stub_request(:get, service.api_url + '/version')
|
||||
.with(
|
||||
headers: {
|
||||
'Accept' => '*/*',
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ RSpec.shared_context 'container repository delete tags service shared context' d
|
|||
content = "{\n \"config\": {\n }\n}"
|
||||
expect_any_instance_of(ContainerRegistry::Client)
|
||||
.to receive(:upload_blob)
|
||||
.with(repository.path, content, digest) { double(success?: success ) }
|
||||
.with(repository.path, content, digest) { double(success?: success) }
|
||||
end
|
||||
|
||||
def expect_delete_tags(names)
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ RSpec.shared_examples 'Web hook destroyer' do
|
|||
|
||||
it 'displays a message about async delete', :aggregate_failures do
|
||||
expect_next_instance_of(WebHooks::DestroyService) do |instance|
|
||||
expect(instance).to receive(:execute).with(anything).and_return({ status: :success, async: true } )
|
||||
expect(instance).to receive(:execute).with(anything).and_return({ status: :success, async: true })
|
||||
end
|
||||
|
||||
delete :destroy, params: params
|
||||
|
|
@ -25,7 +25,7 @@ RSpec.shared_examples 'Web hook destroyer' do
|
|||
|
||||
it 'displays an error if deletion failed', :aggregate_failures do
|
||||
expect_next_instance_of(WebHooks::DestroyService) do |instance|
|
||||
expect(instance).to receive(:execute).with(anything).and_return({ status: :error, async: true, message: "failed" } )
|
||||
expect(instance).to receive(:execute).with(anything).and_return({ status: :error, async: true, message: "failed" })
|
||||
end
|
||||
|
||||
delete :destroy, params: params
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ RSpec.shared_examples 'project features apply to issuables' do |klass|
|
|||
let(:described_class) { klass }
|
||||
|
||||
let(:group) { create(:group) }
|
||||
let(:user_in_group) { create(:group_member, :developer, user: create(:user), group: group ).user }
|
||||
let(:user_in_group) { create(:group_member, :developer, user: create(:user), group: group).user }
|
||||
let(:user_outside_group) { create(:user) }
|
||||
|
||||
let(:project) { create(:project, :public, project_args) }
|
||||
|
|
|
|||
|
|
@ -209,7 +209,7 @@ RSpec.shared_examples 'User views a wiki page' do
|
|||
let(:title) { '<foo> !@#$%^&*()[]{}=_+\'"\\|<>? <bar>' }
|
||||
|
||||
before do
|
||||
wiki_page.update(title: title ) # rubocop:disable Rails/SaveBang
|
||||
wiki_page.update(title: title) # rubocop:disable Rails/SaveBang
|
||||
end
|
||||
|
||||
it 'preserves the special characters' do
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ require 'spec_helper'
|
|||
RSpec.shared_examples 'Debian Distributions Finder' do |factory, can_freeze|
|
||||
let_it_be(:distribution_with_suite, freeze: can_freeze) { create(factory, :with_suite) }
|
||||
let_it_be(:container) { distribution_with_suite.container }
|
||||
let_it_be(:distribution_with_same_container, freeze: can_freeze) { create(factory, container: container ) }
|
||||
let_it_be(:distribution_with_same_codename, freeze: can_freeze) { create(factory, codename: distribution_with_suite.codename ) }
|
||||
let_it_be(:distribution_with_same_suite, freeze: can_freeze) { create(factory, suite: distribution_with_suite.suite ) }
|
||||
let_it_be(:distribution_with_same_container, freeze: can_freeze) { create(factory, container: container) }
|
||||
let_it_be(:distribution_with_same_codename, freeze: can_freeze) { create(factory, codename: distribution_with_suite.codename) }
|
||||
let_it_be(:distribution_with_same_suite, freeze: can_freeze) { create(factory, suite: distribution_with_suite.suite) }
|
||||
let_it_be(:distribution_with_codename_and_suite_flipped, freeze: can_freeze) { create(factory, codename: distribution_with_suite.suite, suite: distribution_with_suite.codename) }
|
||||
|
||||
let(:params) { {} }
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ RSpec.shared_examples 'Gitlab-style deprecations' do
|
|||
|
||||
it 'does not allow :experiment and :deprecated together' do
|
||||
expect do
|
||||
subject(experiment: { milestone: '1.10' }, deprecated: { milestone: '1.10', reason: 'my reason' } )
|
||||
subject(experiment: { milestone: '1.10' }, deprecated: { milestone: '1.10', reason: 'my reason' })
|
||||
end.to raise_error(
|
||||
ArgumentError,
|
||||
eq("`experiment` and `deprecated` arguments cannot be passed at the same time")
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ RSpec.shared_examples "position formatter" do
|
|||
|
||||
describe '#to_h' do
|
||||
let(:formatter_hash) do
|
||||
attrs.merge(position_type: base_attrs[:position_type] || 'text' )
|
||||
attrs.merge(position_type: base_attrs[:position_type] || 'text')
|
||||
end
|
||||
|
||||
subject { formatter.to_h }
|
||||
|
|
|
|||
|
|
@ -196,7 +196,7 @@ RSpec.shared_examples 'deduplicating jobs when scheduling' do |strategy_name|
|
|||
allow(fake_duplicate_job).to receive(:delete!)
|
||||
allow(fake_duplicate_job).to receive(:scheduled?) { false }
|
||||
allow(fake_duplicate_job).to receive(:options) { {} }
|
||||
allow(fake_duplicate_job).to receive(:latest_wal_locations).and_return( wal_locations )
|
||||
allow(fake_duplicate_job).to receive(:latest_wal_locations).and_return(wal_locations)
|
||||
allow(fake_duplicate_job).to receive(:idempotency_key).and_return('abc123')
|
||||
allow(fake_duplicate_job).to receive(:strategy).and_return(:until_executed)
|
||||
allow(fake_duplicate_job).to receive(:reschedulable?) { false }
|
||||
|
|
@ -222,7 +222,7 @@ RSpec.shared_examples 'deduplicating jobs when scheduling' do |strategy_name|
|
|||
|
||||
context 'when latest_wal_location is empty' do
|
||||
before do
|
||||
allow(fake_duplicate_job).to receive(:latest_wal_locations).and_return( {} )
|
||||
allow(fake_duplicate_job).to receive(:latest_wal_locations).and_return({})
|
||||
end
|
||||
|
||||
include_examples 'does not update job hash'
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ RSpec.shared_examples 'a new thread email with reply-by-email enabled' do
|
|||
|
||||
aggregate_failures do
|
||||
is_expected.to have_header('Message-ID', "<#{route_key}@#{host}>")
|
||||
is_expected.to have_header('References', /\A<reply-.*@#{host}>\Z/ )
|
||||
is_expected.to have_header('References', /\A<reply-.*@#{host}>\Z/)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -109,7 +109,7 @@ RSpec.shared_examples 'a thread answer email with reply-by-email enabled' do |gr
|
|||
aggregate_failures do
|
||||
is_expected.to have_header('Message-ID', /\A<.*@#{host}>\Z/)
|
||||
is_expected.to have_header('In-Reply-To', "<#{route_key}@#{host}>")
|
||||
is_expected.to have_header('References', /\A<reply-.*@#{host}> <#{route_key}@#{host}>\Z/ )
|
||||
is_expected.to have_header('References', /\A<reply-.*@#{host}> <#{route_key}@#{host}>\Z/)
|
||||
is_expected.to have_subject(/^Re: /)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name
|
|||
let_it_be(:project) { create(:project, :repository, :wiki_repo) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
let(:chat_integration) { described_class.new( { project: project, webhook: webhook_url, branches_to_be_notified: 'all' }.merge(chat_integration_params)) }
|
||||
let(:chat_integration) { described_class.new({ project: project, webhook: webhook_url, branches_to_be_notified: 'all' }.merge(chat_integration_params)) }
|
||||
let(:chat_integration_params) { {} }
|
||||
let(:data) { Gitlab::DataBuilder::Push.build_sample(project, user) }
|
||||
|
||||
|
|
|
|||
|
|
@ -25,12 +25,12 @@ end
|
|||
RSpec.shared_examples 'Composer package index' do |member_role:, expected_status:, package_returned:|
|
||||
include_context 'Composer user type', member_role: member_role do
|
||||
let_it_be(:expected_packages) { package_returned ? [package] : [] }
|
||||
let_it_be(:presenter) { ::Packages::Composer::PackagesPresenter.new(group, expected_packages ) }
|
||||
let_it_be(:presenter) { ::Packages::Composer::PackagesPresenter.new(group, expected_packages) }
|
||||
|
||||
it_behaves_like 'Composer package index with version', 'public_api/v4/packages/composer/index', expected_status
|
||||
|
||||
context 'with version 2' do
|
||||
let_it_be(:presenter) { ::Packages::Composer::PackagesPresenter.new(group, expected_packages, true ) }
|
||||
let_it_be(:presenter) { ::Packages::Composer::PackagesPresenter.new(group, expected_packages, true) }
|
||||
let(:headers) { super().merge('User-Agent' => 'Composer/2.0.9 (Darwin; 19.6.0; PHP 7.4.8; cURL 7.71.1)') }
|
||||
|
||||
it_behaves_like 'Composer package index with version', 'public_api/v4/packages/composer/index_v2', expected_status
|
||||
|
|
|
|||
|
|
@ -33,8 +33,8 @@ RSpec.shared_examples 'correct release milestone order' do
|
|||
|
||||
context 'start_date' do
|
||||
before do
|
||||
milestone_1.update!(due_date: 1.day.from_now, start_date: 1.day.ago, title: 'z' )
|
||||
milestone_2.update!(due_date: 1.day.from_now, start_date: milestone_2_start_date, title: 'a' )
|
||||
milestone_1.update!(due_date: 1.day.from_now, start_date: 1.day.ago, title: 'z')
|
||||
milestone_2.update!(due_date: 1.day.from_now, start_date: milestone_2_start_date, title: 'a')
|
||||
end
|
||||
|
||||
context 'when both milestones have a start_date' do
|
||||
|
|
@ -52,8 +52,8 @@ RSpec.shared_examples 'correct release milestone order' do
|
|||
|
||||
context 'title' do
|
||||
before do
|
||||
milestone_1.update!(due_date: 1.day.from_now, start_date: Time.zone.now, title: 'a' )
|
||||
milestone_2.update!(due_date: 1.day.from_now, start_date: Time.zone.now, title: 'z' )
|
||||
milestone_1.update!(due_date: 1.day.from_now, start_date: Time.zone.now, title: 'a')
|
||||
milestone_2.update!(due_date: 1.day.from_now, start_date: Time.zone.now, title: 'z')
|
||||
end
|
||||
|
||||
it_behaves_like 'correct sort order'
|
||||
|
|
|
|||
|
|
@ -34,11 +34,11 @@ RSpec.shared_examples 'avoid N+1 on environments serialization' do
|
|||
create_environment_with_associations(project)
|
||||
|
||||
first_page_query = ActiveRecord::QueryRecorder.new do
|
||||
serialize(grouping: false, query: { page: 1, per_page: 1 } )
|
||||
serialize(grouping: false, query: { page: 1, per_page: 1 })
|
||||
end
|
||||
|
||||
second_page_query = ActiveRecord::QueryRecorder.new do
|
||||
serialize(grouping: false, query: { page: 2, per_page: 1 } )
|
||||
serialize(grouping: false, query: { page: 2, per_page: 1 })
|
||||
end
|
||||
|
||||
expect(second_page_query.count).to be < first_page_query.count
|
||||
|
|
|
|||
Loading…
Reference in New Issue