Commit 40b58a5a authored by Enrique Alcántara's avatar Enrique Alcántara Committed by Natalia Tepluhina

Apply code review feedback

Convert saveable into a computed property
Add test coverage for rollbacking changes in edit area
Clean up apollo object
parent d18b4036
<script>
import { GlFormTextarea } from '@gitlab/ui';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import RichContentEditor from '~/vue_shared/components/rich_content_editor/rich_content_editor.vue';
import PublishToolbar from '../components/publish_toolbar.vue';
import EditHeader from '../components/edit_header.vue';
export default {
components: {
GlFormTextarea,
RichContentEditor,
PublishToolbar,
EditHeader,
},
mixins: [glFeatureFlagsMixin()],
props: {
value: {
title: {
type: String,
required: true,
},
content: {
type: String,
required: true,
},
savingChanges: {
type: Boolean,
required: true,
},
returnUrl: {
type: String,
required: false,
default: '',
},
},
data() {
return {
editableContent: this.content,
saveable: false,
};
},
computed: {
modified() {
return this.content !== this.editableContent;
},
},
methods: {
onSubmit() {
this.$emit('submit', { content: this.editableContent });
},
},
};
</script>
<template>
<gl-form-textarea :value="value" v-on="$listeners" />
<div class="d-flex flex-grow-1 flex-column">
<edit-header class="py-2" :title="title" />
<rich-content-editor v-if="glFeatures.richContentEditor" v-model="editableContent" />
<gl-form-textarea v-else v-model="editableContent" class="h-100 shadow-none" />
<publish-toolbar
class="gl-fixed gl-left-0 gl-bottom-0 gl-w-full"
:return-url="returnUrl"
:saveable="modified"
:saving-changes="savingChanges"
@submit="onSubmit"
/>
</div>
</template>
<script>
import { GlSkeletonLoader } from '@gitlab/ui';
export default {
components: {
GlSkeletonLoader,
},
};
</script>
<template>
<gl-skeleton-loader :width="500" :height="102">
<rect width="500" height="16" rx="4" />
<rect y="20" width="375" height="16" rx="4" />
<rect x="380" y="20" width="120" height="16" rx="4" />
<rect y="40" width="250" height="16" rx="4" />
<rect x="255" y="40" width="150" height="16" rx="4" />
<rect x="410" y="40" width="90" height="16" rx="4" />
</gl-skeleton-loader>
</template>
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
import { GlSkeletonLoader } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import RichContentEditor from '~/vue_shared/components/rich_content_editor/rich_content_editor.vue';
import SkeletonLoader from '../components/skeleton_loader.vue';
import EditArea from '../components/edit_area.vue';
import EditHeader from '../components/edit_header.vue';
import SavedChangesMessage from '../components/saved_changes_message.vue';
import PublishToolbar from '../components/publish_toolbar.vue';
import InvalidContentMessage from '../components/invalid_content_message.vue';
import SubmitChangesError from '../components/submit_changes_error.vue';
......@@ -20,16 +16,12 @@ import { LOAD_CONTENT_ERROR } from '../constants';
export default {
components: {
RichContentEditor,
SkeletonLoader,
EditArea,
EditHeader,
InvalidContentMessage,
GlSkeletonLoader,
SavedChangesMessage,
PublishToolbar,
SubmitChangesError,
},
mixins: [glFeatureFlagsMixin()],
apollo: {
appData: {
query: appDataQuery,
......@@ -58,80 +50,51 @@ export default {
},
},
computed: {
...mapState([
'content',
'isLoadingContent',
'isSavingChanges',
'isContentLoaded',
'returnUrl',
'title',
'submitChangesError',
'savedContentMeta',
]),
...mapGetters(['contentChanged']),
...mapState(['isSavingChanges', 'submitChangesError', 'savedContentMeta']),
isLoadingContent() {
return this.$apollo.queries.sourceContent.loading;
},
isContentLoaded() {
return Boolean(this.sourceContent);
},
mounted() {
if (this.appData.isSupportedContent) {
this.loadContent();
}
},
methods: {
...mapActions(['loadContent', 'setContent', 'submitChanges', 'dismissSubmitChangesError']),
...mapActions(['setContent', 'submitChanges', 'dismissSubmitChangesError']),
onSubmit({ content }) {
this.setContent(content);
this.submitChanges();
},
},
};
</script>
<template>
<div class="d-flex justify-content-center h-100 pt-2">
<div class="container d-flex gl-flex-direction-column pt-2 h-100">
<!-- Success view -->
<saved-changes-message
v-if="savedContentMeta"
class="w-75"
:branch="savedContentMeta.branch"
:commit="savedContentMeta.commit"
:merge-request="savedContentMeta.mergeRequest"
:return-url="returnUrl"
:return-url="appData.returnUrl"
/>
<!-- Main view -->
<template v-else-if="appData.isSupportedContent">
<div v-if="isLoadingContent" class="w-50 h-50">
<gl-skeleton-loader :width="500" :height="102">
<rect width="500" height="16" rx="4" />
<rect y="20" width="375" height="16" rx="4" />
<rect x="380" y="20" width="120" height="16" rx="4" />
<rect y="40" width="250" height="16" rx="4" />
<rect x="255" y="40" width="150" height="16" rx="4" />
<rect x="410" y="40" width="90" height="16" rx="4" />
</gl-skeleton-loader>
</div>
<div v-if="isContentLoaded" class="d-flex flex-grow-1 flex-column">
<skeleton-loader v-if="isLoadingContent" class="w-75 gl-align-self-center gl-mt-5" />
<submit-changes-error
v-if="submitChangesError"
class="w-75 align-self-center"
:error="submitChangesError"
@retry="submitChanges"
@dismiss="dismissSubmitChangesError"
/>
<edit-header class="w-75 align-self-center py-2" :title="title" />
<rich-content-editor
v-if="glFeatures.richContentEditor"
class="w-75 gl-align-self-center"
:value="content"
@input="setContent"
/>
<edit-area
v-else
class="w-75 h-100 shadow-none align-self-center"
:value="content"
@input="setContent"
/>
<publish-toolbar
:return-url="returnUrl"
:saveable="contentChanged"
v-if="isContentLoaded"
:title="sourceContent.title"
:content="sourceContent.content"
:saving-changes="isSavingChanges"
@submit="submitChanges"
:return-url="appData.returnUrl"
@submit="onSubmit"
/>
</div>
</template>
<!-- Error view -->
......
import { shallowMount } from '@vue/test-utils';
import RichContentEditor from '~/vue_shared/components/rich_content_editor/rich_content_editor.vue';
import EditArea from '~/static_site_editor/components/edit_area.vue';
import PublishToolbar from '~/static_site_editor/components/publish_toolbar.vue';
import EditHeader from '~/static_site_editor/components/edit_header.vue';
import { sourceContentTitle as title, sourceContent as content, returnUrl } from '../mock_data';
describe('~/static_site_editor/components/edit_area.vue', () => {
let wrapper;
const savingChanges = true;
const newContent = `new ${content}`;
const buildWrapper = (propsData = {}) => {
wrapper = shallowMount(EditArea, {
provide: {
glFeatures: { richContentEditor: true },
},
propsData: {
title,
content,
returnUrl,
savingChanges,
...propsData,
},
});
};
const findEditHeader = () => wrapper.find(EditHeader);
const findRichContentEditor = () => wrapper.find(RichContentEditor);
const findPublishToolbar = () => wrapper.find(PublishToolbar);
beforeEach(() => {
buildWrapper();
});
afterEach(() => {
wrapper.destroy();
});
it('renders edit header', () => {
expect(findEditHeader().exists()).toBe(true);
expect(findEditHeader().props('title')).toBe(title);
});
it('renders rich content editor', () => {
expect(findRichContentEditor().exists()).toBe(true);
expect(findRichContentEditor().props('value')).toBe(content);
});
it('renders publish toolbar', () => {
expect(findPublishToolbar().exists()).toBe(true);
expect(findPublishToolbar().props('returnUrl')).toBe(returnUrl);
expect(findPublishToolbar().props('savingChanges')).toBe(savingChanges);
expect(findPublishToolbar().props('saveable')).toBe(false);
});
describe('when content changes', () => {
beforeEach(() => {
findRichContentEditor().vm.$emit('input', newContent);
return wrapper.vm.$nextTick();
});
it('sets publish toolbar as saveable when content changes', () => {
expect(findPublishToolbar().props('saveable')).toBe(true);
});
it('sets publish toolbar as not saveable when content changes are rollback', () => {
findRichContentEditor().vm.$emit('input', content);
return wrapper.vm.$nextTick().then(() => {
expect(findPublishToolbar().props('saveable')).toBe(false);
});
});
});
});
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlSkeletonLoader } from '@gitlab/ui';
import createState from '~/static_site_editor/store/state';
import Home from '~/static_site_editor/pages/home.vue';
import RichContentEditor from '~/vue_shared/components/rich_content_editor/rich_content_editor.vue';
import EditHeader from '~/static_site_editor/components/edit_header.vue';
import SkeletonLoader from '~/static_site_editor/components/skeleton_loader.vue';
import EditArea from '~/static_site_editor/components/edit_area.vue';
import InvalidContentMessage from '~/static_site_editor/components/invalid_content_message.vue';
import PublishToolbar from '~/static_site_editor/components/publish_toolbar.vue';
import SubmitChangesError from '~/static_site_editor/components/submit_changes_error.vue';
import SavedChangesMessage from '~/static_site_editor/components/saved_changes_message.vue';
import {
returnUrl,
sourceContent,
sourceContentTitle,
sourceContent as content,
sourceContentTitle as title,
savedContentMeta,
submitChangesError,
} from '../mock_data';
......@@ -27,13 +26,12 @@ localVue.use(Vuex);
describe('static_site_editor/pages/home', () => {
let wrapper;
let store;
let loadContentActionMock;
let $apollo;
let setContentActionMock;
let submitChangesActionMock;
let dismissSubmitChangesErrorActionMock;
const buildStore = ({ initialState, getters } = {}) => {
loadContentActionMock = jest.fn();
setContentActionMock = jest.fn();
submitChangesActionMock = jest.fn();
dismissSubmitChangesErrorActionMock = jest.fn();
......@@ -47,53 +45,55 @@ describe('static_site_editor/pages/home', () => {
...getters,
},
actions: {
loadContent: loadContentActionMock,
setContent: setContentActionMock,
submitChanges: submitChangesActionMock,
dismissSubmitChangesError: dismissSubmitChangesErrorActionMock,
},
});
};
const buildContentLoadedStore = ({ initialState, getters } = {}) => {
buildStore({
initialState: {
isContentLoaded: true,
...initialState,
const buildApollo = (queries = {}) => {
$apollo = {
queries: {
sourceContent: {
loading: false,
},
getters: {
...getters,
...queries,
},
});
};
};
const buildWrapper = (data = { appData: { isSupportedContent: true } }) => {
const buildWrapper = (data = {}) => {
wrapper = shallowMount(Home, {
localVue,
store,
provide: {
glFeatures: { richContentEditor: true },
mocks: {
$apollo,
},
data() {
return data;
return {
appData: { isSupportedContent: true, returnUrl },
...data,
};
},
});
};
const findRichContentEditor = () => wrapper.find(RichContentEditor);
const findEditHeader = () => wrapper.find(EditHeader);
const findEditArea = () => wrapper.find(EditArea);
const findInvalidContentMessage = () => wrapper.find(InvalidContentMessage);
const findPublishToolbar = () => wrapper.find(PublishToolbar);
const findSkeletonLoader = () => wrapper.find(GlSkeletonLoader);
const findSkeletonLoader = () => wrapper.find(SkeletonLoader);
const findSubmitChangesError = () => wrapper.find(SubmitChangesError);
const findSavedChangesMessage = () => wrapper.find(SavedChangesMessage);
beforeEach(() => {
buildApollo();
buildStore();
buildWrapper();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
$apollo = null;
});
it('renders the saved changes message when changes are submitted successfully', () => {
......@@ -107,103 +107,69 @@ describe('static_site_editor/pages/home', () => {
});
});
describe('when content is not loaded', () => {
it('does not render rich content editor', () => {
expect(findRichContentEditor().exists()).toBe(false);
});
it('does not render edit header', () => {
expect(findEditHeader().exists()).toBe(false);
});
it('does not render toolbar', () => {
expect(findPublishToolbar().exists()).toBe(false);
});
it('does not render the saved changes message when changes are not submitted', () => {
buildWrapper();
it('does not render saved changes message', () => {
expect(findSavedChangesMessage().exists()).toBe(false);
});
});
describe('when content is loaded', () => {
const content = sourceContent;
const title = sourceContentTitle;
beforeEach(() => {
buildContentLoadedStore({ initialState: { content, title } });
buildWrapper();
buildStore({ initialState: { isSavingChanges: true } });
buildWrapper({ sourceContent: { title, content } });
});
it('renders the rich content editor', () => {
expect(findRichContentEditor().exists()).toBe(true);
it('renders edit area', () => {
expect(findEditArea().exists()).toBe(true);
});
it('renders the edit header', () => {
expect(findEditHeader().exists()).toBe(true);
it('provides source content to the edit area', () => {
expect(findEditArea().props()).toMatchObject({
title,
content,
});
it('does not render skeleton loader', () => {
expect(findSkeletonLoader().exists()).toBe(false);
});
it('passes page content to the rich content editor', () => {
expect(findRichContentEditor().props('value')).toBe(content);
it('provides returnUrl to the edit area', () => {
expect(findEditArea().props('returnUrl')).toBe(returnUrl);
});
it('passes page title to edit header', () => {
expect(findEditHeader().props('title')).toBe(title);
});
it('renders toolbar', () => {
expect(findPublishToolbar().exists()).toBe(true);
it('provides isSavingChanges to the edit area', () => {
expect(findEditArea().props('savingChanges')).toBe(true);
});
});
it('sets toolbar as saveable when content changes', () => {
buildContentLoadedStore({
getters: {
contentChanged: () => true,
},
});
buildWrapper();
it('does not render edit area when content is not loaded', () => {
buildWrapper({ sourceContent: null });
expect(findPublishToolbar().props('saveable')).toBe(true);
expect(findEditArea().exists()).toBe(false);
});
it('displays skeleton loader when loading content', () => {
buildStore({ initialState: { isLoadingContent: true } });
buildWrapper();
expect(findSkeletonLoader().exists()).toBe(true);
it('renders skeleton loader when content is not loading', () => {
buildApollo({
sourceContent: {
loading: true,
},
});
it('does not display submit changes error when an error does not exist', () => {
buildContentLoadedStore();
buildWrapper();
expect(findSubmitChangesError().exists()).toBe(false);
expect(findSkeletonLoader().exists()).toBe(true);
});
it('sets toolbar as saving when saving changes', () => {
buildContentLoadedStore({
initialState: {
isSavingChanges: true,
it('does not render skeleton loader when content is not loading', () => {
buildApollo({
sourceContent: {
loading: false,
},
});
buildWrapper();
expect(findPublishToolbar().props('savingChanges')).toBe(true);
});
it('displays invalid content message when content is not supported', () => {
buildWrapper({ appData: { isSupportedContent: false } });
expect(findInvalidContentMessage().exists()).toBe(true);
expect(findSkeletonLoader().exists()).toBe(false);
});
describe('when submitting changes fail', () => {
beforeEach(() => {
buildContentLoadedStore({
buildStore({
initialState: {
submitChangesError,
},
......@@ -228,24 +194,32 @@ describe('static_site_editor/pages/home', () => {
});
});
it('dispatches load content action', () => {
expect(loadContentActionMock).toHaveBeenCalled();
it('does not display submit changes error when an error does not exist', () => {
buildWrapper();
expect(findSubmitChangesError().exists()).toBe(false);
});
it('dispatches setContent action when rich content editor emits input event', () => {
buildContentLoadedStore();
buildWrapper();
it('displays invalid content message when content is not supported', () => {
buildWrapper({ appData: { isSupportedContent: false } });
findRichContentEditor().vm.$emit('input', sourceContent);
expect(findInvalidContentMessage().exists()).toBe(true);
});
expect(setContentActionMock).toHaveBeenCalledWith(expect.anything(), sourceContent, undefined);
describe('when edit area emits submit event', () => {
const newContent = `new ${content}`;
beforeEach(() => {
buildWrapper({ sourceContent: { title, content } });
findEditArea().vm.$emit('submit', { content: newContent });
});
it('dispatches submitChanges action when toolbar emits submit event', () => {
buildContentLoadedStore();
buildWrapper();
findPublishToolbar().vm.$emit('submit');
it('dispatches setContent property', () => {
expect(setContentActionMock).toHaveBeenCalledWith(expect.anything(), newContent, undefined);
});
it('dispatches submitChanges action', () => {
expect(submitChangesActionMock).toHaveBeenCalled();
});
});
});
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