remove textile

pull/6422/head
Jens Ulferts 6 years ago
parent 7b8880151b
commit 319f33e924
No known key found for this signature in database
GPG Key ID: 3CAA4B1182CF5308
  1. 1
      .codeclimate.yml
  2. 2
      .rubocop.yml
  3. 1
      app/assets/javascripts/application.js.erb
  4. 7
      app/assets/javascripts/forums.js
  5. 30
      app/assets/javascripts/jstoolbar.js
  6. 378
      app/assets/javascripts/jstoolbar/jstoolbar.js
  7. 193
      app/assets/javascripts/jstoolbar/markdown.js
  8. 202
      app/assets/javascripts/jstoolbar/textile.js
  9. 20
      app/assets/javascripts/jstoolbar/translations.js
  10. 1
      app/assets/stylesheets/openproject/_index.sass
  11. 154
      app/assets/stylesheets/openproject/_jstoolbar.sass
  12. 16
      app/controllers/boards_controller.rb
  13. 4
      app/controllers/help_controller.rb
  14. 6
      app/helpers/application_helper.rb
  15. 5
      app/helpers/text_formatting_helper.rb
  16. 3
      app/views/attribute_help_texts/_form.html.erb
  17. 3
      app/views/boards/index.html.erb
  18. 2
      app/views/boards/show.html.erb
  19. 235
      app/views/help/wiki_syntax.html.erb
  20. 297
      app/views/help/wiki_syntax_detailed.html.erb
  21. 7
      app/views/settings/_general.html.erb
  22. 2
      config/initializers/assets.rb
  23. 131
      config/initializers/redcloth_numbered_headings.rb
  24. 3
      config/locales/en.yml
  25. 3
      config/routes.rb
  26. 6
      config/settings.yml
  27. 23
      db/migrate/20180706150714_convert_to_markdown.rb
  28. 15
      docs/api/apiv3/basic-objects.apib
  29. 4
      docs/api/apiv3/endpoints/activities.apib
  30. 4
      docs/api/apiv3/endpoints/forms.apib
  31. 6
      docs/api/apiv3/endpoints/help_texts.apib
  32. 17
      docs/api/apiv3/endpoints/render.apib
  33. 8
      docs/api/apiv3/endpoints/work-packages.apib
  34. 104
      features/messages/message.feature
  35. 73
      features/wiki/wiki_add_edit.feature
  36. 71
      features/wiki/wiki_create_child.feature
  37. 52
      features/wiki/wiki_initial.feature
  38. 66
      features/wiki/wiki_new_child.feature
  39. 2
      frontend/legacy/app/components/help-texts/attribute-help-text.directive.ts
  40. 2
      frontend/src/app/angular4-modules.ts
  41. 23
      frontend/src/app/components/ckeditor/op-ckeditor-form.component.ts
  42. 6
      frontend/src/app/components/wp-activity/user/user-activity.component.ts
  43. 42
      frontend/src/app/globals/augmenting/wiki-toolbar.augment.ts
  44. 91
      frontend/src/app/globals/augmenting/wiki-toolbar.ts
  45. 8
      frontend/src/app/modules/common/config/configuration.service.ts
  46. 7
      frontend/src/app/modules/common/openproject-common.module.ts
  47. 59
      frontend/src/app/modules/common/textile/textile-service.ts
  48. 59
      frontend/src/app/modules/common/wiki-toolbar/wiki-toolbar.directive.ts
  49. 2
      frontend/src/app/modules/fields/edit/field-types/formattable-edit-field.component.ts
  50. 70
      frontend/src/app/modules/fields/edit/field-types/formattable-edit-field.ts
  51. 80
      frontend/src/app/modules/fields/edit/field-types/formattable-textarea-edit-field.component.ts
  52. 9
      frontend/src/app/modules/fields/openproject-fields.module.ts
  53. 16
      lib/api/decorators/formattable.rb
  54. 2
      lib/api/v3/attachments/attachment_metadata_representer.rb
  55. 2
      lib/api/v3/attachments/attachment_representer.rb
  56. 2
      lib/api/v3/queries/queries_api.rb
  57. 3
      lib/api/v3/render/render_api.rb
  58. 12
      lib/api/v3/repositories/revision_representer.rb
  59. 9
      lib/api/v3/utilities/path_helper.rb
  60. 2
      lib/api/v3/versions/version_representer.rb
  61. 2
      lib/open_project/client_preference_extractor.rb
  62. 7
      lib/open_project/form_tag_helper.rb
  63. 2
      lib/open_project/object_linking.rb
  64. 4
      lib/open_project/text_formatting.rb
  65. 2
      lib/open_project/text_formatting/filters/macro_filter.rb
  66. 66
      lib/open_project/text_formatting/formats.rb
  67. 43
      lib/open_project/text_formatting/formats/base_format.rb
  68. 6
      lib/open_project/text_formatting/formats/base_formatter.rb
  69. 43
      lib/open_project/text_formatting/formats/markdown/format.rb
  70. 67
      lib/open_project/text_formatting/formats/markdown/formatter.rb
  71. 26
      lib/open_project/text_formatting/formats/markdown/helper.rb
  72. 2
      lib/open_project/text_formatting/formats/markdown/textile_converter.rb
  73. 43
      lib/open_project/text_formatting/formats/plain/format.rb
  74. 9
      lib/open_project/text_formatting/formats/plain/formatter.rb
  75. 5
      lib/open_project/text_formatting/formats/plain/helper.rb
  76. 88
      lib/open_project/text_formatting/formatters.rb
  77. 515
      lib/open_project/text_formatting/formatters/textile/formatter.rb
  78. 72
      lib/open_project/text_formatting/formatters/textile/helper.rb
  79. 143
      lib/open_project/text_formatting/formatters/textile/redcloth_wrapper.rb
  80. 19
      lib/open_project/text_formatting/matchers/link_handlers/colon_separator.rb
  81. 2
      lib/open_project/text_formatting/matchers/link_handlers/revisions.rb
  82. 2
      lib/open_project/text_formatting/matchers/link_handlers/work_packages.rb
  83. 5
      lib/open_project/text_formatting/matchers/resource_links_matcher.rb
  84. 29
      lib/open_project/text_formatting/matchers/wiki_links_matcher.rb
  85. 13
      lib/open_project/text_formatting/renderer.rb
  86. 81
      lib/open_project/wiki_formatting/macros/default.rb
  87. 66
      lib/open_project/wiki_formatting/macros/work_package_button.rb
  88. 1199
      lib/redcloth3.rb
  89. 100
      lib/redmine/wiki_formatting/macros.rb
  90. 36
      lib/tabular_form_builder.rb
  91. 1
      lib/tasks/code.rake
  92. 5
      lib/tasks/copyright.rake
  93. 46
      lib/tasks/markdown.rake
  94. 2
      spec/factories/wiki_page_factory.rb
  95. 12
      spec/features/admin/attribute_help_texts_spec.rb
  96. 4
      spec/features/auth/consent_auth_stage_spec.rb
  97. 113
      spec/features/boards/message_spec.rb
  98. 82
      spec/features/boards/sticky_spec.rb
  99. 103
      spec/features/wiki/adding_editing_history_spec.rb
  100. 78
      spec/features/wiki/child_pages_spec.rb
  101. Some files were not shown because too many files have changed in this diff Show More

@ -62,4 +62,3 @@ exclude_paths:
- "**/node_modules/"
- spec/**
- spec_legacy/**/*
- lib/redcloth3.rb

@ -1,5 +1,5 @@
AllCops:
TargetRubyVersion: 2.2
TargetRubyVersion: 2.5
Exclude:
- db/schema.rb

@ -34,7 +34,6 @@
//= require action_menu
//= require breadcrumb
//= require findDomElement
//= require jstoolbar
//= require settings
//= require modal
//= require tab_handling

@ -35,7 +35,12 @@
content = $("#reply_content");
subject.val(result.subject);
content.val(result.content);
$('op-ckeditor-form')
.data('editor')
.then(function(editor) {
editor.setData(result.content);
});
reply.slideDown();
content.focus();

@ -1,30 +0,0 @@
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2018 the OpenProject Foundation (OPF)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2017 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See docs/COPYRIGHT.rdoc for more details.
//++
//= require jstoolbar/jstoolbar
//= require jstoolbar/translations

@ -1,378 +0,0 @@
/* ***** BEGIN LICENSE BLOCK *****
* This file is part of DotClear.
* Copyright (c) 2005 Nicolas Martin & Olivier Meunier and contributors. All
* rights reserved.
*
* DotClear is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* DotClear is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with DotClear; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
* ***** END LICENSE BLOCK *****
*/
/* Modified by JP LANG for textile formatting */
function jsToolBar(textarea) {
if (!document.createElement) { return; }
if (!textarea) { return; }
if ((typeof(document["selection"]) == "undefined")
&& (typeof(textarea["setSelectionRange"]) == "undefined")) {
return;
}
this.textarea = textarea;
this.editor = document.createElement('div');
this.editor.className = 'jstEditor';
this.textarea.parentNode.insertBefore(this.editor,this.textarea);
this.editor.appendChild(this.textarea);
this.toolbar = document.createElement("div");
this.toolbar.className = 'jstElements';
this.editor.parentNode.insertBefore(this.toolbar,this.editor);
// Dragable resizing (only for gecko)
if (this.editor.addEventListener)
{
this.handle = document.createElement('div');
this.handle.className = 'jstHandle';
var dragStart = this.resizeDragStart;
var This = this;
this.handle.addEventListener('mousedown',function(event) { dragStart.call(This,event); },false);
// fix memory leak in Firefox (bug #241518)
window.addEventListener('unload',function() {
var del = This.handle.parentNode.removeChild(This.handle);
delete(This.handle);
},false);
this.editor.parentNode.insertBefore(this.handle,this.editor.nextSibling);
}
this.context = null;
this.toolNodes = {}; // lorsque la toolbar est dessinée , cet objet est garni
// de raccourcis vers les éléments DOM correspondants aux outils.
}
function jsButton(title, fn, scope, className) {
if(typeof jsToolBar.strings == 'undefined') {
this.title = title || null;
} else {
this.title = jsToolBar.strings[title] || title || null;
}
this.fn = fn || function(){};
this.scope = scope || null;
this.className = className || null;
}
jsButton.prototype.draw = function() {
if (!this.scope) return null;
var button = document.createElement('button');
button.setAttribute('type','button');
button.setAttribute('aria-label', this.title);
if (this.className) button.className = this.className;
button.title = this.title;
if (this.icon != undefined) {
button.style.backgroundImage = 'url('+this.icon+')';
}
if (typeof(this.fn) == 'function') {
var This = this;
button.onclick = function() { try { This.fn.apply(This.scope, arguments) } catch (e) {} return false; };
}
return button;
};
function jsSpace(id) {
this.id = id || null;
this.width = null;
}
jsSpace.prototype.draw = function() {
var span = document.createElement('span');
if (this.id) span.id = this.id;
span.appendChild(document.createTextNode(String.fromCharCode(160)));
span.className = 'jstSpacer';
if (this.width) span.style.marginRight = this.width+'px';
return span;
};
function jsCombo(title, options, scope, fn, className) {
this.title = title || null;
this.options = options || null;
this.scope = scope || null;
this.fn = fn || function(){};
this.className = className || null;
}
jsCombo.prototype.draw = function() {
if (!this.scope || !this.options) return null;
var select = document.createElement('select');
if (this.className) select.className = className;
select.title = this.title;
for (var o in this.options) {
//var opt = this.options[o];
var option = document.createElement('option');
option.value = o;
option.appendChild(document.createTextNode(this.options[o]));
select.appendChild(option);
}
var This = this;
select.onchange = function() {
try {
This.fn.call(This.scope, this.value);
} catch (e) { alert(e); }
return false;
};
return select;
};
jsToolBar.prototype = {
base_url: '',
mode: 'wiki',
elements: {},
help_link: '',
getMode: function() {
return this.mode;
},
setMode: function(mode) {
this.mode = mode || 'wiki';
},
switchMode: function(mode) {
mode = mode || 'wiki';
this.draw(mode);
},
setHelpLink: function(link) {
this.help_link = link;
},
button: function(toolName) {
var tool = this.elements[toolName];
if (typeof tool.fn[this.mode] != 'function') return null;
var b = new jsButton(tool.title, tool.fn[this.mode], this, 'icon-small jstb_'+toolName);
if (tool.icon != undefined) b.icon = tool.icon;
return b;
},
space: function(toolName) {
var tool = new jsSpace(toolName);
if (this.elements[toolName].width !== undefined)
tool.width = this.elements[toolName].width;
return tool;
},
combo: function(toolName) {
var tool = this.elements[toolName];
var length = tool[this.mode].list.length;
if (typeof tool[this.mode].fn != 'function' || length == 0) {
return null;
} else {
var options = {};
for (var i=0; i < length; i++) {
var opt = tool[this.mode].list[i];
options[opt] = tool.options[opt];
}
return new jsCombo(tool.title, options, this, tool[this.mode].fn);
}
},
draw: function(mode) {
this.setMode(mode);
// Empty toolbar
while (this.toolbar.hasChildNodes()) {
this.toolbar.removeChild(this.toolbar.firstChild)
}
this.toolNodes = {}; // vide les raccourcis DOM/**/
// Draw toolbar elements
var b, tool, newTool;
for (var i in this.elements) {
b = this.elements[i];
var disabled =
b.type == undefined || b.type == ''
|| (b.disabled != undefined && b.disabled)
|| (b.context != undefined && b.context != null && b.context != this.context);
if (!disabled && typeof this[b.type] == 'function') {
tool = this[b.type](i);
if (tool) newTool = tool.draw();
if (newTool) {
this.toolNodes[i] = newTool; //mémorise l'accès DOM pour usage éventuel ultérieur
this.toolbar.appendChild(newTool);
}
}
}
if (this.help_link !== '') {
this.toolbar.appendChild(this.help_link);
}
},
singleTag: function(stag,etag) {
stag = stag || null;
etag = etag || stag;
if (!stag || !etag) { return; }
this.encloseSelection(stag,etag);
},
encloseLineSelection: function (prefix, suffix, fn) {
this.textarea.focus();
prefix = prefix || '';
suffix = suffix || '';
var start, end, sel, scrollPos, subst, res;
if (typeof(document["selection"]) != "undefined") {
// just makes it work in IE8 somehow
var helperRange = document.selection.createRange();
var bookmark = helperRange.getBookmark();
var origParent = helperRange.parentElement();
var moveStart = 0;
// we move the starting point of the selection to the last newline
// and track the number of movements
try {
while (helperRange.text[0] != "\n" && helperRange.text[0] != "\r") {
helperRange.moveStart("character", -1);
if (origParent != helperRange.parentElement()) {
throw "Outside of Textarea";
}
moveStart -= 1;
}
moveStart += 1;
} catch(err) {
if (err != "Outside of Textarea")
throw err;
}
var range = document.selection.createRange();
range.moveStart("character", moveStart);
if (range.text.match(/ $/))
range.moveEnd("character", -1);
sel = range.text;
} else if (typeof(this.textarea["setSelectionRange"]) != "undefined") {
start = this.textarea.selectionStart;
end = this.textarea.selectionEnd;
scrollPos = this.textarea.scrollTop;
// go to the start of the line
start = this.textarea.value.substring(0, start).replace(/[^\r\n]*$/g,'').length;
// go to the end of the line
end = this.textarea.value.length - this.textarea.value.substring(end, this.textarea.value.length).replace(/^[^\r\n]*/, '').length;
sel = this.textarea.value.substring(start, end);
}
if (sel.match(/ $/)) {
sel = sel.substring(0, sel.length - 1);
suffix = suffix + " ";
}
if (typeof(fn) == 'function') {
res = (sel) ? fn.call(this, sel) : fn('');
} else {
res = (sel) ? sel : '';
}
subst = prefix + res + suffix;
if (typeof(document["selection"]) != "undefined") {
range.text = subst;
this.textarea.caretPos -= suffix.length;
} else if (typeof(this.textarea["setSelectionRange"]) != "undefined") {
var cursorPosition = start;
this.textarea.value = this.textarea.value.substring(0, start) + subst + this.textarea.value.substring(end);
if (sel) {
cursorPosition += subst.length;
} else {
cursorPosition += prefix.length + res.length;
}
this.textarea.setSelectionRange(cursorPosition, cursorPosition);
this.textarea.scrollTop = scrollPos;
}
},
encloseSelection: function (prefix, suffix, fn) {
this.textarea.focus();
prefix = prefix || '';
suffix = suffix || '';
var start, end, sel, scrollPos, subst, res;
if (typeof(document["selection"]) != "undefined") {
sel = document.selection.createRange().text;
} else if (typeof(this.textarea["setSelectionRange"]) != "undefined") {
start = this.textarea.selectionStart;
end = this.textarea.selectionEnd;
scrollPos = this.textarea.scrollTop;
sel = this.textarea.value.substring(start, end);
}
if (sel.match(/ $/)) {
sel = sel.substring(0, sel.length - 1);
suffix = suffix + " ";
}
if (typeof(fn) == 'function') {
res = (sel) ? fn.call(this, sel) : fn('');
} else {
res = (sel) ? sel : '';
}
subst = prefix + res + suffix;
if (typeof(document["selection"]) != "undefined") {
var range = document.selection.createRange().text = subst;
this.textarea.caretPos -= suffix.length;
} else if (typeof(this.textarea["setSelectionRange"]) != "undefined") {
this.textarea.value = this.textarea.value.substring(0, start) + subst + this.textarea.value.substring(end);
if (sel) {
this.textarea.setSelectionRange(start + subst.length, start + subst.length);
} else {
this.textarea.setSelectionRange(start + prefix.length, start + prefix.length);
}
this.textarea.scrollTop = scrollPos;
}
},
stripBaseURL: function(url) {
if (this.base_url != '') {
var pos = url.indexOf(this.base_url);
if (pos == 0) {
url = url.substr(this.base_url.length);
}
}
return url;
}
};
/** Resizer
-------------------------------------------------------- */
jsToolBar.prototype.resizeSetStartH = function() {
this.dragStartH = this.textarea.offsetHeight + 0;
};
jsToolBar.prototype.resizeDragStart = function(event) {
var This = this;
this.dragStartY = event.clientY;
this.resizeSetStartH();
document.addEventListener('mousemove', this.dragMoveHdlr=function(event){This.resizeDragMove(event);}, false);
document.addEventListener('mouseup', this.dragStopHdlr=function(event){This.resizeDragStop(event);}, false);
};
jsToolBar.prototype.resizeDragMove = function(event) {
this.textarea.style.height = (this.dragStartH+event.clientY-this.dragStartY)+'px';
};
jsToolBar.prototype.resizeDragStop = function(event) {
document.removeEventListener('mousemove', this.dragMoveHdlr, false);
document.removeEventListener('mouseup', this.dragStopHdlr, false);
};

