diff options
Diffstat (limited to 'app')
20 files changed, 410 insertions, 30 deletions
diff --git a/app/assets/javascripts/api/projects_api.js b/app/assets/javascripts/api/projects_api.js index 5a794dcd035..c72a913aacd 100644 --- a/app/assets/javascripts/api/projects_api.js +++ b/app/assets/javascripts/api/projects_api.js @@ -35,6 +35,13 @@ export function getProjects(query, options, callback = () => {}) { }); } +export function createProject(projectData) { + const url = buildApiUrl(PROJECTS_PATH); + return axios.post(url, projectData).then(({ data }) => { + return data; + }); +} + export function importProjectMembers(sourceId, targetId) { const url = buildApiUrl(PROJECT_IMPORT_MEMBERS_PATH) .replace(':id', sourceId) diff --git a/app/assets/javascripts/groups/settings/components/group_settings_readme.vue b/app/assets/javascripts/groups/settings/components/group_settings_readme.vue new file mode 100644 index 00000000000..123c7fc58f5 --- /dev/null +++ b/app/assets/javascripts/groups/settings/components/group_settings_readme.vue @@ -0,0 +1,147 @@ +<script> +import { GlButton, GlModal, GlModalDirective, GlSprintf } from '@gitlab/ui'; +import { s__, __ } from '~/locale'; +import { createProject } from '~/rest_api'; +import { createAlert } from '~/alert'; +import { openWebIDE } from '~/lib/utils/web_ide_navigator'; +import { README_MODAL_ID, GITLAB_README_PROJECT, README_FILE } from '../constants'; + +export default { + name: 'GroupSettingsReadme', + i18n: { + readme: __('README'), + addReadme: __('Add README'), + cancel: __('Cancel'), + createProjectAndReadme: s__('Groups|Create and add README'), + creatingReadme: s__('Groups|Creating README'), + existingProjectNewReadme: s__('Groups|This will create a README.md for project %{path}.'), + newProjectAndReadme: s__('Groups|This will create a project %{path} and add a README.md.'), + errorCreatingProject: s__('Groups|There was an error creating the Group README.'), + }, + components: { + GlButton, + GlModal, + GlSprintf, + }, + directives: { + GlModal: GlModalDirective, + }, + props: { + groupReadmePath: { + type: String, + required: false, + default: '', + }, + readmeProjectPath: { + type: String, + required: false, + default: '', + }, + groupPath: { + type: String, + required: true, + }, + groupId: { + type: String, + required: true, + }, + }, + data() { + return { + creatingReadme: false, + }; + }, + computed: { + hasReadme() { + return this.groupReadmePath.length > 0; + }, + hasReadmeProject() { + return this.readmeProjectPath.length > 0; + }, + pathToReadmeProject() { + return this.hasReadmeProject + ? this.readmeProjectPath + : `${this.groupPath}/${GITLAB_README_PROJECT}`; + }, + modalBody() { + return this.hasReadmeProject + ? this.$options.i18n.existingProjectNewReadme + : this.$options.i18n.newProjectAndReadme; + }, + modalSubmitButtonText() { + return this.hasReadmeProject + ? this.$options.i18n.addReadme + : this.$options.i18n.createProjectAndReadme; + }, + }, + methods: { + hideModal() { + this.$refs.modal.hide(); + }, + createReadme() { + if (this.hasReadmeProject) { + openWebIDE(this.readmeProjectPath, README_FILE); + } else { + this.createProjectWithReadme(); + } + }, + createProjectWithReadme() { + this.creatingReadme = true; + + const projectData = { + name: GITLAB_README_PROJECT, + namespace_id: this.groupId, + }; + + createProject(projectData) + .then(({ path_with_namespace: pathWithNamespace }) => { + openWebIDE(pathWithNamespace, README_FILE); + }) + .catch(() => { + this.hideModal(); + this.creatingReadme = false; + createAlert({ message: this.$options.i18n.errorCreatingProject }); + }); + }, + }, + README_MODAL_ID, +}; +</script> + +<template> + <div> + <gl-button v-if="hasReadme" icon="doc-text" :href="groupReadmePath">{{ + $options.i18n.readme + }}</gl-button> + <gl-button + v-else + v-gl-modal="$options.README_MODAL_ID" + variant="dashed" + icon="file-addition" + data-testid="group-settings-add-readme-button" + >{{ $options.i18n.addReadme }}</gl-button + > + <gl-modal ref="modal" :modal-id="$options.README_MODAL_ID" :title="$options.i18n.addReadme"> + <div data-testid="group-settings-modal-readme-body"> + <gl-sprintf :message="modalBody"> + <template #path> + <code>{{ pathToReadmeProject }}</code> + </template> + </gl-sprintf> + </div> + <template #modal-footer> + <gl-button variant="default" @click="hideModal">{{ $options.i18n.cancel }}</gl-button> + <gl-button v-if="creatingReadme" variant="default" loading disabled>{{ + $options.i18n.creatingReadme + }}</gl-button> + <gl-button + v-else + variant="confirm" + data-testid="group-settings-modal-create-readme-button" + @click="createReadme" + >{{ modalSubmitButtonText }}</gl-button + > + </template> + </gl-modal> + </div> +</template> diff --git a/app/assets/javascripts/groups/settings/constants.js b/app/assets/javascripts/groups/settings/constants.js index c91c2a20529..023ddf29b36 100644 --- a/app/assets/javascripts/groups/settings/constants.js +++ b/app/assets/javascripts/groups/settings/constants.js @@ -1,3 +1,7 @@ export const LEVEL_TYPES = { GROUP: 'group', }; + +export const README_MODAL_ID = 'add_group_readme_modal'; +export const GITLAB_README_PROJECT = 'gitlab-profile'; +export const README_FILE = 'README.md'; diff --git a/app/assets/javascripts/groups/settings/init_group_settings_readme.js b/app/assets/javascripts/groups/settings/init_group_settings_readme.js new file mode 100644 index 00000000000..d126228d854 --- /dev/null +++ b/app/assets/javascripts/groups/settings/init_group_settings_readme.js @@ -0,0 +1,24 @@ +import Vue from 'vue'; +import GroupSettingsReadme from './components/group_settings_readme.vue'; + +export const initGroupSettingsReadme = () => { + const el = document.getElementById('js-group-settings-readme'); + + if (!el) return false; + + const { groupReadmePath, readmeProjectPath, groupPath, groupId } = el.dataset; + + return new Vue({ + el, + render(createElement) { + return createElement(GroupSettingsReadme, { + props: { + groupReadmePath, + readmeProjectPath, + groupPath, + groupId, + }, + }); + }, + }); +}; diff --git a/app/assets/javascripts/lib/utils/web_ide_navigator.js b/app/assets/javascripts/lib/utils/web_ide_navigator.js new file mode 100644 index 00000000000..f0579b5886d --- /dev/null +++ b/app/assets/javascripts/lib/utils/web_ide_navigator.js @@ -0,0 +1,24 @@ +import { visitUrl, webIDEUrl } from '~/lib/utils/url_utility'; + +/** + * Takes a project path and optional file path and branch + * and then redirects the user to the web IDE. + * + * @param {string} projectPath - Full path to project including namespace (ex. flightjs/Flight) + * @param {string} filePath - optional path to file to be edited, otherwise will open at base directory (ex. README.md) + * @param {string} branch - optional branch to open the IDE, defaults to 'main' + */ + +export const openWebIDE = (projectPath, filePath, branch = 'main') => { + if (!projectPath) { + throw new TypeError('projectPath parameter is required'); + } + + const pathnameSegments = [projectPath, 'edit', branch, '-']; + + if (filePath) { + pathnameSegments.push(filePath); + } + + visitUrl(webIDEUrl(`/${pathnameSegments.join('/')}/`)); +}; diff --git a/app/assets/javascripts/pages/groups/edit/index.js b/app/assets/javascripts/pages/groups/edit/index.js index dec06fe6f4d..721168f6140 100644 --- a/app/assets/javascripts/pages/groups/edit/index.js +++ b/app/assets/javascripts/pages/groups/edit/index.js @@ -9,6 +9,7 @@ import mountBadgeSettings from '~/pages/shared/mount_badge_settings'; import initSearchSettings from '~/search_settings'; import initSettingsPanels from '~/settings_panels'; import initConfirmDanger from '~/init_confirm_danger'; +import { initGroupSettingsReadme } from '~/groups/settings/init_group_settings_readme'; initFilePickers(); initConfirmDanger(); @@ -27,3 +28,5 @@ initProjectSelects(); initSearchSettings(); initCascadingSettingsLockPopovers(); + +initGroupSettingsReadme(); diff --git a/app/assets/javascripts/pages/projects/blob/show/index.js b/app/assets/javascripts/pages/projects/blob/show/index.js index dd39fb7c666..2f8c2a8e86f 100644 --- a/app/assets/javascripts/pages/projects/blob/show/index.js +++ b/app/assets/javascripts/pages/projects/blob/show/index.js @@ -102,6 +102,7 @@ const initForkInfo = () => { sourceDefaultBranch, aheadComparePath, behindComparePath, + canUserCreateMrInFork, } = forkEl.dataset; return new Vue({ el: forkEl, @@ -116,6 +117,7 @@ const initForkInfo = () => { sourceDefaultBranch, aheadComparePath, behindComparePath, + canUserCreateMrInFork, }, }); }, diff --git a/app/assets/javascripts/repository/components/fork_info.vue b/app/assets/javascripts/repository/components/fork_info.vue index d84e197714e..a7795c8da0a 100644 --- a/app/assets/javascripts/repository/components/fork_info.vue +++ b/app/assets/javascripts/repository/components/fork_info.vue @@ -24,7 +24,8 @@ export const i18n = { behindAhead: s__('ForksDivergence|%{messages} the upstream repository.'), limitedVisibility: s__('ForksDivergence|Source project has a limited visibility.'), error: s__('ForksDivergence|Failed to fetch fork details. Try again later.'), - sync: s__('ForksDivergence|Update fork'), + updateFork: s__('ForksDivergence|Update fork'), + createMergeRequest: s__('ForksDivergence|Create merge request'), }; export default { @@ -103,6 +104,16 @@ export default { required: false, default: '', }, + createMrPath: { + type: String, + required: false, + default: '', + }, + canUserCreateMrInFork: { + type: Boolean, + required: false, + default: false, + }, }, data() { return { @@ -173,12 +184,15 @@ export default { hasBehindAheadMessage() { return this.behindAheadMessage.length > 0; }, - isSyncButtonAvailable() { + hasUpdateButton() { return ( this.glFeatures.synchronizeFork && ((this.sourceName && this.forkDetails && this.behind) || this.isUnknownDivergence) ); }, + hasCreateMrButton() { + return this.canUserCreateMrInFork && this.ahead && this.createMrPath; + }, forkDivergenceMessage() { if (!this.forkDetails) { return this.$options.i18n.limitedVisibility; @@ -286,14 +300,26 @@ export default { > {{ $options.i18n.inaccessibleProject }} </div> - <gl-button - v-if="isSyncButtonAvailable" - :disabled="forkDetails.isSyncing" - @click="checkIfSyncIsPossible" - > - <gl-loading-icon v-if="forkDetails.isSyncing" class="gl-display-inline" size="sm" /> - <span>{{ $options.i18n.sync }}</span> - </gl-button> + <div class="gl-display-flex gl-xs-display-none!"> + <gl-button + v-if="hasCreateMrButton" + class="gl-ml-4" + :href="createMrPath" + data-testid="create-mr-button" + > + <span>{{ $options.i18n.createMergeRequest }}</span> + </gl-button> + <gl-button + v-if="hasUpdateButton" + class="gl-ml-4" + :disabled="forkDetails.isSyncing" + data-testid="update-fork-button" + @click="checkIfSyncIsPossible" + > + <gl-loading-icon v-if="forkDetails.isSyncing" class="gl-display-inline" size="sm" /> + <span>{{ $options.i18n.updateFork }}</span> + </gl-button> + </div> <conflicts-modal ref="modal" :source-name="sourceName" diff --git a/app/assets/javascripts/repository/index.js b/app/assets/javascripts/repository/index.js index 8dc67b97a60..b1217881bc3 100644 --- a/app/assets/javascripts/repository/index.js +++ b/app/assets/javascripts/repository/index.js @@ -74,8 +74,10 @@ export default function setupVueRepositoryList() { sourceName, sourcePath, sourceDefaultBranch, + createMrPath, aheadComparePath, behindComparePath, + canUserCreateMrInFork, } = forkEl.dataset; return new Vue({ el: forkEl, @@ -90,6 +92,8 @@ export default function setupVueRepositoryList() { sourceDefaultBranch, aheadComparePath, behindComparePath, + createMrPath, + canUserCreateMrInFork, }, }); }, @@ -153,8 +157,8 @@ export default function setupVueRepositoryList() { initLastCommitApp(); initBlobControlsApp(); - initForkInfo(); initRefSwitcher(); + initForkInfo(); router.afterEach(({ params: { path } }) => { setTitle(path, ref, fullName); diff --git a/app/assets/javascripts/super_sidebar/components/merge_request_menu.vue b/app/assets/javascripts/super_sidebar/components/merge_request_menu.vue index 94fc6aedcc0..d37e863bed9 100644 --- a/app/assets/javascripts/super_sidebar/components/merge_request_menu.vue +++ b/app/assets/javascripts/super_sidebar/components/merge_request_menu.vue @@ -16,7 +16,12 @@ export default { </script> <template> - <gl-disclosure-dropdown :items="items" placement="center"> + <gl-disclosure-dropdown + :items="items" + placement="center" + @shown="$emit('shown')" + @hidden="$emit('hidden')" + > <template #toggle> <slot></slot> </template> diff --git a/app/assets/javascripts/super_sidebar/components/user_bar.vue b/app/assets/javascripts/super_sidebar/components/user_bar.vue index e03c587567e..498c082ddb2 100644 --- a/app/assets/javascripts/super_sidebar/components/user_bar.vue +++ b/app/assets/javascripts/super_sidebar/components/user_bar.vue @@ -56,6 +56,11 @@ export default { required: true, }, }, + data() { + return { + mrMenuShown: false, + }; + }, methods: { collapseSidebar() { toggleSuperSidebarCollapsed(true, true, true); @@ -144,9 +149,11 @@ export default { <merge-request-menu class="gl-flex-basis-third gl-display-block!" :items="sidebarData.merge_request_menu" + @shown="mrMenuShown = true" + @hidden="mrMenuShown = false" > <counter - v-gl-tooltip:super-sidebar.hover.bottom="$options.i18n.mergeRequests" + v-gl-tooltip:super-sidebar.hover.bottom="mrMenuShown ? '' : $options.i18n.mergeRequests" class="gl-w-full" icon="merge-request-open" :count="sidebarData.total_merge_requests_count" diff --git a/app/assets/stylesheets/components/whats_new.scss b/app/assets/stylesheets/components/whats_new.scss index c1c68f64d86..35c619a2e2f 100644 --- a/app/assets/stylesheets/components/whats_new.scss +++ b/app/assets/stylesheets/components/whats_new.scss @@ -1,5 +1,5 @@ .whats-new-drawer { - margin-top: $header-height; + margin-top: calc(#{$header-height} + #{$calc-application-bars-height}); @include gl-shadow-none; overflow-y: hidden; width: 500px; @@ -35,18 +35,6 @@ } } -.with-performance-bar .whats-new-drawer { - margin-top: calc(#{$performance-bar-height} + #{$header-height}); -} - -.with-system-header .whats-new-drawer { - margin-top: calc(#{$system-header-height} + #{$header-height}); -} - -.with-performance-bar.with-system-header .whats-new-drawer { - margin-top: calc(#{$performance-bar-height} + #{$system-header-height} + #{$header-height}); -} - .whats-new-item-title-link { &:hover, &:focus, diff --git a/app/finders/groups/accepting_project_creations_finder.rb b/app/finders/groups/accepting_project_creations_finder.rb new file mode 100644 index 00000000000..a7057b3f672 --- /dev/null +++ b/app/finders/groups/accepting_project_creations_finder.rb @@ -0,0 +1,105 @@ +# frozen_string_literal: true + +module Groups + class AcceptingProjectCreationsFinder + def initialize(current_user) + @current_user = current_user + end + + def execute + if Feature.disabled?(:include_groups_from_group_shares_in_project_creation_locations) + return current_user.manageable_groups(include_groups_with_developer_maintainer_access: true) + end + + groups_accepting_project_creations = + [ + current_user + .manageable_groups(include_groups_with_developer_maintainer_access: true) + .project_creation_allowed, + owner_maintainer_groups_originating_from_group_shares + .project_creation_allowed, + *developer_groups_originating_from_group_shares + ] + + # We move the UNION query into a materialized CTE to improve query performance during text search. + union_query = ::Group.from_union(groups_accepting_project_creations) + cte = Gitlab::SQL::CTE.new(:my_union_cte, union_query) + + Group.with(cte.to_arel).from(cte.alias_to(Group.arel_table)) # rubocop: disable CodeReuse/ActiveRecord + end + + private + + attr_reader :current_user + + def owner_maintainer_groups_originating_from_group_shares + GroupGroupLink + .with_owner_or_maintainer_access + .groups_accessible_via( + groups_that_user_has_owner_or_maintainer_access_via_direct_membership + .select(:id) + ) + end + + def groups_that_user_has_owner_or_maintainer_access_via_direct_membership + current_user.owned_or_maintainers_groups + end + + def developer_groups_originating_from_group_shares + # Example: + # + # Group A -----shared to---> Group B + # + + # Now, there are 2 ways a user in Group A can get "Developer" access to Group B (and it's subgroups) + [ + # 1. User has Developer or above access in Group A, + # but the group_group_link has MAX access level set to Developer + GroupGroupLink + .with_developer_access + .groups_accessible_via( + groups_that_user_has_developer_access_and_above_via_direct_membership + .select(:id) + ).with_project_creation_levels(project_creations_levels_allowing_developers_to_create_projects), + + # 2. User has exactly Developer access in Group A, + # but the group_group_link has MAX access level set to Developer or above. + GroupGroupLink + .with_developer_maintainer_owner_access + .groups_accessible_via( + groups_that_user_has_developer_access_via_direct_membership + .select(:id) + ).with_project_creation_levels(project_creations_levels_allowing_developers_to_create_projects) + ] + + # Lastly, we should make sure that such groups indeed allow Developers to create projects in them, + # based on the value of `groups.project_creation_level`, + # which is why we use the scope .with_project_creation_levels on each set. + end + + def groups_that_user_has_developer_access_and_above_via_direct_membership + current_user.developer_maintainer_owned_groups + end + + def groups_that_user_has_developer_access_via_direct_membership + current_user.developer_groups + end + + def project_creations_levels_allowing_developers_to_create_projects + project_creation_levels = [::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS] + + # When the value of application_settings.default_project_creation is set to `DEVELOPER_MAINTAINER_PROJECT_ACCESS`, + # it means that a `nil` value for `groups.project_creation_level` is telling us: + # such groups also have `project_creation_level` implicitly set to `DEVELOPER_MAINTAINER_PROJECT_ACCESS`. + # ie, `nil` is a placeholder value for inheriting the value from the ApplicationSetting. + # So we will include `nil` in the list, + # when the application_setting's value is `DEVELOPER_MAINTAINER_PROJECT_ACCESS` + + if ::Gitlab::CurrentSettings.default_project_creation == ::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS + project_creation_levels << nil + end + + project_creation_levels + end + end +end diff --git a/app/finders/groups/user_groups_finder.rb b/app/finders/groups/user_groups_finder.rb index b58c1323b1f..83e012b3dbe 100644 --- a/app/finders/groups/user_groups_finder.rb +++ b/app/finders/groups/user_groups_finder.rb @@ -36,7 +36,7 @@ module Groups def by_permission_scope if permission_scope_create_projects? - target_user.manageable_groups(include_groups_with_developer_maintainer_access: true) + Groups::AcceptingProjectCreationsFinder.new(target_user).execute # rubocop: disable CodeReuse/Finder elsif permission_scope_transfer_projects? Groups::AcceptingProjectTransfersFinder.new(target_user).execute # rubocop: disable CodeReuse/Finder else diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index 85bbc796dab..66e710485af 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -180,6 +180,15 @@ module GroupsHelper Feature.enabled?(:show_group_readme, group) && group.group_readme end + def group_settings_readme_app_data(group) + { + group_readme_path: group.group_readme&.present&.web_path, + readme_project_path: group.readme_project&.present&.path_with_namespace, + group_path: group.full_path, + group_id: group.id + } + end + def enabled_git_access_protocol_options_for_group case ::Gitlab::CurrentSettings.enabled_git_access_protocol when nil, "" diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 1a0e26bd848..21a736bf68a 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -2,6 +2,7 @@ module ProjectsHelper include Gitlab::Utils::StrongMemoize + include CompareHelper def project_incident_management_setting @project_incident_management_setting ||= @project.incident_management_setting || @@ -139,9 +140,11 @@ module ProjectsHelper ahead_compare_path: project_compare_path( project, from: source_default_branch, to: ref, from_project_id: source_project.id ), + create_mr_path: create_mr_path(from: ref, source_project: project, to: source_default_branch, target_project: source_project), behind_compare_path: project_compare_path( source_project, from: ref, to: source_default_branch, from_project_id: project.id - ) + ), + can_user_create_mr_in_fork: can_user_create_mr_in_fork(source_project) } end @@ -163,6 +166,10 @@ module ProjectsHelper project.fork_source if project.fork_source && can?(current_user, :read_project, project.fork_source) end + def can_user_create_mr_in_fork(project) + can?(current_user, :create_merge_request_in, project) + end + def project_search_tabs?(tab) return false unless @project.present? diff --git a/app/models/concerns/expirable.rb b/app/models/concerns/expirable.rb index 5975ea23723..cc55315d6d7 100644 --- a/app/models/concerns/expirable.rb +++ b/app/models/concerns/expirable.rb @@ -8,7 +8,7 @@ module Expirable included do scope :not, ->(scope) { where(scope.arel.constraints.reduce(:and).not) } - scope :expired, -> { where('expires_at IS NOT NULL AND expires_at <= ?', Time.current) } + scope :expired, -> { where.not(expires_at: nil).where(arel_table[:expires_at].lteq(Time.current)) } scope :not_expired, -> { self.not(expired) } end diff --git a/app/models/group.rb b/app/models/group.rb index 4aa97786ca1..f13ce2ddca1 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -200,6 +200,10 @@ class Group < Namespace .where(project_authorizations: { user_id: user_ids }) end + scope :with_project_creation_levels, -> (project_creation_levels) do + where(project_creation_level: project_creation_levels) + end + scope :project_creation_allowed, -> do project_creation_allowed_on_levels = [ ::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS, @@ -216,7 +220,7 @@ class Group < Namespace project_creation_allowed_on_levels.delete(nil) end - where(project_creation_level: project_creation_allowed_on_levels) + with_project_creation_levels(project_creation_allowed_on_levels) end scope :shared_into_ancestors, -> (group) do diff --git a/app/models/group_group_link.rb b/app/models/group_group_link.rb index 15949570f9c..fdb8fb9ed75 100644 --- a/app/models/group_group_link.rb +++ b/app/models/group_group_link.rb @@ -19,6 +19,14 @@ class GroupGroupLink < ApplicationRecord where(group_access: [Gitlab::Access::OWNER, Gitlab::Access::MAINTAINER]) end + scope :with_developer_maintainer_owner_access, -> do + where(group_access: [Gitlab::Access::DEVELOPER, Gitlab::Access::MAINTAINER, Gitlab::Access::OWNER]) + end + + scope :with_developer_access, -> do + where(group_access: [Gitlab::Access::DEVELOPER]) + end + scope :with_owner_access, -> do where(group_access: [Gitlab::Access::OWNER]) end diff --git a/app/views/groups/settings/_general.html.haml b/app/views/groups/settings/_general.html.haml index 5258854c931..dc04a2079c9 100644 --- a/app/views/groups/settings/_general.html.haml +++ b/app/views/groups/settings/_general.html.haml @@ -19,6 +19,12 @@ = f.label :description, s_('Groups|Group description (optional)'), class: 'label-bold' = f.text_area :description, class: 'form-control', rows: 3, maxlength: 250 + - if Feature.enabled?(:show_group_readme, @group) + .row.gl-mt-3 + .form-group.col-md-5 + = f.label :description, s_('Groups|Group README'), class: 'label-bold' + #js-group-settings-readme{ data: group_settings_readme_app_data(@group) } + = render 'shared/repository_size_limit_setting_registration_features_cta', form: f = render_if_exists 'shared/repository_size_limit_setting', form: f, type: :group |