Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2025-03-17 00:07:13 +00:00
parent 8b430e689c
commit a4dacb5d7e
14 changed files with 751 additions and 150 deletions

View File

@ -1,20 +1,25 @@
<script>
import { uniqueId, isObject } from 'lodash';
import { GlAlert, GlButton, GlForm, GlFormFields, GlFormTextarea } from '@gitlab/ui';
import { isObject } from 'lodash';
import { GlAccordion, GlAccordionItem, GlAlert, GlToggle } from '@gitlab/ui';
import { updateApplicationSettings } from '~/rest_api';
import { sprintf, s__, __ } from '~/locale';
import { sprintf, s__ } from '~/locale';
import { logError } from '~/lib/logger';
import toast from '~/vue_shared/plugins/global_toast';
import SettingsForm from './settings_form.vue';
export default {
components: {
GlAccordion,
GlAccordionItem,
GlAlert,
GlButton,
GlForm,
GlFormFields,
GlFormTextarea,
GlToggle,
SettingsForm,
},
props: {
presets: {
type: Array,
required: true,
},
initialSettings: {
type: Object,
required: false,
@ -23,18 +28,12 @@ export default {
},
data() {
return {
formId: uniqueId('extension-marketplace-settings-form-'),
formValues: {
settings: JSON.stringify(this.initialSettings, null, 2),
},
isEnabled: Boolean(this.initialSettings.enabled),
isLoading: false,
errorMessage: '',
};
},
computed: {
payload() {
return JSON.parse(this.formValues.settings);
},
errorContent() {
if (!this.errorMessage) {
return null;
@ -61,22 +60,51 @@ export default {
),
};
},
submitButtonAttrs() {
return {
'aria-describedby': 'extension-marketplace-settings-error-alert',
loading: this.isLoading,
};
},
},
methods: {
async onSubmit() {
async submitEnabled(enabled) {
// NOTE: We can update just `vscode_extension_marketplace_enabled` to control enabled without touching the rest
const isSuccess = await this.submit({ vscode_extension_marketplace_enabled: enabled });
if (isSuccess) {
this.isEnabled = enabled;
}
},
async submitForm(values) {
// NOTE: We'll go ahead and update all of `vscode_extension_marketplace`.
// Let's spread ontop of original `initialSettings` so that we don't unintentionally
// overwrite anything.
return this.submit({
vscode_extension_marketplace: {
...this.initialSettings,
enabled: this.isEnabled,
...values,
},
});
},
/**
* @return {boolean} Whether the `submit` was successful or not. This encapsulated error handling already.
*/
async submit(params) {
if (this.isLoading) {
return;
return false;
}
this.isLoading = true;
this.errorMessage = '';
try {
await updateApplicationSettings({
vscode_extension_marketplace: this.payload,
});
await updateApplicationSettings(params);
toast(s__('ExtensionMarketplace|Extension marketplace settings updated.'));
return true;
} catch (e) {
// eslint-disable-next-line @gitlab/require-i18n-strings
logError('Failed to update extension marketplace settings. See error info:', e);
@ -84,66 +112,56 @@ export default {
this.errorMessage =
e?.response?.data?.message ||
s__('ExtensionMarketplace|An unknown error occurred. Please try again.');
return false;
} finally {
this.isLoading = false;
}
},
},
FIELDS: {
settings: {
label: __('Settings'),
},
},
FIELDS: {},
MSG_ENABLE_LABEL: s__('ExtensionMarketplace|Enable Extension Marketplace'),
MSG_ENABLE_DESCRIPTION: s__(
'ExtensionMarketplace|Enable the VS Code extension marketplace for all users.',
),
MSG_INNER_FORM: s__('ExtensionMarketplace|Extension registry settings'),
};
</script>
<template>
<div>
<gl-form :id="formId" @submit.prevent>
<gl-alert
v-if="errorContent"
id="extensions-marketplace-settings-error-alert"
class="gl-mb-3"
variant="danger"
:dismissible="false"
>
{{ errorContent.title }}
<ul v-if="errorContent.list" class="gl-mb-0 gl-mt-3">
<li v-for="({ key, value }, idx) in errorContent.list" :key="idx">
<code>{{ key }}</code>
<span>:</span>
<span>{{ value }}</span>
</li>
</ul>
</gl-alert>
<gl-form-fields
v-model="formValues"
:fields="$options.FIELDS"
:form-id="formId"
@submit="onSubmit"
>
<template #input(settings)="{ id, value, input, blur }">
<gl-form-textarea
:id="id"
class="!gl-font-monospace"
:value="value"
:state="!Boolean(errorContent)"
@input="input"
@blur="blur"
/>
</template>
</gl-form-fields>
<div class="gl-flex">
<gl-button
class="js-no-auto-disable"
aria-describedby="extensions-marketplace-settings-error-alert"
type="submit"
variant="confirm"
category="primary"
:loading="isLoading"
>
{{ __('Save changes') }}
</gl-button>
</div>
</gl-form>
<gl-alert
v-if="errorContent"
id="extension-marketplace-settings-error-alert"
class="gl-mb-3"
variant="danger"
:dismissible="false"
>
{{ errorContent.title }}
<ul v-if="errorContent.list" class="gl-mb-0 gl-mt-3">
<li v-for="({ key, value }, idx) in errorContent.list" :key="idx">
<code>{{ key }}</code>
<span>:</span>
<span>{{ value }}</span>
</li>
</ul>
</gl-alert>
<gl-toggle
:value="isEnabled"
:is-loading="isLoading"
:label="$options.MSG_ENABLE_LABEL"
:help="$options.MSG_ENABLE_DESCRIPTION"
label-position="top"
@change="submitEnabled"
/>
<gl-accordion :header-level="3" class="gl-pt-3">
<gl-accordion-item :title="$options.MSG_INNER_FORM" class="gl-font-normal">
<settings-form
:presets="presets"
:initial-settings="initialSettings"
:submit-button-attrs="submitButtonAttrs"
@submit="submitForm"
/>
</gl-accordion-item>
</gl-accordion>
</div>
</template>

View File

@ -0,0 +1,215 @@
<script>
import { uniqueId, pick } from 'lodash';
import { GlButton, GlForm, GlFormFields, GlIcon, GlLink, GlSprintf, GlToggle } from '@gitlab/ui';
import { s__ } from '~/locale';
import { isValidURL, isAbsolute } from '~/lib/utils/url_utility';
import { PRESET_OPEN_VSX, PRESET_CUSTOM } from '../constants';
const validateRequired = (invalidMsg) => (val) => {
if (val) {
return '';
}
return invalidMsg;
};
const validateURL = (invalidMsg) => (val) => {
if (isAbsolute(val) && isValidURL(val)) {
return '';
}
return invalidMsg;
};
const MSG_VALID_URL = s__('ExtensionMarketplace|A valid URL is required.');
const createUrlField = (label) => ({
label,
validators: [validateURL(MSG_VALID_URL), validateRequired(MSG_VALID_URL)],
inputAttrs: {
width: 'lg',
placeholder: 'https://...',
},
});
const createReadonlyUrlField = (label) => ({
label,
inputAttrs: {
width: 'lg',
readonly: true,
'aria-description': s__(
'ExtensionMarketplace|Disable Open VSX extension registry to set a custom value for this field.',
),
},
});
const FIELDS = {
useOpenVsx: {
label: s__('ExtensionMarketplace|Use Open VSX extension registry'),
},
serviceUrl: createUrlField(s__('ExtensionMarketplace|Service URL')),
itemUrl: createUrlField(s__('ExtensionMarketplace|Item URL')),
resourceUrlTemplate: createUrlField(s__('ExtensionMarketplace|Resource URL Template')),
presetServiceUrl: createReadonlyUrlField(s__('ExtensionMarketplace|Service URL')),
presetItemUrl: createReadonlyUrlField(s__('ExtensionMarketplace|Item URL')),
presetResourceUrlTemplate: createReadonlyUrlField(
s__('ExtensionMarketplace|Resource URL Template'),
),
};
export default {
components: {
GlButton,
GlForm,
GlFormFields,
GlIcon,
GlLink,
GlSprintf,
GlToggle,
},
props: {
presets: {
type: Array,
required: true,
},
initialSettings: {
type: Object,
required: false,
default: () => ({}),
},
submitButtonAttrs: {
type: Object,
required: false,
default: () => ({}),
},
},
data() {
const { preset, custom_values: initialCustomValues } = this.initialSettings;
return {
formId: uniqueId('extension-marketplace-settings-form-'),
formValues: {
useOpenVsx: !preset || preset === PRESET_OPEN_VSX,
// URL fields for "custom" preset.
serviceUrl: '',
itemUrl: '',
resourceUrlTemplate: '',
// URL fields for other presets. The inputs are readonly.
presetServiceUrl: '',
presetItemUrl: '',
presetResourceUrlTemplate: '',
},
customValues: {
serviceUrl: initialCustomValues?.service_url || '',
itemUrl: initialCustomValues?.item_url || '',
resourceUrlTemplate: initialCustomValues?.resource_url_template || '',
},
};
},
computed: {
preset() {
return this.formValues.useOpenVsx ? PRESET_OPEN_VSX : PRESET_CUSTOM;
},
isCustomPreset() {
return this.preset === PRESET_CUSTOM;
},
payload() {
if (this.isCustomPreset) {
return {
preset: this.preset,
custom_values: {
service_url: this.formValues.serviceUrl,
item_url: this.formValues.itemUrl,
resource_url_template: this.formValues.resourceUrlTemplate,
},
};
}
return {
preset: this.preset,
};
},
formFields() {
if (this.isCustomPreset) {
return pick(FIELDS, ['useOpenVsx', 'serviceUrl', 'itemUrl', 'resourceUrlTemplate']);
}
return pick(FIELDS, [
'useOpenVsx',
'presetServiceUrl',
'presetItemUrl',
'presetResourceUrlTemplate',
]);
},
},
watch: {
preset: {
immediate: true,
handler(val, prevVal) {
if (prevVal === PRESET_CUSTOM) {
this.customValues.serviceUrl = this.formValues.serviceUrl;
this.customValues.itemUrl = this.formValues.itemUrl;
this.customValues.resourceUrlTemplate = this.formValues.resourceUrlTemplate;
}
if (val === PRESET_CUSTOM) {
this.formValues.serviceUrl = this.customValues.serviceUrl;
this.formValues.itemUrl = this.customValues.itemUrl;
this.formValues.resourceUrlTemplate = this.customValues.resourceUrlTemplate;
} else {
const values = this.presets.find(({ key }) => key === val)?.values;
this.formValues.presetServiceUrl = values?.serviceUrl;
this.formValues.presetItemUrl = values?.itemUrl;
this.formValues.presetResourceUrlTemplate = values?.resourceUrlTemplate;
}
},
},
},
methods: {
onSubmit() {
this.$emit('submit', this.payload);
},
},
MSG_OPEN_VSX_DESCRIPTION: s__(
'ExtensionMarketplace|Learn more about the %{linkStart}Open VSX Registry%{linkEnd}',
),
};
</script>
<template>
<gl-form :id="formId" @submit.prevent>
<gl-form-fields v-model="formValues" :fields="formFields" :form-id="formId" @submit="onSubmit">
<template #input(useOpenVsx)="{ id, value, input, blur }">
<gl-toggle
:id="id"
:label="formFields.useOpenVsx.label"
label-position="hidden"
:value="value"
@change="input"
@blur="blur"
/>
</template>
<template #group(useOpenVsx)-description>
<gl-sprintf :message="$options.MSG_OPEN_VSX_DESCRIPTION">
<template #link="{ content }">
<gl-link href="https://open-vsx.org/about" target="_blank"
>{{ content }} <gl-icon name="external-link"
/></gl-link>
</template>
</gl-sprintf>
</template>
</gl-form-fields>
<div class="gl-flex">
<gl-button
class="js-no-auto-disable"
type="submit"
variant="confirm"
category="primary"
v-bind="submitButtonAttrs"
>
{{ __('Save changes') }}
</gl-button>
</div>
</gl-form>
</template>

View File

@ -0,0 +1,2 @@
export const PRESET_OPEN_VSX = 'open_vsx';
export const PRESET_CUSTOM = 'custom';

View File

@ -587,7 +587,8 @@ module ApplicationSettingsHelper
:global_search_users_enabled,
:global_search_issues_enabled,
:global_search_merge_requests_enabled,
:vscode_extension_marketplace
:vscode_extension_marketplace,
:vscode_extension_marketplace_enabled
].tap do |settings|
unless Gitlab.com?
settings << :resource_usage_limits
@ -689,10 +690,15 @@ module ApplicationSettingsHelper
# NOTE: This is intentionally not scoped to a specific actor since it affects instance-level settings.
return unless Feature.enabled?(:vscode_extension_marketplace_settings, nil)
presets = ::WebIde::ExtensionMarketplacePreset.all.map do |preset|
preset.to_h.deep_transform_keys { |key| key.to_s.camelize(:lower) }
end
{
title: _('VS Code Extension Marketplace'),
description: vscode_extension_marketplace_settings_description,
view_model: {
presets: presets,
initialSettings: @application_setting.vscode_extension_marketplace || {}
}
}

View File

@ -120,7 +120,6 @@ class ApplicationSetting < ApplicationRecord
attribute :repository_storages_weighted, default: -> { {} }
attribute :kroki_formats, default: -> { {} }
attribute :default_branch_protection_defaults, default: -> { {} }
attribute :vscode_extension_marketplace, default: -> { {} }
chronic_duration_attr_writer :archive_builds_in_human_readable, :archive_builds_in_seconds
@ -950,6 +949,9 @@ class ApplicationSetting < ApplicationRecord
validates :vscode_extension_marketplace,
json_schema: { filename: "application_setting_vscode_extension_marketplace", detail_errors: true }
jsonb_accessor :vscode_extension_marketplace,
vscode_extension_marketplace_enabled: [:boolean, { default: false, store_key: :enabled }]
before_validation :ensure_uuid!
before_validation :coerce_repository_storages_weighted, if: :repository_storages_weighted_changed?
before_validation :normalize_default_branch_name

View File

@ -319,7 +319,7 @@ module ApplicationSettingImplementation
seat_control: 0,
show_migrate_from_jenkins_banner: true,
ropc_without_client_credentials: true,
vscode_extension_marketplace: {}
vscode_extension_marketplace_enabled: false
}.tap do |hsh|
hsh.merge!(non_production_defaults) unless Rails.env.production?
end

View File

@ -24507,18 +24507,48 @@ msgstr ""
msgid "Exported requirements"
msgstr ""
msgid "ExtensionMarketplace|A valid URL is required."
msgstr ""
msgid "ExtensionMarketplace|An unknown error occurred. Please try again."
msgstr ""
msgid "ExtensionMarketplace|Disable Open VSX extension registry to set a custom value for this field."
msgstr ""
msgid "ExtensionMarketplace|Enable Extension Marketplace"
msgstr ""
msgid "ExtensionMarketplace|Enable the VS Code extension marketplace for all users."
msgstr ""
msgid "ExtensionMarketplace|Extension marketplace settings updated."
msgstr ""
msgid "ExtensionMarketplace|Extension registry settings"
msgstr ""
msgid "ExtensionMarketplace|Failed to update extension marketplace settings."
msgstr ""
msgid "ExtensionMarketplace|Failed to update extension marketplace settings. %{message}"
msgstr ""
msgid "ExtensionMarketplace|Item URL"
msgstr ""
msgid "ExtensionMarketplace|Learn more about the %{linkStart}Open VSX Registry%{linkEnd}"
msgstr ""
msgid "ExtensionMarketplace|Resource URL Template"
msgstr ""
msgid "ExtensionMarketplace|Service URL"
msgstr ""
msgid "ExtensionMarketplace|Use Open VSX extension registry"
msgstr ""
msgid "External URL"
msgstr ""

View File

@ -1,18 +1,29 @@
import { nextTick } from 'vue';
import { GlAlert, GlButton, GlForm, GlFormFields, GlFormTextarea } from '@gitlab/ui';
import { GlAlert, GlAccordion, GlAccordionItem, GlToggle } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import { logError } from '~/lib/logger';
import SettingsApp from '~/vscode_extension_marketplace/components/settings_app.vue';
import SettingsForm from '~/vscode_extension_marketplace/components/settings_form.vue';
import toast from '~/vue_shared/plugins/global_toast';
import { PRESETS } from '../mock_data';
jest.mock('~/lib/logger');
jest.mock('~/vue_shared/plugins/global_toast');
jest.mock('lodash/uniqueId', () => (x) => `${x}testUnique`);
const TEST_NEW_SETTINGS = { enabled: false, preset: 'open_vsx' };
const EXPECTED_FORM_ID = 'extension-marketplace-settings-form-testUnique';
const TEST_NEW_SETTINGS = { preset: 'open_vsx' };
const TEST_INIT_SETTINGS = {
enabled: true,
preset: 'custom',
custom_values: {
item_url: 'abc',
service_url: 'def',
resource_url_template: 'ghi',
},
};
describe('~/vscode_extension_marketplace/components/settings_app.vue', () => {
let wrapper;
@ -22,18 +33,16 @@ describe('~/vscode_extension_marketplace/components/settings_app.vue', () => {
const createComponent = (props = {}) => {
wrapper = shallowMount(SettingsApp, {
propsData: {
presets: PRESETS,
...props,
},
stubs: {
GlFormFields,
},
});
};
const findForm = () => wrapper.findComponent(GlForm);
const findFormFields = () => wrapper.findComponent(GlFormFields);
const findTextarea = () => wrapper.findComponent(GlFormTextarea);
const findSaveButton = () => wrapper.findComponent(GlButton);
const findAccordion = () => wrapper.findComponent(GlAccordion);
const findAccordionItem = () => findAccordion().findComponent(GlAccordionItem);
const findSettingsForm = () => findAccordionItem().findComponent(SettingsForm);
const findToggle = () => wrapper.findComponent(GlToggle);
const findErrorAlert = () => wrapper.findComponent(GlAlert);
const findErrorAlertItems = () =>
findErrorAlert()
@ -58,63 +67,67 @@ describe('~/vscode_extension_marketplace/components/settings_app.vue', () => {
createComponent();
});
it('renders form', () => {
expect(findForm().attributes('id')).toBe(EXPECTED_FORM_ID);
it('renders toggle', () => {
expect(findToggle().props()).toMatchObject({
value: false,
isLoading: false,
label: 'Enable Extension Marketplace',
help: 'Enable the VS Code extension marketplace for all users.',
labelPosition: 'top',
});
});
it('renders form fields', () => {
expect(findFormFields().props()).toMatchObject({
formId: EXPECTED_FORM_ID,
values: {
settings: {},
it('renders accordion and accordion item', () => {
expect(findAccordion().props()).toMatchObject({
headerLevel: 3,
});
expect(findAccordionItem().props()).toMatchObject({
title: 'Extension registry settings',
});
});
it('renders inner form', () => {
expect(findSettingsForm().props()).toEqual({
initialSettings: {},
presets: PRESETS,
submitButtonAttrs: {
'aria-describedby': 'extension-marketplace-settings-error-alert',
loading: false,
},
fields: SettingsApp.FIELDS,
});
});
it('renders settings textarea', () => {
expect(findTextarea().attributes()).toMatchObject({
id: 'gl-form-field-testUnique',
value: '{}',
});
});
it('renders save button', () => {
expect(findSaveButton().attributes()).toMatchObject({
type: 'submit',
variant: 'confirm',
category: 'primary',
'aria-describedby': 'extensions-marketplace-settings-error-alert',
});
expect(findSaveButton().props('loading')).toBe(false);
expect(findSaveButton().text()).toBe('Save changes');
});
});
describe('when submitted', () => {
beforeEach(async () => {
describe('when enablement toggle is changed', () => {
beforeEach(() => {
createComponent();
findTextarea().vm.$emit('input', JSON.stringify(TEST_NEW_SETTINGS));
await nextTick();
findFormFields().vm.$emit('submit');
findToggle().vm.$emit('change', true);
});
it('triggers loading', () => {
expect(findSaveButton().props('loading')).toBe(true);
expect(findSettingsForm().props('submitButtonAttrs')).toEqual({
'aria-describedby': 'extension-marketplace-settings-error-alert',
loading: true,
});
expect(findToggle().props()).toMatchObject({
value: false,
isLoading: true,
});
});
it('makes submit request', () => {
expect(submitSpy).toHaveBeenCalledTimes(1);
expect(submitSpy).toHaveBeenCalledWith({
vscode_extension_marketplace: TEST_NEW_SETTINGS,
vscode_extension_marketplace_enabled: true,
});
});
it('while loading, prevents extra submit', () => {
findFormFields().vm.$emit('submit');
findFormFields().vm.$emit('submit');
findToggle().vm.$emit('change', true);
findToggle().vm.$emit('change', true);
expect(submitSpy).toHaveBeenCalledTimes(1);
});
@ -126,7 +139,7 @@ describe('~/vscode_extension_marketplace/components/settings_app.vue', () => {
expect(toast).toHaveBeenCalledTimes(1);
expect(toast).toHaveBeenCalledWith('Extension marketplace settings updated.');
expect(findSaveButton().props('loading')).toBe(false);
expect(findToggle().props('isLoading')).toBe(false);
});
it('does not show error alert', () => {
@ -134,13 +147,49 @@ describe('~/vscode_extension_marketplace/components/settings_app.vue', () => {
});
});
describe('with initial settings', () => {
beforeEach(() => {
createComponent({
initialSettings: TEST_INIT_SETTINGS,
});
});
it('initializes settings in toggle', () => {
expect(findToggle().props('value')).toBe(true);
});
it('initializes settings in form', () => {
expect(findSettingsForm().props('initialSettings')).toBe(TEST_INIT_SETTINGS);
});
it('when submitted, submits settings', async () => {
expect(submitSpy).not.toHaveBeenCalled();
findSettingsForm().vm.$emit('submit', TEST_NEW_SETTINGS);
await waitForPromises();
expect(submitSpy).toHaveBeenCalledTimes(1);
expect(submitSpy).toHaveBeenCalledWith({
vscode_extension_marketplace: {
enabled: true,
preset: 'open_vsx',
custom_values: {
item_url: 'abc',
service_url: 'def',
resource_url_template: 'ghi',
},
},
});
});
});
describe('when submitted and errored', () => {
beforeEach(() => {
submitSpy.mockReturnValue([400]);
createComponent();
findFormFields().vm.$emit('submit');
findSettingsForm().vm.$emit('submit', {});
});
it('shows error message', async () => {
@ -149,7 +198,7 @@ describe('~/vscode_extension_marketplace/components/settings_app.vue', () => {
await axios.waitForAll();
expect(findErrorAlert().exists()).toBe(true);
expect(findErrorAlert().attributes('id')).toBe('extensions-marketplace-settings-error-alert');
expect(findErrorAlert().attributes('id')).toBe('extension-marketplace-settings-error-alert');
expect(findErrorAlert().props('dismissible')).toBe(false);
expect(findErrorAlert().text()).toBe(
'Failed to update extension marketplace settings. An unknown error occurred. Please try again.',
@ -168,20 +217,12 @@ describe('~/vscode_extension_marketplace/components/settings_app.vue', () => {
);
});
it('updates state on textarea', async () => {
expect(findTextarea().attributes('state')).toBe('true');
await axios.waitForAll();
expect(findTextarea().attributes('state')).toBeUndefined();
});
it('hides error message with another submit', async () => {
await axios.waitForAll();
expect(findErrorAlert().exists()).toBe(true);
findFormFields().vm.$emit('submit');
findSettingsForm().vm.$emit('submit', TEST_NEW_SETTINGS);
await nextTick();
expect(findErrorAlert().exists()).toBe(false);
@ -197,7 +238,7 @@ describe('~/vscode_extension_marketplace/components/settings_app.vue', () => {
createComponent();
findFormFields().vm.$emit('submit');
findSettingsForm().vm.$emit('submit', TEST_NEW_SETTINGS);
});
it('shows error message', async () => {
@ -213,16 +254,4 @@ describe('~/vscode_extension_marketplace/components/settings_app.vue', () => {
]);
});
});
describe('with initialSettings', () => {
beforeEach(() => {
createComponent({
initialSettings: TEST_NEW_SETTINGS,
});
});
it('initializes the form with given settings', () => {
expect(findTextarea().props('value')).toBe(JSON.stringify(TEST_NEW_SETTINGS, null, 2));
});
});
});

View File

@ -0,0 +1,240 @@
import { GlForm, GlFormFields, GlToggle, GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import SettingsForm from '~/vscode_extension_marketplace/components/settings_form.vue';
import { PRESETS } from '../mock_data';
jest.mock('lodash/uniqueId', () => (x) => `${x}uniqueId`);
const TEST_FORM_ID = 'extension-marketplace-settings-form-uniqueId';
const TEST_SUBMIT_BUTTON_ATTRS = {
'aria-describedby': 'extension-marketplace-settings-error-alert',
};
const TEST_CUSTOM_VALUES = {
item_url: 'abc',
service_url: 'def',
resource_url_template: 'ghi',
};
describe('~/vscode_extension_marketplace/components/settings_form.vue', () => {
let wrapper;
const findForm = () => wrapper.findComponent(GlForm);
const findFormFields = () => findForm().findComponent(GlFormFields);
const findOpenVsxToggle = () => findFormFields().findComponent(GlToggle);
const findButton = () => findForm().findComponent(GlButton);
const createComponent = (props = {}) => {
wrapper = shallowMount(SettingsForm, {
propsData: {
presets: PRESETS,
submitButtonAttrs: TEST_SUBMIT_BUTTON_ATTRS,
...props,
},
stubs: {
GlFormFields,
},
});
};
beforeEach(() => {
jest.spyOn(console, 'error').mockImplementation(() => {});
});
describe('default', () => {
beforeEach(() => {
createComponent();
});
it('renders form', () => {
expect(findForm().attributes('id')).toBe(TEST_FORM_ID);
});
it('renders form fields', () => {
const expectedInputAttrs = {
readonly: true,
'aria-description':
'Disable Open VSX extension registry to set a custom value for this field.',
width: 'lg',
};
expect(findFormFields().props()).toEqual({
formId: TEST_FORM_ID,
serverValidations: {},
fields: {
useOpenVsx: {
label: 'Use Open VSX extension registry',
},
presetItemUrl: {
label: 'Item URL',
inputAttrs: expectedInputAttrs,
},
presetServiceUrl: {
label: 'Service URL',
inputAttrs: expectedInputAttrs,
},
presetResourceUrlTemplate: {
label: 'Resource URL Template',
inputAttrs: expectedInputAttrs,
},
},
values: {
useOpenVsx: true,
presetItemUrl: 'https://open-vsx.org/vscode/item',
presetResourceUrlTemplate:
'https://open-vsx.org/vscode/unpkg/{publisher}/{name}/{version}/{path}',
presetServiceUrl: 'https://open-vsx.org/vscode/gallery',
},
});
});
it('renders open vsx toggle', () => {
expect(findOpenVsxToggle().props('value')).toEqual(true);
expect(findOpenVsxToggle().attributes()).toMatchObject({
id: 'gl-form-field-uniqueId',
label: 'Use Open VSX extension registry',
labelposition: 'hidden',
});
});
it('renders save button', () => {
expect(findButton().attributes()).toMatchObject({
type: 'submit',
variant: 'confirm',
category: 'primary',
...TEST_SUBMIT_BUTTON_ATTRS,
});
});
});
describe('with preset=open_vsx and custom_values', () => {
beforeEach(() => {
createComponent({
initialSettings: {
custom_values: TEST_CUSTOM_VALUES,
},
});
});
it('changes values when openvsx is toggled', async () => {
// NOTE: gl-form-fields emits `input` on mount to only include fiels created with
expect(findFormFields().props('values')).toEqual({
useOpenVsx: true,
presetItemUrl: 'https://open-vsx.org/vscode/item',
presetServiceUrl: 'https://open-vsx.org/vscode/gallery',
presetResourceUrlTemplate:
'https://open-vsx.org/vscode/unpkg/{publisher}/{name}/{version}/{path}',
});
await findOpenVsxToggle().vm.$emit('change', false);
expect(findFormFields().props('values')).toEqual({
useOpenVsx: false,
presetItemUrl: 'https://open-vsx.org/vscode/item',
presetServiceUrl: 'https://open-vsx.org/vscode/gallery',
presetResourceUrlTemplate:
'https://open-vsx.org/vscode/unpkg/{publisher}/{name}/{version}/{path}',
itemUrl: 'abc',
serviceUrl: 'def',
resourceUrlTemplate: 'ghi',
});
});
});
describe('with preset=custom and custom_values', () => {
beforeEach(() => {
createComponent({
initialSettings: {
custom_values: TEST_CUSTOM_VALUES,
preset: 'custom',
},
});
});
it('stores custom values when preset is changed back and forth', async () => {
await findFormFields().vm.$emit('input', {
useOpenVsx: false,
itemUrl: 'xyz',
serviceUrl: 'xyz',
resourceUrlTemplate: 'xyz',
});
await findOpenVsxToggle().vm.$emit('change', true);
expect(findFormFields().props('values')).toEqual({
useOpenVsx: true,
itemUrl: 'xyz',
serviceUrl: 'xyz',
resourceUrlTemplate: 'xyz',
presetItemUrl: 'https://open-vsx.org/vscode/item',
presetServiceUrl: 'https://open-vsx.org/vscode/gallery',
presetResourceUrlTemplate:
'https://open-vsx.org/vscode/unpkg/{publisher}/{name}/{version}/{path}',
});
await findOpenVsxToggle().vm.$emit('change', false);
expect(findFormFields().props('values')).toEqual({
useOpenVsx: false,
itemUrl: 'xyz',
serviceUrl: 'xyz',
resourceUrlTemplate: 'xyz',
presetItemUrl: 'https://open-vsx.org/vscode/item',
presetServiceUrl: 'https://open-vsx.org/vscode/gallery',
presetResourceUrlTemplate:
'https://open-vsx.org/vscode/unpkg/{publisher}/{name}/{version}/{path}',
});
});
it('renders customizable fields', () => {
expect(findFormFields().props('fields')).toEqual(
expect.objectContaining({
itemUrl: {
label: 'Item URL',
inputAttrs: {
placeholder: 'https://...',
width: 'lg',
},
validators: expect.any(Array),
},
serviceUrl: {
label: 'Service URL',
inputAttrs: {
placeholder: 'https://...',
width: 'lg',
},
validators: expect.any(Array),
},
resourceUrlTemplate: {
label: 'Resource URL Template',
inputAttrs: {
placeholder: 'https://...',
width: 'lg',
},
validators: expect.any(Array),
},
}),
);
});
it.each`
fieldName | value | expectation
${'itemUrl'} | ${''} | ${'A valid URL is required.'}
${'itemUrl'} | ${'abc def'} | ${'A valid URL is required.'}
${'itemUrl'} | ${'https://example.com'} | ${''}
${'serviceUrl'} | ${''} | ${'A valid URL is required.'}
${'serviceUrl'} | ${'abc def'} | ${'A valid URL is required.'}
${'serviceUrl'} | ${'https://example.com'} | ${''}
${'resourceUrlTemplate'} | ${''} | ${'A valid URL is required.'}
${'resourceUrlTemplate'} | ${'abc def'} | ${'A valid URL is required.'}
${'resourceUrlTemplate'} | ${'https://example.com'} | ${''}
`(
'validates $fieldName where $value is "$expectation"',
({ fieldName, value, expectation }) => {
const field = findFormFields().props('fields')[fieldName];
const result = field.validators.reduce((msg, validator) => msg || validator(value), '');
expect(result).toBe(expectation);
},
);
});
});

View File

@ -0,0 +1,11 @@
export const PRESETS = [
{
key: 'open_vsx',
name: 'Open VSX',
values: {
serviceUrl: 'https://open-vsx.org/vscode/gallery',
itemUrl: 'https://open-vsx.org/vscode/item',
resourceUrlTemplate: 'https://open-vsx.org/vscode/unpkg/{publisher}/{name}/{version}/{path}',
},
},
];

View File

@ -462,11 +462,14 @@ RSpec.describe ApplicationSettingsHelper, feature_category: :shared do
context 'with flag on' do
it 'returns hash of view properties' do
expect(helper.vscode_extension_marketplace_settings_view).to eq({
expect(helper.vscode_extension_marketplace_settings_view).to match({
title: _('VS Code Extension Marketplace'),
description: _('Enable VS Code Extension Marketplace and configure the extensions registry for Web IDE.'),
view_model: {
initialSettings: vscode_extension_marketplace
initialSettings: vscode_extension_marketplace,
presets: [
hash_including("key" => "open_vsx")
]
}
})
end

View File

@ -70,7 +70,8 @@ RSpec.describe ApplicationSetting, feature_category: :shared, type: :model do
it { expect(setting.global_search_merge_requests_enabled).to be(true) }
it { expect(setting.global_search_snippet_titles_enabled).to be(true) }
it { expect(setting.global_search_users_enabled).to be(true) }
it { expect(setting.vscode_extension_marketplace).to eq({}) }
it { expect(setting.vscode_extension_marketplace).to eq({ "enabled" => false }) }
it { expect(setting.vscode_extension_marketplace_enabled?).to be(false) }
it do
expect(setting.sign_in_restrictions).to eq({
@ -1776,7 +1777,7 @@ RSpec.describe ApplicationSetting, feature_category: :shared, type: :model do
end
end
describe 'vscode_extension_marketplace' do
describe '#vscode_extension_marketplace' do
let(:invalid_custom) { { enabled: false, preset: "custom", custom_values: {} } }
let(:valid_open_vsx) { { enabled: true, preset: "open_vsx" } }
let(:valid_custom) do
@ -1802,6 +1803,24 @@ RSpec.describe ApplicationSetting, feature_category: :shared, type: :model do
it { is_expected.not_to allow_value(invalid_custom).for(:vscode_extension_marketplace) }
end
describe '#vscode_extension_marketplace_enabled' do
it 'is updated when underlying vscode_extension_marketplace changes' do
expect(setting.vscode_extension_marketplace_enabled).to be(false)
setting.vscode_extension_marketplace = { enabled: true, preset: "open_vsx" }
expect(setting.vscode_extension_marketplace_enabled).to be(true)
end
it 'updates the underlying vscode_extension_marketplace when changed' do
setting.vscode_extension_marketplace = { enabled: true, preset: "open_vsx" }
setting.vscode_extension_marketplace_enabled = false
expect(setting.vscode_extension_marketplace).to eq({ "enabled" => false, "preset" => "open_vsx" })
end
end
describe '#static_objects_external_storage_auth_token=', :aggregate_failures do
subject { setting.static_objects_external_storage_auth_token = token }

View File

@ -1221,5 +1221,16 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu
expect(json_response['resource_usage_limits']).to eq(hash)
end
end
context 'with vscode_extension_marketplace_enabled' do
it 'updates underlying vscode_extension_marketplace field' do
put api("/application/settings", admin),
params: { vscode_extension_marketplace_enabled: true }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['vscode_extension_marketplace_enabled']).to eq(true)
expect(json_response['vscode_extension_marketplace']).to eq({ "enabled" => true })
end
end
end
end

View File

@ -31,7 +31,22 @@ RSpec.describe 'admin/application_settings/_extension_marketplace', feature_cate
it 'renders data-view-model for vue app' do
vue_app = page.at('#js-extension-marketplace-settings-app')
expected_json = { initialSettings: {} }.to_json
expected_presets = ::WebIde::ExtensionMarketplacePreset.all.map do |x|
{
key: x.key,
name: x.name,
values: {
serviceUrl: x.values[:service_url],
itemUrl: x.values[:item_url],
resourceUrlTemplate: x.values[:resource_url_template]
}
}
end
expected_json = {
presets: expected_presets,
initialSettings: { enabled: false }
}.to_json
expect(vue_app).not_to be_nil
expect(vue_app['data-view-model']).to eq(expected_json)