@ -1,193 +0,0 @@
/* ***** BEGIN LICENSE BLOCK *****
* This file is part of DotClear.
* Copyright (c) 2005 Nicolas Martin & Olivier Meunier and contributors. All
* rights reserved.
*
* DotClear is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* DotClear is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with DotClear; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
* ***** END LICENSE BLOCK *****
*/
/* Modified by JP LANG for textile formatting */
// strong
jsToolBar.prototype.elements.strong = {
type: 'button',
title: 'Strong',
fn: {
wiki: function() { this.singleTag('**') }
}
};
// em
jsToolBar.prototype.elements.em = {
type: 'button',
title: 'Italic',
fn: {
wiki: function() { this.singleTag("_") }
}
};
// del
jsToolBar.prototype.elements.del = {
type: 'button',
title: 'Deleted',
fn: {
wiki: function() { this.singleTag('~~') }
}
};
// code
jsToolBar.prototype.elements.code = {
type: 'button',
title: 'Code',
fn: {
wiki: function() { this.singleTag('`') }
}
};
// spacer
jsToolBar.prototype.elements.space1 = {type: 'space'};
// headings
jsToolBar.prototype.elements.h1 = {
type: 'button',
title: 'Heading 1',
fn: {
wiki: function() {
this.encloseLineSelection('# ', '',function(str) {
str = str.replace(/^#+/, '');
return str;
});
}
}
};
jsToolBar.prototype.elements.h2 = {
type: 'button',
title: 'Heading 2',
fn: {
wiki: function() {
this.encloseLineSelection('## ', '',function(str) {
str = str.replace(/^#+/, '');
return str;
});
}
}
};
jsToolBar.prototype.elements.h3 = {
type: 'button',
title: 'Heading 3',
fn: {
wiki: function() {
this.encloseLineSelection('### ', '',function(str) {
str = str.replace(/^#+/, '');
return str;
});
}
}
};
// spacer
jsToolBar.prototype.elements.space2 = {type: 'space'};
// ul
jsToolBar.prototype.elements.ul = {
type: 'button',
title: 'Unordered list',
fn: {
wiki: function() {
this.encloseLineSelection('','',function(str) {
str = str.replace(/\r/g,'');
return str.replace(/(\n|^)[#-]?\s*/g,"$1- ");
});
}
}
};
// ol
jsToolBar.prototype.elements.ol = {
type: 'button',
title: 'Ordered list',
fn: {
wiki: function() {
this.encloseLineSelection('','',function(str) {
str = str.replace(/\r/g,'');
return str.replace(/(\n|^)[*-]?\s*/g,"$11. ");
});
}
}
};
// spacer
jsToolBar.prototype.elements.space3 = {type: 'space'};
// bq
jsToolBar.prototype.elements.bq = {
type: 'button',
title: 'Quote',
fn: {
wiki: function() {
this.encloseLineSelection('','',function(str) {
str = str.replace(/\r/g,'');
return str.replace(/(\n|^) *([^\n]*)/g,"$1> $2");
});
}
}
};
// unbq
jsToolBar.prototype.elements.unbq = {
type: 'button',
title: 'Unquote',
fn: {
wiki: function() {
this.encloseLineSelection('','',function(str) {
str = str.replace(/\r/g,'');
return str.replace(/(\n|^) *[>]? *([^\n]*)/g,"$1$2");
});
}
}
};
// pre
jsToolBar.prototype.elements.code = {
type: 'button',
title: 'Code fence',
fn: {
wiki: function() { this.encloseLineSelection('```\n', '\n```') }
}
};
// spacer
jsToolBar.prototype.elements.space4 = {type: 'space'};
// wiki page
jsToolBar.prototype.elements.link = {
type: 'button',
title: 'Link',
fn: {
wiki: function() { this.encloseSelection("[", "]()") }
}
};
// image
jsToolBar.prototype.elements.img = {
type: 'button',
title: 'Image',
fn: {
wiki: function() { this.encloseSelection("![](", ")") }
}
};

@ -1,202 +0,0 @@
/* ***** BEGIN LICENSE BLOCK *****
* This file is part of DotClear.
* Copyright (c) 2005 Nicolas Martin & Olivier Meunier and contributors. All
* rights reserved.
*
* DotClear is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* DotClear is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with DotClear; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
* ***** END LICENSE BLOCK *****
*/
/* Modified by JP LANG for textile formatting */
// strong
jsToolBar.prototype.elements.strong = {
type: 'button',
title: 'Strong',
fn: {
wiki: function() { this.singleTag('*') }
}
};
// em
jsToolBar.prototype.elements.em = {
type: 'button',
title: 'Italic',
fn: {
wiki: function() { this.singleTag("_") }
}
};
// ins
jsToolBar.prototype.elements.ins = {
type: 'button',
title: 'Underline',
fn: {
wiki: function() { this.singleTag('+') }
}
};
// del
jsToolBar.prototype.elements.del = {
type: 'button',
title: 'Deleted',
fn: {
wiki: function() { this.singleTag('-') }
}
};
// code
jsToolBar.prototype.elements.pre = {
type: 'button',
title: 'Code',
fn: {
wiki: function() { this.singleTag('@') }
}
};
// spacer
jsToolBar.prototype.elements.space1 = {type: 'space'};
// headings
jsToolBar.prototype.elements.h1 = {
type: 'button',
title: 'Heading 1',
fn: {
wiki: function() {
this.encloseLineSelection('h1. ', '',function(str) {
str = str.replace(/^h\d+\.\s+/, '');
return str;
});
}
}
};
jsToolBar.prototype.elements.h2 = {
type: 'button',
title: 'Heading 2',
fn: {
wiki: function() {
this.encloseLineSelection('h2. ', '',function(str) {
str = str.replace(/^h\d+\.\s+/, '');
return str;
});
}
}
};
jsToolBar.prototype.elements.h3 = {
type: 'button',
title: 'Heading 3',
fn: {
wiki: function() {
this.encloseLineSelection('h3. ', '',function(str) {
str = str.replace(/^h\d+\.\s+/, '');
return str;
});
}
}
};
// spacer
jsToolBar.prototype.elements.space2 = {type: 'space'};
// ul
jsToolBar.prototype.elements.ul = {
type: 'button',
title: 'Unordered list',
fn: {
wiki: function() {
this.encloseLineSelection('','',function(str) {
str = str.replace(/\r/g,'');
return str.replace(/(\n|^)[#-]?\s*/g,"$1* ");
});
}
}
};
// ol
jsToolBar.prototype.elements.ol = {
type: 'button',
title: 'Ordered list',
fn: {
wiki: function() {
this.encloseLineSelection('','',function(str) {
str = str.replace(/\r/g,'');
return str.replace(/(\n|^)[*-]?\s*/g,"$1# ");
});
}
}
};
// spacer
jsToolBar.prototype.elements.space3 = {type: 'space'};
// bq
jsToolBar.prototype.elements.bq = {
type: 'button',
title: 'Quote',
fn: {
wiki: function() {
this.encloseLineSelection('','',function(str) {
str = str.replace(/\r/g,'');
return str.replace(/(\n|^) *([^\n]*)/g,"$1> $2");
});
}
}
};
// unbq
jsToolBar.prototype.elements.unbq = {
type: 'button',
title: 'Unquote',
fn: {
wiki: function() {
this.encloseLineSelection('','',function(str) {
str = str.replace(/\r/g,'');
return str.replace(/(\n|^) *[>]? *([^\n]*)/g,"$1$2");
});
}
}
};
// pre
jsToolBar.prototype.elements.pre = {
type: 'button',
title: 'Preformatted text',
fn: {
wiki: function() { this.encloseLineSelection('<pre>\n', '\n</pre>') }
}
};
// spacer
jsToolBar.prototype.elements.space4 = {type: 'space'};
// wiki page
jsToolBar.prototype.elements.link = {
type: 'button',
title: 'Wiki link',
fn: {
wiki: function() { this.encloseSelection("[[", "]]") }
}
};
// image
jsToolBar.prototype.elements.img = {
type: 'button',
title: 'Image',
fn: {
wiki: function() { this.encloseSelection("!", "!") }
}
};

@ -1,20 +0,0 @@
jQuery(function() {
jsToolBar.strings = {
'Strong': I18n.t('js.wiki_formatting.strong'),
'Italic': I18n.t('js.wiki_formatting.italic'),
'Underline': I18n.t('js.wiki_formatting.underline'),
'Deleted': I18n.t('js.wiki_formatting.deleted'),
'Code': I18n.t('js.wiki_formatting.code'),
'Heading 1': I18n.t('js.wiki_formatting.heading1'),
'Heading 2': I18n.t('js.wiki_formatting.heading2'),
'Heading 3': I18n.t('js.wiki_formatting.heading3'),
'Unordered list': I18n.t('js.wiki_formatting.unordered_list'),
'Ordered list': I18n.t('js.wiki_formatting.ordered_list'),
'Quote': I18n.t('js.wiki_formatting.quote'),
'Unquote': I18n.t('js.wiki_formatting.unquote'),
'Preformatted text': I18n.t('js.wiki_formatting.preformatted_text'),
'Wiki link': I18n.t('js.wiki_formatting.wiki_link'),
'Image': I18n.t('js.wiki_formatting.image')
}
});

@ -2,7 +2,6 @@
@import openproject/announcements
@import openproject/functions
@import openproject/homescreen
@import openproject/jstoolbar
// Legacy styles, remove if possible
@import openproject/legacy

@ -1,154 +0,0 @@
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2018 the OpenProject Foundation (OPF)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2017 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See docs/COPYRIGHT.rdoc for more details.
//++
$jstoolbar--icon-background: #fff
$jstoolbar--icon-hover-border: #ccc
$jstoolbar--icon-border: #fff
$jstoolbar--icon-active-border: #bbb
$jstoolbar--icon-active-background: #eee
.jstEditor
padding-left: 0
clear: both
textarea, iframe
margin: 0
textarea
min-height: 200px
textarea.-small
min-height: 50px
.jstHandle
height: 10px
font-size: 0.1em
cursor: s-resize
.jstElements
background: #fff
float: right
line-height: 1.6rem
margin-bottom: 0.25rem
text-align: right
button
@include icon-common
padding: 0.25rem
margin-left: 0.125rem
height: 1.625rem
width: 1.625rem
max-width: 1.625rem
border: 1px solid $jstoolbar--icon-border
background-color: $jstoolbar--icon-background
color: $body-font-color
border-radius: 2px
overflow: hidden
display: inline-block
&:hover
border-color: $jstoolbar--icon-hover-border
cursor: pointer
&:focus
border-color: $jstoolbar--icon-hover-border
&.-active
border-color: $jstoolbar--icon-hover-border
box-shadow: inset 0px 0px 3px $jstoolbar--icon-active-border
background: $jstoolbar--icon-active-background
&:before
display: block
span
display: none
.jstb_help
font-size: 0.8125rem
padding: 0.35rem
line-height: 1rem
@extend .icon-help1
&:before
color: $body-font-color
padding: 0
span
display: inline
.jstSpacer
width: 0
font-size: 1px
margin-right: 4px
.jstb_strong
@extend .icon-bold
.jstb_em
@extend .icon-italic
.jstb_ins
@extend .icon-underline
.jstb_del
@extend .icon-strike-through
.jstb_code
@extend .icon-code-tag
.jstb_h1
@extend .icon-headline1
.jstb_h2
@extend .icon-headline2
.jstb_h3
@extend .icon-headline3
.jstb_ul
@extend .icon-unordered-list
.jstb_ol
@extend .icon-ordered-list
.jstb_bq
@extend .icon-paragraph-right
.jstb_unbq
@extend .icon-paragraph-left
.jstb_pre
@extend .icon-pre
.jstb_link
@extend .icon-link
.jstb_img
@extend .icon-image1

@ -80,7 +80,9 @@ class BoardsController < ApplicationController
render template: 'messages/index'
end
format.atom do
@messages = @board.messages.order(["#{Message.table_name}.sticked_on ASC", sort_clause].compact.join(', '))
@messages = @board
.messages
.order(["#{Message.table_name}.sticked_on ASC", sort_clause].compact.join(', '))
.includes(:author, :board)
.limit(Setting.feeds_limit.to_i)
@ -90,14 +92,15 @@ class BoardsController < ApplicationController
end
def set_topics
@topics = @board.topics.order(["#{Message.table_name}.sticked_on ASC", sort_clause].compact.join(', '))
.includes(:author, last_reply: :author)
@topics = @board
.topics
.order(["#{Message.table_name}.sticked_on ASC", sort_clause].compact.join(', '))
.includes(:author, last_reply: :author)
.page(params[:page])
.per_page(per_page_param)
end
def new
end
def new; end
def create
if @board.save
@ -108,8 +111,7 @@ class BoardsController < ApplicationController
end
end
def edit
end
def edit; end
def update
if @board.update_attributes(permitted_params.board)

@ -30,10 +30,6 @@
class HelpController < ApplicationController
layout 'help'
def wiki_syntax; end
def wiki_syntax_detailed; end
def keyboard_shortcuts; end
def text_formatting

@ -633,12 +633,6 @@ module ApplicationHelper
end
end
def wiki_helper
helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
extend helper
self
end
def link_to_content_update(text, url_params = {}, html_options = {})
link_to(text, url_params, html_options)
end

@ -52,10 +52,9 @@ module TextFormattingHelper
end
end
private
#TODO remove
def current_formatting_helper
helper_class = OpenProject::TextFormatting::Formatters.helper_for(Setting.text_formatting)
helper_class = OpenProject::TextFormatting::Formats.rich_helper
helper_class.new(self)
end
end

@ -41,7 +41,6 @@ See docs/COPYRIGHT.rdoc for more details.
<% end %>
</div>
<div class="form--field -required">
<%= f.text_area :help_text, :cols => 100, :rows => 20, :class => 'wiki-edit' %>
<%= wikitoolbar_for("#{f.object_name}_help_text") %>
<%= f.text_area :help_text, cols: 100, rows: 20, class: 'wiki-edit', with_text_formatting: true %>
</div>
</section>

@ -88,8 +88,9 @@ See docs/COPYRIGHT.rdoc for more details.
<td><%= board.messages_count %></td>
<td>
<% if board.last_message %>
<% board.last_message %>
<%= authoring board.last_message.created_on, board.last_message.author %><br />
<%= link_to_message board.last_message %>
<%= link_to_message board.last_message, no_root: true %>
<% end %>
</td>
</tr>

@ -130,7 +130,7 @@ See docs/COPYRIGHT.rdoc for more details.
<% if message.last_reply %>
<%= authoring message.last_reply.created_on, message.last_reply.author %><br/>
<br/>
<%= link_to message.subject, topic_path(message.last_reply) %>
<%= link_to_message message.last_reply, no_root: true %>
<% end %>
</td>
</tr>

@ -1,235 +0,0 @@
<%#-- copyright
OpenProject is a project management system.
Copyright (C) 2012-2018 the OpenProject Foundation (OPF)
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License version 3.
OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
Copyright (C) 2006-2017 Jean-Philippe Lang
Copyright (C) 2010-2013 the ChiliProject Team
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
See docs/COPYRIGHT.rdoc for more details.
++#%>
<% content_for :styles do %>
h1 { font-family: Verdana, sans-serif; font-size: 14px; text-align: center; color: #444; }
body { font-family: Verdana, sans-serif; font-size: 12px; color: #444; }
table th { padding-top: 1em; }
table td { vertical-align: top; background-color: #f5f5f5; height: 2em; vertical-align: middle;}
table td code { font-size: 1.2em; }
table td h1 { font-size: 1.8em; text-align: left; }
table td h2 { font-size: 1.4em; text-align: left; }
table td h3 { font-size: 1.2em; text-align: left; }
<% end %>
<% html_title "Wiki Syntax Quick Reference" %>
<h1>Wiki Syntax Quick Reference</h1>
<table width="100%">
<tr>
<th colspan="3">Font Styles</th>
</tr>
<tr>
<th><%= image_tag 'jstoolbar/bt_strong.png', style: 'border: 1px solid #bbb', alt: 'Strong' %></th>
<td width="50%">*Strong*</td>
<td width="50%"><strong>Strong</strong></td>
</tr>
<tr>
<th><%= image_tag 'jstoolbar/bt_em.png', style: 'border: 1px solid #bbb', alt: 'Italic' %></th>
<td>_Italic_</td>
<td><em>Italic</em></td>
</tr>
<tr>
<th><%= image_tag 'jstoolbar/bt_ins.png', style: 'border: 1px solid #bbb', alt: 'Underline' %></th>
<td>+Underline+</td>
<td><ins>Underline</ins></td>
</tr>
<tr>
<th><%= image_tag 'jstoolbar/bt_del.png', style: 'border: 1px solid #bbb', alt: 'Deleted' %></th>
<td>-Deleted-</td>
<td><del>Deleted</del></td>
</tr>
<tr>
<th><div class="generic-table--empty-header"></div></th>
<td>??Quote??</td>
<td><cite>Quote</cite></td>
</tr>
<tr>
<th><%= image_tag 'jstoolbar/bt_code.png', style: 'border: 1px solid #bbb', alt: 'Inline Code' %></th>
<td>@Inline Code@</td>
<td><code>Inline Code</code></td>
</tr>
<tr>
<th><%= image_tag 'jstoolbar/bt_pre.png', style: 'border: 1px solid #bbb', alt: 'Preformatted text' %></th>
<td>&lt;pre><br />
&nbsp;lines<br />
&nbsp;of code<br />
&lt;/pre></td>
<td>
<pre>
lines
of code
</pre>
</td>
</tr>
<tr>
<th colspan="3">Lists</th>
</tr>
<tr>
<th><%= image_tag 'jstoolbar/bt_ul.png', style: 'border: 1px solid #bbb', alt: 'Unordered list' %></th>
<td>* Item 1<br />
* Item 2</td>
<td>
<ul>
<li>Item 1</li>
<li>Item 2</li>
</ul>
</td>
</tr>
<tr>
<th><%= image_tag 'jstoolbar/bt_ol.png', style: 'border: 1px solid #bbb', alt: 'Ordered list' %></th>
<td># Item 1<br />
# Item 2</td>
<td>
<ol>
<li>Item 1</li>
<li>Item 2</li>
</ol>
</td>
</tr>
<tr>
<th colspan="3">Headings</th>
</tr>
<tr>
<th><%= image_tag 'jstoolbar/bt_h1.png', style: 'border: 1px solid #bbb', alt: 'Heading 1' %></th>
<td>h1. Title 1</td>
<td>
<h1>Title 1</h1>
</td>
</tr>
<tr>
<th><%= image_tag 'jstoolbar/bt_h2.png', style: 'border: 1px solid #bbb', alt: 'Heading 2' %></th>
<td>h2. Title 2</td>
<td>
<h2>Title 2</h2>
</td>
</tr>
<tr>
<th><%= image_tag 'jstoolbar/bt_h3.png', style: 'border: 1px solid #bbb', alt: 'Heading 3' %></th>
<td>h3. Title 3</td>
<td>
<h3>Title 3</h3>
</td>
</tr>
<tr>
<th colspan="3">Links</th>
</tr>
<tr>
<th><div class="generic-table--empty-header"></div></th>
<td>http://foo.bar</td>
<td><a href="#">http://foo.bar</a></td>
</tr>
<tr>
<th><div class="generic-table--empty-header"></div></th>
<td>"Foo":http://foo.bar</td>
<td><a href="#">Foo</a></td>
</tr>
<tr>
<th colspan="3">OpenProject links</th>
</tr>
<tr>
<th><%= image_tag 'jstoolbar/bt_link.png', style: 'border: 1px solid #bbb', alt: 'Link to a Wiki page' %></th>
<td>[[Wiki page]]</td>
<td><a href="#">Wiki page</a></td>
</tr>
<tr>
<th><%= image_tag 'jstoolbar/bt_link.png', style: 'border: 1px solid #bbb', alt: 'Link to a Wiki page' %></th>
<td>[[Sandbox:Wiki page]]</td>
<td><a href="#">Wiki page</a> (On the Sandbox project)</td>
</tr>
<tr>
<th><div class="generic-table--empty-header"></div></th>
<td>WorkPackage #12</td>
<td>WorkPackage <a href="#">#12</a></td>
</tr>
<tr>
<th><div class="generic-table--empty-header"></div></th>
<td>WorkPackage ##12</td>
<td>WorkPackage <a href="#">Bug #12: WorkPackage Subject</a> 2012-06-06 – 2013-06-06</td>
</tr>
<tr>
<th><div class="generic-table--empty-header"></div></th>
<td>WorkPackage ###12</td>
<td>WorkPackage <a href="#">Bug #12: WorkPackage Subject</a> 2012-06-06 – 2013-06-06
<div style="padding-left: 10px; padding-top: 0.5em; padding-bottom: 0.5em; line-height: 1.5em;">
<strong>Responsible:</strong> John Doe<br>
<strong>Assigned to:</strong> John Doe
</div>
<div style="padding-left: 10px;">
I am the description of this work package.
</div>
</td>
</tr>
<tr>
<th><div class="generic-table--empty-header"></div></th>
<td>Link user#4 via ID<br><i>(Using links in work package descriptions or comments will trigger a "you were mentioned" email notification. Type '@' to let an autocompleter suggest you project members to link to.)</i></td>
<td>Link <a href="#" class="user-mention">John Doe</a> via ID</td>
</tr>
<tr>
<th><div class="generic-table--empty-header"></div></th>
<td>Link user:"johndoe" via login name <br><i>(Using links in work package descriptions or comments will trigger a "you were mentioned" email notification.)</i></td>
<td>Link <a href="#" class="user-mention">John Doe</a> via login name</td>
</tr>
<tr>
<th><div class="generic-table--empty-header"></div></th>
<td>Revision r43</td>
<td>Revision <a href="#">r43</a></td>
</tr>
<tr>
<th><div class="generic-table--empty-header"></div></th>
<td>commit:f30e13e43</td>
<td><a href="#">f30e13e4</a></td>
</tr>
<tr>
<th><div class="generic-table--empty-header"></div></th>
<td>source:some/file</td>
<td><a href="#">source:some/file</a></td>
</tr>
<tr>
<th colspan="3">Inline images</th>
</tr>
<tr>
<th><%= image_tag 'jstoolbar/bt_img.png', style: 'border: 1px solid #bbb', alt: 'Image' %></th>
<td>!<em>image_url</em>!</td>
<td></td>
</tr>
<tr>
<th><div class="generic-table--empty-header"></div></th>
<td>!<em>attached_image</em>!</td>
<td></td>
</tr>
</table>
<p>
<%= link_to_function 'More Information', "window.open('wiki_syntax_detailed', '', '')" %>
</p>

@ -1,297 +0,0 @@
<%#-- copyright
OpenProject is a project management system.
Copyright (C) 2012-2018 the OpenProject Foundation (OPF)
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License version 3.
OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
Copyright (C) 2006-2017 Jean-Philippe Lang
Copyright (C) 2010-2013 the ChiliProject Team
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
See docs/COPYRIGHT.rdoc for more details.
++#%>
<% content_for :styles do %>
body { font:80% Verdana,Tahoma,Arial,sans-serif; }
h1, h2, h3, h4 { font-family: Trebuchet MS,Georgia,"Times New Roman",serif; }
pre, code { font-size:120%; }
pre code { font-size:100%; }
pre {
margin: 1em 1em 1em 1.6em;
padding: 2px;
background-color: #fafafa;
border: 1px solid #dadada;
width:95%;
overflow-x: auto;
}
a.new { color: #b73535; }
.CodeRay .c { color:#666; }
.CodeRay .cl { color:#B06; font-weight:bold }
.CodeRay .dl { color:black }
.CodeRay .fu { color:#06B; font-weight:bold }
.CodeRay .il { background: #eee }
.CodeRay .il .idl { font-weight: bold; color: #888 }
.CodeRay .iv { color:#33B }
.CodeRay .r { color:#080; font-weight:bold }
.CodeRay .s { background-color:#fff0f0 }
.CodeRay .s .dl { color:#710 }
<% end %>
<% html_title "Wiki Formatting" %>
<h1><a name="1" class="wiki-page"></a>Wiki Formatting</h1>
<h2><a name="2" class="wiki-page"></a>Links</h2>
<h3><a name="3" class="wiki-page"></a>OpenProject links</h3>
<p>OpenProject allows hyperlinking between work packages, changesets and wiki pages from anywhere wiki formatting is used.</p>
<ul>
<li><strong>#124</strong> displays a link to a work package: <del><a href="#" class="work package" title="bulk edit doesn't change the category or fixed version properties (Closed)">#124</a></del> (link is striked-through if the work package is closed)</li>
<li><strong>##124</strong> displays a link to a work package with context information: <a href="#" class="work package status-1 priority-2 overdue created-by-me" title="Work package subject (New)">Bug #12: Work package subject</a> 2012-05-14 - 2012-05-23</li>
<li><strong>###124</strong> displays a link to a work package with context information and an excerpt (first 3 lines) of the description</li>
<li><strong>r758</strong> displays a link to a changeset: <a href="#" class="changeset" title="Search engine now only searches objects the user is allowed to view.">r758</a></li>
<li><strong>commit:c6f4d0fd</strong> displays a link to a changeset with a non-numeric hash: <a href="#" class="changeset">c6f4d0fd</a></li>
<li><strong>sandbox:r758</strong> displays a link to a changeset of another project: <a href="#" class="changeset" title="Search engine now only searches objects the user is allowed to view.">sandbox:r758</a></li>
<li><strong>sandbox:c6f4d0fd</strong> displays a link to a changeset with a non-numeric hash: <a href="#" class="changeset">sandbox:c6f4d0fd</a></li>
</ul>
<p>Wiki links:</p>
<ul>
<li><strong>[[Guide]]</strong> displays a link to the page named 'Guide': <a href="#" class="wiki-page">Guide</a></li>
<li><strong>[[Guide#further-reading]]</strong> takes you to the anchor "further-reading". Headings get automatically assigned anchors so that you can refer to them: <a href="#" class="wiki-page">Guide</a></li>
<li><strong>[[Guide|User manual]]</strong> displays a link to the same page but with a different text: <a href="#" class="wiki-page">User manual</a></li>
</ul>
<p>You can also link to pages of an other project wiki:</p>
<ul>
<li><strong>[[sandbox:some page]]</strong> displays a link to the page named 'Some page' of the Sandbox wiki</li>
<li><strong>[[sandbox:]]</strong> displays a link to the Sandbox wiki main page</li>
</ul>
<p>Wiki links are displayed in red if the page doesn't exist yet, eg: <a href="#" class="wiki-page new">Nonexistent page</a>.</p>
<p>Links to other resources:</p>
<ul>
<li>Versions:
<ul>
<li><strong>version#3</strong> (link to version with id 3)</li>
<li><strong>version:1.0.0</strong> (link to version named "1.0.0")</li>
<li><strong>version:"1.0 beta 2"</strong></li>
<li><strong>sandbox:version:1.0.0</strong> (link to version "1.0.0" in the project "sandbox")</li>
</ul>
</li>
</ul>
<ul>
<li>Attachments:
<ul>
<li><strong>attachment:file.zip</strong> (link to the attachment of the current object named file.zip)</li>
<li>For now, attachments of the current object can be referenced only (if you're on a work package, it's possible to reference attachments of this work package only)</li>
</ul>
</li>
</ul>
<ul>
<li>Repository files:
<ul>
<li><strong>source:some/file</strong> (link to the file located at /some/file in the project's repository)</li>
<li><strong>source:some/file@52</strong> (link to the file's revision 52)</li>
<li><strong>source:some/file#L120</strong> (link to line 120 of the file)</li>
<li><strong>source:some/file@52#L120</strong> (link to line 120 of the file's revision 52)</li>
<li><strong>source:"some file@52#L120"</strong> (use double quotes when the URL contains spaces</li>
<li><strong>export:some/file</strong> (force the download of the file)</li>
<li><strong>sandbox:source:some/file</strong> (link to the file located at /some/file in the repository of the project "sandbox")</li>
<li><strong>sandbox:export:some/file</strong> (force the download of the file)</li>
</ul>
</li>
</ul>
<ul>
<li>Forum messages:
<ul>
<li><strong>message#1218</strong> (link to message with id 1218)</li>
</ul>
</li>
</ul>
<ul>
<li>Projects:
<ul>
<li><strong>project#3</strong> (link to project with id 3)</li>
<li><strong>project:someproject</strong> (link to project named "someproject")</li>
</ul>
</li>
</ul>
<p>Escaping:</p>
<ul>
<li>You can prevent OpenProject links from being parsed by preceding them with an exclamation mark: !</li>
</ul>
<h3><a name="4" class="wiki-page"></a>External links</h3>
<p>HTTP URLs and email addresses are automatically turned into clickable links:</p>
<pre>
https://www.openproject.org, someone@foo.bar
</pre>
<p>displays: <a class="external" href="https://www.openproject.org">https://www.openproject.org</a>, <a href="mailto:someone@foo.bar" class="email">someone@foo.bar</a></p>
<p>If you want to display a specific text instead of the URL, you can use the standard textile syntax:</p>
<pre>
"OpenProject web site":https://www.openproject.org
</pre>
<p>displays: <a href="https://www.openproject.org" class="external">OpenProject web site</a></p>
<h2><a name="5" class="wiki-page"></a>Text formatting</h2>
<p>For things such as headlines, bold, tables, lists, OpenProject supports Textile syntax. See <a class="external" href="http://txstyle.org/">http://txstyle.org/</a> for information on using any of these features. A few samples are included below, but the engine is capable of much more of that.</p>
<h3><a name="6" class="wiki-page"></a>Font style</h3>
<pre>
* *bold*
* _italic_
* _*bold italic*_
* +underline+
* -strike-through-
</pre>
<p>Display:</p>
<ul>
<li><strong>bold</strong></li>
<li><em>italic</em></li>
<li><em>*bold italic*</em></li>
<li><ins>underline</ins></li>
<li><del>strike-through</del></li>
</ul>
<h3><a name="7" class="wiki-page"></a>Inline images</h3>
<ul>
<li><strong>!image_url!</strong> displays an image located at image_url (textile syntax)</li>
<li><strong>!>image_url!</strong> right floating image</li>
<li>If you have an image attached to your wiki page, it can be displayed inline using its filename: <strong>!attached_image.png!</strong></li>
</ul>
<h3><a name="8" class="wiki-page"></a>Headings</h3>
<p>Prefixing a line with <code>h1.</code>, <code>h2.</code> etc. will create a heading:</p>
<pre>
h1. Heading
h2. Subheading
h3. Subsubheading
</pre>
<p>OpenProject assigns an anchor to each of those headings thus you can link to them with "#Heading", "#Subheading" and so forth.</p>
<h3>Numbered Headings</h3>
<p>
You may also define numbered headings by prefixing lines with <code>h1#.</code>, <code>h2#.</code> etc. The heading numbers are then automatically managed by OpenProject for you.
</p>
<table>
<thead>
<tr>
<th>
Wiki format
</th>
<th style='padding-left: 3em'>
Resulting Text
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<pre>
h1#. Topic Foo
h2#. Sub-Topic Foo Bar
h1#. Topic Baz
</pre>
</td>
<td style='padding-left: 3em'>
<strong>1. Topic A</strong><br/>
1.2. Sub-Topic 1<br/>
<strong>2. Topic B</strong>
</td>
</tr>
</tbody>
</table>
<h3><a name="9" class="wiki-page"></a>Paragraphs</h3>
<pre>
p>. right aligned
p=. centered
</pre>
<p style="text-align:center;">This is a centered paragraph.</p>
<h3><a name="10" class="wiki-page"></a>Blockquotes</h3>
<p>Start the paragraph with <strong>bq.</strong></p>
<pre>
bq. Rails is a full-stack framework for developing database-backed web applications according to the Model-View-Control pattern.
To go live, all you need to add is a database and a web server.
</pre>
<p>Display:</p>
<blockquote>
<p>Rails is a full-stack framework for developing database-backed web applications according to the Model-View-Control pattern.<br />
To go live, all you need to add is a database and a web server.</p>
</blockquote>
<h3><a name="11" class="wiki-page"></a>Table of content</h3>
<pre>
{{toc}} => left aligned toc
{{>toc}} => right aligned toc
</pre>
<h3><a name="11" class="wiki-page"></a>Acronyms</h3>
<p>Display tooltip for acronym by entering tooltip in parantheses after upper case
acronym.</p>
<pre>
WHO(World Health Organisation) => Displays "World Health Organisation" as
tooltip of "WHO"
</pre>
<p>
<acronym title="World Health Organisation">WHO</acronym>
</p>
<h2><a name="12" class="wiki-page"></a>Macros</h2>
<p>OpenProject has the following builtin macros:</p>
<p>
<dl>
<dt><code>hello_world</code></dt>
<dd>
<p>Sample macro.</p>
</dd>
<dt><code>include</code></dt>
<dd>
<p>Include a wiki page. Example:</p>
<pre><code>{{include(Foo)}}</code></pre>
</dd>
<dt><code>child_pages</code></dt>
<dd>
<p>Displays a list of child pages. With no argument, it displays the child pages of the current wiki page. Examples:</p>
<pre><code>
{{child_pages}} -- can be used from a wiki page only
{{child_pages(Foo)}} -- lists all children of page Foo
{{child_pages(Foo, parent=1)}} -- same as above with a link to page Foo
</code></pre>
</dd>
<dt><code>macro_list</code></dt>
<dd>
<p>Displays a list of all available macros, including description if available.</p>
</dd>
</dl>
</p>
<h2><a name="13" class="wiki-page"></a>Code highlighting</h2>
<p>Code highlighting relies on <a href="http://coderay.rubychan.de/" class="external">CodeRay</a>, a fast syntax highlighting library written completely in Ruby. It currently supports c, cpp, css, delphi, groovy, html, java, javascript, json, php, python, rhtml, ruby, scheme, sql, xml and yaml languages.</p>
<p>You can highlight code in your wiki page using this syntax:</p>
<pre>
&lt;pre&gt;&lt;code class="ruby"&gt;
Place you code here.
&lt;/code&gt;&lt;/pre&gt;
</pre>
<p>Example:</p>
<pre><code class="ruby CodeRay"><span class="no"> 1</span> <span class="c"># The Greeter class</span>
<span class="no"> 2</span> <span class="r">class</span> <span class="cl">Greeter</span>
<span class="no"> 3</span> <span class="r">def</span> <span class="fu">initialize</span>(name)
<span class="no"> 4</span> <span class="iv">@name</span> = name.capitalize
<span class="no"> 5</span> <span class="r">end</span>
<span class="no"> 6</span>
<span class="no"> 7</span> <span class="r">def</span> <span class="fu">salute</span>
<span class="no"> 8</span> puts <span class="s"><span class="dl">"</span><span class="k">Hello </span><span class="il"><span class="idl">#{</span><span class="iv">@name</span><span class="idl">}</span></span><span class="k">!</span><span class="dl">"</span></span>
<span class="no"> 9</span> <span class="r">end</span>
<span class="no"><strong>10</strong></span> <span class="r">end</span>
</code>
</pre>

@ -49,13 +49,6 @@ See docs/COPYRIGHT.rdoc for more details.
</span>
</div>
<div class="form--field"><%= setting_select :protocol, [['HTTP', 'http'], ['HTTPS', 'https']], container_class: '-xslim' %></div>
<div class="form--field"><%= setting_select :text_formatting, OpenProject::TextFormatting::Formatters.format_names.collect{|name| [t("text_formatting.#{name}"), name]}, container_class: '-xslim' %></div>
<div class="form--field">
<%= setting_check_box :use_wysiwyg %>
<span class="form--field-instructions">
<%= t(:setting_use_wysiwyg_description) %>
</span>
</div>
<div class="form--field"><%= setting_check_box :cache_formatted_text %></div>
<div class="form--field"><%= setting_select :wiki_compression, [['Gzip', 'gzip']], blank: :label_none, container_class: '-xslim' %></div>
<div class="form--field"><%= setting_check_box :feeds_enabled, size: 6 %></div>

@ -13,8 +13,6 @@ OpenProject::Application.configure do
copy_issue_actions.js
date-de-DE.js
date-en-US.js
jstoolbar/textile.js
jstoolbar/markdown.js
locales/*.js
members_form.js
members_select_boxes.js

@ -1,131 +0,0 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See docs/COPYRIGHT.rdoc for more details.
#++
require 'redcloth3'
module RedCloth3Patch
private
def block_textile_prefix(text)
text.replace(prepend_number_to_heading(text))
super(text)
end
HEADING = /^h(\d)\.(.*)$/ unless defined? HEADING
NUMBERED_HEADING = /^h(\d)#\.(.*)$/ unless defined? NUMBERED_HEADING
def prepend_number_to_heading(text)
if text =~ NUMBERED_HEADING
level = $1.to_i
number = get_next_number_or_start_new_numbering level
new_text = "h#{level}. #{number}#{$2}"
elsif text =~ HEADING
reset_numbering
end
new_text.nil? ? text : new_text
end
def get_next_number_or_start_new_numbering(level)
begin
number = get_number_for_level level
rescue ArgumentError
reset_numbering
number = get_number_for_level level
end
number
end
def get_number_for_level(level)
@numbering_provider ||= Redcloth3::NumberingStack.new level
@numbering_provider.get_next_numbering_for_level level
end
def reset_numbering
@numbering_provider = nil
end
end
RedCloth3.send(:prepend, RedCloth3Patch)
module Redcloth3
class NumberingStack
def initialize(level)
@stack = []
@init_level = level ? level.to_i : 1
end
def get_next_numbering_for_level(level)
internal_level = map_external_to_internal_level level
increase_numbering_for_level internal_level
current_numbering
end
private
def increase_numbering_for_level(level)
if @stack[level].nil?
@stack[level] = 1
fill_nil_levels_with_zero
else
@stack[level] += 1
reset_higher_levels_than level
end
@stack[level]
end
def reset_higher_levels_than(level)
@stack = @stack.slice! 0, level + 1
end
def current_numbering
@stack.join('.') + '.'
end
def map_external_to_internal_level(level)
if level.to_i < @init_level
raise ArgumentError, 'Current level lower than initial level'
end
level.to_i - @init_level
end
def fill_nil_levels_with_zero
@stack.map! { |e| e.nil? ? 0 : e }
end
end
end

@ -1999,7 +1999,6 @@ en:
setting_brute_force_block_after_failed_logins: "Block user after this number of failed login attempts"
setting_brute_force_block_minutes: "Time the user is blocked for"
setting_cache_formatted_text: "Cache formatted text"
setting_use_wysiwyg: "WYSIWYG editor"
setting_use_wysiwyg_description: "Select to enable CKEditor5 WYSIWYG editor for all users by default. CKEditor has limited functionality for GFM Markdown."
setting_column_options: "Customize the appearance of the work package lists"
setting_commit_fix_keywords: "Fixing keywords"
@ -2074,7 +2073,6 @@ en:
setting_start_of_week: "Week starts on"
setting_sys_api_enabled: "Enable repository management web service"
setting_sys_api_description: "The repository management web service provides integration and user authorization for accessing repositories."
setting_text_formatting: "Text formatting"
setting_time_format: "Time format"
setting_accessibility_mode_for_anonymous: "Enable accessibility mode for anonymous users"
setting_user_format: "Users display format"
@ -2099,7 +2097,6 @@ en:
deletion: "Deletion"
text_formatting:
textile: 'Textile'
markdown: 'Markdown'
plain: 'Plain text'

@ -88,7 +88,6 @@ OpenProject::Application.routes.draw do
mount API::Root => '/'
get '/roles/workflow/:id/:role_id/:type_id' => 'roles#workflow'
get '/help/:ctrl/:page' => 'help#index'
get '/types/:id/edit/:tab' => "types#edit",
as: "edit_type_tab"
@ -493,8 +492,6 @@ OpenProject::Application.routes.draw do
resource :help, controller: :help, only: [] do
member do
get :wiki_syntax
get :wiki_syntax_detailed
get :keyboard_shortcuts
get :text_formatting
end

@ -77,7 +77,7 @@ consent_time:
consent_info:
serialized: true
default:
en: "h2. Consent\n\nYou need to agree to the privacy and security policy of this OpenProject instance."
en: "## Consent\n\nYou need to agree to the privacy and security policy of this OpenProject instance."
# Indicates wether or not users need to consent to something such as privacy policy.
consent_required:
default: 0
@ -144,10 +144,6 @@ bcc_recipients:
default: 1
plain_text_mail:
default: 0
text_formatting:
default: textile
use_wysiwyg:
default: 1
cache_formatted_text:
default: 0
wiki_compression:

@ -1,5 +1,3 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 the OpenProject Foundation (OPF)
@ -28,20 +26,15 @@
# See docs/COPYRIGHT.rdoc for more details.
#++
require_relative '../legacy_spec_helper'
describe HelpController, type: :controller do
render_views
specify 'renders wiki_syntax properly' do
get 'wiki_syntax'
assert_select 'h1', 'Wiki Syntax Quick Reference'
end
class ConvertToMarkdown < ActiveRecord::Migration[5.1]
def up
setting = Setting.where(name: 'text_formatting').pluck(:value)
specify 'renders wiki_syntax_detailed properly' do
get 'wiki_syntax_detailed'
if setting && setting[0] == 'textile'
converter = OpenProject::TextFormatting::Formatters::Markdown::TextileConverter.new
converter.run!
end
assert_select 'h1', 'Wiki Formatting'
Setting.where(name: %w(text_formatting use_wysiwyg)).delete_all
end
end

@ -143,24 +143,21 @@ embedded errors or simply state that multiple errors occured.
# Formattable Text
OpenProject supports text formatting in Textile. Other text formats *may* be introduced in the future.
Properties that contain formattable text have a special representation in this API. In this specification their
OpenProject supports text formatting in Markdown. Properties that contain formattable text have a special representation in this API. In this specification their
type is indicated as `Formattable`. Their representation contains the following properties:
| Property | Description | Type | Example | Supported operations |
|:--------:| -------------------------------------------------- | ------ | ---------------------------------- | -------------------- |
| format | Indicates the formatting language of the raw text | String | textile | READ |
| raw | The raw text, as entered by the user | String | `I *am* formatted!` | READ / WRITE |
| format | Indicates the formatting language of the raw text | String | markdown | READ |
| raw | The raw text, as entered by the user | String | `I **am** formatted!` | READ / WRITE |
| html | The text converted to HTML according to the format | String | `I <strong>am</strong> formatted!` | READ |
`format` can as of today have one of the following values:
* `plain` - only basic formatting, e.g. inserting paragraphs and line breaks into HTML
* `textile` - formatting using Textile
* `markdown` - formatting using Markdown
* `custom` - There is no apparent formatting rule that transforms the raw version to HTML (only used for read only properties)
More formats might be added in the future.
Note that `raw` is the only property supporting the **write** operation. A property of type *Formattable* that
is marked as **read and write**, will only accept changes to the `raw` property. Changes to `format` and `html` will be **silently ignored**.
It is thus sufficient to solely provide the `raw` property for changes.
@ -170,8 +167,8 @@ If the *Formattable* is marked as **read only**, the `raw` attribute also become
#### Example
{
"format": "textile",
"raw": "I *am* formatted!",
"format": "markdown",
"raw": "I **am** formatted!",
"html": "I <strong>am</strong> formatted!"
}

@ -35,13 +35,13 @@ Activity can be either _type Activity or _type Activity::Comment.
"id": 1,
"details": [
{
"format": "textile",
"format": "markdown",
"raw": "Lorem ipsum dolor sit amet.",
"html": "<p>Lorem ipsum dolor sit amet.</p>"
}
],
"comment": {
"format": "textile",
"format": "markdown",
"raw": "Lorem ipsum dolor sit amet.",
"html": "<p>Lorem ipsum dolor sit amet.</p>"
},

@ -21,7 +21,7 @@ Subsequent calls to the form should contain a single JSON object as described by
|:-------------------:| --------------------------------------------------------------------- | -------------------------------- |
| validate | Validate changes, show errors and allowed values for changed resource | |
| commit | Actually perform changes to the resource | form content is valid |
| previewMarkup | Post markup (e.g. textile) here to receive an HTML-rendered response | |
| previewMarkup | Post markup (e.g. markdown) here to receive an HTML-rendered response | |
## Linked Properties
@ -88,7 +88,7 @@ That is because the main purpose of a form is helping the client to sort out val
"method": "POST"
},
"previewMarkup": {
"href": "/api/v3/render/textile",
"href": "/api/v3/render/markdown",
"method": "POST"
},
"commit": {

@ -45,7 +45,7 @@
"attributeCaption": 'ID',
"scope": 'WorkPackage',
"helpText": {
"format": "textile",
"format": "markdown",
"raw": "Help text for id attribute.",
"html": "<p>Help text for id attribute.</p>"
}
@ -62,7 +62,7 @@
"attributeCaption": 'Status',
"scope": 'WorkPackage',
"helpText": {
"format": "textile",
"format": "markdown",
"raw": "Help text for status attribute.",
"html": "<p>Help text for status attribute.</p>"
}
@ -99,7 +99,7 @@
"attributeCaption": 'ID',
"scope": 'WorkPackage',
"helpText": {
"format": "textile",
"format": "markdown",
"raw": "Help text for id attribute.",
"html": "<p>Help text for id attribute.</p>"
}

@ -1,27 +1,28 @@
# Group Previewing
Throughout OpenProject user input for many properties can be formatted (e.g. using *Textile*).
Throughout OpenProject user input for many properties can be formatted using *Markdown*.
Using the appropriate rendering endpoint it is possible to render custom formatted inputs into HTML and thus
receive a preview of the rendered text.
The request to a rendering endpoint must always have a MIME-Type of `text/plain`.
The request body is the actual string that shall be rendered as HTML string.
## Textile [/api/v3/render/textile{?context}]
## Markdown [/api/v3/render/markdown{?context}]
+ Model
+ Body
<p>Hello world! <a href="http://example.com">This</a> <strong>is</strong> textile!</p>
<p>Hello world! <a href="http://example.com">This</a> <strong>is</strong> markdown!</p>
## Preview Textile document [POST]
## Preview Markdown document [POST]
+ Parameters
+ context (optional, string, `/api/v3/work_packages/42`)
API-Link to the context in which the rendering occurs, for example a specific work package.
If left out only context-agnostic rendering takes place.
Please note that OpenProject features textile-extensions that can only work given a context (e.g. display attached images).
Please note that OpenProject features markdown-extensions on top of the extensions GitHub Flavored Markdown (gfm) already
provides that can only work given a context (e.g. display attached images).
**Supported contexts:**
@ -29,11 +30,11 @@ The request body is the actual string that shall be rendered as HTML string.
+ Request (text/plain)
Hello world! "This":http://example.com *is* textile!
Hello world! "This":http://example.com **is** markdown!
+ Response 200 (text/html)
[Textile][]
[Markdown][]
+ Response 400 (application/json)
@ -47,7 +48,7 @@ The request body is the actual string that shall be rendered as HTML string.
{
"_type": "Error",
"errorIdentifier": "urn:openproject-org:api:v3:errors:InvalidRenderContext",
"message": "Could not render textile string in the given context."
"message": "Could not render markdown string in the given context."
}
+ Response 415 (application/json)

@ -9,7 +9,7 @@
| addRelation | Adds a relation to this work package. | **Permission**: manage wp relations |
| addWatcher | Add any user to WP watchers | **Permission**: add watcher |
| customActions | Collection of predefined changes that can be applied to the work package | |
| previewMarkup | Post markup (e.g. textile) here to receive an HTML-rendered response | |
| previewMarkup | Post markup (in markdown) here to receive an HTML-rendered response | |
| removeWatcher | Remove any user from WP watchers | **Permission**: delete watcher |
| unwatch | Remove current user from WP watchers | logged in; watching |
| update | Form endpoint that aids in preparing and performing edits on a WP | **Permission**: edit work package |
@ -253,7 +253,7 @@ and [update](#work-packages-work-package-patch). The attachments the work packag
"id": 1528,
"subject": "Develop API",
"description": {
"format": "textile",
"format": "markdown",
"raw": "Develop super cool OpenProject API.",
"html": "<p>Develop super cool OpenProject API.</p>"
},
@ -1962,7 +1962,7 @@ Only linked revisions from repositories are shown if the user has the view chang
"id": 1,
"details": [ ],
"comment": {
"format": "textile",
"format": "markdown",
"raw": "Lorem ipsum dolor sit amet.",
"html": "<p>Lorem ipsum dolor sit amet.</p>"
},
@ -1985,7 +1985,7 @@ Only linked revisions from repositories are shown if the user has the view chang
"id": 2,
"details": [ ],
"comment": {
"format": "textile",
"format": "markdown",
"raw": "Lorem ipsum dolor sit amet.",
"html": "<p>Lorem ipsum dolor sit amet.</p>"
},

@ -1,104 +0,0 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See docs/COPYRIGHT.rdoc for more details.
#++
Feature: Issue textile quickinfo links
Background:
Given there is 1 project with the following:
| name | parent |
| identifier | parent |
And I am working in project "parent"
And there is a board "development discussion" for project "parent"
And there is a role "member"
And the role "member" may have the following rights:
| manage_boards |
| add_messages |
| edit_messages |
| edit_own_messages |
| delete_messages |
| delete_messages |
| delete_own_messages |
And there is 1 user with the following:
| login | bob |
And the user "bob" is a "member" in the project "parent"
And I am already logged in as "bob"
Scenario: Adding a message to a forum
When I go to the boards page of the project called "parent"
And I follow "New message"
And I fill in "New relase" for "message_subject"
And I fill in "We have release a new version of our software." for "message_content"
When I click on the first button matching "Create"
Then I should see "New relase" within "#content"
Then I should see "We have release a new version of our software." within "#content"
Scenario: Message's reply count is zero
Given the board "development discussion" has the following messages:
| message #1 |
When I go to the message page of message "message #1"
Then I should not see "Replies"
Scenario: Message's reply count is two
Given the board "development discussion" has the following messages:
| message #1 |
And "message #1" has the following replies:
| reply #1 |
| reply #2 |
When I go to the message page of message "message #1"
Then I should see "Replies (2)"
Scenario: Check field value after error message raise when title is empty
When I go to the boards page of the project called "parent"
And I follow "New message"
And I fill in "New relase FAQ" for "message_subject"
When I click on the first button matching "Create"
Then there should be an error message
Then the "message_subject" field should contain "New relase FAQ"
Scenario: Check field value after error message raise when description is empty
When I go to the boards page of the project called "parent"
And I follow "New message"
And I fill in "Here you find the most frequently asked questions" for "message_content"
When I click on the first button matching "Create"
Then there should be an error message
Then the "message_content" field should contain "Here you find the most frequently asked questions"
@javascript
Scenario: Sticky message on top of messages list
Given the board "development discussion" has the following messages:
| message #1 |
| message #2 |
| message #3 |
When I go to the boards page of the project called "parent"
And I follow "New message"
And I fill in "How to?" for "message_subject"
And I fill in "How to st-up project on local mashine." for "message_content"
And I check "Sticky"
When I click on the first button matching "Create"
And I go to the boards page of the project called "parent"
Then "How to?" should be the first row in table

@ -1,73 +0,0 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See docs/COPYRIGHT.rdoc for more details.
#++
Feature: Adding and Editing Wiki Tabs
Background:
Given there is 1 project with the following:
| Name | Wookies |
And the project "Wookies" uses the following modules:
| wiki |
And the project "Wookies" has 1 wiki page with the following:
| Title | wookietest |
@javascript
Scenario: Adding simple wiki tab as admin
Given I am admin
And I am working in project "Wookies"
And I go to the wiki index page of the project called "Wookies"
@javascript
Scenario: Editing of wiki pages as a member with proper rights
Given there is 1 user with the following:
| login | chewbacca|
And I am logged in as "chewbacca"
And there is a role "humanoid"
And the role "humanoid" may have the following rights:
| view_wiki_pages |
| edit_wiki_pages |
And the user "chewbacca" is a "humanoid" in the project "Wookies"
When I go to the wiki page "wookietest" for the project called "Wookies"
And I click "Edit"
And I fill in "content_text" with "testing wookie"
And I click "Save"
Then I should see "testing wookie" within "#content"
And I click "Edit"
@javascript
Scenario: Overview and see the history of a wiki page
Given I am already admin
Given the wiki page "wookietest" of the project "Wookies" has 3 versions
And I go to the wiki page "wookietest" for the project called "Wookies"
And I follow "More" within "#content"
When I click "History"
Then I should see "History" within "#content"
When I press "View differences"
Then I should see "Version 1/4"
Then I should see "Version 2/4"

@ -1,71 +0,0 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See docs/COPYRIGHT.rdoc for more details.
#++
Feature: Creating a wiki child page
Background:
Given there are no wiki menu items
And there is 1 user with the following:
| login | bob |
And there is a role "member"
And the role "member" may have the following rights:
| view_wiki_pages |
| edit_wiki_pages |
And there is 1 project with the following:
| name | project1 |
| identifier | project1 |
| name | project1 |
And the user "bob" is a "member" in the project "project1"
And I am already logged in as "bob"
@javascript
Scenario: A user with proper rights can add a child wiki page
Given the project "project1" has 1 wiki page with the following:
| title | Wikiparentpage |
Given I go to the wiki index page of the project called "project1"
And I click "Wikiparentpage"
And I click "Wiki page"
And I fill in "content_page_title" with "Todd's wiki"
And I fill in "content_text" with "Great content"
And I press "Save"
When I go to the wiki index page of the project called "project1"
Then I should see "Todd's wiki" within "#content"
@javascript
Scenario: Creating a wiki child page the title of which contains special characters
Given the project "project1" has 1 wiki page with the following:
| title | ParentWikiPage |
And the project "project1" has 1 wiki menu item with the following:
| title | ParentWikiPage |
| new_wiki_page | true |
When I go to the wiki page "ParentWikiPage" of the project called "project1"
And I click "Wiki page"
And I fill in "content_page_title" with "Child Page !@#{$%^&*()_},./<>?;':"
And I fill in "content_text" with "Great content"
And I click "Save"
Then I should see "Successful creation."

@ -1,52 +0,0 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See docs/COPYRIGHT.rdoc for more details.
#++
Feature: Activating and deactivating wiki menu as admin
Background:
Given I am admin
And there is 1 project with the following:
| Name | Wookie |
@javascript
Scenario: Activation of wiki module via aproject settings as admin
When I go to the settings page of the project called "Wookie"
And I click on "tab-modules"
And I check "Wiki"
And I press "Save"
Then I should see "Wiki" within "#menu-sidebar"
@javascript
Scenario: Deactivation of wiki module via project settings
When I go to the settings page of the project called "Wookie"
And I click on "tab-modules"
And I uncheck "Wiki"
And I press "Save"
And I should not see "Wiki" within "#menu-sidebar"

@ -1,66 +0,0 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See docs/COPYRIGHT.rdoc for more details.
#++
Feature: Viewing the wiki new child page
Background:
Given there are no wiki menu items
And there is 1 user with the following:
| login | bob |
And there is a role "member"
And the role "member" may have the following rights:
| view_wiki_pages |
| edit_wiki_pages |
And there is 1 project with the following:
| name | project1 |
| identifier | project1 |
And the user "bob" is a "member" in the project "project1"
And I am already logged in as "bob"
Scenario: Visiting the wiki new child page with a parent page that has the new child page option enabled on it's menu item should show the page and select the toc menu entry within the wiki menu item
Given the project "project1" has 1 wiki page with the following:
| title | ParentWikiPage |
And the project "project1" has 1 wiki menu item with the following:
| title | ParentWikiPage |
| new_wiki_page | true |
When I go to the wiki page "ParentWikiPage" of the project called "project1"
Then I should see "Wiki page" within "#content"
Scenario: Visiting the wiki new child page with a related page that has the new child page option disabled on it's menu item should show the page and select no menu item
Given the project "project1" has 1 wiki page with the following:
| title | ParentWikiPage |
And the project "project1" has 1 wiki menu item with the following:
| title | ParentWikiPage |
When I go to the wiki new child page below the "ParentWikiPage" page of the project called "project1"
Then I should see "Wiki page" within "#content"
And there should be no child menu item selected
Scenario: Visiting the wiki new child page with an invalid parent page
When I go to the wiki new child page below the "InvalidPage" page of the project called "project1"
Then I should see "404" within "#content"
Then I should see "The page you were trying to access doesn't exist or has been removed."

@ -71,7 +71,7 @@ export class AttributeHelpTextController {
});
}
// HACK: without this, the tempalte is not displayed
// HACK: without this, the template is not displayed
setTimeout(() => this.$scope.$apply());
});
}

@ -188,7 +188,6 @@ import {WorkPackageCommentComponent} from "core-components/work-packages/work-pa
import {OpCkeditorFormComponent} from "core-components/ckeditor/op-ckeditor-form.component";
import {WorkPackageUploadComponent} from "core-components/wp-attachments/wp-attachments-upload/wp-attachments-upload.component";
import {OpDragScrollDirective} from "core-app/modules/common/ui/op-drag-scroll.directive";
import {TextileService} from "core-app/modules/common/textile/textile-service";
import {UIRouterModule} from "@uirouter/angular";
import {initializeUiRouterConfiguration} from "core-components/routing/ui-router.config";
import {WorkPackagesBaseComponent} from "core-components/routing/main/work-packages-base.component";
@ -250,7 +249,6 @@ import {WikiIncludePageMacroModal} from "core-components/modals/editor/macro-wik
deps: [Injector],
multi: true
},
TextileService,
OpTitleService,
TimezoneService,
WorkPackageRelationsService,

@ -88,7 +88,7 @@ export class OpCkeditorFormComponent implements OnInit {
this.wrappedTextArea = this.formElement.find(this.textareaSelector);
this.wrappedTextArea.hide();
const wrapper = this.$element.find(`.${ckEditorReplacementClass}`);
window.OPClassicEditor
let editorPromise = window.OPClassicEditor
.create(wrapper[0], {
openProject: {
context: null,
@ -100,6 +100,8 @@ export class OpCkeditorFormComponent implements OnInit {
.catch((error:any) => {
console.error(error);
});
this.$element.data('editor', editorPromise);
}
public $onDestroy() {
@ -123,6 +125,23 @@ export class OpCkeditorFormComponent implements OnInit {
// Continue with submission
return true;
});
this.setLabel();
return editor;
}
}
private setLabel() {
let textareaId = this.textareaSelector.substring(1);
let label = jQuery(`label[for=${textareaId}`);
let ckContent = this.$element.find('.ck-content');
ckContent.attr('aria-label', null);
ckContent.attr('aria-labelledby', textareaId);
label.click(() => {
ckContent.focus();
});
}
}

@ -33,7 +33,6 @@ import {ConfigurationService} from 'core-app/modules/common/config/configuration
import {WorkPackageCommentField} from 'core-components/work-packages/work-package-comment/wp-comment-field.module';
import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource';
import {WorkPackagesActivityService} from 'core-components/wp-single-view-tabs/activity-panel/wp-activity.service';
import {TextileService} from "core-app/modules/common/textile/textile-service";
import {AfterViewInit, Component, ElementRef, Inject, Input, OnInit} from "@angular/core";
import {UserCacheService} from "core-components/user/user-cache.service";
import {IEditFieldHandler} from "core-app/modules/fields/edit/editing-portal/edit-field-handler.interface";
@ -76,7 +75,7 @@ export class UserActivityComponent implements IEditFieldHandler, OnInit, AfterVi
label_updated_on: this.I18n.t('js.label_updated_on'),
quote_comment: this.I18n.t('js.label_quote_comment'),
edit_comment: this.I18n.t('js.label_edit_comment'),
}
};
public accessibilityModeEnabled = this.ConfigurationService.accessibilityModeEnabled();
private $element:JQuery;
@ -88,8 +87,7 @@ export class UserActivityComponent implements IEditFieldHandler, OnInit, AfterVi
readonly wpCacheService:WorkPackageCacheService,
readonly ConfigurationService:ConfigurationService,
readonly userCacheService:UserCacheService,
readonly I18n:I18nService,
readonly textileService:TextileService) {
readonly I18n:I18nService) {
}
public ngOnInit() {

@ -1,42 +0,0 @@
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2018 the OpenProject Foundation (OPF)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2017 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See docs/COPYRIGHT.rdoc for more details.
//++
import {WikiToolbar} from "core-app/globals/augmenting/wiki-toolbar";
(function($:JQueryStatic) {
// Identify all uses
$(function () {
// Wrap all static wiki-toolbars (rendered from backend)
$('.wiki-toolbar')
.each((i:number, el:HTMLElement) => {
new WikiToolbar(I18n, el);
});
});
}(jQuery));

@ -1,91 +0,0 @@
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2018 the OpenProject Foundation (OPF)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2017 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See docs/COPYRIGHT.rdoc for more details.
//++
import {GlobalI18n} from "core-app/modules/common/i18n/i18n.service";
export class WikiToolbar {
public isPreview = false;
constructor(protected I18n:GlobalI18n,
protected element:HTMLElement,
public previewCallback?:(preview:boolean) => void) {
this.onInit();
}
onInit() {
const element = jQuery(this.element);
const help_link_title = this.I18n.t('js.inplace.link_formatting_help');
const button = document.createElement('button');
button.classList.add('jstb_help', 'formatting-help-link-button');
button.setAttribute('type', 'button');
button.setAttribute('aria-label', help_link_title);
button.setAttribute('title', help_link_title);
const PREVIEW_ENABLE_TEXT = this.I18n.t('js.inplace.btn_preview_enable');
const PREVIEW_DISABLE_TEXT = this.I18n.t('js.inplace.btn_preview_disable');
const PREVIEW_BUTTON_CLASS = 'jstb_preview';
let previewButtonAttributes:any = {
'class': PREVIEW_BUTTON_CLASS + ' icon-preview icon-small',
'type': 'button',
'title': PREVIEW_ENABLE_TEXT,
'aria-label': PREVIEW_ENABLE_TEXT,
'text': ''
};
let textarea = this.element;
if (!element.is('textarea')) {
textarea = this.element.querySelector('textarea') as any;
}
const wikiToolbar = new (window as any).jsToolBar(textarea);
wikiToolbar.setHelpLink(button);
wikiToolbar.draw();
previewButtonAttributes.click = () => {
this.isPreview = !this.isPreview;
!!this.previewCallback && this.previewCallback(this.isPreview);
const title = this.isPreview ? PREVIEW_DISABLE_TEXT : PREVIEW_ENABLE_TEXT;
const toggledClasses = 'icon-preview icon-ticket-edit -active';
element.closest('.textarea-wrapper')
.find('.' + PREVIEW_BUTTON_CLASS).attr('title', title)
.attr('aria-label', title)
.toggleClass(toggledClasses);
};
if (!!this.previewCallback) {
element
.closest('.textarea-wrapper')
.find('.jstb_help')
.after(jQuery('<button>', previewButtonAttributes));
}
}
}

@ -123,14 +123,6 @@ export class ConfigurationService {
this.settings.display.date_format !== '';
}
public textFormat() {
return this.settings.display.text_format;
}
public useWysiwyg() {
return this.settings.display.use_wysiwyg;
}
public dateFormat() {
return this.settings.display.date_format;
}

@ -55,7 +55,6 @@ import {FocusDirective} from "core-app/modules/common/focus/focus.directive";
import {I18nService} from "core-app/modules/common/i18n/i18n.service";
import {HighlightColDirective} from "core-app/modules/common/highlight-col/highlight-col.directive";
import {CopyToClipboardDirective} from "core-app/modules/common/copy-to-clipboard/copy-to-clipboard.directive";
import {WikiToolbarDirective} from "core-app/modules/common/wiki-toolbar/wiki-toolbar.directive";
import {highlightColBootstrap} from "./highlight-col/highlight-col.directive";
import {HookService} from "../plugins/hook-service";
import {HTMLSanitizeService} from "./html-sanitize/html-sanitize.service";
@ -98,9 +97,6 @@ export function bootstrapModule(injector:Injector) {
// Entries for ng1 downgraded components
AttributeHelpTextComponent,
// Add functionality to rails rendered templates
WikiToolbarDirective,
// Table highlight
HighlightColDirective,
],
@ -129,8 +125,7 @@ export function bootstrapModule(injector:Injector) {
HighlightColDirective,
// Add functionality to rails rendered templates
CopyToClipboardDirective,
WikiToolbarDirective
CopyToClipboardDirective
],
entryComponents: [
OpDateTimeComponent,

@ -1,59 +0,0 @@
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2017 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See doc/COPYRIGHT.rdoc for more details.
//++
import {PathHelperService} from './../path-helper/path-helper.service';
import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource';
import {HalLink} from 'core-app/modules/hal/hal-link/hal-link';
import {WorkPackageNotificationService} from "core-components/wp-edit/wp-notification.service";
import {Inject, Injectable} from "@angular/core";
import {HttpClient} from "@angular/common/http";
@Injectable()
export class TextileService {
constructor(private readonly http:HttpClient,
private readonly wpNotificationsService:WorkPackageNotificationService,
private readonly PathHelper:PathHelperService) {
}
public renderWithWorkPackageContext(workPackage:WorkPackageResource, text:string) {
return this.render(workPackage.previewMarkup, text);
}
public render(link:HalLink, text:string) {
return this.http
.request(
link.method,
link.href!, {
body: text,
responseType: 'text',
headers: {'Content-Type': 'text/plain; charset=UTF-8'}
})
.toPromise()
.catch((error:any) => this.wpNotificationsService.handleRawError(error));
}
}

@ -1,59 +0,0 @@
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2018 the OpenProject Foundation (OPF)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2017 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See docs/COPYRIGHT.rdoc for more details.
//++
import {AfterViewInit, Directive, ElementRef, EventEmitter, OnDestroy, Output} from "@angular/core";
import {I18nService} from "core-app/modules/common/i18n/i18n.service";
import {WikiToolbar} from "core-app/globals/augmenting/wiki-toolbar";
@Directive({
selector: '[op-wiki-toolbar]'
})
export class WikiToolbarDirective implements AfterViewInit, OnDestroy {
@Output() onPreviewToggle = new EventEmitter<undefined>();
public instance:WikiToolbar;
constructor(readonly I18n:I18nService,
readonly elementRef:ElementRef) {
}
ngAfterViewInit() {
this.instance = new WikiToolbar(
this.I18n,
this.elementRef.nativeElement,
() => {
this.onPreviewToggle.emit();
});
}
ngOnDestroy() {
// nothing to do
}
}

@ -44,6 +44,6 @@ import {FormattableEditField} from "core-app/modules/fields/edit/field-types/for
</div>
`
})
export class FormattableWysiwygEditFieldComponent extends EditFieldComponent {
export class FormattableEditFieldComponent extends EditFieldComponent {
public field:FormattableEditField;
}

@ -26,13 +26,10 @@
// See doc/COPYRIGHT.rdoc for more details.
// ++
import {ConfigurationService} from 'core-app/modules/common/config/configuration.service';
import {TextileService} from "core-app/modules/common/textile/textile-service";
import {ICkeditorStatic} from "core-components/ckeditor/op-ckeditor-form.component";
import {EditField} from "core-app/modules/fields/edit/edit.field.module";
import {FormattableTextareaEditFieldComponent} from "core-app/modules/fields/edit/field-types/formattable-textarea-edit-field.component";
import {FormattableWysiwygEditFieldComponent} from "core-app/modules/fields/edit/field-types/formattable-wysiwyg-edit-field.component";
import {PathHelperService} from "core-app/modules/common/path-helper/path-helper.service";
import {FormattableEditFieldComponent} from "core-app/modules/fields/edit/field-types/formattable-edit-field.component";
declare global {
interface Window {
@ -42,10 +39,7 @@ declare global {
}
export class FormattableEditField extends EditField {
// Dependencies
readonly textileService:TextileService = this.$injector.get(TextileService);
readonly pathHelper:PathHelperService = this.$injector.get(PathHelperService);
readonly ConfigurationService:ConfigurationService = this.$injector.get(ConfigurationService);
// Values used in template
public isBusy:boolean = false;
@ -57,34 +51,15 @@ export class FormattableEditField extends EditField {
cancel: this.I18n.t('js.inplace.button_cancel', { attribute: this.schema.name })
};
public wysiwig:boolean;
// CKEditor instance
public ckeditor:any;
protected initialize() {
const configurationService:ConfigurationService = this.$injector.get(ConfigurationService);
this.wysiwig = configurationService.textFormat() === 'markdown' && configurationService.useWysiwyg();
}
public get component() {
if (this.wysiwig) {
return FormattableWysiwygEditFieldComponent;
} else {
return FormattableTextareaEditFieldComponent;
}
}
public onSubmit() {
if (this.wysiwig && this.ckeditor) {
this.rawValue = this.ckeditor.getData();
}
return FormattableEditFieldComponent;
}
public $onInit(container:HTMLElement) {
if (this.wysiwig) {
this.setupMarkdownEditor(container);
}
this.setupMarkdownEditor(container);
}
public setupMarkdownEditor(container:HTMLElement) {
@ -105,16 +80,22 @@ export class FormattableEditField extends EditField {
}
setTimeout(() => editor.editing.view.focus());
this.updateValueOnEditorChange(editor);
})
.catch((error:any) => {
console.error(error);
});
}
private updateValueOnEditorChange(editor:any) {
editor.model.document.on('change', () => {
this.rawValue = this.ckeditor.getData();
} );
}
public reset() {
if (this.wysiwig) {
this.ckeditor.setData(this.rawValue);
}
this.ckeditor.setData(this.rawValue);
}
public get rawValue() {
@ -134,7 +115,7 @@ export class FormattableEditField extends EditField {
}
public isEmpty():boolean {
if (this.wysiwig && this.ckeditor) {
if (this.ckeditor) {
return this.ckeditor.getData() === '';
} else {
return !(this.value && this.value.raw);
@ -148,29 +129,4 @@ export class FormattableEditField extends EditField {
}
});
}
public togglePreview() {
this.isPreview = !this.isPreview;
this.previewHtml = '';
if (!this.rawValue) {
return;
}
if (this.isPreview) {
this.isBusy = true;
this.changeset.getForm().then((form:any) => {
const link = form.previewMarkup.$link;
this.textileService.render(link, this.rawValue)
.then((result:string) => {
this.isBusy = false;
this.previewHtml = result;
})
.catch(() => {
this.isBusy = false;
});
});
}
}
}

@ -1,80 +0,0 @@
// OpenProject is a project management system.
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See doc/COPYRIGHT.rdoc for more details.
// ++
import {AfterViewInit, Component, ElementRef, OnInit, ViewChild} from "@angular/core";
import {EditFieldComponent} from "core-app/modules/fields/edit/edit-field.component";
import {FormattableEditField} from "core-app/modules/fields/edit/field-types/formattable-edit-field";
import {ExpressionService} from "../../../../../../common/expression.service";
@Component({
template: `
<div class="textarea-wrapper"
op-wiki-toolbar
(onPreviewToggle)="field.togglePreview()"
[ngClass]="{'-preview': field.isPreview}">
<textarea
style="min-height: 114px"
#textAreaField
class="focus-input wp-inline-edit--field inplace-edit--textarea -animated"
name="value"
[hidden]="field.isPreview"
[disabled]="field.isBusy || field.inFlight"
[required]="field.required"
[(ngModel)]="field.rawValue"
[id]="handler.htmlId">
</textarea>
<div class="inplace-edit--preview"
*ngIf="field.isPreview && !field.isBusy">
<span [innerHTML]="unEscapedPreviewHtml"></span>
</div>
<div *ngIf="field.isFormattable && !field.isPreview"
class="wp-edit-field-attachment-label">
<span [textContent]="field.text.attachmentLabel"></span>
</div>
<edit-field-controls *ngIf="!handler.inEditMode"
[fieldController]="handler"
(onSave)="handler.handleUserSubmit()"
(onCancel)="handler.handleUserCancel()"
[saveTitle]="field.text.save"
[cancelTitle]="field.text.cancel">
</edit-field-controls>
</div>
`
})
export class FormattableTextareaEditFieldComponent extends EditFieldComponent implements AfterViewInit {
public field:FormattableEditField;
@ViewChild('textAreaField') public textAreaField:ElementRef;
ngAfterViewInit() {
this.textAreaField.nativeElement.focus();
}
public get unEscapedPreviewHtml() {
return ExpressionService.unescape(this.field.previewHtml || '');
}
}

@ -39,10 +39,9 @@ import {FormsModule} from "@angular/forms";
import {DurationEditFieldComponent} from "core-app/modules/fields/edit/field-types/duration-edit-field";
import {FloatEditFieldComponent} from "core-app/modules/fields/edit/field-types/float-edit-field";
import {IntegerEditFieldComponent} from "core-app/modules/fields/edit/field-types/integer-edit-field";
import {FormattableTextareaEditFieldComponent} from "core-app/modules/fields/edit/field-types/formattable-textarea-edit-field.component";
import {MultiSelectEditFieldComponent} from "core-app/modules/fields/edit/field-types/multi-select-edit-field";
import {SelectEditFieldComponent} from "core-app/modules/fields/edit/field-types/select-edit-field";
import {FormattableWysiwygEditFieldComponent} from "core-app/modules/fields/edit/field-types/formattable-wysiwyg-edit-field.component";
import {FormattableEditFieldComponent} from "core-app/modules/fields/edit/field-types/formattable-edit-field.component";
import {TextEditFieldComponent} from "core-app/modules/fields/edit/field-types/text-edit-field";
import {OpenprojectCommonModule} from "core-app/modules/common/openproject-common.module";
import {WorkPackageEditingPortalService} from "core-app/modules/fields/edit/editing-portal/wp-editing-portal-service";
@ -76,8 +75,7 @@ import {OpenprojectAccessibilityModule} from "core-app/modules/a11y/openproject-
DurationEditFieldComponent,
FloatEditFieldComponent,
IntegerEditFieldComponent,
FormattableWysiwygEditFieldComponent,
FormattableTextareaEditFieldComponent,
FormattableEditFieldComponent,
MultiSelectEditFieldComponent,
SelectEditFieldComponent,
TextEditFieldComponent,
@ -91,8 +89,7 @@ import {OpenprojectAccessibilityModule} from "core-app/modules/a11y/openproject-
DurationEditFieldComponent,
FloatEditFieldComponent,
IntegerEditFieldComponent,
FormattableWysiwygEditFieldComponent,
FormattableTextareaEditFieldComponent,
FormattableEditFieldComponent,
MultiSelectEditFieldComponent,
SelectEditFieldComponent,
TextEditFieldComponent,

@ -32,12 +32,14 @@ module API
class Formattable < Single
include OpenProject::TextFormatting
def initialize(model, format: nil, object: nil)
@format = format || Setting.text_formatting
def initialize(model, plain: false, object: nil)
@format = if plain
OpenProject::TextFormatting::Formats.plain_format
else
OpenProject::TextFormatting::Formats.rich_format
end
@object = object
@format = 'plain' if @format.blank?
# Note: TextFormatting actually makes use of User.current, if it was possible to pass a
# current_user explicitly, it would make sense to pass one here too.
super(model, current_user: nil)
@ -45,16 +47,16 @@ module API
property :format,
exec_context: :decorator,
getter: -> (*) { @format },
getter: ->(*) { @format },
writable: false,
render_nil: true
property :raw,
exec_context: :decorator,
getter: -> (*) { represented },
getter: ->(*) { represented },
render_nil: true
property :html,
exec_context: :decorator,
getter: -> (*) { to_html },
getter: ->(*) { to_html },
writable: false,
render_nil: true

@ -42,7 +42,7 @@ module API
property :file_name
property :description,
getter: ->(*) {
::API::Decorators::Formattable.new(description, format: 'plain')
::API::Decorators::Formattable.new(description, plain: true)
},
setter: ->(fragment:, **) { self.description = fragment['raw'] },
render_nil: true

@ -96,7 +96,7 @@ module API
getter: ->(*) { filesize }
property :description,
getter: ->(*) {
::API::Decorators::Formattable.new(description, format: 'plain')
::API::Decorators::Formattable.new(description, plain: true)
},
render_nil: true
property :content_type

@ -48,7 +48,7 @@ module API
helpers do
def authorize_by_policy(action, &block)
authorize_by_with_raise(-> () { allowed_to?(action) }, &block)
authorize_by_with_raise(-> { allowed_to?(action) }, &block)
end
def allowed_to?(action)

@ -57,8 +57,7 @@ module API
end
def check_format(format)
supported_formats = ::OpenProject::TextFormatting::Formatters.format_names.map(&:to_s)
unless supported_formats.include?(format)
unless ::OpenProject::TextFormatting::Formats.supported?(format)
fail ::API::Errors::NotFound, I18n.t('api_v3.errors.code_404')
end
end

@ -34,7 +34,7 @@ module API
include API::V3::Utilities
self_link path: :revision,
title_getter: -> (*) { nil }
title_getter: ->(*) { nil }
link :project do
{
@ -44,10 +44,12 @@ module API
end
link :author do
next if represented.user.nil?
{
href: api_v3_paths.user(represented.user.id),
title: represented.user.name
} unless represented.user.nil?
}
end
link :showRevision do
@ -63,16 +65,16 @@ module API
property :author, as: :authorName
property :message,
exec_context: :decorator,
getter: -> (*) {
getter: ->(*) {
::API::Decorators::Formattable.new(represented.comments,
object: represented,
format: 'plain')
plain: true)
},
render_nil: true
property :created_at,
exec_context: :decorator,
getter: -> (*) {
getter: ->(*) {
datetime_formatter.format_datetime(represented.committed_on)
}

@ -264,9 +264,12 @@ module API
"#{root}/revisions/#{id}"
end
def self.render_markup(format: nil, link: nil)
format = format || Setting.text_formatting
format = 'plain' if format == '' # Setting will return '' for plain
def self.render_markup(link: nil, plain: false)
format = if plain
OpenProject::TextFormatting::Formats.plain_format
else
OpenProject::TextFormatting::Formats.rich_format
end
path = "#{root}/render/#{format}"
path += "?context=#{link}" if link

@ -60,7 +60,7 @@ module API
getter: ->(*) {
::API::Decorators::Formattable.new(represented.description,
object: represented,
format: 'plain')
plain: true)
},
render_nil: true

@ -33,8 +33,6 @@ module OpenProject
gon.settings = {
user_preferences: user_preferences,
display: {
use_wysiwyg: Setting.use_wysiwyg?,
text_format: Setting.text_formatting,
date_format: momentjstify_date_format(Setting.date_format),
time_format: momentjstify_time_format(Setting.time_format),
start_of_week: Setting.start_of_week

@ -31,9 +31,7 @@ module OpenProject
module FormTagHelper
include ActionView::Helpers::FormTagHelper
TEXT_LIKE_FIELDS = [
'number_field', 'password_field', 'url_field', 'telephone_field', 'email_field'
].freeze
TEXT_LIKE_FIELDS = %w(number_field password_field url_field telephone_field email_field).freeze
def styled_form_tag(url_for_options = {}, options = {}, &block)
apply_css_class_to_options(options, 'form')
@ -88,8 +86,7 @@ module OpenProject
def text_formatting_wrapper(target_id)
return ''.html_safe unless target_id.present?
format = Setting.text_formatting
helper = ::OpenProject::TextFormatting::Formatters.helper_for(format).new(self)
helper = ::OpenProject::TextFormatting::Formats.rich_helper.new(self)
helper.wikitoolbar_for target_id
end

@ -95,7 +95,7 @@ module OpenProject
def link_to_message(message, options = {}, html_options = nil)
link_to(
h(truncate(message.subject, length: 60)),
topic_path_or_url(message.root,
topic_path_or_url(options.delete(:no_root) ? message : message.root,
{ r: (message.parent_id && message.id),
anchor: (message.parent_id ? "message-#{message.id}" : nil)
}.merge(options)),

@ -36,7 +36,6 @@ module OpenProject
# * with a String: format_text(text, options)
# * with an object and one of its attribute: format_text(issue, :description, options)
def format_text(*args)
options = args.last.is_a?(Hash) ? args.pop : {}
case args.size
when 1
@ -56,8 +55,11 @@ module OpenProject
only_path = options.delete(:only_path) != false
current_user = options.delete(:current_user) { User.current }
plain = ::OpenProject::TextFormatting::Formats.plain?(options.delete(:format))
Renderer.format_text text,
options.merge(
plain: plain,
object: object,
request: try(:request),
current_user: current_user,

@ -42,7 +42,7 @@ module OpenProject::TextFormatting
def call
doc.search('macro').each do |macro|
registered.each do |macro_class|
next unless macro['class'].include? macro_class.identifier
next unless Array(macro['class']).include? macro_class.identifier
begin
macro_class.apply(macro, result: result, context: context)

@ -0,0 +1,66 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See docs/COPYRIGHT.rdoc for more details.
#++
module OpenProject::TextFormatting
module Formats
class << self
attr_reader :plain, :rich
%i(plain rich).each do |flavor|
define_method("#{flavor}_format") do
send(flavor).format
end
define_method("#{flavor}_formatter") do
send(flavor).formatter
end
define_method("#{flavor}_helper") do
send(flavor).helper
end
define_method("register_#{flavor}!") do |klass|
instance_variable_set("@#{flavor}", klass)
end
end
def supported?(name)
[plain, rich].map(&:format).include?(name.to_sym)
end
def plain?(name)
name && plain.format == name.to_sym
end
end
end
end
OpenProject::TextFormatting::Formats.register_plain! ::OpenProject::TextFormatting::Formats::Plain::Format
OpenProject::TextFormatting::Formats.register_rich! ::OpenProject::TextFormatting::Formats::Markdown::Format

@ -1,4 +1,5 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
@ -27,37 +28,31 @@
# See doc/COPYRIGHT.rdoc for more details.
#++
module OpenProject::TextFormatting::Formatters
module Markdown
class Formatter < OpenProject::TextFormatting::Formatters::Base
attr_reader :context,
:pipeline
def initialize(context)
@context = context
@pipeline = ::HTML::Pipeline.new(located_filters, context)
module OpenProject::TextFormatting::Formats
class BaseFormat
class << self
def format
raise NotImplementedError
end
def to_html(text)
result = pipeline.call(text, context)
output = result[:output].to_s
def priority
raise NotImplementedError
end
output.html_safe
def helper
@helper = "OpenProject::TextFormatting::Formats::#{format.to_s.camelcase}::Helper".constantize
end
def to_document(text)
pipeline.to_document text, context
def formatter
@formatter ||= "OpenProject::TextFormatting::Formats::#{format.to_s.camelcase}::Formatter".constantize
end
def filters
[
:markdown,
:sanitization,
HTML::Pipeline::TableOfContentsFilter,
:macro,
:pattern_matcher,
:autolink
]
def setup
# Force lookup to avoid const errors later on.
helper and formatter
rescue NameError => e
Rails.logger.error "Failed to register wiki formatting #{format}: #{e}"
Rails.logger.debug { e.backtrace }
end
end
end

@ -27,8 +27,8 @@
# See doc/COPYRIGHT.rdoc for more details.
#++
module OpenProject::TextFormatting::Formatters
class Base
module OpenProject::TextFormatting::Formats
class BaseFormatter
attr_reader :options, :project
def initialize(options)
@ -46,8 +46,6 @@ module OpenProject::TextFormatting::Formatters
[]
end
protected
def located_filters
filters.map do |f|
if [Symbol, String].include? f.class

@ -0,0 +1,43 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
module OpenProject::TextFormatting::Formats::Markdown
class Format < OpenProject::TextFormatting::Formats::BaseFormat
class << self
def format
:markdown
end
def priority
5
end
end
end
end

@ -0,0 +1,67 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
module OpenProject::TextFormatting::Formats::Markdown
class Formatter < OpenProject::TextFormatting::Formats::BaseFormatter
attr_reader :context,
:pipeline
def initialize(context)
@context = context
@pipeline = ::HTML::Pipeline.new(located_filters, context)
end
def to_html(text)
result = pipeline.call(text, context)
output = result[:output].to_s
output.html_safe
end
def to_document(text)
pipeline.to_document text, context
end
def filters
[
:markdown,
:sanitization,
HTML::Pipeline::TableOfContentsFilter,
:macro,
:pattern_matcher,
:autolink
]
end
def self.format
:markdown
end
end
end

@ -27,7 +27,7 @@
# See doc/COPYRIGHT.rdoc for more details.
#++
module OpenProject::TextFormatting::Formatters
module OpenProject::TextFormatting::Formats
module Markdown
class Helper
attr_reader :view_context
@ -36,13 +36,8 @@ module OpenProject::TextFormatting::Formatters
@view_context = view_context
end
def text_formatting_js_includes
if Setting.use_wysiwyg?
view_context.javascript_include_tag 'vendor/ckeditor/ckeditor.js'
else
view_context.javascript_include_tag 'jstoolbar/markdown.js'
end
view_context.javascript_include_tag 'vendor/ckeditor/ckeditor.js'
end
def text_formatting_has_preview?
@ -50,11 +45,7 @@ module OpenProject::TextFormatting::Formatters
end
def wikitoolbar_for(field_id)
if Setting.use_wysiwyg?
wysiwyg_for field_id
else
jstoolbar_for field_id
end
wysiwyg_for field_id
end
private
@ -69,17 +60,6 @@ module OpenProject::TextFormatting::Formatters
'',
'textarea-selector': "##{field_id}"
end
def jstoolbar_for(field_id)
view_context.content_for(:additional_js_dom_ready) do
%(
var wikiToolbar = new jsToolBar(document.getElementById('#{field_id}'));
wikiToolbar.draw();
).html_safe
end
''.html_safe
end
end
end
end

@ -54,7 +54,7 @@
require 'open3'
module OpenProject::TextFormatting::Formatters
module OpenProject::TextFormatting::Formats
module Markdown
class TextileConverter
include ActionView::Helpers::TagHelper

@ -0,0 +1,43 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
module OpenProject::TextFormatting::Formats::Plain
class Format < OpenProject::TextFormatting::Formats::BaseFormat
class << self
def format
:plain
end
def priority
100
end
end
end
end

@ -1,4 +1,5 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 the OpenProject Foundation (OPF)
@ -27,9 +28,9 @@
# See docs/COPYRIGHT.rdoc for more details.
#++
module OpenProject::TextFormatting::Formatters
module OpenProject::TextFormatting::Formats
module Plain
class Formatter < OpenProject::TextFormatting::Formatters::Base
class Formatter < OpenProject::TextFormatting::Formats::BaseFormatter
attr_reader :context,
:pipeline
@ -49,6 +50,10 @@ module OpenProject::TextFormatting::Formatters
def filters
%i(plain pattern_matcher)
end
def self.format
:plain
end
end
end
end

@ -27,7 +27,7 @@
# See docs/COPYRIGHT.rdoc for more details.
#++
module OpenProject::TextFormatting::Formatters
module OpenProject::TextFormatting::Formats
module Plain
class Helper
def wikitoolbar_for(_field_id)
@ -38,8 +38,7 @@ module OpenProject::TextFormatting::Formatters
false
end
def text_formatting_js_includes
end
def text_formatting_js_includes; end
end
end
end

@ -1,88 +0,0 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See docs/COPYRIGHT.rdoc for more details.
#++
module OpenProject::TextFormatting
module Formatters
class << self
def registered
unless defined? @formatters
register_default!
end
@formatters
end
def registered?(key)
registered.key? key.to_sym
end
def register(namespace:)
# Force lookup to avoid const errors later on.
key = namespace.to_sym
modulename = namespace.to_s.classify
raise ArgumentError, "format name '#{name}' is already taken" if registered?(key)
begin
formatter = "OpenProject::TextFormatting::Formatters::#{modulename}::Formatter".constantize
helper = "OpenProject::TextFormatting::Formatters::#{modulename}::Helper".constantize
registered[key] = { formatter: formatter, helper: helper }
rescue NameError => e
Rails.logger.error "Failed to register wiki formatting #{namespace}: #{e}"
Rails.logger.debug { e.backtrace }
end
end
def formatter_for(name)
entry = registered.fetch(name.to_sym) { registered[:plain] }
entry[:formatter]
end
def helper_for(name)
entry = registered.fetch(name.to_sym) { registered[:plain] }
entry[:helper]
end
def format_names
registered.keys.map
end
private
def register_default!
@formatters = {}
register namespace: :plain
register namespace: :markdown
register namespace: :textile
end
end
end
end

@ -1,515 +0,0 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
module OpenProject::TextFormatting::Formatters
module Textile
class Formatter < OpenProject::TextFormatting::Formatters::Base
include Redmine::WikiFormatting::Macros::Definitions
include ActionView::Helpers::SanitizeHelper
include Redmine::I18n
# used for the work package quick links
include WorkPackagesHelper
include ApplicationHelper
# Used for escaping helper 'h()'
include ERB::Util
# Rails helper
include ActionView::Context
include ActionView::Helpers::TagHelper
include ActionView::Helpers::UrlHelper
include ActionView::Helpers::TextHelper
# For route path helpers
include OpenProject::ObjectLinking
include OpenProject::StaticRouting::UrlHelpers
# Truncation
include OpenProject::TextFormatting::Truncation
def controller; end
def to_html(text)
edit = !!options[:edit]
# don't return html in edit mode when textile or text formatting is enabled
return text if edit
object = options[:object]
project = options[:project]
only_path = options.delete(:only_path) != false
# offer 'plain' as readable version for 'no formatting' to callers
format = options.delete(:format) { :textile }
text = RedclothWrapper.new(text).to_html
# TODO: transform modifications into WikiFormatting Helper, or at least ask the helper if he wants his stuff to be modified
@parsed_headings = []
text = parse_non_pre_blocks(text) { |text|
[:execute_macros, :parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_headings, :parse_relative_urls].each do |method_name|
send method_name, text, project, object, options[:attribute], only_path, options
end
}
if @parsed_headings.any?
replace_toc(text, @parsed_headings, options)
end
escape_non_macros(text)
text.html_safe
end
##
# Escape double curly braces after macro expansion.
# This will avoid arbitrary angular expressions to be evaluated in
# formatted text marked html_safe.
def escape_non_macros(text)
text.gsub!(/\{\{(?! \$root\.DOUBLE_LEFT_CURLY_BRACE)/, '{{ $root.DOUBLE_LEFT_CURLY_BRACE }}')
end
def parse_non_pre_blocks(text)
s = StringScanner.new(text)
tags = []
parsed = ''
while !s.eos?
s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
text = s[1]
full_tag = s[2]
closing = s[3]
tag = s[4]
if tags.empty?
yield text
end
parsed << text
if tag
if closing
if tags.last == tag.downcase
tags.pop
end
else
tags << tag.downcase
end
parsed << full_tag
end
end
# Close any non closing tags
while tag = tags.pop
parsed << "</#{tag}>"
end
parsed
end
MACROS_RE = /
(!)? # escaping
(
\{\{ # opening tag
([\w]+) # macro name
(\(([^\}]*)\))? # optional arguments
\}\} # closing tag
)
/x unless const_defined?(:MACROS_RE)
# Macros substitution
def execute_macros(text, project, obj, _attr, _only_path, options)
return if !!options[:edit]
text.gsub!(MACROS_RE) do
esc = $1
all = $2
macro = $3
args = ($5 || '').split(',').each(&:strip!)
if esc.nil?
begin
exec_macro(macro, obj, args, view: self, project: project)
rescue => e
"<span class=\"flash error macro-unavailable permanent\">\
#{::I18n.t(:macro_execution_error, macro_name: macro)} (#{e})\
</span>".squish
rescue NotImplementedError
"<span class=\"flash error macro-unavailable permanent\">\
#{::I18n.t(:macro_unavailable, macro_name: macro)}\
</span>".squish
end || all
else
all
end
end
end
RELATIVE_LINK_RE = %r{
<a
(?:
(\shref=
(?: # the href and link
(?:'(\/[^>]+?)')|
(?:"(\/[^>]+?)")
)
)|
[^>]
)*
>
[^<]*?<\/a> # content and closing link tag.
}x unless const_defined?(:RELATIVE_LINK_RE)
def parse_relative_urls(text, _project, _obj, _attr, only_path, _options)
return if only_path
text.gsub!(RELATIVE_LINK_RE) do |m|
href = $1
relative_url = $2 || $3
next m unless href.present?
request = options[:request]
if request.present?
# we have a request!
protocol = request.protocol
host_with_port = request.host_with_port
elsif @controller
# use the same methods as url_for in the Mailer
url_opts = @controller.class.default_url_options
next m unless url_opts && url_opts[:protocol] && url_opts[:host]
protocol = "#{url_opts[:protocol]}://"
host_with_port = url_opts[:host]
else
next m
end
m.sub href, " href=\"#{protocol}#{host_with_port}#{relative_url}\""
end
end
def parse_inline_attachments(text, _project, obj, _attr, only_path, options)
# when using an image link, try to use an attachment, if possible
if options[:attachments] || (obj && obj.respond_to?(:attachments))
attachments = nil
text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
filename = $1.downcase
ext = $2
alt = $3
alttext = $4
attachments ||= (options[:attachments] || obj.attachments).sort_by(&:created_at).reverse
# search for the picture in attachments
if found = attachments.detect { |att| att.filename.downcase == filename }
image_url = url_for only_path: only_path, controller: '/attachments', action: 'download', id: found
desc = found.description.to_s.gsub('"', '')
if !desc.blank? && alttext.blank?
alt = " title=\"#{desc}\" alt=\"#{desc}\""
end
"src=\"#{image_url}\"#{alt}"
else
m
end
end
end
end
# Wiki links
#
# Examples:
# [[mypage]]
# [[mypage|mytext]]
# wiki links can refer other project wikis, using project name or identifier:
# [[project:]] -> wiki starting page
# [[project:|mytext]]
# [[project:mypage]]
# [[project:mypage|mytext]]
def parse_wiki_links(text, project, _obj, _attr, only_path, options)
text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |_m|
link_project = project
esc = $1
all = $2
page = $3
title = $5
if esc.nil?
if page =~ /\A([^\:]+)\:(.*)\z/
link_project = Project.find_by(identifier: $1) || Project.find_by(name: $1)
page = $2
title ||= $1 if page.blank?
end
if link_project && link_project.wiki
# extract anchor
anchor = nil
if page =~ /\A(.+?)\#(.+)\z/
page = $1
anchor = $2
end
# Unescape the escaped entities from textile
page = CGI.unescapeHTML(page)
# check if page exists
wiki_page = link_project.wiki.find_page(page)
default_wiki_title = wiki_page.nil? ? page : wiki_page.title
wiki_title = title || default_wiki_title
url = case options[:wiki_links]
when :local; "#{title}.html"
when :anchor; "##{title}" # used for single-file wiki export
else
wiki_page_id = wiki_page.nil? ? page.to_url : wiki_page.slug
url_for(only_path: only_path,
controller: '/wiki',
action: 'show',
project_id: link_project,
id: wiki_page_id,
title: wiki_page.nil? ? wiki_title.strip : nil,
anchor: anchor)
end
link_to(h(wiki_title), url, class: ('wiki-page' + (wiki_page ? '' : ' new')))
else
# project or wiki doesn't exist
all
end
else
all
end
end
end
# Redmine links
#
# Examples:
# Issues:
# #52 -> Link to issue #52
# Changesets:
# r52 -> Link to revision 52
# commit:a85130f -> Link to scmid starting with a85130f
# Documents:
# document#17 -> Link to document with id 17
# document:Greetings -> Link to the document with title "Greetings"
# document:"Some document" -> Link to the document with title "Some document"
# Versions:
# version#3 -> Link to version with id 3
# version:1.0.0 -> Link to version named "1.0.0"
# version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
# Attachments:
# attachment:file.zip -> Link to the attachment of the current object named file.zip
# Source files:
# source:some/file -> Link to the file located at /some/file in the project's repository
# source:some/file@52 -> Link to the file's revision 52
# source:some/file#L120 -> Link to line 120 of the file
# source:some/file@52#L120 -> Link to line 120 of the file's revision 52
# export:some/file -> Force the download of the file
# Forum messages:
# message#1218 -> Link to message with id 1218
#
# Links can refer other objects from other projects, using project identifier:
# identifier:r52
# identifier:document:"Some document"
# identifier:version:1.0.0
# identifier:source:some/file
def parse_redmine_links(text, project, obj, attr, only_path, options)
text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(attachment|version|commit|source|export|message|project|user|group)?((#+|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|\]|<|$)}) do |_m|
leading = $1
esc = $2
project_prefix = $3
project_identifier = $4
prefix = $5
sep = $7 || $9
identifier = $8 || $10
link = nil
if project_identifier
project = Project.visible.find_by(identifier: project_identifier)
end
if esc.nil?
if prefix.nil? && sep == 'r'
# project.changesets.visible raises an SQL error because of a double join on repositories
if project && project.repository && (changeset = Changeset.visible.find_by(repository_id: project.repository.id, revision: identifier))
link = link_to(h("#{project_prefix}r#{identifier}"), { only_path: only_path, controller: '/repositories', action: 'revision', project_id: project, rev: changeset.revision },
class: 'changeset',
title: truncate_single_line(changeset.comments, length: 100))
end
elsif sep == '#'
oid = identifier.to_i
case prefix
when nil
if work_package = WorkPackage.visible
.includes(:status)
.references(:statuses)
.find_by(id: oid)
link = link_to("##{oid}",
work_package_path_or_url(id: oid, only_path: only_path),
class: work_package_css_classes(work_package),
title: "#{truncate(work_package.subject, length: 100)} (#{work_package.status.try(:name)})")
end
when 'version'
if version = Version.visible.find_by(id: oid)
link = link_to h(version.name), { only_path: only_path, controller: '/versions', action: 'show', id: version },
class: 'version'
end
when 'message'
if message = Message.visible.includes(:parent).find_by(id: oid)
link = link_to_message(message, { only_path: only_path }, class: 'message')
end
when 'project'
if p = Project.visible.find_by(id: oid)
link = link_to_project(p, { only_path: only_path }, class: 'project')
end
when 'user'
if user = User.in_visible_project.find_by(id: oid)
link = link_to_user(user, class: 'user-mention')
end
when 'group'
if group = Group.find_by(id: oid)
link = content_tag(:span, group.name, class: 'user-mention')
end
end
elsif sep == '##'
oid = identifier.to_i
if work_package = WorkPackage.visible
.includes(:status)
.references(:statuses)
.find_by(id: oid)
link = work_package_quick_info(work_package, only_path: only_path)
end
elsif sep == '###'
oid = identifier.to_i
work_package = WorkPackage.visible
.includes(:status)
.references(:statuses)
.find_by(id: oid)
if work_package && obj && !options[:no_nesting] && !(attr == :description && obj.id == work_package.id)
link = work_package_quick_info_with_description(work_package, only_path: only_path)
end
elsif sep == ':'
# removes the double quotes if any
name = identifier.gsub(%r{\A"(.*)"\z}, '\\1')
case prefix
when 'version'
if project && version = project.versions.visible.find_by(name: name)
link = link_to h(version.name), { only_path: only_path, controller: '/versions', action: 'show', id: version },
class: 'version'
end
when 'commit'
if project && project.repository && (changeset = Changeset.visible.where(['repository_id = ? AND scmid LIKE ?', project.repository.id, "#{name}%"]).first)
link = link_to h("#{project_prefix}#{name}"), { only_path: only_path, controller: '/repositories', action: 'revision', project_id: project, rev: changeset.identifier },
class: 'changeset',
title: truncate_single_line(changeset.comments, length: 100)
end
when 'source', 'export'
if project && project.repository && User.current.allowed_to?(:browse_repository, project)
name =~ %r{\A[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?\z}
path = $1
rev = $3
anchor = $5
link = link_to h("#{project_prefix}#{prefix}:#{name}"), { controller: '/repositories', action: 'entry', project_id: project,
repo_path: path.to_s,
rev: rev,
anchor: anchor,
format: (prefix == 'export' ? 'raw' : nil) },
class: (prefix == 'export' ? 'source download' : 'source')
end
when 'attachment'
attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
if attachments && attachment = attachments.detect { |a| a.filename == name }
link = link_to h(attachment.filename), { only_path: only_path, controller: '/attachments', action: 'download', id: attachment },
class: 'attachment'
end
when 'project'
p = Project
.visible
.where(['projects.identifier = :s OR LOWER(projects.name) = :s',
{ s: name.downcase }])
.first
if p
link = link_to_project(p, { only_path: only_path }, class: 'project')
end
when 'user'
if user = User.in_visible_project.find_by(login: name)
link = link_to_user(user, class: 'user-mention')
end
end
end
end
leading + (link || "#{project_prefix}#{prefix}#{sep}#{identifier}")
end
end
HEADING_RE = /<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>/i unless const_defined?(:HEADING_RE)
# Headings and TOC
# Adds ids and links to headings unless options[:headings] is set to false
def parse_headings(text, _project, _obj, _attr, _only_path, options)
return if options[:headings] == false
text.gsub!(HEADING_RE) do
level = $1.to_i
attrs = $2
content = $3
item = strip_tags(content).strip
tocitem = strip_tags(content.gsub(/<br \/>/, ' '))
anchor = item.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
@parsed_headings << [level, anchor, tocitem]
url = full_url(anchor, options[:request])
"<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"#{url}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
end
end
TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
# Renders the TOC with given headings
def replace_toc(text, headings, options)
text.gsub!(TOC_RE) do
if headings.empty?
''
else
div_class = 'toc'
div_class << ' right' if $1 == '>'
div_class << ' left' if $1 == '<'
out = "<fieldset class='form--fieldset -collapsible'>"
out << "<legend class='form--fieldset-legend' title='#{l(:description_toc_toggle)}'>"
out << "<a href='#'>#{l(:label_table_of_contents)}</a></legend><div>"
out << "<ul class=\"#{div_class}\"><li>"
root = headings.map(&:first).min
current = root
started = false
headings.each do |level, anchor, item|
if level > current
out << '<ul><li>' * (level - current)
elsif level < current
out << "</li></ul>\n" * (current - level) + '</li><li>'
elsif started
out << '</li><li>'
end
url = full_url(anchor, options[:request])
out << "<a href=\"#{url}\">#{item}</a>"
current = level
started = true
end
out << '</li></ul>' * (current - root)
out << '</li></ul>'
out << '</div></fieldset>'
end
end
end
#
# displays the current url plus an optional anchor
#
def full_url(anchor_name = '', request)
return "##{anchor_name}" if request.nil?
url_for pagination_params_whitelist(request).merge(anchor: anchor_name, only_path: true)
rescue ActionController::UrlGenerationError
# In a context outside params, we don't know what the relative anchor url is
"##{anchor_name}"
end
end
end
end

@ -1,72 +0,0 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
module OpenProject::TextFormatting::Formatters
module Textile
class Helper
attr_reader :view_context
def initialize(view_context)
@view_context = view_context
end
def text_formatting_js_includes
view_context.javascript_include_tag 'jstoolbar/textile.js'
end
def text_formatting_has_preview?
true
end
def wikitoolbar_for(field_id)
help_button = view_context.content_tag(
:button,
'',
type: 'button',
class: 'jstb_help formatting-help-link-button',
:'aria-label' => ::I18n.t('js.inplace.link_formatting_help'),
title: ::I18n.t('js.inplace.link_formatting_help')
)
view_context.content_for(:additional_js_dom_ready) do
%(
var wikiToolbar = new jsToolBar(document.getElementById('#{field_id}'));
wikiToolbar.setHelpLink(jQuery('#{view_context.escape_javascript help_button}')[0]);
wikiToolbar.draw();
).html_safe
end
''.html_safe
end
end
end
end

@ -1,143 +0,0 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
require 'redcloth3'
module OpenProject::TextFormatting::Formatters
module Textile
class RedclothWrapper < RedCloth3
include ERB::Util
include ActionView::Helpers::TagHelper
# auto_link rule after textile rules so that it doesn't break !image_url! tags
RULES = [:textile, :block_markdown_rule, :inline_auto_link, :inline_auto_mailto]
def initialize(*args)
super
self.hard_breaks = true
self.no_span_caps = true
self.filter_styles = true
end
def to_html(*_rules)
@toc = []
super(*RULES).to_s
end
private
# Patch for RedCloth. Fixed in RedCloth r128 but _why hasn't released it yet.
# <a href="http://code.whytheluckystiff.net/redcloth/changeset/128">http://code.whytheluckystiff.net/redcloth/changeset/128</a>
def hard_break(text)
text.gsub!(/(.)\n(?!\n|\Z| *([#*=]+(\s|$)|[{|]))/, '\\1<br />') if hard_breaks
end
# Patch to add code highlighting support to RedCloth
def smooth_offtags(text)
unless @pre_list.empty?
## replace <pre> content
text.gsub!(/<redpre#(\d+)>/) do
content = @pre_list[$1.to_i]
if content.match(/<code\s+class="(\w+)">\s?(.+)/m)
content = "<code class=\"#{$1} CodeRay\">" +
Redmine::SyntaxHighlighting.highlight_by_language($2, $1)
end
content
end
end
end
def auto_link_regexp
@auto_link_regexp ||= begin
%r{
( # leading text
<\w+.*?>| # leading HTML tag, or
[^=<>!:'"/]| # leading punctuation, or
\{\{\w+\(| # inside a macro?
^ # beginning of line
)
(
(?:https?://)| # protocol spec, or
(?:s?ftps?://)|
(?:www\.) # www.*
)
(
(\S+?) # url
(\/)? # slash
)
((?:&gt;)?|[^\w\=\/;\(\)]*?) # post
(?=<|\s|$)
}x
end
end
# Turns all urls into clickable links (code from Rails).
def inline_auto_link(text)
text.gsub!(auto_link_regexp) do
all = $&
leading = $1
proto = $2
url = $3
post = $6
if url.nil? || leading =~ /<a\s/i || leading =~ /![<>=]?/ || leading =~ /\{\{\w+\(/
# don't replace URLs that are already linked
# and URLs prefixed with ! !> !< != (textile images)
all
else
# Idea below : an URL with unbalanced parethesis and
# ending by ')' is put into external parenthesis
if url[-1] == ?) and ((url.count('(') - url.count(')')) < 0)
url = url[0..-2] # discard closing parenth from url
post = ')' + post # add closing parenth to post
end
tag = content_tag('a',
proto + url,
href: "#{proto == 'www.' ? 'http://www.' : proto}#{url}",
class: 'external icon-context icon-copy')
%(#{leading}#{tag}#{post})
end
end
end
# Turns all email addresses into clickable links (code from Rails).
def inline_auto_mailto(text)
text.gsub!(/((?<!user:")\b[\w\.!#\$%\-+.]+@[A-Za-z0-9\-]+(\.[A-Za-z0-9\-]+)+)/) do
mail = $1
if text.match(/<a\b[^>]*>(.*)(#{Regexp.escape(mail)})(.*)<\/a>/)
mail
else
content_tag('a', mail, href: "mailto:#{mail}", class: 'email')
end
end
end
end
end
end

@ -31,7 +31,6 @@
module OpenProject::TextFormatting::Matchers
module LinkHandlers
class ColonSeparator < Base
def self.allowed_prefixes
%w(commit source export version project user attachment)
end
@ -77,15 +76,13 @@ module OpenProject::TextFormatting::Matchers
end
def oid
if matcher.identifier
matcher.identifier.gsub(%r{\A"(.*)"\z}, '\\1')
end
matcher.identifier&.gsub(%r{\A"(.*)"\z}, '\\1')
end
private
def render_version
if project && version = project.versions.visible.find_by(name: oid)
if project && (version = project.versions.visible.find_by(name: oid))
link_to h(version.name),
{ only_path: context[:only_path], controller: '/versions', action: 'show', id: version },
class: 'version'
@ -93,7 +90,7 @@ module OpenProject::TextFormatting::Matchers
end
def render_commit
if project && project.repository && (changeset = Changeset.visible.where(['repository_id = ? AND scmid LIKE ?', project.repository.id, "#{oid}%"]).first)
if project&.repository && (changeset = Changeset.visible.where(['repository_id = ? AND scmid LIKE ?', project.repository.id, "#{oid}%"]).first)
link_to h("#{project_prefix}#{name}"),
{ only_path: context[:only_path], controller: '/repositories', action: 'revision', project_id: project, rev: changeset.identifier },
class: 'changeset',
@ -102,7 +99,7 @@ module OpenProject::TextFormatting::Matchers
end
def render_source
if project && project.repository && User.current.allowed_to?(:browse_repository, project)
if project&.repository && User.current.allowed_to?(:browse_repository, project)
matcher.identifier =~ %r{\A[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?\z}
path = $1
rev = $3
@ -131,16 +128,16 @@ module OpenProject::TextFormatting::Matchers
def render_project
p = Project
.visible
.where(['projects.identifier = :s OR LOWER(projects.name) = :s', { s: oid.downcase }])
.first
.visible
.where(['projects.identifier = :s OR LOWER(projects.name) = :s', { s: oid.downcase }])
.first
if p
link_to_project(p, { only_path: context[:only_path] }, class: 'project')
end
end
def render_user
if user = User.in_visible_project.find_by(login: oid)
if (user = User.in_visible_project.find_by(login: oid))
link_to_user(user, class: 'user-mention')
end
end

@ -31,7 +31,6 @@
module OpenProject::TextFormatting::Matchers
module LinkHandlers
class Revisions < Base
##
# Match work package links.
# Condition: Separator is #|##|###
@ -45,7 +44,6 @@ module OpenProject::TextFormatting::Matchers
#
# #1234, ##1234, ###1234
def call
# don't handle link unless repository exists
return nil unless project && project.repository
changeset = Changeset.visible.find_by(repository_id: project.repository.id, revision: matcher.identifier)

@ -31,7 +31,6 @@
module OpenProject::TextFormatting::Matchers
module LinkHandlers
class WorkPackages < Base
##
# Match work package links.
# Condition: Separator is #|##|###
@ -66,7 +65,6 @@ module OpenProject::TextFormatting::Matchers
end
def render_work_package_link(work_package)
if matcher.sep == '##'
return work_package_quick_info(work_package, only_path: context[:only_path])
elsif matcher.sep == '###' && !context[:no_nesting]

@ -30,7 +30,6 @@
module OpenProject::TextFormatting
module Matchers
# OpenProject links matching
#
# Examples:
@ -99,7 +98,7 @@ module OpenProject::TextFormatting
)
|\.\z # Allow matching when string ends with .
|, # or with ,
|\s
|[[:space:]]
|\]
|<
|$
@ -111,7 +110,7 @@ module OpenProject::TextFormatting
# Allowed prefix matchers
def self.allowed_prefixes
link_handlers
.map { |handler| handler.allowed_prefixes }
.map(&:allowed_prefixes)
.flatten
.uniq
end

@ -30,7 +30,6 @@
module OpenProject::TextFormatting
module Matchers
# OpenProject wiki link syntax
# Examples:
# [[mypage]]
@ -134,20 +133,20 @@ module OpenProject::TextFormatting
wiki_title = title || default_wiki_title
url = case context[:wiki_links]
when :local;
"#{title}.html"
when :anchor;
"##{title}" # used for single-file wiki export
else
wiki_page_id = wiki_page.nil? ? page.to_url : wiki_page.slug
url_for only_path: context[:only_path],
controller: '/wiki',
action: 'show',
project_id: project.identifier,
title: wiki_page.nil? ? wiki_title.strip : nil,
id: wiki_page_id,
anchor: anchor
end
when :local
"#{title}.html"
when :anchor
"##{title}" # used for single-file wiki export
else
wiki_page_id = wiki_page.nil? ? page.to_url : wiki_page.slug
url_for only_path: context[:only_path],
controller: '/wiki',
action: 'show',
project_id: project.identifier,
title: wiki_page.nil? ? wiki_title.strip : nil,
id: wiki_page_id,
anchor: anchor
end
link_to h(wiki_title),
url,

@ -34,13 +34,16 @@ module OpenProject::TextFormatting
def format_text(text, options = {})
return '' if text.blank?
# offer 'plain' as readable version for 'no formatting' to callers
format = options.fetch(:format, Setting.text_formatting)
formatter = if options.delete(:plain)
OpenProject::TextFormatting::Formats.plain_formatter
else
OpenProject::TextFormatting::Formats.rich_formatter
end
formatter = OpenProject::TextFormatting::Formatters.formatter_for(format).new(options)
formatter.to_html(text)
formatter
.new(options)
.to_html(text)
end
end
end
end

@ -1,81 +0,0 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See docs/COPYRIGHT.rdoc for more details.
#++
module OpenProject
module WikiFormatting
module Macros
module Default
Redmine::WikiFormatting::Macros.register do
# Builtin macros
desc 'Sample macro.'
macro :hello_world do |obj, args|
"Hello world! Object: #{obj.class.name}, " + (args.empty? ? 'Called with no argument.' : "Arguments: #{args.join(', ')}")
end
end
Redmine::WikiFormatting::Macros.register do
desc 'Displays a list of all available macros, including description if available.'
macro :macro_list do |_obj, _args|
out = ''
available_macros = Redmine::WikiFormatting::Macros.available_macros
available_macros.keys.map(&:to_s).sort.each do |macro|
out << content_tag('dt', content_tag('code', macro))
out << content_tag('dd', format_text(available_macros[macro.to_sym]))
end
content_tag('dl', out.html_safe)
end
end
Redmine::WikiFormatting::Macros.register do
desc "Displays a list of child pages. With no argument, it displays the child pages of the current wiki page. Examples:\n\n" +
" !{{child_pages}} -- can be used from a wiki page only\n" +
" !{{child_pages(Foo)}} -- lists all children of page Foo\n" +
' !{{child_pages(Foo, parent=1)}} -- same as above with a link to page Foo'
macro :child_pages do |obj, args|
args, options = extract_macro_options(args, :parent)
page = nil
if args.size > 0
page = Wiki.find_page(args.first.to_s, project: @project)
elsif obj.is_a?(WikiContent)
page = obj.page
else
raise 'With no argument, this macro can be called from wiki pages only.'
end
raise 'Page not found' if page.nil? || !User.current.allowed_to?(:view_wiki_pages, page.wiki.project)
pages = ([page] + page.descendants).group_by(&:parent_id)
render_page_hierarchy(pages, options[:parent] ? page.parent_id : page.id)
end
end
end
end
end
end

@ -1,66 +0,0 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See docs/COPYRIGHT.rdoc for more details.
#++
module OpenProject
module WikiFormatting
module Macros
module WorkPackageButton
Redmine::WikiFormatting::Macros.register do
desc 'Inserts a link or button to the create form of a work package'
macro :create_work_package_link do |_obj, args, options|
project = @project || options[:project]
if project.nil?
raise I18n.t('macros.create_work_package_link.errors.no_project_context')
end
type_name = args.shift
class_name = args.shift == 'button' ? 'button' : nil
if type_name.present?
type = project.types.find_by(name: type_name)
if type.nil?
raise I18n.t(
'macros.create_work_package_link.errors.invalid_type',
type: type_name,
project: project.name
)
end
link_to I18n.t('macros.create_work_package_link.link_name_type', type_name: type_name),
new_project_work_packages_path(project_id: project.identifier, type: type.id),
class: class_name
else
link_to I18n.t('macros.create_work_package_link.link_name'),
new_project_work_packages_path(project_id: project.identifier),
class: class_name
end
end
end
end
end
end
end

File diff suppressed because it is too large Load Diff

@ -1,100 +0,0 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See docs/COPYRIGHT.rdoc for more details.
#++
module Redmine
module WikiFormatting
module Macros
module Definitions
def exec_macro(name, obj, args, options = {})
method_name = "macro_#{name}"
if respond_to?(method_name)
if method(method_name).arity == 2
send(method_name, obj, args)
else
send(method_name, obj, args, options)
end
end
end
def extract_macro_options(args, *keys)
options = {}
while args.last.to_s.strip =~ %r{^(.+)\=(.+)$} && keys.include?($1.downcase.to_sym)
options[$1.downcase.to_sym] = $2
args.pop
end
[args, options]
end
end
@@available_macros = {}
class << self
# Called with a block to define additional macros.
# Macro blocks accept 2 arguments:
# * obj: the object that is rendered
# * args: macro arguments
#
# Plugins can use this method to define new macros:
#
# Redmine::WikiFormatting::Macros.register do
# desc "This is my macro"
# macro :my_macro do |obj, args|
# "My macro output"
# end
# end
def register(&block)
class_eval(&block) if block_given?
end
def available_macros
@@available_macros
end
private
# Defines a new macro with the given name and block.
def macro(name, &block)
name = name.to_sym if name.is_a?(String)
@@available_macros[name] = @@desc || ''
@@desc = nil
raise 'Can not create a macro without a block!' unless block_given?
Definitions.send :define_method, "macro_#{name}".downcase, &block
end
# Sets description for the next macro to be defined
def desc(txt)
@@desc = txt
end
include OpenProject::WikiFormatting::Macros::Default
include OpenProject::WikiFormatting::Macros::WorkPackageButton
end
end
end
end

@ -35,8 +35,8 @@ class TabularFormBuilder < ActionView::Helpers::FormBuilder
include ActionView::Helpers::AssetTagHelper
include ERB::Util
(field_helpers - %i(radio_button hidden_field fields_for label) + %i(date_select)).each do |selector|
define_method selector do |field, options = {}, *args|
def self.tag_with_label_method(selector, &block)
->(field, options, *args) do
options[:class] = Array(options[:class]) + [field_css_class(selector)]
merge_required_attributes(options[:required], options)
@ -45,14 +45,29 @@ class TabularFormBuilder < ActionView::Helpers::FormBuilder
label = label_for_field(field, label_options)
input = super(field, input_options, *args)
if options[:with_text_formatting]
input.concat text_formatting_wrapper options[:id]
end
input = instance_exec(input, options, &block) if block_given?
(label + container_wrap_field(input, selector, options))
end
end
def self.with_text_formatting
->(input, options) {
if options[:with_text_formatting]
# use either the provided id or fetch the one created by rails
input.concat text_formatting_wrapper options[:id] || input.match(/<[^>]* id="(\w+)"[^>]*>/)[1]
end
input
}
end
(field_helpers - %i(radio_button hidden_field fields_for label) + %i(date_select)).each do |selector|
define_method selector, &tag_with_label_method(selector)
end
define_method(:text_area, &tag_with_label_method(:text_area, &with_text_formatting))
def label(method, text = nil, options = {}, &block)
options[:class] = Array(options[:class]) + %w(form--label)
options[:title] = options[:title] || title_from_context(method)
@ -132,7 +147,7 @@ class TabularFormBuilder < ActionView::Helpers::FormBuilder
prefix.html_safe,
class: 'form--field-affix',
id: options[:prefix_id],
:'aria-hidden' => true)
'aria-hidden': true)
end
if suffix
@ -140,7 +155,7 @@ class TabularFormBuilder < ActionView::Helpers::FormBuilder
suffix.html_safe,
class: 'form--field-affix',
id: options[:suffix_id],
:'aria-hidden' => true)
'aria-hidden': true)
end
field_container_wrap_field(ret, options)
@ -148,7 +163,7 @@ class TabularFormBuilder < ActionView::Helpers::FormBuilder
def merge_required_attributes(required, options = nil)
if required
options.merge!(required: true, :'aria-required' => 'true')
options.merge!(required: true, 'aria-required': 'true')
end
end
@ -177,8 +192,7 @@ class TabularFormBuilder < ActionView::Helpers::FormBuilder
def text_formatting_wrapper(target_id)
return ''.html_safe unless target_id.present?
format = Setting.text_formatting
helper = ::OpenProject::TextFormatting::Formatters.helper_for(format).new(@template)
helper = ::OpenProject::TextFormatting::Formats.rich_helper.new(@template)
helper.wikitoolbar_for target_id
end
@ -232,7 +246,7 @@ class TabularFormBuilder < ActionView::Helpers::FormBuilder
content << content_tag('span',
'*',
class: 'form--label-required',
:'aria-hidden' => true)
'aria-hidden': true)
end
end

@ -34,7 +34,6 @@ namespace :code do
files = Dir['**/**{.rb,.html.erb,.rhtml,.rjs,.plain.erb,.rxml,.yml,.rake,.eml}']
files.reject! { |f|
f.include?('lib/plugins') ||
f.include?('lib/redcloth') ||
f.include?('lib/diff')
}

@ -142,10 +142,7 @@ namespace :copyright do
desc 'Update the copyright on .rb source files'
task :update_rb, :arg1 do |_task, args|
excluded = (['acts_as_tree',
'rfpdf',
'verification'].map { |dir| "lib/plugins/#{dir}" }) +
(['redcloth'].map { |dir| "lib/#{dir}" })
excluded = (%w(acts_as_tree rfpdf verification).map { |dir| "lib/plugins/#{dir}" })
rewrite_copyright('rb', excluded, :rb, args[:arg1])
end

@ -1,46 +0,0 @@
# Textile to Markdown converter
# Based on redmine_convert_textile_to_markown
# https://github.com/Ecodev/redmine_convert_textile_to_markown
#
# Original license:
# Copyright (c) 2016
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
namespace :markdown do
task :convert_from_textile => :environment do
warning = <<~EOS
**WARNING**
THIS IS NOT REVERSIBLE.
Ensure you have backed up your installation before running this task.
This rake task will modify EVERY formattable textile field in your database.
It uses pandoc to convert each textile field to GFM-Markdown.
EOS
printf "#{warning}\nPress 'y' to continue: "
prompt = STDIN.gets.chomp
exit(1) unless prompt == 'y'
converter = OpenProject::TextFormatting::Formatters::Markdown::TextileConverter.new
converter.run!
end
end

@ -29,7 +29,7 @@
FactoryBot.define do
factory :wiki_page do
wiki
sequence(:title) do |n| "Wiki Page No. #{n}" end
sequence(:title) { |n| "Wiki Page No. #{n}" }
factory :wiki_page_with_content do
callback(:after_build) do |wiki_page|

@ -36,6 +36,10 @@ describe 'Attribute help texts' do
let(:relation_columns_allowed) { true }
def set_help_text(text)
find('.ck-content').set(text)
end
describe 'Work package help texts' do
before do
with_enterprise_token(relation_columns_allowed ? :attribute_help_texts : nil)
@ -55,7 +59,7 @@ describe 'Attribute help texts' do
# Set attributes
# -> create
select 'Status', from: 'attribute_help_text_attribute_name'
fill_in 'Help text', with: 'My attribute help text'
set_help_text('My attribute help text')
click_button 'Save'
# Should now show on index for editing
@ -67,18 +71,18 @@ describe 'Attribute help texts' do
# -> edit
page.find('.attribute-help-text--entry td a', text: 'Status').click
expect(page).to have_selector('#attribute_help_text_attribute_name[disabled]')
fill_in 'Help text', with: ''
set_help_text(' ')
click_button 'Save'
# Handle errors
expect(page).to have_selector('#errorExplanation', text: "Help text can't be blank.")
fill_in 'Help text', with: 'New *help* text'
set_help_text('New**help**text')
click_button 'Save'
# On index again
expect(page).to have_selector('.attribute-help-text--entry td', text: 'Status')
instance.reload
expect(instance.help_text).to eq 'New *help* text'
expect(instance.help_text).to eq 'New**help**text'
# Open help text modal
modal.open!

@ -85,7 +85,7 @@ describe 'Authentication Stages', type: :feature, js: true do
end
end
context 'when enabled, localized consent exists',
with_settings: { consent_info: { de: 'h1. Einwilligung', en: 'h1. Consent header!' } } do
with_settings: { consent_info: { de: '# Einwilligung', en: '# Consent header!' } } do
let(:consent_required) { true }
let(:language) { 'de' }
@ -97,7 +97,7 @@ describe 'Authentication Stages', type: :feature, js: true do
end
end
context 'when enabled, but consent exists', with_settings: { consent_info: { en: 'h1. Consent header!' } } do
context 'when enabled, but consent exists', with_settings: { consent_info: { en: '# Consent header!' } } do
let(:consent_required) { true }
it 'should show consent' do
expect(Setting.consent_time).to be_blank

@ -28,42 +28,89 @@
require 'spec_helper'
describe 'messages', type: :feature do
let(:user) { FactoryBot.create :admin, firstname: 'Hugo', lastname: 'Hungrig' }
describe 'messages', type: :feature, js: true do
let(:board) { FactoryBot.create(:board) }
let(:user) do
FactoryBot.create :user,
member_in_project: board.project,
member_through_role: role
end
let(:other_user) do
FactoryBot.create :user,
member_in_project: board.project,
member_through_role: role
end
let(:role) { FactoryBot.create(:role, permissions: [:add_messages]) }
let(:index_page) { Pages::Messages::Index.new(board.project) }
before do
allow(User).to receive(:current).and_return user
login_as user
end
describe 'quoting' do
let(:topic) { FactoryBot.create :message }
let!(:reply) do
FactoryBot.create :message,
board: topic.board,
parent: topic,
author: user,
subject: 'Go Ahead!',
content: 'You can quote me on this!'
end
before do
visit topic_path(topic)
end
describe 'clicking on quote', js: true do
it 'opens the filled-in reply form' do
msg = find 'div.reply', text: /Go Ahead!/
within(msg) do click_on 'Quote' end
reply = find '#reply'
expect(reply).to be_visible
subject = find '#reply_subject'
expect(subject.value).to eq 'RE: Go Ahead!'
content = find '#reply_content'
expect(content.value).to eq "Hugo Hungrig wrote:\n> You can quote me on this!\n\n"
end
end
scenario 'adding, checking replies, replying' do
index_page.visit!
create_page = index_page.click_create_message
create_page.set_subject 'The message is'
create_page.click_save
create_page.expect_notification(type: :error, message: 'Content can\'t be blank')
create_page.add_text 'There is no message here'
show_page = create_page.click_save
show_page.expect_current_path
show_page.expect_subject('The message is')
show_page.expect_content('There is no message here')
index_page.visit!
index_page.expect_listed(subject: 'The message is',
replies: 0)
# Replying as other user
login_as other_user
show_page.visit!
show_page.expect_no_replies
reply = show_page.reply 'But, but there should be one'
show_page.expect_current_path(reply)
show_page.expect_num_replies(1)
show_page.expect_reply(subject: 'RE: The message is',
content: 'But, but there should be one')
index_page.visit!
index_page.expect_listed(subject: 'The message is',
replies: 1,
last_message: 'RE: The message is')
# Quoting as first user again
login_as user
show_page.visit!
quote = show_page.quote(quoted_message: reply,
subject: 'And now to something completely different',
content: "No, there really isn't\n\n")
show_page.expect_current_path(quote)
show_page.expect_num_replies(2)
show_page.expect_reply(reply: quote,
subject: 'And now to something completely different',
content: 'No, there really isn\'t')
expect(page).to have_selector('blockquote', text: 'But, but there should be one')
index_page.visit!
index_page.expect_listed(subject: 'The message is',
replies: 2,
last_message: 'And now to something completely different')
end
end

@ -0,0 +1,82 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See docs/COPYRIGHT.rdoc for more details.
#++
require 'spec_helper'
describe 'sticky messages', type: :feature do
let(:board) { FactoryBot.create(:board) }
let!(:message1) do
FactoryBot.create :message, board: board, created_on: Time.now - 1.minute do |message|
Message.where(id: message.id).update_all(updated_on: Time.now - 1.minute)
end
end
let!(:message2) do
FactoryBot.create :message, board: board, created_on: Time.now - 2.minute do |message|
Message.where(id: message.id).update_all(updated_on: Time.now - 2.minute)
end
end
let!(:message3) do
FactoryBot.create :message, board: board, created_on: Time.now - 3.minute do |message|
Message.where(id: message.id).update_all(updated_on: Time.now - 3.minute)
end
end
let(:user) do
FactoryBot.create :user,
member_in_project: board.project,
member_through_role: role
end
let(:role) { FactoryBot.create(:role, permissions: [:edit_messages]) }
before do
login_as user
visit project_boards_path(board.project)
end
def expect_order_of_messages(*order)
order.each_with_index do |message, index|
expect(page).to have_selector("table tbody tr:nth-of-type(#{index + 1})", text: message.subject)
end
end
scenario 'sticky messages are on top' do
expect_order_of_messages(message1, message2, message3)
click_link(message2.subject)
click_link('Edit')
check('Sticky')
click_button('Save')
visit project_boards_path(board.project)
expect_order_of_messages(message2, message1, message3)
end
end

@ -0,0 +1,103 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See docs/COPYRIGHT.rdoc for more details.
#++
require 'spec_helper'
describe 'wiki pages', type: :feature, js: true do
let(:project) do
FactoryBot.create(:project, enabled_module_names: [:news])
end
let(:user) do
FactoryBot.create :user,
member_in_project: project,
member_through_role: role
end
let(:role) do
FactoryBot.create(:role,
permissions: %i[view_wiki_pages
edit_wiki_pages
view_wiki_edits
select_project_modules
edit_project])
end
let(:content_first_version) do
'The new content, first version'
end
let(:content_second_version) do
'The new content, second version'
end
before do
login_as user
end
scenario 'adding, editing and history' do
visit settings_project_path(project, tab: 'modules')
expect(page).to have_no_selector('.menu-sidebar .main-item-wrapper', text: 'Wiki')
within '#content' do
check 'Wiki'
click_button 'Save'
end
expect(page).to have_selector('#menu-sidebar .main-item-wrapper', text: 'Wiki')
# creating by accessing the page
visit project_wiki_path(project, 'new page')
find('.ck-content').set(content_first_version)
click_button 'Save'
expect(page).to have_selector('.title-container', text: 'New page')
expect(page).to have_selector('.wiki-content', text: content_first_version)
within '.toolbar-items' do
click_on "Edit"
end
find('.ck-content').set(content_second_version)
click_button 'Save'
expect(page).to have_selector('.wiki-content', text: content_second_version)
within '.toolbar-items' do
click_on 'More'
click_on 'History'
end
click_on 'View differences'
within '.text-diff' do
expect(page).to have_selector('ins.diffmod', text: 'second')
expect(page).to have_selector('del.diffmod', text: 'first')
end
end
end

@ -0,0 +1,78 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2017 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See docs/COPYRIGHT.rdoc for more details.
#++
require 'spec_helper'
describe 'wiki child pages', type: :feature, js: true do
let(:project) do
FactoryBot.create(:project)
end
let(:user) do
FactoryBot.create :user,
member_in_project: project,
member_through_role: role
end
let(:role) do
FactoryBot.create(:role,
permissions: %i[view_wiki_pages edit_wiki_pages])
end
let(:parent_page) do
FactoryBot.create(:wiki_page_with_content,
wiki: project.wiki)
end
let(:child_page_name) { 'The child page !@#{$%^&*()_},./<>?;\':' }
before do
login_as user
end
scenario 'adding a childpage' do
visit project_wiki_path(project, parent_page.title)
click_on 'Wiki page'
fill_in 'content_page_title', with: child_page_name
find('.ck-content').set('The child page\'s content')
click_button 'Save'
# hierarchy displayed in the breadcrumb
expect(page).to have_selector('#breadcrumb .breadcrumb',
text: "#{parent_page.title}\n#{child_page_name}")
# hierarchy displayed in the sidebar
expect(page).to have_selector('.pages-hierarchy',
text: "#{parent_page.title}\n#{child_page_name}")
# on toc page
visit index_project_wiki_index_path(project)
expect(page).to have_content(child_page_name)
end
end

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save