diff options
author | Bryce Johnson <bryce@gitlab.com> | 2017-04-03 20:54:40 +0300 |
---|---|---|
committer | Alfredo Sumaran <alfredo@gitlab.com> | 2017-04-03 20:54:40 +0300 |
commit | f564cbb21ac31da88841ad93d6051d90677b7c12 (patch) | |
tree | 0d7812f9b2c74f0c6eac5c1e846f0885ec6f1295 | |
parent | ca6a7f1e9c9296317315249de9b8b3803d1c6ddc (diff) |
Make file templates easy to use and discover
31 files changed, 832 insertions, 217 deletions
diff --git a/app/assets/javascripts/blob/file_template_mediator.js b/app/assets/javascripts/blob/file_template_mediator.js new file mode 100644 index 00000000000..3062cd51ee3 --- /dev/null +++ b/app/assets/javascripts/blob/file_template_mediator.js @@ -0,0 +1,241 @@ +/* eslint-disable class-methods-use-this */ +/* global Flash */ + +import FileTemplateTypeSelector from './template_selectors/type_selector'; +import BlobCiYamlSelector from './template_selectors/ci_yaml_selector'; +import DockerfileSelector from './template_selectors/dockerfile_selector'; +import GitignoreSelector from './template_selectors/gitignore_selector'; +import LicenseSelector from './template_selectors/license_selector'; + +export default class FileTemplateMediator { + constructor({ editor, currentAction }) { + this.editor = editor; + this.currentAction = currentAction; + + this.initTemplateSelectors(); + this.initTemplateTypeSelector(); + this.initDomElements(); + this.initDropdowns(); + this.initPageEvents(); + } + + initTemplateSelectors() { + // Order dictates template type dropdown item order + this.templateSelectors = [ + GitignoreSelector, + BlobCiYamlSelector, + DockerfileSelector, + LicenseSelector, + ].map(TemplateSelectorClass => new TemplateSelectorClass({ mediator: this })); + } + + initTemplateTypeSelector() { + this.typeSelector = new FileTemplateTypeSelector({ + mediator: this, + dropdownData: this.templateSelectors + .map((templateSelector) => { + const cfg = templateSelector.config; + + return { + name: cfg.name, + key: cfg.key, + }; + }), + }); + } + + initDomElements() { + const $templatesMenu = $('.template-selectors-menu'); + const $undoMenu = $templatesMenu.find('.template-selectors-undo-menu'); + const $fileEditor = $('.file-editor'); + + this.$templatesMenu = $templatesMenu; + this.$undoMenu = $undoMenu; + this.$undoBtn = $undoMenu.find('button'); + this.$templateSelectors = $templatesMenu.find('.template-selector-dropdowns-wrap'); + this.$filenameInput = $fileEditor.find('.js-file-path-name-input'); + this.$fileContent = $fileEditor.find('#file-content'); + this.$commitForm = $fileEditor.find('form'); + this.$navLinks = $fileEditor.find('.nav-links'); + } + + initDropdowns() { + if (this.currentAction === 'create') { + this.typeSelector.show(); + } else { + this.hideTemplateSelectorMenu(); + } + + this.displayMatchedTemplateSelector(); + } + + initPageEvents() { + this.listenForFilenameInput(); + this.prepFileContentForSubmit(); + this.listenForPreviewMode(); + } + + listenForFilenameInput() { + this.$filenameInput.on('keyup blur', () => { + this.displayMatchedTemplateSelector(); + }); + } + + prepFileContentForSubmit() { + this.$commitForm.submit(() => { + this.$fileContent.val(this.editor.getValue()); + }); + } + + listenForPreviewMode() { + this.$navLinks.on('click', 'a', (e) => { + const urlPieces = e.target.href.split('#'); + const hash = urlPieces[1]; + if (hash === 'preview') { + this.hideTemplateSelectorMenu(); + } else if (hash === 'editor') { + this.showTemplateSelectorMenu(); + } + }); + } + + selectTemplateType(item, el, e) { + if (e) { + e.preventDefault(); + } + + this.templateSelectors.forEach((selector) => { + if (selector.config.key === item.key) { + selector.show(); + } else { + selector.hide(); + } + }); + + this.typeSelector.setToggleText(item.name); + + this.cacheToggleText(); + } + + selectTemplateFile(selector, query, data) { + selector.renderLoading(); + // in case undo menu is already already there + this.destroyUndoMenu(); + this.fetchFileTemplate(selector.config.endpoint, query, data) + .then((file) => { + this.showUndoMenu(); + this.setEditorContent(file); + this.setFilename(selector.config.name); + selector.renderLoaded(); + }) + .catch(err => new Flash(`An error occurred while fetching the template: ${err}`)); + } + + displayMatchedTemplateSelector() { + const currentInput = this.getFilename(); + this.templateSelectors.forEach((selector) => { + const match = selector.config.pattern.test(currentInput); + + if (match) { + this.typeSelector.show(); + this.selectTemplateType(selector.config); + this.showTemplateSelectorMenu(); + } + }); + } + + fetchFileTemplate(apiCall, query, data) { + return new Promise((resolve) => { + const resolveFile = file => resolve(file); + + if (!data) { + apiCall(query, resolveFile); + } else { + apiCall(query, data, resolveFile); + } + }); + } + + setEditorContent(file) { + if (!file && file !== '') return; + + const newValue = file.content || file; + + this.editor.setValue(newValue, 1); + + this.editor.focus(); + + this.editor.navigateFileStart(); + } + + findTemplateSelectorByKey(key) { + return this.templateSelectors.find(selector => selector.config.key === key); + } + + showUndoMenu() { + this.$undoMenu.removeClass('hidden'); + + this.$undoBtn.on('click', () => { + this.restoreFromCache(); + this.destroyUndoMenu(); + }); + } + + destroyUndoMenu() { + this.cacheFileContents(); + this.cacheToggleText(); + this.$undoMenu.addClass('hidden'); + this.$undoBtn.off('click'); + } + + hideTemplateSelectorMenu() { + this.$templatesMenu.hide(); + } + + showTemplateSelectorMenu() { + this.$templatesMenu.show(); + } + + cacheToggleText() { + this.cachedToggleText = this.getTemplateSelectorToggleText(); + } + + cacheFileContents() { + this.cachedContent = this.editor.getValue(); + this.cachedFilename = this.getFilename(); + } + + restoreFromCache() { + this.setEditorContent(this.cachedContent); + this.setFilename(this.cachedFilename); + this.setTemplateSelectorToggleText(); + } + + getTemplateSelectorToggleText() { + return this.$templateSelectors + .find('.js-template-selector-wrap:visible .dropdown-toggle-text') + .text(); + } + + setTemplateSelectorToggleText() { + return this.$templateSelectors + .find('.js-template-selector-wrap:visible .dropdown-toggle-text') + .text(this.cachedToggleText); + } + + getTypeSelectorToggleText() { + return this.typeSelector.getToggleText(); + } + + getFilename() { + return this.$filenameInput.val(); + } + + setFilename(name) { + this.$filenameInput.val(name); + } + + getSelected() { + return this.templateSelectors.find(selector => selector.selected); + } +} diff --git a/app/assets/javascripts/blob/file_template_selector.js b/app/assets/javascripts/blob/file_template_selector.js new file mode 100644 index 00000000000..31dd45fac89 --- /dev/null +++ b/app/assets/javascripts/blob/file_template_selector.js @@ -0,0 +1,60 @@ +/* global Api */ + +export default class FileTemplateSelector { + constructor(mediator) { + this.mediator = mediator; + this.$dropdown = null; + this.$wrapper = null; + } + + init() { + const cfg = this.config; + + this.$dropdown = $(cfg.dropdown); + this.$wrapper = $(cfg.wrapper); + this.$loadingIcon = this.$wrapper.find('.fa-chevron-down'); + this.$dropdownToggleText = this.$wrapper.find('.dropdown-toggle-text'); + + this.initDropdown(); + } + + show() { + if (this.$dropdown === null) { + this.init(); + } + + this.$wrapper.removeClass('hidden'); + } + + hide() { + if (this.$dropdown !== null) { + this.$wrapper.addClass('hidden'); + } + } + + getToggleText() { + return this.$dropdownToggleText.text(); + } + + setToggleText(text) { + this.$dropdownToggleText.text(text); + } + + renderLoading() { + this.$loadingIcon + .addClass('fa-spinner fa-spin') + .removeClass('fa-chevron-down'); + } + + renderLoaded() { + this.$loadingIcon + .addClass('fa-chevron-down') + .removeClass('fa-spinner fa-spin'); + } + + reportSelection(query, el, e, data) { + e.preventDefault(); + return this.mediator.selectTemplateFile(this, query, data); + } +} + diff --git a/app/assets/javascripts/blob/template_selectors/template_selector.js b/app/assets/javascripts/blob/template_selector.js index d7c1c32efbd..d7c1c32efbd 100644 --- a/app/assets/javascripts/blob/template_selectors/template_selector.js +++ b/app/assets/javascripts/blob/template_selector.js diff --git a/app/assets/javascripts/blob/template_selectors/blob_ci_yaml_selector.js b/app/assets/javascripts/blob/template_selectors/blob_ci_yaml_selector.js deleted file mode 100644 index 5a5954e7751..00000000000 --- a/app/assets/javascripts/blob/template_selectors/blob_ci_yaml_selector.js +++ /dev/null @@ -1,9 +0,0 @@ -/* global Api */ - -import TemplateSelector from './template_selector'; - -export default class BlobCiYamlSelector extends TemplateSelector { - requestFile(query) { - return Api.gitlabCiYml(query.name, (file, config) => this.setEditorContent(file, config)); - } -} diff --git a/app/assets/javascripts/blob/template_selectors/blob_ci_yaml_selectors.js b/app/assets/javascripts/blob/template_selectors/blob_ci_yaml_selectors.js deleted file mode 100644 index 7a4d6a42a03..00000000000 --- a/app/assets/javascripts/blob/template_selectors/blob_ci_yaml_selectors.js +++ /dev/null @@ -1,23 +0,0 @@ -/* global Api */ - -import BlobCiYamlSelector from './blob_ci_yaml_selector'; - -export default class BlobCiYamlSelectors { - constructor({ editor, $dropdowns }) { - this.$dropdowns = $dropdowns || $('.js-gitlab-ci-yml-selector'); - this.initSelectors(editor); - } - - initSelectors(editor) { - this.$dropdowns.each((i, dropdown) => { - const $dropdown = $(dropdown); - return new BlobCiYamlSelector({ - editor, - pattern: /(.gitlab-ci.yml)/, - data: $dropdown.data('data'), - wrapper: $dropdown.closest('.js-gitlab-ci-yml-selector-wrap'), - dropdown: $dropdown, - }); - }); - } -} diff --git a/app/assets/javascripts/blob/template_selectors/blob_dockerfile_selector.js b/app/assets/javascripts/blob/template_selectors/blob_dockerfile_selector.js deleted file mode 100644 index 19f8820a0cb..00000000000 --- a/app/assets/javascripts/blob/template_selectors/blob_dockerfile_selector.js +++ /dev/null @@ -1,9 +0,0 @@ -/* global Api */ - -import TemplateSelector from './template_selector'; - -export default class BlobDockerfileSelector extends TemplateSelector { - requestFile(query) { - return Api.dockerfileYml(query.name, (file, config) => this.setEditorContent(file, config)); - } -} diff --git a/app/assets/javascripts/blob/template_selectors/blob_dockerfile_selectors.js b/app/assets/javascripts/blob/template_selectors/blob_dockerfile_selectors.js deleted file mode 100644 index da067035b43..00000000000 --- a/app/assets/javascripts/blob/template_selectors/blob_dockerfile_selectors.js +++ /dev/null @@ -1,23 +0,0 @@ -import BlobDockerfileSelector from './blob_dockerfile_selector'; - -export default class BlobDockerfileSelectors { - constructor({ editor, $dropdowns }) { - this.editor = editor; - this.$dropdowns = $dropdowns || $('.js-dockerfile-selector'); - this.initSelectors(); - } - - initSelectors() { - const editor = this.editor; - this.$dropdowns.each((i, dropdown) => { - const $dropdown = $(dropdown); - return new BlobDockerfileSelector({ - editor, - pattern: /(Dockerfile)/, - data: $dropdown.data('data'), - wrapper: $dropdown.closest('.js-dockerfile-selector-wrap'), - dropdown: $dropdown, - }); - }); - } -} diff --git a/app/assets/javascripts/blob/template_selectors/blob_gitignore_selector.js b/app/assets/javascripts/blob/template_selectors/blob_gitignore_selector.js deleted file mode 100644 index 0b6b02fc2b3..00000000000 --- a/app/assets/javascripts/blob/template_selectors/blob_gitignore_selector.js +++ /dev/null @@ -1,9 +0,0 @@ -/* global Api */ - -import TemplateSelector from './template_selector'; - -export default class BlobGitignoreSelector extends TemplateSelector { - requestFile(query) { - return Api.gitignoreText(query.name, (file, config) => this.setEditorContent(file, config)); - } -} diff --git a/app/assets/javascripts/blob/template_selectors/blob_gitignore_selectors.js b/app/assets/javascripts/blob/template_selectors/blob_gitignore_selectors.js deleted file mode 100644 index dc485d97677..00000000000 --- a/app/assets/javascripts/blob/template_selectors/blob_gitignore_selectors.js +++ /dev/null @@ -1,23 +0,0 @@ -import BlobGitignoreSelector from './blob_gitignore_selector'; - -export default class BlobGitignoreSelectors { - constructor({ editor, $dropdowns }) { - this.$dropdowns = $dropdowns || $('.js-gitignore-selector'); - this.editor = editor; - this.initSelectors(); - } - - initSelectors() { - this.$dropdowns.each((i, dropdown) => { - const $dropdown = $(dropdown); - - return new BlobGitignoreSelector({ - pattern: /(.gitignore)/, - data: $dropdown.data('data'), - wrapper: $dropdown.closest('.js-gitignore-selector-wrap'), - dropdown: $dropdown, - editor: this.editor, - }); - }); - } -} diff --git a/app/assets/javascripts/blob/template_selectors/blob_license_selector.js b/app/assets/javascripts/blob/template_selectors/blob_license_selector.js deleted file mode 100644 index e9cb31cc2dc..00000000000 --- a/app/assets/javascripts/blob/template_selectors/blob_license_selector.js +++ /dev/null @@ -1,13 +0,0 @@ -/* global Api */ - -import TemplateSelector from './template_selector'; - -export default class BlobLicenseSelector extends TemplateSelector { - requestFile(query) { - const data = { - project: this.dropdown.data('project'), - fullname: this.dropdown.data('fullname'), - }; - return Api.licenseText(query.id, data, (file, config) => this.setEditorContent(file, config)); - } -} diff --git a/app/assets/javascripts/blob/template_selectors/blob_license_selectors.js b/app/assets/javascripts/blob/template_selectors/blob_license_selectors.js deleted file mode 100644 index a44f4f78b2d..00000000000 --- a/app/assets/javascripts/blob/template_selectors/blob_license_selectors.js +++ /dev/null @@ -1,24 +0,0 @@ -/* eslint-disable no-unused-vars, no-param-reassign */ - -import BlobLicenseSelector from './blob_license_selector'; - -export default class BlobLicenseSelectors { - constructor({ $dropdowns, editor }) { - this.$dropdowns = $dropdowns || $('.js-license-selector'); - this.initSelectors(editor); - } - - initSelectors(editor) { - this.$dropdowns.each((i, dropdown) => { - const $dropdown = $(dropdown); - - return new BlobLicenseSelector({ - editor, - pattern: /^(.+\/)?(licen[sc]e|copying)($|\.)/i, - data: $dropdown.data('data'), - wrapper: $dropdown.closest('.js-license-selector-wrap'), - dropdown: $dropdown, - }); - }); - } -} diff --git a/app/assets/javascripts/blob/template_selectors/ci_yaml_selector.js b/app/assets/javascripts/blob/template_selectors/ci_yaml_selector.js new file mode 100644 index 00000000000..935df07677c --- /dev/null +++ b/app/assets/javascripts/blob/template_selectors/ci_yaml_selector.js @@ -0,0 +1,32 @@ +/* global Api */ + +import FileTemplateSelector from '../file_template_selector'; + +export default class BlobCiYamlSelector extends FileTemplateSelector { + constructor({ mediator }) { + super(mediator); + this.config = { + key: 'gitlab-ci-yaml', + name: '.gitlab-ci.yml', + pattern: /(.gitlab-ci.yml)/, + endpoint: Api.gitlabCiYml, + dropdown: '.js-gitlab-ci-yml-selector', + wrapper: '.js-gitlab-ci-yml-selector-wrap', + }; + } + + initDropdown() { + // maybe move to super class as well + this.$dropdown.glDropdown({ + data: this.$dropdown.data('data'), + filterable: true, + selectable: true, + toggleLabel: item => item.name, + search: { + fields: ['name'], + }, + clicked: (query, el, e) => this.reportSelection(query.name, el, e), + text: item => item.name, + }); + } +} diff --git a/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js b/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js new file mode 100644 index 00000000000..b4b4d09c315 --- /dev/null +++ b/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js @@ -0,0 +1,32 @@ +/* global Api */ + +import FileTemplateSelector from '../file_template_selector'; + +export default class DockerfileSelector extends FileTemplateSelector { + constructor({ mediator }) { + super(mediator); + this.config = { + key: 'dockerfile', + name: 'Dockerfile', + pattern: /(Dockerfile)/, + endpoint: Api.dockerfileYml, + dropdown: '.js-dockerfile-selector', + wrapper: '.js-dockerfile-selector-wrap', + }; + } + + initDropdown() { + // maybe move to super class as well + this.$dropdown.glDropdown({ + data: this.$dropdown.data('data'), + filterable: true, + selectable: true, + toggleLabel: item => item.name, + search: { + fields: ['name'], + }, + clicked: (query, el, e) => this.reportSelection(query.name, el, e), + text: item => item.name, + }); + } +} diff --git a/app/assets/javascripts/blob/template_selectors/gitignore_selector.js b/app/assets/javascripts/blob/template_selectors/gitignore_selector.js new file mode 100644 index 00000000000..aefae54ae71 --- /dev/null +++ b/app/assets/javascripts/blob/template_selectors/gitignore_selector.js @@ -0,0 +1,31 @@ +/* global Api */ + +import FileTemplateSelector from '../file_template_selector'; + +export default class BlobGitignoreSelector extends FileTemplateSelector { + constructor({ mediator }) { + super(mediator); + this.config = { + key: 'gitignore', + name: '.gitignore', + pattern: /(.gitignore)/, + endpoint: Api.gitignoreText, + dropdown: '.js-gitignore-selector', + wrapper: '.js-gitignore-selector-wrap', + }; + } + + initDropdown() { + this.$dropdown.glDropdown({ + data: this.$dropdown.data('data'), + filterable: true, + selectable: true, + toggleLabel: item => item.name, + search: { + fields: ['name'], + }, + clicked: (query, el, e) => this.reportSelection(query.name, el, e), + text: item => item.name, + }); + } +} diff --git a/app/assets/javascripts/blob/template_selectors/license_selector.js b/app/assets/javascripts/blob/template_selectors/license_selector.js new file mode 100644 index 00000000000..c8abd689ab4 --- /dev/null +++ b/app/assets/javascripts/blob/template_selectors/license_selector.js @@ -0,0 +1,38 @@ +/* global Api */ + +import FileTemplateSelector from '../file_template_selector'; + +export default class BlobLicenseSelector extends FileTemplateSelector { + constructor({ mediator }) { + super(mediator); + this.config = { + key: 'license', + name: 'LICENSE', + pattern: /^(.+\/)?(licen[sc]e|copying)($|\.)/i, + endpoint: Api.licenseText, + dropdown: '.js-license-selector', + wrapper: '.js-license-selector-wrap', + }; + } + + initDropdown() { + this.$dropdown.glDropdown({ + data: this.$dropdown.data('data'), + filterable: true, + selectable: true, + toggleLabel: item => item.name, + search: { + fields: ['name'], + }, + clicked: (query, el, e) => { + const data = { + project: this.$dropdown.data('project'), + fullname: this.$dropdown.data('fullname'), + }; + + this.reportSelection(query.id, el, e, data); + }, + text: item => item.name, + }); + } +} diff --git a/app/assets/javascripts/blob/template_selectors/type_selector.js b/app/assets/javascripts/blob/template_selectors/type_selector.js new file mode 100644 index 00000000000..56f23ef0568 --- /dev/null +++ b/app/assets/javascripts/blob/template_selectors/type_selector.js @@ -0,0 +1,25 @@ +import FileTemplateSelector from '../file_template_selector'; + +export default class FileTemplateTypeSelector extends FileTemplateSelector { + constructor({ mediator, dropdownData }) { + super(mediator); + this.mediator = mediator; + this.config = { + dropdown: '.js-template-type-selector', + wrapper: '.js-template-type-selector-wrap', + dropdownData, + }; + } + + initDropdown() { + this.$dropdown.glDropdown({ + data: this.config.dropdownData, + filterable: false, + selectable: true, + toggleLabel: item => item.name, + clicked: (item, el, e) => this.mediator.selectTemplateType(item, el, e), + text: item => item.name, + }); + } + +} diff --git a/app/assets/javascripts/blob_edit/blob_bundle.js b/app/assets/javascripts/blob_edit/blob_bundle.js index c5deccf631e..1c64ccf536f 100644 --- a/app/assets/javascripts/blob_edit/blob_bundle.js +++ b/app/assets/javascripts/blob_edit/blob_bundle.js @@ -13,8 +13,9 @@ $(() => { const urlRoot = editBlobForm.data('relative-url-root'); const assetsPath = editBlobForm.data('assets-prefix'); const blobLanguage = editBlobForm.data('blob-language'); + const currentAction = $('.js-file-title').data('current-action'); - new EditBlob(`${urlRoot}${assetsPath}`, blobLanguage); + new EditBlob(`${urlRoot}${assetsPath}`, blobLanguage, currentAction); new NewCommitForm(editBlobForm); } diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js index d3560d5df3b..b37988a674d 100644 --- a/app/assets/javascripts/blob_edit/edit_blob.js +++ b/app/assets/javascripts/blob_edit/edit_blob.js @@ -1,17 +1,13 @@ /* global ace */ -import BlobLicenseSelectors from '../blob/template_selectors/blob_license_selectors'; -import BlobGitignoreSelectors from '../blob/template_selectors/blob_gitignore_selectors'; -import BlobCiYamlSelectors from '../blob/template_selectors/blob_ci_yaml_selectors'; -import BlobDockerfileSelectors from '../blob/template_selectors/blob_dockerfile_selectors'; +import TemplateSelectorMediator from '../blob/file_template_mediator'; export default class EditBlob { - constructor(assetsPath, aceMode) { + constructor(assetsPath, aceMode, currentAction) { this.configureAceEditor(aceMode, assetsPath); - this.prepFileContentForSubmit(); this.initModePanesAndLinks(); this.initSoftWrap(); - this.initFileSelectors(); + this.initFileSelectors(currentAction); } configureAceEditor(aceMode, assetsPath) { @@ -19,6 +15,10 @@ export default class EditBlob { ace.config.loadModule('ace/ext/searchbox'); this.editor = ace.edit('editor'); + + // This prevents warnings re: automatic scrolling being logged + this.editor.$blockScrolling = Infinity; + this.editor.focus(); if (aceMode) { @@ -26,29 +26,13 @@ export default class EditBlob { } } - prepFileContentForSubmit() { - $('form').submit(() => { - $('#file-content').val(this.editor.getValue()); + initFileSelectors(currentAction) { + this.fileTemplateMediator = new TemplateSelectorMediator({ + currentAction, + editor: this.editor, }); } - initFileSelectors() { - this.blobTemplateSelectors = [ - new BlobLicenseSelectors({ - editor: this.editor, - }), - new BlobGitignoreSelectors({ - editor: this.editor, - }), - new BlobCiYamlSelectors({ - editor: this.editor, - }), - new BlobDockerfileSelectors({ - editor: this.editor, - }), - ]; - } - initModePanesAndLinks() { this.$editModePanes = $('.js-edit-mode-pane'); this.$editModeLinks = $('.js-edit-mode a'); diff --git a/app/assets/javascripts/templates/issuable_template_selector.js b/app/assets/javascripts/templates/issuable_template_selector.js index 32067ed1fee..e62f429f1ae 100644 --- a/app/assets/javascripts/templates/issuable_template_selector.js +++ b/app/assets/javascripts/templates/issuable_template_selector.js @@ -1,7 +1,7 @@ /* eslint-disable comma-dangle, max-len, no-useless-return, no-param-reassign, max-len */ /* global Api */ -import TemplateSelector from '../blob/template_selectors/template_selector'; +import TemplateSelector from '../blob/template_selector'; ((global) => { class IssuableTemplateSelector extends TemplateSelector { diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss index 4af267403d8..f6b8c8ee2bc 100644 --- a/app/assets/stylesheets/pages/editor.scss +++ b/app/assets/stylesheets/pages/editor.scss @@ -1,4 +1,13 @@ .file-editor { + .nav-links { + border-top: 1px solid $border-color; + border-right: 1px solid $border-color; + border-left: 1px solid $border-color; + border-bottom: none; + border-radius: 2px; + background: $gray-normal; + } + #editor { border: none; border-radius: 0; @@ -72,11 +81,7 @@ } .encoding-selector, - .soft-wrap-toggle, - .license-selector, - .gitignore-selector, - .gitlab-ci-yml-selector, - .dockerfile-selector { + .soft-wrap-toggle { display: inline-block; vertical-align: top; font-family: $regular_font; @@ -103,28 +108,9 @@ } } } - - .gitignore-selector, - .license-selector, - .gitlab-ci-yml-selector, - .dockerfile-selector { - .dropdown { - line-height: 21px; - } - - .dropdown-menu-toggle { - vertical-align: top; - width: 220px; - } - } - - .gitlab-ci-yml-selector { - .dropdown-menu-toggle { - width: 250px; - } - } } + @media(max-width: $screen-xs-max){ .file-editor { .file-title { @@ -149,10 +135,7 @@ margin: 3px 0; } - .encoding-selector, - .license-selector, - .gitignore-selector, - .gitlab-ci-yml-selector { + .encoding-selector { display: block; margin: 3px 0; @@ -163,3 +146,104 @@ } } } + +.blob-new-page-title, +.blob-edit-page-title { + margin: 19px 0 21px; + vertical-align: top; + display: inline-block; + + @media(max-width: $screen-sm-max) { + display: block; + margin: 19px 0 12px; + } +} + +.template-selectors-menu { + display: inline-block; + vertical-align: top; + margin: 14px 0 0 16px; + padding: 0 0 0 14px; + border-left: 1px solid $border-color; + + @media(max-width: $screen-sm-max) { + display: block; + width: 100%; + margin: 5px 0; + padding: 0; + border-left: none; + } +} + +.templates-selectors-label { + display: inline-block; + vertical-align: top; + margin-top: 6px; + line-height: 21px; + + @media(max-width: $screen-sm-max) { + display: block; + margin: 5px 0; + } +} + +.template-selector-dropdowns-wrap { + display: inline-block; + margin-left: 8px; + vertical-align: top; + margin: 5px 0 0 8px; + + @media(max-width: $screen-sm-max) { + display: block; + width: 100%; + margin: 0 0 16px; + } + + .license-selector, + .gitignore-selector, + .gitlab-ci-yml-selector, + .dockerfile-selector, + .template-type-selector { + display: inline-block; + vertical-align: top; + font-family: $regular_font; + margin-top: -5px; + + @media(max-width: $screen-sm-max) { + display: block; + width: 100%; + margin: 5px 0; + } + + .dropdown { + line-height: 21px; + } + + .dropdown-menu-toggle { + width: 250px; + vertical-align: top; + + @media(max-width: $screen-sm-max) { + display: block; + width: 100%; + margin: 5px 0; + } + } + + } +} + +.template-selectors-undo-menu { + display: inline-block; + margin: 7px 0 0 10px; + + @media(max-width: $screen-sm-max) { + display: block; + width: 100%; + margin: 20px 0; + } + + button { + margin: -4px 0 0 15px; + } +} diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml index e7adef5558a..4b344b2edb9 100644 --- a/app/views/projects/blob/_editor.html.haml +++ b/app/views/projects/blob/_editor.html.haml @@ -1,29 +1,23 @@ +- action = current_action?(:edit) || current_action?(:update) ? 'edit' : 'create' + .file-holder.file.append-bottom-default - .js-file-title.file-title.clearfix + .js-file-title.file-title.clearfix{ data: { current_action: action } } .editor-ref = icon('code-fork') = ref %span.editor-file-name - if current_action?(:edit) || current_action?(:update) = text_field_tag 'file_path', (params[:file_path] || @path), - class: 'form-control new-file-path' + class: 'form-control new-file-path js-file-path-name-input' - if current_action?(:new) || current_action?(:create) %span.editor-file-name \/ = text_field_tag 'file_name', params[:file_name], placeholder: "File name", - required: true, class: 'form-control new-file-name' + required: true, class: 'form-control new-file-name js-file-path-name-input' .pull-right.file-buttons - .license-selector.js-license-selector-wrap.hidden - = dropdown_tag("Choose a License template", options: { toggle_class: 'btn js-license-selector', title: "Choose a license", filter: true, placeholder: "Filter", data: { data: licenses_for_select, project: @project.name, fullname: @project.namespace.human_name } } ) - .gitignore-selector.js-gitignore-selector-wrap.hidden - = dropdown_tag("Choose a .gitignore template", options: { toggle_class: 'btn js-gitignore-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { data: gitignore_names } } ) - .gitlab-ci-yml-selector.js-gitlab-ci-yml-selector-wrap.hidden - = dropdown_tag("Choose a GitLab CI Yaml template", options: { toggle_class: 'btn js-gitlab-ci-yml-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { data: gitlab_ci_ymls } } ) - .dockerfile-selector.js-dockerfile-selector-wrap.hidden - = dropdown_tag("Choose a Dockerfile template", options: { toggle_class: 'btn js-dockerfile-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { data: dockerfile_names } } ) - = button_tag class: 'soft-wrap-toggle btn', type: 'button' do + = button_tag class: 'soft-wrap-toggle btn', type: 'button', tabindex: '-1' do %span.no-wrap = custom_icon('icon_no_wrap') No wrap @@ -31,7 +25,7 @@ = custom_icon('icon_soft_wrap') Soft wrap .encoding-selector - = select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2' + = select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2', tabindex: '-1' .file-editor.code %pre.js-edit-mode-pane#editor= params[:content] || local_assigns[:blob_data] diff --git a/app/views/projects/blob/_template_selectors.html.haml b/app/views/projects/blob/_template_selectors.html.haml new file mode 100644 index 00000000000..d52733d2bd6 --- /dev/null +++ b/app/views/projects/blob/_template_selectors.html.haml @@ -0,0 +1,17 @@ +.template-selectors-menu + .templates-selectors-label + Template + .template-selector-dropdowns-wrap + .template-type-selector.js-template-type-selector-wrap.hidden + = dropdown_tag("Choose type", options: { toggle_class: 'btn js-template-type-selector', title: "Choose a template type" } ) + .license-selector.js-license-selector-wrap.js-template-selector-wrap.hidden + = dropdown_tag("Apply a License template", options: { toggle_class: 'btn js-license-selector', title: "Apply a license", filter: true, placeholder: "Filter", data: { data: licenses_for_select, project: @project.name, fullname: @project.namespace.human_name } } ) + .gitignore-selector.js-gitignore-selector-wrap.js-template-selector-wrap.hidden + = dropdown_tag("Apply a .gitignore template", options: { toggle_class: 'btn js-gitignore-selector', title: "Apply a template", filter: true, placeholder: "Filter", data: { data: gitignore_names } } ) + .gitlab-ci-yml-selector.js-gitlab-ci-yml-selector-wrap.js-template-selector-wrap.hidden + = dropdown_tag("Apply a GitLab CI Yaml template", options: { toggle_class: 'btn js-gitlab-ci-yml-selector', title: "Apply a template", filter: true, placeholder: "Filter", data: { data: gitlab_ci_ymls } } ) + .dockerfile-selector.js-dockerfile-selector-wrap.js-template-selector-wrap.hidden + = dropdown_tag("Apply a Dockerfile template", options: { toggle_class: 'btn js-dockerfile-selector', title: "Apply a template", filter: true, placeholder: "Filter", data: { data: dockerfile_names } } ) + .template-selectors-undo-menu.hidden + %span.text-info Template applied + %button.btn.btn-sm.btn-info Undo diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml index afe0b5dba45..4b26f944733 100644 --- a/app/views/projects/blob/edit.html.haml +++ b/app/views/projects/blob/edit.html.haml @@ -11,12 +11,15 @@ Someone edited the file the same time you did. Please check out = link_to "the file", namespace_project_blob_path(@project.namespace, @project, tree_join(@target_branch, @file_path)), target: "_blank", rel: 'noopener noreferrer' and make sure your changes will not unintentionally remove theirs. - + .editor-title-row + %h3.page-title.blob-edit-page-title + Edit file + = render 'template_selectors' .file-editor %ul.nav-links.no-bottom.js-edit-mode %li.active = link_to '#editor' do - Edit File + Write %li = link_to '#preview', 'data-preview-url' => namespace_project_preview_blob_path(@project.namespace, @project, @id) do diff --git a/app/views/projects/blob/new.html.haml b/app/views/projects/blob/new.html.haml index 4c449e040ee..2afb909572a 100644 --- a/app/views/projects/blob/new.html.haml +++ b/app/views/projects/blob/new.html.haml @@ -2,10 +2,10 @@ - content_for :page_specific_javascripts do = page_specific_javascript_tag('lib/ace.js') = page_specific_javascript_bundle_tag('blob') - -%h3.page-title - New File - +.editor-title-row + %h3.page-title.blob-new-page-title + New file + = render 'template_selectors' .file-editor = form_tag(namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post, class: 'form-horizontal js-edit-blob-form js-new-blob-form js-quick-submit js-requires-input', data: blob_editor_paths) do = render 'projects/blob/editor', ref: @ref diff --git a/changelogs/unreleased/25332-make-file-templates-easy-to-use-and-discover.yml b/changelogs/unreleased/25332-make-file-templates-easy-to-use-and-discover.yml new file mode 100644 index 00000000000..fc95858f783 --- /dev/null +++ b/changelogs/unreleased/25332-make-file-templates-easy-to-use-and-discover.yml @@ -0,0 +1,4 @@ +--- +title: Remove no-new annotation from file_template_mediator.js. +merge_request: !9782 +author: diff --git a/spec/features/auto_deploy_spec.rb b/spec/features/auto_deploy_spec.rb index ea7a97d1d4f..009e9c6b04c 100644 --- a/spec/features/auto_deploy_spec.rb +++ b/spec/features/auto_deploy_spec.rb @@ -42,7 +42,7 @@ describe 'Auto deploy' do it 'includes OpenShift as an available template', js: true do click_link 'Set up auto deploy' - click_button 'Choose a GitLab CI Yaml template' + click_button 'Apply a GitLab CI Yaml template' within '.gitlab-ci-yml-selector' do expect(page).to have_content('OpenShift') @@ -51,7 +51,7 @@ describe 'Auto deploy' do it 'creates a merge request using "auto-deploy" branch', js: true do click_link 'Set up auto deploy' - click_button 'Choose a GitLab CI Yaml template' + click_button 'Apply a GitLab CI Yaml template' within '.gitlab-ci-yml-selector' do click_on 'OpenShift' end diff --git a/spec/features/projects/blobs/user_create_spec.rb b/spec/features/projects/blobs/user_create_spec.rb index 5686868a0c4..d214a531138 100644 --- a/spec/features/projects/blobs/user_create_spec.rb +++ b/spec/features/projects/blobs/user_create_spec.rb @@ -88,7 +88,7 @@ feature 'New blob creation', feature: true, js: true do scenario 'shows error message' do expect(page).to have_content('Your changes could not be committed because a file with the same name already exists') - expect(page).to have_content('New File') + expect(page).to have_content('New file') expect(page).to have_content('NextFeature') end end diff --git a/spec/features/projects/files/project_owner_creates_license_file_spec.rb b/spec/features/projects/files/project_owner_creates_license_file_spec.rb index ccadc936567..6b281e6d21d 100644 --- a/spec/features/projects/files/project_owner_creates_license_file_spec.rb +++ b/spec/features/projects/files/project_owner_creates_license_file_spec.rb @@ -40,7 +40,7 @@ feature 'project owner creates a license file', feature: true, js: true do scenario 'project master creates a license file from the "Add license" link' do click_link 'Add License' - expect(page).to have_content('New File') + expect(page).to have_content('New file') expect(current_path).to eq( namespace_project_new_blob_path(project.namespace, project, 'master')) expect(find('#file_name').value).to eq('LICENSE') @@ -63,7 +63,7 @@ feature 'project owner creates a license file', feature: true, js: true do def select_template(template) page.within('.js-license-selector-wrap') do - click_button 'Choose a License template' + click_button 'Apply a License template' click_link template wait_for_ajax end diff --git a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb index 420db962318..87322ac2584 100644 --- a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb +++ b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb @@ -14,7 +14,7 @@ feature 'project owner sees a link to create a license file in empty project', f visit namespace_project_path(project.namespace, project) click_link 'Create empty bare repository' click_on 'LICENSE' - expect(page).to have_content('New File') + expect(page).to have_content('New file') expect(current_path).to eq( namespace_project_new_blob_path(project.namespace, project, 'master')) @@ -40,7 +40,7 @@ feature 'project owner sees a link to create a license file in empty project', f def select_template(template) page.within('.js-license-selector-wrap') do - click_button 'Choose a License template' + click_button 'Apply a License template' click_link template wait_for_ajax end diff --git a/spec/features/projects/files/template_type_dropdown_spec.rb b/spec/features/projects/files/template_type_dropdown_spec.rb new file mode 100644 index 00000000000..5ee5e5b4c4e --- /dev/null +++ b/spec/features/projects/files/template_type_dropdown_spec.rb @@ -0,0 +1,135 @@ +require 'spec_helper' + +feature 'Template type dropdown selector', js: true do + let(:project) { create(:project) } + let(:user) { create(:user) } + + before do + project.team << [user, :master] + login_as user + end + + context 'editing a non-matching file' do + before do + create_and_edit_file('.random-file.js') + end + + scenario 'not displayed' do + check_type_selector_display(false) + end + + scenario 'selects every template type correctly' do + fill_in 'file_path', with: '.gitignore' + try_selecting_all_types + end + + scenario 'updates toggle value when input matches' do + fill_in 'file_path', with: '.gitignore' + check_type_selector_toggle_text('.gitignore') + end + end + + context 'editing a matching file' do + before do + visit namespace_project_edit_blob_path(project.namespace, project, File.join(project.default_branch, 'LICENSE')) + end + + scenario 'displayed' do + check_type_selector_display(true) + end + + scenario 'is displayed when input matches' do + check_type_selector_display(true) + end + + scenario 'selects every template type correctly' do + try_selecting_all_types + end + + context 'user previews changes' do + before do + click_link 'Preview Changes' + end + + scenario 'type selector is hidden and shown correctly' do + check_type_selector_display(false) + click_link 'Write' + check_type_selector_display(true) + end + end + end + + context 'creating a matching file' do + before do + visit namespace_project_new_blob_path(project.namespace, project, 'master', file_name: '.gitignore') + end + + scenario 'is displayed' do + check_type_selector_display(true) + end + + scenario 'toggle is set to the correct value' do + check_type_selector_toggle_text('.gitignore') + end + + scenario 'selects every template type correctly' do + try_selecting_all_types + end + end + + context 'creating a file' do + before do + visit namespace_project_new_blob_path(project.namespace, project, project.default_branch) + end + + scenario 'type selector is shown' do + check_type_selector_display(true) + end + + scenario 'toggle is set to the proper value' do + check_type_selector_toggle_text('Choose type') + end + + scenario 'selects every template type correctly' do + try_selecting_all_types + end + end +end + +def check_type_selector_display(is_visible) + count = is_visible ? 1 : 0 + expect(page).to have_css('.js-template-type-selector', count: count) +end + +def try_selecting_all_types + try_selecting_template_type('LICENSE', 'Apply a License template') + try_selecting_template_type('Dockerfile', 'Apply a Dockerfile template') + try_selecting_template_type('.gitlab-ci.yml', 'Apply a GitLab CI Yaml template') + try_selecting_template_type('.gitignore', 'Apply a .gitignore template') +end + +def try_selecting_template_type(template_type, selector_label) + select_template_type(template_type) + check_template_selector_display(selector_label) + check_type_selector_toggle_text(template_type) +end + +def select_template_type(template_type) + find('.js-template-type-selector').click + find('.dropdown-content li', text: template_type).click +end + +def check_template_selector_display(content) + expect(page).to have_content(content) +end + +def check_type_selector_toggle_text(template_type) + dropdown_toggle_button = find('.template-type-selector .dropdown-toggle-text') + expect(dropdown_toggle_button).to have_content(template_type) +end + +def create_and_edit_file(file_name) + visit namespace_project_new_blob_path(project.namespace, project, 'master', file_name: file_name) + click_button "Commit Changes" + visit namespace_project_edit_blob_path(project.namespace, project, File.join(project.default_branch, file_name)) +end diff --git a/spec/features/projects/files/undo_template_spec.rb b/spec/features/projects/files/undo_template_spec.rb new file mode 100644 index 00000000000..5479ea34610 --- /dev/null +++ b/spec/features/projects/files/undo_template_spec.rb @@ -0,0 +1,67 @@ +require 'spec_helper' +include WaitForAjax + +feature 'Template Undo Button', js: true do + let(:project) { create(:project) } + let(:user) { create(:user) } + + before do + project.team << [user, :master] + login_as user + end + + context 'editing a matching file and applying a template' do + before do + visit namespace_project_edit_blob_path(project.namespace, project, File.join(project.default_branch, "LICENSE")) + select_file_template('.js-license-selector', 'Apache License 2.0') + end + + scenario 'reverts template application' do + try_template_undo('http://www.apache.org/licenses/', 'Apply a License template') + end + end + + context 'creating a non-matching file' do + before do + visit namespace_project_new_blob_path(project.namespace, project, 'master') + select_file_template_type('LICENSE') + select_file_template('.js-license-selector', 'Apache License 2.0') + end + + scenario 'reverts template application' do + try_template_undo('http://www.apache.org/licenses/', 'Apply a License template') + end + end +end + +def try_template_undo(template_content, toggle_text) + check_undo_button_display + check_content_reverted(template_content) + check_toggle_text_set(toggle_text) +end + +def check_toggle_text_set(neutral_toggle_text) + expect(page).to have_content(neutral_toggle_text) +end + +def check_undo_button_display + expect(page).to have_content('Template applied') + expect(page).to have_css('.template-selectors-undo-menu .btn-info') +end + +def check_content_reverted(template_content) + find('.template-selectors-undo-menu .btn-info').click + expect(page).not_to have_content(template_content) + expect(find('.template-type-selector .dropdown-toggle-text')).to have_content() +end + +def select_file_template(template_selector_selector, template_name) + find(template_selector_selector).click + find('.dropdown-content li', text: template_name).click + wait_for_ajax +end + +def select_file_template_type(template_type) + find('.js-template-type-selector').click + find('.dropdown-content li', text: template_type).click +end |