summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/static_site_editor
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/static_site_editor')
-rw-r--r--app/assets/javascripts/static_site_editor/components/app.vue12
-rw-r--r--app/assets/javascripts/static_site_editor/components/edit_area.vue18
-rw-r--r--app/assets/javascripts/static_site_editor/components/saved_changes_message.vue79
-rw-r--r--app/assets/javascripts/static_site_editor/image_repository.js2
-rw-r--r--app/assets/javascripts/static_site_editor/index.js15
-rw-r--r--app/assets/javascripts/static_site_editor/pages/home.vue2
-rw-r--r--app/assets/javascripts/static_site_editor/pages/success.vue72
-rw-r--r--app/assets/javascripts/static_site_editor/services/formatter.js14
-rw-r--r--app/assets/javascripts/static_site_editor/services/templater.js89
9 files changed, 207 insertions, 96 deletions
diff --git a/app/assets/javascripts/static_site_editor/components/app.vue b/app/assets/javascripts/static_site_editor/components/app.vue
index 98240aef810..365fc7ce6e9 100644
--- a/app/assets/javascripts/static_site_editor/components/app.vue
+++ b/app/assets/javascripts/static_site_editor/components/app.vue
@@ -1,3 +1,13 @@
+<script>
+export default {
+ props: {
+ mergeRequestsIllustrationPath: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
<template>
- <router-view />
+ <router-view :merge-requests-illustration-path="mergeRequestsIllustrationPath" />
</template>
diff --git a/app/assets/javascripts/static_site_editor/components/edit_area.vue b/app/assets/javascripts/static_site_editor/components/edit_area.vue
index 84a16f327d9..53fbb2a330d 100644
--- a/app/assets/javascripts/static_site_editor/components/edit_area.vue
+++ b/app/assets/javascripts/static_site_editor/components/edit_area.vue
@@ -7,6 +7,8 @@ import parseSourceFile from '~/static_site_editor/services/parse_source_file';
import { EDITOR_TYPES } from '~/vue_shared/components/rich_content_editor/constants';
import { DEFAULT_IMAGE_UPLOAD_PATH } from '../constants';
import imageRepository from '../image_repository';
+import formatter from '../services/formatter';
+import templater from '../services/templater';
export default {
components: {
@@ -43,7 +45,7 @@ export default {
data() {
return {
saveable: false,
- parsedSource: parseSourceFile(this.content),
+ parsedSource: parseSourceFile(this.preProcess(true, this.content)),
editorMode: EDITOR_TYPES.wysiwyg,
isModified: false,
};
@@ -58,20 +60,30 @@ export default {
},
},
methods: {
+ preProcess(isWrap, value) {
+ const formattedContent = formatter(value);
+ const templatedContent = isWrap
+ ? templater.wrap(formattedContent)
+ : templater.unwrap(formattedContent);
+ return templatedContent;
+ },
onInputChange(newVal) {
this.parsedSource.sync(newVal, this.isWysiwygMode);
this.isModified = this.parsedSource.isModified();
},
onModeChange(mode) {
this.editorMode = mode;
- this.$refs.editor.resetInitialValue(this.editableContent);
+
+ const preProcessedContent = this.preProcess(this.isWysiwygMode, this.editableContent);
+ this.$refs.editor.resetInitialValue(preProcessedContent);
},
onUploadImage({ file, imageUrl }) {
this.$options.imageRepository.add(file, imageUrl);
},
onSubmit() {
+ const preProcessedContent = this.preProcess(false, this.parsedSource.content());
this.$emit('submit', {
- content: this.parsedSource.content(),
+ content: preProcessedContent,
images: this.$options.imageRepository.getAll(),
});
},
diff --git a/app/assets/javascripts/static_site_editor/components/saved_changes_message.vue b/app/assets/javascripts/static_site_editor/components/saved_changes_message.vue
deleted file mode 100644
index dd907570114..00000000000
--- a/app/assets/javascripts/static_site_editor/components/saved_changes_message.vue
+++ /dev/null
@@ -1,79 +0,0 @@
-<script>
-import { isString } from 'lodash';
-
-import { GlLink, GlButton } from '@gitlab/ui';
-
-const validateUrlAndLabel = value => isString(value.label) && isString(value.url);
-
-export default {
- components: {
- GlLink,
- GlButton,
- },
- props: {
- branch: {
- type: Object,
- required: true,
- validator: validateUrlAndLabel,
- },
- commit: {
- type: Object,
- required: true,
- validator: validateUrlAndLabel,
- },
- mergeRequest: {
- type: Object,
- required: true,
- validator: validateUrlAndLabel,
- },
- returnUrl: {
- type: String,
- required: false,
- default: '',
- },
- },
-};
-</script>
-
-<template>
- <div>
- <div class="border-bottom pb-4">
- <h3>{{ s__('StaticSiteEditor|Success!') }}</h3>
- <p>
- {{
- s__(
- 'StaticSiteEditor|Your changes have been submitted and a merge request has been created. The changes won’t be visible on the site until the merge request has been accepted.',
- )
- }}
- </p>
- <div class="d-flex justify-content-end">
- <gl-button v-if="returnUrl" ref="returnToSiteButton" :href="returnUrl">{{
- s__('StaticSiteEditor|Return to site')
- }}</gl-button>
- <gl-button ref="mergeRequestButton" class="ml-2" :href="mergeRequest.url" variant="success">
- {{ s__('StaticSiteEditor|View merge request') }}
- </gl-button>
- </div>
- </div>
-
- <div class="pt-2">
- <h4>{{ s__('StaticSiteEditor|Summary of changes') }}</h4>
- <ul>
- <li>
- {{ s__('StaticSiteEditor|You created a new branch:') }}
- <gl-link ref="branchLink" :href="branch.url">{{ branch.label }}</gl-link>
- </li>
- <li>
- {{ s__('StaticSiteEditor|You created a merge request:') }}
- <gl-link ref="mergeRequestLink" :href="mergeRequest.url">{{
- mergeRequest.label
- }}</gl-link>
- </li>
- <li>
- {{ s__('StaticSiteEditor|You added a commit:') }}
- <gl-link ref="commitLink" :href="commit.url">{{ commit.label }}</gl-link>
- </li>
- </ul>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/static_site_editor/image_repository.js b/app/assets/javascripts/static_site_editor/image_repository.js
index 541d581bda8..02285ccdba3 100644
--- a/app/assets/javascripts/static_site_editor/image_repository.js
+++ b/app/assets/javascripts/static_site_editor/image_repository.js
@@ -1,5 +1,5 @@
import { __ } from '~/locale';
-import Flash from '~/flash';
+import { deprecatedCreateFlash as Flash } from '~/flash';
import { getBinary } from './services/image_service';
const imageRepository = () => {
diff --git a/app/assets/javascripts/static_site_editor/index.js b/app/assets/javascripts/static_site_editor/index.js
index 12aa301e02f..b7e5ea4eee3 100644
--- a/app/assets/javascripts/static_site_editor/index.js
+++ b/app/assets/javascripts/static_site_editor/index.js
@@ -5,7 +5,14 @@ import createRouter from './router';
import createApolloProvider from './graphql';
const initStaticSiteEditor = el => {
- const { isSupportedContent, path: sourcePath, baseUrl, namespace, project } = el.dataset;
+ const {
+ isSupportedContent,
+ path: sourcePath,
+ baseUrl,
+ namespace,
+ project,
+ mergeRequestsIllustrationPath,
+ } = el.dataset;
const { current_username: username } = window.gon;
const returnUrl = el.dataset.returnUrl || null;
@@ -26,7 +33,11 @@ const initStaticSiteEditor = el => {
App,
},
render(createElement) {
- return createElement('app');
+ return createElement('app', {
+ props: {
+ mergeRequestsIllustrationPath,
+ },
+ });
},
});
};
diff --git a/app/assets/javascripts/static_site_editor/pages/home.vue b/app/assets/javascripts/static_site_editor/pages/home.vue
index 156b815e07a..eef2bd88f0e 100644
--- a/app/assets/javascripts/static_site_editor/pages/home.vue
+++ b/app/assets/javascripts/static_site_editor/pages/home.vue
@@ -6,7 +6,7 @@ import SubmitChangesError from '../components/submit_changes_error.vue';
import appDataQuery from '../graphql/queries/app_data.query.graphql';
import sourceContentQuery from '../graphql/queries/source_content.query.graphql';
import submitContentChangesMutation from '../graphql/mutations/submit_content_changes.mutation.graphql';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import Tracking from '~/tracking';
import { LOAD_CONTENT_ERROR, TRACKING_ACTION_INITIALIZE_EDITOR } from '../constants';
import { SUCCESS_ROUTE } from '../router/constants';
diff --git a/app/assets/javascripts/static_site_editor/pages/success.vue b/app/assets/javascripts/static_site_editor/pages/success.vue
index 123683b2833..f0d597d7c9b 100644
--- a/app/assets/javascripts/static_site_editor/pages/success.vue
+++ b/app/assets/javascripts/static_site_editor/pages/success.vue
@@ -1,12 +1,21 @@
<script>
+import { GlEmptyState, GlButton } from '@gitlab/ui';
+import { s__, __, sprintf } from '~/locale';
+
import savedContentMetaQuery from '../graphql/queries/saved_content_meta.query.graphql';
import appDataQuery from '../graphql/queries/app_data.query.graphql';
-import SavedChangesMessage from '../components/saved_changes_message.vue';
import { HOME_ROUTE } from '../router/constants';
export default {
components: {
- SavedChangesMessage,
+ GlEmptyState,
+ GlButton,
+ },
+ props: {
+ mergeRequestsIllustrationPath: {
+ type: String,
+ required: true,
+ },
},
apollo: {
savedContentMeta: {
@@ -16,20 +25,65 @@ export default {
query: appDataQuery,
},
},
+ computed: {
+ updatedFileDescription() {
+ const { sourcePath } = this.appData;
+
+ return sprintf(s__('Update %{sourcePath} file'), { sourcePath });
+ },
+ },
created() {
if (!this.savedContentMeta) {
this.$router.push(HOME_ROUTE);
}
},
+ title: s__('StaticSiteEditor|Your merge request has been created'),
+ primaryButtonText: __('View merge request'),
+ returnToSiteBtnText: s__('StaticSiteEditor|Return to site'),
+ mergeRequestInstructionsHeading: s__(
+ 'StaticSiteEditor|To see your changes live you will need to do the following things:',
+ ),
+ addTitleInstruction: s__('StaticSiteEditor|1. Add a clear title to describe the change.'),
+ addDescriptionInstruction: s__(
+ 'StaticSiteEditor|2. Add a description to explain why the change is being made.',
+ ),
+ assignMergeRequestInstruction: s__(
+ 'StaticSiteEditor|3. Assign a person to review and accept the merge request.',
+ ),
};
</script>
<template>
- <div v-if="savedContentMeta" class="container">
- <saved-changes-message
- :branch="savedContentMeta.branch"
- :commit="savedContentMeta.commit"
- :merge-request="savedContentMeta.mergeRequest"
- :return-url="appData.returnUrl"
- />
+ <div
+ v-if="savedContentMeta"
+ class="container gl-flex-grow-1 gl-display-flex gl-flex-direction-column"
+ >
+ <div class="gl-fixed gl-left-0 gl-right-0 gl-border-b-solid gl-border-b-1 gl-border-b-gray-100">
+ <div class="container gl-py-4">
+ <gl-button
+ v-if="appData.returnUrl"
+ ref="returnToSiteButton"
+ class="gl-mr-5"
+ :href="appData.returnUrl"
+ >{{ $options.returnToSiteBtnText }}</gl-button
+ >
+ <strong>
+ {{ updatedFileDescription }}
+ </strong>
+ </div>
+ </div>
+ <gl-empty-state
+ class="gl-my-9"
+ :primary-button-text="$options.primaryButtonText"
+ :title="$options.title"
+ :primary-button-link="savedContentMeta.mergeRequest.url"
+ :svg-path="mergeRequestsIllustrationPath"
+ >
+ <template #description>
+ <p>{{ $options.mergeRequestInstructionsHeading }}</p>
+ <p>{{ $options.addTitleInstruction }}</p>
+ <p>{{ $options.addDescriptionInstruction }}</p>
+ <p>{{ $options.assignMergeRequestInstruction }}</p>
+ </template>
+ </gl-empty-state>
</div>
</template>
diff --git a/app/assets/javascripts/static_site_editor/services/formatter.js b/app/assets/javascripts/static_site_editor/services/formatter.js
new file mode 100644
index 00000000000..92d5e8a5df8
--- /dev/null
+++ b/app/assets/javascripts/static_site_editor/services/formatter.js
@@ -0,0 +1,14 @@
+const removeOrphanedBrTags = source => {
+ /* Until the underlying Squire editor of Toast UI Editor resolves duplicate `<br>` tags, this
+ `replace` solution will clear out orphaned `<br>` tags that it generates. Additionally,
+ it cleans up orphaned `<br>` tags in the source markdown document that should be new lines.
+ https://gitlab.com/gitlab-org/gitlab/-/issues/227602#note_380765330
+ */
+ return source.replace(/\n^<br>$/gm, '');
+};
+
+const format = source => {
+ return removeOrphanedBrTags(source);
+};
+
+export default format;
diff --git a/app/assets/javascripts/static_site_editor/services/templater.js b/app/assets/javascripts/static_site_editor/services/templater.js
new file mode 100644
index 00000000000..a1c1bb6b8d6
--- /dev/null
+++ b/app/assets/javascripts/static_site_editor/services/templater.js
@@ -0,0 +1,89 @@
+/**
+ * The purpose of this file is to modify Markdown source such that templated code (embedded ruby currently) can be temporarily wrapped and unwrapped in codeblocks:
+ * 1. `wrap()`: temporarily wrap in codeblocks (useful for a WYSIWYG editing experience)
+ * 2. `unwrap()`: undo the temporarily wrapped codeblocks (useful for Markdown editing experience and saving edits)
+ *
+ * Without this `templater`, the templated code is otherwise interpreted as Markdown content resulting in loss of spacing, indentation, escape characters, etc.
+ *
+ */
+
+const ticks = '```';
+const marker = 'sse';
+const wrapPrefix = `${ticks} ${marker}\n`; // Space intentional due to https://github.com/nhn/tui.editor/blob/6bcec75c69028570d93d973aa7533090257eaae0/libs/to-mark/src/renderer.gfm.js#L26
+const wrapPostfix = `\n${ticks}`;
+const markPrefix = `${marker}-${Date.now()}`;
+
+const reHelpers = {
+ template: `.| |\\t|\\n(?!(\\n|${markPrefix}))`,
+ openTag: '<[a-zA-Z]+.*?>',
+ closeTag: '</.+>',
+};
+const reTemplated = new RegExp(`(^${wrapPrefix}(${reHelpers.template})+?${wrapPostfix}$)`, 'gm');
+const rePreexistingCodeBlocks = new RegExp(`(^${ticks}.*\\n(.|\\s)+?${ticks}$)`, 'gm');
+const reHtmlMarkup = new RegExp(
+ `^((${reHelpers.openTag}){1}(${reHelpers.template})*(${reHelpers.closeTag}){1})$`,
+ 'gm',
+);
+const reEmbeddedRubyBlock = new RegExp(`(^<%(${reHelpers.template})+%>$)`, 'gm');
+const reEmbeddedRubyInline = new RegExp(`(^.*[<|&lt;]%(${reHelpers.template})+$)`, 'gm');
+
+const patternGroups = {
+ ignore: [rePreexistingCodeBlocks],
+ // Order is intentional (general to specific) where HTML markup is marked first, then ERB blocks, then inline ERB
+ // Order in combo with the `mark()` algorithm is used to mitigate potential duplicate pattern matches (ERB nested in HTML for example)
+ allow: [reHtmlMarkup, reEmbeddedRubyBlock, reEmbeddedRubyInline],
+};
+
+const mark = (source, groups) => {
+ let text = source;
+ let id = 0;
+ const hash = {};
+
+ Object.entries(groups).forEach(([groupKey, group]) => {
+ group.forEach(pattern => {
+ const matches = text.match(pattern);
+ if (matches) {
+ matches.forEach(match => {
+ const key = `${markPrefix}-${groupKey}-${id}`;
+ text = text.replace(match, key);
+ hash[key] = match;
+ id += 1;
+ });
+ }
+ });
+ });
+
+ return { text, hash };
+};
+
+const unmark = (text, hash) => {
+ let source = text;
+
+ Object.entries(hash).forEach(([key, value]) => {
+ const newVal = key.includes('ignore') ? value : `${wrapPrefix}${value}${wrapPostfix}`;
+ source = source.replace(key, newVal);
+ });
+
+ return source;
+};
+
+const unwrap = source => {
+ let text = source;
+ const matches = text.match(reTemplated);
+
+ if (matches) {
+ matches.forEach(match => {
+ const initial = match.replace(`${wrapPrefix}`, '').replace(`${wrapPostfix}`, '');
+ text = text.replace(match, initial);
+ });
+ }
+
+ return text;
+};
+
+const wrap = source => {
+ const { text, hash } = mark(unwrap(source), patternGroups);
+ return unmark(text, hash);
+};
+
+export default { wrap, unwrap };