Commit 910d1aa4 authored by Denys Mishunov's avatar Denys Mishunov Committed by Sarah Groff Hennigh-Palermo

"Restricted visibility levels" setting

Make sure the visibility options on the snippet edit form
follow the restrictions set in "Restricted visibility levels"
setting
parent caa89cee
...@@ -6,19 +6,21 @@ import { __, sprintf } from '~/locale'; ...@@ -6,19 +6,21 @@ import { __, sprintf } from '~/locale';
import TitleField from '~/vue_shared/components/form/title.vue'; import TitleField from '~/vue_shared/components/form/title.vue';
import { redirectTo, joinPaths } from '~/lib/utils/url_utility'; import { redirectTo, joinPaths } from '~/lib/utils/url_utility';
import FormFooterActions from '~/vue_shared/components/form/form_footer_actions.vue'; 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 UpdateSnippetMutation from '../mutations/updateSnippet.mutation.graphql';
import CreateSnippetMutation from '../mutations/createSnippet.mutation.graphql'; import CreateSnippetMutation from '../mutations/createSnippet.mutation.graphql';
import { getSnippetMixin } from '../mixins/snippets'; import { getSnippetMixin } from '../mixins/snippets';
import { import {
SNIPPET_VISIBILITY_PRIVATE,
SNIPPET_CREATE_MUTATION_ERROR, SNIPPET_CREATE_MUTATION_ERROR,
SNIPPET_UPDATE_MUTATION_ERROR, SNIPPET_UPDATE_MUTATION_ERROR,
SNIPPET_VISIBILITY_PRIVATE,
} from '../constants'; } from '../constants';
import defaultVisibilityQuery from '../queries/snippet_visibility.query.graphql';
import SnippetBlobActionsEdit from './snippet_blob_actions_edit.vue'; import SnippetBlobActionsEdit from './snippet_blob_actions_edit.vue';
import SnippetVisibilityEdit from './snippet_visibility_edit.vue'; import SnippetVisibilityEdit from './snippet_visibility_edit.vue';
import SnippetDescriptionEdit from './snippet_description_edit.vue'; import SnippetDescriptionEdit from './snippet_description_edit.vue';
import { SNIPPET_MARK_EDIT_APP_START } from '~/performance_constants';
export default { export default {
components: { components: {
...@@ -31,6 +33,15 @@ export default { ...@@ -31,6 +33,15 @@ export default {
GlLoadingIcon, GlLoadingIcon,
}, },
mixins: [getSnippetMixin], mixins: [getSnippetMixin],
apollo: {
defaultVisibility: {
query: defaultVisibilityQuery,
manual: true,
result({ data: { selectedLevel } }) {
this.selectedLevelDefault = selectedLevel;
},
},
},
props: { props: {
markdownPreviewPath: { markdownPreviewPath: {
type: String, type: String,
...@@ -56,6 +67,7 @@ export default { ...@@ -56,6 +67,7 @@ export default {
isUpdating: false, isUpdating: false,
newSnippet: false, newSnippet: false,
actions: [], actions: [],
selectedLevelDefault: SNIPPET_VISIBILITY_PRIVATE,
}; };
}, },
computed: { computed: {
...@@ -98,6 +110,13 @@ export default { ...@@ -98,6 +110,13 @@ export default {
descriptionFieldId() { descriptionFieldId() {
return `${this.isProjectSnippet ? 'project' : 'personal'}_snippet_description`; return `${this.isProjectSnippet ? 'project' : 'personal'}_snippet_description`;
}, },
newSnippetSchema() {
return {
title: '',
description: '',
visibilityLevel: this.selectedLevelDefault,
};
},
}, },
beforeCreate() { beforeCreate() {
performance.mark(SNIPPET_MARK_EDIT_APP_START); performance.mark(SNIPPET_MARK_EDIT_APP_START);
...@@ -126,7 +145,7 @@ export default { ...@@ -126,7 +145,7 @@ export default {
}, },
onNewSnippetFetched() { onNewSnippetFetched() {
this.newSnippet = true; this.newSnippet = true;
this.snippet = this.$options.newSnippetSchema; this.snippet = this.newSnippetSchema;
}, },
onExistingSnippetFetched() { onExistingSnippetFetched() {
this.newSnippet = false; this.newSnippet = false;
...@@ -184,11 +203,6 @@ export default { ...@@ -184,11 +203,6 @@ export default {
this.actions = actions; this.actions = actions;
}, },
}, },
newSnippetSchema: {
title: '',
description: '',
visibilityLevel: SNIPPET_VISIBILITY_PRIVATE,
},
}; };
</script> </script>
<template> <template>
......
<script> <script>
import { GlIcon, GlFormGroup, GlFormRadio, GlFormRadioGroup, GlLink } from '@gitlab/ui'; import { GlIcon, GlFormGroup, GlFormRadio, GlFormRadioGroup, GlLink } from '@gitlab/ui';
import { import defaultVisibilityQuery from '../queries/snippet_visibility.query.graphql';
SNIPPET_VISIBILITY, import { defaultSnippetVisibilityLevels } from '../utils/blob';
SNIPPET_VISIBILITY_PRIVATE, import { SNIPPET_LEVELS_RESTRICTED, SNIPPET_LEVELS_DISABLED } from '~/snippets/constants';
SNIPPET_VISIBILITY_INTERNAL,
SNIPPET_VISIBILITY_PUBLIC,
} from '~/snippets/constants';
export default { export default {
components: { components: {
...@@ -15,6 +12,16 @@ export default { ...@@ -15,6 +12,16 @@ export default {
GlFormRadioGroup, GlFormRadioGroup,
GlLink, GlLink,
}, },
apollo: {
defaultVisibility: {
query: defaultVisibilityQuery,
manual: true,
result({ data: { visibilityLevels, multipleLevelsRestricted } }) {
this.visibilityLevels = defaultSnippetVisibilityLevels(visibilityLevels);
this.multipleLevelsRestricted = multipleLevelsRestricted;
},
},
},
props: { props: {
helpLink: { helpLink: {
type: String, type: String,
...@@ -28,19 +35,17 @@ export default { ...@@ -28,19 +35,17 @@ export default {
}, },
value: { value: {
type: String, type: String,
required: false, required: true,
default: SNIPPET_VISIBILITY_PRIVATE,
}, },
}, },
computed: { data() {
visibilityOptions() { return {
return [ visibilityLevels: [],
SNIPPET_VISIBILITY_PRIVATE, multipleLevelsRestricted: false,
SNIPPET_VISIBILITY_INTERNAL, };
SNIPPET_VISIBILITY_PUBLIC,
].map(key => ({ value: key, ...SNIPPET_VISIBILITY[key] }));
},
}, },
SNIPPET_LEVELS_DISABLED,
SNIPPET_LEVELS_RESTRICTED,
}; };
</script> </script>
<template> <template>
...@@ -51,10 +56,10 @@ export default { ...@@ -51,10 +56,10 @@ export default {
><gl-icon :size="12" name="question" ><gl-icon :size="12" name="question"
/></gl-link> /></gl-link>
</label> </label>
<gl-form-group id="visibility-level-setting"> <gl-form-group id="visibility-level-setting" class="gl-mb-0">
<gl-form-radio-group v-bind="$attrs" :checked="value" stacked v-on="$listeners"> <gl-form-radio-group :checked="value" stacked v-bind="$attrs" v-on="$listeners">
<gl-form-radio <gl-form-radio
v-for="option in visibilityOptions" v-for="option in visibilityLevels"
:key="option.value" :key="option.value"
:value="option.value" :value="option.value"
class="mb-3" class="mb-3"
...@@ -71,5 +76,12 @@ export default { ...@@ -71,5 +76,12 @@ export default {
</gl-form-radio> </gl-form-radio>
</gl-form-radio-group> </gl-form-radio-group>
</gl-form-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> </div>
</template> </template>
...@@ -33,3 +33,15 @@ export const SNIPPET_BLOB_ACTION_MOVE = 'move'; ...@@ -33,3 +33,15 @@ export const SNIPPET_BLOB_ACTION_MOVE = 'move';
export const SNIPPET_BLOB_ACTION_DELETE = 'delete'; export const SNIPPET_BLOB_ACTION_DELETE = 'delete';
export const SNIPPET_MAX_BLOBS = 10; 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,6 +5,7 @@ import createDefaultClient from '~/lib/graphql'; ...@@ -5,6 +5,7 @@ import createDefaultClient from '~/lib/graphql';
import SnippetsShow from './components/show.vue'; import SnippetsShow from './components/show.vue';
import SnippetsEdit from './components/edit.vue'; import SnippetsEdit from './components/edit.vue';
import { SNIPPET_LEVELS_MAP, SNIPPET_VISIBILITY_PRIVATE } from '~/snippets/constants';
Vue.use(VueApollo); Vue.use(VueApollo);
Vue.use(Translate); Vue.use(Translate);
...@@ -18,13 +19,23 @@ function appFactory(el, Component) { ...@@ -18,13 +19,23 @@ function appFactory(el, Component) {
defaultClient: createDefaultClient(), 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({ return new Vue({
el, el,
apolloProvider, apolloProvider,
render(createElement) { render(createElement) {
return createElement(Component, { return createElement(Component, {
props: { props: {
...el.dataset, ...restDataset,
}, },
}); });
}, },
......
query defaultSnippetVisibility {
visibilityLevels @client
selectedLevel @client
multipleLevelsRestricted @client
}
...@@ -4,6 +4,8 @@ import { ...@@ -4,6 +4,8 @@ import {
SNIPPET_BLOB_ACTION_UPDATE, SNIPPET_BLOB_ACTION_UPDATE,
SNIPPET_BLOB_ACTION_MOVE, SNIPPET_BLOB_ACTION_MOVE,
SNIPPET_BLOB_ACTION_DELETE, SNIPPET_BLOB_ACTION_DELETE,
SNIPPET_LEVELS_MAP,
SNIPPET_VISIBILITY,
} from '../constants'; } from '../constants';
const createLocalId = () => uniqueId('blob_local_'); const createLocalId = () => uniqueId('blob_local_');
...@@ -64,3 +66,16 @@ export const diffAll = (blobs, origBlobs) => { ...@@ -64,3 +66,16 @@ export const diffAll = (blobs, origBlobs) => {
return [...deletedEntries, ...newEntries]; 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 [];
};
- if Feature.enabled?(:snippets_edit_vue) - if Feature.enabled?(:snippets_edit_vue)
#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") } } - 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? } }
- else - else
.snippet-form-holder .snippet-form-holder
= form_for @snippet, url: url, = form_for @snippet, url: url,
......
...@@ -20,6 +20,7 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] = ...@@ -20,6 +20,7 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] =
</label> </label>
<gl-form-group-stub <gl-form-group-stub
class="gl-mb-0"
id="visibility-level-setting" id="visibility-level-setting"
> >
<gl-form-radio-group-stub <gl-form-radio-group-stub
...@@ -90,5 +91,12 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] = ...@@ -90,5 +91,12 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] =
</gl-form-radio-stub> </gl-form-radio-stub>
</gl-form-radio-group-stub> </gl-form-radio-group-stub>
</gl-form-group-stub> </gl-form-group-stub>
<div
class="text-muted"
data-testid="restricted-levels-info"
>
<!---->
</div>
</div> </div>
`; `;
...@@ -102,6 +102,13 @@ describe('Snippet Edit app', () => { ...@@ -102,6 +102,13 @@ describe('Snippet Edit app', () => {
markdownDocsPath: 'http://docs.foo.bar', markdownDocsPath: 'http://docs.foo.bar',
...props, ...props,
}, },
data() {
return {
snippet: {
visibilityLevel: SNIPPET_VISIBILITY_PRIVATE,
},
};
},
}); });
} }
......
import { GlFormRadio, GlIcon, GlFormRadioGroup, GlLink } from '@gitlab/ui'; import { GlFormRadio, GlIcon, GlFormRadioGroup, GlLink } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils'; import { mount, shallowMount } from '@vue/test-utils';
import SnippetVisibilityEdit from '~/snippets/components/snippet_visibility_edit.vue'; import SnippetVisibilityEdit from '~/snippets/components/snippet_visibility_edit.vue';
import { defaultSnippetVisibilityLevels } from '~/snippets/utils/blob';
import { import {
SNIPPET_VISIBILITY, SNIPPET_VISIBILITY,
SNIPPET_VISIBILITY_PRIVATE, SNIPPET_VISIBILITY_PRIVATE,
SNIPPET_VISIBILITY_INTERNAL, SNIPPET_VISIBILITY_INTERNAL,
SNIPPET_VISIBILITY_PUBLIC, SNIPPET_VISIBILITY_PUBLIC,
SNIPPET_LEVELS_RESTRICTED,
SNIPPET_LEVELS_DISABLED,
} from '~/snippets/constants'; } from '~/snippets/constants';
describe('Snippet Visibility Edit component', () => { describe('Snippet Visibility Edit component', () => {
let wrapper; let wrapper;
const defaultHelpLink = '/foo/bar'; const defaultHelpLink = '/foo/bar';
const defaultVisibilityLevel = 'private'; const defaultVisibilityLevel = 'private';
const defaultVisibility = defaultSnippetVisibilityLevels([0, 10, 20]);
function createComponent(propsData = {}, deep = false) {
function createComponent({
propsData = {},
visibilityLevels = defaultVisibility,
multipleLevelsRestricted = false,
deep = false,
} = {}) {
const method = deep ? mount : shallowMount; const method = deep ? mount : shallowMount;
const $apollo = {
queries: {
defaultVisibility: {
loading: false,
},
},
};
wrapper = method.call(this, SnippetVisibilityEdit, { wrapper = method.call(this, SnippetVisibilityEdit, {
mock: { $apollo },
propsData: { propsData: {
helpLink: defaultHelpLink, helpLink: defaultHelpLink,
isProjectSnippet: false, isProjectSnippet: false,
value: defaultVisibilityLevel, value: defaultVisibilityLevel,
...propsData, ...propsData,
}, },
data() {
return {
visibilityLevels,
multipleLevelsRestricted,
};
},
}); });
} }
const findLabel = () => wrapper.find('label'); const findLink = () => wrapper.find('label').find(GlLink);
const findRadios = () => wrapper.find(GlFormRadioGroup).findAll(GlFormRadio); const findRadios = () => wrapper.find(GlFormRadioGroup).findAll(GlFormRadio);
const findRadiosData = () => const findRadiosData = () =>
findRadios().wrappers.map(x => { findRadios().wrappers.map(x => {
...@@ -47,60 +71,84 @@ describe('Snippet Visibility Edit component', () => { ...@@ -47,60 +71,84 @@ describe('Snippet Visibility Edit component', () => {
expect(wrapper.element).toMatchSnapshot(); expect(wrapper.element).toMatchSnapshot();
}); });
it('renders visibility options', () => { it('renders label help link', () => {
createComponent({}, true); createComponent();
expect(findLink().attributes('href')).toBe(defaultHelpLink);
});
it('when helpLink is not defined, does not render label help link', () => {
createComponent({ propsData: { helpLink: null } });
expect(findRadiosData()).toEqual([ expect(findLink().exists()).toBe(false);
{ });
describe('Visibility options', () => {
const findRestrictedInfo = () => wrapper.find('[data-testid="restricted-levels-info"]');
const RESULTING_OPTIONS = {
0: {
value: SNIPPET_VISIBILITY_PRIVATE, value: SNIPPET_VISIBILITY_PRIVATE,
icon: SNIPPET_VISIBILITY.private.icon, icon: SNIPPET_VISIBILITY.private.icon,
text: SNIPPET_VISIBILITY.private.label, text: SNIPPET_VISIBILITY.private.label,
description: SNIPPET_VISIBILITY.private.description, description: SNIPPET_VISIBILITY.private.description,
}, },
{ 10: {
value: SNIPPET_VISIBILITY_INTERNAL, value: SNIPPET_VISIBILITY_INTERNAL,
icon: SNIPPET_VISIBILITY.internal.icon, icon: SNIPPET_VISIBILITY.internal.icon,
text: SNIPPET_VISIBILITY.internal.label, text: SNIPPET_VISIBILITY.internal.label,
description: SNIPPET_VISIBILITY.internal.description, description: SNIPPET_VISIBILITY.internal.description,
}, },
{ 20: {
value: SNIPPET_VISIBILITY_PUBLIC, value: SNIPPET_VISIBILITY_PUBLIC,
icon: SNIPPET_VISIBILITY.public.icon, icon: SNIPPET_VISIBILITY.public.icon,
text: SNIPPET_VISIBILITY.public.label, text: SNIPPET_VISIBILITY.public.label,
description: SNIPPET_VISIBILITY.public.description, description: SNIPPET_VISIBILITY.public.description,
}, },
]); };
});
it('when project snippet, renders special private description', () => {
createComponent({ isProjectSnippet: true }, true);
expect(findRadiosData()[0]).toEqual({ it.each`
value: SNIPPET_VISIBILITY_PRIVATE, levels | resultOptions
icon: SNIPPET_VISIBILITY.private.icon, ${undefined} | ${[]}
text: SNIPPET_VISIBILITY.private.label, ${''} | ${[]}
description: SNIPPET_VISIBILITY.private.description_project, ${[]} | ${[]}
${[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('renders label help link', () => { it.each`
createComponent(); levels | levelsRestricted | resultText
${[]} | ${false} | ${SNIPPET_LEVELS_DISABLED}
expect( ${[]} | ${true} | ${SNIPPET_LEVELS_DISABLED}
findLabel() ${[0]} | ${true} | ${SNIPPET_LEVELS_RESTRICTED}
.find(GlLink) ${[0]} | ${false} | ${''}
.attributes('href'), ${[0, 10, 20]} | ${false} | ${''}
).toBe(defaultHelpLink); `(
}); 'renders correct information about restricted visibility levels for $levels',
({ levels, levelsRestricted, resultText }) => {
createComponent({
visibilityLevels: defaultSnippetVisibilityLevels(levels),
multipleLevelsRestricted: levelsRestricted,
});
expect(findRestrictedInfo().text()).toBe(resultText);
},
);
it('when helpLink is not defined, does not render label help link', () => { it('when project snippet, renders special private description', () => {
createComponent({ helpLink: null }); createComponent({ propsData: { isProjectSnippet: true }, deep: true });
expect( expect(findRadiosData()[0]).toEqual({
findLabel() value: SNIPPET_VISIBILITY_PRIVATE,
.find(GlLink) icon: SNIPPET_VISIBILITY.private.icon,
.exists(), text: SNIPPET_VISIBILITY.private.label,
).toBe(false); description: SNIPPET_VISIBILITY.private.description_project,
});
});
}); });
}); });
...@@ -108,7 +156,7 @@ describe('Snippet Visibility Edit component', () => { ...@@ -108,7 +156,7 @@ describe('Snippet Visibility Edit component', () => {
it('pre-selects correct option in the list', () => { it('pre-selects correct option in the list', () => {
const value = SNIPPET_VISIBILITY_INTERNAL; const value = SNIPPET_VISIBILITY_INTERNAL;
createComponent({ value }); createComponent({ propsData: { value } });
expect(wrapper.find(GlFormRadioGroup).attributes('checked')).toBe(value); expect(wrapper.find(GlFormRadioGroup).attributes('checked')).toBe(value);
}); });
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment