Commit c98500ea authored by Himanshu Kapoor's avatar Himanshu Kapoor Committed by Enrique Alcántara

Compatibility message and error handling in content editor

parent c17f8a2f
<script>
import { GlForm, GlIcon, GlLink, GlButton, GlSprintf, GlAlert, GlLoadingIcon } from '@gitlab/ui';
import {
GlForm,
GlIcon,
GlLink,
GlButton,
GlSprintf,
GlAlert,
GlLoadingIcon,
GlModal,
GlModalDirective,
} from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
import csrf from '~/lib/utils/csrf';
import { setUrlFragment } from '~/lib/utils/url_utility';
......@@ -15,6 +25,67 @@ const MARKDOWN_LINK_TEXT = {
};
export default {
i18n: {
title: {
label: s__('WikiPage|Title'),
placeholder: s__('WikiPage|Page title'),
helpText: {
existingPage: s__(
'WikiPage|Tip: You can move this page by adding the path to the beginning of the title.',
),
newPage: s__(
'WikiPage|Tip: You can specify the full path for the new file. We will automatically create any missing directories.',
),
moreInformation: s__('WikiPage|More Information.'),
},
},
format: {
label: s__('WikiPage|Format'),
},
content: {
label: s__('WikiPage|Content'),
placeholder: s__('WikiPage|Write your content or drag files here…'),
},
contentEditor: {
renderFailed: {
message: s__(
'WikiPage|An error occured while trying to render the content editor. Please try again later.',
),
primaryAction: s__('WikiPage|Retry'),
},
useNewEditor: s__('WikiPage|Use new editor'),
switchToOldEditor: {
label: s__('WikiPage|Switch to old editor'),
helpText: s__("WikiPage|Switching will discard any changes you've made in the new editor."),
modal: {
title: s__('WikiPage|Are you sure you want to switch to the old editor?'),
primary: s__('WikiPage|Switch to old editor'),
cancel: s__('WikiPage|Keep editing'),
text: s__(
"WikiPage|Switching to the old editor will discard any changes you've made in the new editor.",
),
},
},
helpText: s__(
"WikiPage|This editor is in beta and may not display the page's contents properly.",
),
},
linksHelpText: s__(
'WikiPage|To link to a (new) page, simply type %{linkExample}. More examples are in the %{linkStart}documentation%{linkEnd}.',
),
commitMessage: {
label: s__('WikiPage|Commit message'),
value: {
existingPage: s__('WikiPage|Update %{pageTitle}'),
newPage: s__('WikiPage|Create %{pageTitle}'),
},
},
submitButton: {
existingPage: s__('WikiPage|Save changes'),
newPage: s__('WikiPage|Create page'),
},
cancel: s__('WikiPage|Cancel'),
},
components: {
GlAlert,
GlForm,
......@@ -22,6 +93,7 @@ export default {
GlIcon,
GlLink,
GlButton,
GlModal,
MarkdownField,
GlLoadingIcon,
ContentEditor: () =>
......@@ -29,6 +101,9 @@ export default {
/* webpackChunkName: 'content_editor' */ '~/content_editor/components/content_editor.vue'
),
},
directives: {
GlModalDirective,
},
mixins: [glFeatureFlagMixin()],
inject: ['formatOptions', 'pageInfo'],
data() {
......@@ -41,6 +116,7 @@ export default {
commitMessage: '',
contentEditor: null,
isDirty: false,
contentEditorRenderFailed: false,
};
},
computed: {
......@@ -58,15 +134,21 @@ export default {
},
commitMessageI18n() {
return this.pageInfo.persisted
? s__('WikiPage|Update %{pageTitle}')
: s__('WikiPage|Create %{pageTitle}');
? this.$options.i18n.commitMessage.value.existingPage
: this.$options.i18n.commitMessage.value.newPage;
},
linkExample() {
return MARKDOWN_LINK_TEXT[this.format];
},
submitButtonText() {
if (this.pageInfo.persisted) return s__('WikiPage|Save changes');
return s__('WikiPage|Create page');
return this.pageInfo.persisted
? this.$options.i18n.submitButton.existingPage
: this.$options.i18n.submitButton.newPage;
},
titleHelpText() {
return this.pageInfo.persisted
? this.$options.i18n.title.helpText.existingPage
: this.$options.i18n.title.helpText.newPage;
},
cancelFormPath() {
if (this.pageInfo.persisted) return this.pageInfo.path;
......@@ -81,6 +163,9 @@ export default {
showContentEditorButton() {
return this.isMarkdownFormat && !this.useContentEditor && this.glFeatures.wikiContentEditor;
},
disableSubmitButton() {
return !this.content || !this.title || this.contentEditorRenderFailed;
},
isContentEditorActive() {
return this.isMarkdownFormat && this.useContentEditor;
},
......@@ -147,14 +232,32 @@ export default {
onUpdate: () => this.handleContentChange(),
},
});
await this.contentEditor.setSerializedContent(this.content);
this.isContentEditorLoading = false;
try {
await this.contentEditor.setSerializedContent(this.content);
this.isContentEditorLoading = false;
} catch (e) {
this.contentEditorRenderFailed = true;
}
},
retryInitContentEditor() {
this.contentEditorRenderFailed = false;
this.initContentEditor();
},
switchToOldEditor() {
this.useContentEditor = false;
},
confirmSwitchToOldEditor() {
if (this.contentEditorRenderFailed) {
this.contentEditorRenderFailed = false;
this.switchToOldEditor();
} else {
this.$refs.confirmSwitchToOldEditorModal.show();
}
},
},
};
</script>
......@@ -167,26 +270,15 @@ export default {
@submit="handleFormSubmit"
>
<gl-alert
v-if="isContentEditorActive"
v-if="isContentEditorActive && contentEditorRenderFailed"
class="gl-mb-6"
:dismissible="false"
variant="danger"
:primary-button-text="s__('WikiPage|Switch to old editor')"
@primaryAction="switchToOldEditor()"
:primary-button-text="$options.i18n.contentEditor.renderFailed.primaryAction"
@primaryAction="retryInitContentEditor()"
>
<p>
{{
s__(
"WikiPage|You are editing this page with Content Editor. This editor is in beta and may not display the page's contents properly.",
)
}}
</p>
<p>
{{
s__(
"WikiPage|Switching to the old editor will discard any changes you've made in the new editor.",
)
}}
{{ $options.i18n.contentEditor.renderFailed.message }}
</p>
</gl-alert>
......@@ -200,7 +292,9 @@ export default {
/>
<div class="form-group row">
<div class="col-sm-2 col-form-label">
<label class="control-label-full-width" for="wiki_title">{{ s__('WikiPage|Title') }}</label>
<label class="control-label-full-width" for="wiki_title">{{
$options.i18n.title.label
}}</label>
</div>
<div class="col-sm-10">
<input
......@@ -212,22 +306,15 @@ export default {
data-qa-selector="wiki_title_textbox"
:required="true"
:autofocus="!pageInfo.persisted"
:placeholder="s__('WikiPage|Page title')"
:placeholder="$options.i18n.title.placeholder"
@input="updateCommitMessage"
/>
<span class="gl-display-inline-block gl-max-w-full gl-mt-2 gl-text-gray-600">
<gl-icon class="gl-mr-n1" name="bulb" />
{{
pageInfo.persisted
? s__(
'WikiPage|Tip: You can move this page by adding the path to the beginning of the title.',
)
: s__(
'WikiPage|Tip: You can specify the full path for the new file. We will automatically create any missing directories.',
)
}}
{{ titleHelpText }}
<gl-link :href="helpPath" target="_blank"
><gl-icon name="question-o" /> {{ s__('WikiPage|More Information.') }}</gl-link
><gl-icon name="question-o" />
{{ $options.i18n.title.helpText.moreInformation }}</gl-link
>
</span>
</div>
......@@ -235,10 +322,10 @@ export default {
<div class="form-group row">
<div class="col-sm-2 col-form-label">
<label class="control-label-full-width" for="wiki_format">{{
s__('WikiPage|Format')
$options.i18n.format.label
}}</label>
</div>
<div class="col-sm-10 gl-display-flex gl-flex-wrap">
<div class="col-sm-10">
<select
id="wiki_format"
v-model="format"
......@@ -250,20 +337,43 @@ export default {
{{ label }}
</option>
</select>
<gl-button
v-if="showContentEditorButton"
category="secondary"
variant="confirm"
class="gl-mt-4"
@click="initContentEditor"
>{{ s__('WikiPage|Use new editor') }}</gl-button
>
<div>
<gl-button
v-if="showContentEditorButton"
category="secondary"
variant="confirm"
class="gl-mt-4"
@click="initContentEditor"
>{{ $options.i18n.contentEditor.useNewEditor }}</gl-button
>
<div v-if="isContentEditorActive" class="gl-mt-4 gl-display-flex">
<div class="gl-mr-4">
<gl-button category="secondary" variant="confirm" @click="confirmSwitchToOldEditor">{{
$options.i18n.contentEditor.switchToOldEditor.label
}}</gl-button>
</div>
<div class="gl-mt-2">
<gl-icon name="warning" />
{{ $options.i18n.contentEditor.switchToOldEditor.helpText }}
</div>
</div>
<gl-modal
ref="confirmSwitchToOldEditorModal"
modal-id="confirm-switch-to-old-editor"
:title="$options.i18n.contentEditor.switchToOldEditor.modal.title"
:action-primary="{ text: $options.i18n.contentEditor.switchToOldEditor.modal.primary }"
:action-cancel="{ text: $options.i18n.contentEditor.switchToOldEditor.modal.cancel }"
@primary="switchToOldEditor"
>
{{ $options.i18n.contentEditor.switchToOldEditor.modal.text }}
</gl-modal>
</div>
</div>
</div>
<div class="form-group row">
<div class="col-sm-2 col-form-label">
<label class="control-label-full-width" for="wiki_content">{{
s__('WikiPage|Content')
$options.i18n.content.label
}}</label>
</div>
<div class="col-sm-10">
......@@ -288,8 +398,8 @@ export default {
data-supports-quick-actions="false"
data-qa-selector="wiki_content_textarea"
:autofocus="pageInfo.persisted"
:aria-label="s__('WikiPage|Content')"
:placeholder="s__('WikiPage|Write your content or drag files here…')"
:aria-label="$options.i18n.content.label"
:placeholder="$options.i18n.content.placeholder"
@input="handleContentChange"
>
</textarea>
......@@ -305,14 +415,8 @@ export default {
<div class="clearfix"></div>
<div class="error-alert"></div>
<div v-if="!isContentEditorActive" class="form-text gl-text-gray-600">
<gl-sprintf
:message="
s__(
'WikiPage|To link to a (new) page, simply type %{linkExample}. More examples are in the %{linkStart}documentation%{linkEnd}.',
)
"
>
<div class="form-text gl-text-gray-600">
<gl-sprintf v-if="!isContentEditorActive" :message="$options.i18n.linksHelpText">
<template #linkExample
><code>{{ linkExample }}</code></template
>
......@@ -327,13 +431,16 @@ export default {
></template
>
</gl-sprintf>
<span v-else>
{{ $options.i18n.contentEditor.helpText }}
</span>
</div>
</div>
</div>
<div class="form-group row">
<div class="col-sm-2 col-form-label">
<label class="control-label-full-width" for="wiki_message">{{
s__('WikiPage|Commit message')
$options.i18n.commitMessage.label
}}</label>
</div>
<div class="col-sm-10">
......@@ -344,7 +451,7 @@ export default {
type="text"
class="form-control"
data-qa-selector="wiki_message_textbox"
:placeholder="s__('WikiPage|Commit message')"
:placeholder="$options.i18n.commitMessage.label"
/>
</div>
</div>
......@@ -355,10 +462,10 @@ export default {
type="submit"
data-qa-selector="wiki_submit_button"
data-testid="wiki-submit-button"
:disabled="!content || !title"
:disabled="disableSubmitButton"
>{{ submitButtonText }}</gl-button
>
<gl-button :href="cancelFormPath" class="float-right">{{ s__('WikiPage|Cancel') }}</gl-button>
<gl-button :href="cancelFormPath" class="float-right">{{ $options.i18n.cancel }}</gl-button>
</div>
</gl-form>
</template>
......@@ -36742,6 +36742,12 @@ msgstr ""
msgid "WikiPageConflictMessage|Someone edited the page the same time you did. Please check out %{wikiLinkStart}the page%{wikiLinkEnd} and make sure your changes will not unintentionally remove theirs."
msgstr ""
msgid "WikiPage|An error occured while trying to render the content editor. Please try again later."
msgstr ""
msgid "WikiPage|Are you sure you want to switch to the old editor?"
msgstr ""
msgid "WikiPage|Cancel"
msgstr ""
......@@ -36760,12 +36766,18 @@ msgstr ""
msgid "WikiPage|Format"
msgstr ""
msgid "WikiPage|Keep editing"
msgstr ""
msgid "WikiPage|More Information."
msgstr ""
msgid "WikiPage|Page title"
msgstr ""
msgid "WikiPage|Retry"
msgstr ""
msgid "WikiPage|Save changes"
msgstr ""
......@@ -36775,6 +36787,12 @@ msgstr ""
msgid "WikiPage|Switching to the old editor will discard any changes you've made in the new editor."
msgstr ""
msgid "WikiPage|Switching will discard any changes you've made in the new editor."
msgstr ""
msgid "WikiPage|This editor is in beta and may not display the page's contents properly."
msgstr ""
msgid "WikiPage|Tip: You can move this page by adding the path to the beginning of the title."
msgstr ""
......@@ -36796,9 +36814,6 @@ msgstr ""
msgid "WikiPage|Write your content or drag files here…"
msgstr ""
msgid "WikiPage|You are editing this page with Content Editor. This editor is in beta and may not display the page's contents properly."
msgstr ""
msgid "Wikis"
msgstr ""
......
import { GlAlert, GlButton, GlLoadingIcon } from '@gitlab/ui';
import { GlLoadingIcon, GlModal } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
......@@ -10,6 +10,7 @@ import MarkdownField from '~/vue_shared/components/markdown/field.vue';
describe('WikiForm', () => {
let wrapper;
let mock;
const findForm = () => wrapper.find('form');
const findTitle = () => wrapper.find('#wiki_title');
......@@ -19,6 +20,8 @@ describe('WikiForm', () => {
const findSubmitButton = () => wrapper.findByTestId('wiki-submit-button');
const findCancelButton = () => wrapper.findByRole('link', { name: 'Cancel' });
const findUseNewEditorButton = () => wrapper.findByRole('button', { name: 'Use new editor' });
const findSwitchToOldEditorButton = () =>
wrapper.findByRole('button', { name: 'Switch to old editor' });
const findTitleHelpLink = () => wrapper.findByRole('link', { name: 'More Information.' });
const findMarkdownHelpLink = () => wrapper.findByTestId('wiki-markdown-help-link');
......@@ -84,7 +87,12 @@ describe('WikiForm', () => {
);
}
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
wrapper.destroy();
wrapper = null;
});
......@@ -266,7 +274,14 @@ describe('WikiForm', () => {
const assertOldEditorIsVisible = () => {
expect(wrapper.findComponent(ContentEditor).exists()).toBe(false);
expect(wrapper.findComponent(MarkdownField).exists()).toBe(true);
expect(wrapper.findComponent(GlAlert).exists()).toBe(false);
expect(findSubmitButton().props('disabled')).toBe(false);
expect(wrapper.text()).not.toContain(
"Switching will discard any changes you've made in the new editor.",
);
expect(wrapper.text()).not.toContain(
"This editor is in beta and may not display the page's contents properly.",
);
};
it('shows old editor by default', assertOldEditorIsVisible);
......@@ -294,29 +309,54 @@ describe('WikiForm', () => {
});
});
describe('clicking "use new editor"', () => {
let mock;
describe('clicking "use new editor": editor fails to load', () => {
beforeEach(async () => {
mock = new MockAdapter(axios);
mock.onPost(/preview-markdown/).reply(200, { body: '<p>hello <strong>world</strong></p>' });
mock.onPost(/preview-markdown/).reply(400);
findUseNewEditorButton().trigger('click');
await findUseNewEditorButton().trigger('click');
await wrapper.vm.$nextTick();
// try waiting for content editor to load (but it will never actually load)
await waitForPromises();
});
it('editor is shown in a perpetual loading state', () => {
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
expect(wrapper.findComponent(ContentEditor).exists()).toBe(false);
});
it('disables the submit button', () => {
expect(findSubmitButton().props('disabled')).toBe(true);
});
afterEach(() => {
mock.restore();
describe('clicking "switch to old editor"', () => {
beforeEach(() => {
return findSwitchToOldEditorButton().trigger('click');
});
it('switches to old editor directly without showing a modal', () => {
expect(wrapper.findComponent(ContentEditor).exists()).toBe(false);
expect(wrapper.findComponent(MarkdownField).exists()).toBe(true);
});
});
});
describe('clicking "use new editor": editor loads successfully', () => {
beforeEach(() => {
mock.onPost(/preview-markdown/).reply(200, { body: '<p>hello <strong>world</strong></p>' });
findUseNewEditorButton().trigger('click');
});
it('shows a loading indicator for the rich text editor', () => {
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
it('shows a warning alert that the rich text editor is in beta', () => {
expect(wrapper.findComponent(GlAlert).text()).toContain(
"You are editing this page with Content Editor. This editor is in beta and may not display the page's contents properly.",
it('shows warnings that the rich text editor is in beta and may not work properly', () => {
expect(wrapper.text()).toContain(
"Switching will discard any changes you've made in the new editor.",
);
expect(wrapper.text()).toContain(
"This editor is in beta and may not display the page's contents properly.",
);
});
......@@ -342,8 +382,6 @@ describe('WikiForm', () => {
true,
);
await waitForPromises();
return wrapper.vm.$nextTick();
});
......@@ -377,30 +415,45 @@ describe('WikiForm', () => {
});
describe('clicking "switch to old editor"', () => {
beforeEach(async () => {
// wait for content editor to load
await waitForPromises();
let modal;
wrapper.vm.contentEditor.tiptapEditor.commands.setContent(
'<p>hello __world__ from content editor</p>',
true,
);
wrapper.findComponent(GlAlert).findComponent(GlButton).trigger('click');
beforeEach(async () => {
modal = wrapper.findComponent(GlModal);
jest.spyOn(modal.vm, 'show');
await wrapper.vm.$nextTick();
findSwitchToOldEditorButton().trigger('click');
});
it('switches to old editor', () => {
expect(wrapper.findComponent(ContentEditor).exists()).toBe(false);
expect(wrapper.findComponent(MarkdownField).exists()).toBe(true);
it('shows a modal confirming the change', () => {
expect(modal.vm.show).toHaveBeenCalled();
});
it('does not show a warning alert about content editor', () => {
expect(wrapper.findComponent(GlAlert).exists()).toBe(false);
});
describe('confirming "switch to old editor" in the modal', () => {
beforeEach(async () => {
wrapper.vm.contentEditor.tiptapEditor.commands.setContent(
'<p>hello __world__ from content editor</p>',
true,
);
wrapper.findComponent(GlModal).vm.$emit('primary');
await wrapper.vm.$nextTick();
});
it('switches to old editor', () => {
expect(wrapper.findComponent(ContentEditor).exists()).toBe(false);
expect(wrapper.findComponent(MarkdownField).exists()).toBe(true);
});
it('does not show a warning about content editor', () => {
expect(wrapper.text()).not.toContain(
"This editor is in beta and may not display the page's contents properly.",
);
});
it('the old editor retains its old value and does not use the content from the content editor', () => {
expect(findContent().element.value).toBe('My page content');
it('the old editor retains its old value and does not use the content from the content editor', () => {
expect(findContent().element.value).toBe('My page content');
});
});
});
});
......
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