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