diff --git a/app/assets/javascripts/packages/details/components/maven_installation.vue b/app/assets/javascripts/packages/details/components/maven_installation.vue index c2f6f76967b597e17fc7b1dc0963ebe3b090dd55..35d3e293304e77c9d6eaac0165612e22761187da 100644 --- a/app/assets/javascripts/packages/details/components/maven_installation.vue +++ b/app/assets/javascripts/packages/details/components/maven_installation.vue @@ -12,9 +12,17 @@ export default { GlLink, GlSprintf, }, + data() { + return { + instructionType: 'maven', + }; + }, computed: { ...mapState(['mavenHelpPath']), ...mapGetters(['mavenInstallationXml', 'mavenInstallationCommand', 'mavenSetupXml']), + showMaven() { + return this.instructionType === 'maven'; + }, }, i18n: { xmlText: s__( @@ -36,50 +44,51 @@ export default { <div> <h3 class="gl-font-lg">{{ __('Installation') }}</h3> - <p> - <gl-sprintf :message="$options.i18n.xmlText"> - <template #code="{ content }"> - <code>{{ content }}</code> - </template> - </gl-sprintf> - </p> + <template v-if="showMaven"> + <p> + <gl-sprintf :message="$options.i18n.xmlText"> + <template #code="{ content }"> + <code>{{ content }}</code> + </template> + </gl-sprintf> + </p> - <code-instruction - :label="s__('PackageRegistry|Maven XML')" - :instruction="mavenInstallationXml" - :copy-text="s__('PackageRegistry|Copy Maven XML')" - multiline - :tracking-action="$options.trackingActions.COPY_MAVEN_XML" - :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION" - /> + <code-instruction + :instruction="mavenInstallationXml" + :copy-text="s__('PackageRegistry|Copy Maven XML')" + :tracking-action="$options.trackingActions.COPY_MAVEN_XML" + :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION" + multiline + /> - <code-instruction - :label="s__('PackageRegistry|Maven Command')" - :instruction="mavenInstallationCommand" - :copy-text="s__('PackageRegistry|Copy Maven command')" - :tracking-action="$options.trackingActions.COPY_MAVEN_COMMAND" - :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION" - /> + <code-instruction + :label="s__('PackageRegistry|Maven Command')" + :instruction="mavenInstallationCommand" + :copy-text="s__('PackageRegistry|Copy Maven command')" + :tracking-action="$options.trackingActions.COPY_MAVEN_COMMAND" + :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION" + /> - <h3 class="gl-font-lg">{{ __('Registry setup') }}</h3> - <p> - <gl-sprintf :message="$options.i18n.setupText"> - <template #code="{ content }"> - <code>{{ content }}</code> + <h3 class="gl-font-lg">{{ s__('PackageRegistry|Registry setup') }}</h3> + <p> + <gl-sprintf :message="$options.i18n.setupText"> + <template #code="{ content }"> + <code>{{ content }}</code> + </template> + </gl-sprintf> + </p> + <code-instruction + :instruction="mavenSetupXml" + :copy-text="s__('PackageRegistry|Copy Maven registry XML')" + :tracking-action="$options.trackingActions.COPY_MAVEN_SETUP" + :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION" + multiline + /> + <gl-sprintf :message="$options.i18n.helpText"> + <template #link="{ content }"> + <gl-link :href="mavenHelpPath" target="_blank">{{ content }}</gl-link> </template> </gl-sprintf> - </p> - <code-instruction - :instruction="mavenSetupXml" - :copy-text="s__('PackageRegistry|Copy Maven registry XML')" - multiline - :tracking-action="$options.trackingActions.COPY_MAVEN_SETUP" - :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION" - /> - <gl-sprintf :message="$options.i18n.helpText"> - <template #link="{ content }"> - <gl-link :href="mavenHelpPath" target="_blank">{{ content }}</gl-link> - </template> - </gl-sprintf> + </template> </div> </template> diff --git a/app/assets/javascripts/vue_shared/components/registry/code_instruction.vue b/app/assets/javascripts/vue_shared/components/registry/code_instruction.vue index bc7f8a2b17a5baa4638d2bde0d2e255ecd696642..1a85a641dd194f130dea8e6446f3fbe18fc3651b 100644 --- a/app/assets/javascripts/vue_shared/components/registry/code_instruction.vue +++ b/app/assets/javascripts/vue_shared/components/registry/code_instruction.vue @@ -56,27 +56,29 @@ export default { </script> <template> - <div v-if="!multiline" class="gl-mb-3"> + <div> <label v-if="label" :for="generateFormId('instruction-input')">{{ label }}</label> - <div class="input-group gl-mb-3"> - <input - :id="generateFormId('instruction-input')" - :value="instruction" - type="text" - class="form-control gl-font-monospace" - data-testid="instruction-input" - readonly - @copy="trackCopy" - /> - <span class="input-group-append" data-testid="instruction-button" @click="trackCopy"> - <clipboard-button :text="instruction" :title="copyText" class="input-group-text" /> - </span> + <div v-if="!multiline" class="gl-mb-3"> + <div class="input-group gl-mb-3"> + <input + :id="generateFormId('instruction-input')" + :value="instruction" + type="text" + class="form-control gl-font-monospace" + data-testid="instruction-input" + readonly + @copy="trackCopy" + /> + <span class="input-group-append" data-testid="instruction-button" @click="trackCopy"> + <clipboard-button :text="instruction" :title="copyText" class="input-group-text" /> + </span> + </div> </div> - </div> - <div v-else> - <pre class="gl-font-monospace" data-testid="multiline-instruction" @copy="trackCopy">{{ - instruction - }}</pre> + <div v-else> + <pre class="gl-font-monospace" data-testid="multiline-instruction" @copy="trackCopy">{{ + instruction + }}</pre> + </div> </div> </template> diff --git a/app/assets/javascripts/vue_shared/components/registry/persisted_dropdown_selection.vue b/app/assets/javascripts/vue_shared/components/registry/persisted_dropdown_selection.vue new file mode 100644 index 0000000000000000000000000000000000000000..36b1a9c49f4c09bd5f3598aaa6d43ff08e65b35d --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/registry/persisted_dropdown_selection.vue @@ -0,0 +1,59 @@ +<script> +import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; +import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; + +export default { + name: 'PersistedDropdownSelection', + components: { + GlDropdown, + GlDropdownItem, + LocalStorageSync, + }, + props: { + options: { + type: Array, + required: true, + }, + storageKey: { + type: String, + required: true, + }, + }, + data() { + return { + selected: null, + }; + }, + computed: { + dropdownText() { + const selected = this.parsedOptions.find((o) => o.selected); + return selected?.label || this.options[0].label; + }, + parsedOptions() { + return this.options.map((o) => ({ ...o, selected: o.value === this.selected })); + }, + }, + methods: { + setSelected(value) { + this.selected = value; + this.$emit('change', value); + }, + }, +}; +</script> + +<template> + <local-storage-sync :storage-key="storageKey" :value="selected" @input="setSelected"> + <gl-dropdown :text="dropdownText" lazy> + <gl-dropdown-item + v-for="option in parsedOptions" + :key="option.value" + :is-checked="option.selected" + :is-check-item="true" + @click="setSelected(option.value)" + > + {{ option.label }} + </gl-dropdown-item> + </gl-dropdown> + </local-storage-sync> +</template> diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 9d9452a3a9d8d74715a5a2f0c4fa9452f1962466..7d4e91afdcd1eee23936f0593c88db26d131d435 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -21617,9 +21617,6 @@ msgstr "" msgid "PackageRegistry|Maven Command" msgstr "" -msgid "PackageRegistry|Maven XML" -msgstr "" - msgid "PackageRegistry|NuGet" msgstr "" @@ -21656,6 +21653,9 @@ msgstr "" msgid "PackageRegistry|Recipe: %{recipe}" msgstr "" +msgid "PackageRegistry|Registry setup" +msgstr "" + msgid "PackageRegistry|Remove package" msgstr "" diff --git a/spec/frontend/packages/details/components/__snapshots__/maven_installation_spec.js.snap b/spec/frontend/packages/details/components/__snapshots__/maven_installation_spec.js.snap index 6d22b372d41c394f4561a3fe43e83952fd333ee1..b615a8035a339bb64ba2a9af9be5d6b54552bef4 100644 --- a/spec/frontend/packages/details/components/__snapshots__/maven_installation_spec.js.snap +++ b/spec/frontend/packages/details/components/__snapshots__/maven_installation_spec.js.snap @@ -17,7 +17,7 @@ exports[`MavenInstallation renders all the messages 1`] = ` <code-instruction-stub copytext="Copy Maven XML" instruction="foo/xml" - label="Maven XML" + label="" multiline="true" trackingaction="copy_maven_xml" trackinglabel="code_instruction" diff --git a/spec/frontend/vue_shared/components/registry/__snapshots__/code_instruction_spec.js.snap b/spec/frontend/vue_shared/components/registry/__snapshots__/code_instruction_spec.js.snap index da49778f216079e4aab1cd16239fc3bb603175d3..30b7f0c2d28b489510e44098875008b955a70c87 100644 --- a/spec/frontend/vue_shared/components/registry/__snapshots__/code_instruction_spec.js.snap +++ b/spec/frontend/vue_shared/components/registry/__snapshots__/code_instruction_spec.js.snap @@ -2,20 +2,26 @@ exports[`Package code instruction multiline to match the snapshot 1`] = ` <div> - <pre - class="gl-font-monospace" - data-testid="multiline-instruction" + <label + for="instruction-input_3" > - this is some + foo_label + </label> + + <div> + <pre + class="gl-font-monospace" + data-testid="multiline-instruction" + > + this is some multiline text - </pre> + </pre> + </div> </div> `; exports[`Package code instruction single line to match the default snapshot 1`] = ` -<div - class="gl-mb-3" -> +<div> <label for="instruction-input_2" > @@ -23,42 +29,46 @@ exports[`Package code instruction single line to match the default snapshot 1`] </label> <div - class="input-group gl-mb-3" + class="gl-mb-3" > - <input - class="form-control gl-font-monospace" - data-testid="instruction-input" - id="instruction-input_2" - readonly="readonly" - type="text" - /> - - <span - class="input-group-append" - data-testid="instruction-button" + <div + class="input-group gl-mb-3" > - <button - aria-label="Copy this value" - class="btn input-group-text btn-default btn-md gl-button btn-default-secondary btn-icon" - data-clipboard-text="npm i @my-package" - title="Copy npm install command" - type="button" + <input + class="form-control gl-font-monospace" + data-testid="instruction-input" + id="instruction-input_2" + readonly="readonly" + type="text" + /> + + <span + class="input-group-append" + data-testid="instruction-button" > - <!----> - - <svg - aria-hidden="true" - class="gl-button-icon gl-icon s16" - data-testid="copy-to-clipboard-icon" + <button + aria-label="Copy this value" + class="btn input-group-text btn-default btn-md gl-button btn-default-secondary btn-icon" + data-clipboard-text="npm i @my-package" + title="Copy npm install command" + type="button" > - <use - href="#copy-to-clipboard" - /> - </svg> - - <!----> - </button> - </span> + <!----> + + <svg + aria-hidden="true" + class="gl-button-icon gl-icon s16" + data-testid="copy-to-clipboard-icon" + > + <use + href="#copy-to-clipboard" + /> + </svg> + + <!----> + </button> + </span> + </div> </div> </div> `; diff --git a/spec/frontend/vue_shared/components/registry/persisted_dropdown_selection_spec.js b/spec/frontend/vue_shared/components/registry/persisted_dropdown_selection_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..c65ded000d32cafafa2e794c53f55506124b2988 --- /dev/null +++ b/spec/frontend/vue_shared/components/registry/persisted_dropdown_selection_spec.js @@ -0,0 +1,122 @@ +import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; +import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; +import component from '~/vue_shared/components/registry/persisted_dropdown_selection.vue'; + +describe('Persisted dropdown selection', () => { + let wrapper; + + const defaultProps = { + storageKey: 'foo_bar', + options: [ + { value: 'maven', label: 'Maven' }, + { value: 'gradle', label: 'Gradle' }, + ], + }; + + function createComponent({ props = {}, data = {} } = {}) { + wrapper = shallowMount(component, { + propsData: { + ...defaultProps, + ...props, + }, + data() { + return data; + }, + }); + } + + const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync); + const findDropdown = () => wrapper.findComponent(GlDropdown); + const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('local storage sync', () => { + it('uses the local storage sync component', () => { + createComponent(); + + expect(findLocalStorageSync().exists()).toBe(true); + }); + + it('passes the right props', () => { + createComponent({ data: { selected: 'foo' } }); + + expect(findLocalStorageSync().props()).toMatchObject({ + storageKey: defaultProps.storageKey, + value: 'foo', + }); + }); + + it('on input event updates the model and emits event', async () => { + const inputPayload = 'bar'; + createComponent(); + findLocalStorageSync().vm.$emit('input', inputPayload); + + await nextTick(); + + expect(wrapper.emitted('change')).toStrictEqual([[inputPayload]]); + expect(findLocalStorageSync().props('value')).toBe(inputPayload); + }); + }); + + describe('dropdown', () => { + it('has a dropdown component', () => { + createComponent(); + + expect(findDropdown().exists()).toBe(true); + }); + + describe('dropdown text', () => { + it('when no selection shows the first', () => { + createComponent(); + + expect(findDropdown().props('text')).toBe('Maven'); + }); + + it('when an option is selected, shows that option label', () => { + createComponent({ data: { selected: defaultProps.options[1].value } }); + + expect(findDropdown().props('text')).toBe('Gradle'); + }); + }); + + describe('dropdown items', () => { + it('has one item for each option', () => { + createComponent(); + + expect(findDropdownItems()).toHaveLength(defaultProps.options.length); + }); + + it('binds the correct props', () => { + createComponent({ data: { selected: defaultProps.options[0].value } }); + + expect(findDropdownItems().at(0).props()).toMatchObject({ + isChecked: true, + isCheckItem: true, + }); + + expect(findDropdownItems().at(1).props()).toMatchObject({ + isChecked: false, + isCheckItem: true, + }); + }); + + it('on click updates the data and emits event', async () => { + createComponent({ data: { selected: defaultProps.options[0].value } }); + expect(findDropdownItems().at(0).props('isChecked')).toBe(true); + + findDropdownItems().at(1).vm.$emit('click'); + + await nextTick(); + + expect(wrapper.emitted('change')).toStrictEqual([['gradle']]); + expect(findDropdownItems().at(0).props('isChecked')).toBe(false); + expect(findDropdownItems().at(1).props('isChecked')).toBe(true); + }); + }); + }); +});