diff --git a/app/assets/javascripts/discourse-common/components/combo-box.js.es6 b/app/assets/javascripts/discourse-common/components/combo-box.js.es6 index 314cf47c45..e5828e8fc7 100644 --- a/app/assets/javascripts/discourse-common/components/combo-box.js.es6 +++ b/app/assets/javascripts/discourse-common/components/combo-box.js.es6 @@ -11,12 +11,14 @@ export default Ember.Component.extend(bufferedRender({ buildBuffer(buffer) { const nameProperty = this.get('nameProperty'); const none = this.get('none'); + let noneValue = null; // Add none option if required if (typeof none === "string") { buffer.push('"); } else if (typeof none === "object") { - buffer.push(""); + noneValue = Em.get(none, this.get('valueAttribute')); + buffer.push(``); } let selected = this.get('value'); @@ -47,7 +49,7 @@ export default Ember.Component.extend(bufferedRender({ }); } - if (!selectedFound) { + if (!selectedFound && !noneValue) { if (none) { this.set('value', null); } else { @@ -89,7 +91,8 @@ export default Ember.Component.extend(bufferedRender({ const $elem = this.$(); const caps = this.capabilities; - const minimumResultsForSearch = (caps && caps.isIOS) ? -1 : 5; + const minimumResultsForSearch = this.get('minimumResultsForSearch') || ((caps && caps.isIOS) ? -1 : 5); + if (!this.get("selectionTemplate") && this.get("selectionIcon")) { this.selectionTemplate = (item) => { let name = Em.get(item, 'text'); @@ -97,13 +100,22 @@ export default Ember.Component.extend(bufferedRender({ return `${name}`; }; } - $elem.select2({ - formatResult: this.comboTemplate, - formatSelection: this.selectionTemplate, + + const options = { minimumResultsForSearch, width: this.get('width') || 'resolve', allowClear: true - }); + }; + + if (this.comboTemplate) { + options.formatResult = this.comboTemplate.bind(this); + } + + if (this.selectionTemplate) { + options.formatSelection = this.selectionTemplate.bind(this); + } + + $elem.select2(options); const castInteger = this.get('castInteger'); $elem.on("change", e => { diff --git a/app/assets/javascripts/discourse/components/auto-update-input-selector.js.es6 b/app/assets/javascripts/discourse/components/auto-update-input-selector.js.es6 new file mode 100644 index 0000000000..353c6951cc --- /dev/null +++ b/app/assets/javascripts/discourse/components/auto-update-input-selector.js.es6 @@ -0,0 +1,172 @@ +import { default as computed, observes } from "ember-addons/ember-computed-decorators"; +import Combobox from 'discourse-common/components/combo-box'; +import { CLOSE_STATUS_TYPE } from 'discourse/controllers/edit-topic-status-update'; + +const LATER_TODAY = 'later_today'; +const TOMORROW = 'tomorrow'; +const LATER_THIS_WEEK = 'later_this_week'; +const THIS_WEEKEND = 'this_weekend'; +const NEXT_WEEK = 'next_week'; +export const PICK_DATE_AND_TIME = 'pick_date_and_time'; +export const SET_BASED_ON_LAST_POST = 'set_based_on_last_post'; + +export const FORMAT = 'YYYY-MM-DD HH:mm'; + +export default Combobox.extend({ + classNames: ['auto-update-input-selector'], + isCustom: Ember.computed.equal("value", PICK_DATE_AND_TIME), + + @computed() + content() { + const selections = []; + const now = moment(); + const canScheduleToday = (24 - now.hour()) > 6; + const day = now.day(); + + if (canScheduleToday) { + selections.push({ + id: LATER_TODAY, + name: I18n.t('topic.auto_update_input.later_today') + }); + } + + selections.push({ + id: TOMORROW, + name: I18n.t('topic.auto_update_input.tomorrow') + }); + + if (!canScheduleToday && day < 4) { + selections.push({ + id: LATER_THIS_WEEK, + name: I18n.t('topic.auto_update_input.later_this_week') + }); + } + + if (day < 5) { + selections.push({ + id: THIS_WEEKEND, + name: I18n.t('topic.auto_update_input.this_weekend') + }); + } + + + if (day !== 7) { + selections.push({ + id: NEXT_WEEK, + name: I18n.t('topic.auto_update_input.next_week') + }); + } + + selections.push({ + id: PICK_DATE_AND_TIME, + name: I18n.t('topic.auto_update_input.pick_date_and_time') + }); + + if (this.get('statusType') === CLOSE_STATUS_TYPE) { + selections.push({ + id: SET_BASED_ON_LAST_POST, + name: I18n.t('topic.auto_update_input.set_based_on_last_post') + }); + } + + return selections; + }, + + @observes('value') + _updateInput() { + if (this.get('isCustom')) return; + let input = null; + const { time } = this.get('updateAt'); + + if (time && !Ember.isEmpty(this.get('value'))) { + input = time.format(FORMAT); + } + + this.set('input', input); + }, + + @computed('value') + updateAt(value) { + return this._updateAt(value); + }, + + comboTemplate(state) { + return this._format(state); + }, + + selectionTemplate(state) { + return this._format(state); + }, + + _format(state) { + let { time, icon } = this._updateAt(state.id); + let icons; + + if (icon) { + icons = icon.split(',').map(i => { + return ``; + }).join(" "); + } + + if (time) { + if (state.id === LATER_TODAY) { + time = time.format('hh:mm a'); + } else { + time = time.format('ddd, hh:mm a'); + } + } + + let output = ""; + + if (!Ember.isEmpty(icons)) { + output += ``; + } + + output += `${state.text}`; + + if (time) { + output += `${time}`; + } + + return output; + }, + + _updateAt(selection) { + let time = moment(); + let icon; + const timeOfDay = this.get('statusType') !== CLOSE_STATUS_TYPE ? 8 : 18; + + switch(selection) { + case LATER_TODAY: + time = time.hour(18).minute(0); + icon = 'desktop'; + break; + case TOMORROW: + time = time.add(1, 'day').hour(timeOfDay).minute(0); + icon = 'sun-o'; + break; + case LATER_THIS_WEEK: + time = time.add(2, 'day').hour(timeOfDay).minute(0); + icon = 'briefcase'; + break; + case THIS_WEEKEND: + time = time.day(6).hour(timeOfDay).minute(0); + icon = 'bed'; + break; + case NEXT_WEEK: + time = time.add(1, 'week').day(1).hour(timeOfDay).minute(0); + icon = 'briefcase'; + break; + case PICK_DATE_AND_TIME: + time = null; + icon = 'calendar-plus-o'; + break; + case SET_BASED_ON_LAST_POST: + time = null; + icon = 'clock-o'; + break; + } + + return { time, icon }; + }, +}); diff --git a/app/assets/javascripts/discourse/components/auto-update-input.js.es6 b/app/assets/javascripts/discourse/components/auto-update-input.js.es6 index 1d44119778..747c061793 100644 --- a/app/assets/javascripts/discourse/components/auto-update-input.js.es6 +++ b/app/assets/javascripts/discourse/components/auto-update-input.js.es6 @@ -1,47 +1,92 @@ import { default as computed, observes } from "ember-addons/ember-computed-decorators"; +import { + FORMAT, + PICK_DATE_AND_TIME, + SET_BASED_ON_LAST_POST +} from "discourse/components/auto-update-input-selector"; export default Ember.Component.extend({ - limited: false, + selection: null, + date: null, + time: null, + isCustom: Ember.computed.equal('selection', PICK_DATE_AND_TIME), + isBasedOnLastPost: Ember.computed.equal('selection', SET_BASED_ON_LAST_POST), - didInsertElement() { + init() { this._super(); - this._updateInputValid(); - }, - @computed("limited") - inputUnitsKey(limited) { - return limited ? "topic.auto_update_input.limited.units" : "topic.auto_update_input.all.units"; - }, + const input = this.get('input'); - @computed("limited") - inputExamplesKey(limited) { - return limited ? "topic.auto_update_input.limited.examples" : "topic.auto_update_input.all.examples"; - }, - - @observes("input", "limited") - _updateInputValid() { - this.set( - "inputValid", this._isInputValid(this.get("input"), this.get("limited")) - ); - }, - - _isInputValid(input, limited) { - const t = (input || "").toString().trim(); - - if (t.length === 0) { - return true; - // "empty" is always valid - } else if (limited) { - // only # of hours in limited mode - return t.match(/^(\d+\.)?\d+$/); - } else { - if (t.match(/^\d{4}-\d{1,2}-\d{1,2}(?: \d{1,2}:\d{2}(\s?[AP]M)?){0,1}$/i)) { - // timestamp must be in the future - return moment(t).isAfter(); + if (input) { + if (this.get('basedOnLastPost')) { + this.set('selection', SET_BASED_ON_LAST_POST); } else { - // either # of hours or absolute time - return (t.match(/^(\d+\.)?\d+$/) || t.match(/^\d{1,2}:\d{2}(\s?[AP]M)?$/i)) !== null; + this.set('selection', PICK_DATE_AND_TIME); + const datetime = moment(input); + this.set('date', datetime.toDate()); + this.set('time', datetime.format("HH:mm")); + this._updateInput(); } } - } + }, + + @observes("date", "time") + _updateInput() { + const date = moment(this.get('date')).format("YYYY-MM-DD"); + const time = (this.get('time') && ` ${this.get('time')}`) || ''; + this.set('input', moment(`${date}${time}`).format(FORMAT)); + }, + + @observes("isBasedOnLastPost") + _updateBasedOnLastPost() { + this.set('basedOnLastPost', this.get('isBasedOnLastPost')); + }, + + @computed("input", "isBasedOnLastPost") + duration(input, isBasedOnLastPost) { + const now = moment(); + + if (isBasedOnLastPost) { + return parseFloat(input); + } else { + return moment(input) - now; + } + }, + + @computed("input", "isBasedOnLastPost") + executeAt(input, isBasedOnLastPost) { + if (isBasedOnLastPost) { + return moment().add(input, 'hours').format(FORMAT); + } else { + return input; + } + }, + + @computed("statusType", "input", "isCustom", "date", "time", "willCloseImmediately") + showTopicStatusInfo(statusType, input, isCustom, date, time, willCloseImmediately) { + if (!statusType || willCloseImmediately) return false; + + if (isCustom) { + return date || time; + } else { + return input; + } + }, + + @computed('isBasedOnLastPost', 'input', 'lastPostedAt') + willCloseImmediately(isBasedOnLastPost, input, lastPostedAt) { + if (isBasedOnLastPost && input) { + let closeDate = moment(lastPostedAt); + closeDate = closeDate.add(input, 'hours'); + return closeDate < moment(); + } + }, + + @computed('isBasedOnLastPost', 'lastPostedAt') + willCloseI18n(isBasedOnLastPost, lastPostedAt) { + if (isBasedOnLastPost) { + const diff = Math.round((new Date() - new Date(lastPostedAt)) / (1000*60*60)); + return I18n.t('topic.auto_close_immediate', { count: diff }); + } + }, }); diff --git a/app/assets/javascripts/discourse/components/category-chooser.js.es6 b/app/assets/javascripts/discourse/components/category-chooser.js.es6 index b79e8ef188..d0b8b45009 100644 --- a/app/assets/javascripts/discourse/components/category-chooser.js.es6 +++ b/app/assets/javascripts/discourse/components/category-chooser.js.es6 @@ -1,11 +1,11 @@ -import ComboboxView from 'discourse-common/components/combo-box'; +import Combobox from 'discourse-common/components/combo-box'; import { categoryBadgeHTML } from 'discourse/helpers/category-link'; import computed from 'ember-addons/ember-computed-decorators'; import { observes, on } from 'ember-addons/ember-computed-decorators'; import PermissionType from 'discourse/models/permission-type'; import Category from 'discourse/models/category'; -export default ComboboxView.extend({ +export default Combobox.extend({ classNames: ['combobox category-combobox'], dataAttributes: ['id', 'description_text'], overrideWidths: true, diff --git a/app/assets/javascripts/discourse/components/composer-title.js.es6 b/app/assets/javascripts/discourse/components/composer-title.js.es6 index e9f1790ba5..0c173bdd0c 100644 --- a/app/assets/javascripts/discourse/components/composer-title.js.es6 +++ b/app/assets/javascripts/discourse/components/composer-title.js.es6 @@ -110,9 +110,9 @@ export default Ember.Component.extend({ } }, - @computed('composer.title') - isAbsoluteUrl() { - return this.get('composer.titleLength') > 0 && /^(https?:)?\/\/[\w\.\-]+/i.test(this.get('composer.title')); + @computed('composer.title', 'composer.titleLength') + isAbsoluteUrl(title, titleLength) { + return titleLength > 0 && /^(https?:)?\/\/[\w\.\-]+/i.test(title); }, bodyIsDefault() { diff --git a/app/assets/javascripts/discourse/components/date-picker-future.js.es6 b/app/assets/javascripts/discourse/components/date-picker-future.js.es6 index fa6ed4037e..249c1a57d6 100644 --- a/app/assets/javascripts/discourse/components/date-picker-future.js.es6 +++ b/app/assets/javascripts/discourse/components/date-picker-future.js.es6 @@ -5,7 +5,8 @@ export default DatePicker.extend({ _opts() { return { - defaultDate: moment().add(1, "day").toDate(), + defaultDate: this.get('defaultDate') || moment().add(1, "day").toDate(), + setDefaultDate: !!this.get('defaultDate'), minDate: new Date(), }; } diff --git a/app/assets/javascripts/discourse/components/date-picker.js.es6 b/app/assets/javascripts/discourse/components/date-picker.js.es6 index de36036616..d2667dd863 100644 --- a/app/assets/javascripts/discourse/components/date-picker.js.es6 +++ b/app/assets/javascripts/discourse/components/date-picker.js.es6 @@ -1,6 +1,6 @@ /* global Pikaday:true */ import loadScript from "discourse/lib/load-script"; -import { on } from "ember-addons/ember-computed-decorators"; +import { default as computed, on } from "ember-addons/ember-computed-decorators"; export default Em.Component.extend({ classNames: ["date-picker-wrapper"], @@ -39,6 +39,11 @@ export default Em.Component.extend({ this._picker = null; }, + @computed() + placeholder() { + return I18n.t("dates.placeholder"); + }, + _opts() { return null; } diff --git a/app/assets/javascripts/discourse/components/topic-status-info.js.es6 b/app/assets/javascripts/discourse/components/topic-status-info.js.es6 index a7b5d784b0..8cb29a0b21 100644 --- a/app/assets/javascripts/discourse/components/topic-status-info.js.es6 +++ b/app/assets/javascripts/discourse/components/topic-status-info.js.es6 @@ -2,21 +2,21 @@ import { bufferedRender } from 'discourse-common/lib/buffered-render'; import Category from 'discourse/models/category'; export default Ember.Component.extend(bufferedRender({ - elementId: 'topic-status-info', + classNames: ['topic-status-info'], delayedRerender: null, rerenderTriggers: [ - 'topic.topic_status_update', - 'topic.topic_status_update.execute_at', - 'topic.topic_status_update.based_on_last_post', - 'topic.topic_status_update.duration', - 'topic.topic_status_update.category_id', + 'statusType', + 'executeAt', + 'basedOnLastPost', + 'duration', + 'categoryId', ], buildBuffer(buffer) { - if (!this.get('topic.topic_status_update.execute_at')) return; + if (!this.get('executeAt')) return; - let statusUpdateAt = moment(this.get('topic.topic_status_update.execute_at')); + let statusUpdateAt = moment(this.get('executeAt')); if (statusUpdateAt < new Date()) return; let duration = moment.duration(statusUpdateAt - moment()); @@ -33,7 +33,7 @@ export default Ember.Component.extend(bufferedRender({ rerenderDelay = 60000; } - let autoCloseHours = this.get("topic.topic_status_update.duration") || 0; + let autoCloseHours = this.get("duration") || 0; buffer.push('