diff options
Diffstat (limited to 'app/assets/javascripts')
39 files changed, 545 insertions, 130 deletions
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 8d46ea76be1..e7a5f5ecc06 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -1,4 +1,4 @@ -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; import { __ } from '~/locale'; import axios from './lib/utils/axios_utils'; import { joinPaths } from './lib/utils/url_utility'; @@ -464,7 +464,7 @@ const Api = { throw error; } - createFlash({ + createAlert({ message: __('Something went wrong while fetching projects'), }); @@ -654,7 +654,7 @@ const Api = { }) .then(({ data }) => callback(data)) .catch(() => - createFlash({ + createAlert({ message: __('Something went wrong while fetching projects'), }), ); diff --git a/app/assets/javascripts/behaviors/components/sandboxed_mermaid.vue b/app/assets/javascripts/behaviors/components/sandboxed_mermaid.vue new file mode 100644 index 00000000000..6b4110cff02 --- /dev/null +++ b/app/assets/javascripts/behaviors/components/sandboxed_mermaid.vue @@ -0,0 +1,77 @@ +<script> +import { + getSandboxFrameSrc, + BUFFER_IFRAME_HEIGHT, + SANDBOX_ATTRIBUTES, +} from '../markdown/render_sandboxed_mermaid'; + +export default { + name: 'SandboxedMermaid', + + props: { + source: { + type: String, + required: true, + }, + }, + + data() { + return { + iframeHeight: BUFFER_IFRAME_HEIGHT, + sandboxFrameSrc: getSandboxFrameSrc(), + }; + }, + + watch: { + source() { + this.updateDiagram(); + }, + }, + + mounted() { + window.addEventListener('message', this.onPostMessage, false); + }, + + destroyed() { + window.removeEventListener('message', this.onPostMessage); + }, + + methods: { + getSandboxFrameSrc, + + onPostMessage(event) { + const container = this.$refs.diagramContainer; + + if (event.source === container?.contentWindow) { + this.iframeHeight = Number(event.data.h) + BUFFER_IFRAME_HEIGHT; + } + }, + + updateDiagram() { + const container = this.$refs.diagramContainer; + + // Potential risk associated with '*' discussed in below thread + // https://gitlab.com/gitlab-org/gitlab/-/merge_requests/74414#note_735183398 + container.contentWindow?.postMessage(this.source, '*'); + container.addEventListener('load', () => { + container.contentWindow?.postMessage(this.source, '*'); + }); + }, + }, + + sandboxFrameSrc: getSandboxFrameSrc(), + sandboxAttributes: SANDBOX_ATTRIBUTES, +}; +</script> +<template> + <iframe + ref="diagramContainer" + :src="$options.sandboxFrameSrc" + :sandbox="$options.sandboxAttributes" + frameborder="0" + scrolling="no" + width="100%" + :height="iframeHeight" + > + </iframe> +</template> diff --git a/app/assets/javascripts/behaviors/markdown/render_sandboxed_mermaid.js b/app/assets/javascripts/behaviors/markdown/render_sandboxed_mermaid.js index 543e676e85e..077e96b2fee 100644 --- a/app/assets/javascripts/behaviors/markdown/render_sandboxed_mermaid.js +++ b/app/assets/javascripts/behaviors/markdown/render_sandboxed_mermaid.js @@ -32,7 +32,8 @@ const MAX_CHAR_LIMIT = 2000; const MAX_MERMAID_BLOCK_LIMIT = 50; // Max # of `&` allowed in Chaining of links syntax const MAX_CHAINING_OF_LINKS_LIMIT = 30; -const BUFFER_IFRAME_HEIGHT = 10; +export const BUFFER_IFRAME_HEIGHT = 10; +export const SANDBOX_ATTRIBUTES = 'allow-scripts allow-popups'; // Keep a map of mermaid blocks we've already rendered. const elsProcessingMap = new WeakMap(); let renderedMermaidBlocks = 0; @@ -56,7 +57,7 @@ function fixElementSource(el) { return { source }; } -function getSandboxFrameSrc() { +export function getSandboxFrameSrc() { const path = joinPaths(gon.relative_url_root || '', SANDBOX_FRAME_PATH); if (!darkModeEnabled()) { return path; @@ -69,7 +70,7 @@ function renderMermaidEl(el, source) { const iframeEl = document.createElement('iframe'); setAttributes(iframeEl, { src: getSandboxFrameSrc(), - sandbox: 'allow-scripts allow-popups', + sandbox: SANDBOX_ATTRIBUTES, frameBorder: 0, scrolling: 'no', width: '100%', diff --git a/app/assets/javascripts/content_editor/components/bubble_menus/code_block.vue b/app/assets/javascripts/content_editor/components/bubble_menus/code_block.vue index 4ed89140e5b..6c0ac8e54d2 100644 --- a/app/assets/javascripts/content_editor/components/bubble_menus/code_block.vue +++ b/app/assets/javascripts/content_editor/components/bubble_menus/code_block.vue @@ -36,16 +36,19 @@ export default { directives: { GlTooltip, }, - inject: ['tiptapEditor'], + inject: ['tiptapEditor', 'contentEditor'], data() { return { codeBlockType: undefined, - selectedLanguage: {}, filterTerm: '', filteredLanguages: [], showCustomLanguageInput: false, customLanguageType: '', + + selectedLanguage: {}, + isDiagram: false, + showPreview: false, }; }, watch: { @@ -61,21 +64,36 @@ export default { return CODE_BLOCK_NODE_TYPES.some((type) => editor.isActive(type)); }, - updateSelectedLanguage() { + async updateCodeBlockInfoToState() { this.codeBlockType = CODE_BLOCK_NODE_TYPES.find((type) => this.tiptapEditor.isActive(type)); - if (this.codeBlockType) { - const { language } = this.tiptapEditor.getAttributes(this.codeBlockType); - this.selectedLanguage = codeBlockLanguageLoader.findOrCreateLanguageBySyntax(language); - } + if (!this.codeBlockType) return; + + const { language, isDiagram, showPreview } = this.tiptapEditor.getAttributes( + this.codeBlockType, + ); + this.selectedLanguage = codeBlockLanguageLoader.findOrCreateLanguageBySyntax( + language, + isDiagram, + ); + this.isDiagram = isDiagram; + this.showPreview = showPreview; }, - copyCodeBlockText() { + getCodeBlockText() { const { view } = this.tiptapEditor; const { from } = this.tiptapEditor.state.selection; const node = getParentByTagName(view.domAtPos(from).node, 'pre'); + return node?.textContent || ''; + }, - navigator.clipboard.writeText(node?.textContent || ''); + copyCodeBlockText() { + navigator.clipboard.writeText(this.getCodeBlockText()); + }, + + togglePreview() { + this.showPreview = !this.showPreview; + this.tiptapEditor.commands.updateAttributes(Diagram.name, { showPreview: this.showPreview }); }, async applyLanguage(language) { @@ -125,7 +143,7 @@ export default { getReferenceClientRect, } /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */" > - <editor-state-observer @transaction="updateSelectedLanguage"> + <editor-state-observer @transaction="updateCodeBlockInfoToState"> <gl-button-group> <gl-dropdown category="tertiary" @@ -228,6 +246,19 @@ export default { @click="copyCodeBlockText" /> <gl-button + v-if="isDiagram" + v-gl-tooltip + variant="default" + category="tertiary" + size="medium" + :class="{ active: showPreview }" + data-testid="preview-diagram" + :aria-label="__('Preview diagram')" + :title="__('Preview diagram')" + icon="eye" + @click="togglePreview" + /> + <gl-button v-gl-tooltip variant="default" category="tertiary" diff --git a/app/assets/javascripts/content_editor/components/toolbar_more_dropdown.vue b/app/assets/javascripts/content_editor/components/toolbar_more_dropdown.vue new file mode 100644 index 00000000000..ecde593147c --- /dev/null +++ b/app/assets/javascripts/content_editor/components/toolbar_more_dropdown.vue @@ -0,0 +1,34 @@ +<script> +import { GlDropdown, GlDropdownItem, GlTooltipDirective as GlTooltip } from '@gitlab/ui'; + +export default { + components: { + GlDropdown, + GlDropdownItem, + }, + directives: { + GlTooltip, + }, + inject: ['tiptapEditor'], + methods: { + execute(contentType, attrs) { + this.tiptapEditor.chain().focus().setNode(contentType, attrs).run(); + + this.$emit('execute', { contentType }); + }, + }, +}; +</script> +<template> + <gl-dropdown size="small" category="tertiary" icon="plus"> + <gl-dropdown-item @click="execute('diagram', { language: 'mermaid' })"> + {{ __('Mermaid diagram') }} + </gl-dropdown-item> + <gl-dropdown-item @click="execute('diagram', { language: 'plantuml' })"> + {{ __('PlantUML diagram') }} + </gl-dropdown-item> + <gl-dropdown-item @click="execute('horizontalRule')"> + {{ __('Horizontal rule') }} + </gl-dropdown-item> + </gl-dropdown> +</template> diff --git a/app/assets/javascripts/content_editor/components/top_toolbar.vue b/app/assets/javascripts/content_editor/components/top_toolbar.vue index 19e150a4da9..b652e634b0c 100644 --- a/app/assets/javascripts/content_editor/components/top_toolbar.vue +++ b/app/assets/javascripts/content_editor/components/top_toolbar.vue @@ -5,6 +5,7 @@ import ToolbarImageButton from './toolbar_image_button.vue'; import ToolbarLinkButton from './toolbar_link_button.vue'; import ToolbarTableButton from './toolbar_table_button.vue'; import ToolbarTextStyleDropdown from './toolbar_text_style_dropdown.vue'; +import ToolbarMoreDropdown from './toolbar_more_dropdown.vue'; export default { components: { @@ -13,6 +14,7 @@ export default { ToolbarLinkButton, ToolbarTableButton, ToolbarImageButton, + ToolbarMoreDropdown, }, methods: { trackToolbarControlExecution({ contentType, value }) { @@ -117,16 +119,8 @@ export default { :label="__('Add a collapsible section')" @execute="trackToolbarControlExecution" /> - <toolbar-button - data-testid="horizontal-rule" - content-type="horizontalRule" - icon-name="dash" - class="gl-mx-2" - editor-command="setHorizontalRule" - :label="__('Add a horizontal rule')" - @execute="trackToolbarControlExecution" - /> - <toolbar-table-button @execute="trackToolbarControlExecution" /> + <toolbar-table-button data-testid="table" @execute="trackToolbarControlExecution" /> + <toolbar-more-dropdown data-testid="more" @execute="trackToolbarControlExecution" /> </div> </template> <style> diff --git a/app/assets/javascripts/content_editor/components/wrappers/code_block.vue b/app/assets/javascripts/content_editor/components/wrappers/code_block.vue index 647f0798364..81f9b1f0af5 100644 --- a/app/assets/javascripts/content_editor/components/wrappers/code_block.vue +++ b/app/assets/javascripts/content_editor/components/wrappers/code_block.vue @@ -1,15 +1,26 @@ <script> +import { debounce } from 'lodash'; import { NodeViewWrapper, NodeViewContent } from '@tiptap/vue-2'; import { __ } from '~/locale'; +import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; +import SandboxedMermaid from '~/behaviors/components/sandboxed_mermaid.vue'; import codeBlockLanguageLoader from '../../services/code_block_language_loader'; +import EditorStateObserver from '../editor_state_observer.vue'; export default { name: 'CodeBlock', components: { NodeViewWrapper, NodeViewContent, + EditorStateObserver, + SandboxedMermaid, }, + inject: ['contentEditor'], props: { + editor: { + type: Object, + required: true, + }, node: { type: Object, required: true, @@ -18,13 +29,48 @@ export default { type: Function, required: true, }, + selected: { + type: Boolean, + required: false, + default: false, + }, + }, + data() { + return { + diagramUrl: '', + diagramSource: '', + }; }, async mounted() { + this.updateDiagramPreview = debounce( + this.updateDiagramPreview, + DEFAULT_DEBOUNCE_AND_THROTTLE_MS, + ); + const lang = codeBlockLanguageLoader.findOrCreateLanguageBySyntax(this.node.attrs.language); await codeBlockLanguageLoader.loadLanguage(lang.syntax); this.updateAttributes({ language: this.node.attrs.language }); }, + methods: { + async updateDiagramPreview() { + if (!this.node.attrs.showPreview) { + this.diagramSource = ''; + return; + } + + if (!this.editor.isActive('diagram')) return; + + this.diagramSource = this.$refs.nodeViewContent.$el.textContent; + + if (this.node.attrs.language !== 'mermaid') { + this.diagramUrl = await this.contentEditor.renderDiagram( + this.diagramSource, + this.node.attrs.language, + ); + } + }, + }, i18n: { frontmatter: __('frontmatter'), }, @@ -32,17 +78,26 @@ export default { }; </script> <template> - <node-view-wrapper - :class="`content-editor-code-block gl-relative code highlight ${$options.userColorScheme}`" - as="pre" - > - <span - v-if="node.attrs.isFrontmatter" - data-testid="frontmatter-label" - class="gl-absolute gl-top-0 gl-right-3" - contenteditable="false" - >{{ $options.i18n.frontmatter }}:{{ node.attrs.language }}</span + <editor-state-observer @transaction="updateDiagramPreview"> + <node-view-wrapper + :class="`content-editor-code-block gl-relative code highlight ${$options.userColorScheme}`" + as="pre" > - <node-view-content as="code" /> - </node-view-wrapper> + <div + v-if="node.attrs.showPreview" + class="gl-mt-n3! gl-ml-n4! gl-mr-n4! gl-mb-3 gl-bg-white! gl-p-4 gl-border-b-1 gl-border-b-solid gl-border-b-gray-100" + > + <sandboxed-mermaid v-if="node.attrs.language === 'mermaid'" :source="diagramSource" /> + <img v-else ref="diagramContainer" :src="diagramUrl" /> + </div> + <span + v-if="node.attrs.isFrontmatter" + data-testid="frontmatter-label" + class="gl-absolute gl-top-0 gl-right-3" + contenteditable="false" + >{{ $options.i18n.frontmatter }}:{{ node.attrs.language }}</span + > + <node-view-content ref="nodeViewContent" as="code" /> + </node-view-wrapper> + </editor-state-observer> </template> diff --git a/app/assets/javascripts/content_editor/extensions/diagram.js b/app/assets/javascripts/content_editor/extensions/diagram.js index f9dfeb92e9a..c59ca8a28b8 100644 --- a/app/assets/javascripts/content_editor/extensions/diagram.js +++ b/app/assets/javascripts/content_editor/extensions/diagram.js @@ -1,6 +1,10 @@ +import { textblockTypeInputRule } from '@tiptap/core'; import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants'; +import languageLoader from '../services/code_block_language_loader'; import CodeBlockHighlight from './code_block_highlight'; +const backtickInputRegex = /^```(mermaid|plantuml)[\s\n]$/; + export default CodeBlockHighlight.extend({ name: 'diagram', @@ -17,6 +21,9 @@ export default CodeBlockHighlight.extend({ isDiagram: { default: true, }, + showPreview: { + default: true, + }, }; }, @@ -24,6 +31,11 @@ export default CodeBlockHighlight.extend({ return [ { priority: PARSE_HTML_PRIORITY_HIGHEST, + tag: 'pre[lang="mermaid"]', + getAttrs: () => ({ language: 'mermaid' }), + }, + { + priority: PARSE_HTML_PRIORITY_HIGHEST, tag: '[data-diagram]', getContent(element, schema) { const source = atob(element.dataset.diagramSrc.replace('data:text/plain;base64,', '')); @@ -54,6 +66,14 @@ export default CodeBlockHighlight.extend({ }, addInputRules() { - return []; + const getAttributes = (match) => languageLoader?.loadLanguageFromInputRule(match) || {}; + + return [ + textblockTypeInputRule({ + find: backtickInputRegex, + type: this.type, + getAttributes, + }), + ]; }, }); diff --git a/app/assets/javascripts/content_editor/services/asset_resolver.js b/app/assets/javascripts/content_editor/services/asset_resolver.js index 942457b9664..c0bcddbe58d 100644 --- a/app/assets/javascripts/content_editor/services/asset_resolver.js +++ b/app/assets/javascripts/content_editor/services/asset_resolver.js @@ -1,13 +1,24 @@ import { memoize } from 'lodash'; +const parser = new DOMParser(); + export default ({ renderMarkdown }) => ({ resolveUrl: memoize(async (canonicalSrc) => { const html = await renderMarkdown(`[link](${canonicalSrc})`); if (!html) return canonicalSrc; - const parser = new DOMParser(); const { body } = parser.parseFromString(html, 'text/html'); - return body.querySelector('a').getAttribute('href'); }), + + renderDiagram: memoize(async (code, language) => { + const backticks = '`'.repeat(4); + const html = await renderMarkdown(`${backticks}${language}\n${code}\n${backticks}`); + + const { body } = parser.parseFromString(html, 'text/html'); + const img = body.querySelector('img'); + if (!img) return ''; + + return img.dataset.src || img.getAttribute('src'); + }), }); diff --git a/app/assets/javascripts/content_editor/services/code_block_language_loader.js b/app/assets/javascripts/content_editor/services/code_block_language_loader.js index fa0549d4183..b7cf1bb087c 100644 --- a/app/assets/javascripts/content_editor/services/code_block_language_loader.js +++ b/app/assets/javascripts/content_editor/services/code_block_language_loader.js @@ -8,7 +8,7 @@ const codeBlockLanguageLoader = { allLanguages: CODE_BLOCK_LANGUAGES, - findOrCreateLanguageBySyntax(value) { + findOrCreateLanguageBySyntax(value, isDiagram) { const lowercaseValue = value?.toLowerCase() || 'plaintext'; return ( this.allLanguages.find( @@ -16,7 +16,9 @@ const codeBlockLanguageLoader = { syntax === lowercaseValue || variants?.toLowerCase().split(', ').includes(lowercaseValue), ) || { syntax: lowercaseValue, - label: sprintf(__(`Custom (%{language})`), { language: lowercaseValue }), + label: sprintf(isDiagram ? __(`Diagram (%{language})`) : __(`Custom (%{language})`), { + language: lowercaseValue, + }), } ); }, diff --git a/app/assets/javascripts/content_editor/services/content_editor.js b/app/assets/javascripts/content_editor/services/content_editor.js index 52dacb84153..06757e7a280 100644 --- a/app/assets/javascripts/content_editor/services/content_editor.js +++ b/app/assets/javascripts/content_editor/services/content_editor.js @@ -52,6 +52,10 @@ export class ContentEditor { return this._assetResolver.resolveUrl(canonicalSrc); } + renderDiagram(code, language) { + return this._assetResolver.renderDiagram(code, language); + } + async setSerializedContent(serializedContent) { const { _tiptapEditor: editor, _eventHub: eventHub } = this; const { doc, tr } = editor.state; diff --git a/app/assets/javascripts/diffs/components/commit_item.vue b/app/assets/javascripts/diffs/components/commit_item.vue index fc69dca73a7..54b648e8d03 100644 --- a/app/assets/javascripts/diffs/components/commit_item.vue +++ b/app/assets/javascripts/diffs/components/commit_item.vue @@ -121,7 +121,7 @@ export default { <div class="d-flex float-left align-items-center align-self-start"> <input v-if="isSelectable" - class="mr-2" + class="gl-mr-3" type="checkbox" :checked="checked" @change="$emit('handleCheckboxChange', $event.target.checked)" diff --git a/app/assets/javascripts/issues/show/components/description.vue b/app/assets/javascripts/issues/show/components/description.vue index f926169549d..b8b7cbb5bdc 100644 --- a/app/assets/javascripts/issues/show/components/description.vue +++ b/app/assets/javascripts/issues/show/components/description.vue @@ -453,13 +453,7 @@ export default { > </textarea> - <gl-modal - ref="modal" - modal-id="create-task-modal" - :title="s__('WorkItem|New Task')" - hide-footer - body-class="gl-p-0!" - > + <gl-modal ref="modal" size="lg" modal-id="create-task-modal" hide-footer body-class="gl-p-0!"> <create-work-item is-modal :initial-title="activeTask.title" diff --git a/app/assets/javascripts/lib/utils/color_utils.js b/app/assets/javascripts/lib/utils/color_utils.js index 66d52051905..3d8df4fde05 100644 --- a/app/assets/javascripts/lib/utils/color_utils.js +++ b/app/assets/javascripts/lib/utils/color_utils.js @@ -67,7 +67,7 @@ export function darkModeEnabled() { const ideDarkThemes = ['dark', 'solarized-dark', 'monokai']; // eslint-disable-next-line @gitlab/require-i18n-strings - const isWebIde = document.body.dataset.page.startsWith('ide:'); + const isWebIde = document.body.dataset.page?.startsWith('ide:'); if (isWebIde) { return ideDarkThemes.includes(window.gon?.user_color_scheme); diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue index f397a86a368..e7ac27c5e3e 100644 --- a/app/assets/javascripts/notes/components/comment_form.vue +++ b/app/assets/javascripts/notes/components/comment_form.vue @@ -416,7 +416,7 @@ export default { <gl-form-checkbox v-if="internalNotesEnabled && canSetInternalNote" v-model="noteIsInternal" - class="gl-mb-6" + class="gl-mb-2" data-testid="internal-note-checkbox" > {{ $options.i18n.internal }} diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/additional_metadata.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/additional_metadata.vue index 74c0cb44c51..a3bbd569f41 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/additional_metadata.vue +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/additional_metadata.vue @@ -1,30 +1,68 @@ <script> +import { GlAlert } from '@gitlab/ui'; +import * as Sentry from '@sentry/browser'; +import { s__ } from '~/locale'; import Composer from '~/packages_and_registries/package_registry/components/details/metadata/composer.vue'; import Conan from '~/packages_and_registries/package_registry/components/details/metadata/conan.vue'; import Maven from '~/packages_and_registries/package_registry/components/details/metadata/maven.vue'; import Nuget from '~/packages_and_registries/package_registry/components/details/metadata/nuget.vue'; import Pypi from '~/packages_and_registries/package_registry/components/details/metadata/pypi.vue'; import { + FETCH_PACKAGE_METADATA_ERROR_MESSAGE, PACKAGE_TYPE_COMPOSER, PACKAGE_TYPE_CONAN, PACKAGE_TYPE_MAVEN, PACKAGE_TYPE_NUGET, PACKAGE_TYPE_PYPI, } from '~/packages_and_registries/package_registry/constants'; +import getPackageMetadataQuery from '../../graphql/queries/get_package_metadata.query.graphql'; +import AdditionalMetadataLoader from './additional_metadata_loader.vue'; export default { components: { Composer, Conan, + GlAlert, Maven, Nuget, Pypi, + AdditionalMetadataLoader, }, props: { - packageEntity: { - type: Object, + packageId: { + type: String, required: true, }, + packageType: { + type: String, + required: true, + }, + }, + apollo: { + packageMetadata: { + query: getPackageMetadataQuery, + context: { + isSingleRequest: true, + }, + variables() { + return { + id: this.packageId, + }; + }, + update(data) { + return data.package?.metadata || null; + }, + error(error) { + this.fetchPackageMetadataError = true; + Sentry.captureException(error); + }, + }, + }, + data() { + return { + packageMetadata: null, + fetchPackageMetadataError: false, + }; }, computed: { metadataComponent() { @@ -34,22 +72,43 @@ export default { [PACKAGE_TYPE_MAVEN]: Maven, [PACKAGE_TYPE_NUGET]: Nuget, [PACKAGE_TYPE_PYPI]: Pypi, - }[this.packageEntity.packageType]; + }[this.packageType]; }, showMetadata() { - return this.metadataComponent && this.packageEntity.metadata; + return this.metadataComponent && this.packageMetadata; + }, + isLoading() { + return this.$apollo.queries.packageMetadata.loading; }, }, + i18n: { + componentTitle: s__('PackageRegistry|Additional metadata'), + fetchPackageMetadataErrorMessage: FETCH_PACKAGE_METADATA_ERROR_MESSAGE, + }, }; </script> <template> - <div v-if="showMetadata"> - <h3 class="gl-font-lg" data-testid="title">{{ __('Additional Metadata') }}</h3> - <div class="gl-bg-gray-50 gl-inset-border-1-gray-100 gl-rounded-base" data-testid="main"> + <div> + <h3 v-if="isLoading || showMetadata" class="gl-font-lg" data-testid="title"> + {{ $options.i18n.componentTitle }} + </h3> + <gl-alert + v-if="fetchPackageMetadataError" + variant="danger" + @dismiss="fetchPackageMetadataError = false" + > + {{ $options.i18n.fetchPackageMetadataErrorMessage }} + </gl-alert> + <additional-metadata-loader v-if="isLoading" /> + <div + v-if="showMetadata" + class="gl-bg-gray-50 gl-inset-border-1-gray-100 gl-rounded-base" + data-testid="main" + > <component :is="metadataComponent" - :package-entity="packageEntity" + :package-metadata="packageMetadata" data-testid="component-is" /> </div> diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/additional_metadata_loader.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/additional_metadata_loader.vue new file mode 100644 index 00000000000..628cf441831 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/additional_metadata_loader.vue @@ -0,0 +1,30 @@ +<script> +import { GlSkeletonLoader } from '@gitlab/ui'; + +export default { + components: { + GlSkeletonLoader, + }, + loader: { + width: 302, + height: 16, + repeat: 2, + }, +}; +</script> + +<template> + <div class="gl-bg-gray-50 gl-inset-border-1-gray-100 gl-rounded-base"> + <div + v-for="index in $options.loader.repeat" + :key="index" + class="gl-display-flex gl-align-items-center gl-p-4 gl-border-gray-100 gl-border-b-1" + > + <div class="gl-md-max-w-30p"> + <gl-skeleton-loader :width="$options.loader.width" :height="$options.loader.height"> + <rect :width="$options.loader.width" :height="$options.loader.height" rx="4" /> + </gl-skeleton-loader> + </div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/composer.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/composer.vue index b6a36a0b00f..e3edaa3e45e 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/composer.vue +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/composer.vue @@ -18,7 +18,7 @@ export default { ClipboardButton, }, props: { - packageEntity: { + packageMetadata: { type: Object, required: true, }, @@ -31,10 +31,10 @@ export default { <details-row icon="information-o" padding="gl-p-4" dashed data-testid="composer-target-sha"> <gl-sprintf :message="$options.i18n.targetSha"> <template #sha> - <strong>{{ packageEntity.metadata.targetSha }}</strong> + <strong>{{ packageMetadata.targetSha }}</strong> <clipboard-button :title="$options.i18n.targetShaCopyButton" - :text="packageEntity.metadata.targetSha" + :text="packageMetadata.targetSha" category="tertiary" css-class="gl-p-0!" /> @@ -44,10 +44,10 @@ export default { <details-row icon="information-o" padding="gl-p-4" data-testid="composer-json"> <gl-sprintf :message="$options.i18n.composerJson"> <template #license> - <strong>{{ packageEntity.metadata.composerJson.license }}</strong> + <strong>{{ packageMetadata.composerJson.license }}</strong> </template> <template #version> - <strong>{{ packageEntity.metadata.composerJson.version }}</strong> + <strong>{{ packageMetadata.composerJson.version }}</strong> </template> </gl-sprintf> </details-row> diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/conan.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/conan.vue index 10797d74acf..de7c1bc4cd3 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/conan.vue +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/conan.vue @@ -13,7 +13,7 @@ export default { GlSprintf, }, props: { - packageEntity: { + packageMetadata: { type: Object, required: true, }, @@ -25,7 +25,7 @@ export default { <div> <details-row icon="information-o" padding="gl-p-4" data-testid="conan-recipe"> <gl-sprintf :message="$options.i18n.recipeText"> - <template #recipe>{{ packageEntity.metadata.recipe }}</template> + <template #recipe>{{ packageMetadata.recipe }}</template> </gl-sprintf> </details-row> </div> diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/maven.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/maven.vue index fd9fb49a9f2..7c3eb476a99 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/maven.vue +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/maven.vue @@ -14,7 +14,7 @@ export default { GlSprintf, }, props: { - packageEntity: { + packageMetadata: { type: Object, required: true, }, @@ -27,14 +27,14 @@ export default { <details-row icon="information-o" padding="gl-p-4" dashed data-testid="maven-app"> <gl-sprintf :message="$options.i18n.appName"> <template #name> - <strong>{{ packageEntity.metadata.appName }}</strong> + <strong>{{ packageMetadata.appName }}</strong> </template> </gl-sprintf> </details-row> <details-row icon="information-o" padding="gl-p-4" data-testid="maven-group"> <gl-sprintf :message="$options.i18n.appGroup"> <template #group> - <strong>{{ packageEntity.metadata.appGroup }}</strong> + <strong>{{ packageMetadata.appGroup }}</strong> </template> </gl-sprintf> </details-row> diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/nuget.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/nuget.vue index 1360b03856f..1ddd419a639 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/nuget.vue +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/nuget.vue @@ -14,7 +14,7 @@ export default { GlSprintf, }, props: { - packageEntity: { + packageMetadata: { type: Object, required: true, }, @@ -25,7 +25,7 @@ export default { <template> <div> <details-row - v-if="packageEntity.metadata.projectUrl" + v-if="packageMetadata.projectUrl" icon="project" padding="gl-p-4" dashed @@ -33,22 +33,22 @@ export default { > <gl-sprintf :message="$options.i18n.sourceText"> <template #link> - <gl-link :href="packageEntity.metadata.projectUrl" target="_blank">{{ - packageEntity.metadata.projectUrl + <gl-link :href="packageMetadata.projectUrl" target="_blank">{{ + packageMetadata.projectUrl }}</gl-link> </template> </gl-sprintf> </details-row> <details-row - v-if="packageEntity.metadata.licenseUrl" + v-if="packageMetadata.licenseUrl" icon="license" padding="gl-p-4" data-testid="nuget-license" > <gl-sprintf :message="$options.i18n.licenseText"> <template #link> - <gl-link :href="packageEntity.metadata.licenseUrl" target="_blank">{{ - packageEntity.metadata.licenseUrl + <gl-link :href="packageMetadata.licenseUrl" target="_blank">{{ + packageMetadata.licenseUrl }}</gl-link> </template> </gl-sprintf> diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/pypi.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/pypi.vue index 6534eef532c..ef35349c228 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/pypi.vue +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/pypi.vue @@ -13,7 +13,7 @@ export default { GlSprintf, }, props: { - packageEntity: { + packageMetadata: { type: Object, required: true, }, @@ -26,7 +26,7 @@ export default { <details-row icon="information-o" padding="gl-p-4" data-testid="pypi-required-python"> <gl-sprintf :message="$options.i18n.requiredPython"> <template #pythonVersion> - <strong>{{ packageEntity.metadata.requiredPython }}</strong> + <strong>{{ packageMetadata.requiredPython }}</strong> </template> </gl-sprintf> </details-row> diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_history.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_history.vue index 8b3d781b2df..96b82a20364 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_history.vue +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_history.vue @@ -1,7 +1,7 @@ <script> -import { GlLink, GlSprintf } from '@gitlab/ui'; +import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui'; +import * as Sentry from '@sentry/browser'; import { first } from 'lodash'; -import createFlash from '~/flash'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { truncateSha } from '~/lib/utils/text_utility'; import { s__, n__ } from '~/locale'; @@ -27,8 +27,10 @@ export default { combinedUpdateText: s__( 'PackageRegistry|Package updated by commit %{link} on branch %{branch}, built by pipeline %{pipeline}, and published to the registry %{datetime}', ), + fetchPackagePipelinesErrorMessage: FETCH_PACKAGE_PIPELINES_ERROR_MESSAGE, }, components: { + GlAlert, GlLink, GlSprintf, HistoryItem, @@ -54,15 +56,16 @@ export default { update(data) { return data.package?.pipelines?.nodes || []; }, - error() { - createFlash({ message: FETCH_PACKAGE_PIPELINES_ERROR_MESSAGE }); + error(error) { + this.fetchPackagePipelinesError = true; + Sentry.captureException(error); }, }, }, data() { return { pipelines: [], - showDescription: false, + fetchPackagePipelinesError: false, }; }, computed: { @@ -109,6 +112,13 @@ export default { <template> <div class="issuable-discussion"> <h3 class="gl-font-lg" data-testid="title">{{ __('History') }}</h3> + <gl-alert + v-if="fetchPackagePipelinesError" + variant="danger" + @dismiss="fetchPackagePipelinesError = false" + > + {{ $options.i18n.fetchPackagePipelinesErrorMessage }} + </gl-alert> <package-history-loader v-if="isLoading" /> <ul v-else class="timeline main-notes-list notes gl-mb-4" data-testid="timeline"> <history-item icon="clock" data-testid="created-on"> diff --git a/app/assets/javascripts/packages_and_registries/package_registry/constants.js b/app/assets/javascripts/packages_and_registries/package_registry/constants.js index 65930d94f3e..3c090951b7d 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/constants.js +++ b/app/assets/javascripts/packages_and_registries/package_registry/constants.js @@ -75,6 +75,9 @@ export const FETCH_PACKAGE_DETAILS_ERROR_MESSAGE = s__( export const FETCH_PACKAGE_PIPELINES_ERROR_MESSAGE = s__( 'PackageRegistry|Something went wrong while fetching the package history.', ); +export const FETCH_PACKAGE_METADATA_ERROR_MESSAGE = s__( + 'PackageRegistry|Something went wrong while fetching the package metadata.', +); export const DELETE_PACKAGE_SUCCESS_MESSAGE = s__('PackageRegistry|Package deleted successfully'); export const PACKAGE_REGISTRY_TITLE = __('Package Registry'); diff --git a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql index 651bd2b4d63..5574020c9e4 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql +++ b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql @@ -27,6 +27,17 @@ query getPackageDetails($id: PackagesPackageID!) { name } } + pipelines(first: 1) { + nodes { + ref + id + project { + id + name + webUrl + } + } + } packageFiles(first: 100) { nodes { id @@ -72,37 +83,15 @@ query getPackageDetails($id: PackagesPackageID!) { } } metadata { - ... on ComposerMetadata { - targetSha - composerJson { - license - version - } - } - ... on PypiMetadata { - id - requiredPython - } - ... on ConanMetadata { - id - packageChannel - packageUsername - recipe - recipePath - } ... on MavenMetadata { id appName appGroup appVersion - path } - ... on NugetMetadata { id iconUrl - licenseUrl - projectUrl } } } diff --git a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_metadata.query.graphql b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_metadata.query.graphql new file mode 100644 index 00000000000..fc8b39b37ab --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_metadata.query.graphql @@ -0,0 +1,39 @@ +query getPackageMetadata($id: PackagesPackageID!) { + package(id: $id) { + id + packageType + metadata { + ... on ComposerMetadata { + targetSha + composerJson { + license + version + } + } + ... on PypiMetadata { + id + requiredPython + } + ... on ConanMetadata { + id + packageChannel + packageUsername + recipe + recipePath + } + ... on MavenMetadata { + id + appName + appGroup + appVersion + path + } + ... on NugetMetadata { + id + iconUrl + licenseUrl + projectUrl + } + } + } +} diff --git a/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue b/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue index 162b420a784..768c8d6478b 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue +++ b/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue @@ -27,6 +27,9 @@ import DeletePackage from '~/packages_and_registries/package_registry/components import { PACKAGE_TYPE_NUGET, PACKAGE_TYPE_COMPOSER, + PACKAGE_TYPE_CONAN, + PACKAGE_TYPE_MAVEN, + PACKAGE_TYPE_PYPI, DELETE_PACKAGE_TRACKING_ACTION, REQUEST_DELETE_PACKAGE_TRACKING_ACTION, CANCEL_DELETE_PACKAGE_TRACKING_ACTION, @@ -122,6 +125,9 @@ export default { packageFiles() { return this.packageEntity.packageFiles?.nodes; }, + packageType() { + return this.packageEntity.packageType; + }, isLoading() { return this.$apollo.queries.packageEntity.loading; }, @@ -130,7 +136,7 @@ export default { }, tracking() { return { - category: packageTypeToTrackCategory(this.packageEntity.packageType), + category: packageTypeToTrackCategory(this.packageType), }; }, hasVersions() { @@ -140,10 +146,19 @@ export default { return this.packageEntity.dependencyLinks?.nodes || []; }, showDependencies() { - return this.packageEntity.packageType === PACKAGE_TYPE_NUGET; + return this.packageType === PACKAGE_TYPE_NUGET; }, showFiles() { - return this.packageEntity.packageType !== PACKAGE_TYPE_COMPOSER; + return this.packageType !== PACKAGE_TYPE_COMPOSER; + }, + showMetadata() { + return [ + PACKAGE_TYPE_COMPOSER, + PACKAGE_TYPE_CONAN, + PACKAGE_TYPE_MAVEN, + PACKAGE_TYPE_NUGET, + PACKAGE_TYPE_PYPI, + ].includes(this.packageType); }, }, methods: { @@ -262,7 +277,11 @@ export default { <installation-commands :package-entity="packageEntity" /> - <additional-metadata :package-entity="packageEntity" /> + <additional-metadata + v-if="showMetadata" + :package-id="packageEntity.id" + :package-type="packageType" + /> </div> <package-files diff --git a/app/assets/javascripts/pages/admin/application_settings/general/components/signup_form.vue b/app/assets/javascripts/pages/admin/application_settings/general/components/signup_form.vue index 5ecacb84d65..ccb449f96e1 100644 --- a/app/assets/javascripts/pages/admin/application_settings/general/components/signup_form.vue +++ b/app/assets/javascripts/pages/admin/application_settings/general/components/signup_form.vue @@ -12,6 +12,7 @@ import { import { toSafeInteger } from 'lodash'; import csrf from '~/lib/utils/csrf'; import { __, n__, s__, sprintf } from '~/locale'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import SignupCheckbox from './signup_checkbox.vue'; const DENYLIST_TYPE_RAW = 'raw'; @@ -31,7 +32,12 @@ export default { GlLink, SignupCheckbox, GlModal, + PasswordComplexityCheckboxGroup: () => + import( + 'ee_component/pages/admin/application_settings/general/components/password_complexity_checkbox_group.vue' + ), }, + mixins: [glFeatureFlagMixin()], inject: [ 'host', 'settingsPath', @@ -178,6 +184,9 @@ export default { this.submitForm(); }, + setPasswordComplexity({ name, value }) { + this.$set(this.form, name, value); + }, submitForm() { this.$refs.form.submit(); }, @@ -291,9 +300,7 @@ export default { <template #description> <gl-sprintf :message=" - s__( - 'ApplicationSettings|See GitLab\'s %{linkStart}Password Policy Guidelines%{linkEnd}.', - ) + s__('ApplicationSettings|See %{linkStart}password policy guidelines%{linkEnd}.') " > <template #link="{ content }"> @@ -305,6 +312,10 @@ export default { </template> </gl-form-group> + <password-complexity-checkbox-group + v-if="glFeatures.passwordComplexity" + @set-password-complexity="setPasswordComplexity" + /> <gl-form-group :description="$options.i18n.domainAllowListDescription" :label="$options.i18n.domainAllowListLabel" diff --git a/app/assets/javascripts/pages/admin/application_settings/signup_restrictions.js b/app/assets/javascripts/pages/admin/application_settings/signup_restrictions.js index a50d8de0e88..0d5c55cb87b 100644 --- a/app/assets/javascripts/pages/admin/application_settings/signup_restrictions.js +++ b/app/assets/javascripts/pages/admin/application_settings/signup_restrictions.js @@ -18,6 +18,10 @@ export default function initSignupRestrictions(elementSelector = '#js-signup-for 'domainDenylistEnabled', 'denylistTypeRawSelected', 'emailRestrictionsEnabled', + 'passwordNumberRequired', + 'passwordLowercaseRequired', + 'passwordUppercaseRequired', + 'passwordSymbolRequired', ], }); diff --git a/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue b/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue index be7a89c2869..ef99d540c86 100644 --- a/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue +++ b/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue @@ -274,7 +274,7 @@ export default { <template #collapsed> <div v-gl-tooltip.viewport.left :title="dateLabel" class="sidebar-collapsed-icon"> <gl-icon :size="16" name="calendar" /> - <span class="collapse-truncated-title">{{ formattedDate }}</span> + <span class="gl-pt-2 gl-px-3 gl-font-sm">{{ formattedDate }}</span> </div> <sidebar-inherit-date v-if="canInherit && !initialLoading" diff --git a/app/assets/javascripts/sidebar/components/participants/participants.vue b/app/assets/javascripts/sidebar/components/participants/participants.vue index 77e41648e9b..b8804de653f 100644 --- a/app/assets/javascripts/sidebar/components/participants/participants.vue +++ b/app/assets/javascripts/sidebar/components/participants/participants.vue @@ -99,7 +99,9 @@ export default { > <gl-icon name="users" /> <gl-loading-icon v-if="loading" size="sm" /> - <span v-else data-testid="collapsed-count"> {{ participantCount }} </span> + <span v-else data-testid="collapsed-count" class="gl-pt-2 gl-px-3 gl-font-sm"> + {{ participantCount }} + </span> </div> <div v-if="showParticipantLabel" diff --git a/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue b/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue index 897cab45fe4..3d8a2cd847c 100644 --- a/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue +++ b/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue @@ -322,7 +322,7 @@ export default { class="sidebar-collapsed-icon" > <gl-icon :aria-label="attributeTypeTitle" :name="attributeTypeIcon" /> - <span class="collapse-truncated-title"> + <span class="collapse-truncated-title gl-pt-2 gl-px-3 gl-font-sm"> {{ attributeTitle }} </span> </div> diff --git a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue index 7b67c34ded6..465f971717f 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue @@ -58,7 +58,7 @@ export default { } else if (this.showEstimateOnlyState || this.showSpentOnlyState) { return 'bold'; } else if (this.showNoTimeTrackingState) { - return 'no-value'; + return 'no-value collapse-truncated-title gl-pt-2 gl-px-3 gl-font-sm'; } return ''; diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue index 914d146fcc3..178c57a5666 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue @@ -126,7 +126,7 @@ export default { <div class="gl-display-flex gl-align-items-center"> <span :style="{ backgroundColor: label.color }" - class="gl-display-inline-block mr-2 p-2" + class="gl-display-inline-block gl-mr-3 gl-p-3" ></span> <div>{{ getLabelName(label) }}</div> </div> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue index 57ee816c4c7..57e3ee4aaa5 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue @@ -92,7 +92,9 @@ export default { @click="handleCollapsedClick" > <gl-icon name="labels" /> - <span class="gl-font-base gl-line-height-24">{{ selectedLabels.length }}</span> + <span class="collapse-truncated-title gl-pt-2 gl-px-3 gl-font-sm">{{ + selectedLabels.length + }}</span> </div> <span v-if="!selectedLabels.length" diff --git a/app/assets/javascripts/work_items/components/item_state.vue b/app/assets/javascripts/work_items/components/item_state.vue index 0b6c1a75bb2..69670d3471c 100644 --- a/app/assets/javascripts/work_items/components/item_state.vue +++ b/app/assets/javascripts/work_items/components/item_state.vue @@ -49,14 +49,28 @@ export default { </script> <template> - <gl-form-group :label="$options.i18n.status" :label-for="$options.labelId"> + <gl-form-group + :label="$options.i18n.status" + :label-for="$options.labelId" + label-cols="3" + label-cols-lg="2" + label-class="gl-pb-0!" + class="gl-align-items-center" + > <gl-form-select :id="$options.labelId" :value="state" :options="$options.states" :disabled="loading" - class="gl-w-auto" + class="gl-w-auto hide-select-decoration" @change="setState" /> </gl-form-group> </template> + +<style> +.hide-select-decoration:not(:focus, :hover) { + background-image: none; + box-shadow: none; +} +</style> diff --git a/app/assets/javascripts/work_items/components/item_title.vue b/app/assets/javascripts/work_items/components/item_title.vue index 232510b108d..ce2fa158596 100644 --- a/app/assets/javascripts/work_items/components/item_title.vue +++ b/app/assets/javascripts/work_items/components/item_title.vue @@ -40,18 +40,18 @@ export default { <template> <h2 - class="gl-font-weight-normal gl-sm-font-weight-bold gl-my-5 gl-display-inline-block" + class="gl-font-weight-normal gl-sm-font-weight-bold gl-my-5 gl-w-full" :class="{ 'gl-cursor-not-allowed': disabled }" aria-labelledby="item-title" > - <span + <div id="item-title" ref="titleEl" role="textbox" :aria-label="__('Title')" :data-placeholder="placeholder" :contenteditable="!disabled" - class="gl-pseudo-placeholder" + class="gl-pseudo-placeholder gl-px-4 gl-py-3 gl-ml-n4 gl-border gl-border-white gl-hover-border-gray-200 gl-rounded-base" @blur="handleBlur" @keyup="handleInput" @keydown.enter.exact="handleSubmit" @@ -59,7 +59,8 @@ export default { @keydown.meta.u.prevent @keydown.ctrl.b.prevent @keydown.meta.b.prevent - >{{ title }}</span > + {{ title }} + </div> </h2> </template> diff --git a/app/assets/javascripts/work_items/components/work_item_detail.vue b/app/assets/javascripts/work_items/components/work_item_detail.vue index d9e92ead86c..da8fe222b6a 100644 --- a/app/assets/javascripts/work_items/components/work_item_detail.vue +++ b/app/assets/javascripts/work_items/components/work_item_detail.vue @@ -115,7 +115,7 @@ export default { </gl-skeleton-loader> </div> <template v-else> - <div class="gl-display-flex"> + <div class="gl-display-flex gl-align-items-start"> <work-item-title :work-item-id="workItem.id" :work-item-title="workItem.title" @@ -127,7 +127,7 @@ export default { <work-item-actions :work-item-id="workItem.id" :can-delete="canDelete" - class="gl-ml-auto gl-mt-5" + class="gl-ml-auto gl-mt-6" @deleteWorkItem="$emit('deleteWorkItem')" @error="error = $event" /> diff --git a/app/assets/javascripts/work_items/components/work_item_detail_modal.vue b/app/assets/javascripts/work_items/components/work_item_detail_modal.vue index 425b8290d44..d1c8022ac57 100644 --- a/app/assets/javascripts/work_items/components/work_item_detail_modal.vue +++ b/app/assets/javascripts/work_items/components/work_item_detail_modal.vue @@ -98,7 +98,15 @@ export default { </script> <template> - <gl-modal ref="modal" hide-footer size="lg" modal-id="work-item-detail-modal" @hide="closeModal"> + <gl-modal + ref="modal" + hide-footer + size="lg" + modal-id="work-item-detail-modal" + header-class="gl-p-0 gl-pb-2!" + body-class="gl-pb-6!" + @hide="closeModal" + > <gl-alert v-if="error" variant="danger" @dismiss="error = false"> {{ error }} </gl-alert> @@ -106,6 +114,7 @@ export default { <work-item-detail :work-item-parent-id="issueGid" :work-item-id="workItemId" + class="gl-p-5 gl-mt-n3" @deleteWorkItem="deleteWorkItem" /> </gl-modal> @@ -114,7 +123,7 @@ export default { <style> /* hide the existing modal header */ -#work-item-detail-modal .modal-header { +#work-item-detail-modal .modal-header * { display: none; } </style> |