mirror of
https://github.com/discourse/discourse.git
synced 2024-12-02 05:03:40 +08:00
60ad836313
This is a combined work of Martin Brennan, Loïc Guitaut, and Joffrey Jaffeux. --- This commit implements a base service object when working in chat. The documentation is available at https://discourse.github.io/discourse/chat/backend/Chat/Service.html Generating documentation has been made as part of this commit with a bigger goal in mind of generally making it easier to dive into the chat project. Working with services generally involves 3 parts: - The service object itself, which is a series of steps where few of them are specialized (model, transaction, policy) ```ruby class UpdateAge include Chat::Service::Base model :user, :fetch_user policy :can_see_user contract step :update_age class Contract attribute :age, :integer end def fetch_user(user_id:, **) User.find_by(id: user_id) end def can_see_user(guardian:, **) guardian.can_see_user(user) end def update_age(age:, **) user.update!(age: age) end end ``` - The `with_service` controller helper, handling success and failure of the service within a service and making easy to return proper response to it from the controller ```ruby def update with_service(UpdateAge) do on_success { render_serialized(result.user, BasicUserSerializer, root: "user") } end end ``` - Rspec matchers and steps inspector, improving the dev experience while creating specs for a service ```ruby RSpec.describe(UpdateAge) do subject(:result) do described_class.call(guardian: guardian, user_id: user.id, age: age) end fab!(:user) { Fabricate(:user) } fab!(:current_user) { Fabricate(:admin) } let(:guardian) { Guardian.new(current_user) } let(:age) { 1 } it { expect(user.reload.age).to eq(age) } end ``` Note in case of unexpected failure in your spec, the output will give all the relevant information: ``` 1) UpdateAge when no channel_id is given is expected to fail to find a model named 'user' Failure/Error: it { is_expected.to fail_to_find_a_model(:user) } Expected model 'foo' (key: 'result.model.user') was not found in the result object. [1/4] [model] 'user' ❌ [2/4] [policy] 'can_see_user' [3/4] [contract] 'default' [4/4] [step] 'update_age' /Users/joffreyjaffeux/Code/pr-discourse/plugins/chat/app/services/update_age.rb:32:in `fetch_user': missing keyword: :user_id (ArgumentError) from /Users/joffreyjaffeux/Code/pr-discourse/plugins/chat/app/services/base.rb:202:in `instance_exec' from /Users/joffreyjaffeux/Code/pr-discourse/plugins/chat/app/services/base.rb:202:in `call' from /Users/joffreyjaffeux/Code/pr-discourse/plugins/chat/app/services/base.rb:219:in `call' from /Users/joffreyjaffeux/Code/pr-discourse/plugins/chat/app/services/base.rb:417:in `block in run!' from /Users/joffreyjaffeux/Code/pr-discourse/plugins/chat/app/services/base.rb:417:in `each' from /Users/joffreyjaffeux/Code/pr-discourse/plugins/chat/app/services/base.rb:417:in `run!' from /Users/joffreyjaffeux/Code/pr-discourse/plugins/chat/app/services/base.rb:411:in `run' from <internal:kernel>:90:in `tap' from /Users/joffreyjaffeux/Code/pr-discourse/plugins/chat/app/services/base.rb:302:in `call' from /Users/joffreyjaffeux/Code/pr-discourse/plugins/chat/spec/services/update_age_spec.rb:15:in `block (3 levels) in <main>' ```
217 lines
5.6 KiB
JavaScript
217 lines
5.6 KiB
JavaScript
(function() {
|
|
|
|
var $clicked = $(null);
|
|
var searchTimeout = null;
|
|
var searchCache = [];
|
|
var caseSensitiveMatch = false;
|
|
var ignoreKeyCodeMin = 8;
|
|
var ignoreKeyCodeMax = 46;
|
|
var commandKey = 91;
|
|
|
|
RegExp.escape = function(text) {
|
|
return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
|
|
}
|
|
|
|
function escapeShortcut() {
|
|
$(document).keydown(function(evt) {
|
|
if (evt.which == 27) {
|
|
window.parent.postMessage('navEscape', '*');
|
|
}
|
|
});
|
|
}
|
|
|
|
function navResizer() {
|
|
$(window).mousemove(function(e) {
|
|
window.parent.postMessage({
|
|
action: 'mousemove', event: {pageX: e.pageX, which: e.which}
|
|
}, '*');
|
|
}).mouseup(function(e) {
|
|
window.parent.postMessage({action: 'mouseup'}, '*');
|
|
});
|
|
window.parent.postMessage("navReady", "*");
|
|
}
|
|
|
|
function clearSearchTimeout() {
|
|
clearTimeout(searchTimeout);
|
|
searchTimeout = null;
|
|
}
|
|
|
|
function enableLinks() {
|
|
// load the target page in the parent window
|
|
$('#full_list li').on('click', function(evt) {
|
|
$('#full_list li').removeClass('clicked');
|
|
$clicked = $(this);
|
|
$clicked.addClass('clicked');
|
|
evt.stopPropagation();
|
|
|
|
if (evt.target.tagName === 'A') return true;
|
|
|
|
var elem = $clicked.find('> .item .object_link a')[0];
|
|
var e = evt.originalEvent;
|
|
var newEvent = new MouseEvent(evt.originalEvent.type);
|
|
newEvent.initMouseEvent(e.type, e.canBubble, e.cancelable, e.view, e.detail, e.screenX, e.screenY, e.clientX, e.clientY, e.ctrlKey, e.altKey, e.shiftKey, e.metaKey, e.button, e.relatedTarget);
|
|
elem.dispatchEvent(newEvent);
|
|
evt.preventDefault();
|
|
return false;
|
|
});
|
|
}
|
|
|
|
function enableToggles() {
|
|
// show/hide nested classes on toggle click
|
|
$('#full_list a.toggle').on('click', function(evt) {
|
|
evt.stopPropagation();
|
|
evt.preventDefault();
|
|
$(this).parent().parent().toggleClass('collapsed');
|
|
highlight();
|
|
});
|
|
}
|
|
|
|
function populateSearchCache() {
|
|
$('#full_list li .item').each(function() {
|
|
var $node = $(this);
|
|
var $link = $node.find('.object_link a');
|
|
if ($link.length > 0) {
|
|
searchCache.push({
|
|
node: $node,
|
|
link: $link,
|
|
name: $link.text(),
|
|
fullName: $link.attr('title').split(' ')[0]
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
function enableSearch() {
|
|
$('#search input').keyup(function(event) {
|
|
if (ignoredKeyPress(event)) return;
|
|
if (this.value === "") {
|
|
clearSearch();
|
|
} else {
|
|
performSearch(this.value);
|
|
}
|
|
});
|
|
|
|
$('#full_list').after("<div id='noresults' style='display:none'></div>");
|
|
}
|
|
|
|
function ignoredKeyPress(event) {
|
|
if (
|
|
(event.keyCode > ignoreKeyCodeMin && event.keyCode < ignoreKeyCodeMax) ||
|
|
(event.keyCode == commandKey)
|
|
) {
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function clearSearch() {
|
|
clearSearchTimeout();
|
|
$('#full_list .found').removeClass('found').each(function() {
|
|
var $link = $(this).find('.object_link a');
|
|
$link.text($link.text());
|
|
});
|
|
$('#full_list, #content').removeClass('insearch');
|
|
$clicked.parents().removeClass('collapsed');
|
|
highlight();
|
|
}
|
|
|
|
function performSearch(searchString) {
|
|
clearSearchTimeout();
|
|
$('#full_list, #content').addClass('insearch');
|
|
$('#noresults').text('').hide();
|
|
partialSearch(searchString, 0);
|
|
}
|
|
|
|
function partialSearch(searchString, offset) {
|
|
var lastRowClass = '';
|
|
var i = null;
|
|
for (i = offset; i < Math.min(offset + 50, searchCache.length); i++) {
|
|
var item = searchCache[i];
|
|
var searchName = (searchString.indexOf('::') != -1 ? item.fullName : item.name);
|
|
var matchString = buildMatchString(searchString);
|
|
var matchRegexp = new RegExp(matchString, caseSensitiveMatch ? "" : "i");
|
|
if (searchName.match(matchRegexp) == null) {
|
|
item.node.removeClass('found');
|
|
item.link.text(item.link.text());
|
|
}
|
|
else {
|
|
item.node.addClass('found');
|
|
item.node.removeClass(lastRowClass).addClass(lastRowClass == 'r1' ? 'r2' : 'r1');
|
|
lastRowClass = item.node.hasClass('r1') ? 'r1' : 'r2';
|
|
item.link.html(item.name.replace(matchRegexp, "<strong>$&</strong>"));
|
|
}
|
|
}
|
|
if(i == searchCache.length) {
|
|
searchDone();
|
|
} else {
|
|
searchTimeout = setTimeout(function() {
|
|
partialSearch(searchString, i);
|
|
}, 0);
|
|
}
|
|
}
|
|
|
|
function searchDone() {
|
|
searchTimeout = null;
|
|
highlight();
|
|
if ($('#full_list li:visible').size() === 0) {
|
|
$('#noresults').text('No results were found.').hide().fadeIn();
|
|
} else {
|
|
$('#noresults').text('').hide();
|
|
}
|
|
$('#content').removeClass('insearch');
|
|
}
|
|
|
|
function buildMatchString(searchString, event) {
|
|
caseSensitiveMatch = searchString.match(/[A-Z]/) != null;
|
|
var regexSearchString = RegExp.escape(searchString);
|
|
if (caseSensitiveMatch) {
|
|
regexSearchString += "|" +
|
|
$.map(searchString.split(''), function(e) { return RegExp.escape(e); }).
|
|
join('.+?');
|
|
}
|
|
return regexSearchString;
|
|
}
|
|
|
|
function highlight() {
|
|
$('#full_list li:visible').each(function(n) {
|
|
$(this).removeClass('even odd').addClass(n % 2 == 0 ? 'odd' : 'even');
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Expands the tree to the target element and its immediate
|
|
* children.
|
|
*/
|
|
function expandTo(path) {
|
|
var $target = $(document.getElementById('object_' + path));
|
|
$target.addClass('clicked');
|
|
$target.removeClass('collapsed');
|
|
$target.parentsUntil('#full_list', 'li').removeClass('collapsed');
|
|
if($target[0]) {
|
|
window.scrollTo(window.scrollX, $target.offset().top - 250);
|
|
highlight();
|
|
}
|
|
}
|
|
|
|
function windowEvents(event) {
|
|
var msg = event.data;
|
|
if (msg.action === "expand") {
|
|
expandTo(msg.path);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
window.addEventListener("message", windowEvents, false);
|
|
|
|
$(document).ready(function() {
|
|
escapeShortcut();
|
|
navResizer();
|
|
enableLinks();
|
|
enableToggles();
|
|
populateSearchCache();
|
|
enableSearch();
|
|
});
|
|
|
|
})();
|