Commit 933eaca9 authored by Lukas Eipert's avatar Lukas Eipert Committed by Dmitriy Zaporozhets

Add Frontend for Instance-level project templates

parent 86db303f
...@@ -12,6 +12,7 @@ export default function groupsSelect() { ...@@ -12,6 +12,7 @@ export default function groupsSelect() {
const skipGroups = $select.data('skipGroups') || []; const skipGroups = $select.data('skipGroups') || [];
$select.select2({ $select.select2({
placeholder: 'Search for a group', placeholder: 'Search for a group',
allowClear: $select.hasClass('allowClear'),
multiple: $select.hasClass('multiselect'), multiple: $select.hasClass('multiselect'),
minimumInputLength: 0, minimumInputLength: 0,
ajax: { ajax: {
......
import initProjectLoadingSpinner from '../shared/save_project_loader';
import initProjectVisibilitySelector from '../../../project_visibility'; import initProjectVisibilitySelector from '../../../project_visibility';
import initProjectNew from '../../../projects/project_new'; import initProjectNew from '../../../projects/project_new';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
initProjectLoadingSpinner();
initProjectVisibilitySelector(); initProjectVisibilitySelector();
initProjectNew.bindEvents(); initProjectNew.bindEvents();
}); });
...@@ -37,9 +37,10 @@ const bindEvents = () => { ...@@ -37,9 +37,10 @@ const bindEvents = () => {
const $projectFieldsForm = $('.project-fields-form'); const $projectFieldsForm = $('.project-fields-form');
const $selectedTemplateText = $('.selected-template'); const $selectedTemplateText = $('.selected-template');
const $changeTemplateBtn = $('.change-template'); const $changeTemplateBtn = $('.change-template');
const $selectedIcon = $('.selected-icon svg'); const $selectedIcon = $('.selected-icon');
const $templateProjectNameInput = $('#template-project-name #project_path'); const $templateProjectNameInput = $('#template-project-name #project_path');
const $pushNewProjectTipTrigger = $('.push-new-project-tip'); const $pushNewProjectTipTrigger = $('.push-new-project-tip');
const $projectTemplateButtons = $('.project-templates-buttons');
if ($newProjectForm.length !== 1) { if ($newProjectForm.length !== 1) {
return; return;
...@@ -88,35 +89,35 @@ const bindEvents = () => { ...@@ -88,35 +89,35 @@ const bindEvents = () => {
} }
function chooseTemplate() { function chooseTemplate() {
$('.template-option').hide(); $projectTemplateButtons.addClass('hidden');
$projectFieldsForm.addClass('selected'); $projectFieldsForm.addClass('selected');
$selectedIcon.removeClass('d-block'); $selectedIcon.empty();
const value = $(this).val(); const value = $(this).val();
const templates = { const templates = {
rails: { rails: {
text: 'Ruby on Rails', text: 'Ruby on Rails',
icon: '.selected-icon .icon-rails', icon: '.template-option svg.icon-rails',
}, },
express: { express: {
text: 'NodeJS Express', text: 'NodeJS Express',
icon: '.selected-icon .icon-node-express', icon: '.template-option svg.icon-node-express',
}, },
spring: { spring: {
text: 'Spring', text: 'Spring',
icon: '.selected-icon .icon-java-spring', icon: '.template-option svg.icon-java-spring',
}, },
}; };
const selectedTemplate = templates[value]; const selectedTemplate = templates[value];
$selectedTemplateText.text(selectedTemplate.text); $selectedTemplateText.text(selectedTemplate.text);
$(selectedTemplate.icon).addClass('d-block'); $(selectedTemplate.icon).clone().addClass('d-block').appendTo($selectedIcon);
$templateProjectNameInput.focus(); $templateProjectNameInput.focus();
} }
$useTemplateBtn.on('change', chooseTemplate); $useTemplateBtn.on('change', chooseTemplate);
$changeTemplateBtn.on('click', () => { $changeTemplateBtn.on('click', () => {
$('.template-option').show(); $projectTemplateButtons.removeClass('hidden');
$projectFieldsForm.removeClass('selected'); $projectFieldsForm.removeClass('selected');
$useTemplateBtn.prop('checked', false); $useTemplateBtn.prop('checked', false);
}); });
......
...@@ -486,34 +486,36 @@ ...@@ -486,34 +486,36 @@
} }
.project-template { .project-template {
> .form-group { > .form-group {
margin-bottom: 0; margin-bottom: 0;
} }
.template-option { .tab-pane {
padding: $gl-padding $gl-padding $gl-padding ($gl-padding * 4); padding-top: 0;
position: relative; padding-bottom: 0;
&:not(:first-child) {
border-top: 1px solid $border-color;
} }
.template-option {
.logo {
.btn-template-icon { .btn-template-icon {
position: absolute; width: 40px !important;
left: $gl-padding;
top: $gl-padding;
} }
} }
.template-title { padding: 16px 0;
font-size: 16px;
&:not(:first-child) {
border-top: 1px solid $border-color;
}
.controls {
margin-left: auto;
} }
.template-description {
margin: 6px 0 12px;
} }
.template-button { .choose-template {
input { input {
position: absolute; position: absolute;
clip: rect(0, 0, 0, 0); clip: rect(0, 0, 0, 0);
...@@ -540,8 +542,6 @@ ...@@ -540,8 +542,6 @@
} }
.selected-icon { .selected-icon {
padding-right: $gl-padding;
svg { svg {
display: none; display: none;
top: 7px; top: 7px;
......
...@@ -385,4 +385,6 @@ ...@@ -385,4 +385,6 @@
.settings-content .settings-content
= render 'third_party_offers', application_setting: @application_setting = render 'third_party_offers', application_setting: @application_setting
= render_if_exists 'admin/application_settings/custom_templates_form', expanded: expanded
= render_if_exists 'admin/application_settings/pseudonymizer_settings', expanded: expanded = render_if_exists 'admin/application_settings/pseudonymizer_settings', expanded: expanded
.project-templates-buttons.import-buttons - f ||= local_assigns[:f]
- Gitlab::ProjectTemplate.all.each do |template|
.template-option
= custom_icon(template.logo)
.template-title= template.title
.template-description= template.description
%label.btn.btn-success.template-button.choose-template.append-right-10{ for: template.name }
%input{ type: "radio", autocomplete: "off", name: "project[template_name]", id: template.name, value: template.name }
%span Use template
%a.btn.btn-default{ href: template.preview, rel: 'noopener noreferrer', target: '_blank' } Preview
.project-fields-form .project-templates-buttons.import-buttons.col-sm-12
.row = render 'projects/project_templates/built_in_templates'
.form-group.col-sm-12
%label.label-bold
Template
.input-group.template-input-group
.input-group-prepend
.input-group-text
.selected-icon
- Gitlab::ProjectTemplate.all.each do |template|
= custom_icon(template.logo)
.selected-template
.input-group-append
%button.btn.btn-default.change-template{ type: "button" } Change template
= render 'new_project_fields', f: f, project_name_id: "template-project-name" .project-fields-form
= render 'projects/project_templates/project_fields_form'
= render 'projects/new_project_fields', f: f, project_name_id: "template-project-name"
- Gitlab::ProjectTemplate.all.each do |template|
.template-option.d-flex.align-items-center
.logo.append-right-10
= custom_icon(template.logo, size: 40)
.description
%strong
= template.title
%br
.text-muted
= template.description
.controls.d-flex.align-items-center
%label.btn.btn-success.template-button.choose-template.append-right-10.append-bottom-0{ for: template.name }
%input{ type: "radio", autocomplete: "off", name: "project[template_name]", id: template.name, value: template.name }
%span
= _("Use template")
%a.btn.btn-default{ href: template.preview, rel: 'noopener noreferrer', target: '_blank' }
= _("Preview")
.row
.form-group.col-sm-12
%label.label-bold
= _('Template')
.input-group.template-input-group
.input-group-prepend
.input-group-text
.selected-icon.append-right-10
.selected-template
.input-group-append
%button.btn.btn-default.change-template{ type: "button" }
= _('Change template')
...@@ -65,7 +65,7 @@ scope(constraints: { username: Gitlab::PathRegex.root_namespace_route_regex }) d ...@@ -65,7 +65,7 @@ scope(constraints: { username: Gitlab::PathRegex.root_namespace_route_regex }) d
get '/', to: redirect('%{username}'), as: nil get '/', to: redirect('%{username}'), as: nil
## EE-specific ## EE-specific
get :available_templates, format: :json get :available_templates, format: :js
## EE-specific ## EE-specific
end end
......
...@@ -36,6 +36,16 @@ ...@@ -36,6 +36,16 @@
1. Click **Create project**. 1. Click **Create project**.
## Custom project templates
> **Notes:**
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/6860) in [GitLab Edition Premium][ee] 11.2
When you create a new project, creating it based on custom project templates is a convenient option to bootstrap an existing project boilerplate.
If a custom group is defined on the instance, projects within this group are available as project templates within the "Create from template" section. These templates are shown in the "Custom" tab. In addition, you can click on "Preview" to explore what this project templates includes.
See the [admin area documentation](../user/admin_area/custom_project_templates.md) on how to configure custom project templates.
## Push to create a new project ## Push to create a new project
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/26388) in GitLab 10.5. > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/26388) in GitLab 10.5.
......
import '~/pages/admin/application_settings/index';
import groupsSelect from '~/groups_select';
document.addEventListener('DOMContentLoaded', () => {
groupsSelect();
});
import '~/pages/projects/new/index';
import initCustomProjectTemplates from 'ee/projects/custom_project_templates';
document.addEventListener('DOMContentLoaded', initCustomProjectTemplates);
import $ from 'jquery';
const bindEvents = () => {
const $newProjectForm = $('#new_project');
const $useCustomTemplateBtn = $('.custom-template-button > input');
const $projectFieldsForm = $('.project-fields-form');
const $selectedIcon = $('.selected-icon');
const $selectedTemplateText = $('.selected-template');
const $templateProjectNameInput = $('#template-project-name #project_path');
const $changeTemplateBtn = $('.change-template');
const $projectTemplateButtons = $('.project-templates-buttons');
const $projectFieldsFormInput = $('.project-fields-form input#project_use_custom_template');
if ($newProjectForm.length !== 1 || $useCustomTemplateBtn.length === 0) {
return;
}
function enableCustomTemplate() {
$projectFieldsFormInput.val(true);
}
function disableCustomTemplate() {
$projectFieldsFormInput.val(false);
}
function chooseTemplate() {
$projectTemplateButtons.addClass('hidden');
$projectFieldsForm.addClass('selected');
$selectedIcon.empty();
const value = $(this).val();
$selectedTemplateText.text(value);
$(this)
.parents('.template-option')
.find('.avatar')
.clone()
.addClass('d-block')
.removeClass('s40')
.appendTo($selectedIcon);
$templateProjectNameInput.focus();
enableCustomTemplate();
}
$useCustomTemplateBtn.on('change', chooseTemplate);
$changeTemplateBtn.on('click', () => {
$projectTemplateButtons.removeClass('hidden');
$useCustomTemplateBtn.prop('checked', false);
disableCustomTemplate();
});
};
export default () => {
const $navElement = $('.nav-link[href="#custom-templates"]');
const $tabContent = $('.project-templates-buttons#custom-templates');
$tabContent.on('ajax:success', bindEvents);
$navElement.one('click', () => {
$.get($tabContent.data('initialTemplates'));
});
bindEvents();
};
...@@ -157,5 +157,18 @@ ...@@ -157,5 +157,18 @@
.btn-blank { .btn-blank {
padding: 6px 10px; padding: 6px 10px;
} }
}
.project-template {
.template-input-group {
.selected-icon {
.avatar {
width: 20px;
height: 20px;
line-height: 20px;
font-size: $gl-font-size;
margin-right: 0;
}
}
}
} }
module EE module EE
module UsersController module UsersController
def available_templates def available_templates
render json: load_custom_project_templates load_custom_project_templates
end end
private private
......
- if ::Gitlab::CurrentSettings.custom_project_templates_enabled?
%section.settings.as-custom-project-templates.no-animate#js-mirror-settings{ class: ('expanded' if expanded) }
.settings-header
%h4
= _('Custom project templates')
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? _('Collapse') : _('Expand')
%p
= _('Select the custom project template source group.')
.settings-content
= form_for @application_setting, url: admin_application_settings_path do |f|
= form_errors(@application_setting)
%fieldset
.form-group
= groups_select_tag('application_setting[custom_project_templates_group_id]', selected: @application_setting.custom_project_templates_group_id, class: 'input-clamp allowClear', multiple: false)
= f.submit _('Save changes'), class: "btn btn-success"
- project_template_count = current_user.available_custom_project_templates.count
- if ::Gitlab::CurrentSettings.custom_project_templates_enabled? && project_template_count > 0
.project-templates-buttons.col-sm-12
%ul.nav-tabs.nav-links.nav.scrolling-tabs
%li.built-in-tab
%a.nav-link.active{ href: "#built-in", data: { toggle: 'tab'} }
= _('Built-In')
%span.badge.badge-pill= Gitlab::ProjectTemplate.all.count
%li.custom-templates-tab
%a.nav-link{ href: "#custom-templates", data: { toggle: 'tab'} }
= _('Custom')
%span.badge.badge-pill= project_template_count
.tab-content
.project-templates-buttons.import-buttons.tab-pane.active#built-in
= render 'projects/project_templates/built_in_templates'
.project-templates-buttons.import-buttons.tab-pane#custom-templates{ data: {initial_templates: user_available_templates_path(current_user)} }
= icon("spin spinner 2x")
.project-fields-form
= render 'projects/project_templates/project_fields_form'
= f.hidden_field(:use_custom_template, value: false)
= render 'projects/new_project_fields', f: f, project_name_id: "template-project-name"
- else
= render_ce 'projects/project_templates', f: f
.custom-project-templates
- custom_project_templates.each do |template|
.template-option.d-flex.align-items-center
.avatar-container.s40
= project_icon(template, alt: template.name, class: 'btn-template-icon avatar s40 avatar-tile', lazy: false)
.description
%strong
= template.title
%br
.text-muted
= template.description
.controls.d-flex.align-items-baseline
%label.btn.btn-success.custom-template-button.choose-template.append-right-10.append-bottom-0{ for: template.name }
%input{ type: "radio", autocomplete: "off", name: "project[template_name]", id: template.name, value: template.name }
%span
= _('Use template')
%a.btn.btn-default{ href: project_path(template), rel: 'noopener noreferrer', target: '_blank' } Preview
= paginate custom_project_templates, params: {controller: 'users', action: 'available_templates', username: current_user.username}, theme: 'gitlab', remote: true
:plain
var target = $(".project-templates-buttons#custom-templates");
target.empty();
target.html("#{escape_javascript(render 'custom_project_templates', custom_project_templates: @custom_project_templates)}");
target.trigger("ajax:success");
---
title: Add Frontend for Instance-level project templates
merge_request: 6740
author:
type: added
# frozen_string_literal: true
require 'spec_helper'
describe 'Project' do
describe 'Custom projects templates' do
let(:user) { create(:user) }
let(:group) { create(:group) }
let!(:projects) { create_list(:project, 3, :public, namespace: group) }
before do
stub_ee_application_setting(custom_project_templates_group_id: group.id)
end
describe 'when feature custom_project_templates is enabled' do
before do
stub_licensed_features(custom_project_templates: true)
allow(Project).to receive(:default_per_page).and_return(2)
sign_in user
visit new_project_path
end
it 'shows built-in templates tab' do
page.within '.project-template .built-in-tab' do
expect(page).to have_content 'Built-In'
end
end
it 'shows custom projects templates tab' do
page.within '.project-template .custom-templates-tab' do
expect(page).to have_content 'Custom'
end
end
it 'displays the number of projects templates available to the user' do
page.within '.project-template .custom-templates-tab span.badge' do
expect(page).to have_content '3'
end
end
it 'allows creation from custom project template', :js do
new_name = 'example_custom_project_template'
find('#create-from-template-tab').click
find('.project-template .custom-templates-tab').click
find("label[for='#{projects.first.name}']").click
page.within '.project-fields-form' do
fill_in("project_path", with: new_name)
Sidekiq::Testing.inline! do
click_button "Create project"
end
end
expect(page).to have_content new_name
expect(Project.last.name).to eq new_name
end
it 'has a working pagination', :js do
last_project = "label[for='#{projects.last.name}']"
find('#create-from-template-tab').click
find('.project-template .custom-templates-tab').click
expect(page).to have_css('.custom-project-templates .gl-pagination')
expect(page).not_to have_css(last_project)
find('.js-next-button a').click
expect(page).to have_css(last_project)
end
end
describe 'when feature custom_project_templates is disabled' do
it 'does not show custom project templates tab' do
expect(page).not_to have_css('.project-template .nav-tabs')
end
end
end
end
...@@ -1157,6 +1157,9 @@ msgstr "" ...@@ -1157,6 +1157,9 @@ msgstr ""
msgid "Browse files" msgid "Browse files"
msgstr "" msgstr ""
msgid "Built-In"
msgstr ""
msgid "Business metrics (Custom)" msgid "Business metrics (Custom)"
msgstr "" msgstr ""
...@@ -1256,6 +1259,9 @@ msgstr "" ...@@ -1256,6 +1259,9 @@ msgstr ""
msgid "Change Weight" msgid "Change Weight"
msgstr "" msgstr ""
msgid "Change template"
msgstr ""
msgid "Change this value to influence how frequently the GitLab UI polls for updates." msgid "Change this value to influence how frequently the GitLab UI polls for updates."
msgstr "" msgstr ""
...@@ -2232,6 +2238,9 @@ msgstr "" ...@@ -2232,6 +2238,9 @@ msgstr ""
msgid "CurrentUser|Settings" msgid "CurrentUser|Settings"
msgstr "" msgstr ""
msgid "Custom"
msgstr ""
msgid "Custom CI config path" msgid "Custom CI config path"
msgstr "" msgstr ""
...@@ -2241,6 +2250,9 @@ msgstr "" ...@@ -2241,6 +2250,9 @@ msgstr ""
msgid "Custom notification levels are the same as participating levels. With custom notification levels you will also receive notifications for select events. To find out more, check out %{notification_link}." msgid "Custom notification levels are the same as participating levels. With custom notification levels you will also receive notifications for select events. To find out more, check out %{notification_link}."
msgstr "" msgstr ""
msgid "Custom project templates"
msgstr ""
msgid "Customize colors" msgid "Customize colors"
msgstr "" msgstr ""
...@@ -4923,6 +4935,9 @@ msgstr "" ...@@ -4923,6 +4935,9 @@ msgstr ""
msgid "Preferences|Navigation theme" msgid "Preferences|Navigation theme"
msgstr "" msgstr ""
msgid "Preview"
msgstr ""
msgid "Primary" msgid "Primary"
msgstr "" msgstr ""
...@@ -5708,6 +5723,9 @@ msgstr "" ...@@ -5708,6 +5723,9 @@ msgstr ""
msgid "Select target branch" msgid "Select target branch"
msgstr "" msgstr ""
msgid "Select the custom project template source group."
msgstr ""
msgid "Selecting a GitLab user will add a link to the GitLab user in the descriptions of issues and comments (e.g. \"By <a href=\"#\">@johnsmith</a>\"). It will also associate and/or assign these issues and comments with the selected user." msgid "Selecting a GitLab user will add a link to the GitLab user in the descriptions of issues and comments (e.g. \"By <a href=\"#\">@johnsmith</a>\"). It will also associate and/or assign these issues and comments with the selected user."
msgstr "" msgstr ""
...@@ -6222,6 +6240,9 @@ msgstr "" ...@@ -6222,6 +6240,9 @@ msgstr ""
msgid "Team" msgid "Team"
msgstr "" msgstr ""
msgid "Template"
msgstr ""
msgid "Terms of Service Agreement and Privacy Policy" msgid "Terms of Service Agreement and Privacy Policy"
msgstr "" msgstr ""
...@@ -6892,6 +6913,9 @@ msgstr "" ...@@ -6892,6 +6913,9 @@ msgstr ""
msgid "Use one line per URI" msgid "Use one line per URI"
msgstr "" msgstr ""
msgid "Use template"
msgstr ""
msgid "Use the following registration token during setup:" msgid "Use the following registration token during setup:"
msgstr "" msgstr ""
......
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