diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-02-14 15:09:08 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-02-14 15:09:08 +0000 |
commit | b3a736ed88a1db0391cd9881e70b987bab7d89d1 (patch) | |
tree | a91ca3a06abd4c3412775ac3c49b11e3151df2be /app | |
parent | 5366964a10484c2783a646b35a6da9eece01b242 (diff) | |
download | gitlab-ce-b3a736ed88a1db0391cd9881e70b987bab7d89d1.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
24 files changed, 169 insertions, 68 deletions
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue index d9cd4f3acf1..2581c3e9928 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue @@ -70,7 +70,7 @@ export default { :title="$options.currentBranchPermissionsTooltip" > <span - class="ide-radio-label" + class="ide-option-label" data-qa-selector="commit_to_current_branch_radio" v-html="commitToCurrentBranchText" ></span> diff --git a/app/assets/javascripts/ide/components/commit_sidebar/new_merge_request_option.vue b/app/assets/javascripts/ide/components/commit_sidebar/new_merge_request_option.vue index daa44a42765..0812599c25c 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/new_merge_request_option.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/new_merge_request_option.vue @@ -1,16 +1,27 @@ <script> import { createNamespacedHelpers } from 'vuex'; +import { GlTooltipDirective } from '@gitlab/ui'; +import { s__ } from '~/locale'; -const { - mapState: mapCommitState, - mapActions: mapCommitActions, - mapGetters: mapCommitGetters, -} = createNamespacedHelpers('commit'); +const { mapActions: mapCommitActions, mapGetters: mapCommitGetters } = createNamespacedHelpers( + 'commit', +); export default { + directives: { + GlTooltip: GlTooltipDirective, + }, computed: { - ...mapCommitState(['shouldCreateMR']), - ...mapCommitGetters(['shouldHideNewMrOption']), + ...mapCommitGetters(['shouldHideNewMrOption', 'shouldDisableNewMrOption', 'shouldCreateMR']), + tooltipText() { + if (this.shouldDisableNewMrOption) { + return s__( + 'IDE|This option is disabled because you are not allowed to create merge requests in this project.', + ); + } + + return ''; + }, }, methods: { ...mapCommitActions(['toggleShouldCreateMR']), @@ -21,14 +32,19 @@ export default { <template> <fieldset v-if="!shouldHideNewMrOption"> <hr class="my-2" /> - <label class="mb-0 js-ide-commit-new-mr"> + <label + v-gl-tooltip="tooltipText" + class="mb-0 js-ide-commit-new-mr" + :class="{ 'is-disabled': shouldDisableNewMrOption }" + > <input + :disabled="shouldDisableNewMrOption" :checked="shouldCreateMR" type="checkbox" data-qa-selector="start_new_mr_checkbox" @change="toggleShouldCreateMR" /> - <span class="prepend-left-10"> + <span class="prepend-left-10 ide-option-label"> {{ __('Start a new merge request') }} </span> </label> diff --git a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue index 07073f5f879..a9591805261 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue @@ -67,7 +67,7 @@ export default { @change="updateCommitAction($event.target.value)" /> <span class="prepend-left-10"> - <span v-if="label" class="ide-radio-label"> {{ label }} </span> <slot v-else></slot> + <span v-if="label" class="ide-option-label"> {{ label }} </span> <slot v-else></slot> </span> </label> <div v-if="commitAction === value && showInput" class="ide-commit-new-branch"> diff --git a/app/assets/javascripts/ide/components/nav_dropdown.vue b/app/assets/javascripts/ide/components/nav_dropdown.vue index 2e290de0943..2307efd1d24 100644 --- a/app/assets/javascripts/ide/components/nav_dropdown.vue +++ b/app/assets/javascripts/ide/components/nav_dropdown.vue @@ -1,5 +1,6 @@ <script> import $ from 'jquery'; +import { mapGetters } from 'vuex'; import NavForm from './nav_form.vue'; import NavDropdownButton from './nav_dropdown_button.vue'; @@ -13,6 +14,9 @@ export default { isVisibleDropdown: false, }; }, + computed: { + ...mapGetters(['canReadMergeRequests']), + }, mounted() { this.addDropdownListeners(); }, @@ -42,7 +46,9 @@ export default { <template> <div ref="dropdown" class="btn-group ide-nav-dropdown dropdown"> - <nav-dropdown-button /> - <div class="dropdown-menu dropdown-menu-left p-0"><nav-form v-if="isVisibleDropdown" /></div> + <nav-dropdown-button :show-merge-requests="canReadMergeRequests" /> + <div class="dropdown-menu dropdown-menu-left p-0"> + <nav-form v-if="isVisibleDropdown" :show-merge-requests="canReadMergeRequests" /> + </div> </div> </template> diff --git a/app/assets/javascripts/ide/components/nav_dropdown_button.vue b/app/assets/javascripts/ide/components/nav_dropdown_button.vue index f1d44443125..4cd320d5d66 100644 --- a/app/assets/javascripts/ide/components/nav_dropdown_button.vue +++ b/app/assets/javascripts/ide/components/nav_dropdown_button.vue @@ -10,6 +10,13 @@ export default { Icon, DropdownButton, }, + props: { + showMergeRequests: { + type: Boolean, + required: false, + default: true, + }, + }, computed: { ...mapState(['currentBranchId', 'currentMergeRequestId']), mergeRequestLabel() { @@ -25,10 +32,10 @@ export default { <template> <dropdown-button> <span class="row"> - <span class="col-7 text-truncate"> + <span class="col-auto text-truncate" :class="{ 'col-7': showMergeRequests }"> <icon :size="16" :aria-label="__('Current Branch')" name="branch" /> {{ branchLabel }} </span> - <span class="col-5 pl-0 text-truncate"> + <span v-if="showMergeRequests" class="col-5 pl-0 text-truncate"> <icon :size="16" :aria-label="__('Merge Request')" name="merge-request" /> {{ mergeRequestLabel }} </span> diff --git a/app/assets/javascripts/ide/components/nav_form.vue b/app/assets/javascripts/ide/components/nav_form.vue index 2ccc84ea5d5..195504a6861 100644 --- a/app/assets/javascripts/ide/components/nav_form.vue +++ b/app/assets/javascripts/ide/components/nav_form.vue @@ -11,12 +11,19 @@ export default { BranchesSearchList, MergeRequestSearchList, }, + props: { + showMergeRequests: { + type: Boolean, + required: false, + default: true, + }, + }, }; </script> <template> <div class="ide-nav-form p-0"> - <tabs stop-propagation> + <tabs v-if="showMergeRequests" stop-propagation> <tab active> <template slot="title"> {{ __('Branches') }} @@ -30,5 +37,6 @@ export default { <merge-request-search-list /> </tab> </tabs> + <branches-search-list v-else /> </div> </template> diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js index 673ac1bfa9a..54d3e79411f 100644 --- a/app/assets/javascripts/ide/constants.js +++ b/app/assets/javascripts/ide/constants.js @@ -8,6 +8,9 @@ export const MAX_BODY_LENGTH = 72; export const FILE_VIEW_MODE_EDITOR = 'editor'; export const FILE_VIEW_MODE_PREVIEW = 'preview'; +export const PERMISSION_CREATE_MR = 'createMergeRequestIn'; +export const PERMISSION_READ_MR = 'readMergeRequest'; + export const activityBarViews = { edit: 'ide-tree', commit: 'commit-section', diff --git a/app/assets/javascripts/ide/queries/getUserPermissions.query.graphql b/app/assets/javascripts/ide/queries/getUserPermissions.query.graphql new file mode 100644 index 00000000000..48f63995f44 --- /dev/null +++ b/app/assets/javascripts/ide/queries/getUserPermissions.query.graphql @@ -0,0 +1,8 @@ +query getUserPermissions($projectPath: ID!) { + project(fullPath: $projectPath) { + userPermissions { + createMergeRequestIn, + readMergeRequest + } + } +} diff --git a/app/assets/javascripts/ide/services/gql.js b/app/assets/javascripts/ide/services/gql.js new file mode 100644 index 00000000000..8a7f27328ba --- /dev/null +++ b/app/assets/javascripts/ide/services/gql.js @@ -0,0 +1,8 @@ +import createGqClient, { fetchPolicies } from '~/lib/graphql'; + +export default createGqClient( + {}, + { + fetchPolicy: fetchPolicies.NO_CACHE, + }, +); diff --git a/app/assets/javascripts/ide/services/index.js b/app/assets/javascripts/ide/services/index.js index a5134c64705..84a2b2bd58e 100644 --- a/app/assets/javascripts/ide/services/index.js +++ b/app/assets/javascripts/ide/services/index.js @@ -1,6 +1,18 @@ import axios from '~/lib/utils/axios_utils'; import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility'; import Api from '~/api'; +import getUserPermissions from '../queries/getUserPermissions.query.graphql'; +import gqClient from './gql'; + +const fetchApiProjectData = projectPath => Api.project(projectPath).then(({ data }) => data); + +const fetchGqlProjectData = projectPath => + gqClient + .query({ + query: getUserPermissions, + variables: { projectPath }, + }) + .then(({ data }) => data.project); export default { getFileData(endpoint) { @@ -47,7 +59,16 @@ export default { .then(({ data }) => data); }, getProjectData(namespace, project) { - return Api.project(`${namespace}/${project}`); + const projectPath = `${namespace}/${project}`; + + return Promise.all([fetchApiProjectData(projectPath), fetchGqlProjectData(projectPath)]).then( + ([apiProjectData, gqlProjectData]) => ({ + data: { + ...apiProjectData, + ...gqlProjectData, + }, + }), + ); }, getProjectMergeRequests(projectId, params = {}) { return Api.projectMergeRequests(projectId, params); diff --git a/app/assets/javascripts/ide/stores/actions/merge_request.js b/app/assets/javascripts/ide/stores/actions/merge_request.js index aa44067edf8..9e9c6fc42b3 100644 --- a/app/assets/javascripts/ide/stores/actions/merge_request.js +++ b/app/assets/javascripts/ide/stores/actions/merge_request.js @@ -2,10 +2,17 @@ import flash from '~/flash'; import { __ } from '~/locale'; import service from '../../services'; import * as types from '../mutation_types'; -import { activityBarViews } from '../../constants'; +import { activityBarViews, PERMISSION_READ_MR } from '../../constants'; -export const getMergeRequestsForBranch = ({ commit, state }, { projectId, branchId } = {}) => - service +export const getMergeRequestsForBranch = ( + { commit, state, getters }, + { projectId, branchId } = {}, +) => { + if (!getters.findProjectPermissions(projectId)[PERMISSION_READ_MR]) { + return Promise.resolve(); + } + + return service .getProjectMergeRequests(`${projectId}`, { source_branch: branchId, source_project_id: state.projects[projectId].id, @@ -36,6 +43,7 @@ export const getMergeRequestsForBranch = ({ commit, state }, { projectId, branch ); throw e; }); +}; export const getMergeRequestData = ( { commit, dispatch, state }, diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js index 2fc574cd343..257062d118c 100644 --- a/app/assets/javascripts/ide/stores/getters.js +++ b/app/assets/javascripts/ide/stores/getters.js @@ -1,5 +1,10 @@ import { getChangesCountForFiles, filePathMatches } from './utils'; -import { activityBarViews, packageJsonPath } from '../constants'; +import { + activityBarViews, + packageJsonPath, + PERMISSION_READ_MR, + PERMISSION_CREATE_MR, +} from '../constants'; export const activeFile = state => state.openFiles.find(file => file.active) || null; @@ -141,5 +146,14 @@ export const getDiffInfo = (state, getters) => path => { }; }; +export const findProjectPermissions = (state, getters) => projectId => + getters.findProject(projectId)?.userPermissions || {}; + +export const canReadMergeRequests = (state, getters) => + Boolean(getters.findProjectPermissions(state.currentProjectId)[PERMISSION_READ_MR]); + +export const canCreateMergeRequests = (state, getters) => + Boolean(getters.findProjectPermissions(state.currentProjectId)[PERMISSION_CREATE_MR]); + // prevent babel-plugin-rewire from generating an invalid default during karma tests export default () => {}; diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js index 0740e0523a9..3be350db3da 100644 --- a/app/assets/javascripts/ide/stores/modules/commit/actions.js +++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js @@ -158,7 +158,7 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo commit(rootTypes.SET_LAST_COMMIT_MSG, '', { root: true }); }, 5000); - if (state.shouldCreateMR) { + if (getters.shouldCreateMR) { const { currentProject } = rootGetters; const targetBranch = getters.isCreatingNewBranch ? rootState.currentBranchId diff --git a/app/assets/javascripts/ide/stores/modules/commit/getters.js b/app/assets/javascripts/ide/stores/modules/commit/getters.js index de289e27199..e421d44b6de 100644 --- a/app/assets/javascripts/ide/stores/modules/commit/getters.js +++ b/app/assets/javascripts/ide/stores/modules/commit/getters.js @@ -54,5 +54,11 @@ export const shouldHideNewMrOption = (_state, getters, _rootState, rootGetters) (!rootGetters.hasMergeRequest && rootGetters.isOnDefaultBranch)) && rootGetters.canPushToBranch; +export const shouldDisableNewMrOption = (state, getters, rootState, rootGetters) => + !rootGetters.canCreateMergeRequests; + +export const shouldCreateMR = (state, getters) => + state.shouldCreateMR && !getters.shouldDisableNewMrOption; + // prevent babel-plugin-rewire from generating an invalid default during karma tests export default () => {}; diff --git a/app/assets/javascripts/pages/admin/application_settings/index.js b/app/assets/javascripts/pages/admin/application_settings/index.js index 089dedd14cb..78b7e29ae53 100644 --- a/app/assets/javascripts/pages/admin/application_settings/index.js +++ b/app/assets/javascripts/pages/admin/application_settings/index.js @@ -3,9 +3,7 @@ import projectSelect from '~/project_select'; import selfMonitor from '~/self_monitor'; document.addEventListener('DOMContentLoaded', () => { - if (gon.features && gon.features.selfMonitoringProject) { - selfMonitor(); - } + selfMonitor(); // Initialize expandable settings panels initSettingsPanels(); projectSelect(); diff --git a/app/assets/javascripts/repository/components/table/index.vue b/app/assets/javascripts/repository/components/table/index.vue index 29a3340b83d..2ba170998e8 100644 --- a/app/assets/javascripts/repository/components/table/index.vue +++ b/app/assets/javascripts/repository/components/table/index.vue @@ -71,7 +71,12 @@ export default { <template> <div class="tree-content-holder"> <div class="table-holder bordered-box"> - <table :aria-label="tableCaption" class="table tree-table qa-file-tree" aria-live="polite"> + <table + :aria-label="tableCaption" + class="table tree-table" + aria-live="polite" + data-qa-selector="file_tree_table" + > <table-header v-once /> <tbody> <parent-row diff --git a/app/assets/javascripts/repository/components/table/row.vue b/app/assets/javascripts/repository/components/table/row.vue index 8703796b116..c905c39bbba 100644 --- a/app/assets/javascripts/repository/components/table/row.vue +++ b/app/assets/javascripts/repository/components/table/row.vue @@ -139,7 +139,13 @@ export default { class="d-inline-block align-text-bottom fa-fw" /> <i v-else :aria-label="type" role="img" :class="iconName" class="fa fa-fw"></i> - <component :is="linkComponent" :to="routerLinkTo" :href="url" class="str-truncated"> + <component + :is="linkComponent" + :to="routerLinkTo" + :href="url" + class="str-truncated" + data-qa-selector="file_name_link" + > {{ fullPath }} </component> <!-- eslint-disable-next-line @gitlab/vue-i18n/no-bare-strings --> diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss index 58279bba4ca..990aca5f0c5 100644 --- a/app/assets/stylesheets/page_bundles/ide.scss +++ b/app/assets/stylesheets/page_bundles/ide.scss @@ -688,7 +688,7 @@ $ide-commit-header-height: 48px; font-weight: normal; &.is-disabled { - .ide-radio-label { + .ide-option-label { text-decoration: line-through; } } diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 00771aff26c..8a583e16c0b 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -11,16 +11,6 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController before_action :set_application_setting before_action :whitelist_query_limiting, only: [:usage_data] - before_action :validate_self_monitoring_feature_flag_enabled, only: [ - :create_self_monitoring_project, - :status_create_self_monitoring_project, - :delete_self_monitoring_project, - :status_delete_self_monitoring_project - ] - - before_action do - push_frontend_feature_flag(:self_monitoring_project) - end VALID_SETTING_PANELS = %w(general integrations repository ci_cd reporting metrics_and_profiling @@ -163,10 +153,6 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController private - def validate_self_monitoring_feature_flag_enabled - self_monitoring_project_not_implemented unless Feature.enabled?(:self_monitoring_project) - end - def self_monitoring_data { project_id: @application_setting.self_monitoring_project_id, @@ -174,16 +160,6 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController } end - def self_monitoring_project_not_implemented - render( - status: :not_implemented, - json: { - message: _('Self-monitoring is not enabled on this GitLab server, contact your administrator.'), - documentation_url: help_page_path('administration/monitoring/gitlab_self_monitoring_project/index') - } - ) - end - def set_application_setting @application_setting = ApplicationSetting.current_without_cache end diff --git a/app/services/projects/lsif_data_service.rb b/app/services/projects/lsif_data_service.rb index 1282a0736e7..971885b680e 100644 --- a/app/services/projects/lsif_data_service.rb +++ b/app/services/projects/lsif_data_service.rb @@ -3,7 +3,7 @@ module Projects class LsifDataService attr_reader :file, :project, :path, :commit_id, - :docs, :doc_ranges, :ranges, :def_refs + :docs, :doc_ranges, :ranges, :def_refs, :hover_refs CACHE_EXPIRE_IN = 1.hour @@ -26,7 +26,8 @@ module Projects end_line: line_data.last, start_char: column_data.first, end_char: column_data.last, - definition_url: definition_url_for(def_refs[ref_id]) + definition_url: definition_url_for(def_refs[ref_id]), + hover: highlighted_hover(hover_refs[ref_id]) } end end @@ -54,6 +55,7 @@ module Projects @doc_ranges = data['doc_ranges'] @ranges = data['ranges'] @def_refs = data['def_refs'] + @hover_refs = data['hover_refs'] end def doc_id @@ -86,5 +88,16 @@ module Projects Gitlab::Routing.url_helpers.project_blob_path(project, definition_ref_path, anchor: line_anchor) end + + def highlighted_hover(hovers) + hovers&.map do |hover| + # Documentation for a method which is added as comments on top of the method + # is stored as a raw string value in LSIF file + next { value: hover } unless hover.is_a?(Hash) + + value = Gitlab::Highlight.highlight(nil, hover['value'], language: hover['language']) + { language: hover['language'], value: value } + end + end end end diff --git a/app/views/admin/application_settings/metrics_and_profiling.html.haml b/app/views/admin/application_settings/metrics_and_profiling.html.haml index ff40d7da892..0b747082de0 100644 --- a/app/views/admin/application_settings/metrics_and_profiling.html.haml +++ b/app/views/admin/application_settings/metrics_and_profiling.html.haml @@ -47,8 +47,7 @@ .settings-content = render 'performance_bar' -- if Feature.enabled?(:self_monitoring_project) - .js-self-monitoring-settings{ data: self_monitoring_project_data } +.js-self-monitoring-settings{ data: self_monitoring_project_data } %section.settings.as-usage.no-animate#js-usage-settings{ class: ('expanded' if expanded_by_default?) } .settings-header#usage-statistics diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 7796db5ba63..d9887cb470a 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -10,7 +10,7 @@ = project_icon(@project, alt: @project.name, class: 'avatar avatar-tile s64', width: 64, height: 64) .d-flex.flex-column.flex-wrap.align-items-baseline .d-inline-flex.align-items-baseline - %h1.home-panel-title.prepend-top-8.append-bottom-5.qa-project-name + %h1.home-panel-title.prepend-top-8.append-bottom-5{ data: { qa_selector: 'project_name_content' } } = @project.name %span.visibility-icon.text-secondary.prepend-left-4.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@project) } = visibility_level_icon(@project.visibility_level, fw: false, options: {class: 'icon'}) @@ -70,7 +70,7 @@ - source = visible_fork_source(@project) - if source #{ s_('ForkedFromProjectPath|Forked from') } - = link_to source.full_name, project_path(source) + = link_to source.full_name, project_path(source), data: { qa_selector: 'forked_from_link' } - else = s_('ForkedFromProjectPath|Forked from an inaccessible project') diff --git a/app/views/projects/tree/_tree_content.html.haml b/app/views/projects/tree/_tree_content.html.haml index cb459b031fc..c65420d537b 100644 --- a/app/views/projects/tree/_tree_content.html.haml +++ b/app/views/projects/tree/_tree_content.html.haml @@ -1,6 +1,6 @@ .tree-content-holder.js-tree-content{ data: tree_content_data(@logs_path, @project, @path) } .table-holder.bordered-box - %table.table#tree-slider{ class: "table_#{@hex_path} tree-table qa-file-tree" } + %table.table#tree-slider{ class: "table_#{@hex_path} tree-table" } %thead %tr %th= s_('ProjectFileTree|Name') diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml index 849d9d7e87c..4d3c24aee6b 100644 --- a/app/views/projects/tree/_tree_header.html.haml +++ b/app/views/projects/tree/_tree_header.html.haml @@ -84,17 +84,16 @@ = render 'projects/find_file_link' - - if can_create_mr_from_fork - - if can_collaborate || current_user&.already_forked?(@project) - - if vue_file_list_enabled? - #js-tree-web-ide-link.d-inline-block - - else - = link_to ide_edit_path(@project, @ref, @path), class: 'btn btn-default qa-web-ide-button' do - = _('Web IDE') + - if can_collaborate || current_user&.already_forked?(@project) + - if vue_file_list_enabled? + #js-tree-web-ide-link.d-inline-block - else - = link_to '#modal-confirm-fork', class: 'btn btn-default qa-web-ide-button', data: { target: '#modal-confirm-fork', toggle: 'modal'} do + = link_to ide_edit_path(@project, @ref, @path), class: 'btn btn-default qa-web-ide-button' do = _('Web IDE') - = render 'shared/confirm_fork_modal', fork_path: ide_fork_and_edit_path(@project, @ref, @path) + - elsif can_create_mr_from_fork + = link_to '#modal-confirm-fork', class: 'btn btn-default qa-web-ide-button', data: { target: '#modal-confirm-fork', toggle: 'modal'} do + = _('Web IDE') + = render 'shared/confirm_fork_modal', fork_path: ide_fork_and_edit_path(@project, @ref, @path) - if show_xcode_link?(@project) .project-action-button.project-xcode.inline< |