mirror of
https://github.com/codeninjasllc/discourse.git
synced 2024-11-23 15:48:43 -05:00
When a post returns enqueued
don't insert it in the stream and notify
- Includes removal of a lot of modal boilerplate
This commit is contained in:
parent
76f7786d0d
commit
7f501a0c41
34 changed files with 172 additions and 125 deletions
|
@ -33,6 +33,8 @@
|
|||
"triggerEvent",
|
||||
"count",
|
||||
"exists",
|
||||
"visible",
|
||||
"invisible",
|
||||
"asyncTestDiscourse",
|
||||
"fixture",
|
||||
"find",
|
||||
|
|
|
@ -24,8 +24,8 @@ export default Ember.Route.extend({
|
|||
},
|
||||
|
||||
editGroupings() {
|
||||
const groupings = this.controllerFor('admin-badges').get('badgeGroupings');
|
||||
showModal('modals/admin-edit-badge-groupings', groupings);
|
||||
const model = this.controllerFor('admin-badges').get('badgeGroupings');
|
||||
showModal('modals/admin-edit-badge-groupings', { model });
|
||||
},
|
||||
|
||||
preview(badge, explain) {
|
||||
|
@ -38,9 +38,9 @@ export default Ember.Route.extend({
|
|||
trigger: badge.get('trigger'),
|
||||
explain
|
||||
}
|
||||
}).then(function(json) {
|
||||
}).then(function(model) {
|
||||
badge.set('preview_loading', false);
|
||||
showModal('modals/admin-badge-preview', json);
|
||||
showModal('modals/admin-badge-preview', { model });
|
||||
}).catch(function(error) {
|
||||
badge.set('preview_loading', false);
|
||||
Em.Logger.error(error);
|
||||
|
|
|
@ -12,13 +12,13 @@ export default Discourse.Route.extend({
|
|||
},
|
||||
|
||||
actions: {
|
||||
showAgreeFlagModal(flaggedPost) {
|
||||
showModal('modals/admin-agree-flag', flaggedPost);
|
||||
showAgreeFlagModal(model) {
|
||||
showModal('modals/admin-agree-flag', { model });
|
||||
this.controllerFor('modal').set('modalClass', 'agree-flag-modal');
|
||||
},
|
||||
|
||||
showDeleteFlagModal(flaggedPost) {
|
||||
showModal('modals/admin-delete-flag', flaggedPost);
|
||||
showDeleteFlagModal(model) {
|
||||
showModal('modals/admin-delete-flag', { model });
|
||||
this.controllerFor('modal').set('modalClass', 'delete-flag-modal');
|
||||
}
|
||||
|
||||
|
|
|
@ -12,14 +12,14 @@ export default Discourse.Route.extend({
|
|||
},
|
||||
|
||||
actions: {
|
||||
showDetailsModal(logRecord) {
|
||||
showModal('modals/admin-staff-action-log-details', logRecord);
|
||||
showDetailsModal(model) {
|
||||
showModal('modals/admin-staff-action-log-details', { model });
|
||||
this.controllerFor('modal').set('modalClass', 'log-details-modal');
|
||||
},
|
||||
|
||||
showCustomDetailsModal(logRecord) {
|
||||
const modalName = "modals/" + (logRecord.action_name + '_details').replace("_", "-");
|
||||
showModal(modalName, logRecord);
|
||||
showCustomDetailsModal(model) {
|
||||
const modalName = "modals/" + (model.action_name + '_details').replace("_", "-");
|
||||
showModal(modalName, { model });
|
||||
this.controllerFor('modal').set('modalClass', 'tabbed-modal log-details-modal');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,8 +24,8 @@ export default Discourse.Route.extend({
|
|||
},
|
||||
|
||||
actions: {
|
||||
showSuspendModal(user) {
|
||||
showModal('modals/admin-suspend-user', user);
|
||||
showSuspendModal(model) {
|
||||
showModal('modals/admin-suspend-user', { model });
|
||||
this.controllerFor('modal').set('modalClass', 'suspend-user-modal');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import showModal from 'discourse/lib/show-modal';
|
|||
export default Ember.Component.extend({
|
||||
actions: {
|
||||
showBulkActions() {
|
||||
const controller = showModal('topicBulkActions', this.get('selected'));
|
||||
const controller = showModal('topic-bulk-actions', { model: this.get('selected'), title: 'topics.bulk.actions' });
|
||||
controller.set('refreshTarget', this.get('refreshTarget'));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -218,14 +218,20 @@ export default DiscourseController.extend({
|
|||
const promise = composer.save({
|
||||
imageSizes: this.get('view').imageSizes(),
|
||||
editReason: this.get("editReason")
|
||||
}).then(function(opts) {
|
||||
}).then(function(result) {
|
||||
|
||||
if (result.responseJson.action === "enqueued") {
|
||||
self.send('postWasEnqueued');
|
||||
self.destroyDraft();
|
||||
self.close();
|
||||
return result;
|
||||
}
|
||||
|
||||
// If we replied as a new topic successfully, remove the draft.
|
||||
if (self.get('replyAsNewTopicDraft')) {
|
||||
self.destroyDraft();
|
||||
}
|
||||
|
||||
opts = opts || {};
|
||||
self.close();
|
||||
|
||||
const currentUser = Discourse.User.current();
|
||||
|
@ -238,8 +244,9 @@ export default DiscourseController.extend({
|
|||
// TODO disableJumpReply is super crude, it needs to provide some sort
|
||||
// of notification to the end user
|
||||
if (!composer.get('replyingToTopic') || !disableJumpReply) {
|
||||
if (opts.post && !staged) {
|
||||
Discourse.URL.routeTo(opts.post.get('url'));
|
||||
const post = result.target;
|
||||
if (post && !staged) {
|
||||
Discourse.URL.routeTo(post.get('url'));
|
||||
}
|
||||
}
|
||||
}).catch(function(error) {
|
||||
|
|
|
@ -1,13 +1,33 @@
|
|||
export default (name, model) => {
|
||||
export default (name, opts) => {
|
||||
opts = opts || {};
|
||||
|
||||
const container = Discourse.__container__;
|
||||
|
||||
// We use the container here because modals are like singletons
|
||||
// in Discourse. Only one can be shown with a particular state.
|
||||
const route = Discourse.__container__.lookup('route:application');
|
||||
const route = container.lookup('route:application');
|
||||
const modalController = route.controllerFor('modal');
|
||||
|
||||
route.controllerFor('modal').set('modalClass', null);
|
||||
modalController.set('modalClass', null);
|
||||
|
||||
const viewClass = container.lookupFactory('view:' + name);
|
||||
const controller = container.lookup('controller:' + name);
|
||||
if (viewClass) {
|
||||
route.render(name, { into: 'modal', outlet: 'modalBody' });
|
||||
} else {
|
||||
const templateName = Ember.String.dasherize(name);
|
||||
|
||||
const renderArgs = { into: 'modal', outlet: 'modalBody', view: 'modal-body'};
|
||||
if (controller) { renderArgs.controller = name; }
|
||||
|
||||
route.render('modal/' + templateName, renderArgs);
|
||||
if (opts.title) {
|
||||
modalController.set('title', I18n.t(opts.title));
|
||||
}
|
||||
}
|
||||
|
||||
const controller = route.controllerFor(name);
|
||||
if (controller) {
|
||||
const model = opts.model;
|
||||
if (model) { controller.set('model', model); }
|
||||
if (controller.onShow) { controller.onShow(); }
|
||||
controller.set('flashMessage', null);
|
||||
|
|
|
@ -440,8 +440,9 @@ const Composer = RestModel.extend({
|
|||
this.set('composeState', CLOSED);
|
||||
|
||||
return promise.then(function() {
|
||||
return post.save(props).then(function() {
|
||||
return post.save(props).then(function(result) {
|
||||
self.clearState();
|
||||
return result;
|
||||
}).catch(throwAjaxError(function() {
|
||||
post.set('cooked', oldCooked);
|
||||
self.set('composeState', OPEN);
|
||||
|
@ -526,9 +527,13 @@ const Composer = RestModel.extend({
|
|||
composer.set("stagedPost", state === "staged" && createdPost);
|
||||
|
||||
return createdPost.save().then(function(result) {
|
||||
|
||||
let saving = true;
|
||||
|
||||
if (result.responseJson.action === "enqueued") {
|
||||
if (postStream) { postStream.undoPost(createdPost); }
|
||||
return result;
|
||||
}
|
||||
|
||||
if (topic) {
|
||||
// It's no longer a new post
|
||||
topic.set('draft_sequence', result.target.draft_sequence);
|
||||
|
@ -554,7 +559,7 @@ const Composer = RestModel.extend({
|
|||
composer.set('composeState', SAVING);
|
||||
}
|
||||
|
||||
return { post: createdPost };
|
||||
return result;
|
||||
}).catch(throwAjaxError(function() {
|
||||
if (postStream) {
|
||||
postStream.undoPost(createdPost);
|
||||
|
|
|
@ -30,9 +30,13 @@ const RestModel = Ember.Object.extend(Presence, {
|
|||
const self = this;
|
||||
return adapter.createRecord(store, type, props).then(function(res) {
|
||||
if (!res) { throw "Received no data back from createRecord"; }
|
||||
self.setProperties(self.__munge(res.payload));
|
||||
|
||||
// We can get a response back without properties, for example
|
||||
// when a post is queued.
|
||||
if (res.payload) {
|
||||
self.setProperties(self.__munge(res.payload));
|
||||
self.set('__state', 'created');
|
||||
}
|
||||
|
||||
res.target = self;
|
||||
return res;
|
||||
|
|
|
@ -37,6 +37,10 @@ const ApplicationRoute = Discourse.Route.extend({
|
|||
this.controllerFor('topic-entrance').send('show', data);
|
||||
},
|
||||
|
||||
postWasEnqueued() {
|
||||
showModal('post-enqueued', {title: 'queue.approval.title' });
|
||||
},
|
||||
|
||||
composePrivateMessage(user, post) {
|
||||
const self = this;
|
||||
this.transitionTo('userActivity', user).then(function () {
|
||||
|
@ -76,12 +80,12 @@ const ApplicationRoute = Discourse.Route.extend({
|
|||
showCreateAccount: unlessReadOnly('handleShowCreateAccount'),
|
||||
|
||||
showForgotPassword() {
|
||||
showModal('forgotPassword');
|
||||
showModal('forgotPassword', { title: 'forgot_password.title' });
|
||||
},
|
||||
|
||||
showNotActivated(props) {
|
||||
showModal('notActivated');
|
||||
this.controllerFor('notActivated').setProperties(props);
|
||||
const controller = showModal('not-activated', {title: 'log_in' });
|
||||
controller.setProperties(props);
|
||||
},
|
||||
|
||||
showUploadSelector(composerView) {
|
||||
|
@ -90,13 +94,13 @@ const ApplicationRoute = Discourse.Route.extend({
|
|||
},
|
||||
|
||||
showKeyboardShortcutsHelp() {
|
||||
showModal('keyboardShortcutsHelp');
|
||||
showModal('keyboard-shortcuts-help', { title: 'keyboard_shortcuts_help.title'});
|
||||
},
|
||||
|
||||
showSearchHelp() {
|
||||
// TODO: @EvitTrout how do we get a loading indicator here?
|
||||
Discourse.ajax("/static/search_help.html", { dataType: 'html' }).then(function(html){
|
||||
showModal('searchHelp', html);
|
||||
Discourse.ajax("/static/search_help.html", { dataType: 'html' }).then(function(model){
|
||||
showModal('searchHelp', { model });
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -120,9 +124,9 @@ const ApplicationRoute = Discourse.Route.extend({
|
|||
|
||||
editCategory(category) {
|
||||
const self = this;
|
||||
Discourse.Category.reloadById(category.get('id')).then(function (c) {
|
||||
self.site.updateCategory(c);
|
||||
showModal('editCategory', c);
|
||||
Discourse.Category.reloadById(category.get('id')).then(function (model) {
|
||||
self.site.updateCategory(model);
|
||||
showModal('editCategory', { model });
|
||||
self.controllerFor('editCategory').set('selectedTab', 'general');
|
||||
});
|
||||
},
|
||||
|
@ -140,7 +144,7 @@ const ApplicationRoute = Discourse.Route.extend({
|
|||
const controllerName = w.replace('modal/', ''),
|
||||
factory = this.container.lookupFactory('controller:' + controllerName);
|
||||
|
||||
this.render(w, {into: 'topicBulkActions', outlet: 'bulkOutlet', controller: factory ? controllerName : 'topic-bulk-actions'});
|
||||
this.render(w, {into: 'modal/topic-bulk-actions', outlet: 'bulkOutlet', controller: factory ? controllerName : 'topic-bulk-actions'});
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
import showModal from 'discourse/lib/show-modal';
|
||||
|
||||
const DiscourseRoute = Ember.Route.extend({
|
||||
|
||||
// Set to true to refresh a model without a transition if a query param
|
||||
|
@ -210,11 +208,6 @@ DiscourseRoute.reopenClass({
|
|||
|
||||
this.route('unknown', {path: '*path'});
|
||||
});
|
||||
},
|
||||
|
||||
showModal: function(route, name, model) {
|
||||
Ember.warn('DEPRECATED `Discourse.Route.showModal` - use `showModal` instead');
|
||||
showModal(name, model);
|
||||
}
|
||||
|
||||
});
|
||||
|
|
|
@ -43,52 +43,52 @@ const TopicRoute = Discourse.Route.extend(ShowFooter, {
|
|||
this.controllerFor("topic-admin-menu").send("show");
|
||||
},
|
||||
|
||||
showFlags(post) {
|
||||
showModal('flag', post);
|
||||
showFlags(model) {
|
||||
showModal('flag', { model });
|
||||
this.controllerFor('flag').setProperties({ selected: null });
|
||||
},
|
||||
|
||||
showFlagTopic(topic) {
|
||||
showModal('flag', topic);
|
||||
showFlagTopic(model) {
|
||||
showModal('flag', { model });
|
||||
this.controllerFor('flag').setProperties({ selected: null, flagTopic: true });
|
||||
},
|
||||
|
||||
showAutoClose() {
|
||||
showModal('editTopicAutoClose', this.modelFor('topic'));
|
||||
showModal('edit-topic-auto-close', { model: this.modelFor('topic'), title: 'topic.auto_close_title' });
|
||||
this.controllerFor('modal').set('modalClass', 'edit-auto-close-modal');
|
||||
},
|
||||
|
||||
showFeatureTopic() {
|
||||
showModal('featureTopic', this.modelFor('topic'));
|
||||
showModal('featureTopic', { model: this.modelFor('topic'), title: 'topic.feature_topic.title' });
|
||||
this.controllerFor('modal').set('modalClass', 'feature-topic-modal');
|
||||
},
|
||||
|
||||
showInvite() {
|
||||
showModal('invite', this.modelFor('topic'));
|
||||
showModal('invite', { model: this.modelFor('topic') });
|
||||
this.controllerFor('invite').reset();
|
||||
},
|
||||
|
||||
showHistory(post) {
|
||||
showModal('history', post);
|
||||
this.controllerFor('history').refresh(post.get("id"), "latest");
|
||||
showHistory(model) {
|
||||
showModal('history', { model });
|
||||
this.controllerFor('history').refresh(model.get("id"), "latest");
|
||||
this.controllerFor('modal').set('modalClass', 'history-modal');
|
||||
},
|
||||
|
||||
showRawEmail(post) {
|
||||
showModal('raw-email', post);
|
||||
this.controllerFor('raw_email').loadRawEmail(post.get("id"));
|
||||
showRawEmail(model) {
|
||||
showModal('raw-email', { model });
|
||||
this.controllerFor('raw_email').loadRawEmail(model.get("id"));
|
||||
},
|
||||
|
||||
mergeTopic() {
|
||||
showModal('mergeTopic', this.modelFor('topic'));
|
||||
showModal('merge-topic', { model: this.modelFor('topic'), title: 'topic.merge_topic.title' });
|
||||
},
|
||||
|
||||
splitTopic() {
|
||||
showModal('split-topic', this.modelFor('topic'));
|
||||
showModal('split-topic', { model: this.modelFor('topic') });
|
||||
},
|
||||
|
||||
changeOwner() {
|
||||
showModal('changeOwner', this.modelFor('topic'));
|
||||
showModal('change-owner', { model: this.modelFor('topic'), title: 'topic.change_owner.title' });
|
||||
},
|
||||
|
||||
// Use replaceState to update the URL once it changes
|
||||
|
|
|
@ -21,7 +21,7 @@ export default Discourse.Route.extend(ShowFooter, {
|
|||
|
||||
actions: {
|
||||
showInvite() {
|
||||
showModal('invite', Discourse.User.current());
|
||||
showModal('invite', { model: this.currentUser });
|
||||
this.controllerFor('invite').reset();
|
||||
},
|
||||
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
<div class="modal-body">
|
||||
<p>{{i18n "queue.approval.description"}}</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
{{d-button action="closeModal" class="btn-primary" label="queue.approval.ok"}}
|
||||
</div>
|
|
@ -1,6 +0,0 @@
|
|||
import ModalBodyView from "discourse/views/modal-body";
|
||||
|
||||
export default ModalBodyView.extend({
|
||||
templateName: 'modal/archetype_options',
|
||||
title: I18n.t('topic.options')
|
||||
});
|
|
@ -1,6 +0,0 @@
|
|||
import ModalBodyView from "discourse/views/modal-body";
|
||||
|
||||
export default ModalBodyView.extend({
|
||||
templateName: 'modal/change_owner',
|
||||
title: I18n.t('topic.change_owner.title')
|
||||
});
|
|
@ -1,6 +0,0 @@
|
|||
import ModalBodyView from "discourse/views/modal-body";
|
||||
|
||||
export default ModalBodyView.extend({
|
||||
templateName: 'modal/auto_close',
|
||||
title: I18n.t('topic.auto_close_title')
|
||||
});
|
|
@ -1,6 +0,0 @@
|
|||
import ModalBodyView from "discourse/views/modal-body";
|
||||
|
||||
export default ModalBodyView.extend({
|
||||
templateName: 'modal/feature-topic',
|
||||
title: I18n.t('topic.feature_topic.title')
|
||||
});
|
|
@ -1,6 +0,0 @@
|
|||
import ModalBodyView from "discourse/views/modal-body";
|
||||
|
||||
export default ModalBodyView.extend({
|
||||
templateName: 'modal/forgot_password',
|
||||
title: I18n.t('forgot_password.title'),
|
||||
});
|
|
@ -1,6 +0,0 @@
|
|||
import ModalBodyView from "discourse/views/modal-body";
|
||||
|
||||
export default ModalBodyView.extend({
|
||||
templateName: 'modal/keyboard_shortcuts_help',
|
||||
title: I18n.t('keyboard_shortcuts_help.title')
|
||||
});
|
|
@ -1,6 +0,0 @@
|
|||
import ModalBodyView from "discourse/views/modal-body";
|
||||
|
||||
export default ModalBodyView.extend({
|
||||
templateName: 'modal/merge_topic',
|
||||
title: I18n.t('topic.merge_topic.title')
|
||||
});
|
|
@ -1,6 +0,0 @@
|
|||
import ModalBodyView from "discourse/views/modal-body";
|
||||
|
||||
export default ModalBodyView.extend({
|
||||
templateName: 'modal/not_activated',
|
||||
title: I18n.t('log_in')
|
||||
});
|
|
@ -1,6 +0,0 @@
|
|||
import ModalBodyView from "discourse/views/modal-body";
|
||||
|
||||
export default ModalBodyView.extend({
|
||||
templateName: 'modal/topic-bulk-actions',
|
||||
title: I18n.t('topics.bulk.actions')
|
||||
});
|
|
@ -225,6 +225,12 @@ en:
|
|||
search: "Search for a Topic by name, url or id:"
|
||||
placeholder: "type the topic title here"
|
||||
|
||||
queue:
|
||||
approval:
|
||||
title: "Post Needs Approval"
|
||||
description: "We've received your new post but it needs to be approved by a moderator before it will appear. Please be patient."
|
||||
ok: "OK"
|
||||
|
||||
user_action:
|
||||
user_posted_topic: "<a href='{{userUrl}}'>{{user}}</a> posted <a href='{{topicUrl}}'>the topic</a>"
|
||||
you_posted_topic: "<a href='{{userUrl}}'>You</a> posted <a href='{{topicUrl}}'>the topic</a>"
|
||||
|
|
|
@ -81,9 +81,31 @@ test("Create a Topic", () => {
|
|||
});
|
||||
});
|
||||
|
||||
test("Create an enqueued Topic", () => {
|
||||
visit("/");
|
||||
click('#create-topic');
|
||||
fillIn('#reply-title', "Internationalization Localization");
|
||||
fillIn('#wmd-input', "enqueue this content please");
|
||||
click('#reply-control button.create');
|
||||
andThen(() => {
|
||||
ok(visible('#discourse-modal'), 'it pops up a modal');
|
||||
equal(currentURL(), "/", "it doesn't change routes");
|
||||
});
|
||||
|
||||
click('.modal-footer button');
|
||||
andThen(() => {
|
||||
ok(invisible('#discourse-modal'), 'the modal can be dismissed');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
test("Create a Reply", () => {
|
||||
visit("/t/internationalization-localization/280");
|
||||
|
||||
andThen(() => {
|
||||
ok(!exists('article[data-post-id=12345]'), 'the post is not in the DOM');
|
||||
});
|
||||
|
||||
click('#topic-footer-buttons .btn.create');
|
||||
andThen(() => {
|
||||
ok(exists('#wmd-input'), 'the composer input is visible');
|
||||
|
@ -93,7 +115,32 @@ test("Create a Reply", () => {
|
|||
fillIn('#wmd-input', 'this is the content of my reply');
|
||||
click('#reply-control button.create');
|
||||
andThen(() => {
|
||||
exists('#post_12345', 'it inserts the post into the document');
|
||||
equal(find('.cooked:last p').text(), 'this is the content of my reply');
|
||||
});
|
||||
});
|
||||
|
||||
test("Create an enqueued Reply", () => {
|
||||
visit("/t/internationalization-localization/280");
|
||||
|
||||
click('#topic-footer-buttons .btn.create');
|
||||
andThen(() => {
|
||||
ok(exists('#wmd-input'), 'the composer input is visible');
|
||||
ok(!exists('#reply-title'), 'there is no title since this is a reply');
|
||||
});
|
||||
|
||||
fillIn('#wmd-input', 'enqueue this content please');
|
||||
click('#reply-control button.create');
|
||||
andThen(() => {
|
||||
ok(find('.cooked:last p').text() !== 'enqueue this content please', "it doesn't insert the post");
|
||||
});
|
||||
|
||||
andThen(() => {
|
||||
ok(visible('#discourse-modal'), 'it pops up a modal');
|
||||
});
|
||||
|
||||
click('.modal-footer button');
|
||||
andThen(() => {
|
||||
ok(invisible('#discourse-modal'), 'the modal can be dismissed');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -118,3 +165,4 @@ test("Edit the first post", () => {
|
|||
ok(find('.topic-post:eq(0) .cooked').text().indexOf('This is the new text for the post') !== -1, 'it updates the post');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -88,6 +88,8 @@ export default function() {
|
|||
return [200, {"Content-Type": "text/html"}, "<div class='page-not-found'>not found</div>"];
|
||||
});
|
||||
|
||||
this.delete('/draft.json', success);
|
||||
|
||||
this.get('/draft.json', function() {
|
||||
return response({});
|
||||
});
|
||||
|
@ -148,13 +150,17 @@ export default function() {
|
|||
|
||||
if (data.title === "this title triggers an error") {
|
||||
return response(422, {errors: ['That title has already been taken']});
|
||||
} else {
|
||||
}
|
||||
|
||||
if (data.raw === "enqueue this content please") {
|
||||
return response(200, { success: true, action: 'enqueued' });
|
||||
}
|
||||
|
||||
return response(200, {
|
||||
success: true,
|
||||
action: 'create_post',
|
||||
post: {id: 12345, topic_id: 280, topic_slug: 'internationalization-localization'}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.get('/widgets/:widget_id', function(request) {
|
||||
|
|
Loading…
Reference in a new issue