Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-09-04 03:08:22 +00:00
parent dc965b8cc8
commit 583bde3f83
32 changed files with 333 additions and 221 deletions

View file

@ -0,0 +1,42 @@
<script>
import { GlBanner } from '@gitlab/ui';
import { s__ } from '~/locale';
export default {
components: {
GlBanner,
},
inject: ['svgPath', 'inviteMembersPath'],
data() {
return {
visible: true,
};
},
methods: {
handleClose() {
this.visible = false;
},
},
i18n: {
title: s__('InviteMembersBanner|Collaborate with your team'),
body: s__(
"InviteMembersBanner|We noticed that you haven't invited anyone to this group. Invite your colleagues so you can discuss issues, collaborate on merge requests, and share your knowledge.",
),
button_text: s__('InviteMembersBanner|Invite your colleagues'),
},
};
</script>
<template>
<gl-banner
v-if="visible"
ref="banner"
:title="$options.i18n.title"
:button-text="$options.i18n.button_text"
:svg-path="svgPath"
:button-link="inviteMembersPath"
@close="handleClose"
>
<p>{{ $options.i18n.body }}</p>
</gl-banner>
</template>

View file

@ -0,0 +1,21 @@
import Vue from 'vue';
import InviteMembersBanner from '~/groups/components/invite_members_banner.vue';
export default function initInviteMembersBanner() {
const el = document.querySelector('.js-group-invite-members-banner');
if (!el) {
return false;
}
const { svgPath, inviteMembersPath } = el.dataset;
return new Vue({
el,
provide: {
svgPath,
inviteMembersPath,
},
render: createElement => createElement(InviteMembersBanner),
});
}

View file

