Commit d8fafdf5 authored by Alex Kalderimis's avatar Alex Kalderimis Committed by Douglas Barbosa Alexandre

Improve wiki page grouping

This uses an intermediate index to transform the function from `O(n^2)`
to `O(n.logn)`, which will be very important for large wikis with lots
of directories, which is definitely something we want to support.
parent f72dcdd3
......@@ -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'
......
// currently, this controller inherits all behaviors from wikis
import '../wikis/index';
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,
},
});
},
});
}
});
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,
},
});
},
});
}
});
......@@ -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}');
......
......@@ -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;
}
}
.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;
......
......@@ -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
# 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
# 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
# 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
# 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'
@wiki_entries = case @nesting
when ProjectWiki::NESTING_FLAT
@wiki_pages
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
WikiDirectory.group_by_directory(@wiki_pages)
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'
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')
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)
def sort_params
process_params(sort_params_config)
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 show_children_param
config = nesting_params_config(params[:sort])
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
process_params(config)
end
end
......@@ -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)
......
......@@ -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
......@@ -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)
......
# 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
......@@ -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
......
......@@ -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')
......
%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
%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
= render wiki_directory.to_partial_path(context), wiki_dir: wiki_directory, context: context
- 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.')
- 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) } }
- 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,
= 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)
......@@ -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?
......
= link_to @page.human_title, project_wiki_path(@project, @page)
%span.light
= _('&middot;').html_safe
= subtitle
%li
= link_to wiki_page.title, project_wiki_path(@project, wiki_page)
%small (#{wiki_page.format})
%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
- 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
= render wiki_page.to_partial_path(context), wiki_page: wiki_page
- @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
&middot;
= s_("Wiki|Edit Page")
= render partial: 'page_title', locals: { subtitle: s_("Wiki|Edit Page") }
- else
= s_("Wiki|Create New Page")
......
- @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
&middot;
= _("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'
- @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'
%li{ class: active_when(params[:id] == wiki_page.slug) }
= link_to project_wiki_path(@project, wiki_page) do
= wiki_page.human_title
%li
= wiki_directory.slug
%ul
= render wiki_directory.pages, context: context
= render "#{context}_wiki_page", wiki_page: wiki_page
- 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'
- @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) } }
- 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.')
- 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")
......
- @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'
---
title: Sort wiki pages by date
merge_request: 30245
type: added
......@@ -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
......
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
......@@ -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
......
......@@ -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
......@@ -411,6 +411,9 @@ msgstr ""
msgid "%{verb} %{time_spent_value} spent time."
msgstr ""
msgid "&middot;"
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"
......
......@@ -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
......
......@@ -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
......
......@@ -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
......
......@@ -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
......
# 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
This diff is collapsed.
......@@ -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/)
end
describe 'GET #pages' do
subject do
get :pages, params: { namespace_id: project.namespace, project_id: project, id: wiki_title }.merge(extra_params)
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)
describe 'illegal params' do
shared_examples :a_bad_request do
it do
expect { subject }.to raise_error(ActionController::BadRequest)
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
describe ':sort' do
let(:extra_params) { { sort: 'wibble' } }
it_behaves_like 'fetching history', :not_found do
let(:allow_read_wiki) { false }
end
it_behaves_like :a_bad_request
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)
describe ':show_children' do
let(:extra_params) { { show_children: 'wibble' } }
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
it_behaves_like :a_bad_request
end
end
context 'when the page does not exist' do
let(:id) { 'does not exist' }
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) { {} }
before do
subject
end
it 'builds a new wiki page with the id as the title' do
expect(assigns(:page).title).to eq(id)
it "sets nesting to #{default_nesting} by default" do
expect(assigns :nesting).to eq default_nesting
end
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
end
end
it 'hides children if the default requires it' do
expect(assigns :show_children).to be(default_nesting != ProjectWiki::NESTING_CLOSED)
end
context 'when page is a file' do
include WikiHelpers
ProjectWiki::NESTINGS.each do |nesting|
context "the user explicitly passes show_children = #{nesting}" do
let(:nesting_params) { { show_children: nesting } }
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 '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"
it 'sets nesting to the provided value' do
expect(assigns :nesting).to eq nesting
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
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)
......
......@@ -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
......@@ -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,49 +82,52 @@ 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) }
set(:pipeline) { create(:ci_empty_pipeline, project: project) }
set(: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)
}
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
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
tools.each do |method_name, url|
project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::ENABLED)
project.project_feature.update_attribute(attr_name, 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)
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
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)
......@@ -132,34 +136,34 @@ describe 'Edit Project Settings' do
group.add_owner(member)
tools.each do |method_name, url|
project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::PRIVATE)
project.project_feature.update_attribute(attr_name, 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)
project.team.truncate
sign_in(non_member)
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)
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|
project.project_feature.update_attribute("#{method_name}_access_level", ProjectFeature::PRIVATE)
visit url
expect(page.status_code).to eq(200)
end
end
......
......@@ -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>")
def init_home!
create(:wiki_page, wiki: project.wiki, attrs: { title: 'home', content: '[some link](other-page)' })
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>")
def fill_in_content!
page.within '.wiki-form' do
fill_in :wiki_page_content, with: wiki_content
end
end
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>")
def show_preview!
page.within '.wiki-form' do
click_on 'Preview'
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'
context 'when writing a new page' do
let(:new_wiki_path) { 'a/b/c/d' }
let(:wiki_content) { 'Some [awesome wiki](content)' }
fill_in :wiki_content, with: wiki_content
click_on "Preview"
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).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>")
expect(page).to have_link('awesome wiki')
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"
context 'when editing an existing page' do
let(:wiki_content) { 'Some [bemusing](content)' }
let(:wiki_page) { create(:wiki_page, wiki: project_wiki) }
expect(page).to have_content("regular link")
it 'can show a preview of markdown content, when writing' do
visit project_wiki_edit_path(project, wiki_page)
fill_in_content!
show_preview!
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>")
expect(page).to have_link('bemusing')
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
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)
expect(page).to have_content("do_not_linkify")
expect(page.html).to include('[[do_not_linkify]]')
expect(page.html).to include('[[also_do_not_linkify]]')
end
private
def create_wiki_page(path, content = 'content')
visit project_wiki_path(project, wiki_page)
click_link 'New page'
fill_in :wiki_title, with: path
fill_in :wiki_content, with: content
click_button 'Create page'
end
end
......@@ -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
......@@ -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 &lt;script&gt;')
expect(page).to have_field('wiki_page[message]', with: 'Update &lt;script&gt;')
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)
new_page_path = "baz/quux/#{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 '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
......
......@@ -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
......
......@@ -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
context 'ordered by title' do
let(:pages_ordered_by_title) { [wiki_page2, wiki_page3, wiki_page1] }
def sort_desc!
page.within('.wiki-sort-dropdown') do
page.find('.qa-reverse-sort').click
end
end
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)
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(:sub_folder) { project_wiki.find_dir('sub-folder') }
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
page.within('.wiki-sort-dropdown') do
page.find('.rspec-reverse-sort').click
sort_desc!
end
let(:expected_sequence) { [sub_folder, 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 '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 'ordered by created_at' do
let(:pages_ordered_by_created_at) { [wiki_page1, wiki_page2, wiki_page3] }
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
page.within('.wiki-sort-dropdown') do
click_button('Title')
click_link('Created date')
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
context 'nested' do
before do
page.within('.wiki-nesting-dropdown') do
click_link 'Hide folder contents'
end
end
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)
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
page.within('.wiki-sort-dropdown') do
page.find('.rspec-reverse-sort').click
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, wiki_page4, wiki_page5] }
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)
before do
sort_by_created_at!
end
let(:page_link_selector) { 'a.wiki-page-title' }
context 'asc' do
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
sort_desc!
end
let(:expected_sequence) { [wiki_page5, wiki_page4, wiki_page3, wiki_page2, wiki_page1] }
it_behaves_like 'correctly_sorted_pages'
end
end
end
......@@ -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 });
beforeEach(() => {
setHTMLFixture(formHtmlFixture);
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>`;
titleInput = document.getElementById('wiki_title');
messageInput = document.getElementById('wiki_message');
const init = newPage => {
setHTMLFixture(editFormHtmlFixture(newPage));
titleInput = document.getElementById('wiki_page_title');
messageInput = document.getElementById('wiki_page_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';
......
......@@ -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')
......
......@@ -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
......
......@@ -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
......
......@@ -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
......@@ -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
......
# 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
......@@ -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
......@@ -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
......
# 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
# 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
......@@ -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
......@@ -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();
......
......@@ -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
......@@ -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
......@@ -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}")
......
......@@ -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
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment