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 @@ ...@@ -2,6 +2,7 @@
import RichContentEditor from '~/vue_shared/components/rich_content_editor/rich_content_editor.vue'; import RichContentEditor from '~/vue_shared/components/rich_content_editor/rich_content_editor.vue';
import PublishToolbar from './publish_toolbar.vue'; import PublishToolbar from './publish_toolbar.vue';
import EditHeader from './edit_header.vue'; import EditHeader from './edit_header.vue';
import EditDrawer from './edit_drawer.vue';
import UnsavedChangesConfirmDialog from './unsaved_changes_confirm_dialog.vue'; import UnsavedChangesConfirmDialog from './unsaved_changes_confirm_dialog.vue';
import parseSourceFile from '~/static_site_editor/services/parse_source_file'; import parseSourceFile from '~/static_site_editor/services/parse_source_file';
import { EDITOR_TYPES } from '~/vue_shared/components/rich_content_editor/constants'; import { EDITOR_TYPES } from '~/vue_shared/components/rich_content_editor/constants';
...@@ -15,6 +16,7 @@ export default { ...@@ -15,6 +16,7 @@ export default {
RichContentEditor, RichContentEditor,
PublishToolbar, PublishToolbar,
EditHeader, EditHeader,
EditDrawer,
UnsavedChangesConfirmDialog, UnsavedChangesConfirmDialog,
}, },
props: { props: {
...@@ -48,6 +50,8 @@ export default { ...@@ -48,6 +50,8 @@ export default {
parsedSource: parseSourceFile(this.preProcess(true, this.content)), parsedSource: parseSourceFile(this.preProcess(true, this.content)),
editorMode: EDITOR_TYPES.wysiwyg, editorMode: EDITOR_TYPES.wysiwyg,
isModified: false, isModified: false,
isDrawerOpen: false,
hasMatter: false,
}; };
}, },
imageRepository: imageRepository(), imageRepository: imageRepository(),
...@@ -55,6 +59,12 @@ export default { ...@@ -55,6 +59,12 @@ export default {
editableContent() { editableContent() {
return this.parsedSource.content(this.isWysiwygMode); return this.parsedSource.content(this.isWysiwygMode);
}, },
editableMatter() {
return this.isDrawerOpen ? this.parsedSource.matter() : {};
},
hasSettingsButton() {
return this.hasMatter && this.isWysiwygMode;
},
isWysiwygMode() { isWysiwygMode() {
return this.editorMode === EDITOR_TYPES.wysiwyg; return this.editorMode === EDITOR_TYPES.wysiwyg;
}, },
...@@ -67,9 +77,21 @@ export default { ...@@ -67,9 +77,21 @@ export default {
: templater.unwrap(formattedContent); : templater.unwrap(formattedContent);
return templatedContent; 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) { onInputChange(newVal) {
this.parsedSource.syncContent(newVal, this.isWysiwygMode); this.parsedSource.syncContent(newVal, this.isWysiwygMode);
this.isModified = this.parsedSource.isModified(); this.refreshEditHelpers();
}, },
onModeChange(mode) { onModeChange(mode) {
this.editorMode = mode; this.editorMode = mode;
...@@ -77,6 +99,9 @@ export default { ...@@ -77,6 +99,9 @@ export default {
const preProcessedContent = this.preProcess(this.isWysiwygMode, this.editableContent); const preProcessedContent = this.preProcess(this.isWysiwygMode, this.editableContent);
this.$refs.editor.resetInitialValue(preProcessedContent); this.$refs.editor.resetInitialValue(preProcessedContent);
}, },
onUpdateSettings(settings) {
this.parsedSource.syncMatter(settings);
},
onUploadImage({ file, imageUrl }) { onUploadImage({ file, imageUrl }) {
this.$options.imageRepository.add(file, imageUrl); this.$options.imageRepository.add(file, imageUrl);
}, },
...@@ -93,6 +118,13 @@ export default { ...@@ -93,6 +118,13 @@ export default {
<template> <template>
<div class="d-flex flex-grow-1 flex-column h-100"> <div class="d-flex flex-grow-1 flex-column h-100">
<edit-header class="py-2" :title="title" /> <edit-header class="py-2" :title="title" />
<edit-drawer
v-if="hasMatter"
:is-open="isDrawerOpen"
:settings="editableMatter"
@close="onDrawerClose"
@updateSettings="onUpdateSettings"
/>
<rich-content-editor <rich-content-editor
ref="editor" ref="editor"
:content="editableContent" :content="editableContent"
...@@ -106,9 +138,11 @@ export default { ...@@ -106,9 +138,11 @@ export default {
<unsaved-changes-confirm-dialog :modified="isModified" /> <unsaved-changes-confirm-dialog :modified="isModified" />
<publish-toolbar <publish-toolbar
class="gl-fixed gl-left-0 gl-bottom-0 gl-w-full" class="gl-fixed gl-left-0 gl-bottom-0 gl-w-full"
:is-show-edit="hasSettingsButton"
:return-url="returnUrl" :return-url="returnUrl"
:saveable="isModified" :saveable="isModified"
:saving-changes="savingChanges" :saving-changes="savingChanges"
@editSettings="onDrawerOpen"
@submit="onSubmit" @submit="onSubmit"
/> />
</div> </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 { ...@@ -6,6 +6,11 @@ export default {
GlButton, GlButton,
}, },
props: { props: {
isShowEdit: {
type: Boolean,
required: false,
default: false,
},
returnUrl: { returnUrl: {
type: String, type: String,
required: false, required: false,
...@@ -31,12 +36,21 @@ export default { ...@@ -31,12 +36,21 @@ export default {
s__('StaticSiteEditor|Return to site') s__('StaticSiteEditor|Return to site')
}}</gl-button> }}</gl-button>
<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" variant="success"
:disabled="!saveable" :disabled="!saveable"
:loading="savingChanges" :loading="savingChanges"
@click="$emit('submit')" @click="$emit('submit')"
> >
<span>{{ __('Submit Changes') }}</span> <span>{{ __('Submit changes') }}</span>
</gl-button> </gl-button>
</div> </div>
</div> </div>
......
...@@ -17,33 +17,24 @@ const parseSourceFile = raw => { ...@@ -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 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 matter = () => editable.data;
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 syncMatterObject = obj => { const syncMatter = settings => {
editable.data = obj; const source = grayMatter.stringify(editable.content, settings);
syncContent(source);
}; };
const isModified = () => trimmedEditable() !== raw; const isModified = () => trimmedEditable() !== raw;
const hasMatter = () => editable.matter.length > 0;
return { return {
matter, matter,
syncMatter, syncMatter,
matterObject,
syncMatterObject,
content, content,
syncContent, syncContent,
isModified, 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 "" ...@@ -18078,6 +18078,9 @@ msgstr ""
msgid "Page not found" msgid "Page not found"
msgstr "" msgstr ""
msgid "Page settings"
msgstr ""
msgid "Page was successfully deleted" msgid "Page was successfully deleted"
msgstr "" msgstr ""
...@@ -24309,15 +24312,15 @@ msgstr "" ...@@ -24309,15 +24312,15 @@ msgstr ""
msgid "Submit %{humanized_resource_name}" msgid "Submit %{humanized_resource_name}"
msgstr "" msgstr ""
msgid "Submit Changes"
msgstr ""
msgid "Submit a review" msgid "Submit a review"
msgstr "" msgstr ""
msgid "Submit as spam" msgid "Submit as spam"
msgstr "" msgstr ""
msgid "Submit changes"
msgstr ""
msgid "Submit feedback" msgid "Submit feedback"
msgstr "" msgstr ""
......
...@@ -6,6 +6,7 @@ import { EDITOR_TYPES } from '~/vue_shared/components/rich_content_editor/consta ...@@ -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 EditArea from '~/static_site_editor/components/edit_area.vue';
import PublishToolbar from '~/static_site_editor/components/publish_toolbar.vue'; import PublishToolbar from '~/static_site_editor/components/publish_toolbar.vue';
import EditHeader from '~/static_site_editor/components/edit_header.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 UnsavedChangesConfirmDialog from '~/static_site_editor/components/unsaved_changes_confirm_dialog.vue';
import { import {
...@@ -36,6 +37,7 @@ describe('~/static_site_editor/components/edit_area.vue', () => { ...@@ -36,6 +37,7 @@ describe('~/static_site_editor/components/edit_area.vue', () => {
}; };
const findEditHeader = () => wrapper.find(EditHeader); const findEditHeader = () => wrapper.find(EditHeader);
const findEditDrawer = () => wrapper.find(EditDrawer);
const findRichContentEditor = () => wrapper.find(RichContentEditor); const findRichContentEditor = () => wrapper.find(RichContentEditor);
const findPublishToolbar = () => wrapper.find(PublishToolbar); const findPublishToolbar = () => wrapper.find(PublishToolbar);
const findUnsavedChangesConfirmDialog = () => wrapper.find(UnsavedChangesConfirmDialog); const findUnsavedChangesConfirmDialog = () => wrapper.find(UnsavedChangesConfirmDialog);
...@@ -53,6 +55,10 @@ describe('~/static_site_editor/components/edit_area.vue', () => { ...@@ -53,6 +55,10 @@ describe('~/static_site_editor/components/edit_area.vue', () => {
expect(findEditHeader().props('title')).toBe(title); 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', () => { it('renders rich content editor with a format pass', () => {
expect(findRichContentEditor().exists()).toBe(true); expect(findRichContentEditor().exists()).toBe(true);
expect(findRichContentEditor().props('content')).toBe(formattedBody); expect(findRichContentEditor().props('content')).toBe(formattedBody);
...@@ -148,6 +154,56 @@ describe('~/static_site_editor/components/edit_area.vue', () => { ...@@ -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', () => { describe('when content is submitted', () => {
it('should format the content', () => { it('should format the content', () => {
findPublishToolbar().vm.$emit('submit', 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 { shallowMount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
import PublishToolbar from '~/static_site_editor/components/publish_toolbar.vue'; import PublishToolbar from '~/static_site_editor/components/publish_toolbar.vue';
...@@ -11,6 +10,7 @@ describe('Static Site Editor Toolbar', () => { ...@@ -11,6 +10,7 @@ describe('Static Site Editor Toolbar', () => {
const buildWrapper = (propsData = {}) => { const buildWrapper = (propsData = {}) => {
wrapper = shallowMount(PublishToolbar, { wrapper = shallowMount(PublishToolbar, {
propsData: { propsData: {
isShowEdit: false,
saveable: false, saveable: false,
...propsData, ...propsData,
}, },
...@@ -18,7 +18,8 @@ describe('Static Site Editor Toolbar', () => { ...@@ -18,7 +18,8 @@ describe('Static Site Editor Toolbar', () => {
}; };
const findReturnUrlLink = () => wrapper.find({ ref: 'returnUrlLink' }); const findReturnUrlLink = () => wrapper.find({ ref: 'returnUrlLink' });
const findSaveChangesButton = () => wrapper.find(GlButton); const findSaveChangesButton = () => wrapper.find({ ref: 'submit' });
const findEditSettingsButton = () => wrapper.find({ ref: 'settings' });
beforeEach(() => { beforeEach(() => {
buildWrapper(); buildWrapper();
...@@ -28,6 +29,10 @@ describe('Static Site Editor Toolbar', () => { ...@@ -28,6 +29,10 @@ describe('Static Site Editor Toolbar', () => {
wrapper.destroy(); wrapper.destroy();
}); });
it('does not render Settings button', () => {
expect(findEditSettingsButton().exists()).toBe(false);
});
it('renders Submit Changes button', () => { it('renders Submit Changes button', () => {
expect(findSaveChangesButton().exists()).toBe(true); expect(findSaveChangesButton().exists()).toBe(true);
}); });
...@@ -51,6 +56,14 @@ describe('Static Site Editor Toolbar', () => { ...@@ -51,6 +56,14 @@ describe('Static Site Editor Toolbar', () => {
expect(findReturnUrlLink().attributes('href')).toBe(returnUrl); 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', () => { describe('when saveable', () => {
it('enables Submit Changes button', () => { it('enables Submit Changes button', () => {
buildWrapper({ saveable: true }); buildWrapper({ saveable: true });
......
...@@ -16,16 +16,12 @@ describe('static_site_editor/services/parse_source_file', () => { ...@@ -16,16 +16,12 @@ describe('static_site_editor/services/parse_source_file', () => {
describe('unmodified front matter', () => { describe('unmodified front matter', () => {
it.each` it.each`
parsedSource | targetFrontMatter parsedSource
${parseSourceFile(content)} | ${yamlFrontMatter} ${parseSourceFile(content)}
${parseSourceFile(contentComplex)} | ${yamlFrontMatter} ${parseSourceFile(contentComplex)}
`( `('returns $targetFrontMatter when frontMatter queried', ({ parsedSource }) => {
'returns $targetFrontMatter when frontMatter queried', expect(parsedSource.matter()).toEqual(yamlFrontMatterObj);
({ parsedSource, targetFrontMatter }) => { });
expect(targetFrontMatter).toContain(parsedSource.matter());
expect(parsedSource.matterObject()).toEqual(yamlFrontMatterObj);
},
);
}); });
describe('unmodified content', () => { describe('unmodified content', () => {
...@@ -69,12 +65,11 @@ describe('static_site_editor/services/parse_source_file', () => { ...@@ -69,12 +65,11 @@ describe('static_site_editor/services/parse_source_file', () => {
`( `(
'returns the correct front matter and modified content', 'returns the correct front matter and modified content',
({ parsedSource, targetContent }) => { ({ 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.matter()).toMatchObject(newYamlFrontMatterObj);
expect(parsedSource.matterObject()).toEqual(newYamlFrontMatterObj);
expect(parsedSource.content()).toBe(targetContent); expect(parsedSource.content()).toBe(targetContent);
}, },
); );
...@@ -85,16 +80,19 @@ describe('static_site_editor/services/parse_source_file', () => { ...@@ -85,16 +80,19 @@ describe('static_site_editor/services/parse_source_file', () => {
const newComplexBody = `${complexBody} ${edit}`; const newComplexBody = `${complexBody} ${edit}`;
it.each` it.each`
parsedSource | isModified | targetRaw | targetBody parsedSource | hasMatter | isModified | targetRaw | targetBody
${parseSourceFile(content)} | ${false} | ${content} | ${body} ${parseSourceFile(content)} | ${true} | ${false} | ${content} | ${body}
${parseSourceFile(content)} | ${true} | ${newContent} | ${newBody} ${parseSourceFile(content)} | ${true} | ${true} | ${newContent} | ${newBody}
${parseSourceFile(contentComplex)} | ${false} | ${contentComplex} | ${complexBody} ${parseSourceFile(contentComplex)} | ${true} | ${false} | ${contentComplex} | ${complexBody}
${parseSourceFile(contentComplex)} | ${true} | ${newContentComplex} | ${newComplexBody} ${parseSourceFile(contentComplex)} | ${true} | ${true} | ${newContentComplex} | ${newComplexBody}
${parseSourceFile(body)} | ${false} | ${false} | ${body} | ${body}
${parseSourceFile(body)} | ${false} | ${true} | ${newBody} | ${newBody}
`( `(
'returns $isModified after a $targetRaw sync', 'returns $isModified after a $targetRaw sync',
({ parsedSource, isModified, targetRaw, targetBody }) => { ({ parsedSource, hasMatter, isModified, targetRaw, targetBody }) => {
parsedSource.syncContent(targetRaw); parsedSource.syncContent(targetRaw);
expect(parsedSource.hasMatter()).toBe(hasMatter);
expect(parsedSource.isModified()).toBe(isModified); expect(parsedSource.isModified()).toBe(isModified);
expect(parsedSource.content()).toBe(targetRaw); expect(parsedSource.content()).toBe(targetRaw);
expect(parsedSource.content(true)).toBe(targetBody); 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