@ -8,6 +8,7 @@ import NotificationsForm from '~/notifications_form';
import ProjectsList from '~/projects_list';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import GroupTabs from './group_tabs';
import initInviteMembersBanner from '~/groups/init_invite_members_banner';
export default function initGroupDetails(actionName = 'show') {
const newGroupChildWrapper = document.querySelector('.js-new-project-subgroup');
@ -27,4 +28,5 @@ export default function initGroupDetails(actionName = 'show') {
if (newGroupChildWrapper) {
new NewGroupChild(newGroupChildWrapper);
}
initInviteMembersBanner();
}

View file

@ -6,21 +6,19 @@ import { __, sprintf } from '~/locale';
import TitleField from '~/vue_shared/components/form/title.vue';
import { redirectTo, joinPaths } from '~/lib/utils/url_utility';
import FormFooterActions from '~/vue_shared/components/form/form_footer_actions.vue';
import { SNIPPET_MARK_EDIT_APP_START } from '~/performance_constants';
import UpdateSnippetMutation from '../mutations/updateSnippet.mutation.graphql';
import CreateSnippetMutation from '../mutations/createSnippet.mutation.graphql';
import { getSnippetMixin } from '../mixins/snippets';
import {
SNIPPET_VISIBILITY_PRIVATE,
SNIPPET_CREATE_MUTATION_ERROR,
SNIPPET_UPDATE_MUTATION_ERROR,
SNIPPET_VISIBILITY_PRIVATE,
} from '../constants';
import defaultVisibilityQuery from '../queries/snippet_visibility.query.graphql';
import SnippetBlobActionsEdit from './snippet_blob_actions_edit.vue';
import SnippetVisibilityEdit from './snippet_visibility_edit.vue';
import SnippetDescriptionEdit from './snippet_description_edit.vue';
import { SNIPPET_MARK_EDIT_APP_START } from '~/performance_constants';
export default {
components: {
@ -33,15 +31,6 @@ export default {
GlLoadingIcon,
},
mixins: [getSnippetMixin],
apollo: {
defaultVisibility: {
query: defaultVisibilityQuery,
manual: true,
result({ data: { selectedLevel } }) {
this.selectedLevelDefault = selectedLevel;
},
},
},
props: {
markdownPreviewPath: {
type: String,
@ -67,7 +56,6 @@ export default {
isUpdating: false,
newSnippet: false,
actions: [],
selectedLevelDefault: SNIPPET_VISIBILITY_PRIVATE,
};
},
computed: {
@ -110,13 +98,6 @@ export default {
descriptionFieldId() {
return `${this.isProjectSnippet ? 'project' : 'personal'}_snippet_description`;
},
newSnippetSchema() {
return {
title: '',
description: '',
visibilityLevel: this.selectedLevelDefault,
};
},
},
beforeCreate() {
performance.mark(SNIPPET_MARK_EDIT_APP_START);
@ -145,7 +126,7 @@ export default {
},
onNewSnippetFetched() {
this.newSnippet = true;
this.snippet = this.newSnippetSchema;
this.snippet = this.$options.newSnippetSchema;
},
onExistingSnippetFetched() {
this.newSnippet = false;
@ -203,6 +184,11 @@ export default {
this.actions = actions;
},
},
newSnippetSchema: {
title: '',
description: '',
visibilityLevel: SNIPPET_VISIBILITY_PRIVATE,
},
};
</script>
<template>

View file

@ -1,8 +1,11 @@
<script>
import { GlIcon, GlFormGroup, GlFormRadio, GlFormRadioGroup, GlLink } from '@gitlab/ui';
import defaultVisibilityQuery from '../queries/snippet_visibility.query.graphql';
import { defaultSnippetVisibilityLevels } from '../utils/blob';
import { SNIPPET_LEVELS_RESTRICTED, SNIPPET_LEVELS_DISABLED } from '~/snippets/constants';
import {
SNIPPET_VISIBILITY,
SNIPPET_VISIBILITY_PRIVATE,
SNIPPET_VISIBILITY_INTERNAL,
SNIPPET_VISIBILITY_PUBLIC,
} from '~/snippets/constants';
export default {
components: {
@ -12,16 +15,6 @@ export default {
GlFormRadioGroup,
GlLink,
},
apollo: {
defaultVisibility: {
query: defaultVisibilityQuery,
manual: true,
result({ data: { visibilityLevels, multipleLevelsRestricted } }) {
this.visibilityLevels = defaultSnippetVisibilityLevels(visibilityLevels);
this.multipleLevelsRestricted = multipleLevelsRestricted;
},
},
},
props: {
helpLink: {
type: String,
@ -35,17 +28,19 @@ export default {
},
value: {
type: String,
required: true,
required: false,
default: SNIPPET_VISIBILITY_PRIVATE,
},
},
data() {
return {
visibilityLevels: [],
multipleLevelsRestricted: false,
};
computed: {
visibilityOptions() {
return [
SNIPPET_VISIBILITY_PRIVATE,
SNIPPET_VISIBILITY_INTERNAL,
SNIPPET_VISIBILITY_PUBLIC,
].map(key => ({ value: key, ...SNIPPET_VISIBILITY[key] }));
},
},
SNIPPET_LEVELS_DISABLED,
SNIPPET_LEVELS_RESTRICTED,
};
</script>
<template>
@ -56,10 +51,10 @@ export default {
><gl-icon :size="12" name="question"
/></gl-link>
</label>
<gl-form-group id="visibility-level-setting" class="gl-mb-0">
<gl-form-radio-group :checked="value" stacked v-bind="$attrs" v-on="$listeners">
<gl-form-group id="visibility-level-setting">
<gl-form-radio-group v-bind="$attrs" :checked="value" stacked v-on="$listeners">
<gl-form-radio
v-for="option in visibilityLevels"
v-for="option in visibilityOptions"
:key="option.value"
:value="option.value"
class="mb-3"
@ -76,12 +71,5 @@ export default {
</gl-form-radio>
</gl-form-radio-group>
</gl-form-group>
<div class="text-muted" data-testid="restricted-levels-info">
<template v-if="!visibilityLevels.length">{{ $options.SNIPPET_LEVELS_DISABLED }}</template>
<template v-else-if="multipleLevelsRestricted">{{
$options.SNIPPET_LEVELS_RESTRICTED
}}</template>
</div>
</div>
</template>

View file

@ -33,15 +33,3 @@ export const SNIPPET_BLOB_ACTION_MOVE = 'move';
export const SNIPPET_BLOB_ACTION_DELETE = 'delete';
export const SNIPPET_MAX_BLOBS = 10;
export const SNIPPET_LEVELS_MAP = {
0: SNIPPET_VISIBILITY_PRIVATE,
10: SNIPPET_VISIBILITY_INTERNAL,
20: SNIPPET_VISIBILITY_PUBLIC,
};
export const SNIPPET_LEVELS_RESTRICTED = __(
'Other visibility settings have been disabled by the administrator.',
);
export const SNIPPET_LEVELS_DISABLED = __(
'Visibility settings have been disabled by the administrator.',
);

View file

@ -5,7 +5,6 @@ import createDefaultClient from '~/lib/graphql';
import SnippetsShow from './components/show.vue';
import SnippetsEdit from './components/edit.vue';
import { SNIPPET_LEVELS_MAP, SNIPPET_VISIBILITY_PRIVATE } from '~/snippets/constants';
Vue.use(VueApollo);
Vue.use(Translate);
@ -19,23 +18,13 @@ function appFactory(el, Component) {
defaultClient: createDefaultClient(),
});
const { visibilityLevels, selectedLevel, multipleLevelsRestricted, ...restDataset } = el.dataset;
apolloProvider.clients.defaultClient.cache.writeData({
data: {
visibilityLevels: JSON.parse(visibilityLevels),
selectedLevel: SNIPPET_LEVELS_MAP[selectedLevel] ?? SNIPPET_VISIBILITY_PRIVATE,
multipleLevelsRestricted: 'multipleLevelsRestricted' in el.dataset,
},
});
return new Vue({
el,
apolloProvider,
render(createElement) {
return createElement(Component, {
props: {
...restDataset,
...el.dataset,
},
});
},

View file

@ -1,5 +0,0 @@
query defaultSnippetVisibility {
visibilityLevels @client
selectedLevel @client
multipleLevelsRestricted @client
}

View file

@ -4,8 +4,6 @@ import {
SNIPPET_BLOB_ACTION_UPDATE,
SNIPPET_BLOB_ACTION_MOVE,
SNIPPET_BLOB_ACTION_DELETE,
SNIPPET_LEVELS_MAP,
SNIPPET_VISIBILITY,
} from '../constants';
const createLocalId = () => uniqueId('blob_local_');
@ -66,16 +64,3 @@ export const diffAll = (blobs, origBlobs) => {
return [...deletedEntries, ...newEntries];
};
export const defaultSnippetVisibilityLevels = arr => {
if (Array.isArray(arr)) {
return arr.map(l => {
const translatedLevel = SNIPPET_LEVELS_MAP[l];
return {
value: translatedLevel,
...SNIPPET_VISIBILITY[translatedLevel],
};
});
}
return [];
};

View file

@ -129,6 +129,10 @@ module AuthenticatesWithTwoFactor
def user_changed?(user)
return false unless session[:user_updated_at]
user.updated_at != session[:user_updated_at]
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/244638
# Rounding errors happen when the user is updated, as the Rails ActiveRecord
# object has higher precision than what is stored in the database, therefore
# using .to_i to force truncation to the timestamp
user.updated_at.to_i != session[:user_updated_at].to_i
end
end

View file

@ -167,8 +167,23 @@ module GroupsHelper
@group.packages_feature_enabled?
end
def show_invite_banner?(group)
Feature.enabled?(:invite_your_teammates_banner_a, group) &&
can?(current_user, :admin_group, group) &&
!just_created? &&
!multiple_members?(group)
end
private
def just_created?
flash[:notice] =~ /successfully created/
end
def multiple_members?(group)
group.member_count > 1
end
def get_group_sidebar_links
links = [:overview, :group_members]

View file

@ -2,6 +2,12 @@
- page_title _("Groups")
- @content_class = "limit-container-width" unless fluid_layout
- if show_invite_banner?(@group)
= content_for :group_invite_members_banner do
.container-fluid.container-limited{ class: "gl-pb-2! gl-pt-6! #{@content_class}" }
.js-group-invite-members-banner{ data: { svg_path: image_path('illustrations/merge_requests.svg'),
invite_members_path: group_group_members_path(@group) } }
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, group_url(@group, rss_url_options), title: "#{@group.name} activity")

View file

@ -3,6 +3,7 @@
= render "layouts/nav/sidebar/#{nav}"
.content-wrapper{ class: "#{@content_wrapper_class}" }
.mobile-overlay
= yield :group_invite_members_banner
.alert-wrapper
= render 'shared/outdated_browser'
= render_if_exists "layouts/header/licensed_user_count_threshold"

View file

@ -1,6 +1,5 @@
- if Feature.enabled?(:snippets_edit_vue)
- available_visibility_levels = available_visibility_levels(@snippet)
#js-snippet-edit.snippet-form{ data: {'project_path': @snippet.project&.full_path, 'snippet-gid': @snippet.new_record? ? '' : @snippet.to_global_id, 'markdown-preview-path': preview_markdown_path(parent), 'markdown-docs-path': help_page_path('user/markdown'), 'visibility-help-link': help_page_path("public_access/public_access"), 'visibility_levels': available_visibility_levels, 'selected_level': snippets_selected_visibility_level(available_visibility_levels, @snippet.visibility_level), 'multiple_levels_restricted': multiple_visibility_levels_restricted? } }
#js-snippet-edit.snippet-form{ data: {'project_path': @snippet.project&.full_path, 'snippet-gid': @snippet.new_record? ? '' : @snippet.to_global_id, 'markdown-preview-path': preview_markdown_path(parent), 'markdown-docs-path': help_page_path('user/markdown'), 'visibility-help-link': help_page_path("public_access/public_access") } }
- else
.snippet-form-holder
= form_for @snippet, url: url,

View file

@ -0,0 +1,5 @@
---
title: Update the 2FA user update check to account for rounding errors
merge_request: 41327
author:
type: fixed

View file

@ -0,0 +1,7 @@
---
name: invite_your_teammates_banner_a
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37658
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/231275
group: group::expansion
type: development
default_enabled: false

View file

@ -10,4 +10,4 @@ link: https://docs.gitlab.com/ee/development/documentation/styleguide.html#links
level: error
scope: raw
raw:
- '\[.+\]\(https?:\/\/docs\.gitlab\.com\/ee.*\)'
- '\[.+\]\(https?:\/\/docs\.gitlab\.com\/[ce]e.*\)'

View file

@ -10,9 +10,9 @@ Endpoints for connecting custom domain(s) and TLS certificates in [GitLab Pages]
The GitLab Pages feature must be enabled to use these endpoints. Find out more about [administering](../administration/pages/index.md) and [using](../user/project/pages/index.md) the feature.
## List all pages domains
## List all Pages domains
Get a list of all pages domains. The user must have admin permissions.
Get a list of all Pages domains. The user must have admin permissions.
```plaintext
GET /pages/domains
@ -37,9 +37,9 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/a
]
```
## List pages domains
## List Pages domains
Get a list of project pages domains. The user must have permissions to view pages domains.
Get a list of project Pages domains. The user must have permissions to view Pages domains.
```plaintext
GET /projects/:id/pages/domains
@ -73,9 +73,9 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/a
]
```
## Single pages domain
## Single Pages domain
Get a single project pages domain. The user must have permissions to view pages domains.
Get a single project Pages domain. The user must have permissions to view Pages domains.
```plaintext
GET /projects/:id/pages/domains/:domain
@ -115,9 +115,9 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/a
}
```
## Create new pages domain
## Create new Pages domain
Creates a new pages domain. The user must have permissions to create new pages domains.
Creates a new Pages domain. The user must have permissions to create new Pages domains.
```plaintext
POST /projects/:id/pages/domains
@ -131,14 +131,20 @@ POST /projects/:id/pages/domains
| `certificate` | file/string | no | The certificate in PEM format with intermediates following in most specific to least specific order.|
| `key` | file/string | no | The certificate key in PEM format. |
Create a new Pages domain with a certificate from a `.pem` file:
```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" --form "domain=ssl.domain.example" --form "certificate=@/path/to/cert.pem" --form "key=@/path/to/key.pem" "https://gitlab.example.com/api/v4/projects/5/pages/domains"
```
Create a new Pages domain by using a variable containing the certificate:
```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" --form "domain=ssl.domain.example" --form "certificate=$CERT_PEM" --form "key=$KEY_PEM" "https://gitlab.example.com/api/v4/projects/5/pages/domains"
```
Create a new Pages domain with an [automatic certificate](../user/project/pages/custom_domains_ssl_tls_certification/lets_encrypt_integration.md#enabling-lets-encrypt-integration-for-your-custom-domain):
```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" --form "domain=ssl.domain.example" --form "auto_ssl_enabled=true" "https://gitlab.example.com/api/v4/projects/5/pages/domains"
```
@ -157,9 +163,9 @@ curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" --form "domain
}
```
## Update pages domain
## Update Pages domain
Updates an existing project pages domain. The user must have permissions to change an existing pages domains.
Updates an existing project Pages domain. The user must have permissions to change an existing Pages domains.
```plaintext
PUT /projects/:id/pages/domains/:domain
@ -175,10 +181,14 @@ PUT /projects/:id/pages/domains/:domain
### Adding certificate
Add a certificate for a Pages domain from a `.pem` file:
```shell
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" --form "certificate=@/path/to/cert.pem" --form "key=@/path/to/key.pem" "https://gitlab.example.com/api/v4/projects/5/pages/domains/ssl.domain.example"
```
Add a certificate for a Pages domain by using a variable containing the certificate:
```shell
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" --form "certificate=$CERT_PEM" --form "key=$KEY_PEM" "https://gitlab.example.com/api/v4/projects/5/pages/domains/ssl.domain.example"
```
@ -227,9 +237,9 @@ curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" --form "certifi
}
```
## Delete pages domain
## Delete Pages domain
Deletes an existing project pages domain.
Deletes an existing project Pages domain.
```plaintext
DELETE /projects/:id/pages/domains/:domain

View file

@ -19,7 +19,7 @@ tranches after a default pause of 5 minutes.
Timed rollouts can also be manually triggered before the pause period has expired.
Manual and Timed rollouts are included automatically in projects controlled by
[AutoDevOps](../../topics/autodevops/index.md), but they are also configurable through
[Auto DevOps](../../topics/autodevops/index.md), but they are also configurable through
GitLab CI/CD in the `.gitlab-ci.yml` configuration file.
Manually triggered rollouts can be implemented with your [Continuously Delivery](../introduction/index.md#continuous-delivery)

View file

@ -64,6 +64,7 @@ choose one of these templates:
- [Clojure (`Clojure.gitlab-ci.yml`)](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Clojure.gitlab-ci.yml)
- [Composer `Composer.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Composer.gitlab-ci.yml)
- [Crystal (`Crystal.gitlab-ci.yml`)](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Crystal.gitlab-ci.yml)
- [Dart (`Dart.gitlab-ci.yml`)](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Dart.gitlab-ci.yml)
- [Django (`Django.gitlab-ci.yml`)](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Django.gitlab-ci.yml)
- [Docker (`Docker.gitlab-ci.yml`)](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Docker.gitlab-ci.yml)
- [dotNET (`dotNET.gitlab-ci.yml`)](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/dotNET.gitlab-ci.yml)

View file

@ -982,7 +982,7 @@ If you do want to include the `rake test`, see [`before_script` and `after_scrip
possible to inherit from regular jobs as well.
`extends` supports multi-level inheritance. You should avoid using more than 3 levels,
but you can use as many as ten.
but you can use as many as eleven.
The following example has two levels of inheritance:
```yaml

View file

@ -687,7 +687,7 @@ Next, make sure that Gitaly is configured:
sudo chmod 0700 /home/git/gitlab/tmp/sockets/private
sudo chown git /home/git/gitlab/tmp/sockets/private
# If you are using non-default settings you need to update config.toml
# If you are using non-default settings, you need to update config.toml
cd /home/git/gitaly
sudo -u git -H editor config.toml
```
@ -741,7 +741,7 @@ Download the init script (is `/etc/init.d/gitlab`):
sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
```
And if you are installing with a non-default folder or user copy and edit the defaults file:
And if you are installing with a non-default folder or user, copy and edit the defaults file:
```shell
sudo cp lib/support/init.d/gitlab.default.example /etc/default/gitlab

View file

@ -61,8 +61,8 @@ From GitLab 13.1:
### Node.js versions
Beginning in GitLab 12.9, we only support node.js 10.13.0 or higher, and we have dropped
support for node.js 8. (node.js 6 support was dropped in GitLab 11.8)
Beginning in GitLab 12.9, we only support Node.js 10.13.0 or higher, and we have dropped
support for Node.js 8. (Node.js 6 support was dropped in GitLab 11.8)
We recommend Node 12.x, as it's faster.

View file

@ -190,7 +190,7 @@ pages:
- public
```
Then configure the pipeline to run the job for the master branch only.
Then configure the pipeline to run the job for the `master` branch only.
```yaml
image: ruby:2.7

View file

@ -38,7 +38,9 @@ namespace :gettext do
Rake::Task['gettext:find'].invoke
# leave only the required changes.
`git checkout -- locale/*/gitlab.po`
unless system(*%w(git checkout -- locale/*/gitlab.po))
raise 'failed to cleanup generated locale/*/gitlab.po files'
end
# Remove timestamps from the pot file
pot_content = File.read pot_file

View file

@ -13492,6 +13492,15 @@ msgstr ""
msgid "InviteEmail|to join the %{strong_start}%{project_or_group_name}%{strong_end}"
msgstr ""
msgid "InviteMembersBanner|Collaborate with your team"
msgstr ""
msgid "InviteMembersBanner|Invite your colleagues"
msgstr ""
msgid "InviteMembersBanner|We noticed that you haven't invited anyone to this group. Invite your colleagues so you can discuss issues, collaborate on merge requests, and share your knowledge."
msgstr ""
msgid "Invited"
msgstr ""

View file

@ -64,7 +64,7 @@ then
echo "Merge request pipeline (detached) detected. Testing all files."
else
MERGE_BASE=$(git merge-base ${CI_MERGE_REQUEST_TARGET_BRANCH_SHA} ${CI_MERGE_REQUEST_SOURCE_BRANCH_SHA})
MD_DOC_PATH=$(git diff --name-only "${MERGE_BASE}..${CI_MERGE_REQUEST_SOURCE_BRANCH_SHA}" '*.md')
MD_DOC_PATH=$(git diff --name-only "${MERGE_BASE}..${CI_MERGE_REQUEST_SOURCE_BRANCH_SHA}" 'doc/*.md')
echo -e "Merged results pipeline detected. Testing only the following files:\n${MD_DOC_PATH}"
fi

View file

@ -0,0 +1,76 @@
import { shallowMount } from '@vue/test-utils';
import { GlBanner } from '@gitlab/ui';
import InviteMembersBanner from '~/groups/components/invite_members_banner.vue';
const expectedTitle = 'Collaborate with your team';
const expectedBody =
"We noticed that you haven't invited anyone to this group. Invite your colleagues so you can discuss issues, collaborate on merge requests, and share your knowledge";
const expectedSvgPath = '/illustrations/background';
const expectedInviteMembersPath = 'groups/members';
const expectedButtonText = 'Invite your colleagues';
const createComponent = (stubs = {}) => {
return shallowMount(InviteMembersBanner, {
provide: {
svgPath: expectedSvgPath,
inviteMembersPath: expectedInviteMembersPath,
},
stubs,
});
};
describe('InviteMembersBanner', () => {
let wrapper;
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('rendering', () => {
const findBanner = () => {
return wrapper.find(GlBanner);
};
beforeEach(() => {
wrapper = createComponent();
});
it('uses the svgPath for the banner svgpath', () => {
expect(findBanner().attributes('svgpath')).toBe(expectedSvgPath);
});
it('uses the title from options for title', () => {
expect(findBanner().attributes('title')).toBe(expectedTitle);
});
it('includes the body text from options', () => {
expect(findBanner().html()).toContain(expectedBody);
});
it('uses the button_text text from options for buttontext', () => {
expect(findBanner().attributes('buttontext')).toBe(expectedButtonText);
});
it('uses the href from inviteMembersPath for buttonlink', () => {
expect(findBanner().attributes('buttonlink')).toBe(expectedInviteMembersPath);
});
});
describe('dismissing', () => {
const findButton = () => {
return wrapper.find('button');
};
const stubs = {
GlBanner,
};
it('sets visible to false', () => {
wrapper = createComponent(stubs);
findButton().trigger('click');
expect(wrapper.vm.visible).toBe(false);
});
});
});

View file

@ -20,7 +20,6 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] =
</label>
<gl-form-group-stub
class="gl-mb-0"
id="visibility-level-setting"
>
<gl-form-radio-group-stub
@ -91,12 +90,5 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] =
</gl-form-radio-stub>
</gl-form-radio-group-stub>
</gl-form-group-stub>
<div
class="text-muted"
data-testid="restricted-levels-info"
>
<!---->
</div>
</div>
`;

View file

@ -102,13 +102,6 @@ describe('Snippet Edit app', () => {
markdownDocsPath: 'http://docs.foo.bar',
...props,
},
data() {
return {
snippet: {
visibilityLevel: SNIPPET_VISIBILITY_PRIVATE,
},
};
},
});
}

View file

@ -1,55 +1,31 @@
import { GlFormRadio, GlIcon, GlFormRadioGroup, GlLink } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import SnippetVisibilityEdit from '~/snippets/components/snippet_visibility_edit.vue';
import { defaultSnippetVisibilityLevels } from '~/snippets/utils/blob';
import {
SNIPPET_VISIBILITY,
SNIPPET_VISIBILITY_PRIVATE,
SNIPPET_VISIBILITY_INTERNAL,
SNIPPET_VISIBILITY_PUBLIC,
SNIPPET_LEVELS_RESTRICTED,
SNIPPET_LEVELS_DISABLED,
} from '~/snippets/constants';
describe('Snippet Visibility Edit component', () => {
let wrapper;
const defaultHelpLink = '/foo/bar';
const defaultVisibilityLevel = 'private';
const defaultVisibility = defaultSnippetVisibilityLevels([0, 10, 20]);
function createComponent({
propsData = {},
visibilityLevels = defaultVisibility,
multipleLevelsRestricted = false,
deep = false,
} = {}) {
function createComponent(propsData = {}, deep = false) {
const method = deep ? mount : shallowMount;
const $apollo = {
queries: {
defaultVisibility: {
loading: false,
},
},
};
wrapper = method.call(this, SnippetVisibilityEdit, {
mock: { $apollo },
propsData: {
helpLink: defaultHelpLink,
isProjectSnippet: false,
value: defaultVisibilityLevel,
...propsData,
},
data() {
return {
visibilityLevels,
multipleLevelsRestricted,
};
},
});
}
const findLink = () => wrapper.find('label').find(GlLink);
const findLabel = () => wrapper.find('label');
const findRadios = () => wrapper.find(GlFormRadioGroup).findAll(GlFormRadio);
const findRadiosData = () =>
findRadios().wrappers.map(x => {
@ -71,84 +47,60 @@ describe('Snippet Visibility Edit component', () => {
expect(wrapper.element).toMatchSnapshot();
});
it('renders label help link', () => {
createComponent();
it('renders visibility options', () => {
createComponent({}, true);
expect(findLink().attributes('href')).toBe(defaultHelpLink);
});
it('when helpLink is not defined, does not render label help link', () => {
createComponent({ propsData: { helpLink: null } });
expect(findLink().exists()).toBe(false);
});
describe('Visibility options', () => {
const findRestrictedInfo = () => wrapper.find('[data-testid="restricted-levels-info"]');
const RESULTING_OPTIONS = {
0: {
expect(findRadiosData()).toEqual([
{
value: SNIPPET_VISIBILITY_PRIVATE,
icon: SNIPPET_VISIBILITY.private.icon,
text: SNIPPET_VISIBILITY.private.label,
description: SNIPPET_VISIBILITY.private.description,
},
10: {
{
value: SNIPPET_VISIBILITY_INTERNAL,
icon: SNIPPET_VISIBILITY.internal.icon,
text: SNIPPET_VISIBILITY.internal.label,
description: SNIPPET_VISIBILITY.internal.description,
},
20: {
{
value: SNIPPET_VISIBILITY_PUBLIC,
icon: SNIPPET_VISIBILITY.public.icon,
text: SNIPPET_VISIBILITY.public.label,
description: SNIPPET_VISIBILITY.public.description,
},
};
]);
});
it.each`
levels | resultOptions
${undefined} | ${[]}
${''} | ${[]}
${[]} | ${[]}
${[0]} | ${[RESULTING_OPTIONS[0]]}
${[0, 10]} | ${[RESULTING_OPTIONS[0], RESULTING_OPTIONS[10]]}
${[0, 10, 20]} | ${[RESULTING_OPTIONS[0], RESULTING_OPTIONS[10], RESULTING_OPTIONS[20]]}
${[0, 20]} | ${[RESULTING_OPTIONS[0], RESULTING_OPTIONS[20]]}
${[10, 20]} | ${[RESULTING_OPTIONS[10], RESULTING_OPTIONS[20]]}
`('renders correct visibility options for $levels', ({ levels, resultOptions }) => {
createComponent({ visibilityLevels: defaultSnippetVisibilityLevels(levels), deep: true });
expect(findRadiosData()).toEqual(resultOptions);
it('when project snippet, renders special private description', () => {
createComponent({ isProjectSnippet: true }, true);
expect(findRadiosData()[0]).toEqual({
value: SNIPPET_VISIBILITY_PRIVATE,
icon: SNIPPET_VISIBILITY.private.icon,
text: SNIPPET_VISIBILITY.private.label,
description: SNIPPET_VISIBILITY.private.description_project,
});
});
it.each`
levels | levelsRestricted | resultText
${[]} | ${false} | ${SNIPPET_LEVELS_DISABLED}
${[]} | ${true} | ${SNIPPET_LEVELS_DISABLED}
${[0]} | ${true} | ${SNIPPET_LEVELS_RESTRICTED}
${[0]} | ${false} | ${''}
${[0, 10, 20]} | ${false} | ${''}
`(
'renders correct information about restricted visibility levels for $levels',
({ levels, levelsRestricted, resultText }) => {
createComponent({
visibilityLevels: defaultSnippetVisibilityLevels(levels),
multipleLevelsRestricted: levelsRestricted,
});
expect(findRestrictedInfo().text()).toBe(resultText);
},
);
it('renders label help link', () => {
createComponent();
it('when project snippet, renders special private description', () => {
createComponent({ propsData: { isProjectSnippet: true }, deep: true });
expect(
findLabel()
.find(GlLink)
.attributes('href'),
).toBe(defaultHelpLink);
});
expect(findRadiosData()[0]).toEqual({
value: SNIPPET_VISIBILITY_PRIVATE,
icon: SNIPPET_VISIBILITY.private.icon,
text: SNIPPET_VISIBILITY.private.label,
description: SNIPPET_VISIBILITY.private.description_project,
});
});
it('when helpLink is not defined, does not render label help link', () => {
createComponent({ helpLink: null });
expect(
findLabel()
.find(GlLink)
.exists(),
).toBe(false);
});
});
@ -156,7 +108,7 @@ describe('Snippet Visibility Edit component', () => {
it('pre-selects correct option in the list', () => {
const value = SNIPPET_VISIBILITY_INTERNAL;
createComponent({ propsData: { value } });
createComponent({ value });
expect(wrapper.find(GlFormRadioGroup).attributes('checked')).toBe(value);
});

View file

@ -369,4 +369,48 @@ RSpec.describe GroupsHelper do
it { is_expected.to be_falsey }
end
end
describe '#show_invite_banner?' do
let_it_be(:current_user) { create(:user) }
let_it_be_with_refind(:group) { create(:group) }
let_it_be(:users) { [current_user, create(:user)] }
subject { helper.show_invite_banner?(group) }
before do
allow(helper).to receive(:current_user) { current_user }
allow(helper).to receive(:can?).with(current_user, :admin_group, group).and_return(can_admin_group)
stub_feature_flags(invite_your_teammates_banner_a: feature_enabled_flag)
users.take(group_members_count).each { |user| group.add_guest(user) }
end
using RSpec::Parameterized::TableSyntax
where(:feature_enabled_flag, :can_admin_group, :group_members_count, :expected_result) do
true | true | 1 | true
true | false | 1 | false
false | true | 1 | false
false | false | 1 | false
true | true | 2 | false
true | false | 2 | false
false | true | 2 | false
false | false | 2 | false
end
with_them do
context 'when the group was just created' do
before do
flash[:notice] = "Group #{group.name} was successfully created"
end
it { is_expected.to be_falsey }
end
context 'when no flash message' do
it 'returns the expected result' do
expect(subject).to eq(expected_result)
end
end
end
end
end