diff options
82 files changed, 2333 insertions, 1194 deletions
diff --git a/.haml-lint_todo.yml b/.haml-lint_todo.yml index c6afe2c824c..a181d17d743 100644 --- a/.haml-lint_todo.yml +++ b/.haml-lint_todo.yml @@ -343,8 +343,6 @@ linters: - 'app/views/projects/triggers/_index.html.haml' - 'app/views/projects/triggers/_trigger.html.haml' - 'app/views/projects/triggers/edit.html.haml' - - 'app/views/projects/wikis/_new.html.haml' - - 'app/views/projects/wikis/_pages_wiki_page.html.haml' - 'app/views/projects/wikis/edit.html.haml' - 'app/views/projects/wikis/history.html.haml' - 'app/views/repository_check_mailer/notify.html.haml' diff --git a/app/assets/javascripts/pages/projects/wiki_directories/index.js b/app/assets/javascripts/pages/projects/wiki_directories/index.js new file mode 100644 index 00000000000..b055b7cc322 --- /dev/null +++ b/app/assets/javascripts/pages/projects/wiki_directories/index.js @@ -0,0 +1,2 @@ +// currently, this controller inherits all behaviors from wikis +import '../wikis/index'; diff --git a/app/assets/javascripts/pages/projects/wiki_pages/index.js b/app/assets/javascripts/pages/projects/wiki_pages/index.js new file mode 100644 index 00000000000..92549cb648c --- /dev/null +++ b/app/assets/javascripts/pages/projects/wiki_pages/index.js @@ -0,0 +1,43 @@ +import $ from 'jquery'; +import Vue from 'vue'; + +import Translate from '~/vue_shared/translate'; +import csrf from '~/lib/utils/csrf'; +import ShortcutsWiki from '~/behaviors/shortcuts/shortcuts_wiki'; +import ZenMode from '~/zen_mode'; +import GLForm from '~/gl_form'; + +import deleteWikiModal from '../wikis/components/delete_wiki_modal.vue'; +import Wikis from '../wikis/wikis'; + +document.addEventListener('DOMContentLoaded', () => { + new Wikis(); // eslint-disable-line no-new + new ShortcutsWiki(); // eslint-disable-line no-new + new ZenMode(); // eslint-disable-line no-new + new GLForm($('.wiki-form')); // eslint-disable-line no-new + + const deleteWikiModalWrapperEl = document.getElementById('delete-wiki-modal-wrapper'); + + if (deleteWikiModalWrapperEl) { + Vue.use(Translate); + + const { deleteWikiUrl, pageTitle } = deleteWikiModalWrapperEl.dataset; + + // eslint-disable-next-line no-new + new Vue({ + el: deleteWikiModalWrapperEl, + data: { + deleteWikiUrl: '', + }, + render(createElement) { + return createElement(deleteWikiModal, { + props: { + pageTitle, + deleteWikiUrl, + csrfToken: csrf.token, + }, + }); + }, + }); + } +}); diff --git a/app/assets/javascripts/pages/projects/wikis/index.js b/app/assets/javascripts/pages/projects/wikis/index.js index f5fd84d69ac..1b0d8fb4b71 100644 --- a/app/assets/javascripts/pages/projects/wikis/index.js +++ b/app/assets/javascripts/pages/projects/wikis/index.js @@ -1,41 +1,11 @@ import $ from 'jquery'; -import Vue from 'vue'; -import Translate from '~/vue_shared/translate'; -import csrf from '~/lib/utils/csrf'; import ShortcutsWiki from '~/behaviors/shortcuts/shortcuts_wiki'; +import GLForm from '~/gl_form'; + import Wikis from './wikis'; -import ZenMode from '../../../zen_mode'; -import GLForm from '../../../gl_form'; -import deleteWikiModal from './components/delete_wiki_modal.vue'; document.addEventListener('DOMContentLoaded', () => { new Wikis(); // eslint-disable-line no-new new ShortcutsWiki(); // eslint-disable-line no-new - new ZenMode(); // eslint-disable-line no-new new GLForm($('.wiki-form')); // eslint-disable-line no-new - - const deleteWikiModalWrapperEl = document.getElementById('delete-wiki-modal-wrapper'); - - if (deleteWikiModalWrapperEl) { - Vue.use(Translate); - - const { deleteWikiUrl, pageTitle } = deleteWikiModalWrapperEl.dataset; - - // eslint-disable-next-line no-new - new Vue({ - el: deleteWikiModalWrapperEl, - data: { - deleteWikiUrl: '', - }, - render(createElement) { - return createElement(deleteWikiModal, { - props: { - pageTitle, - deleteWikiUrl, - csrfToken: csrf.token, - }, - }); - }, - }); - } }); diff --git a/app/assets/javascripts/pages/projects/wikis/wikis.js b/app/assets/javascripts/pages/projects/wikis/wikis.js index d41199f6374..1bf799a5ee0 100644 --- a/app/assets/javascripts/pages/projects/wikis/wikis.js +++ b/app/assets/javascripts/pages/projects/wikis/wikis.js @@ -12,8 +12,8 @@ export default class Wikis { } this.isNewWikiPage = Boolean(document.querySelector('.js-new-wiki-page')); - this.editTitleInput = document.querySelector('form.wiki-form #wiki_title'); - this.commitMessageInput = document.querySelector('form.wiki-form #wiki_message'); + this.editTitleInput = document.querySelector('form.wiki-form #wiki_page_title'); + this.commitMessageInput = document.querySelector('form.wiki-form #wiki_page_message'); this.commitMessageI18n = this.isNewWikiPage ? s__('WikiPageCreate|Create %{pageTitle}') : s__('WikiPageEdit|Update %{pageTitle}'); diff --git a/app/assets/stylesheets/framework/icons.scss b/app/assets/stylesheets/framework/icons.scss index a53f5d85949..f73d1b41f84 100644 --- a/app/assets/stylesheets/framework/icons.scss +++ b/app/assets/stylesheets/framework/icons.scss @@ -99,3 +99,18 @@ justify-content: center; color: $gray-700; } + +.svg-icon { + display: inline-flex; + align-self: center; + + svg { + height: 1em; + width: 1em; + } + + &.svg-baseline svg { + top: 0.125em; + position: relative; + } +} diff --git a/app/assets/stylesheets/pages/wiki.scss b/app/assets/stylesheets/pages/wiki.scss index 0b65b915abf..8f1b4cdb6d5 100644 --- a/app/assets/stylesheets/pages/wiki.scss +++ b/app/assets/stylesheets/pages/wiki.scss @@ -1,3 +1,31 @@ +.new-wiki-page { + .new-wiki-page-slug-tip { + display: inline-block; + max-width: 100%; + margin-top: 5px; + } +} + +.wiki-history, +.wiki-page, +.edit-wiki-page { + margin-right: $gutter-width; +} + +.edit-wiki-page { + @media only screen and (min-width: map-get($grid-breakpoints, lg) + (2 * $gutter-width)) and (max-width: map-get($grid-breakpoints, lg) + (3 * $gutter-width)) { + margin-right: $gutter-width * 1.5; + } +} + +.wiki-form { + .edit-wiki-page-slug-tip { + display: inline-block; + max-width: 100%; + margin-top: 5px; + } +} + .title .edit-wiki-header { width: 780px; margin-left: auto; @@ -5,6 +33,15 @@ padding-right: 7px; } +.container-fluid.wiki-page, +.container-fluid.edit-wiki-page { + width: initial; +} + +.wiki-history.breadcrumbs { + min-height: (2 * $gl-padding) + 22; +} + .wiki-page-header { position: relative; @@ -79,7 +116,7 @@ } .sidebar-container { - padding: $gl-padding 0; + padding-bottom: $gl-padding; width: calc(100% + 100px); padding-right: 100px; height: 100%; @@ -125,7 +162,7 @@ } .wiki-sidebar-header { - padding: 0 $gl-padding $gl-padding; + padding: $gl-padding; .gutter-toggle { margin-top: 0; diff --git a/app/controllers/concerns/preview_markdown.rb b/app/controllers/concerns/preview_markdown.rb index 2a9729b6ffd..a3938ea3652 100644 --- a/app/controllers/concerns/preview_markdown.rb +++ b/app/controllers/concerns/preview_markdown.rb @@ -9,11 +9,10 @@ module PreviewMarkdown markdown_params = case controller_name - when 'wikis' then { pipeline: :wiki, project_wiki: @project_wiki, page_slug: params[:id] } when 'snippets' then { skip_project_check: true } when 'groups' then { group: group } when 'projects' then projects_filter_params - else {} + else preview_markdown_params end render json: { @@ -25,6 +24,7 @@ module PreviewMarkdown } } end + # rubocop:enable Gitlab/ModuleWithInstanceVariables def projects_filter_params { @@ -32,5 +32,11 @@ module PreviewMarkdown suggestions_filter_enabled: params[:preview_suggestions].present? } end - # rubocop:enable Gitlab/ModuleWithInstanceVariables + + private + + # Override this method to customise the markdown for your controller + def preview_markdown_params + {} + end end diff --git a/app/controllers/concerns/project_wiki_actions.rb b/app/controllers/concerns/project_wiki_actions.rb new file mode 100644 index 00000000000..304e16a894a --- /dev/null +++ b/app/controllers/concerns/project_wiki_actions.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +# Controllers that include this concern must provide: +# * project +# * current_user +module ProjectWikiActions + extend ActiveSupport::Concern + + included do + before_action :authorize_read_wiki! + before_action :init_wiki_actions + + attr_accessor :project_wiki, :sidebar_page, :sidebar_wiki_entries + end + + def init_wiki_actions + load_project_wiki + load_wiki_sidebar + rescue ProjectWiki::CouldNotCreateWikiError + flash[:notice] = _("Could not create Wiki Repository at this time. Please try again later.") + redirect_to project_path(project) + end + + def load_project_wiki + self.project_wiki = load_wiki + end + + def load_wiki_sidebar + self.sidebar_page = project_wiki.find_sidebar(params[:version_id]) + + return if sidebar_page.present? + + # Fallback to default sidebar + self.sidebar_wiki_entries = WikiDirectory.group_by_directory(project_wiki.list_pages(limit: 15)) + end + + def load_wiki + # Call #wiki to make sure the Wiki Repo is initialized + ProjectWiki.new(project, current_user).tap(&:wiki) + end +end diff --git a/app/controllers/projects/wiki_directories_controller.rb b/app/controllers/projects/wiki_directories_controller.rb new file mode 100644 index 00000000000..f66da60f498 --- /dev/null +++ b/app/controllers/projects/wiki_directories_controller.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +class Projects::WikiDirectoriesController < Projects::ApplicationController + include ProjectWikiActions + + def self.local_prefixes + [controller_path, 'shared/wiki'] + end + + def show + @wiki_dir = find_dir || WikiDirectory.new(params[:id]) + + return render('empty') if @wiki_dir.empty? + + @wiki_entries = @wiki_pages = Kaminari + .paginate_array(@wiki_dir.pages) + .page(params[:page]) + + render 'show' + end + + private + + def find_dir + dir_params = params.permit(:id, :sort, :direction).dup.tap do |h| + Gitlab::Utils.allow_hash_values(h, sort_params_config[:allowed]) + end + + project_wiki.find_dir(dir_params[:id], dir_params[:sort], dir_params[:direction]) + end +end diff --git a/app/controllers/projects/wiki_pages_controller.rb b/app/controllers/projects/wiki_pages_controller.rb new file mode 100644 index 00000000000..0ba397b08f8 --- /dev/null +++ b/app/controllers/projects/wiki_pages_controller.rb @@ -0,0 +1,180 @@ +# frozen_string_literal: true + +class Projects::WikiPagesController < Projects::ApplicationController + include ProjectWikiActions + include SendsBlob + include PreviewMarkdown + include Gitlab::Utils::StrongMemoize + + def self.local_prefixes + [controller_path, 'shared/wiki'] + end + + before_action :authorize_create_wiki!, only: [:edit, :create, :update] + before_action :authorize_admin_wiki!, only: :destroy + + before_action :load_page, only: [:show, :edit, :update, :history, :destroy] + before_action :valid_encoding?, + if: -> { %w[show edit update].include?(action_name) && load_page } + before_action only: [:edit, :update], unless: :valid_encoding? do + redirect_to(project_wiki_path(@project, @page)) + end + + def new + redirect_to project_wiki_path(@project, SecureRandom.uuid, random_title: true) + end + + # `#show` handles a number of scenarios: + # + # - If `id` matches a WikiPage, then show the wiki page. + # - If `id` is a file in the wiki repository, then send the file. + # - If we know the user wants to create a new page with the given `id`, + # then display a create form. + # - Otherwise show the empty wiki page and invite the user to create a page. + def show + if @page + show_page + elsif file_blob + show_blob + elsif should_create_missing_page? + create_missing_page + else + render 'missing_page' + end + end + + def edit + end + + def update + @page = WikiPages::UpdateService + .new(@project, current_user, wiki_params) + .execute(@page) + + return saved(:updated) if @page.valid? + + render 'edit' + rescue WikiPage::PageChangedError, WikiPage::PageRenameError, Gitlab::Git::Wiki::OperationError => e + @error = e + render 'edit' + end + + def create + @page = WikiPages::CreateService + .new(@project, current_user, wiki_params) + .execute + + return saved(:created) if @page.persisted? + + render action: "edit" + rescue Gitlab::Git::Wiki::OperationError => e + @page = project_wiki.build_page(wiki_params) + @error = e + + render 'edit' + end + + def history + if @page + @page_versions = Kaminari.paginate_array(@page.versions(page: params[:page].to_i), + total_count: @page.count_versions) + .page(params[:page]) + else + redirect_to( + project_wiki_path(@project, :home), + notice: _("Page not found") + ) + end + end + + def destroy + WikiPages::DestroyService.new(@project, current_user).execute(@page) + + redirect_to project_wiki_path(@project, :home), + status: 302, + notice: _("Page was successfully deleted") + rescue Gitlab::Git::Wiki::OperationError => e + @error = e + render 'edit' + end + + private + + # Callback for PreviewMarkdown + def preview_markdown_params + { pipeline: :wiki, project_wiki: project_wiki, page_slug: params[:id] } + end + + def show_page + set_encoding_error unless valid_encoding? + + @page_dir = @project_wiki.find_dir(@page.directory) if @page.directory.present? + @show_children = true + + render 'show' + end + + def show_blob + send_blob(@project_wiki.repository, file_blob) + end + + def should_create_missing_page? + view_param = @project_wiki.exists? ? 'create' : params[:view] + view_param == 'create' && can?(current_user, :create_wiki, @project) + end + + def create_missing_page + # Assign a title to the WikiPage unless `id` is a randomly generated slug from #new + title = params[:id] unless params[:random_title].present? + + @page = project_wiki.build_page(title: title) + + render 'edit' + end + + def wiki_params + params.require(:wiki_page).permit(:title, :content, :format, :message, :last_commit_sha) + end + + def load_page + @page ||= @project_wiki.find_page(*page_params) + end + + def page_params + keys = [:id] + keys << :version_id if params[:action] == 'show' + + params.values_at(*keys) + end + + def valid_encoding? + strong_memoize(:valid_encoding) do + @page.content.encoding == Encoding::UTF_8 + end + end + + def set_encoding_error + flash.now[:notice] = _("The content of this page is not encoded in UTF-8. Edits can only be made via the Git repository.") + end + + def file_blob + strong_memoize(:file_blob) do + commit = @project_wiki.repository.commit(@project_wiki.default_branch) + + next unless commit + + @project_wiki.repository.blob_at(commit.id, params[:id]) + end + end + + def saved(action) + msg = case action + when :updated + _('Wiki was successfully updated') + when :created + _('Wiki was successfully created') + end + + redirect_to(project_wiki_path(@project, @page), notice: msg) + end +end diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb index b187fdb2723..d531d256721 100644 --- a/app/controllers/projects/wikis_controller.rb +++ b/app/controllers/projects/wikis_controller.rb @@ -1,120 +1,28 @@ # frozen_string_literal: true class Projects::WikisController < Projects::ApplicationController - include PreviewMarkdown - include SendsBlob - include Gitlab::Utils::StrongMemoize + include ProjectWikiActions + include WikiHelper - before_action :authorize_read_wiki! - before_action :authorize_create_wiki!, only: [:edit, :create] - before_action :authorize_admin_wiki!, only: :destroy - before_action :load_project_wiki - before_action :load_page, only: [:show, :edit, :update, :history, :destroy] - before_action :valid_encoding?, - if: -> { %w[show edit update].include?(action_name) && load_page } - before_action only: [:edit, :update], unless: :valid_encoding? do - redirect_to(project_wiki_path(@project, @page)) - end - - def new - redirect_to project_wiki_path(@project, SecureRandom.uuid, random_title: true) + def self.local_prefixes + [controller_path, 'shared/wiki'] end def pages + @nesting = show_children_param + @show_children = @nesting != ProjectWiki::NESTING_CLOSED @wiki_pages = Kaminari.paginate_array( - @project_wiki.list_pages(sort: params[:sort], direction: params[:direction]) + project_wiki.list_pages(**sort_params) ).page(params[:page]) - @wiki_entries = WikiPage.group_by_directory(@wiki_pages) - end - - # `#show` handles a number of scenarios: - # - # - If `id` matches a WikiPage, then show the wiki page. - # - If `id` is a file in the wiki repository, then send the file. - # - If we know the user wants to create a new page with the given `id`, - # then display a create form. - # - Otherwise show the empty wiki page and invite the user to create a page. - def show - if @page - set_encoding_error unless valid_encoding? - - render 'show' - elsif file_blob - send_blob(@project_wiki.repository, file_blob) - elsif show_create_form? - # Assign a title to the WikiPage unless `id` is a randomly generated slug from #new - title = params[:id] unless params[:random_title].present? - - @page = build_page(title: title) - - render 'edit' - else - render 'empty' - end - end - - def edit - end - - def update - return render('empty') unless can?(current_user, :create_wiki, @project) - - @page = WikiPages::UpdateService.new(@project, current_user, wiki_params).execute(@page) - - if @page.valid? - redirect_to( - project_wiki_path(@project, @page), - notice: _('Wiki was successfully updated.') - ) - else - render 'edit' - end - rescue WikiPage::PageChangedError, WikiPage::PageRenameError, Gitlab::Git::Wiki::OperationError => e - @error = e - render 'edit' - end - - def create - @page = WikiPages::CreateService.new(@project, current_user, wiki_params).execute - - if @page.persisted? - redirect_to( - project_wiki_path(@project, @page), - notice: _('Wiki was successfully updated.') - ) - else - render action: "edit" - end - rescue Gitlab::Git::Wiki::OperationError => e - @page = build_page(wiki_params) - @error = e - - render 'edit' - end - - def history - if @page - @page_versions = Kaminari.paginate_array(@page.versions(page: params[:page].to_i), - total_count: @page.count_versions) - .page(params[:page]) - else - redirect_to( - project_wiki_path(@project, :home), - notice: _("Page not found") - ) - end - end - - def destroy - WikiPages::DestroyService.new(@project, current_user).execute(@page) + @wiki_entries = case @nesting + when ProjectWiki::NESTING_FLAT + @wiki_pages + else + WikiDirectory.group_by_directory(@wiki_pages) + end - redirect_to project_wiki_path(@project, :home), - status: 302, - notice: _("Page was successfully deleted") - rescue Gitlab::Git::Wiki::OperationError => e - @error = e - render 'edit' + render 'show' end def git_access @@ -122,74 +30,13 @@ class Projects::WikisController < Projects::ApplicationController private - def show_create_form? - can?(current_user, :create_wiki, @project) && - @page.nil? && - # Always show the create form when the wiki has had at least one page created. - # Otherwise, we only show the form when the user has navigated from - # the 'empty wiki' page - (@project_wiki.exists? || params[:view] == 'create') + def sort_params + process_params(sort_params_config) end - def load_project_wiki - @project_wiki = load_wiki - - # Call #wiki to make sure the Wiki Repo is initialized - @project_wiki.wiki - - @sidebar_page = @project_wiki.find_sidebar(params[:version_id]) - - unless @sidebar_page # Fallback to default sidebar - @sidebar_wiki_entries = WikiPage.group_by_directory(@project_wiki.list_pages(limit: 15)) - end - rescue ProjectWiki::CouldNotCreateWikiError - flash[:notice] = _("Could not create Wiki Repository at this time. Please try again later.") - redirect_to project_path(@project) - false - end - - def load_wiki - ProjectWiki.new(@project, current_user) - end - - def wiki_params - params.require(:wiki).permit(:title, :content, :format, :message, :last_commit_sha) - end - - def build_page(args = {}) - WikiPage.new(@project_wiki).tap do |page| - page.update_attributes(args) # rubocop:disable Rails/ActiveRecordAliases - end - end - - def load_page - @page ||= @project_wiki.find_page(*page_params) - end - - def page_params - keys = [:id] - keys << :version_id if params[:action] == 'show' - - params.values_at(*keys) - end - - def valid_encoding? - strong_memoize(:valid_encoding) do - @page.content.encoding == Encoding::UTF_8 - end - end - - def set_encoding_error - flash.now[:notice] = _("The content of this page is not encoded in UTF-8. Edits can only be made via the Git repository.") - end - - def file_blob - strong_memoize(:file_blob) do - commit = @project_wiki.repository.commit(@project_wiki.default_branch) - - next unless commit + def show_children_param + config = nesting_params_config(params[:sort]) - @project_wiki.repository.blob_at(commit.id, params[:id]) - end + process_params(config) end end diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb index 4f73270577f..66b88f6b626 100644 --- a/app/helpers/icons_helper.rb +++ b/app/helpers/icons_helper.rb @@ -41,6 +41,12 @@ module IconsHelper ActionController::Base.helpers.image_path('file_icons.svg', host: sprite_base_url) end + def sprite_icon_with_text(icon_name, content, opts = {}) + wrapper_class = opts.delete(:wrapper_class) + icon = sprite_icon(icon_name, opts) + content_tag(:span, [icon, content].join('').html_safe, class: wrapper_class) + end + def sprite_icon(icon_name, size: nil, css_class: nil) if Gitlab::Sentry.should_raise_for_dev? unless known_sprites.include?(icon_name) diff --git a/app/helpers/wiki_helper.rb b/app/helpers/wiki_helper.rb index dd8fde2a697..3cf610cbd38 100644 --- a/app/helpers/wiki_helper.rb +++ b/app/helpers/wiki_helper.rb @@ -48,14 +48,23 @@ module WikiHelper expose_url(api_v4_projects_wikis_attachments_path(id: @project.id)) end - def wiki_sort_controls(project, sort, direction) - sort ||= ProjectWiki::TITLE_ORDER - link_class = 'btn btn-default has-tooltip reverse-sort-btn qa-reverse-sort rspec-reverse-sort' - reversed_direction = direction == 'desc' ? 'asc' : 'desc' - icon_class = direction == 'desc' ? 'highest' : 'lowest' - - link_to(project_wikis_pages_path(project, sort: sort, direction: reversed_direction), - type: 'button', class: link_class, title: _('Sort direction')) do + WIKI_SORT_CSS_CLASSES = 'btn btn-default has-tooltip reverse-sort-btn qa-reverse-sort rspec-reverse-sort' + + def wiki_sort_controls(sort_params = {}, &block) + current_sort = sort_params[:sort] || ProjectWiki::TITLE_ORDER + current_direction = (sort_params[:direction] || 'asc').inquiry + + reversed_direction = current_direction.desc? ? 'asc' : 'desc' + icon_class = current_direction.desc? ? 'highest' : 'lowest' + + sorting = sort_params.merge(sort: current_sort, direction: reversed_direction) + opts = { + type: 'button', + class: WIKI_SORT_CSS_CLASSES, + title: _('Sort direction') + } + + link_to(yield(sorting), opts) do sprite_icon("sort-#{icon_class}", size: 16) end end @@ -67,4 +76,86 @@ module WikiHelper s_("Wiki|Title") end end + + # Render the sprite icon given the current show_children state + def wiki_show_children_icon(nesting) + icon_name, icon_text = + case nesting + when ProjectWiki::NESTING_TREE + ['folder-open', s_("Wiki|Show folder contents")] + when ProjectWiki::NESTING_CLOSED + ['folder-o', s_("Wiki|Hide folder contents")] + else + ['list-bulleted', s_("Wiki|Show files separately")] + end + + sprite_icon_with_text(icon_name, icon_text, size: 16) + end + + def wiki_page_link(wiki_page, nesting, project) + link = link_to(wiki_page.title, + project_wiki_path(project, wiki_page), + class: 'wiki-page-title') + + case nesting + when ProjectWiki::NESTING_FLAT + tags = [] + if wiki_page.directory.present? + wiki_dir = WikiDirectory.new(wiki_page.directory) + tags << link_to(wiki_dir.slug, project_wiki_dir_path(project, wiki_dir), class: 'wiki-page-dir-name') + tags << content_tag(:span, '/', class: 'wiki-page-name-separator') + end + + tags << link + tags.join.html_safe + else + link + end + end + + def sort_params_config + { + keys: [:sort, :direction], + defaults: { + sort: ProjectWiki::TITLE_ORDER, direction: ProjectWiki::DIRECTION_ASC + }, + allowed: { + sort: ProjectWiki::SORT_ORDERS, direction: ProjectWiki::SORT_DIRECTIONS + } + } + end + + def nesting_params_config(sort_key) + default_val = case sort_key + when ProjectWiki::CREATED_AT_ORDER + ProjectWiki::NESTING_FLAT + else + ProjectWiki::NESTING_CLOSED + end + { + keys: [:show_children], + defaults: { show_children: default_val }, + allowed: { show_children: ProjectWiki::NESTINGS } + } + end + + def process_params(config) + unprocessed = params.permit(*config[:keys]) + + processed = unprocessed + .with_defaults(config[:defaults]) + .tap { |h| Gitlab::Utils.allow_hash_values(h, config[:allowed]) } + .to_hash + .transform_keys(&:to_sym) + + if processed.keys == config[:keys] + processed.size == 1 ? processed.values.first : processed + else + raise ActionController::BadRequest, "illegal parameters: #{unprocessed}" + end + end + + def home_page? + params[:id] == 'home' + end end diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb index bb222ac7629..e0fcebf2642 100644 --- a/app/models/project_wiki.rb +++ b/app/models/project_wiki.rb @@ -17,6 +17,13 @@ class ProjectWiki CREATED_AT_ORDER = 'created_at' DIRECTION_DESC = 'desc' DIRECTION_ASC = 'asc' + SORT_ORDERS = [TITLE_ORDER, CREATED_AT_ORDER].freeze + SORT_DIRECTIONS = [DIRECTION_ASC, DIRECTION_DESC].freeze + + NESTING_FLAT = 'flat' + NESTING_TREE = 'tree' + NESTING_CLOSED = 'hidden' + NESTINGS = [NESTING_TREE, NESTING_CLOSED, NESTING_FLAT].freeze # Returns a string describing what went wrong after # an operation fails. @@ -58,7 +65,11 @@ class ProjectWiki end def wiki_base_path - [Gitlab.config.gitlab.relative_url_root, '/', @project.full_path, '/wikis'].join('') + ::File.join(project_base_path, 'wikis') + end + + def wiki_page_path + ::File.join(project_base_path, '-', 'wiki_pages') end # Returns the Gitlab::Git::Wiki object. @@ -125,6 +136,23 @@ class ProjectWiki end end + # Finds directory within the repository based on a slug + # + # dir_name - The directory prefix. + # + # Returns an initialized WikiDirectory instance or nil + def find_dir(dir_name, sort = nil, direction = DIRECTION_ASC) + descending = direction == DIRECTION_DESC + # WikiListPagesRequest currently does not support server-side + # filtering. Ideally this logic should be moved to the gitaly + # side. + pages = wiki + .list_pages(sort: sort, direction_desc: descending) + .map { |page| WikiPage.new(self, page, true) } + .select { |wp| wp.directory == dir_name } + WikiDirectory.new(dir_name, pages) if pages.present? + end + def find_sidebar(version = nil) find_page(SIDEBAR, version) end @@ -144,6 +172,12 @@ class ProjectWiki false end + def build_page(attrs) + WikiPage.new(self).tap do |page| + page.update_attributes(attrs) # rubocop:disable Rails/ActiveRecordAliases + end + end + def update_page(page, content:, title: nil, format: :markdown, message: nil) commit = commit_details(:updated, message, page.title) @@ -171,7 +205,7 @@ class ProjectWiki title_array = title.split("/") title = title_array.pop - [title, title_array.join("/")] + [title, ::File.join(title_array)] end def repository @@ -198,6 +232,10 @@ class ProjectWiki private + def project_base_path + ::File.join(Gitlab.config.gitlab.relative_url_root, @project.full_path) + end + def create_repo!(raw_repository) gitlab_shell.create_wiki_repository(project) diff --git a/app/models/wiki_directory.rb b/app/models/wiki_directory.rb index 712ba79bbd2..6c86c11e7fb 100644 --- a/app/models/wiki_directory.rb +++ b/app/models/wiki_directory.rb @@ -1,20 +1,75 @@ # frozen_string_literal: true class WikiDirectory + include StaticModel include ActiveModel::Validations attr_accessor :slug, :pages validates :slug, presence: true + # StaticModel overrides and configuration: + + def self.primary_key + 'slug' + end + + def id + "#{slug}@#{last_version&.sha}" + end + + def self.model_name + ActiveModel::Name.new(self, nil, 'wiki_dir') + end + + alias_method :to_param, :slug + alias_method :title, :slug + + # Sorts and groups pages by directory. + # + # pages - an array of WikiPage objects. + # + # Returns an array of WikiPage and WikiDirectory objects. + # The entries are sorted in the order of the input array, where + # directories appear in the position of their first member. + def self.group_by_directory(pages) + grouped = [] + dirs = Hash.new do |h, k| + new(k).tap { |dir| grouped << (h[k] = dir) } + end + + Array.wrap(pages).each_with_object(grouped) do |page, top_level| + group = page.directory.present? ? dirs[page.directory] : top_level + + group << page + end + end + def initialize(slug, pages = []) @slug = slug @pages = pages end - # Relative path to the partial to be used when rendering collections - # of this object. - def to_partial_path - 'projects/wikis/wiki_directory' + def <<(page) + @pages << page + @last_version = nil + end + + def last_version + @last_version ||= @pages.map(&:last_version).max_by(&:authored_date) + end + + def page_count + @pages.size + end + + def empty? + page_count.zero? + end + + def to_partial_path(context = nil) + name = [context, 'wiki_directory'].compact.join('_') + + "projects/wiki_directories/#{name}" end end diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb index 1fa29e5b933..6b3cb0b39d8 100644 --- a/app/models/wiki_page.rb +++ b/app/models/wiki_page.rb @@ -15,30 +15,7 @@ class WikiPage end def self.model_name - ActiveModel::Name.new(self, nil, 'wiki') - end - - # Sorts and groups pages by directory. - # - # pages - an array of WikiPage objects. - # - # Returns an array of WikiPage and WikiDirectory objects. The entries are - # sorted by alphabetical order (directories and pages inside each directory). - # Pages at the root level come before everything. - def self.group_by_directory(pages) - return [] if pages.blank? - - pages.each_with_object([]) do |page, grouped_pages| - next grouped_pages << page unless page.directory.present? - - directory = grouped_pages.find do |obj| - obj.is_a?(WikiDirectory) && obj.slug == page.directory - end - - next directory.pages << page if directory - - grouped_pages << WikiDirectory.new(page.directory, [page]) - end + ActiveModel::Name.new(self, nil, 'wiki_page') end def self.unhyphenize(name) @@ -66,6 +43,16 @@ class WikiPage Gitlab::HookData::WikiPageBuilder.new(self).build end + # Create a new WikiPage + # + # == Parameters: + # wiki:: + # A `ProjectWiki` model object + # page:: + # A `Gitlab::Git::WikiPage` business object, to which this class provides a facade + # persisted:: + # Is this page fully saved on disk? + # def initialize(wiki, page = nil, persisted = false) @wiki = wiki @page = page @@ -250,10 +237,10 @@ class WikiPage end end - # Relative path to the partial to be used when rendering collections - # of this object. - def to_partial_path - 'projects/wikis/wiki_page' + def to_partial_path(context = nil) + name = [context, 'wiki_page'].compact.join('_') + + "projects/wiki_pages/#{name}" end def id diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml index c84bc0b5cd4..ec27f3c24df 100644 --- a/app/views/layouts/nav/sidebar/_project.html.haml +++ b/app/views/layouts/nav/sidebar/_project.html.haml @@ -282,14 +282,14 @@ - if project_nav_tab? :wiki - wiki_url = project_wiki_path(@project, :home) - = nav_link(controller: :wikis) do + = nav_link(controller: [:wikis, :wiki_pages, :wiki_directories]) do = link_to wiki_url, class: 'shortcuts-wiki', data: { qa_selector: 'wiki_link' } do .nav-icon-container = sprite_icon('book') %span.nav-item-name = _('Wiki') %ul.sidebar-sub-level-items.is-fly-out-only - = nav_link(controller: :wikis, html_options: { class: "fly-out-top-item" } ) do + = nav_link(controller: [:wikis, :wiki_pages, :wiki_directories], html_options: { class: "fly-out-top-item" } ) do = link_to wiki_url do %strong.fly-out-top-item-name = _('Wiki') diff --git a/app/views/projects/wiki_directories/_pages_wiki_directory.html.haml b/app/views/projects/wiki_directories/_pages_wiki_directory.html.haml new file mode 100644 index 00000000000..a9d2f38da88 --- /dev/null +++ b/app/views/projects/wiki_directories/_pages_wiki_directory.html.haml @@ -0,0 +1,15 @@ +%li + %span.text-secondary-500.svg-icon.svg-baseline + - if @show_children + = sprite_icon('folder-open', size: 16) + - else + = sprite_icon('folder-o', size: 16) + + = link_to wiki_dir.slug, project_wiki_dir_path(@project, wiki_dir) + - unless @show_children + %span.badge.badge-pill.wiki-dir-page-count= wiki_dir.page_count + .float-right + %small= (s_("Last edited %{date}") % { date: time_ago_with_tooltip(wiki_dir.last_version.authored_date) }).html_safe + - if @show_children + %ul + = render wiki_dir.pages, context: context diff --git a/app/views/projects/wiki_directories/_sidebar_wiki_directory.html.haml b/app/views/projects/wiki_directories/_sidebar_wiki_directory.html.haml new file mode 100644 index 00000000000..2f62e9d4516 --- /dev/null +++ b/app/views/projects/wiki_directories/_sidebar_wiki_directory.html.haml @@ -0,0 +1,7 @@ +%li + %span.text-secondary-300.svg-icon.svg-baseline + = sprite_icon('folder-open', size: 16) + + = link_to wiki_dir.slug, project_wiki_dir_path(@project, wiki_dir) + %ul= render wiki_dir.pages, context: context + diff --git a/app/views/projects/wiki_directories/_wiki_directory.html.haml b/app/views/projects/wiki_directories/_wiki_directory.html.haml new file mode 100644 index 00000000000..022c209c5bd --- /dev/null +++ b/app/views/projects/wiki_directories/_wiki_directory.html.haml @@ -0,0 +1 @@ += render wiki_directory.to_partial_path(context), wiki_dir: wiki_directory, context: context diff --git a/app/views/projects/wiki_directories/empty.html.haml b/app/views/projects/wiki_directories/empty.html.haml new file mode 100644 index 00000000000..cab1c3acb60 --- /dev/null +++ b/app/views/projects/wiki_directories/empty.html.haml @@ -0,0 +1,34 @@ +- layout_path = 'shared/empty_states/wikis_layout' +- add_to_breadcrumbs _("Wiki"), project_wiki_path(@project, :home) +- add_to_breadcrumbs s_("Wiki|Pages"), project_wikis_pages_path(@project) +- breadcrumb_title s_(@wiki_dir.slug) +- page_title @wiki_dir.slug + +- if can?(current_user, :create_wiki, @project) + - create_path = project_wiki_path(@project, params[:id], { view: 'create', params: { title: "#{params[:id]}/" } }) + - create_link = link_to s_('WikiDirEmpty|Create a page in this directory'), create_path, class: 'btn btn-success qa-create-first-page-link', title: s_('WikiDirEmpty|Create a page') + + = render layout: layout_path, locals: { image_path: 'illustrations/wiki_login_empty.svg' } do + %h4.text-left + = s_('WikiDirEmpty|This directory has no wiki pages') + %p.text-left + = s_("WikiDirEmpty|A wiki is where you can store all the details about your project. This can include why you've created it, its principles, how to use it, and so on.") + = create_link + +- elsif can?(current_user, :read_issue, @project) + - issues_link = link_to s_('WikiEmptyIssueMessage|issue tracker'), project_issues_path(@project) + - new_issue_link = link_to s_('WikiEmpty|Suggest wiki improvement'), new_project_issue_path(@project), class: 'btn btn-success', title: s_('WikiEmptyIssueMessage|Suggest wiki improvement') + + = render layout: layout_path, locals: { image_path: 'illustrations/wiki_logout_empty.svg' } do + %h4 + = s_('WikiDirEmpty|This directory has no wiki pages') + %p.text-left + = s_('WikiEmptyIssueMessage|You must be a project member in order to add wiki pages. If you have suggestions for how to improve the wiki for this project, consider opening an issue in the %{issues_link}.').html_safe % { issues_link: issues_link } + = new_issue_link + +- else + = render layout: layout_path, locals: { image_path: 'illustrations/wiki_logout_empty.svg' } do + %h4 + = s_('WikiDirEmpty|This directory has no wiki pages') + %p + = s_('WikiEmpty|You must be a project member in order to add wiki pages.') diff --git a/app/views/projects/wiki_directories/show.html.haml b/app/views/projects/wiki_directories/show.html.haml new file mode 100644 index 00000000000..4c7978d1216 --- /dev/null +++ b/app/views/projects/wiki_directories/show.html.haml @@ -0,0 +1,6 @@ +- add_to_breadcrumbs _("Wiki"), project_wiki_path(@project, :home) +- add_to_breadcrumbs s_("Wiki|Pages"), project_wikis_pages_path(@project) +- breadcrumb_title s_(@wiki_dir.slug) +- page_title @wiki_dir.slug + += render 'page_listing', { allow_change_nesting: false, wiki_page_title: page_title, page_path: ->(opts) { project_wiki_dir_path(@project, @wiki_dir, opts) } } diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wiki_pages/_form.html.haml index a153f527ee0..8f5757d6d98 100644 --- a/app/views/projects/wikis/_form.html.haml +++ b/app/views/projects/wiki_pages/_form.html.haml @@ -1,9 +1,11 @@ - form_classes = 'wiki-form common-note-form prepend-top-default js-quick-submit' - form_classes += ' js-new-wiki-page' unless @page.persisted? -= form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post, - html: { class: form_classes }, - data: { uploads_path: uploads_path } do |f| += form_for [@project.namespace.becomes(Namespace), @project, @page], + method: @page.persisted? ? 'put' : 'post', + url: { controller: 'wiki_pages', action: @page.persisted? ? :update : :create }, + html: { class: form_classes }, + data: { uploads_path: uploads_path } do |f| = form_errors(@page) - if @page.persisted? @@ -12,7 +14,7 @@ .form-group.row .col-sm-12= f.label :title, class: 'control-label-full-width' .col-sm-12 - = f.text_field :title, class: 'form-control qa-wiki-title-textbox', value: @page.title, required: true, autofocus: !@page.persisted?, placeholder: _('Wiki|Page title') + = f.text_field :title, class: 'form-control qa-wiki-title-textbox', value: @page.title, required: true, autofocus: !@page.persisted?, placeholder: s_('Wiki|Page title') %span.d-inline-block.mw-100.prepend-top-5 = icon('lightbulb-o') - if @page.persisted? diff --git a/app/views/projects/wiki_pages/_page_title.html.haml b/app/views/projects/wiki_pages/_page_title.html.haml new file mode 100644 index 00000000000..a3b077999e6 --- /dev/null +++ b/app/views/projects/wiki_pages/_page_title.html.haml @@ -0,0 +1,4 @@ += link_to @page.human_title, project_wiki_path(@project, @page) +%span.light + = _('·').html_safe + = subtitle diff --git a/app/views/projects/wiki_pages/_pages_wiki_page.html.haml b/app/views/projects/wiki_pages/_pages_wiki_page.html.haml new file mode 100644 index 00000000000..c177d03bee1 --- /dev/null +++ b/app/views/projects/wiki_pages/_pages_wiki_page.html.haml @@ -0,0 +1,8 @@ +%li + %span.text-secondary-500.svg-icon.svg-baseline= sprite_icon('book', size: 16) + = wiki_page_link(wiki_page, @nesting, @project) + .float-right + %span.badge.badge-pill.wiki-page-format= _(wiki_page.format) + - if wiki_page.last_version + = '/' + %small= (s_("Last edited %{date}") % { date: time_ago_with_tooltip(wiki_page.last_version.authored_date) }).html_safe diff --git a/app/views/projects/wiki_pages/_sidebar_wiki_page.html.haml b/app/views/projects/wiki_pages/_sidebar_wiki_page.html.haml new file mode 100644 index 00000000000..205c3dd76bf --- /dev/null +++ b/app/views/projects/wiki_pages/_sidebar_wiki_page.html.haml @@ -0,0 +1,11 @@ +- is_active = params[:id] == wiki_page.slug +- icon_active_class = is_active ? 'text-secondary-800' : 'text-secondary-300' + +%li{ class: active_when(is_active) } + %span.svg-icon.svg-baseline{ class: icon_active_class } + = sprite_icon('book', size: 16) + - if is_active + = wiki_page.human_title + - else + = link_to project_wiki_path(@project, wiki_page) do + = wiki_page.human_title diff --git a/app/views/projects/wiki_pages/_wiki_page.html.haml b/app/views/projects/wiki_pages/_wiki_page.html.haml new file mode 100644 index 00000000000..947e96fed6b --- /dev/null +++ b/app/views/projects/wiki_pages/_wiki_page.html.haml @@ -0,0 +1 @@ += render wiki_page.to_partial_path(context), wiki_page: wiki_page diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wiki_pages/edit.html.haml index 9ccf5acfefc..4e5f6a077a4 100644 --- a/app/views/projects/wikis/edit.html.haml +++ b/app/views/projects/wiki_pages/edit.html.haml @@ -1,5 +1,10 @@ -- @content_class = "limit-container-width" unless fluid_layout -- add_to_breadcrumbs _("Wiki"), project_wiki_path(@project, @page) +- @content_class = 'edit-wiki-page' + (fluid_layout ? '' : ' limit-container-width') +- add_to_breadcrumbs _("Wiki"), project_wiki_path(@project, :home) +- add_to_breadcrumbs s_("Wiki|Pages"), project_wikis_pages_path(@project) +- if @page.persisted? && @page_dir.present? + - add_to_breadcrumbs _(@page_dir.slug), project_wiki_dir_path(@project, @page_dir) +- if @page.persisted? + - add_to_breadcrumbs @page.human_title, project_wiki_path(@project, @page) - breadcrumb_title @page.persisted? ? _("Edit") : _("New") - page_title @page.persisted? ? _("Edit") : _("New"), @page.human_title, _("Wiki") @@ -12,10 +17,7 @@ .nav-text %h2.wiki-page-title - if @page.persisted? - = link_to @page.human_title, project_wiki_path(@project, @page) - %span.light - · - = s_("Wiki|Edit Page") + = render partial: 'page_title', locals: { subtitle: s_("Wiki|Edit Page") } - else = s_("Wiki|Create New Page") diff --git a/app/views/projects/wikis/history.html.haml b/app/views/projects/wiki_pages/history.html.haml index d3a55c53649..d60a32750df 100644 --- a/app/views/projects/wikis/history.html.haml +++ b/app/views/projects/wiki_pages/history.html.haml @@ -1,3 +1,4 @@ +- @content_class = 'wiki-history' - page_title _("History"), @page.human_title, _("Wiki") .wiki-page-header.top-area.has-sidebar-toggle.flex-column.flex-lg-row @@ -6,10 +7,7 @@ .nav-text %h2.wiki-page-title - = link_to @page.human_title, project_wiki_path(@project, @page) - %span.light - · - = _("History") + = render partial: 'page_title', locals: { subtitle: _("History") } .table-holder %table.table @@ -39,4 +37,4 @@ = version.format = paginate @page_versions, theme: 'gitlab' -= render 'sidebar' += render 'shared/wiki/sidebar' diff --git a/app/views/projects/wikis/empty.html.haml b/app/views/projects/wiki_pages/missing_page.html.haml index 62fa6e1907b..62fa6e1907b 100644 --- a/app/views/projects/wikis/empty.html.haml +++ b/app/views/projects/wiki_pages/missing_page.html.haml diff --git a/app/views/projects/wiki_pages/show.html.haml b/app/views/projects/wiki_pages/show.html.haml new file mode 100644 index 00000000000..879ddfb03c8 --- /dev/null +++ b/app/views/projects/wiki_pages/show.html.haml @@ -0,0 +1,34 @@ +- @content_class = 'wiki-page' + (fluid_layout ? '' : ' limit-container-width') +- breadcrumb_title @page.human_title +- page_title @page.human_title, _("Wiki") +- add_to_breadcrumbs _("Wiki"), project_wiki_path(@project, :home) +- add_to_breadcrumbs s_("Wiki|Pages"), project_wikis_pages_path(@project) +- if @page_dir.present? + - add_to_breadcrumbs _(@page_dir.slug), project_wiki_dir_path(@project, @page_dir) + +.wiki-page-header.top-area.has-sidebar-toggle.flex-column.flex-lg-row + %button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" } + = icon('angle-double-left') + + .nav-text.flex-fill + %h2.wiki-page-title= @page.human_title + %span.wiki-last-edit-by + - if @page.last_version + = (_("Last edited by %{name}") % { name: "<strong>#{@page.last_version.author_name}</strong>" }).html_safe + #{time_ago_with_tooltip(@page.last_version.authored_date)} + + .nav-controls.pb-md-3.pb-lg-0 + = render 'main_links' + +- if @page.historical? + .warning_message + = s_("WikiHistoricalPage|This is an old version of this page.") + - most_recent_link = link_to s_("WikiHistoricalPage|most recent version"), project_wiki_path(@project, @page) + - history_link = link_to s_("WikiHistoricalPage|history"), project_wiki_history_path(@project, @page) + = (s_("WikiHistoricalPage|You can view the %{most_recent_link} or browse the %{history_link}.") % { most_recent_link: most_recent_link, history_link: history_link }).html_safe + +.prepend-top-default.append-bottom-default + .md.md-file.qa-wiki-page-content + = render_wiki_content(@page) + += render 'sidebar' diff --git a/app/views/projects/wikis/_pages_wiki_page.html.haml b/app/views/projects/wikis/_pages_wiki_page.html.haml deleted file mode 100644 index c156f8cbf50..00000000000 --- a/app/views/projects/wikis/_pages_wiki_page.html.haml +++ /dev/null @@ -1,6 +0,0 @@ -%li - = link_to wiki_page.title, project_wiki_path(@project, wiki_page) - %small (#{wiki_page.format}) - .float-right - - if wiki_page.last_version - %small= (s_("Last edited %{date}") % { date: time_ago_with_tooltip(wiki_page.last_version.authored_date) }).html_safe diff --git a/app/views/projects/wikis/_sidebar_wiki_page.html.haml b/app/views/projects/wikis/_sidebar_wiki_page.html.haml deleted file mode 100644 index 769d869bd53..00000000000 --- a/app/views/projects/wikis/_sidebar_wiki_page.html.haml +++ /dev/null @@ -1,3 +0,0 @@ -%li{ class: active_when(params[:id] == wiki_page.slug) } - = link_to project_wiki_path(@project, wiki_page) do - = wiki_page.human_title diff --git a/app/views/projects/wikis/_wiki_directory.html.haml b/app/views/projects/wikis/_wiki_directory.html.haml deleted file mode 100644 index 0e5f32ed859..00000000000 --- a/app/views/projects/wikis/_wiki_directory.html.haml +++ /dev/null @@ -1,4 +0,0 @@ -%li - = wiki_directory.slug - %ul - = render wiki_directory.pages, context: context diff --git a/app/views/projects/wikis/_wiki_page.html.haml b/app/views/projects/wikis/_wiki_page.html.haml deleted file mode 100644 index c84d06dad02..00000000000 --- a/app/views/projects/wikis/_wiki_page.html.haml +++ /dev/null @@ -1 +0,0 @@ -= render "#{context}_wiki_page", wiki_page: wiki_page diff --git a/app/views/projects/wikis/pages.html.haml b/app/views/projects/wikis/pages.html.haml deleted file mode 100644 index d9dcd8f9acd..00000000000 --- a/app/views/projects/wikis/pages.html.haml +++ /dev/null @@ -1,32 +0,0 @@ -- add_to_breadcrumbs "Wiki", project_wiki_path(@project, :home) -- breadcrumb_title s_("Wiki|Pages") -- page_title s_("Wiki|Pages"), _("Wiki") -- sort_title = wiki_sort_title(params[:sort]) - -.wiki-page-header.top-area.flex-column.flex-lg-row - - .nav-text.flex-fill - %h2.wiki-page-title - = s_("Wiki|Wiki Pages") - - .nav-controls.pb-md-3.pb-lg-0 - = link_to project_wikis_git_access_path(@project), class: 'btn' do - = icon('cloud-download') - = _("Clone repository") - - .dropdown.inline.wiki-sort-dropdown - .btn-group{ role: 'group' } - .btn-group{ role: 'group' } - %button.dropdown-toggle{ type: 'button', data: { toggle: 'dropdown', display: 'static' }, class: 'btn btn-default' } - = sort_title - = icon('chevron-down') - %ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable.dropdown-menu-sort - %li - = sortable_item(s_("Wiki|Title"), project_wikis_pages_path(@project, sort: ProjectWiki::TITLE_ORDER), sort_title) - = sortable_item(s_("Wiki|Created date"), project_wikis_pages_path(@project, sort: ProjectWiki::CREATED_AT_ORDER), sort_title) - = wiki_sort_controls(@project, params[:sort], params[:direction]) - -%ul.wiki-pages-list.content-list - = render @wiki_entries, context: 'pages' - -= paginate @wiki_pages, theme: 'gitlab' diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml index ebd99cf8605..9c732a933cf 100644 --- a/app/views/projects/wikis/show.html.haml +++ b/app/views/projects/wikis/show.html.haml @@ -1,32 +1,5 @@ -- @content_class = "limit-container-width" unless fluid_layout -- breadcrumb_title @page.human_title -- wiki_breadcrumb_dropdown_links(@page.slug) -- page_title @page.human_title, _("Wiki") -- add_to_breadcrumbs _("Wiki"), project_wiki_path(@project, :home) +- add_to_breadcrumbs "Wiki", project_wiki_path(@project, :home) +- breadcrumb_title s_("Wiki|Pages") +- page_title s_("Wiki|Contents"), _("Wiki") -.wiki-page-header.top-area.has-sidebar-toggle.flex-column.flex-lg-row - %button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" } - = icon('angle-double-left') - - .nav-text.flex-fill - %h2.wiki-page-title= @page.human_title - %span.wiki-last-edit-by - - if @page.last_version - = (_("Last edited by %{name}") % { name: "<strong>#{@page.last_version.author_name}</strong>" }).html_safe - #{time_ago_with_tooltip(@page.last_version.authored_date)} - - .nav-controls.pb-md-3.pb-lg-0 - = render 'main_links' - -- if @page.historical? - .warning_message - = s_("WikiHistoricalPage|This is an old version of this page.") - - most_recent_link = link_to s_("WikiHistoricalPage|most recent version"), project_wiki_path(@project, @page) - - history_link = link_to s_("WikiHistoricalPage|history"), project_wiki_history_path(@project, @page) - = (s_("WikiHistoricalPage|You can view the %{most_recent_link} or browse the %{history_link}.") % { most_recent_link: most_recent_link, history_link: history_link }).html_safe - -.prepend-top-default.append-bottom-default - .md.md-file{ data: { qa_selector: 'wiki_page_content' } } - = render_wiki_content(@page) - -= render 'sidebar' += render 'page_listing', { allow_change_nesting: ::Feature.enabled?(:wikis_allow_change_nesting), wiki_page_title: page_title, page_path: ->(opts) { project_wikis_pages_path(@project, opts) } } diff --git a/app/views/shared/empty_states/_wikis.html.haml b/app/views/shared/empty_states/_wikis.html.haml index 73eedcc1dc9..e05230de457 100644 --- a/app/views/shared/empty_states/_wikis.html.haml +++ b/app/views/shared/empty_states/_wikis.html.haml @@ -1,8 +1,11 @@ - layout_path = 'shared/empty_states/wikis_layout' +- wiki_is_empty = @project_wiki.empty? +- empty_msg = wiki_is_empty ? s_('WikiEmpty|This project has no wiki pages') : s_('WikiEmpty|This page does not exist') +- create_msg = wiki_is_empty ? s_('WikiEmpty|Create your first page') : s_('WikiEmpty|Create this page') - if can?(current_user, :create_wiki, @project) - create_path = project_wiki_path(@project, params[:id], { view: 'create' }) - - create_link = link_to s_('WikiEmpty|Create your first page'), create_path, class: 'btn btn-success qa-create-first-page-link', title: s_('WikiEmpty|Create your first page') + - create_link = link_to create_msg, create_path, class: 'btn btn-success qa-create-first-page-link', title: create_msg = render layout: layout_path, locals: { image_path: 'illustrations/wiki_login_empty.svg' } do %h4.text-left @@ -17,7 +20,7 @@ = render layout: layout_path, locals: { image_path: 'illustrations/wiki_logout_empty.svg' } do %h4 - = s_('WikiEmpty|This project has no wiki pages') + = empty_msg %p.text-left = s_('WikiEmptyIssueMessage|You must be a project member in order to add wiki pages. If you have suggestions for how to improve the wiki for this project, consider opening an issue in the %{issues_link}.').html_safe % { issues_link: issues_link } = new_issue_link @@ -25,6 +28,6 @@ - else = render layout: layout_path, locals: { image_path: 'illustrations/wiki_logout_empty.svg' } do %h4 - = s_('WikiEmpty|This project has no wiki pages') + = empty_msg %p = s_('WikiEmpty|You must be a project member in order to add wiki pages.') diff --git a/app/views/projects/wikis/_main_links.html.haml b/app/views/shared/wiki/_main_links.html.haml index 2e1e176c42a..5e41bb6a9cd 100644 --- a/app/views/projects/wikis/_main_links.html.haml +++ b/app/views/shared/wiki/_main_links.html.haml @@ -1,6 +1,6 @@ - if (@page && @page.persisted?) - if can?(current_user, :create_wiki, @project) - = link_to project_wikis_new_path(@project), class: "add-new-wiki btn btn-success", role: "button" do + = link_to project_wiki_pages_new_path(@project), class: "add-new-wiki btn btn-success", role: "button" do = s_("Wiki|New page") = link_to project_wiki_history_path(@project, @page), class: "btn", role: "button" do = s_("Wiki|Page history") diff --git a/app/views/shared/wiki/_page_listing.html.haml b/app/views/shared/wiki/_page_listing.html.haml new file mode 100644 index 00000000000..80f3071a8b2 --- /dev/null +++ b/app/views/shared/wiki/_page_listing.html.haml @@ -0,0 +1,45 @@ +- @no_container = true +- current_sorting = params.permit(:sort, :direction) +- sort_title = wiki_sort_title(params[:sort]) + +%div{ class: container_class } + .wiki-page-header.top-area.flex-column.flex-lg-row + + .nav-text.flex-fill + %h2.wiki-page-title + = wiki_page_title + + .nav-controls.pb-md-3.pb-lg-0 + - if can?(current_user, :create_wiki, @project) + = link_to project_wiki_pages_new_path(@project), class: "add-new-wiki btn btn-success" do + = s_("Wiki|New page") + + = link_to project_wikis_git_access_path(@project), class: 'btn qa-clone-repository-link' do + = sprite_icon('download', size: 16) + = _("Clone repository") + + - if @nesting.present? && allow_change_nesting + .dropdown.inline.wiki-nesting-dropdown + .btn-group{ role: 'group' } + %button.dropdown-toggle{ type: 'button', data: { toggle: 'dropdown', display: 'static' }, class: 'btn btn-default' } + = wiki_show_children_icon(@nesting) + = sprite_icon('chevron-down', size: 16) + %ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable.dropdown-menu-sort + - ProjectWiki::NESTINGS.each do |choice| + %li= link_to wiki_show_children_icon(choice), page_path.call(current_sorting.merge(show_children: choice)), class: @nesting == choice ? 'is-active' : '' + + .dropdown.inline.wiki-sort-dropdown + .btn-group{ role: 'group' } + %button.dropdown-toggle{ type: 'button', data: { toggle: 'dropdown', display: 'static' }, class: 'btn btn-default' } + = sort_title + = sprite_icon('chevron-down', size: 16) + %ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable.dropdown-menu-sort + %li + = sortable_item(s_("Wiki|Title"), page_path.call(sort: ProjectWiki::TITLE_ORDER), sort_title) + = sortable_item(s_("Wiki|Created date"), page_path.call(sort: ProjectWiki::CREATED_AT_ORDER), sort_title) + = wiki_sort_controls(current_sorting.merge(show_children: @nesting), &page_path) + + %ul.wiki-pages-list.content-list + = render @wiki_entries, context: 'pages' + + = paginate @wiki_pages, theme: 'gitlab' diff --git a/app/views/projects/wikis/_sidebar.html.haml b/app/views/shared/wiki/_sidebar.html.haml index 83d145444d8..83d145444d8 100644 --- a/app/views/projects/wikis/_sidebar.html.haml +++ b/app/views/shared/wiki/_sidebar.html.haml diff --git a/changelogs/unreleased/sort-wiki-pages-by-date.yml b/changelogs/unreleased/sort-wiki-pages-by-date.yml new file mode 100644 index 00000000000..be5238fd6c3 --- /dev/null +++ b/changelogs/unreleased/sort-wiki-pages-by-date.yml @@ -0,0 +1,4 @@ +--- +title: Sort wiki pages by date +merge_request: 30245 +type: added diff --git a/config/routes/project.rb b/config/routes/project.rb index 056289a72db..3dd34083c44 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -615,7 +615,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do end # Since both wiki and repository routing contains wildcard characters - # its preferable to keep it below all other project routes + # its preferable to keep them below all other project routes draw :wiki draw :repository diff --git a/config/routes/wiki.rb b/config/routes/wiki.rb index d439c99270e..3e71c5eb924 100644 --- a/config/routes/wiki.rb +++ b/config/routes/wiki.rb @@ -1,13 +1,21 @@ scope(controller: :wikis) do + scope(path: 'wikis/pages', as: :wiki_pages, format: false) do + get :new, to: 'wiki_pages#new' + post '/', to: 'wiki_pages#create' + end + scope(path: 'wikis', as: :wikis) do get :git_access get :pages - get :new - get '/', to: redirect('%{namespace_id}/%{project_id}/wikis/home') - post '/', to: 'wikis#create' + get '/', to: redirect('%{namespace_id}/%{project_id}/-/wiki_pages/home') + get '/*id', to: redirect('%{namespace_id}/%{project_id}/-/wiki_pages/%{id}') + end + + scope(path: '-/wiki_pages', as: :wiki_page, format: false) do + post '/', to: 'wiki_pages#create' end - scope(path: 'wikis/*id', as: :wiki, format: false) do + scope(path: '-/wiki_pages/*id', as: :wiki, format: false, controller: :wiki_pages) do get :edit get :history post :preview_markdown @@ -15,4 +23,8 @@ scope(controller: :wikis) do put '/', action: :update delete '/', action: :destroy end + + scope(path: '-/wiki_dirs/*id', as: :wiki_dir, format: false, controller: :wiki_directories) do + get '/', action: :show + end end diff --git a/doc/user/index.md b/doc/user/index.md index ee5d4a0a07b..37b99eb7413 100644 --- a/doc/user/index.md +++ b/doc/user/index.md @@ -130,7 +130,7 @@ gather feedback through [resolvable threads](discussions/index.md#resolvable-com Read through the [GFM documentation](markdown.md) to learn how to apply the best of GitLab Flavored Markdown in your threads, comments, -issues and merge requests descriptions, and everywhere else GMF is +issues and merge requests descriptions, and everywhere else GFM is supported. ## Todos diff --git a/lib/banzai/filter/wiki_link_filter/rewriter.rb b/lib/banzai/filter/wiki_link_filter/rewriter.rb index f4cc8beeb52..e346b03754b 100644 --- a/lib/banzai/filter/wiki_link_filter/rewriter.rb +++ b/lib/banzai/filter/wiki_link_filter/rewriter.rb @@ -6,7 +6,7 @@ module Banzai class Rewriter def initialize(link_string, wiki:, slug:) @uri = Addressable::URI.parse(link_string) - @wiki_base_path = wiki && wiki.wiki_base_path + @wiki_base_path = wiki && wiki.wiki_page_path @slug = slug end diff --git a/lib/gitlab/utils.rb b/lib/gitlab/utils.rb index 7fbfc4c45c4..5cba6c52773 100644 --- a/lib/gitlab/utils.rb +++ b/lib/gitlab/utils.rb @@ -130,5 +130,15 @@ module Gitlab IPAddr.new(str) rescue IPAddr::InvalidAddressError end + + # Filter a Hash against a mapping of keys to sets of allowed values. + # + # Keys that do not pass the filter will be removed from the Hash. + # This mutates the input hash. + def allow_hash_values(hash, allowed) + allowed.each do |key, allowed_values| + hash.delete(key) if hash.key?(key) && !allowed_values.include?(hash[key]) + end + end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 7f3b6cbbcf8..d08b0d17b17 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -411,6 +411,9 @@ msgstr "" msgid "%{verb} %{time_spent_value} spent time." msgstr "" +msgid "·" +msgstr "" + msgid "'%{level}' is not a valid visibility level" msgstr "" @@ -18704,7 +18707,10 @@ msgstr "" msgid "Wiki pages" msgstr "" -msgid "Wiki was successfully updated." +msgid "Wiki was successfully created" +msgstr "" + +msgid "Wiki was successfully updated" msgstr "" msgid "WikiClone|Clone your wiki" @@ -18722,6 +18728,18 @@ msgstr "" msgid "WikiClone|Start Gollum and edit locally" msgstr "" +msgid "WikiDirEmpty|A wiki is where you can store all the details about your project. This can include why you've created it, its principles, how to use it, and so on." +msgstr "" + +msgid "WikiDirEmpty|Create a page" +msgstr "" + +msgid "WikiDirEmpty|Create a page in this directory" +msgstr "" + +msgid "WikiDirEmpty|This directory has no wiki pages" +msgstr "" + msgid "WikiEditPageTip|Tip: You can move this page by adding the path to the beginning of the title." msgstr "" @@ -18740,6 +18758,9 @@ msgstr "" msgid "WikiEmpty|A wiki is where you can store all the details about your project. This can include why you've created it, its principles, how to use it, and so on." msgstr "" +msgid "WikiEmpty|Create this page" +msgstr "" + msgid "WikiEmpty|Create your first page" msgstr "" @@ -18749,6 +18770,9 @@ msgstr "" msgid "WikiEmpty|The wiki lets you write documentation for your project" msgstr "" +msgid "WikiEmpty|This page does not exist" +msgstr "" + msgid "WikiEmpty|This project has no wiki pages" msgstr "" @@ -18803,6 +18827,9 @@ msgstr "" msgid "WikiPage|Write your content or drag files here…" msgstr "" +msgid "Wiki|Contents" +msgstr "" + msgid "Wiki|Create New Page" msgstr "" @@ -18815,6 +18842,9 @@ msgstr "" msgid "Wiki|Edit Page" msgstr "" +msgid "Wiki|Hide folder contents" +msgstr "" + msgid "Wiki|More Pages" msgstr "" @@ -18833,10 +18863,13 @@ msgstr "" msgid "Wiki|Pages" msgstr "" -msgid "Wiki|Title" +msgid "Wiki|Show files separately" msgstr "" -msgid "Wiki|Wiki Pages" +msgid "Wiki|Show folder contents" +msgstr "" + +msgid "Wiki|Title" msgstr "" msgid "Will deploy to" diff --git a/qa/qa/page/project/wiki/edit.rb b/qa/qa/page/project/wiki/edit.rb index f6edc28c41a..bdc1cda8950 100644 --- a/qa/qa/page/project/wiki/edit.rb +++ b/qa/qa/page/project/wiki/edit.rb @@ -5,7 +5,7 @@ module QA module Project module Wiki class Edit < Page::Base - view 'app/views/projects/wikis/_main_links.html.haml' do + view 'app/views/shared/wiki/_main_links.html.haml' do element :new_page_link, 'New page' # rubocop:disable QA/ElementWithPattern element :page_history_link, 'Page history' # rubocop:disable QA/ElementWithPattern element :edit_page_link, 'Edit' # rubocop:disable QA/ElementWithPattern diff --git a/qa/qa/page/project/wiki/new.rb b/qa/qa/page/project/wiki/new.rb index 792eba4bab7..abad499d700 100644 --- a/qa/qa/page/project/wiki/new.rb +++ b/qa/qa/page/project/wiki/new.rb @@ -7,7 +7,7 @@ module QA class New < Page::Base include Component::LazyLoader - view 'app/views/projects/wikis/_form.html.haml' do + view 'app/views/projects/wiki_pages/_form.html.haml' do element :wiki_title_textbox element :wiki_content_textarea element :wiki_message_textbox diff --git a/qa/qa/page/project/wiki/show.rb b/qa/qa/page/project/wiki/show.rb index 44619d177b1..c6b34ddc317 100644 --- a/qa/qa/page/project/wiki/show.rb +++ b/qa/qa/page/project/wiki/show.rb @@ -7,11 +7,11 @@ module QA class Show < Page::Base include Page::Component::LegacyClonePanel - view 'app/views/projects/wikis/pages.html.haml' do + view 'app/views/shared/wiki/_page_listing.html.haml' do element :clone_repository_link, 'Clone repository' # rubocop:disable QA/ElementWithPattern end - view 'app/views/projects/wikis/show.html.haml' do + view 'app/views/projects/wiki_pages/show.html.haml' do element :wiki_page_content end diff --git a/qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb b/qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb index 2c3f2c86c23..b996492ce08 100644 --- a/qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb @@ -13,7 +13,7 @@ module QA resource.message = 'Update home' end - validate_content('My First Wiki Content') + validate_created('My First Wiki Content') Page::Project::Wiki::Edit.perform(&:click_edit) Page::Project::Wiki::New.perform do |page| # rubocop:disable QA/AmbiguousPageObjectName @@ -21,7 +21,7 @@ module QA page.save_changes end - validate_content('My Second Wiki Content') + validate_edited('My Second Wiki Content') Resource::Repository::WikiPush.fabricate! do |push| push.wiki = wiki @@ -34,7 +34,12 @@ module QA expect(page).to have_content('My Third Wiki Content') end - def validate_content(content) + def validate_created(content) + expect(page).to have_content('Wiki was successfully created') + expect(page).to have_content(/#{content}/) + end + + def validate_edited(content) expect(page).to have_content('Wiki was successfully updated') expect(page).to have_content(/#{content}/) end diff --git a/spec/controllers/projects/wiki_directories_controller_spec.rb b/spec/controllers/projects/wiki_directories_controller_spec.rb new file mode 100644 index 00000000000..b09e1bc2ca4 --- /dev/null +++ b/spec/controllers/projects/wiki_directories_controller_spec.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Projects::WikiDirectoriesController do + set(:project) { create(:project, :public, :repository) } + + let(:user) { project.owner } + let(:project_wiki) { ProjectWiki.new(project, user) } + let(:wiki) { project_wiki.wiki } + let(:dir_slug) { 'the-directory' } + let(:dir_contents) { [create(:wiki_page)] } + let(:the_dir) { WikiDirectory.new(dir_slug, dir_contents) } + + before do + allow(controller).to receive(:find_dir).and_return(the_dir) + + sign_in(user) + end + + describe 'GET #show' do + let(:show_params) do + { + namespace_id: project.namespace, + project_id: project, + id: dir_slug + } + end + + before do + get :show, params: show_params + end + + context 'the directory is empty' do + let(:the_dir) { nil } + + it { is_expected.to render_template('empty') } + end + + context 'the directory does exist' do + it { is_expected.to render_template('show') } + + it 'sets the wiki_dir attribute' do + expect(assigns(:wiki_dir)).to eq(the_dir) + end + + it 'assigns the wiki pages' do + expect(assigns(:wiki_pages)).to eq(dir_contents) + end + end + end +end diff --git a/spec/controllers/projects/wiki_pages_controller_spec.rb b/spec/controllers/projects/wiki_pages_controller_spec.rb new file mode 100644 index 00000000000..01a84fbbf20 --- /dev/null +++ b/spec/controllers/projects/wiki_pages_controller_spec.rb @@ -0,0 +1,399 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Projects::WikiPagesController do + set(:project) { create(:project, :public, :repository) } + let(:user) { project.owner } + let(:project_wiki) { ProjectWiki.new(project, user) } + let(:wiki) { project_wiki.wiki } + let(:wiki_title) { 'page-title-test' } + let(:parent_ids) { { namespace_id: project.namespace.path, project_id: project.name } } + let(:redirect_destination) { Rails.application.routes.recognize_path(response.redirect_url) } + + before do + create_page(wiki_title, 'hello world') + + sign_in(user) + end + + after do + destroy_page(wiki_title) + end + + def helper + Helper.instance + end + + class Helper + include Singleton + include ActionView::Helpers::UrlHelper + end + + describe 'GET #new' do + subject { get :new, params: parent_ids } + + it 'redirects to #show and appends a `random_title` param' do + subject + + expect(response).to have_http_status(302) + + expect(redirect_destination) + .to include(parent_ids.merge(controller: 'projects/wiki_pages', action: 'show')) + + expect(response.redirect_url).to match(/\?random_title=true\Z/) + end + end + + describe 'GET #show' do + render_views + let(:requested_wiki_page) { wiki_title } + let(:random_title) { nil } + + subject do + get :show, params: { + namespace_id: project.namespace, + project_id: project, + id: requested_wiki_page, + random_title: random_title + } + end + + context 'when the wiki repo cannot be created' do + before do + allow(controller).to receive(:load_wiki) { raise ProjectWiki::CouldNotCreateWikiError } + end + + it 'redirects to the project path' do + headers = { 'Location' => a_string_ending_with(Gitlab::Routing.url_helpers.project_path(project)) } + + subject + + expect(response).to be_redirect + expect(response.header.to_hash).to include(headers) + end + end + + context 'when the page exists' do + it 'limits the retrieved pages for the sidebar' do + expect(controller).to receive(:load_wiki).and_return(project_wiki) + + # Sidebar entries + expect(project_wiki).to receive(:list_pages).with(limit: 15).and_call_original + + subject + + expect(response).to have_http_status(:ok) + expect(response.body).to include(wiki_title) + end + + context 'when page content encoding is invalid' do + it 'sets flash error' do + allow(controller).to receive(:valid_encoding?).and_return(false) + + subject + + expect(response).to have_http_status(:ok) + expect(flash[:notice]).to eq 'The content of this page is not encoded in UTF-8. Edits can only be made via the Git repository.' + end + end + end + + context 'when the page does not exist' do + let(:requested_wiki_page) { 'this-page-does-not-yet-exist' } + + context 'the current user can create wiki pages' do + it { is_expected.to render_template('edit') } + + it 'makes a call to see if the wiki is empty' do + expect(controller).to receive(:load_wiki).and_return(project_wiki) + expect(project_wiki).to receive(:list_pages).once.with(limit: anything).and_call_original + expect(project_wiki).to receive(:list_pages).with(limit: 1).and_call_original + subject + end + + describe 'assigned title' do + shared_examples :wiki_page_with_correct_title do + it 'assigns the correct title' do + subject + + expect(assigns(:page)).to have_attributes(title: assigned_title) + end + end + + context 'random_title is absent' do + let(:random_title) { nil } + + it_behaves_like :wiki_page_with_correct_title do + let(:assigned_title) { WikiPage.unhyphenize(requested_wiki_page) } + end + end + + context 'random_title is present' do + let(:random_title) { true } + + it_behaves_like :wiki_page_with_correct_title do + let(:assigned_title) { be_empty } + end + end + end + end + + context 'the current user cannot create wiki pages' do + before do + forbid_controller_ability! :create_wiki + end + it { is_expected.to render_template('missing_page') } + end + end + + context 'when page is a file' do + include WikiHelpers + + let(:path) { upload_file_to_wiki(project, user, file_name) } + + before do + get :show, params: { namespace_id: project.namespace, project_id: project, id: path } + end + + context 'when file is an image' do + let(:file_name) { 'dk.png' } + + it 'delivers the image' do + expect(response.headers['Content-Disposition']).to match(/^inline/) + expect(response.headers[Gitlab::Workhorse::DETECT_HEADER]).to eq "true" + end + + context 'when file is a svg' do + let(:file_name) { 'unsanitized.svg' } + + it 'delivers the image' do + expect(response.headers['Content-Disposition']).to match(/^inline/) + expect(response.headers[Gitlab::Workhorse::DETECT_HEADER]).to eq "true" + end + end + end + + context 'when file is a pdf' do + let(:file_name) { 'git-cheat-sheet.pdf' } + + it 'sets the content type to sets the content response headers' do + expect(response.headers['Content-Disposition']).to match(/^inline/) + expect(response.headers[Gitlab::Workhorse::DETECT_HEADER]).to eq "true" + end + end + end + end + + describe 'POST #preview_markdown' do + let(:page_id) { 'page/path' } + let(:markdown_text) { '*Markdown* text' } + let(:wiki_page) { create(:wiki_page, wiki: project_wiki, attrs: { title: wiki_title }) } + let(:processed_md) { json_response.fetch('body') } + + let(:preview_params) do + { namespace_id: project.namespace, project_id: project, id: wiki_page.slug, text: markdown_text } + end + + before do + post :preview_markdown, params: preview_params + end + + it 'renders json in a correct format' do + expect(response).to have_http_status(:ok) + expect(json_response).to include('body' => String, 'references' => Hash) + end + + describe 'double brackets within backticks' do + let(:markdown_text) do + <<-HEREDOC + `[[do_not_linkify]]` + ``` + [[also_do_not_linkify]] + ``` + HEREDOC + end + + it "does not linkify double brackets inside code blocks as expected" do + expect(processed_md).to include('[[do_not_linkify]]', '[[also_do_not_linkify]]') + end + end + + describe 'link re-writing' do + let(:links) do + [ + { text: 'regular link', path: 'regular' }, + { text: 'relative link 1', path: '../relative' }, + { text: 'relative link 2', path: './relative' }, + { text: 'relative link 3', path: './e/f/relative' }, + { text: 'spaced link', path: 'title with spaces' } + ] + end + + shared_examples :wiki_link_rewriter do + let(:markdown_text) { links.map { |text:, path:| "[#{text}](#{path})" }.join("\n") } + let(:expected_links) do + links.zip(paths).map do |(link, path)| + helper.link_to(link[:text], "#{project_wiki.wiki_page_path}/#{path}") + end + end + + it 'processes the links correctly' do + expect(processed_md).to include(*expected_links) + end + end + + context 'the current page has spaces in its title' do + let(:wiki_title) { 'page a/page b/page c/page d' } + it_behaves_like :wiki_link_rewriter do + let(:paths) do + ['regular', + 'page-a/page-b/relative', + 'page-a/page-b/page-c/relative', + 'page-a/page-b/page-c/e/f/relative', + 'title%20with%20spaces'] + end + end + end + + context 'the current page has an unproblematic title' do + let(:wiki_title) { 'a/b/c/d' } + it_behaves_like :wiki_link_rewriter do + let(:paths) do + ['regular', 'a/b/relative', 'a/b/c/relative', 'a/b/c/e/f/relative', 'title%20with%20spaces'] + end + end + end + + context "when there are hyphens in the page name" do + let(:wiki_title) { 'page-a/page-b/page-c/page-d' } + it_behaves_like :wiki_link_rewriter do + let(:paths) do + ['regular', + 'page-a/page-b/relative', + 'page-a/page-b/page-c/relative', + 'page-a/page-b/page-c/e/f/relative', + 'title%20with%20spaces'] + end + end + end + end + end + + describe 'GET #edit' do + subject { get(:edit, params: { namespace_id: project.namespace, project_id: project, id: wiki_title }) } + + context 'when page content encoding is invalid' do + it 'redirects to show' do + allow(controller).to receive(:valid_encoding?).and_return(false) + + subject + + expect(response).to redirect_to(project_wiki_path(project, project_wiki.list_pages.first)) + end + end + + context 'when page content encoding is valid' do + render_views + + it 'shows the edit page' do + subject + + expect(response).to have_http_status(:ok) + expect(response.body).to include('Edit Page') + end + end + end + + describe 'PATCH #update' do + let(:new_title) { 'New title' } + let(:new_content) { 'New content' } + subject do + patch(:update, + params: { + namespace_id: project.namespace, + project_id: project, + id: wiki_title, + wiki_page: { title: new_title, content: new_content } + }) + end + + context 'when page content encoding is invalid' do + it 'redirects to show' do + allow(controller).to receive(:valid_encoding?).and_return(false) + + subject + expect(response).to redirect_to(project_wiki_path(project, project_wiki.list_pages.first)) + end + end + + context 'when page content encoding is valid' do + render_views + + it 'updates the page' do + subject + + wiki_page = project_wiki.list_pages(load_content: true).first + + expect(wiki_page.title).to eq new_title + expect(wiki_page.content).to eq new_content + end + end + end + + describe 'GET #history' do + before do + allow(controller) + .to receive(:can?) + .with(any_args) + .and_call_original + + # The :create_wiki permission is irrelevant to reading history. + expect(controller) + .not_to receive(:can?) + .with(anything, :create_wiki, any_args) + + allow(controller) + .to receive(:can?) + .with(anything, :read_wiki, any_args) + .and_return(allow_read_wiki) + end + + shared_examples 'fetching history' do |expected_status| + before do + get :history, params: { namespace_id: project.namespace, project_id: project, id: wiki_title } + end + + it "returns status #{expected_status}" do + expect(response).to have_http_status(expected_status) + end + end + + it_behaves_like 'fetching history', :ok do + let(:allow_read_wiki) { true } + + it 'assigns @page_versions' do + expect(assigns(:page_versions)).to be_present + end + end + + it_behaves_like 'fetching history', :not_found do + let(:allow_read_wiki) { false } + end + end + + private + + def create_page(name, content) + wiki.write_page(name, :markdown, content, commit_details(name)) + end + + def commit_details(name) + Gitlab::Git::Wiki::CommitDetails.new(user.id, user.username, user.name, user.email, "created page #{name}") + end + + def destroy_page(title, dir = '') + page = wiki.page(title: title, dir: dir) + project_wiki.delete_page(page, "test commit") + end +end diff --git a/spec/controllers/projects/wikis_controller_spec.rb b/spec/controllers/projects/wikis_controller_spec.rb index f46da908218..7fbefa5787b 100644 --- a/spec/controllers/projects/wikis_controller_spec.rb +++ b/spec/controllers/projects/wikis_controller_spec.rb @@ -4,10 +4,10 @@ require 'spec_helper' describe Projects::WikisController do let_it_be(:project) { create(:project, :public, :repository) } - let(:user) { project.owner } - let(:project_wiki) { ProjectWiki.new(project, user) } - let(:wiki) { project_wiki.wiki } - let(:wiki_title) { 'page title test' } + let_it_be(:user) { project.owner } + let_it_be(:project_wiki) { ProjectWiki.new(project, user) } + let_it_be(:wiki) { project_wiki.wiki } + let_it_be(:wiki_title) { 'page title test' } before do create_page(wiki_title, 'hello world') @@ -19,231 +19,86 @@ describe Projects::WikisController do destroy_page(wiki_title) end - describe 'GET #new' do - subject { get :new, params: { namespace_id: project.namespace, project_id: project } } - - it 'redirects to #show and appends a `random_title` param' do - subject - - expect(response).to have_http_status(302) - expect(Rails.application.routes.recognize_path(response.redirect_url)).to include( - controller: 'projects/wikis', - action: 'show' - ) - expect(response.redirect_url).to match(/\?random_title=true\Z/) + describe 'GET #pages' do + subject do + get :pages, params: { namespace_id: project.namespace, project_id: project, id: wiki_title }.merge(extra_params) end - end - describe 'GET #pages' do - subject { get :pages, params: { namespace_id: project.namespace, project_id: project, id: wiki_title } } + let(:extra_params) { {} } it 'does not load the pages content' do expect(controller).to receive(:load_wiki).and_return(project_wiki) - expect(project_wiki).to receive(:list_pages).twice.and_call_original subject end - end - - describe 'GET #history' do - before do - allow(controller) - .to receive(:can?) - .with(any_args) - .and_call_original - # The :create_wiki permission is irrelevant to reading history. - expect(controller) - .not_to receive(:can?) - .with(anything, :create_wiki, any_args) - - allow(controller) - .to receive(:can?) - .with(anything, :read_wiki, any_args) - .and_return(allow_read_wiki) - end - - shared_examples 'fetching history' do |expected_status| - before do - get :history, params: { namespace_id: project.namespace, project_id: project, id: wiki_title } - end - - it "returns status #{expected_status}" do - expect(response).to have_http_status(expected_status) + describe 'illegal params' do + shared_examples :a_bad_request do + it do + expect { subject }.to raise_error(ActionController::BadRequest) + end end - end - it_behaves_like 'fetching history', :ok do - let(:allow_read_wiki) { true } + describe ':sort' do + let(:extra_params) { { sort: 'wibble' } } - it 'assigns @page_versions' do - expect(assigns(:page_versions)).to be_present + it_behaves_like :a_bad_request end - end - it_behaves_like 'fetching history', :not_found do - let(:allow_read_wiki) { false } - end - end - - describe 'GET #show' do - render_views - - let(:random_title) { nil } - - subject { get :show, params: { namespace_id: project.namespace, project_id: project, id: id, random_title: random_title } } - - context 'when page exists' do - let(:id) { wiki_title } + describe ':direction' do + let(:extra_params) { { direction: 'wibble' } } - it 'limits the retrieved pages for the sidebar' do - expect(controller).to receive(:load_wiki).and_return(project_wiki) - expect(project_wiki).to receive(:list_pages).with(limit: 15).and_call_original - - subject - - expect(response).to have_http_status(:ok) - expect(assigns(:page).title).to eq(wiki_title) + it_behaves_like :a_bad_request end - context 'when page content encoding is invalid' do - it 'sets flash error' do - allow(controller).to receive(:valid_encoding?).and_return(false) - - subject + describe ':show_children' do + let(:extra_params) { { show_children: 'wibble' } } - expect(response).to have_http_status(:ok) - expect(flash[:notice]).to eq('The content of this page is not encoded in UTF-8. Edits can only be made via the Git repository.') - end + it_behaves_like :a_bad_request end end - context 'when the page does not exist' do - let(:id) { 'does not exist' } - - before do - subject - end - - it 'builds a new wiki page with the id as the title' do - expect(assigns(:page).title).to eq(id) - end + shared_examples 'sorting-and-nesting' do |sort_key, default_nesting| + context "the user is sorting by #{sort_key}" do + let(:extra_params) { sort_params.merge(nesting_params) } + let(:sort_params) { { sort: sort_key } } + let(:nesting_params) { {} } - context 'when a random_title param is present' do - let(:random_title) { true } - - it 'builds a new wiki page with no title' do - expect(assigns(:page).title).to be_empty + before do + subject end - end - end - context 'when page is a file' do - include WikiHelpers - - let(:id) { upload_file_to_wiki(project, user, file_name) } - - before do - subject - end - - context 'when file is an image' do - let(:file_name) { 'dk.png' } + it "sets nesting to #{default_nesting} by default" do + expect(assigns :nesting).to eq default_nesting + end - it 'delivers the image' do - expect(response.headers['Content-Disposition']).to match(/^inline/) - expect(response.headers[Gitlab::Workhorse::DETECT_HEADER]).to eq "true" + it 'hides children if the default requires it' do + expect(assigns :show_children).to be(default_nesting != ProjectWiki::NESTING_CLOSED) end - context 'when file is a svg' do - let(:file_name) { 'unsanitized.svg' } + ProjectWiki::NESTINGS.each do |nesting| + context "the user explicitly passes show_children = #{nesting}" do + let(:nesting_params) { { show_children: nesting } } - it 'delivers the image' do - expect(response.headers['Content-Disposition']).to match(/^inline/) - expect(response.headers[Gitlab::Workhorse::DETECT_HEADER]).to eq "true" + it 'sets nesting to the provided value' do + expect(assigns :nesting).to eq nesting + end end end - end - context 'when file is a pdf' do - let(:file_name) { 'git-cheat-sheet.pdf' } + context 'the user wants children hidden' do + let(:nesting_params) { { show_children: 'hidden' } } - it 'sets the content type to sets the content response headers' do - expect(response.headers['Content-Disposition']).to match(/^inline/) - expect(response.headers[Gitlab::Workhorse::DETECT_HEADER]).to eq "true" + it 'hides children' do + expect(assigns :show_children).to be false + end end end end - end - - describe 'POST #preview_markdown' do - it 'renders json in a correct format' do - post :preview_markdown, params: { namespace_id: project.namespace, project_id: project, id: 'page/path', text: '*Markdown* text' } - - expect(json_response.keys).to match_array(%w(body references)) - end - end - - describe 'GET #edit' do - subject { get(:edit, params: { namespace_id: project.namespace, project_id: project, id: wiki_title }) } - - context 'when page content encoding is invalid' do - it 'redirects to show' do - allow(controller).to receive(:valid_encoding?).and_return(false) - - subject - - expect(response).to redirect_to(project_wiki_path(project, project_wiki.list_pages.first)) - end - end - context 'when page content encoding is valid' do - render_views - - it 'shows the edit page' do - subject - - expect(response).to have_http_status(:ok) - expect(response.body).to include('Edit Page') - end - end - end - - describe 'PATCH #update' do - let(:new_title) { 'New title' } - let(:new_content) { 'New content' } - subject do - patch(:update, - params: { - namespace_id: project.namespace, - project_id: project, - id: wiki_title, - wiki: { title: new_title, content: new_content } - }) - end - - context 'when page content encoding is invalid' do - it 'redirects to show' do - allow(controller).to receive(:valid_encoding?).and_return(false) - - subject - expect(response).to redirect_to(project_wiki_path(project, project_wiki.list_pages.first)) - end - end - - context 'when page content encoding is valid' do - render_views - - it 'updates the page' do - subject - - wiki_page = project_wiki.list_pages(load_content: true).first - - expect(wiki_page.title).to eq new_title - expect(wiki_page.content).to eq new_content - end - end + include_examples 'sorting-and-nesting', ProjectWiki::CREATED_AT_ORDER, ProjectWiki::NESTING_FLAT + include_examples 'sorting-and-nesting', ProjectWiki::TITLE_ORDER, ProjectWiki::NESTING_CLOSED end def create_page(name, content) diff --git a/spec/factories/wiki_pages.rb b/spec/factories/wiki_pages.rb index 761ba58edb2..3ac8fe4ae43 100644 --- a/spec/factories/wiki_pages.rb +++ b/spec/factories/wiki_pages.rb @@ -18,12 +18,12 @@ FactoryBot.define do association :wiki, factory: :project_wiki, strategy: :build initialize_with { new(wiki, page, true) } - before(:create) do |page, evaluator| - page.attributes = evaluator.attrs + before(:create) do |wiki_page, evaluator| + wiki_page.attributes = evaluator.attrs.with_indifferent_access end - to_create do |page| - page.create + to_create do |wiki_page| + wiki_page.create end end end diff --git a/spec/features/projects/features_visibility_spec.rb b/spec/features/projects/features_visibility_spec.rb index 9ec61743a11..8b7fef84dd7 100644 --- a/spec/features/projects/features_visibility_spec.rb +++ b/spec/features/projects/features_visibility_spec.rb @@ -3,8 +3,9 @@ require 'spec_helper' describe 'Edit Project Settings' do + set(:project) { create(:project, :public, :repository) } + let(:member) { create(:user) } - let!(:project) { create(:project, :public, :repository) } let!(:issue) { create(:issue, project: project) } let(:non_member) { create(:user) } @@ -81,85 +82,88 @@ describe 'Edit Project Settings' do end describe 'project features visibility pages' do - let(:pipeline) { create(:ci_empty_pipeline, project: project) } - let(:job) { create(:ci_build, pipeline: pipeline) } - - let(:tools) do - { - builds: project_job_path(project, job), - issues: project_issues_path(project), - wiki: project_wiki_path(project, :home), - snippets: project_snippets_path(project), - merge_requests: project_merge_requests_path(project) - } + set(:pipeline) { create(:ci_empty_pipeline, project: project) } + set(:job) { create(:ci_build, pipeline: pipeline) } + + where(:method_name, :build_url) do + [ + [:builds, -> { project_job_path(project, job) }], + [:issues, -> { project_issues_path(project) }], + [:wiki, -> { project_wiki_path(project, :home) }], + [:snippets, -> { project_snippets_path(project) }], + [:merge_requests, -> { project_merge_requests_path(project) }] + ] end - context 'normal user' do - before do - sign_in(member) - end + with_them do + let(:url) { build_url.call } + let(:attr_name) { "#{method_name}_access_level" } + + context 'normal user' do + before do + project.team.truncate + sign_in(member) + end + + it 'renders 200 if tool is enabled' do + project.project_feature.update_attribute(attr_name, ProjectFeature::ENABLED) - it 'renders 200 if tool is enabled' do - tools.each do |method_name, url| - project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::ENABLED) visit url + expect(page.status_code).to eq(200) end - end - it 'renders 404 if feature is disabled' do - tools.each do |method_name, url| - project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::DISABLED) + it 'renders 404 if feature is disabled' do + project.project_feature.update_attribute(attr_name, ProjectFeature::DISABLED) + visit url + expect(page.status_code).to eq(404) end - end - it 'renders 404 if feature is enabled only for team members' do - project.team.truncate + it 'renders 404 if feature is enabled only for team members' do + project.project_feature.update_attribute(attr_name, ProjectFeature::PRIVATE) - tools.each do |method_name, url| - project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::PRIVATE) visit url + expect(page.status_code).to eq(404) end - end - it 'renders 200 if user is member of group' do - group = create(:group) - project.group = group - project.save + it 'renders 200 if user is member of group' do + group = create(:group) + project.group = group + project.save - group.add_owner(member) + group.add_owner(member) + + project.project_feature.update_attribute(attr_name, ProjectFeature::PRIVATE) - tools.each do |method_name, url| - project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::PRIVATE) visit url + expect(page.status_code).to eq(200) end end - end - context 'admin user' do - before do - non_member.update_attribute(:admin, true) - sign_in(non_member) - end + context 'admin user' do + before do + non_member.update_attribute(:admin, true) + project.team.truncate + sign_in(non_member) + end - it 'renders 404 if feature is disabled' do - tools.each do |method_name, url| + it 'renders 404 if feature is disabled' do project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::DISABLED) + visit url + expect(page.status_code).to eq(404) end - end - it 'renders 200 if feature is enabled only for team members' do - project.team.truncate - - tools.each do |method_name, url| + it 'renders 200 if feature is enabled only for team members' do project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::PRIVATE) + visit url + expect(page.status_code).to eq(200) end end diff --git a/spec/features/projects/wiki/markdown_preview_spec.rb b/spec/features/projects/wiki/markdown_preview_spec.rb index 5c6b04a7141..a56b3e4955a 100644 --- a/spec/features/projects/wiki/markdown_preview_spec.rb +++ b/spec/features/projects/wiki/markdown_preview_spec.rb @@ -4,164 +4,54 @@ require 'spec_helper' describe 'Projects > Wiki > User previews markdown changes', :js do set(:user) { create(:user) } - let(:project) { create(:project, :wiki_repo, namespace: user.namespace) } - let(:wiki_page) { create(:wiki_page, wiki: project.wiki, attrs: { title: 'home', content: '[some link](other-page)' }) } - let(:wiki_content) do - <<-HEREDOC -[regular link](regular) -[relative link 1](../relative) -[relative link 2](./relative) -[relative link 3](./e/f/relative) -[spaced link](title with spaces) - HEREDOC - end + set(:project) { create(:project, :wiki_repo, namespace: user.namespace) } + let(:project_wiki) { ProjectWiki.new(project, user) } before do project.add_maintainer(user) - sign_in(user) + init_home! end - context "while creating a new wiki page" do - context "when there are no spaces or hyphens in the page name" do - it "rewrites relative links as expected" do - create_wiki_page('a/b/c/d', content: wiki_content) - - expect(page).to have_content("regular link") - - expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/regular\">regular link</a>") - expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a/b/relative\">relative link 1</a>") - expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a/b/c/relative\">relative link 2</a>") - expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a/b/c/e/f/relative\">relative link 3</a>") - expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/title%20with%20spaces\">spaced link</a>") - end - end - - context "when there are spaces in the page name" do - it "rewrites relative links as expected" do - create_wiki_page('a page/b page/c page/d page', content: wiki_content) - - expect(page).to have_content("regular link") - - expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/regular\">regular link</a>") - expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/relative\">relative link 1</a>") - expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/c-page/relative\">relative link 2</a>") - expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/c-page/e/f/relative\">relative link 3</a>") - expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/title%20with%20spaces\">spaced link</a>") - end - end - - context "when there are hyphens in the page name" do - it "rewrites relative links as expected" do - create_wiki_page('a-page/b-page/c-page/d-page', content: wiki_content) - - expect(page).to have_content("regular link") - - expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/regular\">regular link</a>") - expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/relative\">relative link 1</a>") - expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/c-page/relative\">relative link 2</a>") - expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/c-page/e/f/relative\">relative link 3</a>") - expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/title%20with%20spaces\">spaced link</a>") - end - end + def init_home! + create(:wiki_page, wiki: project.wiki, attrs: { title: 'home', content: '[some link](other-page)' }) end - context "while editing a wiki page" do - context "when there are no spaces or hyphens in the page name" do - it "rewrites relative links as expected" do - create_wiki_page('a/b/c/d') - click_link 'Edit' - - fill_in :wiki_content, with: wiki_content - click_on "Preview" - - expect(page).to have_content("regular link") - - expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/regular\">regular link</a>") - expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a/b/relative\">relative link 1</a>") - expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a/b/c/relative\">relative link 2</a>") - expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a/b/c/e/f/relative\">relative link 3</a>") - expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/title%20with%20spaces\">spaced link</a>") - end - end - - context "when there are spaces in the page name" do - it "rewrites relative links as expected" do - create_wiki_page('a page/b page/c page/d page') - click_link 'Edit' - - fill_in :wiki_content, with: wiki_content - click_on "Preview" - - expect(page).to have_content("regular link") - - expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/regular\">regular link</a>") - expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/relative\">relative link 1</a>") - expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/c-page/relative\">relative link 2</a>") - expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/c-page/e/f/relative\">relative link 3</a>") - expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/title%20with%20spaces\">spaced link</a>") - end - end - - context "when there are hyphens in the page name" do - it "rewrites relative links as expected" do - create_wiki_page('a-page/b-page/c-page/d-page') - click_link 'Edit' - - fill_in :wiki_content, with: wiki_content - click_on "Preview" - - expect(page).to have_content("regular link") - - expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/regular\">regular link</a>") - expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/relative\">relative link 1</a>") - expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/c-page/relative\">relative link 2</a>") - expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/c-page/e/f/relative\">relative link 3</a>") - expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/title%20with%20spaces\">spaced link</a>") - end + def fill_in_content! + page.within '.wiki-form' do + fill_in :wiki_page_content, with: wiki_content end + end - context 'when rendering the preview' do - it 'renders content with CommonMark' do - create_wiki_page('a-page/b-page/c-page/common-mark') - click_link 'Edit' - - fill_in :wiki_content, with: "1. one\n - sublist\n" - click_on "Preview" - - # the above generates two separate lists (not embedded) in CommonMark - expect(page).to have_content("sublist") - expect(page).not_to have_xpath("//ol//li//ul") - end + def show_preview! + page.within '.wiki-form' do + click_on 'Preview' end end - it "does not linkify double brackets inside code blocks as expected" do - wiki_content = <<-HEREDOC - `[[do_not_linkify]]` - ``` - [[also_do_not_linkify]] - ``` - HEREDOC - - create_wiki_page('linkify_test', wiki_content) + context 'when writing a new page' do + let(:new_wiki_path) { 'a/b/c/d' } + let(:wiki_content) { 'Some [awesome wiki](content)' } - expect(page).to have_content("do_not_linkify") + it 'can show a preview of markdown content' do + visit project_wiki_pages_new_path(project, id: new_wiki_path) + fill_in_content! + show_preview! - expect(page.html).to include('[[do_not_linkify]]') - expect(page.html).to include('[[also_do_not_linkify]]') + expect(page).to have_link('awesome wiki') + end end - private - - def create_wiki_page(path, content = 'content') - visit project_wiki_path(project, wiki_page) + context 'when editing an existing page' do + let(:wiki_content) { 'Some [bemusing](content)' } + let(:wiki_page) { create(:wiki_page, wiki: project_wiki) } - click_link 'New page' + it 'can show a preview of markdown content, when writing' do + visit project_wiki_edit_path(project, wiki_page) + fill_in_content! + show_preview! - fill_in :wiki_title, with: path - fill_in :wiki_content, with: content - - click_button 'Create page' + expect(page).to have_link('bemusing') + end end end diff --git a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb index 56d0518015d..6b651ccb67a 100644 --- a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb +++ b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb @@ -3,9 +3,15 @@ require "spec_helper" describe "User creates wiki page" do - let(:user) { create(:user) } - let(:wiki) { ProjectWiki.new(project, user) } + include CapybaraHelpers + include WikiHelpers + + set(:user) { create(:user) } + let(:project) { create(:project) } + let(:wiki) { ProjectWiki.new(project, user) } + let(:new_page) { WikiPage.new(wiki) } + let(:message_field) { form_field_name(new_page, :message) } before do project.add_maintainer(user) @@ -13,38 +19,78 @@ describe "User creates wiki page" do sign_in(user) end + def start_writing(page_path) + click_link("New page") + fill_in(:wiki_page_title, with: page_path) + end + + def create_page(attrs = {}) + page.within(".wiki-form") do + attrs.each do |k, v| + fill_in("wiki_page_#{k}".to_sym, with: v) + end + end + click_on("Create page") + end + + shared_examples 'updates commit message' do + describe 'commit message', :js do + it "has `Create home` as a commit message" do + wait_for_requests + + expect(page).to have_field(message_field, with: "Create home") + end + end + end + context "when wiki is empty" do before do visit(project_wikis_path(project)) click_link "Create your first page" + find('.wiki-form') end context "in a user namespace" do let(:project) { create(:project, :wiki_repo, namespace: user.namespace) } + let(:wiki_page_content) { '' } it "shows validation error message" do - page.within(".wiki-form") do - fill_in(:wiki_content, with: "") + create_page - click_on("Create page") - end - - expect(page).to have_content("The form contains the following error:").and have_content("Content can't be blank") - - page.within(".wiki-form") do - fill_in(:wiki_content, with: "[link test](test)") + expect(page) + .to have_content("The form contains the following error:") + .and have_content("Content can't be blank") + .and have_css('.wiki-form') + .and have_css('.qa-create-page-button') + end - click_on("Create page") - end + it 'offers to create pages that do not yet exist' do + create_page(content: "[link test](test)") - expect(page).to have_content("Home").and have_content("link test") + expect(page) + .to have_content("Home") + .and have_content("link test") click_link("link test") expect(page).to have_content("Create New Page") end + it "has a link to the parent directory in the pages sidebar" do + wiki_full_path = "one/two/three-test" + create_page(title: wiki_full_path, content: 'wiki content') + + wiki_page = wiki.find_page(wiki_full_path) + expect(wiki_page).to be_present + dir = wiki.find_dir(wiki_page.directory) + expect(dir).to be_present + + expect(current_path).to include(wiki_full_path) + + expect(page).to have_link(dir.slug, href: project_wiki_dir_path(project, dir)) + end + it "shows non-escaped link in the pages list", :quarantine do fill_in(:wiki_title, with: "one/two/three-test") @@ -58,19 +104,17 @@ describe "User creates wiki page" do expect(page).to have_xpath("//a[@href='/#{project.full_path}/wikis/one/two/three-test']") end - it "has `Create home` as a commit message", :js do - wait_for_requests - - expect(page).to have_field("wiki[message]", with: "Create home") - end + it_behaves_like 'updates commit message' it "creates a page from the home page" do - fill_in(:wiki_content, with: "[test](test)\n[GitLab API doc](api)\n[Rake tasks](raketasks)\n# Wiki header\n") - fill_in(:wiki_message, with: "Adding links to wiki") + page_content = <<~WIKI_CONTENT + [test](test) + [GitLab API doc](api) + [Rake tasks](raketasks) + # Wiki header + WIKI_CONTENT - page.within(".wiki-form") do - click_button("Create page") - end + create_page(content: page_content, message: "Adding links to wiki") expect(current_path).to eq(project_wiki_path(project, "home")) expect(page).to have_content("test GitLab API doc Rake tasks Wiki header") @@ -111,7 +155,7 @@ describe "User creates wiki page" do end end - it "creates ASCII wiki with LaTeX blocks", :js do + it "creates ASCIIdoc wiki with LaTeX blocks", :js do stub_application_setting(plantuml_url: "http://localhost", plantuml_enabled: true) ascii_content = <<~MD @@ -132,37 +176,25 @@ describe "User creates wiki page" do stem:[2+2] is 4 MD - find("#wiki_format option[value=asciidoc]").select_option - - fill_in(:wiki_content, with: ascii_content) + find("#wiki_page_format option[value=asciidoc]").select_option - page.within(".wiki-form") do - click_button("Create page") - end + create_page(content: ascii_content) page.within ".md" do expect(page).to have_selector(".katex", count: 3).and have_content("2+2 is 4") end end - it_behaves_like 'wiki file attachments', :quarantine + it_behaves_like 'wiki file attachments' end - context "in a group namespace", :js do + context "in a group namespace" do let(:project) { create(:project, :wiki_repo, namespace: create(:group, :public)) } - it "has `Create home` as a commit message" do - wait_for_requests - - expect(page).to have_field("wiki[message]", with: "Create home") - end - - it "creates a page from the home page", :quarantine do - page.within(".wiki-form") do - fill_in(:wiki_content, with: "My awesome wiki!") + it_behaves_like 'updates commit message' - click_button("Create page") - end + it "creates a page from the home page" do + create_page(content: "My awesome wiki!") expect(page).to have_content("Home") .and have_content("Last edited by #{user.name}") @@ -178,76 +210,37 @@ describe "User creates wiki page" do visit(project_wikis_path(project)) end - context "in a user namespace" do - let(:project) { create(:project, :wiki_repo, namespace: user.namespace) } - - context "via the `new wiki page` page" do - it "creates a page with a single word" do - click_link("New page") - - page.within(".wiki-form") do - fill_in(:wiki_title, with: "foo") - fill_in(:wiki_content, with: "My awesome wiki!") - end - - # Commit message field should have correct value. - expect(page).to have_field("wiki[message]", with: "Create foo") - - click_button("Create page") - - expect(page).to have_content("foo") - .and have_content("Last edited by #{user.name}") - .and have_content("My awesome wiki!") - end - - it "creates a page with spaces in the name" do - click_link("New page") + shared_examples 'creates page by slug' do |slug, unslug| + it "creates #{slug}" do + start_writing(slug) - page.within(".wiki-form") do - fill_in(:wiki_title, with: "Spaces in the name") - fill_in(:wiki_content, with: "My awesome wiki!") - end + # Commit message field should have correct value. + expect(page).to have_field(message_field, with: "Create #{unslug}") - # Commit message field should have correct value. - expect(page).to have_field("wiki[message]", with: "Create Spaces in the name") + create_page(content: "My awesome wiki!") - click_button("Create page") - - expect(page).to have_content("Spaces in the name") - .and have_content("Last edited by #{user.name}") - .and have_content("My awesome wiki!") - end - - it "creates a page with hyphens in the name" do - click_link("New page") - - page.within(".wiki-form") do - fill_in(:wiki_title, with: "hyphens-in-the-name") - fill_in(:wiki_content, with: "My awesome wiki!") - end - - # Commit message field should have correct value. - expect(page).to have_field("wiki[message]", with: "Create hyphens in the name") - - page.within(".wiki-form") do - fill_in(:wiki_content, with: "My awesome wiki!") + expect(page).to have_content(unslug) + .and have_content("Last edited by #{user.name}") + .and have_content("My awesome wiki!") + end + end - click_button("Create page") - end + context "in a user namespace" do + let(:project) { create(:project, :wiki_repo, namespace: user.namespace) } - expect(page).to have_content("hyphens in the name") - .and have_content("Last edited by #{user.name}") - .and have_content("My awesome wiki!") - end + context "via the `new wiki page` page" do + include_examples 'creates page by slug', 'foo', 'foo' + include_examples 'creates page by slug', 'Spaces in the name', 'Spaces in the name' + include_examples 'creates page by slug', 'Hyphens-in-the-name', 'Hyphens in the name' end it "shows the emoji autocompletion dropdown" do - click_link("New page") + start_writing('text-autocomplete') page.within(".wiki-form") do - find("#wiki_content").native.send_keys("") + find("#wiki_page_content").native.send_keys("") - fill_in(:wiki_content, with: ":") + fill_in(:wiki_page_content, with: ":") end expect(page).to have_selector(".atwho-view") @@ -258,23 +251,9 @@ describe "User creates wiki page" do let(:project) { create(:project, :wiki_repo, namespace: create(:group, :public)) } context "via the `new wiki page` page" do - it "creates a page" do - click_link("New page") - - page.within(".wiki-form") do - fill_in(:wiki_title, with: "foo") - fill_in(:wiki_content, with: "My awesome wiki!") - end - - # Commit message field should have correct value. - expect(page).to have_field("wiki[message]", with: "Create foo") - - click_button("Create page") - - expect(page).to have_content("foo") - .and have_content("Last edited by #{user.name}") - .and have_content("My awesome wiki!") - end + include_examples 'creates page by slug', 'foo', 'foo' + include_examples 'creates page by slug', 'Spaces in the name', 'Spaces in the name' + include_examples 'creates page by slug', 'Hyphens-in-the-name', 'Hyphens in the name' end end end diff --git a/spec/features/projects/wiki/user_deletes_wiki_page_spec.rb b/spec/features/projects/wiki/user_deletes_wiki_page_spec.rb index 38e5e292064..9b9d3100bf5 100644 --- a/spec/features/projects/wiki/user_deletes_wiki_page_spec.rb +++ b/spec/features/projects/wiki/user_deletes_wiki_page_spec.rb @@ -6,6 +6,7 @@ describe 'User deletes wiki page', :js do let(:user) { create(:user) } let(:project) { create(:project, :wiki_repo, namespace: user.namespace) } let(:wiki_page) { create(:wiki_page, wiki: project.wiki) } + let(:project_wiki) { ProjectWiki.new(project, user) } before do sign_in(user) @@ -18,5 +19,6 @@ describe 'User deletes wiki page', :js do find('.modal-footer .btn-danger').click expect(page).to have_content('Page was successfully deleted') + expect(project_wiki.find_page(wiki_page.slug)).to be nil end end diff --git a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb index 3f3711f9eb8..5ab44e83f73 100644 --- a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb +++ b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb @@ -10,6 +10,13 @@ describe 'User updates wiki page' do sign_in(user) end + def create_page(attrs = {}) + page.within('.wiki-form') do + attrs.each { |k, v| fill_in("wiki_page_#{k}".to_sym, with: v) } + click_on('Create page') + end + end + context 'when wiki is empty' do before do visit(project_wikis_path(project)) @@ -28,12 +35,7 @@ describe 'User updates wiki page' do end it 'updates a page that has a path', :js do - fill_in(:wiki_title, with: 'one/two/three-test') - - page.within '.wiki-form' do - fill_in(:wiki_content, with: 'wiki content') - click_on('Create page') - end + create_page(title: 'one/two/three-test', content: 'wiki content') expect(current_path).to include('one/two/three-test') expect(find('.wiki-pages')).to have_content('three') @@ -72,9 +74,9 @@ describe 'User updates wiki page' do it 'updates a page', :js do # Commit message field should have correct value. - expect(page).to have_field('wiki[message]', with: 'Update home') + expect(page).to have_field('wiki_page[message]', with: 'Update home') - fill_in(:wiki_content, with: 'My awesome wiki!') + fill_in(:wiki_page_content, with: 'My awesome wiki!') click_button('Save changes') expect(page).to have_content('Home') @@ -83,31 +85,31 @@ describe 'User updates wiki page' do end it 'updates the commit message as the title is changed', :js do - fill_in(:wiki_title, with: 'Wiki title') + fill_in(:wiki_page_title, with: 'Wiki title') - expect(page).to have_field('wiki[message]', with: 'Update Wiki title') + expect(page).to have_field('wiki_page[message]', with: 'Update Wiki title') end it 'does not allow XSS', :js do - fill_in(:wiki_title, with: '<script>') + fill_in(:wiki_page_title, with: '<script>') - expect(page).to have_field('wiki[message]', with: 'Update <script>') + expect(page).to have_field('wiki_page[message]', with: 'Update <script>') end it 'shows a validation error message' do - fill_in(:wiki_content, with: '') + fill_in(:wiki_page_content, with: '') click_button('Save changes') expect(page).to have_selector('.wiki-form') expect(page).to have_content('Edit Page') expect(page).to have_content('The form contains the following error:') expect(page).to have_content("Content can't be blank") - expect(find('textarea#wiki_content').value).to eq('') + expect(find('textarea#wiki_page_content').value).to eq('') end it 'shows the emoji autocompletion dropdown', :js do - find('#wiki_content').native.send_keys('') - fill_in(:wiki_content, with: ':') + find('#wiki_page_content').native.send_keys('') + fill_in(:wiki_page_content, with: ':') expect(page).to have_selector('.atwho-view') end @@ -143,9 +145,9 @@ describe 'User updates wiki page' do it 'updates a page', :js do # Commit message field should have correct value. - expect(page).to have_field('wiki[message]', with: 'Update home') + expect(page).to have_field('wiki_page[message]', with: 'Update home') - fill_in(:wiki_content, with: 'My awesome wiki!') + fill_in(:wiki_page_content, with: 'My awesome wiki!') click_button('Save changes') @@ -169,50 +171,43 @@ describe 'User updates wiki page' do visit(project_wiki_edit_path(project, wiki_page)) end - it 'moves the page to the root folder' do - fill_in(:wiki_title, with: "/#{page_name}") + def edit_title!(title) + fill_in(:wiki_page_title, with: title) click_button('Save changes') + end + + it 'moves the page to the root folder' do + edit_title!("/#{page_name}") expect(current_path).to eq(project_wiki_path(project, page_name)) end it 'moves the page to other dir' do - new_page_dir = "foo1/bar1/#{page_name}" - - fill_in(:wiki_title, with: new_page_dir) - - click_button('Save changes') + new_page_path = "baz/quux/#{page_name}" + edit_title!(new_page_path) - expect(current_path).to eq(project_wiki_path(project, new_page_dir)) + expect(current_path).to eq(project_wiki_path(project, new_page_path)) end it 'remains in the same place if title has not changed' do original_path = project_wiki_path(project, wiki_page) - - fill_in(:wiki_title, with: page_name) - - click_button('Save changes') + edit_title!(page_name) expect(current_path).to eq(original_path) end it 'can be moved to a different dir with a different name' do - new_page_dir = "foo1/bar1/new_page_name" - - fill_in(:wiki_title, with: new_page_dir) + new_page_path = "quux/baz/new_page_name" + edit_title!(new_page_path) - click_button('Save changes') - - expect(current_path).to eq(project_wiki_path(project, new_page_dir)) + expect(current_path).to eq(project_wiki_path(project, new_page_path)) end it 'can be renamed and moved to the root folder' do new_name = 'new_page_name' - fill_in(:wiki_title, with: "/#{new_name}") - - click_button('Save changes') + edit_title!("/#{new_name}") expect(current_path).to eq(project_wiki_path(project, new_name)) end @@ -220,9 +215,7 @@ describe 'User updates wiki page' do it 'squishes the title before creating the page' do new_page_dir = " foo1 / bar1 / #{page_name} " - fill_in(:wiki_title, with: new_page_dir) - - click_button('Save changes') + edit_title!(new_page_dir) expect(current_path).to eq(project_wiki_path(project, "foo1/bar1/#{page_name}")) end diff --git a/spec/features/projects/wiki/user_views_wiki_page_spec.rb b/spec/features/projects/wiki/user_views_wiki_page_spec.rb index 77e725e7f11..af8f86106d9 100644 --- a/spec/features/projects/wiki/user_views_wiki_page_spec.rb +++ b/spec/features/projects/wiki/user_views_wiki_page_spec.rb @@ -8,10 +8,11 @@ describe 'User views a wiki page' do let(:user) { create(:user) } let(:project) { create(:project, :wiki_repo, namespace: user.namespace) } let(:path) { 'image.png' } + let(:wiki_content) { "Look at this [image](#{path})\n\n ![alt text](#{path})" } let(:wiki_page) do create(:wiki_page, wiki: project.wiki, - attrs: { title: 'home', content: "Look at this [image](#{path})\n\n ![alt text](#{path})" }) + attrs: { title: 'home', content: wiki_content }) end before do @@ -19,17 +20,18 @@ describe 'User views a wiki page' do sign_in(user) end + def create_page(attrs = {}) + page.within('.wiki-form') do + attrs.each { |k, v| fill_in("wiki_page_#{k}".to_sym, with: v) } + click_on('Create page') + end + end + context 'when wiki is empty' do before do visit(project_wikis_path(project)) click_link "Create your first page" - - fill_in(:wiki_title, with: 'one/two/three-test') - - page.within('.wiki-form') do - fill_in(:wiki_content, with: 'wiki content') - click_on('Create page') - end + create_page(title: 'one/two/three-test', content: 'wiki content') end it 'shows the history of a page that has a path', :js do @@ -83,24 +85,27 @@ describe 'User views a wiki page' do context 'shows a file stored in a page' do let(:path) { upload_file_to_wiki(project, user, 'dk.png') } + let(:image_path) { project_wiki_path(project, path) } it do - expect(page).to have_xpath("//img[@data-src='#{project.wiki.wiki_base_path}/#{path}']") - expect(page).to have_link('image', href: "#{project.wiki.wiki_base_path}/#{path}") + expect(page).to have_xpath("//img[@data-src='#{image_path}']") + expect(page).to have_link('image', href: "#{image_path}") click_on('image') - expect(current_path).to match("wikis/#{path}") + expect(current_path).to match(path) expect(page).not_to have_xpath('/html') # Page should render the image which means there is no html involved end end it 'shows the creation page if file does not exist' do - expect(page).to have_link('image', href: "#{project.wiki.wiki_base_path}/#{path}") + href = project_wiki_path(project, path) + + expect(page).to have_link('image', href: href) click_on('image') - expect(current_path).to match("wikis/#{path}") + expect(current_path).to match(href) expect(page).to have_content('Create New Page') end end diff --git a/spec/features/projects/wiki/user_views_wiki_pages_spec.rb b/spec/features/projects/wiki/user_views_wiki_pages_spec.rb index 6740df1d4ed..adb59c891c6 100644 --- a/spec/features/projects/wiki/user_views_wiki_pages_spec.rb +++ b/spec/features/projects/wiki/user_views_wiki_pages_spec.rb @@ -7,6 +7,7 @@ describe 'User views wiki pages' do let(:user) { create(:user) } let(:project) { create(:project, :wiki_repo, namespace: user.namespace) } + let(:project_wiki) { ProjectWiki.new(project, user) } let!(:wiki_page1) do create(:wiki_page, wiki: project.wiki, attrs: { title: '3 home', content: '3' }) @@ -17,73 +18,182 @@ describe 'User views wiki pages' do let!(:wiki_page3) do create(:wiki_page, wiki: project.wiki, attrs: { title: '2 home', content: '2' }) end + let!(:wiki_page4) do + create(:wiki_page, wiki: project.wiki, attrs: { title: 'sub-folder/0', content: 'a' }) + end + let!(:wiki_page5) do + create(:wiki_page, wiki: project.wiki, attrs: { title: 'sub-folder/b', content: 'b' }) + end + + let(:page_link_selector) { 'a' } let(:pages) do - page.find('.wiki-pages-list').all('li').map { |li| li.find('a') } + page.all(".wiki-pages-list li #{page_link_selector}") end + let(:wikis_allow_change_nesting) { false } before do + stub_feature_flags(wikis_allow_change_nesting: wikis_allow_change_nesting) project.add_maintainer(user) sign_in(user) visit(project_wikis_pages_path(project)) end + def sort_desc! + page.within('.wiki-sort-dropdown') do + page.find('.qa-reverse-sort').click + end + end + + def sort_by_created_at! + page.within('.wiki-sort-dropdown') do + click_button('Title') + click_link('Created date') + end + end + + shared_examples 'correctly_sorted_pages' do + it 'has pages displayed in correct order' do + displayed_texts = pages.map(&:text) + expect(displayed_texts).to eq expected_sequence.map(&:title) + end + end + context 'ordered by title' do - let(:pages_ordered_by_title) { [wiki_page2, wiki_page3, wiki_page1] } + let(:sub_folder) { project_wiki.find_dir('sub-folder') } - context 'asc' do - it 'pages are displayed in direct order' do - pages.each.with_index do |page_title, index| - expect(page_title.text).to eq(pages_ordered_by_title[index].title) + context 'default display settings' do + context 'asc' do + let(:expected_sequence) { [wiki_page2, wiki_page3, wiki_page1, sub_folder] } + + it_behaves_like 'correctly_sorted_pages' + end + + context 'desc' do + before do + sort_desc! end + + let(:expected_sequence) { [sub_folder, wiki_page1, wiki_page3, wiki_page2] } + + it_behaves_like 'correctly_sorted_pages' end end - context 'desc' do - before do - page.within('.wiki-sort-dropdown') do - page.find('.rspec-reverse-sort').click + context 'changing nesting is disabled' do + let(:wikis_allow_change_nesting) { false } + + it 'does not display a nesting controller' do + expect(page).not_to have_css('.wiki-nesting-dropdown') + end + end + + context 'changing nesting is enabled' do + let(:wikis_allow_change_nesting) { true } + + it 'displays a nesting controller' do + expect(page).to have_css('.wiki-nesting-dropdown') + end + + context 'tree' do + before do + page.within('.wiki-nesting-dropdown') do + click_link 'Show folder contents' + end + end + + context 'asc' do + let(:expected_sequence) { [wiki_page2, wiki_page3, wiki_page1, sub_folder, wiki_page4, wiki_page5] } + + it_behaves_like 'correctly_sorted_pages' + end + + context 'desc' do + before do + sort_desc! + end + + let(:expected_sequence) { [sub_folder, wiki_page5, wiki_page4, wiki_page1, wiki_page3, wiki_page2] } + + it_behaves_like 'correctly_sorted_pages' end end - it 'pages are displayed in reversed order' do - pages.reverse_each.with_index do |page_title, index| - expect(page_title.text).to eq(pages_ordered_by_title[index].title) + context 'nested' do + before do + page.within('.wiki-nesting-dropdown') do + click_link 'Hide folder contents' + end + end + + context 'asc' do + let(:expected_sequence) { [wiki_page2, wiki_page3, wiki_page1, sub_folder] } + + it_behaves_like 'correctly_sorted_pages' + end + + context 'desc' do + before do + sort_desc! + end + + let(:expected_sequence) { [sub_folder, wiki_page1, wiki_page3, wiki_page2] } + + it_behaves_like 'correctly_sorted_pages' + end + end + + context 'flat' do + before do + page.within('.wiki-nesting-dropdown') do + click_link 'Show files separately' + end + end + + let(:page_link_selector) { 'a.wiki-page-title' } + + context 'asc' do + let(:expected_sequence) { [wiki_page2, wiki_page3, wiki_page1, wiki_page4, wiki_page5] } + + it_behaves_like 'correctly_sorted_pages' + end + + context 'desc' do + before do + sort_desc! + end + + let(:expected_sequence) { [wiki_page5, wiki_page4, wiki_page1, wiki_page3, wiki_page2] } + + it_behaves_like 'correctly_sorted_pages' end end end end context 'ordered by created_at' do - let(:pages_ordered_by_created_at) { [wiki_page1, wiki_page2, wiki_page3] } + let(:pages_ordered_by_created_at) { [wiki_page1, wiki_page2, wiki_page3, wiki_page4, wiki_page5] } before do - page.within('.wiki-sort-dropdown') do - click_button('Title') - click_link('Created date') - end + sort_by_created_at! end + let(:page_link_selector) { 'a.wiki-page-title' } + context 'asc' do - it 'pages are displayed in direct order' do - pages.each.with_index do |page_title, index| - expect(page_title.text).to eq(pages_ordered_by_created_at[index].title) - end - end + let(:expected_sequence) { [wiki_page1, wiki_page2, wiki_page3, wiki_page4, wiki_page5] } + + it_behaves_like 'correctly_sorted_pages' end context 'desc' do before do - page.within('.wiki-sort-dropdown') do - page.find('.rspec-reverse-sort').click - end + sort_desc! end - it 'pages are displayed in reversed order' do - pages.reverse_each.with_index do |page_title, index| - expect(page_title.text).to eq(pages_ordered_by_created_at[index].title) - end - end + let(:expected_sequence) { [wiki_page5, wiki_page4, wiki_page3, wiki_page2, wiki_page1] } + + it_behaves_like 'correctly_sorted_pages' end end end diff --git a/spec/frontend/wikis_spec.js b/spec/frontend/wikis_spec.js index b2475488d97..a2b68bf866e 100644 --- a/spec/frontend/wikis_spec.js +++ b/spec/frontend/wikis_spec.js @@ -3,27 +3,27 @@ import { setHTMLFixture } from './helpers/fixtures'; describe('Wikis', () => { describe('setting the commit message when the title changes', () => { - const editFormHtmlFixture = args => `<form class="wiki-form ${ - args.newPage ? 'js-new-wiki-page' : '' - }"> - <input type="text" id="wiki_title" value="My title" /> - <input type="text" id="wiki_message" /> - </form>`; - let wikis; let titleInput; let messageInput; + const CREATE = true; + const UPDATE = false; - describe('when the wiki page is being created', () => { - const formHtmlFixture = editFormHtmlFixture({ newPage: true }); + const editFormHtmlFixture = newPage => + `<form class="wiki-form ${newPage ? 'js-new-wiki-page' : ''}"> + <input type="text" id="wiki_page_title" value="My title" /> + <input type="text" id="wiki_page_message" /> + </form>`; - beforeEach(() => { - setHTMLFixture(formHtmlFixture); + const init = newPage => { + setHTMLFixture(editFormHtmlFixture(newPage)); + titleInput = document.getElementById('wiki_page_title'); + messageInput = document.getElementById('wiki_page_message'); + wikis = new Wikis(); + }; - titleInput = document.getElementById('wiki_title'); - messageInput = document.getElementById('wiki_message'); - wikis = new Wikis(); - }); + describe('when the wiki page is being created', () => { + beforeEach(() => init(CREATE)); it('binds an event listener to the title input', () => { wikis.handleWikiTitleChange = jest.fn(); @@ -51,15 +51,7 @@ describe('Wikis', () => { }); describe('when the wiki page is being updated', () => { - const formHtmlFixture = editFormHtmlFixture({ newPage: false }); - - beforeEach(() => { - setHTMLFixture(formHtmlFixture); - - titleInput = document.getElementById('wiki_title'); - messageInput = document.getElementById('wiki_message'); - wikis = new Wikis(); - }); + beforeEach(() => init(UPDATE)); it('sets the commit message when title changes, prefixing with "Update"', () => { titleInput.value = 'My title'; diff --git a/spec/helpers/wiki_helper_spec.rb b/spec/helpers/wiki_helper_spec.rb index bcc2bd71da1..4310e4bd22c 100644 --- a/spec/helpers/wiki_helper_spec.rb +++ b/spec/helpers/wiki_helper_spec.rb @@ -23,8 +23,13 @@ describe WikiHelper do describe '#wiki_sort_controls' do let(:project) { create(:project) } - let(:wiki_link) { helper.wiki_sort_controls(project, sort, direction) } - let(:classes) { "btn btn-default has-tooltip reverse-sort-btn qa-reverse-sort rspec-reverse-sort" } + let(:classes) { described_class::WIKI_SORT_CSS_CLASSES } + + subject(:wiki_link) do + helper.wiki_sort_controls(sort: sort, direction: direction) do |opts| + project_wikis_pages_path(project, opts) + end + end def expected_link(sort, direction, icon_class) path = "/#{project.full_path}/wikis/pages?direction=#{direction}&sort=#{sort}" @@ -62,6 +67,18 @@ describe WikiHelper do end end + describe '#wiki_show_children_icon' do + ProjectWiki::NESTINGS.each do |nesting| + context "When the nesting parameter is `#{nesting}`" do + let(:element) { helper.wiki_show_children_icon(nesting) } + + it 'produces something that contains an SVG' do + expect(element).to match(/svg/) + end + end + end + end + describe '#wiki_sort_title' do it 'returns a title corresponding to a key' do expect(helper.wiki_sort_title('created_at')).to eq('Created date') diff --git a/spec/lib/banzai/filter/wiki_link_filter_spec.rb b/spec/lib/banzai/filter/wiki_link_filter_spec.rb index 4587bd85939..7a83dc3ec1d 100644 --- a/spec/lib/banzai/filter/wiki_link_filter_spec.rb +++ b/spec/lib/banzai/filter/wiki_link_filter_spec.rb @@ -11,6 +11,10 @@ describe Banzai::Filter::WikiLinkFilter do let(:wiki) { ProjectWiki.new(project, user) } let(:repository_upload_folder) { Wikis::CreateAttachmentService::ATTACHMENT_PATH } + def upload_href(file_name) + ::File.join(wiki.wiki_page_path, repository_upload_folder, file_name) + end + it "doesn't rewrite absolute links" do filtered_link = filter("<a href='http://example.com:8000/'>Link</a>", project_wiki: wiki).children[0] @@ -28,12 +32,12 @@ describe Banzai::Filter::WikiLinkFilter do it 'rewrites links' do filtered_link = filter("<a href='#{repository_upload_folder}/a.test'>Link</a>", project_wiki: wiki).children[0] - expect(filtered_link.attribute('href').value).to eq("#{wiki.wiki_base_path}/#{repository_upload_folder}/a.test") + expect(filtered_link.attribute('href').value).to eq(upload_href "a.test") end end context 'with "img" html tag' do - let(:path) { "#{wiki.wiki_base_path}/#{repository_upload_folder}/a.jpg" } + let(:path) { upload_href "a.jpg" } context 'inside an "a" html tag' do it 'rewrites links' do @@ -57,7 +61,7 @@ describe Banzai::Filter::WikiLinkFilter do it 'rewrites links' do filtered_link = filter("<video src='#{repository_upload_folder}/a.mp4'></video>", project_wiki: wiki).children[0] - expect(filtered_link.attribute('src').value).to eq("#{wiki.wiki_base_path}/#{repository_upload_folder}/a.mp4") + expect(filtered_link.attribute('src').value).to eq(upload_href "a.mp4") end end @@ -65,7 +69,8 @@ describe Banzai::Filter::WikiLinkFilter do it 'rewrites links' do filtered_link = filter("<audio src='#{repository_upload_folder}/a.wav'></audio>", project_wiki: wiki).children[0] - expect(filtered_link.attribute('src').value).to eq("#{wiki.wiki_base_path}/#{repository_upload_folder}/a.wav") + # expect(filtered_link.attribute('src').value).to eq("#{wiki.wiki_base_path}/#{repository_upload_folder}/a.wav") + expect(filtered_link.attribute('src').value).to eq(upload_href "a.wav") end end end diff --git a/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb b/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb index 26f2b0b0acf..7f60106cdca 100644 --- a/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb +++ b/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb @@ -3,6 +3,12 @@ require 'spec_helper' describe Banzai::Pipeline::WikiPipeline do + let_it_be(:namespace) { create(:namespace, name: "wiki_link_ns") } + let_it_be(:project) { create(:project, :public, name: "wiki_link_project", namespace: namespace) } + let_it_be(:project_wiki) { ProjectWiki.new(project, double(:user)) } + let_it_be(:page) { build(:wiki_page, wiki: project_wiki, page: OpenStruct.new(url_path: 'nested/twice/start-page')) } + let(:prefix) { project_wiki.wiki_page_path } + describe 'TableOfContents' do it 'replaces the tag with the TableOfContentsFilter result' do markdown = <<-MD.strip_heredoc @@ -54,132 +60,138 @@ describe Banzai::Pipeline::WikiPipeline do end describe "Links" do - let(:namespace) { create(:namespace, name: "wiki_link_ns") } - let(:project) { create(:project, :public, name: "wiki_link_project", namespace: namespace) } - let(:project_wiki) { ProjectWiki.new(project, double(:user)) } - let(:page) { build(:wiki_page, wiki: project_wiki, page: OpenStruct.new(url_path: 'nested/twice/start-page')) } - - { "when GitLab is hosted at a root URL" => '/', - "when GitLab is hosted at a relative URL" => '/nested/relative/gitlab' }.each do |test_name, relative_url_root| - context test_name do + shared_examples 'a correct link rewrite' do + it 'rewrites links correctly' do + output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug) + + expect(output).to include("href=\"#{page_href}\"") + end + end + + shared_examples 'link examples' do |test_name| + let(:page_href) { "#{prefix}/#{expected_page_path}" } + + context "when GitLab is hosted at a #{test_name} URL" do before do allow(Gitlab.config.gitlab).to receive(:relative_url_root).and_return(relative_url_root) end describe "linking to pages within the wiki" do - context "when creating hierarchical links to the current directory" do - it "rewrites non-file links to be at the scope of the current directory" do - markdown = "[Page](./page)" - output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug) + let(:markdown) { "[Page](#{nesting}page#{extension})" } - expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/nested/twice/page\"") + context "when creating hierarchical links to the current directory" do + let(:nesting) { './' } + context 'non file links' do + let(:extension) { '' } + let(:expected_page_path) { 'nested/twice/page' } + it_behaves_like 'a correct link rewrite' end - it "rewrites file links to be at the scope of the current directory" do - markdown = "[Link to Page](./page.md)" - output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug) - - expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/nested/twice/page.md\"") + context 'file-like links' do + let(:extension) { '.md' } + let(:expected_page_path) { 'nested/twice/page.md' } + it_behaves_like 'a correct link rewrite' end end context "when creating hierarchical links to the parent directory" do - it "rewrites non-file links to be at the scope of the parent directory" do - markdown = "[Link to Page](../page)" - output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug) - - expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/nested/page\"") + let(:nesting) { '../' } + context "non file links" do + let(:extension) { '' } + let(:expected_page_path) { 'nested/page' } + it_behaves_like 'a correct link rewrite' end - it "rewrites file links to be at the scope of the parent directory" do - markdown = "[Link to Page](../page.md)" - output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug) - - expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/nested/page.md\"") + context "file-like links" do + let(:extension) { '.md' } + let(:expected_page_path) { 'nested/page.md' } + it_behaves_like 'a correct link rewrite' end end context "when creating hierarchical links to a sub-directory" do - it "rewrites non-file links to be at the scope of the sub-directory" do - markdown = "[Link to Page](./subdirectory/page)" - output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug) + let(:nesting) { './subdirectory/' } - expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/nested/twice/subdirectory/page\"") + context "non file links" do + let(:extension) { '' } + let(:expected_page_path) { 'nested/twice/subdirectory/page' } + it_behaves_like 'a correct link rewrite' end - it "rewrites file links to be at the scope of the sub-directory" do - markdown = "[Link to Page](./subdirectory/page.md)" - output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug) - - expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/nested/twice/subdirectory/page.md\"") + context 'file-like links' do + let(:extension) { '.md' } + let(:expected_page_path) { 'nested/twice/subdirectory/page.md' } + it_behaves_like 'a correct link rewrite' end end describe "when creating non-hierarchical links" do - it 'rewrites non-file links to be at the scope of the wiki root' do - markdown = "[Link to Page](page)" - output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug) + let(:nesting) { '' } - expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/page\"") + context 'non-file links' do + let(:extension) { '' } + let(:expected_page_path) { 'page' } + it_behaves_like 'a correct link rewrite' end - it 'rewrites non-file links (with spaces) to be at the scope of the wiki root' do - markdown = "[Link to Page](page slug)" - output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug) - - expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/page%20slug\"") + context 'non-file links (with spaces)' do + let(:extension) { ' slug' } + let(:expected_page_path) { 'page%20slug' } + it_behaves_like 'a correct link rewrite' end - it "rewrites file links to be at the scope of the current directory" do - markdown = "[Link to Page](page.md)" - output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug) - - expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/nested/twice/page.md\"") + context "file links" do + let(:extension) { '.md' } + let(:expected_page_path) { 'nested/twice/page.md' } + it_behaves_like 'a correct link rewrite' end - it 'rewrites links with anchor' do - markdown = '[Link to Header](start-page#title)' - output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug) - - expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/start-page#title\"") + context 'links with anchor' do + let(:extension) { '#title' } + let(:expected_page_path) { 'page#title' } + it_behaves_like 'a correct link rewrite' end - it 'rewrites links (with spaces) with anchor' do - markdown = '[Link to Header](start page#title)' - output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug) - - expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/start%20page#title\"") + context 'links (with spaces) with anchor' do + let(:extension) { ' two#title' } + let(:expected_page_path) { 'page%20two#title' } + it_behaves_like 'a correct link rewrite' end end describe "when creating root links" do - it 'rewrites non-file links to be at the scope of the wiki root' do - markdown = "[Link to Page](/page)" - output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug) + let(:nesting) { '/' } - expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/page\"") + context 'non-file links' do + let(:extension) { '' } + let(:expected_page_path) { 'page' } + it_behaves_like 'a correct link rewrite' end - it 'rewrites file links to be at the scope of the wiki root' do - markdown = "[Link to Page](/page.md)" - output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug) - - expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/page.md\"") + context 'file links' do + let(:extension) { '.md' } + let(:expected_page_path) { 'page.md' } + it_behaves_like 'a correct link rewrite' end end end describe "linking to pages outside the wiki (absolute)" do - it "doesn't rewrite links" do - markdown = "[Link to Page](http://example.com/page)" - output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug) - - expect(output).to include('href="http://example.com/page"') - end + let(:markdown) { "[Link to Page](http://example.com/page)" } + let(:page_href) { 'http://example.com/page' } + it_behaves_like 'a correct link rewrite' end end end + include_examples 'link examples', :root do + let(:relative_url_root) { '/' } + end + + include_examples 'link examples', :relative do + let(:relative_url_root) { '/nested/relative/gitlab' } + end + describe "checking slug validity when assembling links" do context "with a valid slug" do let(:valid_slug) { "http://example.com" } @@ -261,37 +273,54 @@ describe Banzai::Pipeline::WikiPipeline do end describe 'videos and audio' do - let_it_be(:namespace) { create(:namespace, name: "wiki_link_ns") } - let_it_be(:project) { create(:project, :public, name: "wiki_link_project", namespace: namespace) } - let_it_be(:project_wiki) { ProjectWiki.new(project, double(:user)) } - let_it_be(:page) { build(:wiki_page, wiki: project_wiki, page: OpenStruct.new(url_path: 'nested/twice/start-page')) } + def src(file_name) + "#{prefix}/nested/twice/#{file_name}" + end - it 'generates video html structure' do - markdown = "![video_file](video_file_name.mp4)" - output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug) + shared_examples 'correct video rewrite' do + let(:markdown) { "![video_file](#{file_name})" } + let(:video_fragment) { "<video src=\"#{prefix}/#{expected_file_path}\"" } + let(:options) do + { + project: project, + project_wiki: project_wiki, + page_slug: page.slug + } + end + + it 'generates video html structure' do + output = described_class.to_html(markdown, options) - expect(output).to include('<video src="/wiki_link_ns/wiki_link_project/wikis/nested/twice/video_file_name.mp4"') + expect(output).to include(video_fragment) + end end - it 'rewrites and replaces video links names with white spaces to %20' do - markdown = "![video file](video file name.mp4)" - output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug) + context 'underscores' do + let(:file_name) { 'video_file_name.mp4' } + let(:expected_file_path) { 'nested/twice/video_file_name.mp4' } + it_behaves_like 'correct video rewrite' + end - expect(output).to include('<video src="/wiki_link_ns/wiki_link_project/wikis/nested/twice/video%20file%20name.mp4"') + context 'spaces' do + let(:file_name) { 'video file name.mp4' } + let(:expected_file_path) { 'nested/twice/video%20file%20name.mp4' } + it_behaves_like 'correct video rewrite' end it 'generates audio html structure' do markdown = "![audio_file](audio_file_name.wav)" + safe_name = "audio_file_name.wav" output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug) - expect(output).to include('<audio src="/wiki_link_ns/wiki_link_project/wikis/nested/twice/audio_file_name.wav"') + expect(output).to include(%Q'<audio src="#{src(safe_name)}"') end it 'rewrites and replaces audio links names with white spaces to %20' do markdown = "![audio file](audio file name.wav)" + safe_name = "audio%20file%20name.wav" output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug) - expect(output).to include('<audio src="/wiki_link_ns/wiki_link_project/wikis/nested/twice/audio%20file%20name.wav"') + expect(output).to include(%Q'<audio src="#{src(safe_name)}"') end end end diff --git a/spec/lib/gitlab/url_builder_spec.rb b/spec/lib/gitlab/url_builder_spec.rb index 08d3c638f9e..4ed2c232f97 100644 --- a/spec/lib/gitlab/url_builder_spec.rb +++ b/spec/lib/gitlab/url_builder_spec.rb @@ -150,7 +150,7 @@ describe Gitlab::UrlBuilder do wiki_page = build(:wiki_page) url = described_class.build(wiki_page) - expect(url).to eq "#{Gitlab.config.gitlab.url}#{wiki_page.wiki.wiki_base_path}/#{wiki_page.slug}" + expect(url).to eq "#{Gitlab.config.gitlab.url}#{wiki_page.wiki.wiki_page_path}/#{wiki_page.slug}" end end end diff --git a/spec/lib/gitlab/utils_spec.rb b/spec/lib/gitlab/utils_spec.rb index 890918d4a7c..a68434c8c66 100644 --- a/spec/lib/gitlab/utils_spec.rb +++ b/spec/lib/gitlab/utils_spec.rb @@ -252,4 +252,41 @@ describe Gitlab::Utils do expect(described_class.string_to_ip_object('1:0:0:0:0:0:0:0/124')).to eq(IPAddr.new('1:0:0:0:0:0:0:0/124')) end end + + describe '.allow_hash_values' do + it 'removes keys that do not pass the inclusion filters' do + symbols = %i[x y z] + ints = (0..100) + strings = %w[foo bar baz].to_set + + hash = { + a: :x, + b: 100, + c: 'foo', + d: :irrelevant, + aa: :w, + bb: 200, + cc: 'food', + dd: :totally_irrelevant + } + allowed = { + a: symbols, + b: ints, + c: strings, + aa: symbols, + bb: ints, + cc: strings + } + + described_class.allow_hash_values(hash, allowed) + + expect(hash).to eq({ + a: :x, + b: 100, + c: 'foo', + d: :irrelevant, + dd: :totally_irrelevant + }) + end + end end diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb index 31d1d1fd7d1..f5c827e5a29 100644 --- a/spec/models/project_wiki_spec.rb +++ b/spec/models/project_wiki_spec.rb @@ -28,7 +28,9 @@ describe ProjectWiki do describe '#web_url' do it 'returns the full web URL to the wiki' do - expect(subject.web_url).to eq("#{Gitlab.config.gitlab.url}/#{project.full_path}/wikis/home") + home_url = Gitlab::Routing.url_helpers.project_wiki_url(project, :home) + + expect(subject.web_url).to eq(home_url) end end @@ -71,9 +73,23 @@ describe ProjectWiki do describe "#wiki_base_path" do it "returns the wiki base path" do - wiki_base_path = "#{Gitlab.config.gitlab.relative_url_root}/#{project.full_path}/wikis" + wiki_path = Gitlab::Routing.url_helpers.project_wikis_path(project) + + expect(subject.wiki_base_path).to eq(wiki_path) + end + end - expect(subject.wiki_base_path).to eq(wiki_base_path) + describe "#wiki_page_path" do + let(:page) { create(:wiki_page, wiki: project_wiki) } + + describe 'suffixed with /:page_slug' do + subject { "#{project_wiki.wiki_page_path}/#{page.slug}" } + + it "equals the project_wiki_path" do + path = Gitlab::Routing.url_helpers.project_wiki_path(project, page) + + expect(subject).to eq(path) + end end end diff --git a/spec/models/wiki_directory_spec.rb b/spec/models/wiki_directory_spec.rb index 5fbcccf897e..aff21888605 100644 --- a/spec/models/wiki_directory_spec.rb +++ b/spec/models/wiki_directory_spec.rb @@ -1,14 +1,94 @@ # frozen_string_literal: true require 'spec_helper' +require 'set' RSpec.describe WikiDirectory do + include GitHelpers + + let(:project) { create(:project, :wiki_repo) } + let(:user) { project.owner } + let(:wiki) { ProjectWiki.new(project, user) } + describe 'validations' do subject { build(:wiki_directory) } it { is_expected.to validate_presence_of(:slug) } end + describe '.group_by_directory' do + context 'when there are no pages' do + it 'returns an empty array' do + expect(described_class.group_by_directory(nil)).to eq([]) + expect(described_class.group_by_directory([])).to eq([]) + end + end + + context 'when there are pages' do + before do + create_page('dir_1/dir_1_1/page_3', 'content') + create_page('page_1', 'content') + create_page('dir_1/page_2', 'content') + create_page('dir_2', 'page with dir name') + create_page('dir_2/page_5', 'content') + create_page('page_6', 'content') + create_page('dir_2/page_4', 'content') + end + + let(:page_1) { wiki.find_page('page_1') } + let(:page_6) { wiki.find_page('page_6') } + let(:page_dir_2) { wiki.find_page('dir_2') } + + let(:dir_1) do + described_class.new('dir_1', [wiki.find_page('dir_1/page_2')]) + end + let(:dir_1_1) do + described_class.new('dir_1/dir_1_1', [wiki.find_page('dir_1/dir_1_1/page_3')]) + end + let(:dir_2) do + pages = [wiki.find_page('dir_2/page_5'), + wiki.find_page('dir_2/page_4')] + described_class.new('dir_2', pages) + end + + context "#list_pages" do + shared_examples "a correct grouping" do + let(:grouped_slugs) { grouped_entries.map(&method(:slugs)) } + let(:expected_slugs) { expected_grouped_entries.map(&method(:slugs)).map(&method(:match_array)) } + + it 'returns an array with pages and directories' do + expect(grouped_slugs).to match_array(expected_slugs) + end + end + + context 'sort by title' do + let(:grouped_entries) { described_class.group_by_directory(wiki.list_pages) } + + let(:expected_grouped_entries) { [dir_1_1, dir_1, page_dir_2, dir_2, page_1, page_6] } + + it_behaves_like "a correct grouping" + end + + context 'sort by created_at' do + let(:grouped_entries) { described_class.group_by_directory(wiki.list_pages(sort: 'created_at')) } + let(:expected_grouped_entries) { [dir_1_1, page_1, dir_1, page_dir_2, dir_2, page_6] } + + it_behaves_like "a correct grouping" + end + + it 'returns an array with retained order with directories at the top' do + expected_order = ['dir_1/dir_1_1/page_3', 'dir_1/page_2', 'dir_2', 'dir_2/page_4', 'dir_2/page_5', 'page_1', 'page_6'] + + grouped_entries = described_class.group_by_directory(wiki.list_pages) + + actual_order = grouped_entries.flat_map(&method(:slugs)) + + expect(actual_order).to eq(expected_order) + end + end + end + end + describe '#initialize' do context 'when there are pages' do let(:pages) { [build(:wiki_page)] } @@ -40,7 +120,112 @@ RSpec.describe WikiDirectory do it 'returns the relative path to the partial to be used' do directory = build(:wiki_directory) - expect(directory.to_partial_path).to eq('projects/wikis/wiki_directory') + expect(directory.to_partial_path).to eq('projects/wiki_directories/wiki_directory') + end + end + + describe 'attributes' do + def page_path(index) + "dir-path/page-#{index}" + end + + let(:page_paths) { (1..3).map { |n| page_path(n) } } + + let(:pages) do + page_paths.map { |p| wiki.find_page(p) } end + + subject { described_class.new('dir-path', pages) } + + context 'there are no pages' do + let(:pages) { [] } + + it { is_expected.to have_attributes(page_count: 0, last_version: be_nil) } + end + + context 'there is one page' do + before do + create_page("dir-path/singleton", "Just this page") + end + + let(:the_page) { wiki.find_page("dir-path/singleton") } + let(:pages) { [the_page] } + + it { is_expected.to have_attributes(page_count: 1, last_version: the_page.last_version) } + end + + context 'there are a few pages, each with a single version' do + before do + page_paths.each_with_index do |path, n| + Timecop.freeze(Time.local(1990) + n.minutes) do + create_page(path, "this is page #{n}") + end + end + end + + let(:expected_last_version) { pages.last.last_version } + + it { is_expected.to have_attributes(page_count: 3, last_version: expected_last_version) } + end + + context 'there are a few pages, each with a few versions' do + before do + page_paths.each_with_index do |path, n| + t = Time.local(1990) + n.minutes + Timecop.freeze(t) do + create_page(path, "This is page #{n}") + (2..3).each do |v| + Timecop.freeze(t + v.seconds) do + update_page(path, "Now at version #{v}") + end + end + end + end + end + + it { is_expected.to have_attributes(page_count: 3, last_version: pages.last.last_version) } + end + end + + private + + def create_page(name, content) + wiki.wiki.write_page(name, :markdown, content, commit_details) + set_time(name) + end + + def update_page(name, content) + wiki.wiki.update_page(name, name, :markdown, content, update_commit_details) + set_time(name) + end + + def set_time(name) + return unless Timecop.frozen? + + new_date = Time.now + page = wiki.find_page(name).page + commit = page.version.commit + repo = commit.instance_variable_get(:@repository) + + rug_commit = rugged_repo_at_path(repo.relative_path).lookup(commit.id) + rug_commit.amend( + message: rug_commit.message, + tree: rug_commit.tree, + author: rug_commit.author.merge(time: new_date), + committer: rug_commit.committer.merge(time: new_date), + update_ref: 'HEAD' + ) + end + + def commit_details + Gitlab::Git::Wiki::CommitDetails.new(user.id, user.username, user.name, user.email, "test commit") + end + + def update_commit_details + Gitlab::Git::Wiki::CommitDetails.new(user.id, user.username, user.name, user.email, "test update") + end + + def slugs(thing) + Array.wrap(thing.respond_to?(:pages) ? thing.pages.map(&:slug) : thing.slug) end end diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb index 18c62c917dc..8f3730f97ac 100644 --- a/spec/models/wiki_page_spec.rb +++ b/spec/models/wiki_page_spec.rb @@ -9,87 +9,6 @@ describe WikiPage do subject { described_class.new(wiki) } - describe '.group_by_directory' do - context 'when there are no pages' do - it 'returns an empty array' do - expect(described_class.group_by_directory(nil)).to eq([]) - expect(described_class.group_by_directory([])).to eq([]) - end - end - - context 'when there are pages' do - before do - create_page('dir_1/dir_1_1/page_3', 'content') - create_page('page_1', 'content') - create_page('dir_1/page_2', 'content') - create_page('dir_2', 'page with dir name') - create_page('dir_2/page_5', 'content') - create_page('page_6', 'content') - create_page('dir_2/page_4', 'content') - end - - let(:page_1) { wiki.find_page('page_1') } - let(:page_6) { wiki.find_page('page_6') } - let(:page_dir_2) { wiki.find_page('dir_2') } - - let(:dir_1) do - WikiDirectory.new('dir_1', [wiki.find_page('dir_1/page_2')]) - end - let(:dir_1_1) do - WikiDirectory.new('dir_1/dir_1_1', [wiki.find_page('dir_1/dir_1_1/page_3')]) - end - let(:dir_2) do - pages = [wiki.find_page('dir_2/page_5'), - wiki.find_page('dir_2/page_4')] - WikiDirectory.new('dir_2', pages) - end - - context "#list_pages" do - context 'sort by title' do - let(:grouped_entries) { described_class.group_by_directory(wiki.list_pages) } - let(:expected_grouped_entries) { [dir_1_1, dir_1, page_dir_2, dir_2, page_1, page_6] } - - it 'returns an array with pages and directories' do - grouped_entries.each_with_index do |page_or_dir, i| - expected_page_or_dir = expected_grouped_entries[i] - expected_slugs = get_slugs(expected_page_or_dir) - slugs = get_slugs(page_or_dir) - - expect(slugs).to match_array(expected_slugs) - end - end - end - - context 'sort by created_at' do - let(:grouped_entries) { described_class.group_by_directory(wiki.list_pages(sort: 'created_at')) } - let(:expected_grouped_entries) { [dir_1_1, page_1, dir_1, page_dir_2, dir_2, page_6] } - - it 'returns an array with pages and directories' do - grouped_entries.each_with_index do |page_or_dir, i| - expected_page_or_dir = expected_grouped_entries[i] - expected_slugs = get_slugs(expected_page_or_dir) - slugs = get_slugs(page_or_dir) - - expect(slugs).to match_array(expected_slugs) - end - end - end - - it 'returns an array with retained order with directories at the top' do - expected_order = ['dir_1/dir_1_1/page_3', 'dir_1/page_2', 'dir_2', 'dir_2/page_4', 'dir_2/page_5', 'page_1', 'page_6'] - - grouped_entries = described_class.group_by_directory(wiki.list_pages) - - actual_order = - grouped_entries.flat_map do |page_or_dir| - get_slugs(page_or_dir) - end - expect(actual_order).to eq(expected_order) - end - end - end - end - describe '.unhyphenize' do it 'removes hyphens from a name' do name = 'a-name--with-hyphens' @@ -505,7 +424,7 @@ describe WikiPage do it 'returns the relative path to the partial to be used' do page = build(:wiki_page) - expect(page.to_partial_path).to eq('projects/wikis/wiki_page') + expect(page.to_partial_path).to eq('projects/wiki_pages/wiki_page') end end @@ -585,12 +504,4 @@ describe WikiPage do page = wiki.wiki.page(title: title, dir: dir) wiki.delete_page(page, "test commit") end - - def get_slugs(page_or_dir) - if page_or_dir.is_a? WikiPage - [page_or_dir.slug] - else - page_or_dir.pages.present? ? page_or_dir.pages.map(&:slug) : [] - end - end end diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index acdbf064a73..3e57bb70287 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -3,9 +3,12 @@ require 'spec_helper' describe 'project routing' do before do allow(Project).to receive(:find_by_full_path).and_return(false) - allow(Project).to receive(:find_by_full_path).with('gitlab/gitlabhq', any_args).and_return(true) + allow(Project).to receive(:find_by_full_path).with('gitlab/gitlabhq', any_args).and_return(project) end + set(:namespace) { create(:namespace, name: 'gitlab') } + set(:project) { create(:project, namespace: namespace, name: 'gitlabhq') } + # Shared examples for a resource inside a Project # # By default it tests all the default REST actions: index, create, new, edit, @@ -145,24 +148,39 @@ describe 'project routing' do it_behaves_like 'redirecting a legacy project path', "/gitlab/gitlabhq/autocomplete_sources/labels", "/gitlab/gitlabhq/-/autocomplete_sources/labels" end - # pages_project_wikis GET /:project_id/wikis/pages(.:format) projects/wikis#pages - # history_project_wiki GET /:project_id/wikis/:id/history(.:format) projects/wikis#history - # project_wikis POST /:project_id/wikis(.:format) projects/wikis#create - # edit_project_wiki GET /:project_id/wikis/:id/edit(.:format) projects/wikis#edit - # project_wiki GET /:project_id/wikis/:id(.:format) projects/wikis#show - # DELETE /:project_id/wikis/:id(.:format) projects/wikis#destroy + # GET /:project_id/wikis/pages(.:format) projects/wikis#pages + # GET /:project_id/-/wiki_pages/:id/history(.:format) projects/wiki_pages#history + # POST /:project_id/-/wiki_pages(.:format) projects/wiki_pages#create + # GET /:project_id/-/wiki_pages/:id/edit(.:format) projects/wiki_pages#edit + # GET /:project_id/-/wiki_pages/:id(.:format) projects/wiki_pages#show + # DELETE /:project_id/-/wiki_pages/:id(.:format) projects/wiki_pages#destroy describe Projects::WikisController, 'routing' do - it 'to #pages' do - expect(get('/gitlab/gitlabhq/wikis/pages')).to route_to('projects/wikis#pages', namespace_id: 'gitlab', project_id: 'gitlabhq') + let(:wiki) { ProjectWiki.new(project, project.owner) } + let(:wiki_page) { create(:wiki_page, wiki: wiki) } + + it '#pages' do + expect(get('/gitlab/gitlabhq/wikis/pages')) + .to route_to('projects/wikis#pages', + namespace_id: 'gitlab', + project_id: 'gitlabhq') end - it 'to #history' do - expect(get('/gitlab/gitlabhq/wikis/1/history')).to route_to('projects/wikis#history', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') + describe '#history' do + let(:history_path) { project_wiki_history_path(project, wiki_page) } + + it 'routes to history' do + expect(get(history_path)) + .to route_to('projects/wiki_pages#history', + namespace_id: namespace.path, + project_id: project.name, + id: wiki_page.slug) + end end it_behaves_like 'RESTful project resources' do let(:actions) { [:create, :edit, :show, :destroy] } - let(:controller) { 'wikis' } + let(:controller) { 'wiki_pages' } + let(:controller_path) { '-/wiki_pages' } end end diff --git a/spec/routing/wiki_routing_spec.rb b/spec/routing/wiki_routing_spec.rb new file mode 100644 index 00000000000..94349dbaa74 --- /dev/null +++ b/spec/routing/wiki_routing_spec.rb @@ -0,0 +1,103 @@ +# frozen_string_literal: true + +require 'spec_helper' + +# We build URIs to wiki pages manually in various places (most notably +# in markdown generation). To ensure these do not get out of sync, these +# tests verify that our path generation assumptions are sound. +describe 'Wiki path generation assumptions' do + set(:project) { create(:project, :public, :repository) } + + let(:project_wiki) { ProjectWiki.new(project, project.owner) } + let(:some_page_name) { 'some-wiki-page' } + let(:wiki_page) do + create(:wiki_page, wiki: project_wiki, attrs: { title: some_page_name }) + end + + describe 'WikiProject#wiki_page_path', 'routing' do + it 'is consistent with routing to wiki#show' do + uri = URI.parse(project_wiki.wiki_page_path) + path = ::File.join(uri.path, some_page_name) + + expect(get('/' + path)).to route_to('projects/wiki_pages#show', + id: some_page_name, + namespace_id: project.namespace.to_param, + project_id: project.to_param) + end + end + + describe 'project_wiki_path', 'routing' do + describe 'GET' do + it 'routes to the :show action' do + path = project_wiki_path(project, wiki_page) + + expect(get('/' + path)).to route_to('projects/wiki_pages#show', + id: wiki_page.slug, + namespace_id: project.namespace.to_param, + project_id: project.to_param) + end + end + end + + describe 'project_wiki_pages_new_path', 'routing' do + describe 'GET' do + it 'routes to the :new action' do + path = project_wiki_pages_new_path(project) + + expect(get('/' + path)).to route_to('projects/wiki_pages#new', + namespace_id: project.namespace.to_param, + project_id: project.to_param) + end + end + end + + # Early versions of the wiki paths routed all wiki pages at + # /wikis/:id - this test exists to guarantee that we support + # old URIs that may be out there, saved in bookmarks, on other wikis, etc. + describe 'legacy route support', type: 'request' do + let(:path) { ::File.join(project_wikis_path(project), some_page_name) } + + before do + get(path) + end + + it 'routes to new wiki paths' do + dest = project_wiki_path(project, wiki_page) + + expect(response).to redirect_to(dest) + end + + context 'the page is nested in a directory' do + let(:some_page_name) { 'some-dir/some-deep-dir/some-page' } + let(:path) { ::File.join(project_wikis_path(project), some_page_name) } + + it 'still routes correctly' do + dest = project_wiki_path(project, wiki_page) + + expect(response).to redirect_to(dest) + end + end + + context 'the user requested the old history path' do + let(:some_page_name) { 'some-dir/some-deep-dir/some-page' } + let(:path) { ::File.join(project_wikis_path(project), some_page_name, 'history') } + + it 'redirects to the new history path' do + dest = project_wiki_history_path(project, wiki_page) + + expect(response).to redirect_to(dest) + end + end + + context 'the user requested the old edit path' do + let(:some_page_name) { 'some-dir/some-deep-dir/some-page' } + let(:path) { ::File.join(project_wikis_path(project), some_page_name, 'edit') } + + it 'redirects to the new history path' do + dest = project_wiki_edit_path(project, wiki_page) + + expect(response).to redirect_to(dest) + end + end + end +end diff --git a/spec/support/controllers/authorization_helpers.rb b/spec/support/controllers/authorization_helpers.rb new file mode 100644 index 00000000000..e1786e0ca8a --- /dev/null +++ b/spec/support/controllers/authorization_helpers.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +def forbid_controller_ability!(ability) + allow(controller).to receive(:can?).and_call_original + allow(controller).to receive(:can?).with(anything, ability, any_args).and_return(false) +end diff --git a/spec/support/helpers/capybara_helpers.rb b/spec/support/helpers/capybara_helpers.rb index a7baa7042c9..13ec179b734 100644 --- a/spec/support/helpers/capybara_helpers.rb +++ b/spec/support/helpers/capybara_helpers.rb @@ -46,4 +46,14 @@ module CapybaraHelpers def javascript_test? Capybara.current_driver == Capybara.javascript_driver end + + def scroll_to(element) + raise 'JS not available' unless javascript_test? + + script = <<-JS + arguments[0].scrollIntoView(true); + JS + + page.driver.browser.execute_script(script, element.native) + end end diff --git a/spec/support/helpers/dropzone_helper.rb b/spec/support/helpers/dropzone_helper.rb index a0f261b312e..3f37673dadd 100644 --- a/spec/support/helpers/dropzone_helper.rb +++ b/spec/support/helpers/dropzone_helper.rb @@ -14,6 +14,8 @@ module DropzoneHelper # If it's 'false', then the helper will NOT wait for backend response # It lets to test behaviors while AJAX is processing. def dropzone_file(files, max_file_size = 0, wait_for_queuecomplete = true) + # Assert that there is a dropzone to use (waiting until it is ready) + expect(page).to have_css('.div-dropzone') # Generate a fake file input that Capybara can attach to page.execute_script <<-JS.strip_heredoc $('#fakeFileInput').remove(); diff --git a/spec/support/helpers/git_helpers.rb b/spec/support/helpers/git_helpers.rb index 99c5871ba54..05e31a1154a 100644 --- a/spec/support/helpers/git_helpers.rb +++ b/spec/support/helpers/git_helpers.rb @@ -2,8 +2,11 @@ module GitHelpers def rugged_repo(repository) - path = File.join(TestEnv.repos_path, repository.disk_path + '.git') + rugged_repo_at_path(repository.disk_path + '.git') + end + def rugged_repo_at_path(relative_path) + path = File.join(TestEnv.repos_path, relative_path) Rugged::Repository.new(path) end end diff --git a/spec/support/helpers/wiki_helpers.rb b/spec/support/helpers/wiki_helpers.rb index 06cea728b42..0cf70fd4ef7 100644 --- a/spec/support/helpers/wiki_helpers.rb +++ b/spec/support/helpers/wiki_helpers.rb @@ -12,4 +12,10 @@ module WikiHelpers ::Wikis::CreateAttachmentService.new(project, user, opts) .execute[:result][:file_path] end + + # Generate the form field name for a given attribute of an object. + # This is rather general, but is currently only used in the wiki featur tests. + def form_field_name(obj, attr_name) + "#{ActiveModel::Naming.param_key(obj)}[#{attr_name}]" + end end diff --git a/spec/support/matchers/issuable_matchers.rb b/spec/support/matchers/issuable_matchers.rb index 743f0b8c932..ab15a80bf60 100644 --- a/spec/support/matchers/issuable_matchers.rb +++ b/spec/support/matchers/issuable_matchers.rb @@ -2,7 +2,8 @@ RSpec::Matchers.define :have_header_with_correct_id_and_link do |level, text, id, parent = ".md"| match do |actual| - node = find("#{parent} h#{level} a#user-content-#{id}") + # anchors may be invisible + node = find("#{parent} h#{level} a#user-content-#{id}", visible: false) expect(node[:href]).to end_with("##{id}") diff --git a/spec/support/shared_examples/wiki_file_attachments_examples.rb b/spec/support/shared_examples/wiki_file_attachments_examples.rb index 22fbfb48928..a43b7c0300f 100644 --- a/spec/support/shared_examples/wiki_file_attachments_examples.rb +++ b/spec/support/shared_examples/wiki_file_attachments_examples.rb @@ -42,7 +42,7 @@ shared_examples 'wiki file attachments' do end end - context 'uploading is complete', :quarantine do + context 'uploading is complete' do it 'shows "Attach a file" button on uploading complete' do attach_with_dropzone wait_for_requests @@ -52,11 +52,11 @@ shared_examples 'wiki file attachments' do end it 'the markdown link is added to the page' do - fill_in(:wiki_content, with: '') + fill_in(:wiki_page_content, with: '') attach_with_dropzone(true) wait_for_requests - expect(page.find('#wiki_content').value) + expect(page.find('#wiki_page_content').value) .to match(%r{\!\[dk\]\(uploads/\h{32}/dk\.png\)$}) end @@ -70,7 +70,7 @@ shared_examples 'wiki file attachments' do img_link = page.find('a.no-attachment-icon img')['src'] expect(link).to eq img_link - expect(URI.parse(link).path).to eq File.join(wiki.wiki_base_path, file_path) + expect(URI.parse(link).path).to eq File.join(wiki.wiki_page_path, file_path) end it 'the file has been added to the wiki repository' do |