Commit f70a0ca3 authored by Paul Slaughter's avatar Paul Slaughter

Merge branch '207463-bind-components' into 'master'

Make Snippet edit fields v-model aware

See merge request gitlab-org/gitlab!28192
parents fc4b8356 b49b66bd
...@@ -7,21 +7,18 @@ export default { ...@@ -7,21 +7,18 @@ export default {
BlobHeaderEdit, BlobHeaderEdit,
BlobContentEdit, BlobContentEdit,
}, },
inheritAttrs: false,
props: { props: {
content: {
type: String,
required: true,
},
fileName: { fileName: {
type: String, type: String,
required: true, required: false,
default: '',
}, },
}, },
data() { methods: {
return { emitFileNameChange(newFileName) {
name: this.fileName, this.$emit('name-change', newFileName);
blobContent: this.content, },
};
}, },
}; };
</script> </script>
...@@ -29,8 +26,8 @@ export default { ...@@ -29,8 +26,8 @@ export default {
<div class="form-group file-editor"> <div class="form-group file-editor">
<label>{{ s__('Snippets|File') }}</label> <label>{{ s__('Snippets|File') }}</label>
<div class="file-holder snippet"> <div class="file-holder snippet">
<blob-header-edit v-model="name" /> <blob-header-edit :value="fileName" @input="emitFileNameChange" />
<blob-content-edit v-model="blobContent" :file-name="name" /> <blob-content-edit v-bind="$attrs" :file-name="fileName" v-on="$listeners" />
</div> </div>
</div> </div>
</template> </template>
...@@ -9,11 +9,6 @@ export default { ...@@ -9,11 +9,6 @@ export default {
MarkdownField, MarkdownField,
}, },
props: { props: {
description: {
type: String,
default: '',
required: false,
},
markdownPreviewPath: { markdownPreviewPath: {
type: String, type: String,
required: true, required: true,
...@@ -22,11 +17,11 @@ export default { ...@@ -22,11 +17,11 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
}, value: {
data() { type: String,
return { required: false,
text: this.description, default: '',
}; },
}, },
mounted() { mounted() {
setupCollapsibleInputs(); setupCollapsibleInputs();
...@@ -37,7 +32,7 @@ export default { ...@@ -37,7 +32,7 @@ export default {
<div class="form-group js-description-input"> <div class="form-group js-description-input">
<label>{{ s__('Snippets|Description (optional)') }}</label> <label>{{ s__('Snippets|Description (optional)') }}</label>
<div class="js-collapsible-input"> <div class="js-collapsible-input">
<div class="js-collapsed" :class="{ 'd-none': text }"> <div class="js-collapsed" :class="{ 'd-none': value }">
<gl-form-input <gl-form-input
class="form-control" class="form-control"
:placeholder=" :placeholder="
...@@ -50,20 +45,21 @@ export default { ...@@ -50,20 +45,21 @@ export default {
</div> </div>
<markdown-field <markdown-field
class="js-expanded" class="js-expanded"
:class="{ 'd-none': !text }" :class="{ 'd-none': !value }"
:markdown-preview-path="markdownPreviewPath" :markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath" :markdown-docs-path="markdownDocsPath"
> >
<textarea <textarea
id="snippet-description" id="snippet-description"
slot="textarea" slot="textarea"
v-model="text"
class="note-textarea js-gfm-input js-autosize markdown-area class="note-textarea js-gfm-input js-autosize markdown-area
qa-description-textarea" qa-description-textarea"
dir="auto" dir="auto"
data-supports-quick-actions="false" data-supports-quick-actions="false"
:value="value"
:aria-label="__('Description')" :aria-label="__('Description')"
:placeholder="__('Write a comment or drag your files here…')" :placeholder="__('Write a comment or drag your files here…')"
@input="$emit('input', $event.target.value)"
> >
</textarea> </textarea>
</markdown-field> </markdown-field>
......
<script> <script>
import { GlIcon, GlFormGroup, GlFormRadio, GlFormRadioGroup, GlLink } from '@gitlab/ui'; import { GlIcon, GlFormGroup, GlFormRadio, GlFormRadioGroup, GlLink } from '@gitlab/ui';
import { SNIPPET_VISIBILITY } from '~/snippets/constants'; import { SNIPPET_VISIBILITY, SNIPPET_VISIBILITY_PRIVATE } from '~/snippets/constants';
export default { export default {
components: { components: {
...@@ -21,48 +21,22 @@ export default { ...@@ -21,48 +21,22 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
visibilityLevel: { value: {
type: String, type: String,
default: '0',
required: false, required: false,
default: SNIPPET_VISIBILITY_PRIVATE,
}, },
}, },
data() {
return {
selected: this.visibilityLevel,
};
},
computed: { computed: {
visibilityOptions() { visibilityOptions() {
return [ const options = [];
{ Object.keys(SNIPPET_VISIBILITY).forEach(key => {
value: '0', options.push({
icon: 'lock', value: key,
text: SNIPPET_VISIBILITY.private.label, ...SNIPPET_VISIBILITY[key],
description: this.isProjectSnippet });
? SNIPPET_VISIBILITY.private.description_project });
: SNIPPET_VISIBILITY.private.description, return options;
},
{
value: '1',
icon: 'shield',
text: SNIPPET_VISIBILITY.internal.label,
description: SNIPPET_VISIBILITY.internal.description,
},
{
value: '2',
icon: 'earth',
text: SNIPPET_VISIBILITY.public.label,
description: SNIPPET_VISIBILITY.public.description,
},
];
},
},
methods: {
updateSelectedOption(newVal) {
if (newVal !== this.selected) {
this.selected = newVal;
}
}, },
}, },
}; };
...@@ -76,18 +50,22 @@ export default { ...@@ -76,18 +50,22 @@ export default {
/></gl-link> /></gl-link>
</label> </label>
<gl-form-group id="visibility-level-setting"> <gl-form-group id="visibility-level-setting">
<gl-form-radio-group :checked="selected" stacked @change="updateSelectedOption"> <gl-form-radio-group v-bind="$attrs" :checked="value" stacked v-on="$listeners">
<gl-form-radio <gl-form-radio
v-for="option in visibilityOptions" v-for="option in visibilityOptions"
:key="option.icon" :key="option.value"
:value="option.value" :value="option.value"
class="mb-3" class="mb-3"
> >
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<gl-icon :size="16" :name="option.icon" /> <gl-icon :size="16" :name="option.icon" />
<span class="font-weight-bold ml-1">{{ option.text }}</span> <span class="font-weight-bold ml-1 js-visibility-option">{{ option.label }}</span>
</div> </div>
<template #help>{{ option.description }}</template> <template #help>{{
isProjectSnippet && option.description_project
? option.description_project
: option.description
}}</template>
</gl-form-radio> </gl-form-radio>
</gl-form-radio-group> </gl-form-radio-group>
</gl-form-group> </gl-form-group>
......
...@@ -5,17 +5,20 @@ export const SNIPPET_VISIBILITY_INTERNAL = 'internal'; ...@@ -5,17 +5,20 @@ export const SNIPPET_VISIBILITY_INTERNAL = 'internal';
export const SNIPPET_VISIBILITY_PUBLIC = 'public'; export const SNIPPET_VISIBILITY_PUBLIC = 'public';
export const SNIPPET_VISIBILITY = { export const SNIPPET_VISIBILITY = {
private: { [SNIPPET_VISIBILITY_PRIVATE]: {
label: __('Private'), label: __('Private'),
icon: 'lock',
description: __('The snippet is visible only to me.'), description: __('The snippet is visible only to me.'),
description_project: __('The snippet is visible only to project members.'), description_project: __('The snippet is visible only to project members.'),
}, },
internal: { [SNIPPET_VISIBILITY_INTERNAL]: {
label: __('Internal'), label: __('Internal'),
icon: 'shield',
description: __('The snippet is visible to any logged in user.'), description: __('The snippet is visible to any logged in user.'),
}, },
public: { [SNIPPET_VISIBILITY_PUBLIC]: {
label: __('Public'), label: __('Public'),
icon: 'earth',
description: __('The snippet can be accessed without any authentication.'), description: __('The snippet can be accessed without any authentication.'),
}, },
}; };
...@@ -23,7 +23,7 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] = ...@@ -23,7 +23,7 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] =
id="visibility-level-setting" id="visibility-level-setting"
> >
<gl-form-radio-group-stub <gl-form-radio-group-stub
checked="0" checked="private"
disabledfield="disabled" disabledfield="disabled"
htmlfield="html" htmlfield="html"
options="" options=""
...@@ -33,7 +33,7 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] = ...@@ -33,7 +33,7 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] =
> >
<gl-form-radio-stub <gl-form-radio-stub
class="mb-3" class="mb-3"
value="0" value="private"
> >
<div <div
class="d-flex align-items-center" class="d-flex align-items-center"
...@@ -44,7 +44,7 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] = ...@@ -44,7 +44,7 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] =
/> />
<span <span
class="font-weight-bold ml-1" class="font-weight-bold ml-1 js-visibility-option"
> >
Private Private
</span> </span>
...@@ -52,7 +52,7 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] = ...@@ -52,7 +52,7 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] =
</gl-form-radio-stub> </gl-form-radio-stub>
<gl-form-radio-stub <gl-form-radio-stub
class="mb-3" class="mb-3"
value="1" value="internal"
> >
<div <div
class="d-flex align-items-center" class="d-flex align-items-center"
...@@ -63,7 +63,7 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] = ...@@ -63,7 +63,7 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] =
/> />
<span <span
class="font-weight-bold ml-1" class="font-weight-bold ml-1 js-visibility-option"
> >
Internal Internal
</span> </span>
...@@ -71,7 +71,7 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] = ...@@ -71,7 +71,7 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] =
</gl-form-radio-stub> </gl-form-radio-stub>
<gl-form-radio-stub <gl-form-radio-stub
class="mb-3" class="mb-3"
value="2" value="public"
> >
<div <div
class="d-flex align-items-center" class="d-flex align-items-center"
...@@ -82,7 +82,7 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] = ...@@ -82,7 +82,7 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] =
/> />
<span <span
class="font-weight-bold ml-1" class="font-weight-bold ml-1 js-visibility-option"
> >
Public Public
</span> </span>
......
...@@ -2,18 +2,21 @@ import SnippetBlobEdit from '~/snippets/components/snippet_blob_edit.vue'; ...@@ -2,18 +2,21 @@ import SnippetBlobEdit from '~/snippets/components/snippet_blob_edit.vue';
import BlobHeaderEdit from '~/blob/components/blob_edit_header.vue'; import BlobHeaderEdit from '~/blob/components/blob_edit_header.vue';
import BlobContentEdit from '~/blob/components/blob_edit_content.vue'; import BlobContentEdit from '~/blob/components/blob_edit_content.vue';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
jest.mock('~/blob/utils', () => jest.fn()); jest.mock('~/blob/utils', () => jest.fn());
describe('Snippet Blob Edit component', () => { describe('Snippet Blob Edit component', () => {
let wrapper; let wrapper;
const content = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.'; const value = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.';
const fileName = 'lorem.txt'; const fileName = 'lorem.txt';
const findHeader = () => wrapper.find(BlobHeaderEdit);
const findContent = () => wrapper.find(BlobContentEdit);
function createComponent() { function createComponent() {
wrapper = shallowMount(SnippetBlobEdit, { wrapper = shallowMount(SnippetBlobEdit, {
propsData: { propsData: {
content, value,
fileName, fileName,
}, },
}); });
...@@ -33,8 +36,20 @@ describe('Snippet Blob Edit component', () => { ...@@ -33,8 +36,20 @@ describe('Snippet Blob Edit component', () => {
}); });
it('renders required components', () => { it('renders required components', () => {
expect(wrapper.contains(BlobHeaderEdit)).toBe(true); expect(findHeader().exists()).toBe(true);
expect(wrapper.contains(BlobContentEdit)).toBe(true); expect(findContent().exists()).toBe(true);
});
});
describe('functionality', () => {
it('emits "name-change" event when the file name gets changed', () => {
expect(wrapper.emitted('name-change')).toBeUndefined();
const newFilename = 'foo.bar';
findHeader().vm.$emit('input', newFilename);
return nextTick().then(() => {
expect(wrapper.emitted('name-change')[0]).toEqual([newFilename]);
});
}); });
}); });
}); });
...@@ -6,11 +6,12 @@ describe('Snippet Description Edit component', () => { ...@@ -6,11 +6,12 @@ describe('Snippet Description Edit component', () => {
const defaultDescription = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.'; const defaultDescription = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.';
const markdownPreviewPath = 'foo/'; const markdownPreviewPath = 'foo/';
const markdownDocsPath = 'help/'; const markdownDocsPath = 'help/';
const findTextarea = () => wrapper.find('textarea');
function createComponent(description = defaultDescription) { function createComponent(value = defaultDescription) {
wrapper = shallowMount(SnippetDescriptionEdit, { wrapper = shallowMount(SnippetDescriptionEdit, {
propsData: { propsData: {
description, value,
markdownPreviewPath, markdownPreviewPath,
markdownDocsPath, markdownDocsPath,
}, },
...@@ -49,4 +50,14 @@ describe('Snippet Description Edit component', () => { ...@@ -49,4 +50,14 @@ describe('Snippet Description Edit component', () => {
expect(isHidden('.js-expanded')).toBe(true); expect(isHidden('.js-expanded')).toBe(true);
}); });
}); });
describe('functionality', () => {
it('emits "input" event when description is changed', () => {
expect(wrapper.emitted('input')).toBeUndefined();
const newDescription = 'dummy';
findTextarea().setValue(newDescription);
expect(wrapper.emitted('input')[0]).toEqual([newDescription]);
});
});
}); });
import SnippetVisibilityEdit from '~/snippets/components/snippet_visibility_edit.vue'; import SnippetVisibilityEdit from '~/snippets/components/snippet_visibility_edit.vue';
import { GlFormRadio } from '@gitlab/ui'; import { GlFormRadio, GlIcon, GlFormRadioGroup, GlLink } from '@gitlab/ui';
import { SNIPPET_VISIBILITY } from '~/snippets/constants'; import {
SNIPPET_VISIBILITY,
SNIPPET_VISIBILITY_PRIVATE,
SNIPPET_VISIBILITY_INTERNAL,
SNIPPET_VISIBILITY_PUBLIC,
} from '~/snippets/constants';
import { mount, shallowMount } from '@vue/test-utils'; import { mount, shallowMount } from '@vue/test-utils';
describe('Snippet Visibility Edit component', () => { describe('Snippet Visibility Edit component', () => {
let wrapper; let wrapper;
let radios;
const defaultHelpLink = '/foo/bar'; const defaultHelpLink = '/foo/bar';
const defaultVisibilityLevel = '0'; const defaultVisibilityLevel = 'private';
function findElements(sel) { function createComponent(propsData = {}, deep = false) {
return wrapper.findAll(sel);
}
function createComponent(
{
helpLink = defaultHelpLink,
isProjectSnippet = false,
visibilityLevel = defaultVisibilityLevel,
} = {},
deep = false,
) {
const method = deep ? mount : shallowMount; const method = deep ? mount : shallowMount;
wrapper = method.call(this, SnippetVisibilityEdit, { wrapper = method.call(this, SnippetVisibilityEdit, {
propsData: { propsData: {
helpLink, helpLink: defaultHelpLink,
isProjectSnippet, isProjectSnippet: false,
visibilityLevel, value: defaultVisibilityLevel,
...propsData,
}, },
}); });
radios = findElements(GlFormRadio);
} }
const findLabel = () => wrapper.find('label');
const findRadios = () => wrapper.find(GlFormRadioGroup).findAll(GlFormRadio);
const findRadiosData = () =>
findRadios().wrappers.map(x => {
return {
value: x.find('input').attributes('value'),
icon: x.find(GlIcon).props('name'),
description: x.find('.help-text').text(),
text: x.find('.js-visibility-option').text(),
};
});
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
}); });
...@@ -42,53 +47,66 @@ describe('Snippet Visibility Edit component', () => { ...@@ -42,53 +47,66 @@ describe('Snippet Visibility Edit component', () => {
expect(wrapper.element).toMatchSnapshot(); expect(wrapper.element).toMatchSnapshot();
}); });
it.each` it('renders visibility options', () => {
label | value createComponent({}, true);
${SNIPPET_VISIBILITY.private.label} | ${`0`}
${SNIPPET_VISIBILITY.internal.label} | ${`1`}
${SNIPPET_VISIBILITY.public.label} | ${`2`}
`('should render correct $label label', ({ label, value }) => {
createComponent();
const radio = radios.at(parseInt(value, 10));
expect(radio.attributes('value')).toBe(value); expect(findRadiosData()).toEqual([
expect(radio.text()).toContain(label); {
value: SNIPPET_VISIBILITY_PRIVATE,
icon: SNIPPET_VISIBILITY.private.icon,
text: SNIPPET_VISIBILITY.private.label,
description: SNIPPET_VISIBILITY.private.description,
},
{
value: SNIPPET_VISIBILITY_INTERNAL,
icon: SNIPPET_VISIBILITY.internal.icon,
text: SNIPPET_VISIBILITY.internal.label,
description: SNIPPET_VISIBILITY.internal.description,
},
{
value: SNIPPET_VISIBILITY_PUBLIC,
icon: SNIPPET_VISIBILITY.public.icon,
text: SNIPPET_VISIBILITY.public.label,
description: SNIPPET_VISIBILITY.public.description,
},
]);
}); });
describe('rendered help-text', () => { it('when project snippet, renders special private description', () => {
it.each` createComponent({ isProjectSnippet: true }, true);
description | value | label
${SNIPPET_VISIBILITY.private.description} | ${`0`} | ${SNIPPET_VISIBILITY.private.label}
${SNIPPET_VISIBILITY.internal.description} | ${`1`} | ${SNIPPET_VISIBILITY.internal.label}
${SNIPPET_VISIBILITY.public.description} | ${`2`} | ${SNIPPET_VISIBILITY.public.label}
`('should render correct $label description', ({ description, value }) => {
createComponent({}, true);
const help = findElements('.help-text').at(parseInt(value, 10));
expect(help.text()).toBe(description); 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('renders label help link', () => {
createComponent();
it('renders correct Private description for a project snippet', () => { expect(
createComponent({ isProjectSnippet: true }, true); findLabel()
.find(GlLink)
.attributes('href'),
).toBe(defaultHelpLink);
});
const helpText = findElements('.help-text') it('when helpLink is not defined, does not render label help link', () => {
.at(0) createComponent({ helpLink: null });
.text();
expect(helpText).not.toContain(SNIPPET_VISIBILITY.private.description); expect(findLabel().contains(GlLink)).toBe(false);
expect(helpText).toBe(SNIPPET_VISIBILITY.private.description_project);
});
}); });
}); });
describe('functionality', () => { describe('functionality', () => {
it('pre-selects correct option in the list', () => { it('pre-selects correct option in the list', () => {
const pos = 1; const value = SNIPPET_VISIBILITY_INTERNAL;
createComponent({ value });
createComponent({ visibilityLevel: `${pos}` }, true); expect(wrapper.find(GlFormRadioGroup).attributes('checked')).toBe(value);
const radio = radios.at(pos);
expect(radio.find('input[type="radio"]').element.checked).toBe(true);
}); });
}); });
}); });
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