diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-10-18 21:09:37 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-10-18 21:09:37 +0000 |
commit | cace5e8ff1f766b8098e35adc94abc4402aeb2a9 (patch) | |
tree | 96bea3616ee60702be89f4845580f3b3db22f936 /app | |
parent | e4220eeccaf1d53444fdd9102a4061336f91784e (diff) | |
download | gitlab-ce-cace5e8ff1f766b8098e35adc94abc4402aeb2a9.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
38 files changed, 666 insertions, 84 deletions
diff --git a/app/assets/javascripts/graphql_shared/issuable_client.js b/app/assets/javascripts/graphql_shared/issuable_client.js index 3849bd0289d..3b737dfff33 100644 --- a/app/assets/javascripts/graphql_shared/issuable_client.js +++ b/app/assets/javascripts/graphql_shared/issuable_client.js @@ -4,10 +4,14 @@ import { concatPagination } from '@apollo/client/utilities'; import getIssueStateQuery from '~/issues/show/queries/get_issue_state.query.graphql'; import createDefaultClient from '~/lib/graphql'; import typeDefs from '~/work_items/graphql/typedefs.graphql'; +import { WIDGET_TYPE_MILESTONE } from '~/work_items/constants'; export const temporaryConfig = { typeDefs, cacheConfig: { + possibleTypes: { + LocalWorkItemWidget: ['LocalWorkItemMilestone'], + }, typePolicies: { Project: { fields: { @@ -18,6 +22,28 @@ export const temporaryConfig = { }, WorkItem: { fields: { + mockWidgets: { + read(widgets) { + return ( + widgets || [ + { + __typename: 'LocalWorkItemMilestone', + type: WIDGET_TYPE_MILESTONE, + nodes: [ + { + dueDate: null, + expired: false, + id: 'gid://gitlab/Milestone/30', + title: 'v4.0', + // eslint-disable-next-line @gitlab/require-i18n-strings + __typename: 'Milestone', + }, + ], + }, + ] + ); + }, + }, widgets: { merge(existing = [], incoming) { if (existing.length === 0) { diff --git a/app/assets/javascripts/groups/components/app.vue b/app/assets/javascripts/groups/components/app.vue index d74cb2d8175..15f5a3518a5 100644 --- a/app/assets/javascripts/groups/components/app.vue +++ b/app/assets/javascripts/groups/components/app.vue @@ -51,7 +51,6 @@ export default { isModalVisible: false, isLoading: true, isSearchEmpty: false, - searchEmptyMessage: '', targetGroup: null, targetParentGroup: null, showEmptyState: false, @@ -88,10 +87,6 @@ export default { }, }, created() { - this.searchEmptyMessage = this.hideProjects - ? COMMON_STR.GROUP_SEARCH_EMPTY - : COMMON_STR.GROUP_PROJECT_SEARCH_EMPTY; - eventHub.$on(`${this.action}fetchPage`, this.fetchPage); eventHub.$on(`${this.action}toggleChildren`, this.toggleChildren); eventHub.$on(`${this.action}showLeaveGroupModal`, this.showLeaveGroupModal); @@ -259,7 +254,7 @@ export default { const hasGroups = groups && groups.length > 0; if (this.renderEmptyState) { - this.isSearchEmpty = this.filterGroupsBy !== null && !hasGroups; + this.isSearchEmpty = fromSearch && !hasGroups; } else { this.isSearchEmpty = !hasGroups; } @@ -294,7 +289,6 @@ export default { v-else :groups="groups" :search-empty="isSearchEmpty" - :search-empty-message="searchEmptyMessage" :page-info="pageInfo" :action="action" /> diff --git a/app/assets/javascripts/groups/components/groups.vue b/app/assets/javascripts/groups/components/groups.vue index 3a05c308a2a..43aa0753082 100644 --- a/app/assets/javascripts/groups/components/groups.vue +++ b/app/assets/javascripts/groups/components/groups.vue @@ -1,11 +1,18 @@ <script> +import { GlEmptyState } from '@gitlab/ui'; import PaginationLinks from '~/vue_shared/components/pagination_links.vue'; import { getParameterByName } from '~/lib/utils/url_utility'; +import { __ } from '~/locale'; import eventHub from '../event_hub'; export default { + i18n: { + emptyStateTitle: __('No results found'), + emptyStateDescription: __('Edit your search and try again'), + }, components: { PaginationLinks, + GlEmptyState, }, props: { groups: { @@ -20,10 +27,6 @@ export default { type: Boolean, required: true, }, - searchEmptyMessage: { - type: String, - required: true, - }, action: { type: String, required: false, @@ -43,12 +46,11 @@ export default { <template> <div class="groups-list-tree-container" data-qa-selector="groups_list_tree_container"> - <div + <gl-empty-state v-if="searchEmpty" - class="has-no-search-results gl-font-style-italic gl-text-center gl-text-gray-600 gl-p-5" - > - {{ searchEmptyMessage }} - </div> + :title="$options.i18n.emptyStateTitle" + :description="$options.i18n.emptyStateDescription" + /> <template v-else> <group-folder :groups="groups" :action="action" /> <pagination-links diff --git a/app/assets/javascripts/groups/components/overview_tabs.vue b/app/assets/javascripts/groups/components/overview_tabs.vue index 15a0c686548..d0c5846ac88 100644 --- a/app/assets/javascripts/groups/components/overview_tabs.vue +++ b/app/assets/javascripts/groups/components/overview_tabs.vue @@ -2,6 +2,7 @@ import { GlTabs, GlTab, GlSearchBoxByType, GlSorting, GlSortingItem } from '@gitlab/ui'; import { isString, debounce } from 'lodash'; import { __ } from '~/locale'; +import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants'; import GroupsStore from '../store/groups_store'; import GroupsService from '../service/groups_service'; import { @@ -61,11 +62,6 @@ export default { return this.isAscending ? this.sort.asc : this.sort.desc; }, }, - watch: { - search: debounce(async function debouncedSearch() { - this.handleSearchOrSortChange(); - }, 250), - }, mounted() { this.search = this.$route.query?.filter || ''; @@ -137,6 +133,14 @@ export default { this.handleSearchOrSortChange(); }, + handleSearchInput(value) { + this.search = value; + + this.debouncedSearch(); + }, + debouncedSearch: debounce(async function debouncedSearch() { + this.handleSearchOrSortChange(); + }, DEBOUNCE_DELAY), }, i18n: { [ACTIVE_TAB_SUBGROUPS_AND_PROJECTS]: __('Subgroups and projects'), @@ -169,9 +173,10 @@ export default { <div class="gl-lg-display-flex gl-justify-content-end gl-mx-n2 gl-my-n2"> <div class="gl-p-2 gl-lg-form-input-md gl-w-full"> <gl-search-box-by-type - v-model="search" + :value="search" :placeholder="$options.i18n.searchPlaceholder" data-qa-selector="groups_filter_field" + @input="handleSearchInput" /> </div> <div class="gl-p-2 gl-w-full gl-lg-w-auto"> diff --git a/app/assets/javascripts/groups/constants.js b/app/assets/javascripts/groups/constants.js index 33bfcade336..6fb12cd6270 100644 --- a/app/assets/javascripts/groups/constants.js +++ b/app/assets/javascripts/groups/constants.js @@ -24,8 +24,6 @@ export const COMMON_STR = { EDIT_BTN_TITLE: s__('GroupsTree|Edit'), REMOVE_BTN_TITLE: s__('GroupsTree|Delete'), OPTIONS_DROPDOWN_TITLE: s__('GroupsTree|Options'), - GROUP_SEARCH_EMPTY: s__('GroupsTree|No groups matched your search'), - GROUP_PROJECT_SEARCH_EMPTY: s__('GroupsTree|No groups or projects matched your search'), }; export const ITEM_TYPE = { diff --git a/app/assets/javascripts/ide/index.js b/app/assets/javascripts/ide/index.js index 10e9f6a9488..1a191f6f76f 100644 --- a/app/assets/javascripts/ide/index.js +++ b/app/assets/javascripts/ide/index.js @@ -99,7 +99,9 @@ export function startIde(options) { return; } - if (gon.features?.vscodeWebIde) { + const useNewWebIde = parseBoolean(ideElement.dataset.useNewWebIde); + + if (useNewWebIde) { initGitlabWebIDE(ideElement); } else { resetServiceWorkersPublicPath(); diff --git a/app/assets/javascripts/ide/init_gitlab_web_ide.js b/app/assets/javascripts/ide/init_gitlab_web_ide.js index a061da38d4f..140f2895a29 100644 --- a/app/assets/javascripts/ide/init_gitlab_web_ide.js +++ b/app/assets/javascripts/ide/init_gitlab_web_ide.js @@ -7,8 +7,7 @@ export const initGitlabWebIDE = async (el) => { const baseUrl = new URL(process.env.GITLAB_WEB_IDE_PUBLIC_PATH, window.location.origin); // what: Pull what we need from the element. We will replace it soon. - const { path_with_namespace: projectPath } = JSON.parse(el.dataset.project); - const { cspNonce: nonce, branchName: ref } = el.dataset; + const { cspNonce: nonce, branchName: ref, projectPath } = el.dataset; // what: Clean up the element, but preserve id. // why: This way we don't inherit any `ide-loading` side-effects. This diff --git a/app/assets/javascripts/pages/import/fogbugz/new_user_map/components/user_select.vue b/app/assets/javascripts/pages/import/fogbugz/new_user_map/components/user_select.vue new file mode 100644 index 00000000000..20ce296bbec --- /dev/null +++ b/app/assets/javascripts/pages/import/fogbugz/new_user_map/components/user_select.vue @@ -0,0 +1,95 @@ +<script> +import { GlAvatarLabeled, GlListbox } from '@gitlab/ui'; +import { __ } from '~/locale'; +import searchUsersQuery from '~/graphql_shared/queries/users_search_all.query.graphql'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; + +import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; + +const USERS_PER_PAGE = 20; + +export default { + components: { + GlAvatarLabeled, + GlListbox, + }, + props: { + name: { + type: String, + required: true, + }, + }, + apollo: { + usersQuery: { + query: searchUsersQuery, + variables() { + return { + search: this.search, + first: USERS_PER_PAGE, + }; + }, + update(data) { + return data; + }, + debounce: DEFAULT_DEBOUNCE_AND_THROTTLE_MS, + }, + }, + data() { + return { + user: '', + search: '', + }; + }, + computed: { + userId() { + return getIdFromGraphQLId(this.user); + }, + users() { + return [ + { text: __('(no user)'), value: '' }, + ...(this.usersQuery?.users.nodes || []).map((u) => ({ + username: `@${u.username}`, + avatarUrl: u.avatarUrl, + text: u.name, + value: u.id, + })), + ]; + }, + }, + methods: { + clearTransform() { + // FIXME: workaround for listbox issue + // https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1986 + const { listbox } = this.$refs; + if (listbox.querySelector('.dropdown-menu')) { + listbox.querySelector('.dropdown-menu').style.transform = ''; + } + }, + }, +}; +</script> +<template> + <div> + <gl-listbox + ref="listbox" + v-model="user" + :items="users" + searchable + is-check-centered + :searching="$apollo.loading" + @click.capture.native="clearTransform" + @search="search = $event" + > + <template #list-item="{ item }"> + <gl-avatar-labeled + shape="circle" + :size="32" + :src="item.avatarUrl" + :label="item.text" + :sub-label="item.username" + /> + </template> + </gl-listbox> + <input type="hidden" :name="name" :value="userId" /> + </div> +</template> diff --git a/app/assets/javascripts/pages/import/fogbugz/new_user_map/index.js b/app/assets/javascripts/pages/import/fogbugz/new_user_map/index.js index 86b80a0ba5b..ef549f20cf3 100644 --- a/app/assets/javascripts/pages/import/fogbugz/new_user_map/index.js +++ b/app/assets/javascripts/pages/import/fogbugz/new_user_map/index.js @@ -1,3 +1,19 @@ -import UsersSelect from '~/users_select'; +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createDefaultClient from '~/lib/graphql'; +import UserSelect from './components/user_select.vue'; -new UsersSelect(); // eslint-disable-line no-new +Vue.use(VueApollo); + +const apolloProvider = new VueApollo({ + defaultClient: createDefaultClient(), +}); + +Array.from(document.querySelectorAll('.js-gitlab-user')).forEach( + (node) => + new Vue({ + el: node, + apolloProvider, + render: (h) => h(UserSelect, { props: { name: node.dataset.name } }), + }), +); diff --git a/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue b/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue index 7126d69c8c6..c33b1468ca4 100644 --- a/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue +++ b/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue @@ -25,6 +25,8 @@ import { Tracking, IssuableAttributeState, IssuableAttributeType, + LocalizedIssuableAttributeType, + IssuableAttributeTypeKeyMap, issuableAttributesQueries, noAttributeId, defaultEpicSort, @@ -229,7 +231,9 @@ export default { return timeFor(this.currentAttribute?.dueDate); }, i18n() { - return dropdowni18nText(this.issuableAttribute, this.issuableType); + const localizedAttribute = + LocalizedIssuableAttributeType[IssuableAttributeTypeKeyMap[this.issuableAttribute]]; + return dropdowni18nText(localizedAttribute, this.issuableType); }, isEpic() { // MV to EE https://gitlab.com/gitlab-org/gitlab/-/issues/345311 diff --git a/app/assets/javascripts/sidebar/constants.js b/app/assets/javascripts/sidebar/constants.js index 60cb4cff727..6248bcb8e2d 100644 --- a/app/assets/javascripts/sidebar/constants.js +++ b/app/assets/javascripts/sidebar/constants.js @@ -1,3 +1,4 @@ +import { invert } from 'lodash'; import { s__, __, sprintf } from '~/locale'; import updateIssueLabelsMutation from '~/boards/graphql/issue_set_labels.mutation.graphql'; import userSearchQuery from '~/graphql_shared/queries/users_search.query.graphql'; @@ -251,6 +252,12 @@ export const IssuableAttributeType = { Milestone: 'milestone', }; +export const LocalizedIssuableAttributeType = { + Milestone: s__('Issuable|milestone'), +}; + +export const IssuableAttributeTypeKeyMap = invert(IssuableAttributeType); + export const IssuableAttributeState = { [IssuableAttributeType.Milestone]: 'active', }; diff --git a/app/assets/javascripts/users_select/index.js b/app/assets/javascripts/users_select/index.js index 5963568a00b..bd425bdc2a8 100644 --- a/app/assets/javascripts/users_select/index.js +++ b/app/assets/javascripts/users_select/index.js @@ -819,13 +819,14 @@ UsersSelect.prototype.renderRow = function ( const tooltipAttributes = tooltip ? `data-container="body" data-placement="left" data-title="${tooltip}"` : ''; + const dataUserSuggested = user.suggested ? `data-user-suggested=${user.suggested}` : ''; const name = user?.availability && isUserBusy(user.availability) ? sprintf(__('%{name} (Busy)'), { name: user.name }) : user.name; return ` - <li data-user-id=${user.id}> + <li data-user-id=${user.id} ${dataUserSuggested}> <a href="#" class="dropdown-menu-user-link gl-display-flex! gl-align-items-center ${linkClasses}" ${tooltipAttributes}> ${this.renderRowAvatar(issuableType, user, img)} <span class="gl-display-flex gl-flex-direction-column gl-overflow-hidden"> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/telemetry.js b/app/assets/javascripts/vue_merge_request_widget/components/extensions/telemetry.js index d67ff11f297..e3f87c08ad4 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/telemetry.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/telemetry.js @@ -28,7 +28,7 @@ const nonStandardEvents = { }, counter: {}, }, - testReport: { + testSummary: { uniqueUser: { expand: ['i_testing_summary_widget_total'], }, diff --git a/app/assets/javascripts/webhooks/components/form_url_app.vue b/app/assets/javascripts/webhooks/components/form_url_app.vue index 62d6c03bbb3..5ec16d4ba15 100644 --- a/app/assets/javascripts/webhooks/components/form_url_app.vue +++ b/app/assets/javascripts/webhooks/components/form_url_app.vue @@ -1,5 +1,6 @@ <script> -import { GlFormGroup, GlFormInput, GlFormRadio, GlFormRadioGroup } from '@gitlab/ui'; +import { isEmpty } from 'lodash'; +import { GlFormGroup, GlFormInput, GlFormRadio, GlFormRadioGroup, GlLink } from '@gitlab/ui'; import { __, s__ } from '~/locale'; import FormUrlMaskItem from './form_url_mask_item.vue'; @@ -11,19 +12,60 @@ export default { GlFormInput, GlFormRadio, GlFormRadioGroup, + GlLink, + }, + props: { + initialUrl: { + type: String, + required: false, + default: null, + }, + initialUrlVariables: { + type: Array, + required: false, + default: null, + }, }, data() { return { - maskEnabled: false, - url: null, + maskEnabled: !isEmpty(this.initialUrlVariables), + url: this.initialUrl, + items: isEmpty(this.initialUrlVariables) ? [{}] : this.initialUrlVariables, }; }, computed: { maskedUrl() { - return this.url; + if (!this.url) { + return null; + } + + let maskedUrl = this.url; + + this.items.forEach(({ key, value }) => { + if (!key || !value) { + return; + } + + const replacementExpression = new RegExp(value, 'g'); + maskedUrl = maskedUrl.replace(replacementExpression, `{${key}}`); + }); + + return maskedUrl; + }, + }, + methods: { + onItemInput({ index, key, value }) { + this.$set(this.items, index, { key, value }); + }, + addItem() { + this.items.push({}); + }, + removeItem(index) { + this.items.splice(index, 1); }, }, i18n: { + addItem: s__('Webhooks|+ Mask another portion of URL'), radioFullUrlText: s__('Webhooks|Show full URL'), radioMaskUrlText: s__('Webhooks|Mask portions of URL'), radioMaskUrlHelp: s__('Webhooks|Do not show sensitive data such as tokens in the UI.'), @@ -49,6 +91,7 @@ export default { v-model="url" name="hook[url]" :placeholder="$options.i18n.urlPlaceholder" + data-testid="form-url" /> </gl-form-group> <div class="gl-mt-5"> @@ -63,9 +106,27 @@ export default { </gl-form-radio-group> <div v-if="maskEnabled" class="gl-ml-6" data-testid="url-mask-section"> - <form-url-mask-item :index="0" /> + <form-url-mask-item + v-for="({ key, value }, index) in items" + :key="index" + :index="index" + :item-key="key" + :item-value="value" + @input="onItemInput" + @remove="removeItem" + /> + <div class="gl-mb-5"> + <gl-link @click="addItem">{{ $options.i18n.addItem }}</gl-link> + </div> + <gl-form-group :label="$options.i18n.urlPreview" label-for="webhook-url-preview"> - <gl-form-input id="webhook-url-preview" :value="maskedUrl" readonly /> + <gl-form-input + id="webhook-url-preview" + :value="maskedUrl" + readonly + name="hook[url]" + data-testid="form-url-preview" + /> </gl-form-group> </div> </div> diff --git a/app/assets/javascripts/webhooks/components/form_url_mask_item.vue b/app/assets/javascripts/webhooks/components/form_url_mask_item.vue index 1e74b4a8215..3b75f9b6c0d 100644 --- a/app/assets/javascripts/webhooks/components/form_url_mask_item.vue +++ b/app/assets/javascripts/webhooks/components/form_url_mask_item.vue @@ -14,6 +14,16 @@ export default { required: false, default: null, }, + itemKey: { + type: String, + required: false, + default: null, + }, + itemValue: { + type: String, + required: false, + default: null, + }, }, computed: { keyInputId() { @@ -30,6 +40,15 @@ export default { inputName(type) { return `hook[url_variables][][${type}]`; }, + onKeyInput(key) { + this.$emit('input', { index: this.index, key, value: this.itemValue }); + }, + onValueInput(value) { + this.$emit('input', { index: this.index, key: this.itemKey, value }); + }, + onRemoveClick() { + this.$emit('remove', this.index); + }, }, i18n: { keyLabel: s__('Webhooks|How it looks in the UI'), @@ -39,14 +58,19 @@ export default { </script> <template> - <div class="gl-display-flex gl-align-items-flex-end gl-gap-3 gl-mb-5"> + <div class="gl-display-flex gl-align-items-flex-end gl-gap-3 gl-mb-3"> <gl-form-group :label="$options.i18n.valueLabel" :label-for="valueInputId" class="gl-flex-grow-1 gl-mb-0" data-testid="mask-item-value" > - <gl-form-input :id="valueInputId" :name="inputName('value')" /> + <gl-form-input + :id="valueInputId" + :name="inputName('value')" + :value="itemValue" + @input="onValueInput" + /> </gl-form-group> <gl-form-group :label="$options.i18n.keyLabel" @@ -54,8 +78,13 @@ export default { class="gl-flex-grow-1 gl-mb-0" data-testid="mask-item-key" > - <gl-form-input :id="keyInputId" :name="inputName('key')" /> + <gl-form-input + :id="keyInputId" + :name="inputName('key')" + :value="itemKey" + @input="onKeyInput" + /> </gl-form-group> - <gl-button icon="remove" /> + <gl-button icon="remove" :aria-label="__('Remove')" @click="onRemoveClick" /> </div> </template> diff --git a/app/assets/javascripts/webhooks/index.js b/app/assets/javascripts/webhooks/index.js index bfa33560fa5..1b2b33e44c1 100644 --- a/app/assets/javascripts/webhooks/index.js +++ b/app/assets/javascripts/webhooks/index.js @@ -8,11 +8,18 @@ export default () => { return null; } + const { url: initialUrl, urlVariables } = el.dataset; + return new Vue({ el, name: 'WebhookFormRoot', render(createElement) { - return createElement(FormUrlApp, {}); + return createElement(FormUrlApp, { + props: { + initialUrl, + initialUrlVariables: urlVariables ? JSON.parse(urlVariables) : undefined, + }, + }); }, }); }; 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 cf0aafc2eb0..af9b8c6101a 100644 --- a/app/assets/javascripts/work_items/components/work_item_detail.vue +++ b/app/assets/javascripts/work_items/components/work_item_detail.vue @@ -23,6 +23,7 @@ import { WIDGET_TYPE_WEIGHT, WIDGET_TYPE_HIERARCHY, WORK_ITEM_VIEWED_STORAGE_KEY, + WIDGET_TYPE_MILESTONE, WIDGET_TYPE_ITERATION, } from '../constants'; @@ -40,6 +41,7 @@ import WorkItemDescription from './work_item_description.vue'; import WorkItemDueDate from './work_item_due_date.vue'; import WorkItemAssignees from './work_item_assignees.vue'; import WorkItemLabels from './work_item_labels.vue'; +import WorkItemMilestone from './work_item_milestone.vue'; import WorkItemInformation from './work_item_information.vue'; export default { @@ -67,6 +69,7 @@ export default { LocalStorageSync, WorkItemTypeIcon, WorkItemIteration: () => import('ee_component/work_items/components/work_item_iteration.vue'), + WorkItemMilestone, }, mixins: [glFeatureFlagMixin()], props: { @@ -208,6 +211,9 @@ export default { workItemIteration() { return this.isWidgetPresent(WIDGET_TYPE_ITERATION); }, + workItemMilestone() { + return this.workItem?.mockWidgets?.find((widget) => widget.type === WIDGET_TYPE_MILESTONE); + }, }, beforeDestroy() { /** make sure that if the user has not even dismissed the alert , @@ -411,6 +417,17 @@ export default { :work-item-type="workItemType" @error="updateError = $event" /> + <template v-if="workItemsMvc2Enabled"> + <work-item-milestone + v-if="workItemMilestone" + :work-item-id="workItem.id" + :work-item-milestone="workItemMilestone.nodes[0]" + :work-item-type="workItemType" + :can-update="canUpdate" + :full-path="fullPath" + @error="updateError = $event" + /> + </template> <work-item-weight v-if="workItemWeight" class="gl-mb-5" diff --git a/app/assets/javascripts/work_items/components/work_item_milestone.vue b/app/assets/javascripts/work_items/components/work_item_milestone.vue new file mode 100644 index 00000000000..c4a36e36555 --- /dev/null +++ b/app/assets/javascripts/work_items/components/work_item_milestone.vue @@ -0,0 +1,248 @@ +<script> +import { + GlFormGroup, + GlDropdown, + GlDropdownItem, + GlDropdownDivider, + GlSkeletonLoader, + GlSearchBoxByType, + GlDropdownText, +} from '@gitlab/ui'; +import * as Sentry from '@sentry/browser'; +import { debounce } from 'lodash'; +import Tracking from '~/tracking'; +import { s__ } from '~/locale'; +import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; +import projectMilestonesQuery from '~/sidebar/queries/project_milestones.query.graphql'; +import localUpdateWorkItemMutation from '../graphql/local_update_work_item.mutation.graphql'; +import { + I18N_WORK_ITEM_ERROR_UPDATING, + sprintfWorkItem, + TRACKING_CATEGORY_SHOW, +} from '../constants'; + +const noMilestoneId = 'no-milestone-id'; + +export default { + i18n: { + MILESTONE: s__('WorkItem|Milestone'), + NONE: s__('WorkItem|None'), + MILESTONE_PLACEHOLDER: s__('WorkItem|Add to milestone'), + NO_MATCHING_RESULTS: s__('WorkItem|No matching results'), + NO_MILESTONE: s__('WorkItem|No milestone'), + MILESTONE_FETCH_ERROR: s__( + 'WorkItem|Something went wrong while fetching milestones. Please try again.', + ), + }, + components: { + GlFormGroup, + GlDropdown, + GlDropdownItem, + GlDropdownDivider, + GlSkeletonLoader, + GlSearchBoxByType, + GlDropdownText, + }, + mixins: [Tracking.mixin()], + props: { + workItemId: { + type: String, + required: true, + }, + workItemMilestone: { + type: Object, + required: false, + default: () => {}, + }, + workItemType: { + type: String, + required: false, + default: '', + }, + canUpdate: { + type: Boolean, + required: false, + default: false, + }, + fullPath: { + type: String, + required: true, + }, + }, + data() { + return { + localMilestone: this.workItemMilestone, + searchTerm: '', + shouldFetch: false, + updateInProgress: false, + isFocused: false, + milestones: [], + }; + }, + computed: { + tracking() { + return { + category: TRACKING_CATEGORY_SHOW, + label: 'item_milestone', + property: `type_${this.workItemType}`, + }; + }, + emptyPlaceholder() { + return this.canUpdate ? this.$options.i18n.MILESTONE_PLACEHOLDER : this.$options.i18n.NONE; + }, + dropdownText() { + return this.localMilestone?.title || this.emptyPlaceholder; + }, + isLoadingMilestones() { + return this.$apollo.queries.milestones.loading; + }, + isNoMilestone() { + return this.localMilestone?.id === noMilestoneId || !this.localMilestone?.id; + }, + dropdownClasses() { + return { + 'gl-text-gray-500!': this.canUpdate && this.isNoMilestone, + 'is-not-focused': !this.isFocused, + }; + }, + }, + created() { + this.debouncedSearchKeyUpdate = debounce(this.setSearchKey, DEFAULT_DEBOUNCE_AND_THROTTLE_MS); + }, + apollo: { + milestones: { + query: projectMilestonesQuery, + variables() { + return { + fullPath: this.fullPath, + title: this.searchTerm, + first: 20, + }; + }, + skip() { + return !this.shouldFetch; + }, + update(data) { + return data?.workspace?.attributes?.nodes || []; + }, + error() { + this.$emit('error', this.i18n.MILESTONE_FETCH_ERROR); + }, + }, + }, + methods: { + handleMilestoneClick(milestone) { + this.localMilestone = milestone; + }, + onDropdownShown() { + this.$refs.search.focusInput(); + this.shouldFetch = true; + this.isFocused = true; + }, + onDropdownHide() { + this.isFocused = false; + this.searchTerm = ''; + this.shouldFetch = false; + this.updateMilestone(); + }, + setSearchKey(value) { + this.searchTerm = value; + }, + isMilestoneChecked(milestone) { + return this.localMilestone?.id === milestone?.id; + }, + updateMilestone() { + if (this.workItemMilestone?.id === this.localMilestone?.id) { + return; + } + + this.track('updated_milestone'); + this.updateInProgress = true; + this.$apollo + .mutate({ + mutation: localUpdateWorkItemMutation, + variables: { + input: { + id: this.workItemId, + milestone: { + milestoneId: this.localMilestone?.id, + }, + }, + }, + }) + .then(({ data }) => { + if (data.workItemUpdate.errors.length) { + throw new Error(data.workItemUpdate.errors.join('\n')); + } + }) + .catch((error) => { + const msg = sprintfWorkItem(I18N_WORK_ITEM_ERROR_UPDATING, this.workItemType); + this.$emit('error', msg); + Sentry.captureException(error); + }) + .finally(() => { + this.updateInProgress = false; + }); + }, + }, +}; +</script> + +<template> + <gl-form-group + class="work-item-dropdown" + :label="$options.i18n.MILESTONE" + label-class="gl-pb-0! gl-overflow-wrap-break gl-mt-3" + label-cols="3" + label-cols-lg="2" + > + <span + v-if="!canUpdate" + class="gl-text-secondary gl-ml-4 gl-mt-3 gl-display-inline-block gl-line-height-normal" + data-testid="disabled-text" + > + {{ dropdownText }} + </span> + <gl-dropdown + v-else + :toggle-class="dropdownClasses" + :text="dropdownText" + :loading="updateInProgress" + @shown="onDropdownShown" + @hide="onDropdownHide" + > + <template #header> + <gl-search-box-by-type ref="search" :value="searchTerm" @input="debouncedSearchKeyUpdate" /> + </template> + <gl-dropdown-item + data-testid="no-milestone" + is-check-item + :is-checked="isNoMilestone" + @click="handleMilestoneClick({ id: 'no-milestone-id' })" + > + {{ $options.i18n.NO_MILESTONE }} + </gl-dropdown-item> + <gl-dropdown-divider /> + <gl-dropdown-text v-if="isLoadingMilestones"> + <gl-skeleton-loader :height="90"> + <rect width="380" height="10" x="10" y="15" rx="4" /> + <rect width="280" height="10" x="10" y="30" rx="4" /> + <rect width="380" height="10" x="10" y="50" rx="4" /> + <rect width="280" height="10" x="10" y="65" rx="4" /> + </gl-skeleton-loader> + </gl-dropdown-text> + <template v-else-if="milestones.length"> + <gl-dropdown-item + v-for="milestone in milestones" + :key="milestone.id" + is-check-item + :is-checked="isMilestoneChecked(milestone)" + @click="handleMilestoneClick(milestone)" + > + {{ milestone.title }} + </gl-dropdown-item> + </template> + <gl-dropdown-text v-else>{{ $options.i18n.NO_MATCHING_RESULTS }}</gl-dropdown-text> + </gl-dropdown> + </gl-form-group> +</template> diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js index 0d426299408..7737c535650 100644 --- a/app/assets/javascripts/work_items/constants.js +++ b/app/assets/javascripts/work_items/constants.js @@ -17,6 +17,7 @@ export const WIDGET_TYPE_LABELS = 'LABELS'; export const WIDGET_TYPE_START_AND_DUE_DATE = 'START_AND_DUE_DATE'; export const WIDGET_TYPE_WEIGHT = 'WEIGHT'; export const WIDGET_TYPE_HIERARCHY = 'HIERARCHY'; +export const WIDGET_TYPE_MILESTONE = 'MILESTONE'; export const WIDGET_TYPE_ITERATION = 'ITERATION'; export const WORK_ITEM_VIEWED_STORAGE_KEY = 'gl-show-work-item-banner'; diff --git a/app/assets/javascripts/work_items/graphql/typedefs.graphql b/app/assets/javascripts/work_items/graphql/typedefs.graphql index d3712da1329..36779dfe11e 100644 --- a/app/assets/javascripts/work_items/graphql/typedefs.graphql +++ b/app/assets/javascripts/work_items/graphql/typedefs.graphql @@ -1,5 +1,6 @@ enum LocalWidgetType { ASSIGNEES + MILESTONE } interface LocalWorkItemWidget { @@ -11,6 +12,15 @@ type LocalWorkItemAssignees implements LocalWorkItemWidget { nodes: [UserCore] } +type LocalWorkItemMilestone implements LocalWorkItemWidget { + type: LocalWidgetType! + nodes: [Milestone!] +} + +extend type WorkItem { + mockWidgets: [LocalWorkItemWidget] +} + input LocalUserInput { id: ID! name: String @@ -19,9 +29,14 @@ input LocalUserInput { avatarUrl: String } +input LocalMilestoneInput { + milestoneId: ID! +} + input LocalUpdateWorkItemInput { id: WorkItemID! assignees: [LocalUserInput!] + milestone: LocalMilestoneInput! } type LocalWorkItemPayload { diff --git a/app/assets/javascripts/work_items/graphql/work_item.query.graphql b/app/assets/javascripts/work_items/graphql/work_item.query.graphql index 3b46fed97ec..fa0ab56df75 100644 --- a/app/assets/javascripts/work_items/graphql/work_item.query.graphql +++ b/app/assets/javascripts/work_items/graphql/work_item.query.graphql @@ -3,5 +3,16 @@ query workItem($id: WorkItemID!) { workItem(id: $id) { ...WorkItem + mockWidgets @client { + ... on LocalWorkItemMilestone { + type + nodes { + id + title + expired + dueDate + } + } + } } } diff --git a/app/assets/stylesheets/page_bundles/work_items.scss b/app/assets/stylesheets/page_bundles/work_items.scss index 7a5cc72ceb8..820a1a0b53e 100644 --- a/app/assets/stylesheets/page_bundles/work_items.scss +++ b/app/assets/stylesheets/page_bundles/work_items.scss @@ -64,7 +64,7 @@ } } -.work-item-iteration { +.work-item-dropdown { .gl-dropdown-toggle { background: none !important; @@ -82,4 +82,3 @@ } } } - diff --git a/app/controllers/ide_controller.rb b/app/controllers/ide_controller.rb index ebd958822ed..fcf6871d137 100644 --- a/app/controllers/ide_controller.rb +++ b/app/controllers/ide_controller.rb @@ -11,7 +11,6 @@ class IdeController < ApplicationController push_frontend_feature_flag(:build_service_proxy) push_frontend_feature_flag(:schema_linting) push_frontend_feature_flag(:reject_unsigned_commits_by_gitlab) - push_frontend_feature_flag(:vscode_web_ide, current_user) define_index_vars end @@ -27,7 +26,7 @@ class IdeController < ApplicationController namespace: project&.namespace, user: current_user) end - render layout: 'fullscreen', locals: { minimal: Feature.enabled?(:vscode_web_ide, current_user) } + render layout: 'fullscreen', locals: { minimal: helpers.use_new_web_ide? } end private diff --git a/app/controllers/profiles/preferences_controller.rb b/app/controllers/profiles/preferences_controller.rb index c0360d10392..a57c87bf691 100644 --- a/app/controllers/profiles/preferences_controller.rb +++ b/app/controllers/profiles/preferences_controller.rb @@ -56,7 +56,8 @@ class Profiles::PreferencesController < Profiles::ApplicationController :gitpod_enabled, :render_whitespace_in_code, :markdown_surround_selection, - :markdown_automatic_lists + :markdown_automatic_lists, + :use_legacy_web_ide ] end end diff --git a/app/graphql/types/environment_type.rb b/app/graphql/types/environment_type.rb index 2484081a828..dd2286d333d 100644 --- a/app/graphql/types/environment_type.rb +++ b/app/graphql/types/environment_type.rb @@ -57,7 +57,7 @@ module Types field :deployments, Types::DeploymentType.connection_type, null: true, - description: 'Deployments of the environment. This field can only be resolved for one project in any single request.', + description: 'Deployments of the environment. This field can only be resolved for one environment in any single request.', resolver: Resolvers::DeploymentsResolver do extension ::Gitlab::Graphql::Limit::FieldCallCount, limit: 1 end diff --git a/app/helpers/hooks_helper.rb b/app/helpers/hooks_helper.rb index 1e50033e0e0..e050ccc0e40 100644 --- a/app/helpers/hooks_helper.rb +++ b/app/helpers/hooks_helper.rb @@ -1,6 +1,13 @@ # frozen_string_literal: true module HooksHelper + def webhook_form_data(hook) + { + url: hook.url, + url_variables: nil + } + end + def link_to_test_hook(hook, trigger) path = test_hook_path(hook, trigger) trigger_human_name = trigger.to_s.tr('_', ' ').camelize diff --git a/app/helpers/ide_helper.rb b/app/helpers/ide_helper.rb index ec1327cf7ae..5b3ca25b5af 100644 --- a/app/helpers/ide_helper.rb +++ b/app/helpers/ide_helper.rb @@ -3,6 +3,32 @@ module IdeHelper def ide_data { + 'can-use-new-web-ide' => can_use_new_web_ide?.to_s, + 'use-new-web-ide' => use_new_web_ide?.to_s, + 'user-preferences-path' => profile_preferences_path, + 'branch-name' => @branch + }.merge(use_new_web_ide? ? new_ide_data : legacy_ide_data) + end + + def can_use_new_web_ide? + Feature.enabled?(:vscode_web_ide, current_user) + end + + def use_new_web_ide? + can_use_new_web_ide? && !current_user.use_legacy_web_ide + end + + private + + def new_ide_data + { + 'project-path' => @project&.path_with_namespace, + 'csp-nonce' => content_security_policy_nonce + } + end + + def legacy_ide_data + { 'empty-state-svg-path' => image_path('illustrations/multi_file_editor_empty.svg'), 'no-changes-state-svg-path' => image_path('illustrations/multi-editor_no_changes_empty.svg'), 'committed-state-svg-path' => image_path('illustrations/multi-editor_all_changes_committed_empty.svg'), @@ -13,7 +39,6 @@ module IdeHelper 'clientside-preview-enabled': Gitlab::CurrentSettings.web_ide_clientside_preview_enabled?.to_s, 'render-whitespace-in-code': current_user.render_whitespace_in_code.to_s, 'codesandbox-bundler-url': Gitlab::CurrentSettings.web_ide_clientside_preview_bundler_url, - 'branch-name' => @branch, 'default-branch' => @project && @project.default_branch, 'file-path' => @path, 'merge-request' => @merge_request, @@ -24,13 +49,10 @@ module IdeHelper 'web-terminal-svg-path' => image_path('illustrations/web-ide_promotion.svg'), 'web-terminal-help-path' => help_page_path('user/project/web_ide/index.md', anchor: 'interactive-web-terminals-for-the-web-ide'), 'web-terminal-config-help-path' => help_page_path('user/project/web_ide/index.md', anchor: 'web-ide-configuration-file'), - 'web-terminal-runners-help-path' => help_page_path('user/project/web_ide/index.md', anchor: 'runner-configuration'), - 'csp-nonce' => content_security_policy_nonce + 'web-terminal-runners-help-path' => help_page_path('user/project/web_ide/index.md', anchor: 'runner-configuration') } end - private - def convert_to_project_entity_json(project) return unless project diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index f83aa79b461..361b1a8dca9 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -410,6 +410,13 @@ class ApplicationSetting < ApplicationRecord allow_nil: false, inclusion: { in: [true, false], message: N_('must be a boolean value') } + # rubocop:disable Cop/StaticTranslationDefinition + validates :deactivate_dormant_users_period, + presence: true, + numericality: { only_integer: true, greater_than_or_equal_to: 90, message: _("'%{value}' days of inactivity must be greater than or equal to 90") }, + if: :deactivate_dormant_users? + # rubocop:enable Cop/StaticTranslationDefinition + Gitlab::SSHPublicKey.supported_types.each do |type| validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type } end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 4287c0b7884..950e0a583bc 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -1364,9 +1364,9 @@ module Ci self.builds.latest.build_matchers(project) end - def authorized_cluster_agents - strong_memoize(:authorized_cluster_agents) do - ::Clusters::AgentAuthorizationsFinder.new(project).execute.map(&:agent) + def cluster_agent_authorizations + strong_memoize(:cluster_agent_authorizations) do + ::Clusters::AgentAuthorizationsFinder.new(project).execute end end diff --git a/app/models/clusters/agents/implicit_authorization.rb b/app/models/clusters/agents/implicit_authorization.rb index 9f7f653ed65..a365ccdc568 100644 --- a/app/models/clusters/agents/implicit_authorization.rb +++ b/app/models/clusters/agents/implicit_authorization.rb @@ -16,7 +16,7 @@ module Clusters end def config - nil + {} end end end diff --git a/app/models/user.rb b/app/models/user.rb index b36b00fcbaf..6d198fc755b 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -354,6 +354,7 @@ class User < ApplicationRecord :markdown_automatic_lists, :markdown_automatic_lists=, :diffs_deletion_color, :diffs_deletion_color=, :diffs_addition_color, :diffs_addition_color=, + :use_legacy_web_ide, :use_legacy_web_ide=, to: :user_preference delegate :path, to: :namespace, allow_nil: true, prefix: true diff --git a/app/models/user_preference.rb b/app/models/user_preference.rb index b8f30413404..c6ebd550daf 100644 --- a/app/models/user_preference.rb +++ b/app/models/user_preference.rb @@ -22,6 +22,7 @@ class UserPreference < ApplicationRecord validates :diffs_deletion_color, :diffs_addition_color, format: { with: ColorsHelper::HEX_COLOR_PATTERN }, allow_blank: true + validates :use_legacy_web_ide, allow_nil: false, inclusion: { in: [true, false] } ignore_columns :experience_level, remove_with: '14.10', remove_after: '2021-03-22' diff --git a/app/presenters/ci/build_runner_presenter.rb b/app/presenters/ci/build_runner_presenter.rb index 71a05ef2c72..706608e3029 100644 --- a/app/presenters/ci/build_runner_presenter.rb +++ b/app/presenters/ci/build_runner_presenter.rb @@ -34,7 +34,9 @@ module Ci def runner_variables stop_expanding_file_vars = ::Feature.enabled?(:ci_stop_expanding_file_vars_for_runners, project) - variables.sort_and_expand_all(keep_undefined: true, expand_file_vars: !stop_expanding_file_vars).to_runner_variables + variables + .sort_and_expand_all(keep_undefined: true, expand_file_vars: !stop_expanding_file_vars, project: project) + .to_runner_variables end def refspecs diff --git a/app/services/bulk_imports/uploads_export_service.rb b/app/services/bulk_imports/uploads_export_service.rb index 7f5ee7b8624..315590bea31 100644 --- a/app/services/bulk_imports/uploads_export_service.rb +++ b/app/services/bulk_imports/uploads_export_service.rb @@ -22,8 +22,9 @@ module BulkImports subdir_path = export_subdir_path(upload) mkdir_p(subdir_path) download_or_copy_upload(uploader, File.join(subdir_path, uploader.filename)) - rescue Errno::ENAMETOOLONG => e - # Do not fail entire export process if downloaded file has filename that exceeds 255 characters. + rescue StandardError => e + # Do not fail entire project export if something goes wrong during file download + # (e.g. downloaded file has filename that exceeds 255 characters). # Ignore raised exception, skip such upload, log the error and keep going with the export instead. Gitlab::ErrorTracking.log_exception(e, portable_id: portable.id, portable_class: portable.class.name, upload_id: upload.id) end diff --git a/app/services/ci/generate_kubeconfig_service.rb b/app/services/ci/generate_kubeconfig_service.rb index 894ab8e8505..347bc99dbf5 100644 --- a/app/services/ci/generate_kubeconfig_service.rb +++ b/app/services/ci/generate_kubeconfig_service.rb @@ -14,7 +14,8 @@ module Ci url: Gitlab::Kas.tunnel_url ) - agents.each do |agent| + agent_authorizations.each do |authorization| + agent = authorization.agent user = user_name(agent) template.add_user( @@ -24,6 +25,7 @@ module Ci template.add_context( name: context_name(agent), + namespace: context_namespace(authorization), cluster: cluster_name, user: user ) @@ -36,8 +38,8 @@ module Ci attr_reader :pipeline, :token, :template - def agents - pipeline.authorized_cluster_agents + def agent_authorizations + pipeline.cluster_agent_authorizations end def cluster_name @@ -52,6 +54,10 @@ module Ci [agent.project.full_path, agent.name].join(delimiter) end + def context_namespace(authorization) + authorization.config['default_namespace'] + end + def agent_token(agent) ['ci', agent.id, token].join(delimiter) end diff --git a/app/views/admin/application_settings/_account_and_limit.html.haml b/app/views/admin/application_settings/_account_and_limit.html.haml index b08a549148d..c091a2180c5 100644 --- a/app/views/admin/application_settings/_account_and_limit.html.haml +++ b/app/views/admin/application_settings/_account_and_limit.html.haml @@ -54,10 +54,10 @@ - dormant_users_help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: dormant_users_help_link } = f.gitlab_ui_checkbox_component :deactivate_dormant_users, _('Deactivate dormant users after a period of inactivity'), help_text: _('Users can reactivate their account by signing in. %{link_start}Learn more.%{link_end}').html_safe % { link_start: dormant_users_help_link_start, link_end: '</a>'.html_safe } .form-group - = f.label :deactivate_dormant_users_period, _('Period of inactivity (days)'), class: 'label-light' - = f.number_field :deactivate_dormant_users_period, class: 'form-control gl-form-input', min: '1' + = f.label :deactivate_dormant_users_period, _('Days of inactivity before deactivation'), class: 'label-light' + = f.number_field :deactivate_dormant_users_period, class: 'form-control gl-form-input', min: '90', step: '1' .form-text.text-muted - = _('Period of inactivity before deactivation.') + = _('Must be 90 days or more.') .form-group = f.label :personal_access_token_prefix, _('Personal Access Token prefix'), class: 'label-light' diff --git a/app/views/import/fogbugz/new_user_map.html.haml b/app/views/import/fogbugz/new_user_map.html.haml index 28836055e0e..9d4c0f62134 100644 --- a/app/views/import/fogbugz/new_user_map.html.haml +++ b/app/views/import/fogbugz/new_user_map.html.haml @@ -23,23 +23,21 @@ %p = html_escape(_('Selecting a GitLab user will add a link to the GitLab user in the descriptions of issues and comments (e.g. "By %{link_open}@johnsmith%{link_close}"). It will also associate and/or assign these issues and comments with the selected user.')) % { link_open: '<a href="#">'.html_safe, link_close: '</a>'.html_safe } - .table-holder - %table.table - %thead + %table.table + %thead + %tr + %th= _("ID") + %th= _("Name") + %th= _("Email") + %th= _("GitLab User") + %tbody + - @user_map.each do |id, user| %tr - %th= _("ID") - %th= _("Name") - %th= _("Email") - %th= _("GitLab User") - %tbody - - @user_map.each do |id, user| - %tr - %td= id - %td= text_field_tag "users[#{id}][name]", user[:name], class: 'form-control' - %td= text_field_tag "users[#{id}][email]", user[:email], class: 'form-control' - %td - = users_select_tag("users[#{id}][gitlab_user]", class: 'custom-form-control', - scope: :all, email_user: true, selected: user[:gitlab_user]) + %td= id + %td= text_field_tag "users[#{id}][name]", user[:name], class: 'form-control gl-form-input' + %td= text_field_tag "users[#{id}][email]", user[:email], class: 'form-control gl-form-input' + %td + .js-gitlab-user{ data: { name: "users[#{id}][gitlab_user]" } } .form-actions = submit_tag _('Continue to the next step'), class: 'gl-button btn btn-confirm' diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml index 549436ccabf..c95e63bdc83 100644 --- a/app/views/shared/web_hooks/_form.html.haml +++ b/app/views/shared/web_hooks/_form.html.haml @@ -1,7 +1,7 @@ = form_errors(hook) - if Feature.enabled?(:webhook_form_mask_url) - .js-vue-webhook-form + .js-vue-webhook-form{ data: webhook_form_data(hook) } - else .form-group = form.label :url, s_('Webhooks|URL'), class: 'label-bold' |