diff --git a/app/assets/javascripts/content_editor/components/bubble_menus/bubble_menu.vue b/app/assets/javascripts/content_editor/components/bubble_menus/bubble_menu.vue index 4d123cfb2b0f103251f4ef78375d7c5a402562b5..9afc2e449d5a99fce3daadb37a2874a93eec5c6d 100644 --- a/app/assets/javascripts/content_editor/components/bubble_menus/bubble_menu.vue +++ b/app/assets/javascripts/content_editor/components/bubble_menus/bubble_menu.vue @@ -34,7 +34,6 @@ export default { element: this.$el, shouldShow: this.shouldShow, tippyOptions: { - ...this.tippyOptions, onShow: (...args) => { this.$emit('show', ...args); this.menuVisible = true; @@ -47,6 +46,7 @@ export default { strategy: 'fixed', }, maxWidth: '400px', + ...this.tippyOptions, }, }), ); diff --git a/app/assets/javascripts/content_editor/components/bubble_menus/glql_bubble_menu.vue b/app/assets/javascripts/content_editor/components/bubble_menus/glql_bubble_menu.vue new file mode 100644 index 0000000000000000000000000000000000000000..4f3496331f126df78073e16152cc4b97d5fa79f7 --- /dev/null +++ b/app/assets/javascripts/content_editor/components/bubble_menus/glql_bubble_menu.vue @@ -0,0 +1,146 @@ + + diff --git a/app/assets/javascripts/content_editor/components/content_editor.vue b/app/assets/javascripts/content_editor/components/content_editor.vue index 29f5adb7df6b217f0e969ceb84edd3f193e8c1d0..542fbbb6d94b8bf70dcc38cec251439694426c51 100644 --- a/app/assets/javascripts/content_editor/components/content_editor.vue +++ b/app/assets/javascripts/content_editor/components/content_editor.vue @@ -20,6 +20,7 @@ import LinkBubbleMenu from './bubble_menus/link_bubble_menu.vue'; import MediaBubbleMenu from './bubble_menus/media_bubble_menu.vue'; import ReferenceBubbleMenu from './bubble_menus/reference_bubble_menu.vue'; import TableBubbleMenu from './bubble_menus/table_bubble_menu.vue'; +import GlqlBubbleMenu from './bubble_menus/glql_bubble_menu.vue'; import FormattingToolbar from './formatting_toolbar.vue'; export default { @@ -37,6 +38,7 @@ export default { EditorStateObserver, ReferenceBubbleMenu, TableBubbleMenu, + GlqlBubbleMenu, EditorModeSwitcher, }, directives: { @@ -302,6 +304,7 @@ export default { +
+import { NodeViewWrapper } from '@tiptap/vue-2'; +import GlqlFacade from '~/glql/components/common/facade.vue'; + +export default { + name: 'GlqlViewWrapper', + components: { + NodeViewWrapper, + GlqlFacade, + }, + props: { + node: { + type: Object, + required: true, + }, + selected: { + type: Boolean, + required: true, + }, + }, +}; + + diff --git a/app/assets/javascripts/content_editor/extensions/glql/view.js b/app/assets/javascripts/content_editor/extensions/glql/view.js new file mode 100644 index 0000000000000000000000000000000000000000..4dfc2b2f845af4951dec8541ddbdd31198a5abc9 --- /dev/null +++ b/app/assets/javascripts/content_editor/extensions/glql/view.js @@ -0,0 +1,42 @@ +import { Node } from '@tiptap/core'; +import { VueNodeViewRenderer } from '@tiptap/vue-2'; +import { parseYAML } from '~/glql/core/parser'; +import { PARSE_HTML_PRIORITY_HIGHEST } from '../../constants'; +import GlqlViewWrapper from '../../components/wrappers/glql/view.vue'; + +export default Node.create({ + name: 'glqlView', + group: 'block', + atom: true, + marks: '', + + addAttributes() { + return { + queryYaml: { default: '' }, + query: { default: '' }, + config: { default: {} }, + }; + }, + + parseHTML() { + return [ + { + priority: PARSE_HTML_PRIORITY_HIGHEST, + tag: 'pre[data-canonical-lang="glql"]', + getAttrs: (el) => ({ + queryYaml: el.textContent, + ...parseYAML(el.textContent), + }), + }, + { tag: 'glql-view' }, + ]; + }, + + renderHTML() { + return ['div', {}, 0]; + }, + + addNodeView() { + return VueNodeViewRenderer(GlqlViewWrapper); + }, +}); diff --git a/app/assets/javascripts/content_editor/extensions/index.js b/app/assets/javascripts/content_editor/extensions/index.js index 453ef011e32bbd6ddb1636c56a4890a9c93b3be5..f59018f8b8f171cf934c9ffd9f265b4864ac3d57 100644 --- a/app/assets/javascripts/content_editor/extensions/index.js +++ b/app/assets/javascripts/content_editor/extensions/index.js @@ -65,3 +65,5 @@ export { default as Text } from './text'; export { default as Time } from './time'; export { default as Video } from './video'; export { default as WordBreak } from './word_break'; + +export { default as GlqlView } from './glql/view'; diff --git a/app/assets/javascripts/content_editor/services/markdown_serializer.js b/app/assets/javascripts/content_editor/services/markdown_serializer.js index 9de79d050237f1ad3f247f1107c7a9ebbf6b70b4..b0a1921f56037d65bd89921cfbb45995c82725fb 100644 --- a/app/assets/javascripts/content_editor/services/markdown_serializer.js +++ b/app/assets/javascripts/content_editor/services/markdown_serializer.js @@ -54,6 +54,7 @@ import tableRow from './serializer/table_row'; import table from './serializer/table'; import time from './serializer/time'; import htmlNode from './serializer/html_node'; +import glqlView from './serializer/glql/view'; const LIST_TYPES = [ extensions.BulletList.name, @@ -120,6 +121,8 @@ const defaultSerializerConfig = { [extensions.Video.name]: video, [extensions.WordBreak.name]: wordBreak, ...extensions.HTMLNodes.reduce((acc, { name }) => ({ ...acc, [name]: htmlNode(name) }), {}), + + [extensions.GlqlView.name]: glqlView, }, }; diff --git a/app/assets/javascripts/content_editor/services/serializer/glql/view.js b/app/assets/javascripts/content_editor/services/serializer/glql/view.js new file mode 100644 index 0000000000000000000000000000000000000000..72bcbdcdd7618e40852f067c0e6484d5548c92ba --- /dev/null +++ b/app/assets/javascripts/content_editor/services/serializer/glql/view.js @@ -0,0 +1,11 @@ +const glqlView = (state, node) => { + if (state.options.skipEmptyNodes && !node.childCount) return; + + state.write(`\`\`\`glql\n`); + state.text(node.attrs.query, false); + state.ensureNewLine(); + state.write('```'); + state.closeBlock(node); +}; + +export default glqlView; diff --git a/app/assets/javascripts/glql/components/common/facade.vue b/app/assets/javascripts/glql/components/common/facade.vue index a0417322bc77c77b66bffc4cd2771ad3417fc2f9..2ac8a7838a0d71740a6f0ebab0a393e1e771928e 100644 --- a/app/assets/javascripts/glql/components/common/facade.vue +++ b/app/assets/javascripts/glql/components/common/facade.vue @@ -47,7 +47,11 @@ export default { SafeHtml, }, mixins: [InternalEvents.mixin(), glFeatureFlagsMixin()], - inject: ['queryKey'], + inject: { + queryKey: { + default: '', + }, + }, props: { query: { required: true, @@ -120,7 +124,7 @@ export default { async mounted() { this.loadOnClick = this.glFeatures.glqlLoadOnClick; - this.eventHub.$on('loadMore', this.loadMore.bind(this)); + this.eventHub?.$on('loadMore', this.loadMore.bind(this)); }, methods: { diff --git a/app/assets/javascripts/glql/components/common/pagination.vue b/app/assets/javascripts/glql/components/common/pagination.vue index 9452c8fdd32b530ca19fb4b7c1315f83e587eb0f..8c44bfad097979a1c32fef8446422f11ac58fcf3 100644 --- a/app/assets/javascripts/glql/components/common/pagination.vue +++ b/app/assets/javascripts/glql/components/common/pagination.vue @@ -9,7 +9,11 @@ export default { components: { GlButton, }, - inject: ['queryKey'], + inject: { + queryKey: { + default: '', + }, + }, props: { count: { type: Number, @@ -47,21 +51,21 @@ export default { }, mounted() { - this.eventHub.$on('loadMore', () => { + this.eventHub?.$on('loadMore', () => { this.isLoadingMore = true; }); - this.eventHub.$on('loadMoreComplete', () => { + this.eventHub?.$on('loadMoreComplete', () => { this.isLoadingMore = false; }); - this.eventHub.$on('loadMoreError', () => { + this.eventHub?.$on('loadMoreError', () => { this.isLoadingMore = false; }); }, methods: { loadMore() { - this.eventHub.$emit('loadMore', this.actualPageSize); + this.eventHub?.$emit('loadMore', this.actualPageSize); }, }, }; @@ -76,6 +80,7 @@ export default { variant="default" :aria-label="loadMoreLabel" :loading="isLoadingMore" + :disabled="!eventHub" @click="loadMore" > {{ loadMoreLabel }} diff --git a/app/assets/javascripts/glql/components/presenters/list.vue b/app/assets/javascripts/glql/components/presenters/list.vue index 1e1d135beb96fa56946b30732d9a1e46d7f6303f..4abf5c356bed0dcb3aacc76bdcee1c5a82766fd7 100644 --- a/app/assets/javascripts/glql/components/presenters/list.vue +++ b/app/assets/javascripts/glql/components/presenters/list.vue @@ -11,7 +11,14 @@ export default { GlSprintf, GlSkeletonLoader, }, - inject: ['presenter', 'queryKey'], + inject: { + presenter: { + default: null, + }, + queryKey: { + default: '', + }, + }, props: { data: { required: true, @@ -51,16 +58,16 @@ export default { }, }, mounted() { - this.eventHub.$on('loadMore', (pageSize) => { + this.eventHub?.$on('loadMore', (pageSize) => { this.pageSize = pageSize; this.isLoadingMore = true; }); - this.eventHub.$on('loadMoreComplete', () => { + this.eventHub?.$on('loadMoreComplete', () => { this.isLoadingMore = false; }); - this.eventHub.$on('loadMoreError', () => { + this.eventHub?.$on('loadMoreError', () => { this.isLoadingMore = false; }); }, diff --git a/app/assets/javascripts/glql/components/presenters/table.vue b/app/assets/javascripts/glql/components/presenters/table.vue index b4ae5b3c507c8a493c28cbe678bedaf3e7e29b5b..4dc76afab7594ea4e3a3aaa18f639a12ffaa1e83 100644 --- a/app/assets/javascripts/glql/components/presenters/table.vue +++ b/app/assets/javascripts/glql/components/presenters/table.vue @@ -13,7 +13,14 @@ export default { GlSkeletonLoader, ThResizable, }, - inject: ['presenter', 'queryKey'], + inject: { + presenter: { + default: null, + }, + queryKey: { + default: '', + }, + }, props: { data: { required: true, @@ -44,18 +51,18 @@ export default { }; }, mounted() { - this.eventHub.$on('loadMore', (pageSize) => { + this.eventHub?.$on('loadMore', (pageSize) => { this.pageSize = pageSize; this.isLoadingMore = true; }); - this.eventHub.$on('loadMoreComplete', (newData) => { + this.eventHub?.$on('loadMoreComplete', (newData) => { this.items = newData.nodes.slice(); this.sorter = this.sorter.clone(this.items); this.isLoadingMore = false; }); - this.eventHub.$on('loadMoreError', () => { + this.eventHub?.$on('loadMoreError', () => { this.isLoadingMore = false; }); }, diff --git a/app/assets/javascripts/glql/utils/event_hub_factory.js b/app/assets/javascripts/glql/utils/event_hub_factory.js index dc573a059604d661a46786e368667283ab469bfc..04ec6b0722a926243e568010f7bda2d2bbe72e1a 100644 --- a/app/assets/javascripts/glql/utils/event_hub_factory.js +++ b/app/assets/javascripts/glql/utils/event_hub_factory.js @@ -3,6 +3,8 @@ import createEventHub from '~/helpers/event_hub_factory'; const eventHubs = {}; export const eventHubByKey = (key) => { + if (!key) return null; + eventHubs[key] = eventHubs[key] || createEventHub(); return eventHubs[key]; }; diff --git a/app/assets/stylesheets/components/content_editor.scss b/app/assets/stylesheets/components/content_editor.scss index 5b7142d5694f8267a87424932af9a292f2760a0a..d6def65666d0e055bd241114b9d0d4b923cc5699 100644 --- a/app/assets/stylesheets/components/content_editor.scss +++ b/app/assets/stylesheets/components/content_editor.scss @@ -326,6 +326,10 @@ @apply gl-mb-0; } } + + .glql-view { + --gl-focus-ring-outer-color: var(--blue-200); + } } // Fixes a problem with the layout shifting @@ -406,3 +410,26 @@ min-width: 320px; } +.glql-bubble-menu { + --gl-control-border-color-default: transparent; + --gl-control-border-color-hover: transparent; + --gl-control-border-color-focus: transparent; + + --gl-focus-ring-inner-color: transparent; + --gl-focus-ring-outer-color: transparent; +} + +.glql-filtered-search { + .gl-filtered-search-scrollable { + @apply gl-flex-wrap gl-gap-2; + } + + .gl-filtered-search-scrollable-container { + @apply gl-px-0; + } + + .gl-filtered-search-item { + @apply gl-p-0; + } +} +