From 77c1dccb8d14c76418688d1657831954206ca6eb Mon Sep 17 00:00:00 2001 From: Himanshu Kapoor Date: Fri, 18 Jul 2025 01:10:04 +0200 Subject: [PATCH] wip --- .../components/bubble_menus/bubble_menu.vue | 2 +- .../bubble_menus/glql_bubble_menu.vue | 146 ++++++++++++++++++ .../components/content_editor.vue | 3 + .../components/wrappers/glql/view.vue | 31 ++++ .../content_editor/extensions/glql/view.js | 42 +++++ .../content_editor/extensions/index.js | 2 + .../services/markdown_serializer.js | 3 + .../services/serializer/glql/view.js | 11 ++ .../glql/components/common/facade.vue | 8 +- .../glql/components/common/pagination.vue | 15 +- .../glql/components/presenters/list.vue | 15 +- .../glql/components/presenters/table.vue | 15 +- .../glql/utils/event_hub_factory.js | 2 + .../components/content_editor.scss | 27 ++++ 14 files changed, 306 insertions(+), 16 deletions(-) create mode 100644 app/assets/javascripts/content_editor/components/bubble_menus/glql_bubble_menu.vue create mode 100644 app/assets/javascripts/content_editor/components/wrappers/glql/view.vue create mode 100644 app/assets/javascripts/content_editor/extensions/glql/view.js create mode 100644 app/assets/javascripts/content_editor/services/serializer/glql/view.js 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 4d123cfb2b0f10..9afc2e449d5a99 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 00000000000000..4f3496331f126d --- /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 29f5adb7df6b21..542fbbb6d94b8b 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 00000000000000..4dfc2b2f845af4 --- /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 453ef011e32bbd..f59018f8b8f171 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 9de79d050237f1..b0a1921f56037d 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 00000000000000..72bcbdcdd7618e --- /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 a0417322bc77c7..2ac8a7838a0d71 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 9452c8fdd32b53..8c44bfad097979 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 1e1d135beb96fa..4abf5c356bed0d 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 b4ae5b3c507c8a..4dc76afab7594e 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 dc573a059604d6..04ec6b0722a926 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 5b7142d5694f82..d6def65666d0e0 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; + } +} + -- GitLab