提交 583bde3f 编写于 作者: G GitLab Bot

Add latest changes from gitlab-org/gitlab@master

上级 dc965b8c
<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>
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),
});
}
......@@ -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();
}
......@@ -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>
......
<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>
......@@ -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.',
);
......@@ -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,
},
});
},
......
query defaultSnippetVisibility {
visibilityLevels @client
selectedLevel @client
multipleLevelsRestricted @client
}
......@@ -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 [];
};
......@@ -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
......@@ -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]
......
......@@ -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")
......
......@@ -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"
......
- 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,
......
---
title: Update the 2FA user update check to account for rounding errors
merge_request: 41327
author:
type: fixed
---
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
\ No newline at end of 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.*\)'
......@@ -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
......
......@@ -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)
......
......@@ -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)
......
......@@ -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
......
......@@ -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
......
......@@ -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.
......
......@@ -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
......
......@@ -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
......
......@@ -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 ""
......
......@@ -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
......
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);
});
});
});
......@@ -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>
`;
......@@ -102,13 +102,6 @@ describe('Snippet Edit app', () => {
markdownDocsPath: 'http://docs.foo.bar',
...props,
},
data() {
return {
snippet: {
visibilityLevel: SNIPPET_VISIBILITY_PRIVATE,
},
};
},
});
}
......
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,
} = {}) {
const method = deep ? mount : shallowMount;
const $apollo = {
queries: {
defaultVisibility: {
loading: false,
},
},
};
function createComponent(propsData = {}, deep = false) {
const method = deep ? mount : shallowMount;
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();
expect(findLink().attributes('href')).toBe(defaultHelpLink);
});
it('when helpLink is not defined, does not render label help link', () => {
createComponent({ propsData: { helpLink: null } });
it('renders visibility options', () => {
createComponent({}, true);
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('when project snippet, renders special private description', () => {
createComponent({ isProjectSnippet: true }, true);
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);
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);
});
......
......@@ -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
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册