diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-07-15 21:09:09 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-07-15 21:09:09 +0300 |
commit | da1962d9ac710f95d350d2645c87f5a663123cf2 (patch) | |
tree | 1725ade126a9b4ae0148cd100cee94c44f9ce9f3 /app/assets | |
parent | e69e3f1eb695b4e852c56e7ddf8c52915ae2631b (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets')
12 files changed, 265 insertions, 69 deletions
diff --git a/app/assets/javascripts/code_navigation/components/popover.vue b/app/assets/javascripts/code_navigation/components/popover.vue index 4cc5a5cd6a2..b7fa3242fbf 100644 --- a/app/assets/javascripts/code_navigation/components/popover.vue +++ b/app/assets/javascripts/code_navigation/components/popover.vue @@ -1,10 +1,14 @@ <script> -import { GlButton } from '@gitlab/ui'; +import { GlButton, GlTabs, GlTab, GlLink, GlBadge } from '@gitlab/ui'; import DocLine from './doc_line.vue'; export default { components: { GlButton, + GlTabs, + GlTab, + GlLink, + GlBadge, DocLine, }, props: { @@ -54,6 +58,9 @@ export default { isDefinitionCurrentBlob() { return this.data.definition_path.indexOf(this.blobPath) === 0; }, + references() { + return this.data.references || []; + }, }, watch: { position: { @@ -82,37 +89,61 @@ export default { class="popover code-navigation-popover popover-font-size-normal gl-popover bs-popover-bottom show" > <div :style="{ left: `${offsetLeft}px` }" class="arrow"></div> - <div class="overflow-auto code-navigation-popover-container"> - <div - v-for="(hover, index) in data.hover" - :key="index" - :class="{ 'border-bottom': index !== data.hover.length - 1 }" - > - <pre - v-if="hover.language" - ref="code-output" - :class="$options.colorScheme" - class="border-0 bg-transparent m-0 code highlight text-wrap" - ><doc-line v-for="(tokens, tokenIndex) in hover.tokens" :key="tokenIndex" :language="hover.language" :tokens="tokens"/></pre> - <p v-else ref="doc-output" class="p-3 m-0 gl-font-base"> - {{ hover.value }} + <gl-tabs nav-class="gl-hidden" content-class="gl-py-0"> + <gl-tab :title="__('Definition')"> + <div class="overflow-auto code-navigation-popover-container"> + <div + v-for="(hover, index) in data.hover" + :key="index" + :class="{ 'border-bottom': index !== data.hover.length - 1 }" + > + <pre + v-if="hover.language" + ref="code-output" + :class="$options.colorScheme" + class="border-0 bg-transparent m-0 code highlight text-wrap" + ><doc-line v-for="(tokens, tokenIndex) in hover.tokens" :key="tokenIndex" :language="hover.language" :tokens="tokens"/></pre> + <p v-else ref="doc-output" class="p-3 m-0"> + {{ hover.value }} + </p> + </div> + </div> + <div v-if="definitionPath || isCurrentDefinition" class="popover-body border-top"> + <span v-if="isCurrentDefinition" class="gl-font-weight-bold gl-font-base"> + {{ s__('CodeIntelligence|This is the definition') }} + </span> + <gl-button + v-else + :href="definitionPath" + :target="isDefinitionCurrentBlob ? null : '_blank'" + class="w-100" + variant="default" + data-testid="go-to-definition-btn" + > + {{ __('Go to definition') }} + </gl-button> + </div> + </gl-tab> + <gl-tab data-testid="references-tab" class="py-2"> + <template #title> + {{ __('References') }} + <gl-badge size="sm" class="gl-tab-counter-badge">{{ references.length }}</gl-badge> + </template> + <template v-if="references.length"> + <div v-for="(reference, index) in references" :key="index" class="gl-dropdown-item"> + <gl-link + :href="`${definitionPathPrefix}/${reference.path}`" + class="dropdown-item" + data-testid="reference-link" + > + {{ reference.path }} + </gl-link> + </div> + </template> + <p v-else class="gl-my-4 gl-px-4"> + {{ s__('CodeNavigation|No references found') }} </p> - </div> - </div> - <div v-if="definitionPath || isCurrentDefinition" class="popover-body border-top"> - <span v-if="isCurrentDefinition" class="gl-font-weight-bold gl-font-base"> - {{ s__('CodeIntelligence|This is the definition') }} - </span> - <gl-button - v-else - :href="definitionPath" - :target="isDefinitionCurrentBlob ? null : '_blank'" - class="w-100" - variant="default" - data-testid="go-to-definition-btn" - > - {{ __('Go to definition') }} - </gl-button> - </div> + </gl-tab> + </gl-tabs> </div> </template> diff --git a/app/assets/javascripts/integrations/edit/components/integration_form.vue b/app/assets/javascripts/integrations/edit/components/integration_form.vue index 8d603e3ea44..12e2ce88b3c 100644 --- a/app/assets/javascripts/integrations/edit/components/integration_form.vue +++ b/app/assets/javascripts/integrations/edit/components/integration_form.vue @@ -53,7 +53,7 @@ export default { return this.type === 'jira'; }, showJiraIssuesFields() { - return this.isJira && this.glFeatures.jiraIntegration; + return this.isJira && this.glFeatures.jiraIssuesIntegration; }, }, }; diff --git a/app/assets/javascripts/issuables_list/components/issuables_list_app.vue b/app/assets/javascripts/issuables_list/components/issuables_list_app.vue index e1a40323f5d..f4787f5c1d4 100644 --- a/app/assets/javascripts/issuables_list/components/issuables_list_app.vue +++ b/app/assets/javascripts/issuables_list/components/issuables_list_app.vue @@ -293,10 +293,10 @@ export default { this.filters = filters; }, refetchIssuables() { - const ignored = ['utf8', 'state']; + const ignored = ['utf8']; const params = omit(this.filters, ignored); - historyPushState(setUrlParams(params, window.location.href, true)); + historyPushState(setUrlParams(params, window.location.href, true, true)); this.fetchIssuables(); }, handleFilter(filters) { diff --git a/app/assets/javascripts/issuables_list/index.js b/app/assets/javascripts/issuables_list/index.js index 6b0c56c8dbd..40252c10d5f 100644 --- a/app/assets/javascripts/issuables_list/index.js +++ b/app/assets/javascripts/issuables_list/index.js @@ -36,7 +36,7 @@ function mountIssuableListRootApp() { } function mountIssuablesListApp() { - if (!gon.features?.vueIssuablesList && !gon.features?.jiraIntegration) { + if (!gon.features?.vueIssuablesList && !gon.features?.jiraIssuesIntegration) { return; } diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js index 0472b8cf51f..c6c34b831ee 100644 --- a/app/assets/javascripts/lib/utils/url_utility.js +++ b/app/assets/javascripts/lib/utils/url_utility.js @@ -344,9 +344,15 @@ export function objectToQuery(obj) { * @param {Object} params The query params to be set/updated * @param {String} url The url to be operated on * @param {Boolean} clearParams Indicates whether existing query params should be removed or not + * @param {Boolean} railsArraySyntax When enabled, changes the array syntax from `keys=` to `keys[]=` according to Rails conventions * @returns {String} A copy of the original with the updated query params */ -export const setUrlParams = (params, url = window.location.href, clearParams = false) => { +export const setUrlParams = ( + params, + url = window.location.href, + clearParams = false, + railsArraySyntax = false, +) => { const urlObj = new URL(url); const queryString = urlObj.search; const searchParams = clearParams ? new URLSearchParams('') : new URLSearchParams(queryString); @@ -355,11 +361,12 @@ export const setUrlParams = (params, url = window.location.href, clearParams = f if (params[key] === null || params[key] === undefined) { searchParams.delete(key); } else if (Array.isArray(params[key])) { + const keyName = railsArraySyntax ? `${key}[]` : key; params[key].forEach((val, idx) => { if (idx === 0) { - searchParams.set(key, val); + searchParams.set(keyName, val); } else { - searchParams.append(key, val); + searchParams.append(keyName, val); } }); } else { diff --git a/app/assets/javascripts/pages/projects/edit/index.js b/app/assets/javascripts/pages/projects/edit/index.js index 9fb07917f9b..63762e414df 100644 --- a/app/assets/javascripts/pages/projects/edit/index.js +++ b/app/assets/javascripts/pages/projects/edit/index.js @@ -7,11 +7,13 @@ import dirtySubmitFactory from '~/dirty_submit/dirty_submit_factory'; import initFilePickers from '~/file_pickers'; import initProjectLoadingSpinner from '../shared/save_project_loader'; import initProjectPermissionsSettings from '../shared/permissions'; +import initProjectRemoveModal from '~/projects/project_remove_modal'; document.addEventListener('DOMContentLoaded', () => { initFilePickers(); initConfirmDangerModal(); initSettingsPanels(); + initProjectRemoveModal(); mountBadgeSettings(PROJECT_BADGE); initProjectLoadingSpinner(); diff --git a/app/assets/javascripts/projects/components/remove_modal.vue b/app/assets/javascripts/projects/components/remove_modal.vue new file mode 100644 index 00000000000..37f58efcb30 --- /dev/null +++ b/app/assets/javascripts/projects/components/remove_modal.vue @@ -0,0 +1,108 @@ +<script> +import { GlModal, GlModalDirective, GlSprintf, GlFormInput, GlButton } from '@gitlab/ui'; +import { __ } from '~/locale'; +import { rstrip } from '~/lib/utils/common_utils'; +import csrf from '~/lib/utils/csrf'; + +export default { + components: { + GlModal, + GlSprintf, + GlFormInput, + GlButton, + }, + directives: { + GlModal: GlModalDirective, + }, + props: { + confirmPhrase: { + type: String, + required: true, + }, + warningMessage: { + type: String, + required: true, + }, + formPath: { + type: String, + required: true, + }, + }, + data() { + return { + userInput: null, + }; + }, + computed: { + buttonDisabled() { + return rstrip(this.userInput) !== this.confirmPhrase; + }, + csrfToken() { + return csrf.token; + }, + }, + methods: { + submitForm() { + this.$refs.form.submit(); + }, + }, + strings: { + removeProject: __('Remove project'), + title: __('Confirmation required'), + confirm: __('Confirm'), + dataLoss: __( + 'This action can lead to data loss. To prevent accidental actions we ask you to confirm your intention.', + ), + confirmText: __('Please type %{phrase_code} to proceed or close this modal to cancel.'), + }, + modalId: 'remove-project-modal', +}; +</script> + +<template> + <form ref="form" :action="formPath" method="post"> + <input type="hidden" name="_method" value="delete" /> + <input :value="csrfToken" type="hidden" name="authenticity_token" /> + <gl-button v-gl-modal="$options.modalId" category="primary" variant="danger">{{ + $options.strings.removeProject + }}</gl-button> + <gl-modal + ref="removeModal" + :modal-id="$options.modalId" + size="sm" + ok-variant="danger" + footer-class="bg-gray-light gl-p-5" + > + <template #modal-title>{{ $options.strings.title }}</template> + <template #modal-footer> + <div class="gl-w-full gl-display-flex gl-just-content-start gl-m-0"> + <gl-button + :disabled="buttonDisabled" + category="primary" + variant="danger" + @click="submitForm" + > + {{ $options.strings.confirm }} + </gl-button> + </div> + </template> + <div> + <p class="gl-text-red-500 gl-font-weight-bold">{{ warningMessage }}</p> + <p class="gl-mb-0">{{ $options.strings.dataLoss }}</p> + <p> + <gl-sprintf :message="$options.strings.confirmText"> + <template #phrase_code> + <code>{{ confirmPhrase }}</code> + </template> + </gl-sprintf> + </p> + <gl-form-input + id="confirm_name_input" + v-model="userInput" + name="confirm_name_input" + type="text" + /> + </div> + </gl-modal> + </form> +</template> diff --git a/app/assets/javascripts/projects/project_remove_modal.js b/app/assets/javascripts/projects/project_remove_modal.js new file mode 100644 index 00000000000..dbdad1bf6f1 --- /dev/null +++ b/app/assets/javascripts/projects/project_remove_modal.js @@ -0,0 +1,24 @@ +import Vue from 'vue'; +import RemoveProjectModal from './components/remove_modal.vue'; + +export default (selector = '#js-confirm-project-remove') => { + const el = document.querySelector(selector); + + if (!el) return; + + const { formPath, confirmPhrase, warningMessage } = el.dataset; + + // eslint-disable-next-line no-new + new Vue({ + el, + render(createElement) { + return createElement(RemoveProjectModal, { + props: { + confirmPhrase, + warningMessage, + formPath, + }, + }); + }, + }); +}; diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js index 7bb6c275832..34cb74efabe 100644 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js +++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js @@ -1,4 +1,4 @@ -import renderHtml from './renderers/render_html'; +import renderBlockHtml from './renderers/render_html_block'; import renderKramdownList from './renderers/render_kramdown_list'; import renderKramdownText from './renderers/render_kramdown_text'; import renderIdentifierParagraph from './renderers/render_identifier_paragraph'; @@ -6,7 +6,7 @@ import renderEmbeddedRubyText from './renderers/render_embedded_ruby_text'; import renderFontAwesomeHtmlInline from './renderers/render_font_awesome_html_inline'; const htmlInlineRenderers = [renderFontAwesomeHtmlInline]; -const htmlRenderers = [renderHtml]; +const htmlBlockRenderers = [renderBlockHtml]; const listRenderers = [renderKramdownList]; const paragraphRenderers = [renderIdentifierParagraph]; const textRenderers = [renderKramdownText, renderEmbeddedRubyText]; @@ -32,9 +32,9 @@ const buildCustomHTMLRenderer = ( ) => { const defaults = { htmlBlock(node, context) { - const allHtmlRenderers = [...customRenderers.list, ...htmlRenderers]; + const allHtmlBlockRenderers = [...customRenderers.htmlBlock, ...htmlBlockRenderers]; - return executeRenderer(allHtmlRenderers, node, context); + return executeRenderer(allHtmlBlockRenderers, node, context); }, htmlInline(node, context) { const allHtmlInlineRenderers = [...customRenderers.htmlInline, ...htmlInlineRenderers]; @@ -47,7 +47,7 @@ const buildCustomHTMLRenderer = ( return executeRenderer(allListRenderers, node, context); }, paragraph(node, context) { - const allParagraphRenderers = [...customRenderers.list, ...paragraphRenderers]; + const allParagraphRenderers = [...customRenderers.paragraph, ...paragraphRenderers]; return executeRenderer(allParagraphRenderers, node, context); }, diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token.js index c81478a8405..6937d2acb47 100644 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token.js +++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token.js @@ -4,25 +4,36 @@ const buildToken = (type, tagName, props) => { const TAG_TYPES = { block: 'div', - inline: 'span', + inline: 'a', }; -export const buildUneditableOpenTokens = (token, type = TAG_TYPES.block) => { - return [ - buildToken('openTag', type, { - attributes: { contenteditable: false }, - classNames: [ - 'gl-px-4 gl-py-2 gl-opacity-5 gl-bg-gray-100 gl-user-select-none gl-cursor-not-allowed', - ], - }), - token, - ]; +// Open helpers (singular and multiple) + +const buildUneditableOpenToken = (tagType = TAG_TYPES.block) => + buildToken('openTag', tagType, { + attributes: { contenteditable: false }, + classNames: [ + 'gl-px-4 gl-py-2 gl-opacity-5 gl-bg-gray-100 gl-user-select-none gl-cursor-not-allowed', + ], + }); + +export const buildUneditableOpenTokens = (token, tagType = TAG_TYPES.block) => { + return [buildUneditableOpenToken(tagType), token]; +}; + +// Close helpers (singular and multiple) + +export const buildUneditableCloseToken = (tagType = TAG_TYPES.block) => + buildToken('closeTag', tagType); + +export const buildUneditableCloseTokens = (token, tagType = TAG_TYPES.block) => { + return [token, buildUneditableCloseToken(tagType)]; }; -export const buildUneditableCloseToken = (type = TAG_TYPES.block) => buildToken('closeTag', type); +// Complete helpers (open plus close) -export const buildUneditableCloseTokens = (token, type = TAG_TYPES.block) => { - return [token, buildUneditableCloseToken(type)]; +export const buildUneditableTokens = token => { + return [...buildUneditableOpenTokens(token), buildUneditableCloseToken()]; }; export const buildUneditableInlineTokens = token => { @@ -32,6 +43,19 @@ export const buildUneditableInlineTokens = token => { ]; }; -export const buildUneditableTokens = token => { - return [...buildUneditableOpenTokens(token), buildUneditableCloseToken()]; +export const buildUneditableHtmlAsTextTokens = node => { + /* + Toast UI internally appends ' data-tomark-pass ' attribute flags so it can target certain + nested nodes for internal use during Markdown <=> WYSIWYG conversions. In our case, we want + to prevent HTML being rendered completely in WYSIWYG mode and thus we use a `text` vs. `html` + type when building the token. However, in doing so, we need to strip out the ` data-tomark-pass ` + to prevent their persistence within the `text` content as the user did not intend these as edits. + + https://github.com/nhn/tui.editor/blob/cc54ec224fc3a4b6e5a2b19a71650959f41adc0e/apps/editor/src/js/convertor.js#L72 + */ + const regex = / data-tomark-pass /gm; + const content = node.literal.replace(regex, ''); + const htmlAsTextToken = buildToken('text', null, { content }); + + return [buildUneditableOpenToken(), htmlAsTextToken, buildUneditableCloseToken()]; }; diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_html.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_html.js deleted file mode 100644 index a3b467851dc..00000000000 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_html.js +++ /dev/null @@ -1,9 +0,0 @@ -import { buildUneditableTokens } from './build_uneditable_token'; - -const canRender = ({ type }) => { - return type === 'htmlBlock'; -}; - -const render = (_, { origin }) => buildUneditableTokens(origin()); - -export default { canRender, render }; diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_html_block.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_html_block.js new file mode 100644 index 00000000000..b179ca61dba --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_html_block.js @@ -0,0 +1,9 @@ +import { buildUneditableHtmlAsTextTokens } from './build_uneditable_token'; + +const canRender = ({ type }) => { + return type === 'htmlBlock'; +}; + +const render = node => buildUneditableHtmlAsTextTokens(node); + +export default { canRender, render }; |