diff --git a/framework/core/ember/app/components/discussions/composer-discussion.js b/framework/core/ember/app/components/discussions/composer-discussion.js new file mode 100644 index 000000000..4f4780125 --- /dev/null +++ b/framework/core/ember/app/components/discussions/composer-discussion.js @@ -0,0 +1,57 @@ +import Ember from 'ember'; + +import TaggedArray from '../../utils/tagged-array'; +import { PositionEnum } from '../../controllers/composer'; + +var precompileTemplate = Ember.Handlebars.compile; + +export default Ember.Component.extend(Ember.Evented, { + layoutName: 'components/discussions/composer-body', + + submitLabel: 'Post Discussion', + titlePlaceholder: 'Discussion Title', + placeholder: '', + title: '', + content: '', + submit: null, + loading: false, + + disabled: Ember.computed.equal('composer.position', PositionEnum.MINIMIZED), + + didInsertElement: function() { + var controls = TaggedArray.create(); + this.trigger('populateControls', controls); + this.set('controls', controls); + }, + + populateControls: function(controls) { + var title = Ember.Component.create({ + tagName: 'h3', + layout: precompileTemplate('{{ui/controls/text-input value=component.title class="form-control" placeholder=component.titlePlaceholder disabled=component.disabled}}'), + component: this + }); + controls.pushObjectWithTag(title, 'title'); + }, + + actions: { + submit: function(content) { + this.get('submit')({ + title: this.get('title'), + content: content + }); + }, + + willExit: function(abort) { + // If the user has typed something, prompt them before exiting + // this composer state. + if ((this.get('title') || this.get('content')) && !confirm('You have not posted your discussion. Do you wish to discard it?')) { + abort(); + } + }, + + reset: function() { + this.set('loading', false); + this.set('content', ''); + } + } +}); diff --git a/framework/core/ember/app/components/ui/controls/text-input.js b/framework/core/ember/app/components/ui/controls/text-input.js new file mode 100644 index 000000000..7934f3e5f --- /dev/null +++ b/framework/core/ember/app/components/ui/controls/text-input.js @@ -0,0 +1,18 @@ +import Ember from 'ember'; + +export default Ember.TextField.extend({ + didInsertElement: function() { + var component = this; + this.$().on('input', function() { + var empty = !$(this).val(); + if (empty) { + $(this).val(component.get('placeholder')); + } + $(this).css('width', 0); + $(this).width($(this)[0].scrollWidth); + if (empty) { + $(this).val(''); + } + }); + } +}); diff --git a/framework/core/ember/app/controllers/index.js b/framework/core/ember/app/controllers/index.js index 14bc3d466..9a6f12e13 100644 --- a/framework/core/ember/app/controllers/index.js +++ b/framework/core/ember/app/controllers/index.js @@ -3,6 +3,8 @@ import Ember from 'ember'; import DiscussionResult from '../models/discussion-result'; import PostResult from '../models/post-result'; import PaneableMixin from '../mixins/paneable'; +import ComposerDiscussion from '../components/discussions/composer-discussion'; +import AlertMessage from '../components/alert-message'; export default Ember.Controller.extend(Ember.Evented, PaneableMixin, { needs: ['application', 'composer', 'index/index', 'discussion'], @@ -11,6 +13,39 @@ export default Ember.Controller.extend(Ember.Evented, PaneableMixin, { paneDisabled: Ember.computed.not('index.model.length'), + saveDiscussion: function(data) { + var controller = this; + var composer = this.get('controllers.composer'); + var stream = this.get('stream'); + + composer.set('content.loading', true); + controller.get('controllers.application').send('clearAlerts'); + + var discussion = this.store.createRecord('discussion', { + title: data.title, + content: data.content + }); + + return discussion.save().then(function(discussion) { + composer.send('hide'); + controller.get('index').set('model', null).send('refresh'); + controller.transitionToRoute('discussion', discussion); + }, + function(reason) { + var errors = reason.errors; + for (var i in reason.errors) { + var message = AlertMessage.create({ + type: 'warning', + message: reason.errors[i] + }); + controller.get('controllers.application').send('alert', message); + } + }) + .finally(function() { + composer.set('content.loading', false); + }); + }, + actions: { transitionFromBackButton: function() { this.transitionToRoute('index'); @@ -21,11 +56,21 @@ export default Ember.Controller.extend(Ember.Evented, PaneableMixin, { }, newDiscussion: function() { - var composer = this.get('controllers.composer'); - composer.set('minimized', false); - composer.set('showing', true); - composer.set('title', 'Discussion Title'); // needs to be editable - composer.set('delegate', this); - } + var controller = this; + var composer = this.get('controllers.composer'); + + // If the composer is already set up for starting a discussion, then we + // don't need to change its content - we can just show it. + if (!(composer.get('content') instanceof ComposerDiscussion)) { + composer.switchContent(ComposerDiscussion.create({ + user: controller.get('session.user'), + submit: function(data) { + controller.saveDiscussion(data); + } + })); + } + + composer.send('show'); + } } }); diff --git a/framework/core/ember/app/models/discussion.js b/framework/core/ember/app/models/discussion.js index 455bd510e..8d9f6da3d 100644 --- a/framework/core/ember/app/models/discussion.js +++ b/framework/core/ember/app/models/discussion.js @@ -4,6 +4,7 @@ import DS from 'ember-data'; var Discussion = DS.Model.extend({ title: DS.attr('string'), + content: DS.attr('string'), // only used to save a new discussion slug: function() { return this.get('title').toLowerCase().replace(/[^a-z0-9]/gi, '-').replace(/-+/g, '-'); @@ -31,7 +32,8 @@ var Discussion = DS.Model.extend({ posts: DS.attr('string'), postIds: function() { - return this.get('posts').split(','); + var posts = this.get('posts') || ''; + return posts.split(','); }.property('posts'), readTime: DS.attr('date'), diff --git a/framework/core/ember/app/routes/discussion.js b/framework/core/ember/app/routes/discussion.js index 1559e49fc..a6a901290 100644 --- a/framework/core/ember/app/routes/discussion.js +++ b/framework/core/ember/app/routes/discussion.js @@ -8,11 +8,6 @@ export default Ember.Route.extend({ }, model: function(params) { - // Each time we view a discussion we want to reload its posts from - // scratch so that we have the most up-to-date data. Also, if we were - // to leave them in the store, the stream would try and render them - // which has the potential to be slow. - this.store.unloadAll('post'); return this.store.findQueryOne('discussion', params.id, { include: 'posts', near: params.start @@ -49,6 +44,12 @@ export default Ember.Route.extend({ start: controller.get('start') }); + // Each time we view a discussion we want to reload its posts from + // scratch so that we have the most up-to-date data. Also, if we were + // to leave them in the store, the stream would try and render them + // which has the potential to be slow. + this.store.unloadAll('post'); + // When we know we have the post IDs, we can set up the post stream with // them. Then we will tell the view that we have finished loading so that // it can scroll down to the appropriate post. diff --git a/framework/core/ember/app/styles/flarum/composer.less b/framework/core/ember/app/styles/flarum/composer.less index 21ac76025..2db3e0ea2 100644 --- a/framework/core/ember/app/styles/flarum/composer.less +++ b/framework/core/ember/app/styles/flarum/composer.less @@ -99,9 +99,17 @@ & h3 { margin: 5px 0 10px; - color: @fl-body-muted-color; - font-size: 16px; - font-weight: normal; + &, & input { + color: @fl-body-muted-color; + font-size: 16px; + font-weight: normal; + } + & input { + background: none; + border: 0; + padding: 0; + height: auto; + } } .minimized & { @@ -153,4 +161,4 @@ opacity: 1; pointer-events: auto; } -} \ No newline at end of file +} diff --git a/framework/core/ember/app/views/index.js b/framework/core/ember/app/views/index.js index 83b098246..423078fd0 100644 --- a/framework/core/ember/app/views/index.js +++ b/framework/core/ember/app/views/index.js @@ -70,10 +70,14 @@ export default Ember.View.extend({ }.observes('controller.controllers.discussion.model'), populateSidebarDefault: function(sidebar) { + var view = this; var newDiscussion = ActionButton.create({ label: 'Start a Discussion', icon: 'edit', - className: 'btn btn-primary new-discussion' + className: 'btn btn-primary new-discussion', + action: function() { + view.get('controller').send('newDiscussion'); + } }); sidebar.pushObjectWithTag(newDiscussion, 'newDiscussion'); diff --git a/framework/core/src/Flarum/Core/Discussions/Commands/StartDiscussionCommandHandler.php b/framework/core/src/Flarum/Core/Discussions/Commands/StartDiscussionCommandHandler.php index 4cd017688..bd549f6d9 100644 --- a/framework/core/src/Flarum/Core/Discussions/Commands/StartDiscussionCommandHandler.php +++ b/framework/core/src/Flarum/Core/Discussions/Commands/StartDiscussionCommandHandler.php @@ -52,6 +52,10 @@ class StartDiscussionCommandHandler implements CommandHandler new PostReplyCommand($discussion->id, $command->content, $command->user) ); + // The discussion may have been updated by the PostReplyCommand; we need + // to refresh its data. + $discussion = $this->discussionRepo->find($discussion->id); + $this->dispatchEventsFor($discussion); return $discussion; diff --git a/framework/core/tests/api/DiscussionsResourceCest.php b/framework/core/tests/api/DiscussionsResourceCest.php index e0ca66cf1..13624f100 100644 --- a/framework/core/tests/api/DiscussionsResourceCest.php +++ b/framework/core/tests/api/DiscussionsResourceCest.php @@ -50,6 +50,8 @@ class DiscussionsResourceCest { $I->seeResponseContainsJson(['title' => 'foo']); $I->seeResponseContainsJson(['type' => 'comment', 'contentHtml' => '

bar

']); + // @todo check for post in response + $id = $I->grabDataFromJsonResponse('discussions.id'); $I->seeRecord('discussions', ['id' => $id, 'title' => 'foo']); } @@ -86,4 +88,4 @@ class DiscussionsResourceCest { $I->dontSeeRecord('discussions', ['id' => $discussion->id]); } -} \ No newline at end of file +}