Commit a600662a authored by derek-knox's avatar derek-knox

Wire up initial front matter editing UI

Add edit_drawer and updated publish_toolbar
so that edit_area can faciliate content and
front matter updates via parse_source_file
delegation
parent 3ec3b6b0
......@@ -2,6 +2,7 @@
import RichContentEditor from '~/vue_shared/components/rich_content_editor/rich_content_editor.vue';
import PublishToolbar from './publish_toolbar.vue';
import EditHeader from './edit_header.vue';
import EditDrawer from './edit_drawer.vue';
import UnsavedChangesConfirmDialog from './unsaved_changes_confirm_dialog.vue';
import parseSourceFile from '~/static_site_editor/services/parse_source_file';
import { EDITOR_TYPES } from '~/vue_shared/components/rich_content_editor/constants';
......@@ -15,6 +16,7 @@ export default {
RichContentEditor,
PublishToolbar,
EditHeader,
EditDrawer,
UnsavedChangesConfirmDialog,
},
props: {
......@@ -48,6 +50,8 @@ export default {
parsedSource: parseSourceFile(this.preProcess(true, this.content)),
editorMode: EDITOR_TYPES.wysiwyg,
isModified: false,
isDrawerOpen: false,
hasMatter: false,
};
},
imageRepository: imageRepository(),
......@@ -55,6 +59,12 @@ export default {
editableContent() {
return this.parsedSource.content(this.isWysiwygMode);
},
editableMatter() {
return this.isDrawerOpen ? this.parsedSource.matter() : {};
},
hasSettingsButton() {
return this.hasMatter && this.isWysiwygMode;
},
isWysiwygMode() {
return this.editorMode === EDITOR_TYPES.wysiwyg;
},
......@@ -67,9 +77,21 @@ export default {
: templater.unwrap(formattedContent);
return templatedContent;
},
refreshEditHelpers() {
this.isModified = this.parsedSource.isModified();
this.hasMatter = this.parsedSource.hasMatter();
},
onDrawerOpen() {
this.isDrawerOpen = true;
this.refreshEditHelpers();
},
onDrawerClose() {
this.isDrawerOpen = false;
this.refreshEditHelpers();
},
onInputChange(newVal) {
this.parsedSource.syncContent(newVal, this.isWysiwygMode);
this.isModified = this.parsedSource.isModified();
this.refreshEditHelpers();
},
onModeChange(mode) {
this.editorMode = mode;
......@@ -77,6 +99,9 @@ export default {
const preProcessedContent = this.preProcess(this.isWysiwygMode, this.editableContent);
this.$refs.editor.resetInitialValue(preProcessedContent);
},
onUpdateSettings(settings) {
this.parsedSource.syncMatter(settings);
},
onUploadImage({ file, imageUrl }) {
this.$options.imageRepository.add(file, imageUrl);
},
......@@ -93,6 +118,13 @@ export default {
<template>
<div class="d-flex flex-grow-1 flex-column h-100">
<edit-header class="py-2" :title="title" />
<edit-drawer
v-if="hasMatter"
:is-open="isDrawerOpen"
:settings="editableMatter"
@close="onDrawerClose"
@updateSettings="onUpdateSettings"
/>
<rich-content-editor
ref="editor"
:content="editableContent"
......@@ -106,9 +138,11 @@ export default {
<unsaved-changes-confirm-dialog :modified="isModified" />
<publish-toolbar
class="gl-fixed gl-left-0 gl-bottom-0 gl-w-full"
:is-show-edit="hasSettingsButton"
:return-url="returnUrl"
:saveable="isModified"
:saving-changes="savingChanges"
@editSettings="onDrawerOpen"
@submit="onSubmit"
/>
</div>
......
<script>
import { GlDrawer } from '@gitlab/ui';
import FrontMatterControls from './front_matter_controls.vue';
export default {
components: {
GlDrawer,
FrontMatterControls,
},
props: {
isOpen: {
type: Boolean,
required: true,
},
settings: {
type: Object,
required: true,
},
},
};
</script>
<template>
<gl-drawer class="pt-6" :open="isOpen" @close="$emit('close')">
<template #header>{{ __('Page settings') }}</template>
<template>
<front-matter-controls
:settings="settings"
@updateSettings="$emit('updateSettings', $event)"
/>
</template>
</gl-drawer>
</template>
......@@ -6,6 +6,11 @@ export default {
GlButton,
},
props: {
isShowEdit: {
type: Boolean,
required: false,
default: false,
},
returnUrl: {
type: String,
required: false,
......@@ -31,12 +36,21 @@ export default {
s__('StaticSiteEditor|Return to site')
}}</gl-button>
<gl-button
v-if="isShowEdit"
ref="settings"
:loading="savingChanges"
@click="$emit('editSettings')"
>
<span>{{ __('Settings') }}</span>
</gl-button>
<gl-button
ref="submit"
variant="success"
:disabled="!saveable"
:loading="savingChanges"
@click="$emit('submit')"
>
<span>{{ __('Submit Changes') }}</span>
<span>{{ __('Submit changes') }}</span>
</gl-button>
</div>
</div>
......
......@@ -17,33 +17,24 @@ const parseSourceFile = raw => {
const content = (isBody = false) => (isBody ? editable.content.trim() : trimmedEditable()); // gray-matter internally adds an eof newline so we trim to bypass, open issue: https://github.com/jonschlinkert/gray-matter/issues/96
const matter = () => editable.matter;
const syncMatter = newMatter => {
const targetMatter = newMatter.replace(/---/gm, ''); // TODO dynamic delimiter removal vs. hard code
const currentMatter = matter();
const currentContent = content();
const newSource = currentContent.replace(currentMatter, targetMatter);
syncContent(newSource);
editable.matter = newMatter;
};
const matterObject = () => editable.data;
const matter = () => editable.data;
const syncMatterObject = obj => {
editable.data = obj;
const syncMatter = settings => {
const source = grayMatter.stringify(editable.content, settings);
syncContent(source);
};
const isModified = () => trimmedEditable() !== raw;
const hasMatter = () => editable.matter.length > 0;
return {
matter,
syncMatter,
matterObject,
syncMatterObject,
content,
syncContent,
isModified,
hasMatter,
};
};
......
---
title: Add a front matter editing UI in WYSIWYG mode of the Static Site Editor
merge_request: 41920
author:
type: added
......@@ -18078,6 +18078,9 @@ msgstr ""
msgid "Page not found"
msgstr ""
msgid "Page settings"
msgstr ""
msgid "Page was successfully deleted"
msgstr ""
......@@ -24309,15 +24312,15 @@ msgstr ""
msgid "Submit %{humanized_resource_name}"
msgstr ""
msgid "Submit Changes"
msgstr ""
msgid "Submit a review"
msgstr ""
msgid "Submit as spam"
msgstr ""
msgid "Submit changes"
msgstr ""
msgid "Submit feedback"
msgstr ""
......
......@@ -6,6 +6,7 @@ import { EDITOR_TYPES } from '~/vue_shared/components/rich_content_editor/consta
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 EditDrawer from '~/static_site_editor/components/edit_drawer.vue';
import UnsavedChangesConfirmDialog from '~/static_site_editor/components/unsaved_changes_confirm_dialog.vue';
import {
......@@ -36,6 +37,7 @@ describe('~/static_site_editor/components/edit_area.vue', () => {
};
const findEditHeader = () => wrapper.find(EditHeader);
const findEditDrawer = () => wrapper.find(EditDrawer);
const findRichContentEditor = () => wrapper.find(RichContentEditor);
const findPublishToolbar = () => wrapper.find(PublishToolbar);
const findUnsavedChangesConfirmDialog = () => wrapper.find(UnsavedChangesConfirmDialog);
......@@ -53,6 +55,10 @@ describe('~/static_site_editor/components/edit_area.vue', () => {
expect(findEditHeader().props('title')).toBe(title);
});
it('does not render edit drawer', () => {
expect(findEditDrawer().exists()).toBe(false);
});
it('renders rich content editor with a format pass', () => {
expect(findRichContentEditor().exists()).toBe(true);
expect(findRichContentEditor().props('content')).toBe(formattedBody);
......@@ -148,6 +154,56 @@ describe('~/static_site_editor/components/edit_area.vue', () => {
});
});
describe('when hasMatter', () => {
beforeEach(() => {
wrapper.setData({ hasMatter: true, isDrawerOpen: false });
wrapper.vm.$nextTick();
});
afterEach(() => {
wrapper.setData({ hasMatter: false });
});
it('renders a closed edit drawer', () => {
expect(findEditDrawer().exists()).toBe(true);
expect(findEditDrawer().props('isOpen')).toBe(false);
});
it('opens the edit drawer', () => {
findPublishToolbar().vm.$emit('editSettings');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.isDrawerOpen).toBe(true);
expect(findEditDrawer().props('isOpen')).toBe(true);
});
});
it('closes the edit drawer', () => {
wrapper.setData({ isDrawerOpen: true });
findEditDrawer().vm.$emit('close');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.isDrawerOpen).toBe(false);
expect(findEditDrawer().props('isOpen')).toBe(false);
});
});
it('forwards the matter settings', () => {
expect(findEditDrawer().props('settings')).toBe(wrapper.vm.editableMatter);
});
it('syncs matter changes', () => {
const newSettings = { title: 'test' };
const spySyncParsedSource = jest.spyOn(wrapper.vm.parsedSource, 'syncMatter');
findEditDrawer().vm.$emit('updateSettings', newSettings);
expect(spySyncParsedSource).toHaveBeenCalledWith(newSettings);
});
});
describe('when content is submitted', () => {
it('should format the content', () => {
findPublishToolbar().vm.$emit('submit', content);
......
import { shallowMount } from '@vue/test-utils';
import { GlDrawer } from '@gitlab/ui';
import EditDrawer from '~/static_site_editor/components/edit_drawer.vue';
import FrontMatterControls from '~/static_site_editor/components/front_matter_controls.vue';
describe('~/static_site_editor/components/edit_drawer.vue', () => {
let wrapper;
const buildWrapper = (propsData = {}) => {
wrapper = shallowMount(EditDrawer, {
propsData: {
isOpen: false,
settings: { title: 'Some title' },
...propsData,
},
});
};
const findFrontMatterControls = () => wrapper.find(FrontMatterControls);
const findGlDrawer = () => wrapper.find(GlDrawer);
beforeEach(() => {
buildWrapper();
});
afterEach(() => {
wrapper.destroy();
});
it('renders the GlDrawer', () => {
expect(findGlDrawer().exists()).toBe(true);
});
it('renders the FrontMatterControls', () => {
expect(findFrontMatterControls().exists()).toBe(true);
});
it('forwards the settings to FrontMatterControls', () => {
expect(findFrontMatterControls().props('settings')).toBe(wrapper.props('settings'));
});
it('is closed be default', () => {
expect(findGlDrawer().props('open')).toBe(false);
});
it('can open', () => {
buildWrapper({ isOpen: true });
expect(findGlDrawer().props('open')).toBe(true);
});
it.each`
event | payload | finderFn
${'close'} | ${undefined} | ${findGlDrawer}
${'updateSettings'} | ${{ some: 'data' }} | ${findFrontMatterControls}
`(
'forwards the emitted $event event from the $finderFn with $payload',
({ event, payload, finderFn }) => {
finderFn().vm.$emit(event, payload);
expect(wrapper.emitted()[event][0][0]).toBe(payload);
},
);
});
import { shallowMount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
import PublishToolbar from '~/static_site_editor/components/publish_toolbar.vue';
......@@ -11,6 +10,7 @@ describe('Static Site Editor Toolbar', () => {
const buildWrapper = (propsData = {}) => {
wrapper = shallowMount(PublishToolbar, {
propsData: {
isShowEdit: false,
saveable: false,
...propsData,
},
......@@ -18,7 +18,8 @@ describe('Static Site Editor Toolbar', () => {
};
const findReturnUrlLink = () => wrapper.find({ ref: 'returnUrlLink' });
const findSaveChangesButton = () => wrapper.find(GlButton);
const findSaveChangesButton = () => wrapper.find({ ref: 'submit' });
const findEditSettingsButton = () => wrapper.find({ ref: 'settings' });
beforeEach(() => {
buildWrapper();
......@@ -28,6 +29,10 @@ describe('Static Site Editor Toolbar', () => {
wrapper.destroy();
});
it('does not render Settings button', () => {
expect(findEditSettingsButton().exists()).toBe(false);
});
it('renders Submit Changes button', () => {
expect(findSaveChangesButton().exists()).toBe(true);
});
......@@ -51,6 +56,14 @@ describe('Static Site Editor Toolbar', () => {
expect(findReturnUrlLink().attributes('href')).toBe(returnUrl);
});
describe('when providing settings CTA', () => {
it('enables Submit Changes button', () => {
buildWrapper({ isShowEdit: true });
expect(findEditSettingsButton().exists()).toBe(true);
});
});
describe('when saveable', () => {
it('enables Submit Changes button', () => {
buildWrapper({ saveable: true });
......
......@@ -16,16 +16,12 @@ describe('static_site_editor/services/parse_source_file', () => {
describe('unmodified front matter', () => {
it.each`
parsedSource | targetFrontMatter
${parseSourceFile(content)} | ${yamlFrontMatter}
${parseSourceFile(contentComplex)} | ${yamlFrontMatter}
`(
'returns $targetFrontMatter when frontMatter queried',
({ parsedSource, targetFrontMatter }) => {
expect(targetFrontMatter).toContain(parsedSource.matter());
expect(parsedSource.matterObject()).toEqual(yamlFrontMatterObj);
},
);
parsedSource
${parseSourceFile(content)}
${parseSourceFile(contentComplex)}
`('returns $targetFrontMatter when frontMatter queried', ({ parsedSource }) => {
expect(parsedSource.matter()).toEqual(yamlFrontMatterObj);
});
});
describe('unmodified content', () => {
......@@ -69,12 +65,11 @@ describe('static_site_editor/services/parse_source_file', () => {
`(
'returns the correct front matter and modified content',
({ parsedSource, targetContent }) => {
expect(yamlFrontMatter).toContain(parsedSource.matter());
expect(parsedSource.matter()).toMatchObject(yamlFrontMatterObj);
parsedSource.syncMatter(newYamlFrontMatter);
parsedSource.syncMatter(newYamlFrontMatterObj);
expect(parsedSource.matter()).toBe(newYamlFrontMatter);
expect(parsedSource.matterObject()).toEqual(newYamlFrontMatterObj);
expect(parsedSource.matter()).toMatchObject(newYamlFrontMatterObj);
expect(parsedSource.content()).toBe(targetContent);
},
);
......@@ -85,16 +80,19 @@ describe('static_site_editor/services/parse_source_file', () => {
const newComplexBody = `${complexBody} ${edit}`;
it.each`
parsedSource | isModified | targetRaw | targetBody
${parseSourceFile(content)} | ${false} | ${content} | ${body}
${parseSourceFile(content)} | ${true} | ${newContent} | ${newBody}
${parseSourceFile(contentComplex)} | ${false} | ${contentComplex} | ${complexBody}
${parseSourceFile(contentComplex)} | ${true} | ${newContentComplex} | ${newComplexBody}
parsedSource | hasMatter | isModified | targetRaw | targetBody
${parseSourceFile(content)} | ${true} | ${false} | ${content} | ${body}
${parseSourceFile(content)} | ${true} | ${true} | ${newContent} | ${newBody}
${parseSourceFile(contentComplex)} | ${true} | ${false} | ${contentComplex} | ${complexBody}
${parseSourceFile(contentComplex)} | ${true} | ${true} | ${newContentComplex} | ${newComplexBody}
${parseSourceFile(body)} | ${false} | ${false} | ${body} | ${body}
${parseSourceFile(body)} | ${false} | ${true} | ${newBody} | ${newBody}
`(
'returns $isModified after a $targetRaw sync',
({ parsedSource, isModified, targetRaw, targetBody }) => {
({ parsedSource, hasMatter, isModified, targetRaw, targetBody }) => {
parsedSource.syncContent(targetRaw);
expect(parsedSource.hasMatter()).toBe(hasMatter);
expect(parsedSource.isModified()).toBe(isModified);
expect(parsedSource.content()).toBe(targetRaw);
expect(parsedSource.content(true)).toBe(targetBody);
......
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