Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/api/projects_api.js7
-rw-r--r--app/assets/javascripts/groups/settings/components/group_settings_readme.vue147
-rw-r--r--app/assets/javascripts/groups/settings/constants.js4
-rw-r--r--app/assets/javascripts/groups/settings/init_group_settings_readme.js24
-rw-r--r--app/assets/javascripts/lib/utils/web_ide_navigator.js24
-rw-r--r--app/assets/javascripts/pages/groups/edit/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/blob/show/index.js2
-rw-r--r--app/assets/javascripts/repository/components/fork_info.vue46
-rw-r--r--app/assets/javascripts/repository/index.js6
-rw-r--r--app/assets/javascripts/super_sidebar/components/merge_request_menu.vue7
-rw-r--r--app/assets/javascripts/super_sidebar/components/user_bar.vue9
-rw-r--r--app/assets/stylesheets/components/whats_new.scss14
-rw-r--r--app/finders/groups/accepting_project_creations_finder.rb105
-rw-r--r--app/finders/groups/user_groups_finder.rb2
-rw-r--r--app/helpers/groups_helper.rb9
-rw-r--r--app/helpers/projects_helper.rb9
-rw-r--r--app/models/concerns/expirable.rb2
-rw-r--r--app/models/group.rb6
-rw-r--r--app/models/group_group_link.rb8
-rw-r--r--app/views/groups/settings/_general.html.haml6
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