Commit b7f76383 authored by Luke Bennett's avatar Luke Bennett

Improve project settings page

Prioritize and simplify project settings content.
parent dc884879
import _ from 'underscore'; import _ from 'underscore';
import $ from 'jquery';
class DirtySubmitForm { class DirtySubmitForm {
constructor(form) { constructor(form) {
...@@ -26,6 +27,7 @@ class DirtySubmitForm { ...@@ -26,6 +27,7 @@ class DirtySubmitForm {
); );
this.form.addEventListener('input', throttledUpdateDirtyInput); this.form.addEventListener('input', throttledUpdateDirtyInput);
this.form.addEventListener('change', throttledUpdateDirtyInput); this.form.addEventListener('change', throttledUpdateDirtyInput);
$(this.form).on('change.select2', throttledUpdateDirtyInput);
this.form.addEventListener('submit', event => this.formSubmit(event)); this.form.addEventListener('submit', event => this.formSubmit(event));
} }
......
...@@ -3,17 +3,24 @@ import initSettingsPanels from '~/settings_panels'; ...@@ -3,17 +3,24 @@ import initSettingsPanels from '~/settings_panels';
import setupProjectEdit from '~/project_edit'; import setupProjectEdit from '~/project_edit';
import initConfirmDangerModal from '~/confirm_danger_modal'; import initConfirmDangerModal from '~/confirm_danger_modal';
import mountBadgeSettings from '~/pages/shared/mount_badge_settings'; import mountBadgeSettings from '~/pages/shared/mount_badge_settings';
import dirtySubmitFactory from '~/dirty_submit/dirty_submit_factory';
import initAvatarPicker from '~/avatar_picker'; import initAvatarPicker from '~/avatar_picker';
import initProjectLoadingSpinner from '../shared/save_project_loader'; import initProjectLoadingSpinner from '../shared/save_project_loader';
import initProjectPermissionsSettings from '../shared/permissions'; import initProjectPermissionsSettings from '../shared/permissions';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
initProjectLoadingSpinner();
setupProjectEdit();
// Initialize expandable settings panels
initSettingsPanels();
initAvatarPicker(); initAvatarPicker();
initProjectPermissionsSettings();
initConfirmDangerModal(); initConfirmDangerModal();
initSettingsPanels();
mountBadgeSettings(PROJECT_BADGE); mountBadgeSettings(PROJECT_BADGE);
initProjectLoadingSpinner();
initProjectPermissionsSettings();
setupProjectEdit();
dirtySubmitFactory(
document.querySelectorAll(
'.js-general-settings-form, .js-mr-settings-form, .js-add-approver-form',
),
);
}); });
...@@ -157,6 +157,10 @@ label { ...@@ -157,6 +157,10 @@ label {
padding-left: 10px; padding-left: 10px;
padding-right: 10px; padding-right: 10px;
appearance: none; appearance: none;
/* stylelint-disable property-no-vendor-prefix */
-webkit-appearance: none;
-moz-appearance: none;
/* stylelint-enable property-no-vendor-prefix */
&::-ms-expand { &::-ms-expand {
display: none; display: none;
......
...@@ -39,7 +39,7 @@ ...@@ -39,7 +39,7 @@
.settings-header { .settings-header {
position: relative; position: relative;
padding: 20px 110px 10px 0; padding: 20px 110px 0 0;
h4 { h4 {
margin-top: 0; margin-top: 0;
......
- if ::Gitlab::ExternalAuthorization.enabled? - if ::Gitlab::ExternalAuthorization.enabled?
.form-group .form-group.col-md-9
= f.label :external_authorization_classification_label, class: 'label-bold' do = f.label :external_authorization_classification_label, _('Classification Label (optional)'), class: 'label-bold'
= s_('ExternalAuthorizationService|Classification Label')
%span.light (optional)
= f.text_field :external_authorization_classification_label, class: "form-control" = f.text_field :external_authorization_classification_label, class: "form-control"
%span.form-text.text-muted %span.form-text.text-muted
= external_classification_label_help_message = external_classification_label_help_message
- return unless Gitlab::CurrentSettings.project_export_enabled? - return unless Gitlab::CurrentSettings.project_export_enabled?
- project = local_assigns.fetch(:project) - project = local_assigns.fetch(:project)
- expanded = Rails.env.test?
%section.settings.no-animate#js-export-project{ class: ('expanded' if expanded) } .sub-section
.settings-header %h4= _('Export project')
%h4 %p= _('Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the "New Project" page.')
Export project
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the "New Project" page.
.settings-content
.bs-callout.bs-callout-info .bs-callout.bs-callout-info
%p.append-bottom-0 %p.append-bottom-0
%p %p= _('The following items will be exported:')
The following items will be exported:
%ul %ul
%li Project and wiki repositories %li= _('Project and wiki repositories')
%li Project uploads %li= _('Project uploads')
%li Project configuration, including services %li= _('Project configuration, including services')
%li Issues with comments, merge requests with diffs and comments, labels, milestones, snippets, and other project entities %li= _('Issues with comments, merge requests with diffs and comments, labels, milestones, snippets, and other project entities')
%li LFS objects %li= _('LFS objects')
%p %p= _('The following items will NOT be exported:')
The following items will NOT be exported:
%ul %ul
%li Job traces and artifacts %li= _('Job traces and artifacts')
%li Container registry images %li= _('Container registry images')
%li CI variables %li= _('CI variables')
%li Webhooks %li= _('Webhooks')
%li Any encrypted tokens %li= _('Any encrypted tokens')
%p %p= _('Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page.')
Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page.
- if project.export_status == :finished - if project.export_status == :finished
= link_to 'Download export', download_export_project_path(project), = link_to _('Download export'), download_export_project_path(project),
rel: 'nofollow', download: '', method: :get, class: "btn btn-default" rel: 'nofollow', download: '', method: :get, class: "btn btn-default"
= link_to 'Generate new export', generate_new_export_project_path(project), = link_to _('Generate new export'), generate_new_export_project_path(project),
method: :post, class: "btn btn-default" method: :post, class: "btn btn-default"
- else - else
= link_to 'Export project', export_project_path(project), = link_to _('Export project'), export_project_path(project),
method: :post, class: "btn btn-default" method: :post, class: "btn btn-default"
This diff is collapsed.
- if @project.feature_available?(:issuable_default_templates)
- expanded = Rails.env.test?
%section.settings.issues-feature.no-animate#js-issue-settings{ class: [('expanded' if expanded), ('hidden' if @project.project_feature.send(:issues_access_level) == 0)] }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Default issue template')
%button.btn.js-settings-toggle= expanded ? _('Collapse') : _('Expand')
%p= _('Set a default template for issue descriptions.')
.settings-content
= form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "issue-settings-form" }, authenticity_token: true do |f|
%input{ type: 'hidden', name: 'update_section', value: 'js-issue-settings' }
.row
.form-group.col-md-9
= f.label :issues_template, class: 'label-bold' do
= _('Default description template for issues')
= link_to icon('question-circle'), help_page_path('user/project/description_templates', anchor: 'setting-a-default-template-for-issues-and-merge-requests'), target: '_blank'
= f.text_area :issues_template, class: "form-control", rows: 3
.text-secondary
- link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/markdown') }
= _('Description parsed with %{link_start}GitLab Flavored Markdown%{link_end}').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
= f.submit _('Save changes'), class: "btn btn-success"
= form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "edit-project js-general-settings-form" }, authenticity_token: true do |f|
%input{ name: 'update_section', type: 'hidden', value: 'js-general-settings' }
= form_errors(@project)
%fieldset
.row
.form-group.col-md-5
= f.label :name, class: 'label-bold', for: 'project_name_edit' do
= _('Project name')
= f.text_field :name, class: "form-control", id: "project_name_edit"
.form-group.col-md-7
= f.label :id, class: 'label-bold' do
= _('Project ID')
= f.text_field :id, class: 'form-control w-auto', readonly: true
.row
.form-group.col-md-9
= f.label :tag_list, _('Topics'), class: 'label-bold'
= f.text_field :tag_list, value: @project.tag_list.join(', '), maxlength: 2000, class: "form-control"
%p.form-text.text-muted= _('Separate topics with commas.')
.row
.form-group.col-md-9
= f.label :description, _('Project description (optional)'), class: 'label-bold'
= f.text_area :description, class: 'form-control', rows: 3, maxlength: 250
.row= render_if_exists 'projects/classification_policy_settings', f: f
.row= render_if_exists 'shared/repository_size_limit_setting', form: f, type: :project
.form-group.prepend-top-default.append-bottom-20
.avatar-container.s90
= project_icon(@project, alt: _('Project avatar'), class: 'avatar project-avatar s90')
= f.label :avatar, _('Project avatar'), class: 'label-bold d-block'
= render 'shared/choose_avatar_button', f: f
- if @project.avatar?
%hr
= link_to _('Remove avatar'), project_avatar_path(@project), data: { confirm: _('Avatar will be removed. Are you sure?')}, method: :delete, class: 'btn btn-link'
= f.submit _('Save changes'), class: "btn btn-success mt-4 qa-save-naming-topics-avatar-button"
...@@ -2,8 +2,7 @@ ...@@ -2,8 +2,7 @@
.modal-dialog .modal-dialog
.modal-content .modal-content
.modal-header .modal-header
%h3.page-title %h3.page-title= _('Confirmation required')
Confirmation required
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') } %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
%span{ "aria-hidden": true } &times; %span{ "aria-hidden": true } &times;
...@@ -11,8 +10,7 @@ ...@@ -11,8 +10,7 @@
%p.text-danger.js-confirm-text %p.text-danger.js-confirm-text
%p %p
This action can lead to data loss. %span.js-warning-text= _('This action can lead to data loss. To prevent accidental actions we ask you to confirm your intention.')
To prevent accidental actions we ask you to confirm your intention.
%br %br
Please type Please type
%code.js-confirm-danger-match= phrase %code.js-confirm-danger-match= phrase
...@@ -21,4 +19,4 @@ ...@@ -21,4 +19,4 @@
.form-group .form-group
= text_field_tag 'confirm_name_input', '', class: 'form-control js-confirm-danger-input' = text_field_tag 'confirm_name_input', '', class: 'form-control js-confirm-danger-input'
.form-actions .form-actions
= submit_tag 'Confirm', class: "btn btn-danger js-confirm-danger-submit" = submit_tag _('Confirm'), class: "btn btn-danger js-confirm-danger-submit"
This diff is collapsed.
...@@ -9,24 +9,33 @@ describe 'Projects > Settings > User renames a project' do ...@@ -9,24 +9,33 @@ describe 'Projects > Settings > User renames a project' do
visit edit_project_path(project) visit edit_project_path(project)
end end
def rename_project(project, name: nil, path: nil) def change_path(project, path)
fill_in('project_name', with: name) if name within('.advanced-settings') do
fill_in('Path', with: path) if path fill_in('Path', with: path)
click_button('Rename project') click_button('Change path')
end
project.reload
wait_for_edit_project_page_reload wait_for_edit_project_page_reload
end
def change_name(project, name)
within('.general-settings') do
fill_in('Project name', with: name)
click_button('Save changes')
end
project.reload project.reload
wait_for_edit_project_page_reload
end end
def wait_for_edit_project_page_reload def wait_for_edit_project_page_reload
expect(find('.project-edit-container')).to have_content('Rename repository') expect(find('.advanced-settings')).to have_content('Change path')
end end
context 'with invalid characters' do context 'with invalid characters' do
it 'shows errors for invalid project path/name' do it 'shows errors for invalid project path' do
rename_project(project, name: 'foo&bar', path: 'foo&bar') change_path(project, 'foo&bar')
expect(page).to have_field 'Project name', with: 'foo&bar'
expect(page).to have_field 'Path', with: 'foo&bar' expect(page).to have_field 'Path', with: 'foo&bar'
expect(page).to have_content "Name can contain only letters, digits, emojis, '_', '.', dash, space. It must start with letter, digit, emoji or '_'."
expect(page).to have_content "Path can contain only letters, digits, '_', '-' and '.'. Cannot start with '-', end in '.git' or end in '.atom'" expect(page).to have_content "Path can contain only letters, digits, '_', '-' and '.'. Cannot start with '-', end in '.git' or end in '.atom'"
end end
end end
...@@ -42,13 +51,13 @@ describe 'Projects > Settings > User renames a project' do ...@@ -42,13 +51,13 @@ describe 'Projects > Settings > User renames a project' do
context 'when changing project name' do context 'when changing project name' do
it 'renames the repository' do it 'renames the repository' do
rename_project(project, name: 'bar') change_name(project, 'bar')
expect(find('.breadcrumbs')).to have_content(project.name) expect(find('.breadcrumbs')).to have_content(project.name)
end end
context 'with emojis' do context 'with emojis' do
it 'shows error for invalid project name' do it 'shows error for invalid project name' do
rename_project(project, name: '🚀 foo bar ☁️') change_name(project, '🚀 foo bar ☁️')
expect(page).to have_field 'Project name', with: '🚀 foo bar ☁️' expect(page).to have_field 'Project name', with: '🚀 foo bar ☁️'
expect(page).not_to have_content "Name can contain only letters, digits, emojis '_', '.', dash and space. It must start with letter, digit, emoji or '_'." expect(page).not_to have_content "Name can contain only letters, digits, emojis '_', '.', dash and space. It must start with letter, digit, emoji or '_'."
end end
...@@ -67,7 +76,7 @@ describe 'Projects > Settings > User renames a project' do ...@@ -67,7 +76,7 @@ describe 'Projects > Settings > User renames a project' do
end end
it 'the project is accessible via the new path' do it 'the project is accessible via the new path' do
rename_project(project, path: 'bar') change_path(project, 'bar')
new_path = namespace_project_path(project.namespace, 'bar') new_path = namespace_project_path(project.namespace, 'bar')
visit new_path visit new_path
...@@ -77,7 +86,7 @@ describe 'Projects > Settings > User renames a project' do ...@@ -77,7 +86,7 @@ describe 'Projects > Settings > User renames a project' do
it 'the project is accessible via a redirect from the old path' do it 'the project is accessible via a redirect from the old path' do
old_path = project_path(project) old_path = project_path(project)
rename_project(project, path: 'bar') change_path(project, 'bar')
new_path = namespace_project_path(project.namespace, 'bar') new_path = namespace_project_path(project.namespace, 'bar')
visit old_path visit old_path
...@@ -88,7 +97,7 @@ describe 'Projects > Settings > User renames a project' do ...@@ -88,7 +97,7 @@ describe 'Projects > Settings > User renames a project' do
context 'and a new project is added with the same path' do context 'and a new project is added with the same path' do
it 'overrides the redirect' do it 'overrides the redirect' do
old_path = project_path(project) old_path = project_path(project)
rename_project(project, path: 'bar') change_path(project, 'bar')
new_project = create(:project, namespace: user.namespace, path: 'gitlabhq', name: 'quz') new_project = create(:project, namespace: user.namespace, path: 'gitlabhq', name: 'quz')
visit old_path visit old_path
......
...@@ -373,6 +373,21 @@ describe 'Project' do ...@@ -373,6 +373,21 @@ describe 'Project' do
end end
end end
describe 'edit' do
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
let(:path) { edit_project_path(project) }
before do
project.add_maintainer(user)
sign_in(user)
visit path
end
it_behaves_like 'dirty submit form', [{ form: '.js-general-settings-form', input: 'input[name="project[name]"]' },
{ form: '.qa-merge-request-settings', input: '#project_printing_merge_request_link_enabled' }]
end
def remove_with_confirm(button_text, confirm_with) def remove_with_confirm(button_text, confirm_with)
click_button button_text click_button button_text
fill_in 'confirm_name_input', with: confirm_with fill_in 'confirm_name_input', with: confirm_with
......
shared_examples 'dirty submit form' do |selector_args| shared_examples 'dirty submit form' do |selector_args|
selectors = selector_args.is_a?(Array) ? selector_args : [selector_args] selectors = selector_args.is_a?(Array) ? selector_args : [selector_args]
def expect_disabled_state(form, submit, is_disabled = true) def expect_disabled_state(form, submit_selector, is_disabled = true)
disabled_selector = is_disabled == true ? '[disabled]' : ':not([disabled])' disabled_selector = is_disabled == true ? '[disabled]' : ':not([disabled])'
form.find(".js-dirty-submit#{disabled_selector}", match: :first) form.find("#{submit_selector}#{disabled_selector}")
expect(submit.disabled?).to be is_disabled
end end
selectors.each do |selector| selectors.each do |selector|
it "disables #{selector[:form]} submit until there are changes on #{selector[:input]}", :js do it "disables #{selector[:form]} submit until there are changes on #{selector[:input]}", :js do
form = find(selector[:form]) form = find(selector[:form])
submit = form.first('.js-dirty-submit') submit_selector = selector[:submit] || 'input[type="submit"]'
submit = form.first(submit_selector)
input = form.first(selector[:input]) input = form.first(selector[:input])
is_radio = input[:type] == 'radio' is_radio = input[:type] == 'radio'
is_checkbox = input[:type] == 'checkbox' is_checkbox = input[:type] == 'checkbox'
...@@ -22,15 +21,14 @@ shared_examples 'dirty submit form' do |selector_args| ...@@ -22,15 +21,14 @@ shared_examples 'dirty submit form' do |selector_args|
original_checkable = input if is_checkbox original_checkable = input if is_checkbox
expect(submit.disabled?).to be true expect(submit.disabled?).to be true
expect(input.checked?).to be false
is_checkable ? input.click : input.set("#{original_value} changes") is_checkable ? input.click : input.set("#{original_value} changes")
expect_disabled_state(form, submit, false) expect_disabled_state(form, submit_selector, false)
is_checkable ? original_checkable.click : input.set(original_value) is_checkable ? original_checkable.click : input.set(original_value)
expect_disabled_state(form, submit) expect_disabled_state(form, submit_selector)
end end
end end
end end
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