Commit 5f1b71d1 authored by Phil Hughes's avatar Phil Hughes

Merge branch '6861-group-level-project-templates-ce' into 'master'

Backport from gitlab-org/gitlab-ee!6878

See merge request gitlab-org/gitlab-ce!23391
parents 93d6f569 fa7acd99
...@@ -5,6 +5,7 @@ import axios from './lib/utils/axios_utils'; ...@@ -5,6 +5,7 @@ import axios from './lib/utils/axios_utils';
const Api = { const Api = {
groupsPath: '/api/:version/groups.json', groupsPath: '/api/:version/groups.json',
groupPath: '/api/:version/groups/:id', groupPath: '/api/:version/groups/:id',
subgroupsPath: '/api/:version/groups/:id/subgroups',
namespacesPath: '/api/:version/namespaces.json', namespacesPath: '/api/:version/namespaces.json',
groupProjectsPath: '/api/:version/groups/:id/projects.json', groupProjectsPath: '/api/:version/groups/:id/projects.json',
projectsPath: '/api/:version/projects.json', projectsPath: '/api/:version/projects.json',
......
...@@ -10,13 +10,18 @@ export default function groupsSelect() { ...@@ -10,13 +10,18 @@ export default function groupsSelect() {
const $select = $(this); const $select = $(this);
const allAvailable = $select.data('allAvailable'); const allAvailable = $select.data('allAvailable');
const skipGroups = $select.data('skipGroups') || []; const skipGroups = $select.data('skipGroups') || [];
const parentGroupID = $select.data('parentId');
const groupsPath = parentGroupID
? Api.subgroupsPath.replace(':id', parentGroupID)
: Api.groupsPath;
$select.select2({ $select.select2({
placeholder: 'Search for a group', placeholder: 'Search for a group',
allowClear: $select.hasClass('allowClear'), allowClear: $select.hasClass('allowClear'),
multiple: $select.hasClass('multiselect'), multiple: $select.hasClass('multiselect'),
minimumInputLength: 0, minimumInputLength: 0,
ajax: { ajax: {
url: Api.buildUrl(Api.groupsPath), url: Api.buildUrl(groupsPath),
dataType: 'json', dataType: 'json',
quietMillis: 250, quietMillis: 250,
transport(params) { transport(params) {
......
...@@ -5,6 +5,7 @@ import initSettingsPanels from '~/settings_panels'; ...@@ -5,6 +5,7 @@ import initSettingsPanels from '~/settings_panels';
import dirtySubmitFactory from '~/dirty_submit/dirty_submit_factory'; import dirtySubmitFactory from '~/dirty_submit/dirty_submit_factory';
import mountBadgeSettings from '~/pages/shared/mount_badge_settings'; import mountBadgeSettings from '~/pages/shared/mount_badge_settings';
import { GROUP_BADGE } from '~/badges/constants'; import { GROUP_BADGE } from '~/badges/constants';
import groupsSelect from '~/groups_select';
import projectSelect from '~/project_select'; import projectSelect from '~/project_select';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
...@@ -17,5 +18,8 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -17,5 +18,8 @@ document.addEventListener('DOMContentLoaded', () => {
); );
mountBadgeSettings(GROUP_BADGE); mountBadgeSettings(GROUP_BADGE);
// Initialize Subgroups selector
groupsSelect();
projectSelect(); projectSelect();
}); });
...@@ -33,7 +33,11 @@ ...@@ -33,7 +33,11 @@
.bs-callout-warning { .bs-callout-warning {
background-color: $orange-100; background-color: $orange-100;
border-color: $orange-200; border-color: $orange-200;
color: $orange-700; color: $orange-900;
a {
color: $orange-900;
}
} }
.bs-callout-info { .bs-callout-info {
......
...@@ -9,7 +9,7 @@ module Projects ...@@ -9,7 +9,7 @@ module Projects
end end
def execute def execute
if @params[:template_name]&.present? if @params[:template_name].present?
return ::Projects::CreateFromTemplateService.new(current_user, params).execute return ::Projects::CreateFromTemplateService.new(current_user, params).execute
end end
......
...@@ -37,6 +37,7 @@ ...@@ -37,6 +37,7 @@
.settings-content .settings-content
= render 'shared/badges/badge_settings' = render 'shared/badges/badge_settings'
= render_if_exists 'groups/custom_project_templates_setting'
= render_if_exists 'groups/templates_setting', expanded: expanded = render_if_exists 'groups/templates_setting', expanded: expanded
%section.settings.gs-advanced.no-animate#js-advanced-settings{ class: ('expanded' if expanded) } %section.settings.gs-advanced.no-animate#js-advanced-settings{ class: ('expanded' if expanded) }
......
...@@ -50,7 +50,7 @@ ...@@ -50,7 +50,7 @@
.project-template .project-template
.form-group .form-group
%div %div
= render 'project_templates', f: f = render 'project_templates', f: f, project: @project
.tab-pane.import-project-pane.js-toggle-container{ id: 'import-project-pane', class: active_when(active_tab == 'import'), role: 'tabpanel' } .tab-pane.import-project-pane.js-toggle-container{ id: 'import-project-pane', class: active_when(active_tab == 'import'), role: 'tabpanel' }
- if import_sources_enabled? - if import_sources_enabled?
......
...@@ -9,9 +9,9 @@ ...@@ -9,9 +9,9 @@
.text-muted .text-muted
= template.description = template.description
.controls.d-flex.align-items-center .controls.d-flex.align-items-center
%label.btn.btn-success.template-button.choose-template.append-right-10.append-bottom-0{ for: template.name } %a.btn.btn-default.append-right-10{ href: template.preview, rel: 'noopener noreferrer', target: '_blank', data: { track_label: "create_from_template", track_property: "template_preview", track_event: "click_button", track_value: template.name } }
= _("Preview")
%label.btn.btn-success.template-button.choose-template.append-bottom-0{ for: template.name }
%input{ type: "radio", autocomplete: "off", name: "project[template_name]", id: template.name, value: template.name, data: { track_label: "create_from_template", track_property: "template_use", track_event: "click_button" } } %input{ type: "radio", autocomplete: "off", name: "project[template_name]", id: template.name, value: template.name, data: { track_label: "create_from_template", track_property: "template_use", track_event: "click_button" } }
%span %span
= _("Use template") = _("Use template")
%a.btn.btn-default{ href: template.preview, rel: 'noopener noreferrer', target: '_blank', data: { track_label: "create_from_template", track_property: "template_preview", track_event: "click_button", track_value: template.name } }
= _("Preview")
# Custom instance-level project templates **[PREMIUM ONLY]**
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/6860) in [GitLab Premium](https://about.gitlab.com/pricing) 11.2.
When you create a new project, creating it based on custom project templates is
a convenient option to bootstrap from an existing project boilerplate.
The administration setting to configure a GitLab group that serves as template
source can be found under **Admin > Settings > Custom project templates**.
Within this section, you can configure the group where all the custom project
templates are sourced. Every project directly under the group namespace will be
available to the user if they have access to them. For example, every public
project in the group will be available to every logged user. However,
private projects will be available only if the user has view [permissions](../permissions.md)
in the project:
- Project Owner, Maintainer, Developer, Reporter or Guest
- Is a member of the Group: Owner, Maintainer, Developer, Reporter or Guest
Projects below subgroups of the template group are **not** supported.
Repository and database information that are copied over to each new project are
identical to the data exported with [GitLab's Project Import/Export](../project/settings/import_export.md).
If you would like to set project templates at a group level, please see [Custom group-level project templates](../group/custom_project_templates.md).
\ No newline at end of file
# Custom group-level project templates **[PREMIUM ONLY]**
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/6861) in [GitLab Premium](https://about.gitlab.com/pricing) 11.6.
When you create a new project, creating it based on custom project templates is
a convenient option to bootstrap from an existing project boilerplate.
The group-level setting to configure a GitLab group that serves as template
source can be found under **Group > Settings > General > Custom project templates**.
Within this section, you can configure the group where all the custom project
templates are sourced. Every project directly under the group namespace will be
available to the user if they have access to them. For example, every public
project in the group will be available to every logged in user. However,
private projects will be available only if the user has view [permissions](../permissions.md)
in the project. That is, users with Owner, Maintainer, Developer, Reporter or Guest roles for projects,
or for groups to which the project belongs.
Projects of nested subgroups of a selected template source cannot be used.
Repository and database information that are copied over to each new project are
identical to the data exported with [GitLab's Project Import/Export](../project/settings/import_export.md).
If you would like to set project templates at an instance level, please see [Custom instance-level project templates](../admin_area/custom_project_templates.md).
\ No newline at end of file
...@@ -259,6 +259,11 @@ types with every project in a group. ...@@ -259,6 +259,11 @@ types with every project in a group.
Learn more about [Group-level file templates](https://docs.gitlab.com/ee/user/group/index.html#group-level-file-templates-premium). Learn more about [Group-level file templates](https://docs.gitlab.com/ee/user/group/index.html#group-level-file-templates-premium).
#### Group-level project templates **[PREMIUM]**
Define project templates at a group-level by setting a group as a template source.
[Learn more about group-level project templates](custom_project_templates.md).
### Advanced settings ### Advanced settings
- **Projects**: view all projects within that group, add members to each project, - **Projects**: view all projects within that group, add members to each project,
......
...@@ -3,6 +3,7 @@ require Rails.root.join('db', 'migrate', '20171216111734_clean_up_for_members.rb ...@@ -3,6 +3,7 @@ require Rails.root.join('db', 'migrate', '20171216111734_clean_up_for_members.rb
describe CleanUpForMembers, :migration do describe CleanUpForMembers, :migration do
let(:migration) { described_class.new } let(:migration) { described_class.new }
let(:groups) { table(:namespaces) }
let!(:group_member) { create_group_member } let!(:group_member) { create_group_member }
let!(:unbinded_group_member) { create_group_member } let!(:unbinded_group_member) { create_group_member }
let!(:invited_group_member) { create_group_member(true) } let!(:invited_group_member) { create_group_member(true) }
...@@ -25,7 +26,7 @@ describe CleanUpForMembers, :migration do ...@@ -25,7 +26,7 @@ describe CleanUpForMembers, :migration do
end end
def create_group_member(invited = false) def create_group_member(invited = false)
fill_member(GroupMember.new(group: create_group), invited) fill_member(GroupMember.new(source_id: create_group.id, source_type: 'Namespace'), invited)
end end
def create_project_member(invited = false) def create_project_member(invited = false)
...@@ -54,7 +55,7 @@ describe CleanUpForMembers, :migration do ...@@ -54,7 +55,7 @@ describe CleanUpForMembers, :migration do
def create_group def create_group
name = FFaker::Lorem.characters(10) name = FFaker::Lorem.characters(10)
Group.create(name: name, path: name.downcase.gsub(/\s/, '_')) groups.create!(type: 'Group', name: name, path: name.downcase.gsub(/\s/, '_'))
end end
def create_project def create_project
......
...@@ -94,17 +94,18 @@ describe DeleteInconsistentInternalIdRecords, :migration do ...@@ -94,17 +94,18 @@ describe DeleteInconsistentInternalIdRecords, :migration do
end end
context 'for milestones (by group)' do context 'for milestones (by group)' do
# milestones (by group) is a little different than all of the other models # milestones (by group) is a little different than most of the other models
let!(:group1) { create(:group) } let(:groups) { table(:namespaces) }
let!(:group2) { create(:group) } let(:group1) { groups.create(name: 'Group 1', type: 'Group', path: 'group_1') }
let!(:group3) { create(:group) } let(:group2) { groups.create(name: 'Group 2', type: 'Group', path: 'group_2') }
let(:group3) { groups.create(name: 'Group 2', type: 'Group', path: 'group_3') }
let(:internal_id_query) { ->(group) { InternalId.where(usage: InternalId.usages['milestones'], namespace: group) } } let(:internal_id_query) { ->(group) { InternalId.where(usage: InternalId.usages['milestones'], namespace: group) } }
before do before do
3.times { create(:milestone, group: group1) } 3.times { create(:milestone, group_id: group1.id) }
3.times { create(:milestone, group: group2) } 3.times { create(:milestone, group_id: group2.id) }
3.times { create(:milestone, group: group3) } 3.times { create(:milestone, group_id: group3.id) }
internal_id_query.call(group1).first.tap do |iid| internal_id_query.call(group1).first.tap do |iid|
iid.last_value = iid.last_value - 2 iid.last_value = iid.last_value - 2
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment