From 9a56b398a1225a462c6f4f795fec813ca5ac693a Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Thu, 28 Mar 2019 16:34:56 +0100 Subject: [PATCH] UX: improves local-dates form (#7268) --- .../discourse-local-dates-create-form.js.es6 | 329 +++++++++++------- .../discourse-local-dates-create-form.hbs | 81 +++-- .../common/discourse-local-dates.scss | 112 +++--- .../config/locales/client.en.yml | 12 +- plugins/discourse-local-dates/plugin.rb | 43 +-- 5 files changed, 370 insertions(+), 207 deletions(-) diff --git a/plugins/discourse-local-dates/assets/javascripts/discourse/components/discourse-local-dates-create-form.js.es6 b/plugins/discourse-local-dates/assets/javascripts/discourse/components/discourse-local-dates-create-form.js.es6 index 028a70ce138..811994a2fff 100644 --- a/plugins/discourse-local-dates/assets/javascripts/discourse/components/discourse-local-dates-create-form.js.es6 +++ b/plugins/discourse-local-dates/assets/javascripts/discourse/components/discourse-local-dates-create-form.js.es6 @@ -1,11 +1,11 @@ -import computed from "ember-addons/ember-computed-decorators"; -import { observes } from "ember-addons/ember-computed-decorators"; +import { default as computed } from "ember-addons/ember-computed-decorators"; +import { cookAsync } from "discourse/lib/text"; +import debounce from "discourse/lib/debounce"; export default Ember.Component.extend({ timeFormat: "HH:mm:ss", dateFormat: "YYYY-MM-DD", dateTimeFormat: "YYYY-MM-DD HH:mm:ss", - config: null, date: null, toDate: null, time: null, @@ -15,23 +15,152 @@ export default Ember.Component.extend({ recurring: null, advancedMode: false, isValid: true, + timezone: null, + timezones: null, init() { - this._super(); + this._super(...arguments); - this.set("date", moment().format(this.dateFormat)); - this.set("timezones", []); - this.set( - "formats", - (this.siteSettings.discourse_local_dates_default_formats || "") + this.setProperties({ + timezones: [], + formats: (this.siteSettings.discourse_local_dates_default_formats || "") .split("|") - .filter(f => f) - ); + .filter(f => f), + timezone: moment.tz.guess(), + date: moment().format(this.dateFormat) + }); }, - @observes("date", "time", "toDate", "toTime") - _resetFormValidity() { - this.set("isValid", true); + didInsertElement() { + this._super(...arguments); + + this._renderPreview(); + }, + + _renderPreview: debounce(function() { + const markup = this.get("markup"); + + if (markup) { + cookAsync(markup).then(result => { + this.set("currentPreview", result); + + Ember.run.schedule("afterRender", () => + this.$(".preview .discourse-local-date").applyLocalDates() + ); + }); + } + }, 250).observes("markup"), + + @computed("date", "toDate", "toTime") + isRange(date, toDate, toTime) { + return date && (toDate || toTime); + }, + + @computed("computedConfig", "isRange") + isValid(config, isRange) { + const fromConfig = config.from; + if (!config.from.dateTime || !config.from.dateTime.isValid()) { + return false; + } + + if (isRange) { + const toConfig = config.to; + + if ( + !toConfig.dateTime || + !toConfig.dateTime.isValid() || + toConfig.dateTime.diff(fromConfig.dateTime) < 0 + ) { + return false; + } + } + + return true; + }, + + @computed("date", "time", "isRange", "options.{format,timezone}") + fromConfig(date, time, isRange, options = {}) { + const timeInferred = time ? false : true; + + let dateTime; + if (!timeInferred) { + dateTime = moment.tz(`${date} ${time}`, options.timezone); + } else { + dateTime = moment.tz(date, options.timezone); + } + + if (!timeInferred) { + time = dateTime.format(this.timeFormat); + } + + let format = options.format; + if (timeInferred && this.get("formats").includes(format)) { + format = "LL"; + } + + return Ember.Object.create({ + date: dateTime.format(this.dateFormat), + time, + dateTime, + format, + range: isRange ? "start" : false + }); + }, + + @computed("toDate", "toTime", "isRange", "options.{timezone,format}") + toConfig(date, time, isRange, options = {}) { + const timeInferred = time ? false : true; + + if (time && !date) { + date = moment().format(this.dateFormat); + } + + let dateTime; + if (!timeInferred) { + dateTime = moment.tz(`${date} ${time}`, options.timezone); + } else { + dateTime = moment.tz(date, options.timezone).endOf("day"); + } + + if (!timeInferred) { + time = dateTime.format(this.timeFormat); + } + + let format = options.format; + if (timeInferred && this.get("formats").includes(format)) { + format = "LL"; + } + + return Ember.Object.create({ + date: dateTime.format(this.dateFormat), + time, + dateTime, + format, + range: isRange ? "end" : false + }); + }, + + @computed("recurring", "timezones", "timezone", "format") + options(recurring, timezones, timezone, format) { + return Ember.Object.create({ + recurring, + timezones, + timezone, + format + }); + }, + + @computed( + "fromConfig.{date}", + "toConfig.{date}", + "options.{recurring,timezones,timezone,format}" + ) + computedConfig(fromConfig, toConfig, options) { + return Ember.Object.create({ + from: fromConfig, + to: toConfig, + options + }); }, @computed @@ -51,15 +180,41 @@ export default Ember.Component.extend({ @computed recurringOptions() { + const key = "discourse_local_dates.create.form.recurring"; + return [ - { name: "Every day", id: "1.days" }, - { name: "Every week", id: "1.weeks" }, - { name: "Every two weeks", id: "2.weeks" }, - { name: "Every month", id: "1.months" }, - { name: "Every two months", id: "2.months" }, - { name: "Every three months", id: "3.months" }, - { name: "Every six months", id: "6.months" }, - { name: "Every year", id: "1.years" } + { + name: I18n.t(`${key}.every_day`), + id: "1.days" + }, + { + name: I18n.t(`${key}.every_week`), + id: "1.weeks" + }, + { + name: I18n.t(`${key}.every_two_weeks`), + id: "2.weeks" + }, + { + name: I18n.t(`${key}.every_month`), + id: "1.months" + }, + { + name: I18n.t(`${key}.every_two_months`), + id: "2.months" + }, + { + name: I18n.t(`${key}.every_three_months`), + id: "3.months" + }, + { + name: I18n.t(`${key}.every_six_months`), + id: "6.months" + }, + { + name: I18n.t(`${key}.every_year`), + id: "1.years" + } ]; }, @@ -74,77 +229,27 @@ export default Ember.Component.extend({ return moment.tz.names(); }, - getConfig(range) { - const endOfRange = range && range === "end"; - const time = endOfRange ? this.get("toTime") : this.get("time"); - let date = endOfRange ? this.get("toDate") : this.get("date"); - - if (endOfRange && time && !date) { - date = moment().format(this.dateFormat); - } - - const recurring = this.get("recurring"); - const format = this.get("format"); - const timezones = this.get("timezones"); - const timeInferred = time ? false : true; - const timezone = this.get("currentUserTimezone"); - - let dateTime; - if (!timeInferred) { - dateTime = moment.tz(`${date} ${time}`, timezone); - } else { - if (endOfRange) { - dateTime = moment.tz(date, timezone).endOf("day"); - } else { - dateTime = moment.tz(date, timezone); - } - } - - let config = { - date: dateTime.format(this.dateFormat), - dateTime, - recurring, - format, - timezones, - timezone - }; - - if (!timeInferred) { - config.time = dateTime.format(this.timeFormat); - } - - if (timeInferred) { - config.displayedTimezone = this.get("currentUserTimezone"); - } - - if (timeInferred && this.get("formats").includes(format)) { - config.format = "LL"; - } - - return config; - }, - - _generateDateMarkup(config) { + _generateDateMarkup(config, options, isRange) { let text = `[date=${config.date}`; if (config.time) { - text += ` time=${config.time} `; + text += ` time=${config.time}`; } if (config.format && config.format.length) { - text += ` format="${config.format}" `; + text += ` format="${config.format}"`; } - if (config.timezone) { - text += ` timezone="${config.timezone}"`; + if (options.timezone) { + text += ` timezone="${options.timezone}"`; } - if (config.timezones && config.timezones.length) { - text += ` timezones="${config.timezones.join("|")}"`; + if (options.timezones && options.timezones.length) { + text += ` timezones="${options.timezones.join("|")}"`; } - if (config.recurring) { - text += ` recurring="${config.recurring}"`; + if (options.recurring && !isRange) { + text += ` recurring="${options.recurring}"`; } text += `]`; @@ -152,31 +257,6 @@ export default Ember.Component.extend({ return text; }, - valid(isRange) { - const fromConfig = this.getConfig(isRange ? "start" : null); - - if (!fromConfig.dateTime || !fromConfig.dateTime.isValid()) { - this.set("isValid", false); - return false; - } - - if (isRange) { - const toConfig = this.getConfig("end"); - - if ( - !toConfig.dateTime || - !toConfig.dateTime.isValid() || - toConfig.dateTime.diff(fromConfig.dateTime) < 0 - ) { - this.set("isValid", false); - return false; - } - } - - this.set("isValid", true); - return true; - }, - @computed("advancedMode") toggleModeBtnLabel(advancedMode) { return advancedMode @@ -184,35 +264,36 @@ export default Ember.Component.extend({ : "discourse_local_dates.create.form.advanced_mode"; }, + @computed("computedConfig.{from,to,options}", "options", "isValid", "isRange") + markup(config, options, isValid, isRange) { + let text; + + if (isValid && config.from) { + text = this._generateDateMarkup(config.from, options, isRange); + + if (config.to && config.to.range) { + text += ` → `; + text += this._generateDateMarkup(config.to, options, isRange); + } + } + + return text; + }, + actions: { advancedMode() { this.toggleProperty("advancedMode"); }, save() { - const isRange = - this.get("date") && (this.get("toDate") || this.get("toTime")); + const markup = this.get("markup"); - if (this.valid(isRange)) { + if (markup) { this._closeModal(); - - let text = this._generateDateMarkup( - this.getConfig(isRange ? "start" : null) - ); - - if (isRange) { - text += ` → `; - text += this._generateDateMarkup(this.getConfig("end")); - } - - this.get("toolbarEvent").addText(text); + this.get("toolbarEvent").addText(markup); } }, - fillFormat(format) { - this.set("format", format); - }, - cancel() { this._closeModal(); } diff --git a/plugins/discourse-local-dates/assets/javascripts/discourse/templates/components/discourse-local-dates-create-form.hbs b/plugins/discourse-local-dates/assets/javascripts/discourse/templates/components/discourse-local-dates-create-form.hbs index e7696f16241..638bdd9c25f 100644 --- a/plugins/discourse-local-dates/assets/javascripts/discourse/templates/components/discourse-local-dates-create-form.hbs +++ b/plugins/discourse-local-dates/assets/javascripts/discourse/templates/components/discourse-local-dates-create-form.hbs @@ -5,6 +5,19 @@ style="overflow: auto"}}
+ {{#unless isValid}} +
+ {{i18n "discourse_local_dates.create.form.invalid_date"}} +
+ {{else}} +
+ {{i18n "discourse_local_dates.create.form.preview_for" timezone=currentUserTimezone}} + {{currentPreview}} +
+ {{/unless}} + + {{computeDate}} +
@@ -13,7 +26,11 @@ {{i18n "discourse_local_dates.create.form.date_title"}}
- {{date-picker class="date-input" value=date defaultDate="DD-MM-YYYY"}} + {{date-picker + onSelect=(action (mut date)) + class="date-input" + value=date + defaultDate="DD-MM-YYYY"}}
@@ -22,12 +39,14 @@ {{i18n "discourse_local_dates.create.form.time_title"}}
- {{input type="time" value=time class="time-input"}} + {{input input=(mut time) type="time" value=time class="time-input"}}
- {{i18n "discourse_local_dates.create.form.to"}} +
+ {{if site.mobileView "↓" "→"}} +
@@ -35,7 +54,11 @@ {{i18n "discourse_local_dates.create.form.date_title"}}
- {{date-picker class="date-input" value=toDate defaultDate="DD-MM-YYYY"}} + {{date-picker + onSelect=(action (mut toDate)) + class="date-input" + value=toDate + defaultDate="DD-MM-YYYY"}}
@@ -44,33 +67,49 @@ {{i18n "discourse_local_dates.create.form.time_title"}}
- {{input type="time" value=toTime class="time-input"}} + {{input input=(mut toTime) type="time" value=toTime class="time-input"}}
- {{currentUserTimezone}} - +
+
+ +
+ {{combo-box + class="timezone-input" + allowAny=false + content=allTimezones + value=timezone + onSelect=(action (mut timezone))}} +
+
- {{#unless isValid}} - {{i18n "discourse_local_dates.create.form.invalid_date"}} - {{/unless}} - -
- - {{#if advancedMode}} -

{{{i18n "discourse_local_dates.create.form.recurring_description"}}}

- {{/if}} -
- {{combo-box content=recurringOptions class="recurrence-input" value=recurring none="discourse_local_dates.create.form.recurring_none"}}
{{#if advancedMode}}
+ {{#unless isRange}} +
+ +

{{{i18n "discourse_local_dates.create.form.recurring_description"}}}

+
+ {{combo-box + content=recurringOptions + class="recurrence-input" + value=recurring + onSelect=(action (mut recurring)) + none="discourse_local_dates.create.form.recurring_none"}} +
+
+ {{/unless}} +

@@ -87,7 +126,7 @@

    {{#each previewedFormats as |previewedFormat|}}
  • - + {{previewedFormat.format}} diff --git a/plugins/discourse-local-dates/assets/stylesheets/common/discourse-local-dates.scss b/plugins/discourse-local-dates/assets/stylesheets/common/discourse-local-dates.scss index 708f8d7438a..8ba97792b84 100644 --- a/plugins/discourse-local-dates/assets/stylesheets/common/discourse-local-dates.scss +++ b/plugins/discourse-local-dates/assets/stylesheets/common/discourse-local-dates.scss @@ -57,7 +57,7 @@ } .discourse-local-dates-create-modal { - min-height: 300px; + min-height: 200px; display: flex; flex-direction: row; @@ -70,72 +70,69 @@ .date-time-configuration { display: flex; - align-items: center; - flex-direction: row; - .range { + .range, + .timezone { + display: flex; + justify-content: flex-start; + flex: 1; + align-items: center; + .from { - flex-direction: row; display: flex; + flex-direction: row; + justify-content: space-between; } .to { - flex-direction: row; display: flex; + flex-direction: row; + justify-content: space-between; } .to-indicator { display: flex; - justify-content: center; - margin: 0.5em 0; + flex-direction: row; + padding-top: 0.5em; + margin: 0 1em; + font-size: $font-up-2; } } .date { + margin-right: 0.5em; .date-input { - margin-right: 1em; - + width: 100px; + text-align: center; .date-picker { - padding-top: 5px; - bottom: 5px; + padding: 0; margin: 0; - width: 120px; - text-align: left; + width: 100%; } } } + .timezone { + margin-left: 1em; + .timezone-input { + width: 180px; + } + } + .time { .time-input { - margin: 0 0.5em 0 0; - width: 120px; - padding: 3.5px 10px; + width: 80px; + margin: 0; + padding: 0; + text-align: center; } } + } - .preview { - flex: 1 0 0px; - margin-top: 16px; - text-align: center; - } - - @include breakpoint(medium) { - flex-direction: column; - align-items: flex-start; - - .range .from, - .range .to { - flex-direction: column; - } - - .date .date-input .date-picker { - width: 200px; - } - - .time .time-input { - width: 200px; - } - } + .preview { + text-align: center; + margin-top: 0; + margin-bottom: 1em; } .validation-error { @@ -176,3 +173,38 @@ width: 99%; } } + +@media (max-width: 700px) { + .discourse-local-dates-create-modal { + .form { + .date-time-configuration { + flex-direction: column; + .range, + .timezone { + display: flex; + flex: 1; + flex-direction: column; + + .controls, + .control-group { + width: 100%; + } + + .to-indicator { + margin: 0.5em 1em; + } + } + + .timezone { + margin: 0.5em 0 0 0; + padding: 0.5em 0 0 0; + border-top: 1px solid $primary-low; + + .timezone-input { + width: 100%; + } + } + } + } + } +} diff --git a/plugins/discourse-local-dates/config/locales/client.en.yml b/plugins/discourse-local-dates/config/locales/client.en.yml index 3939e684b1a..9d7dc32594a 100644 --- a/plugins/discourse-local-dates/config/locales/client.en.yml +++ b/plugins/discourse-local-dates/config/locales/client.en.yml @@ -10,7 +10,6 @@ en: modal_title: Insert date modal_subtitle: "We will automatically convert the date and time to the viewer’s local time zone." form: - to: "to" insert: Insert advanced_mode: Advanced mode simple_mode: Simple mode @@ -24,3 +23,14 @@ en: date_title: Date time_title: Time format_title: Date format + timezone: Timezone + preview_for: Preview for %{timezone} + recurring: + every_day: "Every day" + every_week: "Every week" + every_two_weeks: "Every two weeks" + every_month: "Every month" + every_two_months: "Every two months" + every_three_months: "Every three months" + every_six_months: "Every six months" + every_year: "Every year" diff --git a/plugins/discourse-local-dates/plugin.rb b/plugins/discourse-local-dates/plugin.rb index 02486ec4e68..c30c5e27cad 100644 --- a/plugins/discourse-local-dates/plugin.rb +++ b/plugins/discourse-local-dates/plugin.rb @@ -4,38 +4,39 @@ # author: Joffrey Jaffeux hide_plugin if self.respond_to?(:hide_plugin) -register_asset "javascripts/discourse-local-dates.js.no-module.es6" -register_asset "stylesheets/common/discourse-local-dates.scss" -register_asset "moment.js", :vendored_core_pretty_text -register_asset "moment-timezone.js", :vendored_core_pretty_text +register_asset 'javascripts/discourse-local-dates.js.no-module.es6' +register_asset 'stylesheets/common/discourse-local-dates.scss' +register_asset 'moment.js', :vendored_core_pretty_text +register_asset 'moment-timezone.js', :vendored_core_pretty_text enabled_site_setting :discourse_local_dates_enabled after_initialize do module ::DiscourseLocalDates - PLUGIN_NAME ||= "discourse-local-dates".freeze - POST_CUSTOM_FIELD ||= "local_dates".freeze + PLUGIN_NAME ||= 'discourse-local-dates'.freeze + POST_CUSTOM_FIELD ||= 'local_dates'.freeze end - [ - "../lib/discourse_local_dates/engine.rb", - ].each { |path| load File.expand_path(path, __FILE__) } + %w[../lib/discourse_local_dates/engine.rb].each do |path| + load File.expand_path(path, __FILE__) + end register_post_custom_field_type(DiscourseLocalDates::POST_CUSTOM_FIELD, :json) on(:before_post_process_cooked) do |doc, post| - dates = doc.css('span.discourse-local-date').map do |cooked_date| - date = {} - cooked_date.attributes.values.each do |attribute| - data_name = attribute.name&.gsub('data-', '') - if data_name && ['date', 'time', 'timezone', 'recurring'].include?(data_name) - unless attribute.value == 'undefined' - date[data_name] = CGI.escapeHTML(attribute.value || "") + dates = + doc.css('span.discourse-local-date').map do |cooked_date| + date = {} + cooked_date.attributes.values.each do |attribute| + data_name = attribute.name&.gsub('data-', '') + if data_name && %w[date time timezone recurring].include?(data_name) + unless attribute.value == 'undefined' + date[data_name] = CGI.escapeHTML(attribute.value || '') + end end end + date end - date - end if dates.present? post.custom_fields[DiscourseLocalDates::POST_CUSTOM_FIELD] = dates @@ -51,9 +52,9 @@ after_initialize do end on(:reduce_cooked) do |fragment| - fragment.css(".discourse-local-date").each do |container| - if container.attributes["data-email-preview"] - preview = container.attributes["data-email-preview"].value + fragment.css('.discourse-local-date').each do |container| + if container.attributes['data-email-preview'] + preview = container.attributes['data-email-preview'].value container.content = preview end end